Refine goal HUD animations

This commit is contained in:
Daddy32
2025-12-16 21:15:14 +01:00
parent e3dfeb2e70
commit 9d554c8805
5 changed files with 195 additions and 6 deletions

View File

@@ -56,6 +56,14 @@
}, },
}, },
messages: { ...defaultMessageConfig }, messages: { ...defaultMessageConfig },
goalEffects: {
enabled: true,
nearThreshold: 0.7,
completeThreshold: 0.98,
pulseSpeedMs: 900,
glowStrength: 0.55,
gradientBlend: true,
},
}; };
return { return {

View File

@@ -65,8 +65,16 @@
}, },
}); });
const normalizeGoalEffects = (
goalEffects = {},
defaults = baseConfig.goalEffects || {},
) => ({
...defaults,
...goalEffects,
});
const normalizeSceneConfig = (sceneConfig = {}, defaults = baseConfig) => { const normalizeSceneConfig = (sceneConfig = {}, defaults = baseConfig) => {
const { link = {}, messages = {}, ...rest } = sceneConfig; const { link = {}, messages = {}, goalEffects = {}, ...rest } = sceneConfig;
const base = defaults || {}; const base = defaults || {};
return { return {
...base, ...base,
@@ -76,6 +84,7 @@
messages, messages,
base.messages || defaultMessageConfig, base.messages || defaultMessageConfig,
), ),
goalEffects: normalizeGoalEffects(goalEffects, base.goalEffects),
}; };
}; };
@@ -403,7 +412,7 @@
const checkWinCondition = () => { const checkWinCondition = () => {
if (state.levelWon) return; if (state.levelWon) return;
const goal = goals.getGoalState(); const goal = goals.getGoalState();
ui.setGoal(goal || { label: "—", progress: 0 }); ui.setGoal(goal || { label: "—", progress: 0 }, config.goalEffects);
if (!goal || !goal.met) return; if (!goal || !goal.met) return;
applyWinEffects(); applyWinEffects();
state.levelWon = true; state.levelWon = true;
@@ -424,7 +433,7 @@
activeColor: chain.color, activeColor: chain.color,
}); });
const goal = goals.getGoalState(); const goal = goals.getGoalState();
ui.setGoal(goal || { label: "—", progress: 0 }); ui.setGoal(goal || { label: "—", progress: 0 }, config.goalEffects);
goals.maybeAnnounceGoalProgress(goal); goals.maybeAnnounceGoalProgress(goal);
}; };

View File

