From 9d554c880564180934e1e4154133666cb2bb22bc Mon Sep 17 00:00:00 2001 From: Daddy32 Date: Tue, 16 Dec 2025 21:15:14 +0100 Subject: [PATCH] Refine goal HUD animations --- src/config.js | 8 +++ src/main.js | 15 ++++- src/scenes/scene-template.js | 8 +++ src/ui.js | 43 +++++++++++- styles.css | 127 ++++++++++++++++++++++++++++++++++- 5 files changed, 195 insertions(+), 6 deletions(-) diff --git a/src/config.js b/src/config.js index 408e27a..9a1bf89 100644 --- a/src/config.js +++ b/src/config.js @@ -56,6 +56,14 @@ }, }, messages: { ...defaultMessageConfig }, + goalEffects: { + enabled: true, + nearThreshold: 0.7, + completeThreshold: 0.98, + pulseSpeedMs: 900, + glowStrength: 0.55, + gradientBlend: true, + }, }; return { diff --git a/src/main.js b/src/main.js index 6f21a4c..886fdf5 100644 --- a/src/main.js +++ b/src/main.js @@ -65,8 +65,16 @@ }, }); + const normalizeGoalEffects = ( + goalEffects = {}, + defaults = baseConfig.goalEffects || {}, + ) => ({ + ...defaults, + ...goalEffects, + }); + const normalizeSceneConfig = (sceneConfig = {}, defaults = baseConfig) => { - const { link = {}, messages = {}, ...rest } = sceneConfig; + const { link = {}, messages = {}, goalEffects = {}, ...rest } = sceneConfig; const base = defaults || {}; return { ...base, @@ -76,6 +84,7 @@ messages, base.messages || defaultMessageConfig, ), + goalEffects: normalizeGoalEffects(goalEffects, base.goalEffects), }; }; @@ -403,7 +412,7 @@ const checkWinCondition = () => { if (state.levelWon) return; const goal = goals.getGoalState(); - ui.setGoal(goal || { label: "—", progress: 0 }); + ui.setGoal(goal || { label: "—", progress: 0 }, config.goalEffects); if (!goal || !goal.met) return; applyWinEffects(); state.levelWon = true; @@ -424,7 +433,7 @@ activeColor: chain.color, }); const goal = goals.getGoalState(); - ui.setGoal(goal || { label: "—", progress: 0 }); + ui.setGoal(goal || { label: "—", progress: 0 }, config.goalEffects); goals.maybeAnnounceGoalProgress(goal); }; diff --git a/src/scenes/scene-template.js b/src/scenes/scene-template.js index 6808398..2dd2e86 100644 --- a/src/scenes/scene-template.js +++ b/src/scenes/scene-template.js @@ -141,6 +141,14 @@ // Scoring/persistence 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) => { // Return an array of Matter bodies that make up scene obstacles/boundaries. diff --git a/src/ui.js b/src/ui.js index 4d65c2c..2e463a0 100644 --- a/src/ui.js +++ b/src/ui.js @@ -108,7 +108,7 @@ } }; - const setGoal = ({ label, progress, colors }) => { + const setGoal = ({ label, progress, colors }, effects = {}) => { if (goalLabelEl) { goalLabelEl.innerHTML = ""; if (Array.isArray(colors) && colors.length > 0) { @@ -135,6 +135,47 @@ 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) => { diff --git a/styles.css b/styles.css index 593cd4b..2199908 100644 --- a/styles.css +++ b/styles.css @@ -159,14 +159,137 @@ canvas { height: 8px; border-radius: 999px; background: rgba(255, 255, 255, 0.08); - overflow: hidden; + overflow: visible; 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 { height: 100%; width: 0%; 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 { background: rgba(0, 0, 0, 0.25);