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.
|
// games and stays untouched; this is a mastervega-local reskin only.
|
||||||
//
|
//
|
||||||
// Look: angular chamfered panel (not rounded), a static neon glow on the
|
// Look: angular chamfered panel (not rounded), a static neon glow on the
|
||||||
// border (baseline look, not a hover animation), and two hover-only
|
// border (baseline look, not a hover animation), and hover-only animations —
|
||||||
// animations — a scanline sweep and four corner brackets snapping outward,
|
// two upper-right/lower-left corner brackets snapping outward like a
|
||||||
// like a targeting reticle acquiring a lock.
|
// 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';
|
import * as Phaser from 'phaser';
|
||||||
|
|
||||||
|
|
@ -50,19 +52,29 @@ export class Button extends Phaser.GameObjects.Container {
|
||||||
|
|
||||||
const scm = SCHEMES[scheme] || SCHEMES.cyan;
|
const scm = SCHEMES[scheme] || SCHEMES.cyan;
|
||||||
this.options = { width, height, bg, textColor, fontSize, variant, scheme: scm };
|
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';
|
const isGhost = variant === 'ghost';
|
||||||
|
|
||||||
this.bgRect = scene.add.graphics();
|
this.bgRect = scene.add.graphics();
|
||||||
this.bgRect.postFX.addGlow(scm.glow, 3, 0, false, 0.1, 10);
|
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
|
// spill past the panel outline. Created before the first _drawBg() call
|
||||||
// since that call populates fxMaskShape's geometry.
|
// since that call populates fxMaskShape's geometry.
|
||||||
this.fxMaskShape = scene.add.graphics().setVisible(false);
|
this.fxMaskShape = scene.add.graphics().setVisible(false);
|
||||||
this.scanline = scene.add.graphics().setAlpha(0);
|
this.shimmer = scene.add.graphics().setAlpha(0);
|
||||||
this.scanline.setMask(new Phaser.Display.Masks.GeometryMask(scene, this.fxMaskShape));
|
this.shimmer.setMask(new Phaser.Display.Masks.GeometryMask(scene, this.fxMaskShape));
|
||||||
this.brackets = scene.add.graphics().setAlpha(0);
|
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);
|
this._drawBg(bg, isGhost ? GHOST_ALPHA : 1);
|
||||||
|
|
||||||
|
|
@ -84,7 +96,10 @@ export class Button extends Phaser.GameObjects.Container {
|
||||||
|
|
||||||
this._drawBrackets(false);
|
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.setSize(width, height);
|
||||||
this.setInteractive({
|
this.setInteractive({
|
||||||
|
|
@ -103,7 +118,8 @@ export class Button extends Phaser.GameObjects.Container {
|
||||||
this._drawBg(s.glow, 1);
|
this._drawBg(s.glow, 1);
|
||||||
this.text.setColor(s.textDark);
|
this.text.setColor(s.textDark);
|
||||||
}
|
}
|
||||||
this._startScanline();
|
this._startShimmer();
|
||||||
|
this._startBorderLights();
|
||||||
this._drawBrackets(true);
|
this._drawBrackets(true);
|
||||||
};
|
};
|
||||||
const onOut = () => {
|
const onOut = () => {
|
||||||
|
|
@ -111,7 +127,8 @@ export class Button extends Phaser.GameObjects.Container {
|
||||||
const { bg: b, textColor: tc, variant: v } = this.options;
|
const { bg: b, textColor: tc, variant: v } = this.options;
|
||||||
this._drawBg(b, v === 'ghost' ? GHOST_ALPHA : 1);
|
this._drawBg(b, v === 'ghost' ? GHOST_ALPHA : 1);
|
||||||
this.text.setColor(tc);
|
this.text.setColor(tc);
|
||||||
this._stopScanline();
|
this._stopShimmer();
|
||||||
|
this._stopBorderLights();
|
||||||
this._drawBrackets(false);
|
this._drawBrackets(false);
|
||||||
};
|
};
|
||||||
const onDown = () => this.bgRect.setScale(0.97);
|
const onDown = () => this.bgRect.setScale(0.97);
|
||||||
|
|
@ -124,7 +141,10 @@ export class Button extends Phaser.GameObjects.Container {
|
||||||
this.on('pointerupoutside', onUp);
|
this.on('pointerupoutside', onUp);
|
||||||
if (onClick) this.on('pointerup', onClick);
|
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);
|
scene.add.existing(this);
|
||||||
}
|
}
|
||||||
|
|
@ -167,10 +187,12 @@ export class Button extends Phaser.GameObjects.Container {
|
||||||
const hh = height / 2;
|
const hh = height / 2;
|
||||||
const arm = 10;
|
const arm = 10;
|
||||||
const pad = extended ? 4 : 0;
|
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 = [
|
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 },
|
|
||||||
{ cx: -hw - pad, cy: hh + pad, dx: 1, dy: -1 },
|
{ cx: -hw - pad, cy: hh + pad, dx: 1, dy: -1 },
|
||||||
];
|
];
|
||||||
this.brackets.clear();
|
this.brackets.clear();
|
||||||
|
|
@ -184,36 +206,116 @@ export class Button extends Phaser.GameObjects.Container {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_startScanline() {
|
// A tall, narrow, tilted bright band tweened across in X — much taller
|
||||||
this._stopScanline();
|
// than the button so the tilt never uncovers the top/bottom edge as it
|
||||||
const { width, height, scheme } = this.options;
|
// 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 hw = width / 2;
|
||||||
const hh = height / 2;
|
const margin = height; // clears the band fully off both sides at rest
|
||||||
this.scanline.clear();
|
this.shimmer.clear();
|
||||||
this.scanline.fillStyle(scheme.glow, 0.85);
|
this.shimmer.fillStyle(0xffffff, 0.5);
|
||||||
this.scanline.fillRect(-hw, -3, width, 3);
|
this.shimmer.fillRect(-12, -height * 2, 24, height * 4);
|
||||||
this.scanline.y = -hh;
|
this.shimmer.rotation = Phaser.Math.DegToRad(22);
|
||||||
this.scanline.setAlpha(1);
|
this.shimmer.x = -hw - margin;
|
||||||
|
this.shimmer.y = 0;
|
||||||
|
this.shimmer.setAlpha(1);
|
||||||
this.brackets.setAlpha(1);
|
this.brackets.setAlpha(1);
|
||||||
this._scanTween = this.scene.tweens.add({
|
this._shimmerTween = this.scene.tweens.add({
|
||||||
targets: this.scanline,
|
targets: this.shimmer,
|
||||||
y: hh,
|
x: hw + margin,
|
||||||
duration: 650,
|
duration: 550,
|
||||||
repeat: -1,
|
repeat: -1,
|
||||||
ease: 'Sine.easeInOut',
|
repeatDelay: 1500,
|
||||||
yoyo: true,
|
ease: 'Cubic.easeInOut',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_stopScanline() {
|
_stopShimmer() {
|
||||||
if (this._scanTween) {
|
if (this._shimmerTween) {
|
||||||
this._scanTween.stop();
|
this._shimmerTween.stop();
|
||||||
this._scanTween = null;
|
this._shimmerTween = null;
|
||||||
}
|
}
|
||||||
this.scanline.setAlpha(0);
|
this.shimmer.setAlpha(0);
|
||||||
this.brackets.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) {
|
setActive(active) {
|
||||||
this._active = active;
|
this._active = active;
|
||||||
const { bg, textColor, scheme, variant } = this.options;
|
const { bg, textColor, scheme, variant } = this.options;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue