106 lines
4.0 KiB
JavaScript
106 lines
4.0 KiB
JavaScript
import Phaser from '../vendor/phaser.js';
|
|
import { toCss } from '../utils/Color.js';
|
|
|
|
/**
|
|
* A header title with the RGB pull-apart of the glitch theme: the main
|
|
* text plus a magenta-red ghost offset left and a cyan ghost offset
|
|
* right, both additive-blended so the fringes glow on the dark bg.
|
|
*
|
|
* Motion model:
|
|
* - IDLE — a slow shimmer keeps the split alive (a ~1.5 px wobble);
|
|
* - BURST — setBurst(level 0..1) pushes the ghosts apart and adds
|
|
* high-frequency jitter; the level decays on its own (~90 ms).
|
|
* Usually driven by CyberOverlay's glitch bursts:
|
|
* overlay.onGlitch((level) => title.setBurst(level));
|
|
* - BOOT — start(time) plays the flicker-in once.
|
|
*
|
|
* The scene must call update(time, delta) from its own update loop
|
|
* (Phaser v4 does not auto-update plain objects).
|
|
*/
|
|
export class GlitchText extends Phaser.GameObjects.Container {
|
|
constructor(scene, x, y, text, style, o = {}) {
|
|
super(scene, x, y);
|
|
|
|
this.level = 0; // current burst level 0..1 (decays in update)
|
|
this.baseOffset = o.baseOffset ?? 1.5; // px of idle RGB split
|
|
this.burstOffset = o.burstOffset ?? 12; // extra px of split at level 1
|
|
this.ghostAlpha = o.ghostAlpha ?? 0.55;
|
|
this.bootT0 = null;
|
|
this.bootDur = o.bootDuration ?? 650;
|
|
|
|
const main = scene.add.text(0, 0, text, style).setOrigin(0.5);
|
|
const ghostStyle = { ...style, shadow: undefined };
|
|
// v4 quirk: canvas fillStyle needs CSS strings — normalize (toCss is
|
|
// a pass-through for strings, see utils/Color.js).
|
|
const redColor = toCss(o.redColor ?? '#ff2d6f');
|
|
const cyanColor = toCss(o.cyanColor ?? '#00e5ff');
|
|
const ghostRed = scene
|
|
.add.text(0, 0, text, { ...ghostStyle, color: redColor })
|
|
.setOrigin(0.5)
|
|
.setAlpha(0)
|
|
.setBlendMode(Phaser.BlendModes.ADD);
|
|
const ghostCyan = scene
|
|
.add.text(0, 0, text, { ...ghostStyle, color: cyanColor })
|
|
.setOrigin(0.5)
|
|
.setAlpha(0)
|
|
.setBlendMode(Phaser.BlendModes.ADD);
|
|
|
|
this.main = main;
|
|
this.ghostRed = ghostRed;
|
|
this.ghostCyan = ghostCyan;
|
|
|
|
// ghosts first, main on top
|
|
this.add([ghostRed, ghostCyan, main]);
|
|
this.setSize(main.width, main.height);
|
|
|
|
// Phaser v4: a directly-constructed GameObject is NOT added to the scene's
|
|
// display list (the scene.add.* factories do that) — register it here or it
|
|
// never renders. Verified Sept 2026 against lib/phaser.min.js (4.2.1 Giedi).
|
|
this.scene.add.existing(this);
|
|
}
|
|
|
|
/** Feed a burst level (0..1); it decays automatically. */
|
|
setBurst(level) {
|
|
this.level = Math.max(this.level, Math.min(1, Math.max(0, level ?? 0)));
|
|
}
|
|
|
|
/** Arm the boot flicker (time = scene time, e.g. this.time.now). */
|
|
start(time) {
|
|
this.bootT0 = time;
|
|
}
|
|
|
|
update(time, delta) {
|
|
// Boot flicker: a few hard on/off frames, then settle.
|
|
if (this.bootT0 !== null) {
|
|
const u = (time - this.bootT0) / this.bootDur;
|
|
if (u >= 1) {
|
|
this.bootT0 = null;
|
|
this.main.setAlpha(1);
|
|
this.setScale(1);
|
|
} else if (u >= 0) {
|
|
this.main.setAlpha(Math.sin(u * 70) > -0.35 ? 1 : 0.08);
|
|
this.setScale(1.06 - 0.06 * Math.min(1, u * 1.6));
|
|
}
|
|
}
|
|
|
|
// Burst level decays (~90 ms time constant).
|
|
this.level *= Math.exp(-(delta / 90));
|
|
if (this.level < 0.002) this.level = 0;
|
|
|
|
const t = time * 0.001;
|
|
// Idle shimmer + burst jitter (deterministic pseudo-noise from sines).
|
|
const idle = 0.65 + 0.35 * Math.sin(t * 1.9);
|
|
const off = this.baseOffset * idle + this.level * this.burstOffset;
|
|
const jx = this.level * 6 * Math.sin(time * 0.053) * Math.sin(time * 0.021 + 1.7);
|
|
const jy = this.level * 4 * Math.sin(time * 0.083 + 0.4);
|
|
|
|
this.ghostRed.setPosition(-off + jx, jy * 0.5);
|
|
this.ghostCyan.setPosition(off * 0.85, -off * 0.3 + jy);
|
|
|
|
const flick = 0.6 + 0.4 * Math.sin(t * 3.3);
|
|
const alpha = Math.min(1, this.ghostAlpha * flick + this.level * 0.5);
|
|
this.ghostRed.setAlpha(alpha);
|
|
this.ghostCyan.setAlpha(alpha);
|
|
}
|
|
}
|