From 7a025ce68d4fa423a99bd71c367b88ad05b7e35c Mon Sep 17 00:00:00 2001 From: Daddy32 Date: Tue, 16 Dec 2025 21:33:01 +0100 Subject: [PATCH] Add scene backdrop controls, FPS display, and toned backdrops --- src/config.js | 8 +++ src/loop.js | 2 + src/main.js | 25 +++++++- src/scenes/scene-template.js | 8 +++ src/ui.js | 107 +++++++++++++++++++++++++++++++++++ styles.css | 102 +++++++++++++++++++++++++++++++-- 6 files changed, 245 insertions(+), 7 deletions(-) diff --git a/src/config.js b/src/config.js index 9a1bf89..f834d93 100644 --- a/src/config.js +++ b/src/config.js @@ -64,6 +64,14 @@ glowStrength: 0.55, gradientBlend: true, }, + backdrop: { + enabled: true, + colors: null, // optional array; defaults derived from palette + opacity: 0.19, + blur: 24, + speedSec: 30, + }, + showFps: true, }; return { diff --git a/src/loop.js b/src/loop.js index 676dfaa..b5bf0a7 100644 --- a/src/loop.js +++ b/src/loop.js @@ -12,6 +12,7 @@ getMaxLinkDistance, updateHud, checkWinCondition, + ui, }) => { const runSceneBeforeUpdateHook = () => { const currentScene = getCurrentScene(); @@ -297,6 +298,7 @@ Events.on(engine, "afterUpdate", keepBallsInBounds); Events.on(engine, "beforeUpdate", beforeUpdateStep); Events.on(render, "afterRender", drawLinkGlowAndSparkles); + Events.on(render, "afterRender", () => ui.updateFps && ui.updateFps()); }; window.PhysilinksLoop = { create }; diff --git a/src/main.js b/src/main.js index 886fdf5..05f8388 100644 --- a/src/main.js +++ b/src/main.js @@ -73,8 +73,27 @@ ...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 { link = {}, messages = {}, goalEffects = {}, ...rest } = sceneConfig; + const { + link = {}, + messages = {}, + goalEffects = {}, + backdrop = {}, + ...rest + } = sceneConfig; const base = defaults || {}; return { ...base, @@ -85,6 +104,7 @@ base.messages || defaultMessageConfig, ), goalEffects: normalizeGoalEffects(goalEffects, base.goalEffects), + backdrop: normalizeBackdrop(backdrop, base.backdrop), }; }; @@ -216,6 +236,8 @@ setSceneIdInUrl(next.id); const prevRadius = config.ballRadius; setConfigForScene(next.config); + ui.setBackdrop(config.backdrop, config.palette); + ui.setFpsVisibility(config.showFps); ui.setMessageDefaults(config.messages); resetEngineForScene(next.config, { prevRadius }); state.clearedCount = 0; @@ -525,6 +547,7 @@ getMaxLinkDistance, updateHud, checkWinCondition, + ui, }); window.addEventListener("resize", handleResize); diff --git a/src/scenes/scene-template.js b/src/scenes/scene-template.js index 2dd2e86..0226c74 100644 --- a/src/scenes/scene-template.js +++ b/src/scenes/scene-template.js @@ -149,6 +149,14 @@ glowStrength: 0.55, // opacity multiplier for glow 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) => { // Return an array of Matter bodies that make up scene obstacles/boundaries. diff --git a/src/ui.js b/src/ui.js index 2e463a0..d051599 100644 --- a/src/ui.js +++ b/src/ui.js @@ -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 = { sceneEl, updateHud, @@ -441,6 +545,9 @@ setMessageDefaults, clearMessages, spawnClearEffects, + setBackdrop, + setFpsVisibility, + updateFps, }; return api; }; diff --git a/styles.css b/styles.css index 2199908..ac0a48f 100644 --- a/styles.css +++ b/styles.css @@ -105,18 +105,103 @@ header .pill { position: relative; width: 100%; 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: radial-gradient( circle at 30% 30%, - rgba(255, 255, 255, 0.02), - transparent 40% + var(--backdrop-color-a, rgba(56, 189, 248, 0.045)), + transparent 50% ), radial-gradient( - circle at 75% 60%, - rgba(255, 255, 255, 0.02), - transparent 45% + circle at 70% 60%, + var(--backdrop-color-b, rgba(167, 139, 250, 0.04)), + 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 { display: block; @@ -488,4 +573,9 @@ canvas { header h1 { font-size: 18px; } + #scene-wrapper::before, + #scene-wrapper::after { + filter: blur(18px); + opacity: 0.6; + } }