fertig-classic-games/src/ui/ArcadeCRTOverlay.js

107 lines
3.4 KiB
JavaScript

import * as Phaser from 'phaser';
import { GAME_WIDTH, GAME_HEIGHT } from '../config.js';
import { attachArcadeCurve } from './ArcadeCurvePipeline.js';
// Reusable "arcade cabinet" screen dressing: animated scanlines (GameObject-
// based, works on any renderer) plus a real barrel-distorted curved-CRT look
// (WebGL post-FX pipeline, see ArcadeCurvePipeline.js — Canvas gets a no-op
// stub for that half, scanlines still show). Drop this onto any
// arcade/console game scene.
//
// (src/games/balatro/BalatroCrtPipeline.js is a separate, single-consumer
// WebGL CRT effect — not reused here, but this module follows the same
// PostFXPipeline convention for its curvature half.)
//
// Usage:
// this.crt = applyArcadeCRTOverlay(this, { accentTint: 0xff6b3a, curveAmount: 0.4 });
// this.events.once('shutdown', () => this.crt.destroy());
// this.crt.pulse(1.0, 400); // one-off impact flash
// this.crt.setIntensity({ accentTint: C.accent }); // re-skin on palette change
const SCAN_TEXTURE_KEY = 'arcade-crt-scan';
const SCAN_STRIP_HEIGHT = 12;
const SCAN_LINE_HEIGHT = 6;
function ensureScanTexture(scene) {
if (scene.textures.exists(SCAN_TEXTURE_KEY)) return;
const g = scene.make.graphics({ x: 0, y: 0, add: false });
g.fillStyle(0xffffff, 0.18);
g.fillRect(0, 0, GAME_WIDTH, SCAN_LINE_HEIGHT);
g.generateTexture(SCAN_TEXTURE_KEY, GAME_WIDTH, SCAN_STRIP_HEIGHT);
g.destroy();
}
export function applyArcadeCRTOverlay(scene, options = {}) {
const opts = {
scanlineAlpha: 0.65,
scanlineTint: 0x00f0ff,
scanlineSpeedMs: 9000,
accentTint: 0xc8a84b,
depth: { scan: 58 },
curveAmount: 0.35,
curveBezelColor: 0x05060a,
curveVignetteStrength: 0.35,
curveSheenStrength: 0.08,
curveAberration: 0.0035,
...options,
};
ensureScanTexture(scene);
const scan = scene.add
.tileSprite(GAME_WIDTH / 2, GAME_HEIGHT / 2, GAME_WIDTH, GAME_HEIGHT, SCAN_TEXTURE_KEY)
.setDepth(opts.depth.scan)
.setAlpha(opts.scanlineAlpha)
.setTint(opts.scanlineTint);
scan.setBlendMode(Phaser.BlendModes.ADD);
const scanTween = scene.tweens.add({
targets: scan,
tilePositionY: GAME_HEIGHT,
duration: opts.scanlineSpeedMs,
repeat: -1,
ease: 'Linear',
});
const curve = attachArcadeCurve(scene, {
amount: opts.curveAmount,
bezelColor: opts.curveBezelColor,
vignetteStrength: opts.curveVignetteStrength,
sheenStrength: opts.curveSheenStrength,
aberration: opts.curveAberration,
});
let flashTween = null;
const controller = {
pulse(strength = 0.5, durationMs = 150) {
scene.cameras.main.shake(durationMs, 0.004 * strength);
if (flashTween) flashTween.stop();
scan.setAlpha(Math.min(1, opts.scanlineAlpha + 0.5 * strength));
flashTween = scene.tweens.add({
targets: scan, alpha: opts.scanlineAlpha, duration: durationMs * 2,
});
curve.pulse(strength);
},
setIntensity(patch = {}) {
Object.assign(opts, patch);
scan.setAlpha(opts.scanlineAlpha).setTint(opts.scanlineTint);
curve.setUniforms({
amount: opts.curveAmount,
bezelColor: opts.curveBezelColor,
vignetteStrength: opts.curveVignetteStrength,
sheenStrength: opts.curveSheenStrength,
aberration: opts.curveAberration,
});
},
destroy() {
scanTween.stop();
if (flashTween) flashTween.stop();
scan.destroy();
curve.destroy();
},
};
return controller;
}