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

125 lines
4.0 KiB
JavaScript

import * as Phaser from 'phaser';
import { GAME_WIDTH, GAME_HEIGHT } from '../config.js';
// Reusable "arcade cabinet" screen dressing: animated scanlines + a vector
// TV-style bevel/vignette. Drop this onto any arcade/console game scene.
//
// (src/games/balatro/BalatroCrtPipeline.js is a different, WebGL-shader-based
// CRT effect that's single-consumer and Canvas-incompatible — not reused here;
// this module is GameObject-based so it works on any renderer.)
//
// Usage:
// this.crt = applyArcadeCRTOverlay(this, { accentTint: 0xff6b3a });
// 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,
bevelThickness: 78,
bevelColor: 0x05060a,
bevelRadius: 32,
vignetteStrength: 0.78,
accentTint: 0xc8a84b,
depth: { scan: 58, bevel: 59 },
...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 bevel = scene.add.graphics().setDepth(opts.depth.bevel);
function drawVignette(g) {
const corners = [
[0, 0], [GAME_WIDTH, 0], [0, GAME_HEIGHT], [GAME_WIDTH, GAME_HEIGHT],
];
for (const [cx, cy] of corners) {
g.fillStyle(0x000000, opts.vignetteStrength * 0.42);
g.fillCircle(cx, cy, 480);
g.fillStyle(0x000000, opts.vignetteStrength * 0.3);
g.fillCircle(cx, cy, 320);
}
const rings = 7;
for (let i = 0; i < rings; i += 1) {
const t = i / (rings - 1);
const inset = opts.bevelThickness + t * 180;
const alpha = opts.vignetteStrength * (1 - t) * 0.16;
g.lineStyle(52, 0x000000, alpha);
g.strokeRoundedRect(inset, inset, GAME_WIDTH - inset * 2, GAME_HEIGHT - inset * 2, opts.bevelRadius + 8);
}
}
function drawBevel() {
bevel.clear();
drawVignette(bevel);
const inset = opts.bevelThickness / 2;
bevel.lineStyle(opts.bevelThickness, opts.bevelColor, 1);
bevel.strokeRoundedRect(inset, inset, GAME_WIDTH - opts.bevelThickness, GAME_HEIGHT - opts.bevelThickness, opts.bevelRadius);
bevel.lineStyle(9, opts.accentTint, 0.65);
bevel.strokeRoundedRect(
opts.bevelThickness, opts.bevelThickness,
GAME_WIDTH - opts.bevelThickness * 2, GAME_HEIGHT - opts.bevelThickness * 2,
Math.max(0, opts.bevelRadius - 8),
);
}
drawBevel();
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,
});
},
setIntensity(patch = {}) {
Object.assign(opts, patch);
scan.setAlpha(opts.scanlineAlpha).setTint(opts.scanlineTint);
drawBevel();
},
destroy() {
scanTween.stop();
if (flashTween) flashTween.stop();
scan.destroy();
bevel.destroy();
},
};
return controller;
}