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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,10 +35,24 @@
|
||||
color: #f35dcb;
|
||||
font-size: 80px;
|
||||
font-weight: bold;
|
||||
text-shadow: 3px 3px 8px rgba(0, 0, 0, 0.8);
|
||||
letter-spacing: 2px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.5s ease;
|
||||
background: linear-gradient(to right, #ffffff 0%, #ffffff 0%, #f35dcb 0%, #f35dcb 100%);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
filter: drop-shadow(3px 3px 8px rgba(0, 0, 0, 0.8));
|
||||
}
|
||||
|
||||
@keyframes meter-full-pulse {
|
||||
0% { filter: drop-shadow(3px 3px 8px rgba(0,0,0,0.8)); }
|
||||
50% { filter: drop-shadow(0 0 20px rgba(255,255,255,0.6)) drop-shadow(3px 3px 8px rgba(0,0,0,0.8)); }
|
||||
100% { filter: drop-shadow(3px 3px 8px rgba(0,0,0,0.8)); }
|
||||
}
|
||||
|
||||
#header.meter-full-pulse {
|
||||
animation: meter-full-pulse 0.6s ease-out;
|
||||
}
|
||||
|
||||
#footer {
|
||||
|
||||
Reference in New Issue
Block a user