Add scene backdrop controls, FPS display, and toned backdrops

This commit is contained in:
Daddy32
2025-12-16 21:33:01 +01:00
parent 9d554c8805
commit 7a025ce68d
6 changed files with 245 additions and 7 deletions

View File

@@ -64,6 +64,14 @@
glowStrength: 0.55, glowStrength: 0.55,
gradientBlend: true, gradientBlend: true,
}, },
backdrop: {
enabled: true,
colors: null, // optional array; defaults derived from palette
opacity: 0.19,
blur: 24,
speedSec: 30,
},
showFps: true,
}; };
return { return {

View File

@@ -12,6 +12,7 @@
getMaxLinkDistance, getMaxLinkDistance,
updateHud, updateHud,
checkWinCondition, checkWinCondition,
ui,
}) => { }) => {
const runSceneBeforeUpdateHook = () => { const runSceneBeforeUpdateHook = () => {
const currentScene = getCurrentScene(); const currentScene = getCurrentScene();
@@ -297,6 +298,7 @@
Events.on(engine, "afterUpdate", keepBallsInBounds); Events.on(engine, "afterUpdate", keepBallsInBounds);
Events.on(engine, "beforeUpdate", beforeUpdateStep); Events.on(engine, "beforeUpdate", beforeUpdateStep);
Events.on(render, "afterRender", drawLinkGlowAndSparkles); Events.on(render, "afterRender", drawLinkGlowAndSparkles);
Events.on(render, "afterRender", () => ui.updateFps && ui.updateFps());
}; };
window.PhysilinksLoop = { create }; window.PhysilinksLoop = { create };

View File

@@ -73,8 +73,27 @@
...goalEffects, ...goalEffects,
}); });
const normalizeBackdrop = (
backdrop = {},
defaults = baseConfig.backdrop || {},
) => ({
...defaults,
...backdrop,
colors: Array.isArray(backdrop.colors)
? [...backdrop.colors]
: Array.isArray(defaults.colors)
? [...defaults.colors]
: null,
});
const normalizeSceneConfig = (sceneConfig = {}, defaults = baseConfig) => { const normalizeSceneConfig = (sceneConfig = {}, defaults = baseConfig) => {
const { link = {}, messages = {}, goalEffects = {}, ...rest } = sceneConfig; const {
link = {},
messages = {},
goalEffects = {},
backdrop = {},
...rest
} = sceneConfig;
const base = defaults || {}; const base = defaults || {};
return { return {
...base, ...base,
@@ -85,6 +104,7 @@
base.messages || defaultMessageConfig, base.messages || defaultMessageConfig,
), ),
goalEffects: normalizeGoalEffects(goalEffects, base.goalEffects), goalEffects: normalizeGoalEffects(goalEffects, base.goalEffects),
backdrop: normalizeBackdrop(backdrop, base.backdrop),
}; };
}; };
@@ -216,6 +236,8 @@
setSceneIdInUrl(next.id); setSceneIdInUrl(next.id);
const prevRadius = config.ballRadius; const prevRadius = config.ballRadius;
setConfigForScene(next.config); setConfigForScene(next.config);
ui.setBackdrop(config.backdrop, config.palette);
ui.setFpsVisibility(config.showFps);
ui.setMessageDefaults(config.messages); ui.setMessageDefaults(config.messages);
resetEngineForScene(next.config, { prevRadius }); resetEngineForScene(next.config, { prevRadius });
state.clearedCount = 0; state.clearedCount = 0;
@@ -525,6 +547,7 @@
getMaxLinkDistance, getMaxLinkDistance,
updateHud, updateHud,
checkWinCondition, checkWinCondition,
ui,
}); });
window.addEventListener("resize", handleResize); window.addEventListener("resize", handleResize);

View File

