export class UnknownPleasuresPreset { constructor() { this.name = 'Unknown Pleasures'; this.historySize = 30; this.dataPoints = 96; this.history = []; this.writeIndex = 0; this.pLookup = new Float32Array(this.dataPoints); this.xLookup = new Float32Array(this.dataPoints); // palette cache this._palette = null; this._paletteColor = ''; // Rotation constants (cached for performance) this.rotationAngle = Math.PI / 6; // 30 degrees this._cos = Math.cos(this.rotationAngle); this._sin = Math.sin(this.rotationAngle); this.reset(); this._precompute(); } reset() { this.history.length = 0; for (let i = 0; i < this.historySize; i++) { this.history.push(new Float32Array(this.dataPoints)); } this.writeIndex = 0; } resize() {} destroy() { this.history.length = 0; } _precompute() { const pts = this.dataPoints; const inv = 1 / (pts - 1); for (let i = 0; i < pts; i++) { const p = Math.abs(i * inv - 0.5) * 2; this.pLookup[i] = 1 - p * p * p; this.xLookup[i] = i * inv; } } _buildPalette(color) { const r = parseInt(color.slice(1, 3), 16); const g = parseInt(color.slice(3, 5), 16); const b = parseInt(color.slice(5, 7), 16); // perceptual grayscale (same weights browsers use) const gray = 0.299 * r + 0.587 * g + 0.114 * b; this._palette = new Array(this.historySize); for (let i = 0; i < this.historySize; i++) { const p = i / (this.historySize - 1); // === Saturation gradient (HSL-like) === const sat = 3.0 - 2 * p; const rr = (gray + (r - gray) * sat) | 0; const gg = (gray + (g - gray) * sat) | 0; const bb = (gray + (b - gray) * sat) | 0; this._palette[i] = `rgba(${rr},${gg},${bb}, 1.0)`; } this._paletteColor = color; } draw(ctx, canvas, analyser, dataArray, params) { // Init if empty (e.g. after destroy/switch) if (this.history.length === 0) { this.reset(); } const pts = this.dataPoints; const len = dataArray.length | 0; const line = this.history[this.writeIndex]; if (line) { for (let i = 0; i < pts; i++) { line[i] = (dataArray[(this.xLookup[i] * len) | 0] / 255) * this.pLookup[i]; } } this.writeIndex = (this.writeIndex + 1) % this.historySize; if (this._paletteColor !== params.primaryColor) { this._buildPalette(params.primaryColor); } const { width, height } = canvas; const { mode } = params; const isDark = document.documentElement.getAttribute('data-theme') !== 'light'; ctx.clearRect(0, 0, width, height); if (mode !== 'blended') { ctx.fillStyle = isDark ? '#050505' : '#e6e6e6'; ctx.fillRect(0, 0, width, height); } // Compute rotated bounding box that covers the entire viewport // When rotating by angle θ, a WxH rectangle needs a bounding box of: // rotatedW = |W*cos(θ)| + |H*sin(θ)| // rotatedH = |W*sin(θ)| + |H*cos(θ)| const rotatedW = Math.abs(width * this._cos) + Math.abs(height * this._sin); const rotatedH = Math.abs(width * this._sin) + Math.abs(height * this._cos); const size = Math.max(rotatedW, rotatedH) * 1.15; // 15% padding for safety ctx.save(); // Translate to center, rotate, then offset to position lines ctx.translate(width / 2, height / 2); ctx.rotate(this.rotationAngle); ctx.translate(-size / 2, -size / 2); // SINGLE shadow pass (cheap) ctx.shadowColor = params.primaryColor; ctx.shadowBlur = 32 * (1 + params.kick * 2); ctx.lineJoin = 'round'; ctx.lineCap = 'round'; ctx.shadowOffsetX = params.kick * 10; ctx.shadowOffsetY = params.kick * 10; const horizonY = size * 0.1; const frontY = size * 0.8; const depth = 2.0; const totalH = frontY - horizonY; const B = totalH / (1 - 1 / (1 + depth)); const A = frontY - B; for (let i = this.historySize - 1; i >= 0; i--) { const idx = (this.writeIndex + i) % this.historySize; const data = this.history[idx]; const p = 1 - i / (this.historySize - 1); const z = 1 + p * depth; const scale = 1 / z; const y = A + B / z; ctx.strokeStyle = this._palette[i]; ctx.lineWidth = Math.max(1, 8 * scale + params.kick * 6); const lw = size * scale * 1.5; const margin = (size - lw) * 0.5; const amp = 200 * scale + params.kick * 100; ctx.beginPath(); ctx.moveTo(margin, y); for (let j = 0; j < pts; j++) { ctx.lineTo(margin + this.xLookup[j] * lw, y - data[j] * amp); } ctx.stroke(); } ctx.restore(); } }