Refine goal HUD animations
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
15
src/main.js
15
src/main.js
@@ -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);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
43
src/ui.js
43
src/ui.js
@@ -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) => {
|
||||||
|
|||||||
127
styles.css
127
styles.css
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user