@@ -149,6 +149,14 @@
glowStrength: 0.55, // opacity multiplier for glow glowStrength: 0.55, // opacity multiplier for glow
gradientBlend: true, // when colors are provided, blend them into the bar gradientBlend: true, // when colors are provided, blend them into the bar
}, },
backdrop: {
enabled: true, // disable to turn off scene backdrop
colors: null, // array of colors for backdrop blobs; defaults derive from palette
opacity: 0.24, // base opacity
blur: 24, // blur radius in px
speedSec: 30, // drift speed in seconds
},
showFps: false, // set true to show FPS overlay for perf checking
}, },
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.

107
src/ui.js
View File

@@ -423,6 +423,110 @@
} }
}; };
const hexToRgb = (hex) => {
if (typeof hex !== "string") return null;
const match = hex
.trim()
.toLowerCase()
.match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/);
if (!match) return null;
let value = match[1];
if (value.length === 3) {
value = value
.split("")
.map((c) => c + c)
.join("");
}
const num = parseInt(value, 16);
return {
r: (num >> 16) & 255,
g: (num >> 8) & 255,
b: num & 255,
};
};
const deriveOppositeColors = (palette = []) => {
const alphas = [0.4, 0.32, 0.24];
return palette
.slice(0, 3)
.map((c, idx) => {
const rgb = hexToRgb(c);
if (!rgb) return null;
const opp = {
r: 255 - rgb.r,
g: 255 - rgb.g,
b: 255 - rgb.b,
};
return `rgba(${opp.r}, ${opp.g}, ${opp.b}, ${alphas[idx] ?? 0.1})`;
})
.filter(Boolean);
};
const setBackdrop = (backdrop = {}, palette = []) => {
if (!sceneEl) return;
const derived =
Array.isArray(backdrop.colors) && backdrop.colors.length > 0
? backdrop.colors
: deriveOppositeColors(palette);
const colors =
derived && derived.length > 0
? derived
: [
"rgba(56, 189, 248, 0.08)",
"rgba(167, 139, 250, 0.07)",
"rgba(52, 211, 153, 0.06)",
];
if (backdrop.enabled === false) {
sceneEl.style.removeProperty("--backdrop-opacity");
sceneEl.style.removeProperty("--backdrop-blur");
sceneEl.style.removeProperty("--backdrop-speed");
sceneEl.style.removeProperty("--backdrop-color-a");
sceneEl.style.removeProperty("--backdrop-color-b");
sceneEl.style.removeProperty("--backdrop-color-c");
sceneEl.classList.remove("backdrop-enabled");
return;
}
const [
c1 = "rgba(56, 189, 248, 0.16)",
c2 = "rgba(167, 139, 250, 0.14)",
c3 = "rgba(52, 211, 153, 0.12)",
] = colors;
sceneEl.style.setProperty(
"--backdrop-opacity",
`${backdrop.opacity ?? 0.24}`,
);
sceneEl.style.setProperty("--backdrop-blur", `${backdrop.blur ?? 24}px`);
const speed = Math.max(8, backdrop.speedSec ?? 30);
sceneEl.style.setProperty("--backdrop-speed", `${speed}s`);
sceneEl.style.setProperty("--backdrop-color-a", c1);
sceneEl.style.setProperty("--backdrop-color-b", c2);
sceneEl.style.setProperty("--backdrop-color-c", c3);
sceneEl.classList.add("backdrop-enabled");
};
const fpsEl = document.createElement("div");
fpsEl.className = "fps-counter";
fpsEl.textContent = "FPS: —";
if (sceneEl) sceneEl.appendChild(fpsEl);
let fpsVisible = false;
const setFpsVisibility = (on) => {
fpsVisible = !!on;
fpsEl.classList.toggle("visible", fpsVisible);
};
let lastFpsUpdate = 0;
let frameCount = 0;
const updateFps = () => {
if (!fpsVisible) return;
frameCount += 1;
const now = performance.now();
if (now - lastFpsUpdate >= 500) {
const fps = Math.round((frameCount * 1000) / (now - lastFpsUpdate));
fpsEl.textContent = `FPS: ${fps}`;
frameCount = 0;
lastFpsUpdate = now;
}
};
const api = { const api = {
sceneEl, sceneEl,
updateHud, updateHud,
@@ -441,6 +545,9 @@
setMessageDefaults, setMessageDefaults,
clearMessages, clearMessages,
spawnClearEffects, spawnClearEffects,
setBackdrop,
setFpsVisibility,
updateFps,
}; };
return api; return api;
}; };

