// ♉ 水彩天幕:启动时把每种色调预绘成离屏纹理,播放时漂移 + 交叉溶解(全新编写) import { h1, col, clamp, lerp, TAU, halo } from './canvas-kit.js'; export const TONES = { meadow: { base: ['#0b2a22', '#3a1838'], blobs: [[40, 140, 100], [220, 110, 150], [255, 190, 150], [20, 90, 90], [150, 60, 120]], star: .55, warm: [255, 200, 170] }, dusk: { base: ['#24160e', '#3a2416'], blobs: [[150, 96, 52], [96, 60, 40], [220, 170, 110], [70, 50, 60]], star: .5, warm: [255, 200, 140] }, night: { base: ['#060b1c', '#0d1430'], blobs: [[30, 50, 110], [60, 40, 100], [20, 80, 110], [90, 70, 140]], star: 1.2, warm: [170, 190, 255] }, ink: { base: ['#07100e', '#121a1a'], blobs: [[30, 60, 56], [60, 70, 70], [20, 40, 40], [110, 90, 60]], star: .9, warm: [220, 210, 180] }, sea: { base: ['#06202a', '#0c2f3c'], blobs: [[30, 110, 130], [60, 150, 160], [200, 170, 140], [20, 60, 90]], star: .6, warm: [200, 240, 255] }, rose: { base: ['#2a0f1c', '#3a1630'], blobs: [[190, 90, 120], [120, 50, 90], [240, 180, 170], [80, 40, 80]], star: .7, warm: [255, 190, 200] }, earth: { base: ['#1a1a0e', '#2a1c12'], blobs: [[110, 120, 60], [170, 110, 70], [70, 90, 50], [220, 180, 120]], star: .5, warm: [255, 220, 160] }, gilt: { base: ['#05140f', '#140a14'], blobs: [[30, 100, 76], [150, 110, 50], [60, 40, 70], [220, 180, 110]], star: .9, warm: [255, 220, 150] }, sunset: { base: ['#2a1420', '#4a2418'], blobs: [[255, 160, 110], [230, 110, 120], [255, 210, 150], [120, 60, 100]], star: .45, warm: [255, 200, 150] }, }; const TEX = 720; function paintTexture(tone, seed) { const T = TONES[tone], cv = document.createElement('canvas'); cv.width = cv.height = TEX; const g = cv.getContext('2d'); const gr = g.createLinearGradient(0, 0, TEX * .3, TEX); gr.addColorStop(0, T.base[0]); gr.addColorStop(1, T.base[1]); g.fillStyle = gr; g.fillRect(0, 0, TEX, TEX); // 水彩色团:多层低透明度柔边圆,叠出晕染边缘 for (let i = 0; i < 260; i++) { const c = T.blobs[i % T.blobs.length]; const cx = TEX * (h1(i * 3.1 + seed) * 1.2 - .1), cy = TEX * (h1(i * 7.3 + seed) * 1.2 - .1), r = TEX * (.05 + h1(i * 1.7 + seed) * .22); const g2 = g.createRadialGradient(cx, cy, r * .2, cx, cy, r); const a = .07 + h1(i + seed * 2) * .09; g2.addColorStop(0, col(c, a)); g2.addColorStop(.7, col(c, a * .6)); g2.addColorStop(1, col(c, 0)); g.fillStyle = g2; g.beginPath(); g.ellipse(cx, cy, r, r * (.6 + h1(i + 11) * .6), h1(i + 5) * 3, 0, TAU); g.fill(); } // 纸纹颗粒 const img = g.getImageData(0, 0, TEX, TEX), d = img.data; for (let i = 0; i < d.length; i += 4) { const n = (h1(i * .25 + seed) - .5) * 10; d[i] += n; d[i + 1] += n; d[i + 2] += n; } g.putImageData(img, 0, 0); return cv; } const STARS = Array.from({ length: 420 }, (_, i) => [h1(i * 1.3), h1(i * 2.9 + 4), Math.pow(h1(i * 5.1 + 2), 3), h1(i + 99) * TAU]); const BOKEH = Array.from({ length: 26 }, (_, i) => [h1(i * 4.1 + 1), h1(i * 6.7 + 2), .4 + h1(i * 2.3) * .6, h1(i + 33) * TAU]); export function createBackdrop() { const cache = {}; const tex = tone => cache[tone] || (cache[tone] = paintTexture(tone, Object.keys(TONES).indexOf(tone) * 13.7 + 2)); function layer(g, tone, t, W, H, a) { if (a <= 0) return; const cv = tex(tone), s = Math.max(W, H) * 1.25 / TEX; const dx = Math.sin(t * .021) * W * .06 - W * .06, dy = Math.cos(t * .017) * H * .05 - H * .06; g.save(); g.globalAlpha = a; g.drawImage(cv, (W - TEX * s) / 2 + dx, (H - TEX * s) / 2 + dy, TEX * s, TEX * s); g.restore(); } // from → to 溶解,k = 0..1 function draw(g, t, W, H, from, to, k) { g.fillStyle = '#05070a'; g.fillRect(0, 0, W, H); layer(g, from, t, W, H, 1); if (to !== from) layer(g, to, t, W, H, k); const S = Math.min(W, H) / 1080; const st = lerp(TONES[from].star, TONES[to].star, k); // 星空 g.save(); g.globalCompositeOperation = 'lighter'; for (const [x, y, m, ph] of STARS) { const tw = .55 + .45 * Math.sin(t * (1 + m * 2) + ph), r = (.6 + m * 2.2) * S; g.fillStyle = col([255, 248, 235], clamp(st * tw * (.25 + m * .75))); g.beginPath(); g.arc(x * W, y * H, r, 0, TAU); g.fill(); } g.restore(); // 散景光斑 const warm = TONES[to].warm; for (const [x, y, sz, ph] of BOKEH) { const px = (x * W + Math.sin(t * .05 + ph) * 60 * S), py = (y * H + Math.cos(t * .04 + ph) * 40 * S); halo(g, px, py, (40 + sz * 70) * S, warm, .16 * sz * (.6 + .4 * Math.sin(t * .5 + ph))); } // 暗角 const v = g.createRadialGradient(W / 2, H * .5, Math.min(W, H) * .35, W / 2, H * .5, Math.max(W, H) * .78); v.addColorStop(0, 'rgba(0,0,0,0)'); v.addColorStop(1, 'rgba(0,0,0,.6)'); g.fillStyle = v; g.fillRect(0, 0, W, H); } return { draw, warm: () => Object.keys(TONES).forEach(tex) }; }