feat: add animated gradient meter to header text based on player count
CSS background-clip: text with dynamic linear-gradient replaces flat header color. Fill sweeps left-to-right (pink to white) proportional to playerCount/maxPlayers. Pulse glow fires when lobby is full. Made-with: Cursor
This commit is contained in:
@@ -56,6 +56,18 @@ export class RoomCodeDisplay {
|
||||
/** @type {boolean} */
|
||||
#active = false;
|
||||
|
||||
/** @type {number} */
|
||||
#meterFill = 0;
|
||||
|
||||
/** @type {number} */
|
||||
#meterTarget = 0;
|
||||
|
||||
/** @type {number | null} */
|
||||
#meterRafId = null;
|
||||
|
||||
/** @type {boolean} */
|
||||
#meterPulseFired = false;
|
||||
|
||||
/**
|
||||
* @param {RoomCodeDisplayElements} elements
|
||||
* @param {RoomCodeDisplayInputs} inputs
|
||||
@@ -89,6 +101,15 @@ export class RoomCodeDisplay {
|
||||
}
|
||||
|
||||
this.#applySettings();
|
||||
|
||||
const count = Number(ctx?.playerCount ?? 0);
|
||||
const max = Number(ctx?.maxPlayers ?? 0);
|
||||
const initialFill = max > 0 ? count / max : 0;
|
||||
this.#meterFill = Math.max(0, Math.min(1, initialFill));
|
||||
this.#meterTarget = this.#meterFill;
|
||||
this.#meterPulseFired = false;
|
||||
this.#applyMeterGradient();
|
||||
|
||||
this.#startAnimation();
|
||||
}
|
||||
|
||||
@@ -96,6 +117,14 @@ export class RoomCodeDisplay {
|
||||
this.#active = false;
|
||||
this.#clearAnimationTimers();
|
||||
|
||||
if (this.#meterRafId != null) {
|
||||
cancelAnimationFrame(this.#meterRafId);
|
||||
this.#meterRafId = null;
|
||||
}
|
||||
this.#meterFill = 0;
|
||||
this.#meterTarget = 0;
|
||||
this.#meterPulseFired = false;
|
||||
|
||||
const { header, footer, codePart1, codePart2 } = this.#elements ?? {};
|
||||
if (!header || !footer || !codePart1 || !codePart2) {
|
||||
return;
|
||||
@@ -127,6 +156,14 @@ export class RoomCodeDisplay {
|
||||
|
||||
if (next && next !== current) {
|
||||
this.activate(ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
const count = Number(ctx?.playerCount ?? 0);
|
||||
const max = Number(ctx?.maxPlayers ?? 0);
|
||||
const newTarget = max > 0 ? count / max : 0;
|
||||
if (Math.abs(newTarget - this.#meterTarget) > 0.001) {
|
||||
this.#animateMeterTo(newTarget);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,6 +177,7 @@ export class RoomCodeDisplay {
|
||||
active: this.#active,
|
||||
roomCode,
|
||||
timersRunning: this.#animationTimers.length > 0,
|
||||
meterFill: Math.round(this.#meterFill * 100),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -175,7 +213,7 @@ export class RoomCodeDisplay {
|
||||
codePart2.style.fontSize = `${inputs.size.value}px`;
|
||||
|
||||
header.textContent = inputs.headerText.value;
|
||||
header.style.color = inputs.headerColor.value;
|
||||
this.#applyMeterGradient();
|
||||
header.style.fontSize = `${inputs.headerSize.value}px`;
|
||||
header.style.transform = `translateY(${inputs.headerOffset.value}px)`;
|
||||
|
||||
@@ -279,4 +317,77 @@ export class RoomCodeDisplay {
|
||||
}, cycleDuration),
|
||||
);
|
||||
}
|
||||
|
||||
#applyMeterGradient() {
|
||||
const header = this.#elements?.header;
|
||||
const inputs = this.#inputs;
|
||||
if (!header || !inputs) return;
|
||||
|
||||
const pct = Math.round(this.#meterFill * 100);
|
||||
const baseColor = inputs.headerColor.value || '#f35dcb';
|
||||
|
||||
header.style.background =
|
||||
`linear-gradient(to right, #ffffff 0%, #ffffff ${pct}%, ${baseColor} ${pct}%, ${baseColor} 100%)`;
|
||||
header.style.webkitBackgroundClip = 'text';
|
||||
header.style.backgroundClip = 'text';
|
||||
header.style.webkitTextFillColor = 'transparent';
|
||||
}
|
||||
|
||||
#animateMeterTo(target) {
|
||||
const clamped = Math.max(0, Math.min(1, target));
|
||||
this.#meterTarget = clamped;
|
||||
|
||||
if (this.#meterRafId != null) {
|
||||
cancelAnimationFrame(this.#meterRafId);
|
||||
this.#meterRafId = null;
|
||||
}
|
||||
|
||||
const start = this.#meterFill;
|
||||
const delta = clamped - start;
|
||||
if (Math.abs(delta) < 0.001) {
|
||||
this.#meterFill = clamped;
|
||||
this.#applyMeterGradient();
|
||||
this.#checkFullPulse();
|
||||
return;
|
||||
}
|
||||
|
||||
const duration = 400;
|
||||
const startTime = performance.now();
|
||||
|
||||
const step = (now) => {
|
||||
const elapsed = now - startTime;
|
||||
const t = Math.min(elapsed / duration, 1);
|
||||
const eased = 1 - Math.pow(1 - t, 3);
|
||||
this.#meterFill = start + delta * eased;
|
||||
this.#applyMeterGradient();
|
||||
|
||||
if (t < 1) {
|
||||
this.#meterRafId = requestAnimationFrame(step);
|
||||
} else {
|
||||
this.#meterRafId = null;
|
||||
this.#meterFill = clamped;
|
||||
this.#applyMeterGradient();
|
||||
this.#checkFullPulse();
|
||||
}
|
||||
};
|
||||
|
||||
this.#meterRafId = requestAnimationFrame(step);
|
||||
}
|
||||
|
||||
#checkFullPulse() {
|
||||
const header = this.#elements?.header;
|
||||
if (!header) return;
|
||||
|
||||
if (this.#meterFill >= 1 && !this.#meterPulseFired) {
|
||||
this.#meterPulseFired = true;
|
||||
header.classList.add('meter-full-pulse');
|
||||
header.addEventListener('animationend', () => {
|
||||
header.classList.remove('meter-full-pulse');
|
||||
}, { once: true });
|
||||
}
|
||||
|
||||
if (this.#meterFill < 1) {
|
||||
this.#meterPulseFired = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user