Add scene backdrop controls, FPS display, and toned backdrops
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
25
src/main.js
25
src/main.js
@@ -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);
|
||||||
|
|||||||
@@ -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
107
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 = {
|
const api = {
|
||||||
sceneEl,
|
sceneEl,
|
||||||
updateHud,
|
updateHud,
|
||||||
@@ -441,6 +545,9 @@
|
|||||||
setMessageDefaults,
|
setMessageDefaults,
|
||||||
clearMessages,
|
clearMessages,
|
||||||
spawnClearEffects,
|
spawnClearEffects,
|
||||||
|
setBackdrop,
|
||||||
|
setFpsVisibility,
|
||||||
|
updateFps,
|
||||||
};
|
};
|
||||||
return api;
|
return api;
|
||||||
};
|
};
|
||||||
|
|||||||
102
styles.css
102
styles.css
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user