Files

124 lines
4.4 KiB
JavaScript

/**
* VIZ 2.0 - Visual FX (Phosphor Afterglow, CRT, Scanlines & VHS Noise)
*/
import { state } from '../state.js';
/**
* Returns an offscreen canvas context configured for the spectrum afterglow pass
*/
export function getSpectrumOffscreenCtx(width, height, shakeX, shakeY, shakeZoom) {
if (!state.spectrumCanvas || state.spectrumCanvas.width !== width || state.spectrumCanvas.height !== height) {
state.spectrumCanvas = document.createElement('canvas');
state.spectrumCanvas.width = width;
state.spectrumCanvas.height = height;
state.spectrumCtx = state.spectrumCanvas.getContext('2d', { alpha: true });
}
const sCtx = state.spectrumCtx;
sCtx.setTransform(1, 0, 0, 1, 0, 0);
sCtx.clearRect(0, 0, width, height);
sCtx.save();
sCtx.translate(width / 2 + shakeX, height / 2 + shakeY);
sCtx.scale(shakeZoom, shakeZoom);
return sCtx;
}
/**
* Composites the glowing phosphor afterglow trails onto the target canvas
*/
export function updateAndCompositeSpectrumAfterglow(targetCtx, width, height, cfg) {
if (!state.spectrumCanvas) return;
if (!state.afterglowCanvas || state.afterglowCanvas.width !== width || state.afterglowCanvas.height !== height) {
state.afterglowCanvas = document.createElement('canvas');
state.afterglowCanvas.width = width;
state.afterglowCanvas.height = height;
state.afterglowCtx = state.afterglowCanvas.getContext('2d', { alpha: true });
}
const aCtx = state.afterglowCtx;
const decay = cfg.afterglowDecay || 0.82;
// 1. Decay previous spectrum trails cleanly with destination-out
aCtx.save();
aCtx.globalCompositeOperation = 'destination-out';
aCtx.fillStyle = `rgba(0, 0, 0, ${1 - decay})`;
aCtx.fillRect(0, 0, width, height);
aCtx.restore();
// 2. Optional Phosphor Color Tint
if (cfg.afterglowMode === 'green_p1') {
aCtx.save();
aCtx.globalCompositeOperation = 'source-atop';
aCtx.fillStyle = 'rgba(0, 255, 68, 0.08)';
aCtx.fillRect(0, 0, width, height);
aCtx.restore();
} else if (cfg.afterglowMode === 'amber_p4') {
aCtx.save();
aCtx.globalCompositeOperation = 'source-atop';
aCtx.fillStyle = 'rgba(255, 170, 0, 0.08)';
aCtx.fillRect(0, 0, width, height);
aCtx.restore();
} else if (cfg.afterglowMode === 'cyan_neon') {
aCtx.save();
aCtx.globalCompositeOperation = 'source-atop';
aCtx.fillStyle = 'rgba(0, 240, 255, 0.08)';
aCtx.fillRect(0, 0, width, height);
aCtx.restore();
}
// 3. Draw afterglow trail onto targetCtx (under the current sharp spectrum)
targetCtx.save();
targetCtx.setTransform(1, 0, 0, 1, 0, 0);
targetCtx.globalCompositeOperation = (cfg.backgroundMode === 'transparent') ? 'source-over' : 'screen';
targetCtx.drawImage(state.afterglowCanvas, 0, 0);
// 4. Draw current sharp spectrum onto targetCtx
targetCtx.drawImage(state.spectrumCanvas, 0, 0);
targetCtx.restore();
// 5. Stamp current spectrum into afterglow canvas for next frame
aCtx.save();
aCtx.globalCompositeOperation = 'source-over';
aCtx.drawImage(state.spectrumCanvas, 0, 0);
aCtx.restore();
}
/**
* Applies CRT curvature vignette, scanlines, and VHS noise jitter
*/
export function applyVhsAndCrt(targetCtx, width, height, currentTime, cfg) {
if (!cfg.enableVhs) return;
// In transparent mode, skip full-screen overlays to preserve pure Alpha transparency
if (cfg.backgroundMode === 'transparent') return;
targetCtx.save();
// 1. Scanlines
const intensity = cfg.vhsScanlineIntensity ?? 0.25;
targetCtx.fillStyle = `rgba(0, 0, 0, ${intensity})`;
const step = 4;
for (let y = 0; y < height; y += step) {
targetCtx.fillRect(0, y, width, 1.5);
}
// 2. VHS Noise & Tracking Jitter
const noise = cfg.vhsNoiseIntensity ?? 0.15;
if (noise > 0.02) {
targetCtx.fillStyle = `rgba(255, 255, 255, ${noise * 0.12})`;
const bandY = ((currentTime * 1.5) % 1) * height;
targetCtx.fillRect(0, bandY, width, 6);
const bandY2 = (((currentTime * 0.7) + 0.5) % 1) * height;
targetCtx.fillRect(0, bandY2, width, 3);
}
// 3. CRT Vignette & Curvature Mask
if (cfg.vhsCrtCurvature) {
const crtGrad = targetCtx.createRadialGradient(width / 2, height / 2, width * 0.42, width / 2, height / 2, width * 0.72);
crtGrad.addColorStop(0, 'rgba(0, 0, 0, 0)');
crtGrad.addColorStop(0.85, 'rgba(0, 0, 0, 0.35)');
crtGrad.addColorStop(1, 'rgba(0, 0, 0, 0.75)');
targetCtx.fillStyle = crtGrad;
targetCtx.fillRect(0, 0, width, height);
}
targetCtx.restore();
}