@@ -141,6 +141,14 @@
// Scoring/persistence // Scoring/persistence
noGameOver: false, // true disables game-over-on-blocked-entry checks noGameOver: false, // true disables game-over-on-blocked-entry checks
goalEffects: {
enabled: true, // set false to disable HUD goal bling
nearThreshold: 0.7, // fraction (0-1) that triggers “near” pulse
completeThreshold: 0.98, // fraction (0-1) for “almost there” glow
pulseSpeedMs: 900, // pulse speed for near/complete states
glowStrength: 0.55, // opacity multiplier for glow
gradientBlend: true, // when colors are provided, blend them into the bar
},
}, },
createBodies: (w, h) => { createBodies: (w, h) => {
// Return an array of Matter bodies that make up scene obstacles/boundaries. // Return an array of Matter bodies that make up scene obstacles/boundaries.

View File

@@ -108,7 +108,7 @@
} }
}; };
const setGoal = ({ label, progress, colors }) => { const setGoal = ({ label, progress, colors }, effects = {}) => {
if (goalLabelEl) { if (goalLabelEl) {
goalLabelEl.innerHTML = ""; goalLabelEl.innerHTML = "";
if (Array.isArray(colors) && colors.length > 0) { if (Array.isArray(colors) && colors.length > 0) {
@@ -135,6 +135,47 @@
0, 0,
Math.min(100, progress ?? 0), Math.min(100, progress ?? 0),
)}%`; )}%`;
if (goalProgressEl) {
const parentProgress = goalProgressEl.parentElement;
const targetEls = [goalProgressEl, parentProgress].filter(Boolean);
targetEls.forEach((el) => {
el.classList.remove("goal-near", "goal-complete", "goal-gradient");
});
goalProgressEl.style.removeProperty("background-image");
goalProgressEl.style.removeProperty("--goal-glow-alpha");
const fraction = Math.max(0, Math.min(1, (progress ?? 0) / 100));
if (effects?.enabled) {
const near = effects.nearThreshold ?? 0.7;
const complete = effects.completeThreshold ?? 0.98;
const pulseSpeed = Math.max(200, effects.pulseSpeedMs ?? 900);
goalProgressEl.style.setProperty(
"--goal-pulse-speed",
`${pulseSpeed}ms`,
);
goalProgressEl.style.setProperty(
"--goal-glow-alpha",
effects.glowStrength ?? 0.55,
);
if (fraction >= complete) {
targetEls.forEach((el) => el.classList.add("goal-complete"));
} else if (fraction >= near) {
targetEls.forEach((el) => el.classList.add("goal-near"));
}
const useGradient =
effects.gradientBlend !== false &&
Array.isArray(colors) &&
colors.length > 0;
if (useGradient) {
goalProgressEl.classList.add("goal-gradient");
const denom = Math.max(1, colors.length - 1);
const stops = colors
.map((c, idx) => `${c} ${(idx / denom) * 100}%`)
.join(", ");
goalProgressEl.style.backgroundImage = `linear-gradient(90deg, ${stops})`;
}
}
}
}; };
const showWin = (message) => { const showWin = (message) => {

View File

@@ -159,14 +159,137 @@ canvas {
height: 8px; height: 8px;
border-radius: 999px; border-radius: 999px;
background: rgba(255, 255, 255, 0.08); background: rgba(255, 255, 255, 0.08);
overflow: hidden; overflow: visible;
border: 1px solid rgba(148, 163, 184, 0.16); border: 1px solid rgba(148, 163, 184, 0.16);
position: relative;
}
.progress.goal-near,
.progress.goal-complete {
box-shadow:
0 0 8px rgba(34, 211, 238, 0.2),
0 0 12px rgba(167, 139, 250, 0.16);
}
.progress.goal-near {
animation: goalAura 1100ms ease-in-out infinite;
}
.progress.goal-complete {
animation: goalAura 920ms ease-in-out infinite;
box-shadow:
0 0 12px rgba(34, 211, 238, 0.28),
0 0 18px rgba(167, 139, 250, 0.22);
} }
.progress__bar { .progress__bar {
height: 100%; height: 100%;
width: 0%; width: 0%;
background: linear-gradient(135deg, #22d3ee, #a78bfa); background: linear-gradient(135deg, #22d3ee, #a78bfa);
transition: width 150ms ease; transition:
width 150ms ease,
transform 220ms ease,
filter 220ms ease;
position: relative;
box-shadow: 0 0 0 rgba(34, 211, 238, 0);
transform-origin: left center;
}
.progress__bar::after {
content: "";
position: absolute;
inset: -6px -2px;
border-radius: inherit;
background: inherit;
opacity: 0;
filter: blur(10px);
transform: scale(0.98);
pointer-events: none;
transition:
opacity 180ms ease,
transform 180ms ease;
}
.progress__bar.goal-gradient {
background: linear-gradient(90deg, #22d3ee, #a78bfa);
background-size: 180% 100%;
animation: goalShimmer 2200ms ease-in-out infinite;
}
.progress__bar.goal-near {
animation:
goalPulse var(--goal-pulse-speed, 900ms) ease-in-out infinite,
goalGlow 1400ms ease-in-out infinite;
box-shadow:
0 0 12px rgba(34, 211, 238, 0.4),
0 0 18px rgba(167, 139, 250, 0.3);
filter: saturate(1.1);
}
.progress__bar.goal-near::after {
opacity: 0.45;
transform: scale(1.03);
animation: goalAura 900ms ease-in-out infinite;
}
.progress__bar.goal-complete {
animation:
goalPulse 820ms ease-in-out infinite,
goalGlow 1000ms ease-in-out infinite,
goalShimmer 1400ms linear infinite;
box-shadow:
0 0 18px rgba(34, 211, 238, 0.5),
0 0 26px rgba(167, 139, 250, 0.4),
0 0 36px rgba(14, 165, 233, 0.35);
filter: saturate(1.2);
}
.progress__bar.goal-complete::after {
opacity: 0.65;
transform: scale(1.06);
animation: goalAura 760ms ease-in-out infinite;
}
@keyframes goalPulse {
0% {
transform: scaleY(1);
filter: brightness(1);
}
50% {
transform: scaleY(1.2);
filter: brightness(1.12);
}
100% {
transform: scaleY(1);
filter: brightness(1);
}
}
@keyframes goalAura {
0% {
opacity: 0.2;
transform: scale(1.02);
}
50% {
opacity: 0.7;
transform: scale(1.12);
}
100% {
opacity: 0.2;
transform: scale(1.02);
}
}
@keyframes goalGlow {
0% {
filter: drop-shadow(0 0 0 rgba(34, 211, 238, 0));
}
50% {
filter: drop-shadow(
0 0 14px rgba(34, 211, 238, var(--goal-glow-alpha, 0.55))
);
}
100% {
filter: drop-shadow(0 0 0 rgba(34, 211, 238, 0));
}
}
@keyframes goalShimmer {
0% {
background-position: 0% 0;
}
50% {
background-position: 80% 0;
}
100% {
background-position: 0% 0;
}
} }
.selector { .selector {
background: rgba(0, 0, 0, 0.25); background: rgba(0, 0, 0, 0.25);