124 lines
4.4 KiB
JavaScript
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();
|
|
}
|