View File

@@ -105,18 +105,103 @@ header .pill {
position: relative; position: relative;
width: 100%; width: 100%;
height: 680px; height: 680px;
background: #0b1222;
overflow: hidden;
}
#scene-wrapper .fps-counter {
position: absolute;
top: 10px;
left: 10px;
padding: 6px 10px;
border-radius: 10px;
background: rgba(0, 0, 0, 0.35);
color: #e2e8f0;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.4px;
opacity: 0;
transform: translateY(-6px);
transition:
opacity 160ms ease,
transform 160ms ease;
pointer-events: none;
z-index: 5;
}
#scene-wrapper .fps-counter.visible {
opacity: 1;
transform: translateY(0);
}
#scene-wrapper::before,
#scene-wrapper::after {
content: "";
position: absolute;
inset: -30% -10%;
background: background:
radial-gradient( radial-gradient(
circle at 30% 30%, circle at 30% 30%,
rgba(255, 255, 255, 0.02), var(--backdrop-color-a, rgba(56, 189, 248, 0.045)),
transparent 40% transparent 50%
), ),
radial-gradient( radial-gradient(
circle at 75% 60%, circle at 70% 60%,
rgba(255, 255, 255, 0.02), var(--backdrop-color-b, rgba(167, 139, 250, 0.04)),
transparent 45% transparent 48%
), ),
#0b1222; radial-gradient(
circle at 50% 20%,
var(--backdrop-color-c, rgba(52, 211, 153, 0.035)),
transparent 45%
);
filter: blur(var(--backdrop-blur, 24px));
opacity: var(--backdrop-opacity, 0.24);
pointer-events: none;
transform: scale(1.05);
z-index: 0;
animation: floatBackdrop var(--backdrop-speed, 28s) ease-in-out infinite
alternate;
}
#scene-wrapper::after {
inset: -35% -15%;
background:
radial-gradient(
circle at 20% 60%,
var(--backdrop-color-a, rgba(14, 165, 233, 0.032)),
transparent 50%
),
radial-gradient(
circle at 80% 30%,
var(--backdrop-color-b, rgba(244, 114, 182, 0.032)),
transparent 50%
);
opacity: calc(var(--backdrop-opacity, 0.24) * 0.7);
animation: floatBackdropAlt calc(var(--backdrop-speed, 28s) * 1.1)
ease-in-out infinite alternate;
}
#scene-wrapper:not(.backdrop-enabled)::before,
#scene-wrapper:not(.backdrop-enabled)::after {
display: none;
}
@keyframes floatBackdrop {
0% {
transform: translate3d(0, 0, 0) scale(1.04);
}
50% {
transform: translate3d(-2%, -1%, 0) scale(1.07);
}
100% {
transform: translate3d(2%, 1.5%, 0) scale(1.05);
}
}
@keyframes floatBackdropAlt {
0% {
transform: translate3d(0, 0, 0) scale(1.03);
}
50% {
transform: translate3d(1.5%, -2%, 0) scale(1.06);
}
100% {
transform: translate3d(-1%, 2%, 0) scale(1.04);
}
} }
canvas { canvas {
display: block; display: block;
@@ -488,4 +573,9 @@ canvas {
header h1 { header h1 {
font-size: 18px; font-size: 18px;
} }
#scene-wrapper::before,
#scene-wrapper::after {
filter: blur(18px);
opacity: 0.6;
}
} }