refactor: replace scanline with diagonal shimmer and add orbiting border lights to VegaButton
- Replace horizontal scanline sweep with an occasional diagonal shimmer effect that crosses the button face at a 22° angle with a 1.5s pause between passes - Add three white border lights that slowly orbit the button's stroke outline (~3.6s per full revolution), drawn above all other layers - Reduce corner brackets from all four corners to upper-right and lower-left only, matching the chamfered panel's 45° cuts - Extract reusable perimeter utilities (_computePerimeter, _perimeterPoint) cached once at construction to avoid per-frame arc-length recomputation - Wire shimmer and border lights into hover enter/exit and destroy lifecycle
This commit is contained in:
parent
4c3b17610a
commit
167fdd6e3f
|
|
@ -7,9 +7,11 @@
|
|||
// games and stays untouched; this is a mastervega-local reskin only.
|
||||
//
|
||||
// Look: angular chamfered panel (not rounded), a static neon glow on the
|
||||
// border (baseline look, not a hover animation), and two hover-only
|
||||
// animations — a scanline sweep and four corner brackets snapping outward,
|
||||
// like a targeting reticle acquiring a lock.
|
||||
// border (baseline look, not a hover animation), and hover-only animations —
|
||||
// two upper-right/lower-left corner brackets snapping outward like a
|
||||
// targeting reticle acquiring a lock, an occasional diagonal shimmer
|
||||
// crossing the face, and a few small white lights slowly orbiting the
|
||||
// button's own stroke outline, drawn above everything else in the button.
|
||||
|
||||
import * as Phaser from 'phaser';
|
||||
|
||||
|
|
@ -50,19 +52,29 @@ export class Button extends Phaser.GameObjects.Container {
|
|||
|
||||
const scm = SCHEMES[scheme] || SCHEMES.cyan;
|
||||
this.options = { width, height, bg, textColor, fontSize, variant, scheme: scm };
|
||||
// Cached once — _chamferPoints() only depends on width/height, which
|
||||
// never change after construction — so the border-lights orbit
|
||||
// (_perimeterPoint) doesn't recompute the polygon's arc-length table
|
||||
// on every animation frame.
|
||||
this._perimeter = this._computePerimeter();
|
||||
|
||||
const isGhost = variant === 'ghost';
|
||||
|
||||
this.bgRect = scene.add.graphics();
|
||||
this.bgRect.postFX.addGlow(scm.glow, 3, 0, false, 0.1, 10);
|
||||
|
||||
// Scanline + corner-bracket overlays, chamfer-clipped so they never
|
||||
// Shimmer + corner-bracket overlays, chamfer-clipped so they never
|
||||
// spill past the panel outline. Created before the first _drawBg() call
|
||||
// since that call populates fxMaskShape's geometry.
|
||||
this.fxMaskShape = scene.add.graphics().setVisible(false);
|
||||
this.scanline = scene.add.graphics().setAlpha(0);
|
||||
this.scanline.setMask(new Phaser.Display.Masks.GeometryMask(scene, this.fxMaskShape));
|
||||
this.shimmer = scene.add.graphics().setAlpha(0);
|
||||
this.shimmer.setMask(new Phaser.Display.Masks.GeometryMask(scene, this.fxMaskShape));
|
||||
this.brackets = scene.add.graphics().setAlpha(0);
|
||||
// Unmasked (unlike shimmer/brackets, which stay inside or hug the
|
||||
// panel) — its points are computed directly on the stroke's own path,
|
||||
// so it never needs clipping, and it is added to the container LAST,
|
||||
// below, so it renders above bgRect's stroke rather than under it.
|
||||
this.borderLights = scene.add.graphics().setAlpha(0);
|
||||
|
||||
this._drawBg(bg, isGhost ? GHOST_ALPHA : 1);
|
||||
|
||||
|
|
@ -84,7 +96,10 @@ export class Button extends Phaser.GameObjects.Container {
|
|||
|
||||
this._drawBrackets(false);
|
||||
|
||||
this.add([this.bgRect, this.fxMaskShape, this.scanline, this.brackets, this.text]);
|
||||
// borderLights is last — Phaser Containers render children in insertion
|
||||
// order regardless of individual depth, so this is what actually puts
|
||||
// it above bgRect's stroke (and everything else) rather than under it.
|
||||
this.add([this.bgRect, this.fxMaskShape, this.shimmer, this.brackets, this.text, this.borderLights]);
|
||||
|
||||
this.setSize(width, height);
|
||||
this.setInteractive({
|
||||
|
|
@ -103,7 +118,8 @@ export class Button extends Phaser.GameObjects.Container {
|
|||
this._drawBg(s.glow, 1);
|
||||
this.text.setColor(s.textDark);
|
||||
}
|
||||
this._startScanline();
|
||||
this._startShimmer();
|
||||
this._startBorderLights();
|
||||
this._drawBrackets(true);
|
||||
};
|
||||
const onOut = () => {
|
||||
|
|
@ -111,7 +127,8 @@ export class Button extends Phaser.GameObjects.Container {
|
|||
const { bg: b, textColor: tc, variant: v } = this.options;
|
||||
this._drawBg(b, v === 'ghost' ? GHOST_ALPHA : 1);
|
||||
this.text.setColor(tc);
|
||||
this._stopScanline();
|
||||
this._stopShimmer();
|
||||
this._stopBorderLights();
|
||||
this._drawBrackets(false);
|
||||
};
|
||||
const onDown = () => this.bgRect.setScale(0.97);
|
||||
|
|
@ -124,7 +141,10 @@ export class Button extends Phaser.GameObjects.Container {
|
|||
this.on('pointerupoutside', onUp);
|
||||
if (onClick) this.on('pointerup', onClick);
|
||||
|
||||
this.once(Phaser.GameObjects.Events.DESTROY, () => this._stopScanline());
|
||||
this.once(Phaser.GameObjects.Events.DESTROY, () => {
|
||||
this._stopShimmer();
|
||||
this._stopBorderLights();
|
||||
});
|
||||
|
||||
scene.add.existing(this);
|
||||
}
|
||||
|
|
@ -167,10 +187,12 @@ export class Button extends Phaser.GameObjects.Container {
|
|||
const hh = height / 2;
|
||||
const arm = 10;
|
||||
const pad = extended ? 4 : 0;
|
||||
// Only the upper-right and lower-left corners (Brian's ask) — these are
|
||||
// also the two the chamfered panel itself cuts at 45° (_chamferPoints),
|
||||
// so the brackets echo the panel's own shape instead of framing all
|
||||
// four corners generically.
|
||||
const corners = [
|
||||
{ cx: -hw - pad, cy: -hh - pad, dx: 1, dy: 1 },
|
||||
{ cx: hw + pad, cy: -hh - pad, dx: -1, dy: 1 },
|
||||
{ cx: hw + pad, cy: hh + pad, dx: -1, dy: -1 },
|
||||
{ cx: -hw - pad, cy: hh + pad, dx: 1, dy: -1 },
|
||||
];
|
||||
this.brackets.clear();
|
||||
|
|
@ -184,36 +206,116 @@ export class Button extends Phaser.GameObjects.Container {
|
|||
}
|
||||
}
|
||||
|
||||
_startScanline() {
|
||||
this._stopScanline();
|
||||
const { width, height, scheme } = this.options;
|
||||
// A tall, narrow, tilted bright band tweened across in X — much taller
|
||||
// than the button so the tilt never uncovers the top/bottom edge as it
|
||||
// crosses — clipped to the chamfer shape by the shimmer/fxMaskShape mask
|
||||
// set up in the constructor, then a repeatDelay pause before it crosses
|
||||
// again: "occasional", not a continuous back-and-forth sweep.
|
||||
_startShimmer() {
|
||||
this._stopShimmer();
|
||||
const { width, height } = this.options;
|
||||
const hw = width / 2;
|
||||
const hh = height / 2;
|
||||
this.scanline.clear();
|
||||
this.scanline.fillStyle(scheme.glow, 0.85);
|
||||
this.scanline.fillRect(-hw, -3, width, 3);
|
||||
this.scanline.y = -hh;
|
||||
this.scanline.setAlpha(1);
|
||||
const margin = height; // clears the band fully off both sides at rest
|
||||
this.shimmer.clear();
|
||||
this.shimmer.fillStyle(0xffffff, 0.5);
|
||||
this.shimmer.fillRect(-12, -height * 2, 24, height * 4);
|
||||
this.shimmer.rotation = Phaser.Math.DegToRad(22);
|
||||
this.shimmer.x = -hw - margin;
|
||||
this.shimmer.y = 0;
|
||||
this.shimmer.setAlpha(1);
|
||||
this.brackets.setAlpha(1);
|
||||
this._scanTween = this.scene.tweens.add({
|
||||
targets: this.scanline,
|
||||
y: hh,
|
||||
duration: 650,
|
||||
this._shimmerTween = this.scene.tweens.add({
|
||||
targets: this.shimmer,
|
||||
x: hw + margin,
|
||||
duration: 550,
|
||||
repeat: -1,
|
||||
ease: 'Sine.easeInOut',
|
||||
yoyo: true,
|
||||
repeatDelay: 1500,
|
||||
ease: 'Cubic.easeInOut',
|
||||
});
|
||||
}
|
||||
|
||||
_stopScanline() {
|
||||
if (this._scanTween) {
|
||||
this._scanTween.stop();
|
||||
this._scanTween = null;
|
||||
_stopShimmer() {
|
||||
if (this._shimmerTween) {
|
||||
this._shimmerTween.stop();
|
||||
this._shimmerTween = null;
|
||||
}
|
||||
this.scanline.setAlpha(0);
|
||||
this.shimmer.setAlpha(0);
|
||||
this.brackets.setAlpha(0);
|
||||
}
|
||||
|
||||
// Arc-length table for _perimeterPoint: cumulative distance walked around
|
||||
// the chamfered polygon, point by point, closing back to the start.
|
||||
_computePerimeter() {
|
||||
const pts = this._chamferPoints();
|
||||
const cum = [0];
|
||||
for (let i = 0; i < pts.length; i += 1) {
|
||||
const a = pts[i];
|
||||
const b = pts[(i + 1) % pts.length];
|
||||
cum.push(cum[i] + Phaser.Math.Distance.Between(a.x, a.y, b.x, b.y));
|
||||
}
|
||||
return { pts, cum, total: cum[cum.length - 1] };
|
||||
}
|
||||
|
||||
/** Point at `frac` (0..1, wraps) of the way around the perimeter. */
|
||||
_perimeterPoint(frac) {
|
||||
const { pts, cum, total } = this._perimeter;
|
||||
const target = (((frac % 1) + 1) % 1) * total;
|
||||
for (let i = 0; i < pts.length; i += 1) {
|
||||
if (target <= cum[i + 1] || i === pts.length - 1) {
|
||||
const a = pts[i];
|
||||
const b = pts[(i + 1) % pts.length];
|
||||
const segLen = cum[i + 1] - cum[i];
|
||||
const t = segLen > 0 ? (target - cum[i]) / segLen : 0;
|
||||
return { x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t };
|
||||
}
|
||||
}
|
||||
return pts[0];
|
||||
}
|
||||
|
||||
// A few short white segments, evenly spaced around the perimeter, redrawn
|
||||
// each tick as `phase` advances — the "orbiting lights" effect. Each
|
||||
// segment is a straight line between two perimeter points rather than a
|
||||
// true corner-following polyline, so a segment straddling a corner cuts
|
||||
// it very slightly; negligible at this segment length, not worth a
|
||||
// multi-point path for.
|
||||
_drawBorderLights(phase) {
|
||||
const LIGHT_COUNT = 3;
|
||||
const LIGHT_FRAC = 0.05;
|
||||
this.borderLights.clear();
|
||||
this.borderLights.lineStyle(3, 0xffffff, 0.9);
|
||||
for (let i = 0; i < LIGHT_COUNT; i += 1) {
|
||||
const start = phase + i / LIGHT_COUNT;
|
||||
const a = this._perimeterPoint(start);
|
||||
const b = this._perimeterPoint(start + LIGHT_FRAC);
|
||||
this.borderLights.beginPath();
|
||||
this.borderLights.moveTo(a.x, a.y);
|
||||
this.borderLights.lineTo(b.x, b.y);
|
||||
this.borderLights.strokePath();
|
||||
}
|
||||
}
|
||||
|
||||
_startBorderLights() {
|
||||
this._stopBorderLights();
|
||||
const counter = { phase: 0 };
|
||||
this.borderLights.setAlpha(1);
|
||||
this._lightsTween = this.scene.tweens.add({
|
||||
targets: counter,
|
||||
phase: 1,
|
||||
duration: 3600, // slow, per Brian's ask
|
||||
repeat: -1,
|
||||
onUpdate: () => this._drawBorderLights(counter.phase),
|
||||
});
|
||||
}
|
||||
|
||||
_stopBorderLights() {
|
||||
if (this._lightsTween) {
|
||||
this._lightsTween.stop();
|
||||
this._lightsTween = null;
|
||||
}
|
||||
this.borderLights.clear();
|
||||
this.borderLights.setAlpha(0);
|
||||
}
|
||||
|
||||
setActive(active) {
|
||||
this._active = active;
|
||||
const { bg, textColor, scheme, variant } = this.options;
|
||||
|
|
|
|||
Loading…
Reference in New Issue