feat: Comet visualizer adopts album art colors and fades on silence
- Extract dominant hues from album artwork via canvas sampling - Replace all hardcoded hue values with palette-derived colors - Smooth palette transitions when track changes (lerp on hue circle) - Add time-based mutation for continuous color evolution - Wisps, rings, and clouds fade to near-invisible when silent - Black hole shrinks significantly when no audio energy
This commit is contained in:
1 file changed
+142
-28
@@ -80,6 +80,99 @@
|
|||||||
let previousHigh = 0;
|
let previousHigh = 0;
|
||||||
let beatCooldown = 0;
|
let beatCooldown = 0;
|
||||||
|
|
||||||
|
// Album art color extraction
|
||||||
|
const DEFAULT_PALETTE = [356, 196, 280, 30]; // warm red, cyan, purple, orange
|
||||||
|
let artPalette: number[] = [...DEFAULT_PALETTE];
|
||||||
|
let paletteTarget: number[] = [...DEFAULT_PALETTE];
|
||||||
|
let currentArtwork = '';
|
||||||
|
|
||||||
|
function rgbToHsl(r: number, g: number, b: number): [number, number, number] {
|
||||||
|
r /= 255; g /= 255; b /= 255;
|
||||||
|
const max = Math.max(r, g, b), min = Math.min(r, g, b);
|
||||||
|
const l = (max + min) / 2;
|
||||||
|
if (max === min) return [0, 0, l];
|
||||||
|
const d = max - min;
|
||||||
|
const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
||||||
|
let h = 0;
|
||||||
|
if (max === r) h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
|
||||||
|
else if (max === g) h = ((b - r) / d + 2) / 6;
|
||||||
|
else h = ((r - g) / d + 4) / 6;
|
||||||
|
return [h * 360, s, l];
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractPalette(img: HTMLImageElement) {
|
||||||
|
const size = 32;
|
||||||
|
const sampleCanvas = document.createElement('canvas');
|
||||||
|
sampleCanvas.width = size;
|
||||||
|
sampleCanvas.height = size;
|
||||||
|
const sCtx = sampleCanvas.getContext('2d');
|
||||||
|
if (!sCtx) return;
|
||||||
|
|
||||||
|
sCtx.drawImage(img, 0, 0, size, size);
|
||||||
|
const data = sCtx.getImageData(0, 0, size, size).data;
|
||||||
|
|
||||||
|
// Bucket hues into 12 sectors (30° each), weighted by saturation and away from gray
|
||||||
|
const buckets = new Float32Array(12);
|
||||||
|
const bucketHueSum = new Float32Array(12);
|
||||||
|
const bucketCount = new Float32Array(12);
|
||||||
|
|
||||||
|
for (let i = 0; i < data.length; i += 4) {
|
||||||
|
const [h, s, l] = rgbToHsl(data[i], data[i + 1], data[i + 2]);
|
||||||
|
if (s < 0.12 || l < 0.08 || l > 0.92) continue; // skip grays
|
||||||
|
const bucket = Math.floor(h / 30) % 12;
|
||||||
|
const weight = s * (0.5 + Math.abs(l - 0.5)); // prefer saturated, non-mid colors
|
||||||
|
buckets[bucket] += weight;
|
||||||
|
bucketHueSum[bucket] += h * weight;
|
||||||
|
bucketCount[bucket] += weight;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pick top 4 buckets
|
||||||
|
const indexed = Array.from(buckets).map((w, i) => ({ w, i }));
|
||||||
|
indexed.sort((a, b) => b.w - a.w);
|
||||||
|
|
||||||
|
const extracted: number[] = [];
|
||||||
|
for (let j = 0; j < Math.min(4, indexed.length); j++) {
|
||||||
|
const idx = indexed[j].i;
|
||||||
|
if (bucketCount[idx] > 0) {
|
||||||
|
extracted.push(Math.round(bucketHueSum[idx] / bucketCount[idx]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill remaining slots with defaults if needed
|
||||||
|
while (extracted.length < 4) {
|
||||||
|
extracted.push(DEFAULT_PALETTE[extracted.length]);
|
||||||
|
}
|
||||||
|
|
||||||
|
paletteTarget = extracted;
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadArtworkPalette(src: string) {
|
||||||
|
if (!src || src === currentArtwork) return;
|
||||||
|
currentArtwork = src;
|
||||||
|
const img = new window.Image();
|
||||||
|
img.crossOrigin = 'anonymous';
|
||||||
|
img.onload = () => extractPalette(img);
|
||||||
|
img.onerror = () => { paletteTarget = [...DEFAULT_PALETTE]; };
|
||||||
|
img.src = src;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Smoothly interpolate palette hues toward target each frame
|
||||||
|
function lerpPalette() {
|
||||||
|
for (let i = 0; i < 4; i++) {
|
||||||
|
let diff = paletteTarget[i] - artPalette[i];
|
||||||
|
// Shortest path around hue circle
|
||||||
|
if (diff > 180) diff -= 360;
|
||||||
|
if (diff < -180) diff += 360;
|
||||||
|
artPalette[i] = wrapHue(artPalette[i] + diff * 0.02);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get palette hue with time-based mutation
|
||||||
|
function palHue(index: number, timeMs: number, drift: number = 18): number {
|
||||||
|
const base = artPalette[index % 4];
|
||||||
|
return wrapHue(base + Math.sin(timeMs * 0.0008 + index * 2.1) * drift);
|
||||||
|
}
|
||||||
|
|
||||||
function clamp01(value: number): number {
|
function clamp01(value: number): number {
|
||||||
return Math.max(0, Math.min(1, value));
|
return Math.max(0, Math.min(1, value));
|
||||||
}
|
}
|
||||||
@@ -105,15 +198,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createWisp(intensity: number = 0.5): TunnelWisp {
|
function createWisp(intensity: number = 0.5): TunnelWisp {
|
||||||
const colorFamilies = [2, 8, 14, 20, 190, 206];
|
// Pick from album-derived palette with random spread
|
||||||
const family = colorFamilies[Math.floor(Math.random() * colorFamilies.length)];
|
const palIdx = Math.floor(Math.random() * 4);
|
||||||
|
const baseHue = artPalette[palIdx];
|
||||||
return {
|
return {
|
||||||
depth: Math.random() * 0.22,
|
depth: Math.random() * 0.22,
|
||||||
angle: Math.random() * Math.PI * 2,
|
angle: Math.random() * Math.PI * 2,
|
||||||
speed: 0.002 + Math.random() * 0.004 + intensity * 0.008,
|
speed: 0.002 + Math.random() * 0.004 + intensity * 0.008,
|
||||||
width: 2.8 + Math.random() * 4.5 + intensity * 5.0,
|
width: 2.8 + Math.random() * 4.5 + intensity * 5.0,
|
||||||
alpha: 0.25 + Math.random() * 0.3 + intensity * 0.35,
|
alpha: 0.25 + Math.random() * 0.3 + intensity * 0.35,
|
||||||
hue: wrapHue(family + (Math.random() - 0.5) * 16),
|
hue: wrapHue(baseHue + (Math.random() - 0.5) * 30),
|
||||||
spin: (Math.random() - 0.5) * 0.012,
|
spin: (Math.random() - 0.5) * 0.012,
|
||||||
curve: 0.05 + Math.random() * 0.14 + intensity * 0.09,
|
curve: 0.05 + Math.random() * 0.14 + intensity * 0.09,
|
||||||
phase: Math.random() * Math.PI * 2,
|
phase: Math.random() * Math.PI * 2,
|
||||||
@@ -185,14 +279,15 @@
|
|||||||
|
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.globalCompositeOperation = 'screen';
|
ctx.globalCompositeOperation = 'screen';
|
||||||
ctx.filter = `hue-rotate(22deg) saturate(${160 + bass * 130}%)`;
|
const chromaHueShift = Math.round(artPalette[0] - artPalette[1]) % 60;
|
||||||
|
ctx.filter = `hue-rotate(${chromaHueShift}deg) saturate(${160 + bass * 130}%)`;
|
||||||
ctx.globalAlpha = 0.14 + bass * 0.14;
|
ctx.globalAlpha = 0.14 + bass * 0.14;
|
||||||
ctx.drawImage(canvasRef, chromaOffset, -chromaOffset * 0.7, drawWidth, drawHeight);
|
ctx.drawImage(canvasRef, chromaOffset, -chromaOffset * 0.7, drawWidth, drawHeight);
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
|
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.globalCompositeOperation = 'screen';
|
ctx.globalCompositeOperation = 'screen';
|
||||||
ctx.filter = `hue-rotate(-26deg) saturate(${150 + high * 150}%)`;
|
ctx.filter = `hue-rotate(${-chromaHueShift}deg) saturate(${150 + high * 150}%)`;
|
||||||
ctx.globalAlpha = 0.12 + high * 0.16;
|
ctx.globalAlpha = 0.12 + high * 0.16;
|
||||||
ctx.drawImage(canvasRef, -chromaOffset * 0.8, chromaOffset * 0.7, drawWidth, drawHeight);
|
ctx.drawImage(canvasRef, -chromaOffset * 0.8, chromaOffset * 0.7, drawWidth, drawHeight);
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
@@ -222,8 +317,8 @@
|
|||||||
centerY,
|
centerY,
|
||||||
minDim * 1.08
|
minDim * 1.08
|
||||||
);
|
);
|
||||||
redFlood.addColorStop(0, `hsla(${wrapHue(4 + timeMs * 0.022)}, 96%, 44%, ${0.18 + bass * 0.16})`);
|
redFlood.addColorStop(0, `hsla(${palHue(0, timeMs, 22)}, 96%, 44%, ${0.18 + bass * 0.16})`);
|
||||||
redFlood.addColorStop(0.45, `hsla(${wrapHue(356 + timeMs * 0.017)}, 96%, 36%, ${0.16 + mid * 0.12})`);
|
redFlood.addColorStop(0.45, `hsla(${palHue(1, timeMs, 18)}, 96%, 36%, ${0.16 + mid * 0.12})`);
|
||||||
redFlood.addColorStop(1, 'rgba(24, 0, 8, 0)');
|
redFlood.addColorStop(1, 'rgba(24, 0, 8, 0)');
|
||||||
ctx.globalCompositeOperation = 'screen';
|
ctx.globalCompositeOperation = 'screen';
|
||||||
ctx.fillStyle = redFlood;
|
ctx.fillStyle = redFlood;
|
||||||
@@ -244,12 +339,14 @@
|
|||||||
|
|
||||||
const radiusX = minDim * (0.17 + seed.scale * 0.26 + bass * 0.1 + travel * 0.12);
|
const radiusX = minDim * (0.17 + seed.scale * 0.26 + bass * 0.1 + travel * 0.12);
|
||||||
const radiusY = minDim * (0.13 + seed.scale * 0.22 + high * 0.08 + travel * 0.1);
|
const radiusY = minDim * (0.13 + seed.scale * 0.22 + high * 0.08 + travel * 0.1);
|
||||||
const warmHue = wrapHue(356 + Math.sin(timeMs * 0.0013 + seed.offset * 7) * 18 + bass * 28);
|
const warmHue = wrapHue(palHue(0, timeMs, 24) + Math.sin(timeMs * 0.0013 + seed.offset * 7) * 18 + bass * 28);
|
||||||
const coolHue = wrapHue(196 + Math.cos(timeMs * 0.0018 + seed.offset * 10) * 20 + high * 16);
|
const coolHue = wrapHue(palHue(1, timeMs, 20) + Math.cos(timeMs * 0.0018 + seed.offset * 10) * 20 + high * 16);
|
||||||
const hue = seedIdx % 5 === 0 ? coolHue : warmHue;
|
const mutantHue = wrapHue(palHue(2, timeMs, 16) + Math.sin(timeMs * 0.001 + seed.offset * 5) * 22);
|
||||||
|
const hue = seedIdx % 5 === 0 ? coolHue : seedIdx % 7 === 0 ? mutantHue : warmHue;
|
||||||
const saturation = 70 + high * 20;
|
const saturation = 70 + high * 20;
|
||||||
const lightness = 38 + travel * 18 + mid * 8;
|
const lightness = 38 + travel * 18 + mid * 8;
|
||||||
const alpha = 0.08 + (1 - travel) * (0.16 + bass * 0.18);
|
const cloudEnergy = clamp01((bass + mid + high) * 2.5);
|
||||||
|
const alpha = (0.08 + (1 - travel) * (0.16 + bass * 0.18)) * (0.1 + cloudEnergy * 0.9);
|
||||||
|
|
||||||
const cloudGradient = ctx.createRadialGradient(cx, cy, 0, cx, cy, radiusX * 1.25);
|
const cloudGradient = ctx.createRadialGradient(cx, cy, 0, cx, cy, radiusX * 1.25);
|
||||||
cloudGradient.addColorStop(0, `hsla(${hue}, ${saturation}%, ${lightness + 8}%, ${alpha})`);
|
cloudGradient.addColorStop(0, `hsla(${hue}, ${saturation}%, ${lightness + 8}%, ${alpha})`);
|
||||||
@@ -275,11 +372,14 @@
|
|||||||
timeMs: number,
|
timeMs: number,
|
||||||
bass: number,
|
bass: number,
|
||||||
mid: number,
|
mid: number,
|
||||||
high: number
|
high: number,
|
||||||
|
totalEnergy: number
|
||||||
) {
|
) {
|
||||||
if (!ctx) return;
|
if (!ctx) return;
|
||||||
primeWisps();
|
primeWisps();
|
||||||
|
|
||||||
|
// Fade wisps nearly out when silent
|
||||||
|
const energyGate = clamp01(totalEnergy * 3.0);
|
||||||
ctx.globalCompositeOperation = 'lighter';
|
ctx.globalCompositeOperation = 'lighter';
|
||||||
ctx.lineCap = 'round';
|
ctx.lineCap = 'round';
|
||||||
ctx.lineJoin = 'round';
|
ctx.lineJoin = 'round';
|
||||||
@@ -320,7 +420,7 @@
|
|||||||
const hue = wrapHue(wisp.hue + timeMs * 0.028 + Math.sin(timeMs * 0.0017 + wisp.phase) * 26);
|
const hue = wrapHue(wisp.hue + timeMs * 0.028 + Math.sin(timeMs * 0.0017 + wisp.phase) * 26);
|
||||||
const saturation = 74 + high * 18;
|
const saturation = 74 + high * 18;
|
||||||
const lightness = 52 + bass * 20 + (1 - depthCurve) * 8;
|
const lightness = 52 + bass * 20 + (1 - depthCurve) * 8;
|
||||||
const alpha = clamp01(wisp.alpha * (0.45 + bass * 0.6 + high * 0.15));
|
const alpha = clamp01(wisp.alpha * (0.45 + bass * 0.6 + high * 0.15) * (0.05 + energyGate * 0.95));
|
||||||
|
|
||||||
const strokeGradient = ctx.createLinearGradient(sx, sy, ex, ey);
|
const strokeGradient = ctx.createLinearGradient(sx, sy, ex, ey);
|
||||||
strokeGradient.addColorStop(0, `hsla(${wrapHue(hue - 20)}, ${Math.max(30, saturation - 18)}%, ${lightness - 8}%, 0)`);
|
strokeGradient.addColorStop(0, `hsla(${wrapHue(hue - 20)}, ${Math.max(30, saturation - 18)}%, ${lightness - 8}%, 0)`);
|
||||||
@@ -355,6 +455,7 @@
|
|||||||
) {
|
) {
|
||||||
if (!ctx) return;
|
if (!ctx) return;
|
||||||
|
|
||||||
|
const ringEnergy = clamp01((bass + mid + high) * 2.5);
|
||||||
const ringCount = 12;
|
const ringCount = 12;
|
||||||
const tunnelSpin = timeMs * 0.00042 + phase * 0.03;
|
const tunnelSpin = timeMs * 0.00042 + phase * 0.03;
|
||||||
ctx.globalCompositeOperation = 'screen';
|
ctx.globalCompositeOperation = 'screen';
|
||||||
@@ -372,9 +473,11 @@
|
|||||||
const rotation = tunnelSpin + ringIdx * 0.065 + Math.sin(timeMs * 0.0013 + ringIdx) * 0.18;
|
const rotation = tunnelSpin + ringIdx * 0.065 + Math.sin(timeMs * 0.0013 + ringIdx) * 0.18;
|
||||||
|
|
||||||
const hue = ringIdx % 4 === 0
|
const hue = ringIdx % 4 === 0
|
||||||
? wrapHue(198 + Math.cos(timeMs * 0.0018 + ringIdx) * 18 + high * 18)
|
? wrapHue(palHue(1, timeMs, 18) + Math.cos(timeMs * 0.0018 + ringIdx) * 18 + high * 18)
|
||||||
: wrapHue(356 + Math.sin(timeMs * 0.0015 + ringIdx * 0.7) * 14 + bass * 22);
|
: ringIdx % 3 === 0
|
||||||
const alpha = 0.08 + fade * (0.24 + bass * 0.35);
|
? wrapHue(palHue(2, timeMs, 14) + Math.sin(timeMs * 0.0012 + ringIdx * 0.9) * 16 + mid * 20)
|
||||||
|
: wrapHue(palHue(0, timeMs, 14) + Math.sin(timeMs * 0.0015 + ringIdx * 0.7) * 14 + bass * 22);
|
||||||
|
const alpha = (0.08 + fade * (0.24 + bass * 0.35)) * (0.05 + ringEnergy * 0.95);
|
||||||
const width = 1.6 + fade * (2.6 + bass * 3.5);
|
const width = 1.6 + fade * (2.6 + bass * 3.5);
|
||||||
|
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
@@ -395,28 +498,31 @@
|
|||||||
if (!ctx) return;
|
if (!ctx) return;
|
||||||
|
|
||||||
const veilA = ctx.createLinearGradient(0, 0, drawWidth, drawHeight);
|
const veilA = ctx.createLinearGradient(0, 0, drawWidth, drawHeight);
|
||||||
veilA.addColorStop(0, `hsla(${wrapHue(356 + timeMs * 0.028)}, 100%, 46%, ${0.08 + bass * 0.16})`);
|
veilA.addColorStop(0, `hsla(${palHue(0, timeMs, 28)}, 100%, 46%, ${0.08 + bass * 0.16})`);
|
||||||
veilA.addColorStop(0.52, 'rgba(255, 48, 108, 0)');
|
veilA.addColorStop(0.52, 'rgba(255, 48, 108, 0)');
|
||||||
veilA.addColorStop(1, `hsla(${wrapHue(204 + timeMs * 0.022)}, 100%, 50%, ${0.06 + high * 0.11})`);
|
veilA.addColorStop(1, `hsla(${palHue(1, timeMs, 22)}, 100%, 50%, ${0.06 + high * 0.11})`);
|
||||||
ctx.globalCompositeOperation = 'overlay';
|
ctx.globalCompositeOperation = 'overlay';
|
||||||
ctx.fillStyle = veilA;
|
ctx.fillStyle = veilA;
|
||||||
ctx.fillRect(0, 0, drawWidth, drawHeight);
|
ctx.fillRect(0, 0, drawWidth, drawHeight);
|
||||||
|
|
||||||
const veilB = ctx.createLinearGradient(drawWidth, 0, 0, drawHeight);
|
const veilB = ctx.createLinearGradient(drawWidth, 0, 0, drawHeight);
|
||||||
veilB.addColorStop(0, `hsla(${wrapHue(196 + timeMs * 0.018)}, 90%, 48%, ${0.04 + high * 0.08})`);
|
veilB.addColorStop(0, `hsla(${palHue(2, timeMs, 18)}, 90%, 48%, ${0.04 + high * 0.08})`);
|
||||||
veilB.addColorStop(0.5, 'rgba(28, 8, 24, 0)');
|
veilB.addColorStop(0.5, 'rgba(28, 8, 24, 0)');
|
||||||
veilB.addColorStop(1, `hsla(${wrapHue(10 + timeMs * 0.015)}, 92%, 44%, ${0.06 + bass * 0.12})`);
|
veilB.addColorStop(1, `hsla(${palHue(3, timeMs, 15)}, 92%, 44%, ${0.06 + bass * 0.12})`);
|
||||||
ctx.globalCompositeOperation = 'soft-light';
|
ctx.globalCompositeOperation = 'soft-light';
|
||||||
ctx.fillStyle = veilB;
|
ctx.fillStyle = veilB;
|
||||||
ctx.fillRect(0, 0, drawWidth, drawHeight);
|
ctx.fillRect(0, 0, drawWidth, drawHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawAbyss(centerX: number, centerY: number, minDim: number, bass: number) {
|
function drawAbyss(centerX: number, centerY: number, minDim: number, bass: number, totalEnergy: number) {
|
||||||
if (!ctx) return;
|
if (!ctx) return;
|
||||||
const abyssRadius = minDim * (0.22 + bass * 0.08);
|
// Shrink black hole when silent, grow with bass
|
||||||
const abyss = ctx.createRadialGradient(centerX, centerY, minDim * 0.02, centerX, centerY, abyssRadius);
|
const energyScale = clamp01(totalEnergy * 2.5);
|
||||||
abyss.addColorStop(0, 'rgba(0, 0, 0, 0.98)');
|
const abyssRadius = minDim * (0.04 + energyScale * 0.18 + bass * 0.08);
|
||||||
abyss.addColorStop(0.45, `rgba(8, 0, 22, ${0.76 + bass * 0.1})`);
|
const abyssAlpha = 0.3 + energyScale * 0.68;
|
||||||
|
const abyss = ctx.createRadialGradient(centerX, centerY, minDim * 0.01, centerX, centerY, abyssRadius);
|
||||||
|
abyss.addColorStop(0, `rgba(0, 0, 0, ${abyssAlpha})`);
|
||||||
|
abyss.addColorStop(0.45, `rgba(8, 0, 22, ${(0.3 + energyScale * 0.46) + bass * 0.1})`);
|
||||||
abyss.addColorStop(1, 'rgba(0, 0, 0, 0)');
|
abyss.addColorStop(1, 'rgba(0, 0, 0, 0)');
|
||||||
ctx.globalCompositeOperation = 'source-over';
|
ctx.globalCompositeOperation = 'source-over';
|
||||||
ctx.fillStyle = abyss;
|
ctx.fillStyle = abyss;
|
||||||
@@ -456,18 +562,20 @@
|
|||||||
const mid = getMidEnergy();
|
const mid = getMidEnergy();
|
||||||
const high = getHighEnergy();
|
const high = getHighEnergy();
|
||||||
phase += 0.004 + bass * 0.022 + high * 0.01;
|
phase += 0.004 + bass * 0.022 + high * 0.01;
|
||||||
|
lerpPalette();
|
||||||
if (beatCooldown > 0) beatCooldown -= 1;
|
if (beatCooldown > 0) beatCooldown -= 1;
|
||||||
|
|
||||||
drawFeedbackWarp(centerX, centerY, drawWidth, drawHeight, minDim, timestamp, bass, high);
|
drawFeedbackWarp(centerX, centerY, drawWidth, drawHeight, minDim, timestamp, bass, high);
|
||||||
|
|
||||||
ctx.fillStyle = `rgba(42, 0, 10, ${0.09 + high * 0.03})`;
|
ctx.fillStyle = `hsla(${wrapHue(artPalette[0] - 10)}, 60%, 6%, ${0.09 + high * 0.03})`;
|
||||||
ctx.fillRect(0, 0, drawWidth, drawHeight);
|
ctx.fillRect(0, 0, drawWidth, drawHeight);
|
||||||
|
|
||||||
|
const totalEnergy = (bass + mid + high) / 3;
|
||||||
drawWatercolorClouds(centerX, centerY, minDim, timestamp, bass, mid, high);
|
drawWatercolorClouds(centerX, centerY, minDim, timestamp, bass, mid, high);
|
||||||
drawWarpedTunnelRings(centerX, centerY, minDim, timestamp, bass, mid, high);
|
drawWarpedTunnelRings(centerX, centerY, minDim, timestamp, bass, mid, high);
|
||||||
drawCurvedTunnelWisps(centerX, centerY, minDim, timestamp, bass, mid, high);
|
drawCurvedTunnelWisps(centerX, centerY, minDim, timestamp, bass, mid, high, totalEnergy);
|
||||||
drawRefractiveVeil(drawWidth, drawHeight, timestamp, bass, high);
|
drawRefractiveVeil(drawWidth, drawHeight, timestamp, bass, high);
|
||||||
drawAbyss(centerX, centerY, minDim, bass);
|
drawAbyss(centerX, centerY, minDim, bass, totalEnergy);
|
||||||
|
|
||||||
ctx.globalCompositeOperation = 'source-over';
|
ctx.globalCompositeOperation = 'source-over';
|
||||||
animationFrame = requestAnimationFrame(render);
|
animationFrame = requestAnimationFrame(render);
|
||||||
@@ -556,6 +664,12 @@
|
|||||||
cleanup();
|
cleanup();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (artwork) {
|
||||||
|
loadArtworkPalette(artwork);
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="comet-flow-panel" class:visible={enabled}>
|
<div class="comet-flow-panel" class:visible={enabled}>
|
||||||
|
|||||||
Reference in new issue
Block a user