Spaces:
Running
Running
File size: 4,839 Bytes
0ac8545 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 | // ♉ 水彩天幕:启动时把每种色调预绘成离屏纹理,播放时漂移 + 交叉溶解(全新编写)
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) };
}
|