From 0ef5644d1df034ba74e8a884410ed50badc28e09 Mon Sep 17 00:00:00 2001 From: Daddy32 Date: Sun, 14 Dec 2025 13:06:56 +0100 Subject: [PATCH] Add floating goal messages --- index.html | 1 + src/main.js | 54 ++++++++++++++++++++++++++++++- src/scenes/scene-relax.js | 3 ++ src/ui.js | 67 +++++++++++++++++++++++++++++++++++++++ styles.css | 33 +++++++++++++++++++ 5 files changed, 157 insertions(+), 1 deletion(-) diff --git a/index.html b/index.html index cd3583d..69bdee6 100644 --- a/index.html +++ b/index.html @@ -24,6 +24,7 @@
+
Paused
diff --git a/src/main.js b/src/main.js index 4050406..b825d7a 100644 --- a/src/main.js +++ b/src/main.js @@ -38,6 +38,13 @@ } }; + const defaultMessageConfig = { + durationMs: 4200, + position: { xPercent: 50, yPercent: 10 }, + text: null, + colors: null, + }; + const config = { gravity: 1, spawnIntervalMs: 520, @@ -54,6 +61,7 @@ renderType: "line", maxLengthMultiplier: 3.1, }, + messages: { ...defaultMessageConfig }, }; const ui = window.PhysilinksUI.create(); @@ -167,7 +175,31 @@ setSceneIdInUrl(next.id); const prevRadius = config.ballRadius; Object.assign(config, next.config); - config.link = { ...next.config.link }; + config.link = { ...(next.config?.link || {}) }; + const sceneMessages = next.config?.messages || {}; + config.messages = { + durationMs: Number.isFinite(sceneMessages.durationMs) + ? sceneMessages.durationMs + : defaultMessageConfig.durationMs, + text: + typeof sceneMessages.text === "string" && sceneMessages.text.trim() + ? sceneMessages.text.trim() + : defaultMessageConfig.text, + position: { + xPercent: + typeof sceneMessages.position?.xPercent === "number" + ? sceneMessages.position.xPercent + : defaultMessageConfig.position.xPercent, + yPercent: + typeof sceneMessages.position?.yPercent === "number" + ? sceneMessages.position.yPercent + : defaultMessageConfig.position.yPercent, + }, + colors: Array.isArray(sceneMessages.colors) + ? sceneMessages.colors + : defaultMessageConfig.colors, + }; + ui.setMessageDefaults(config.messages); engine.gravity.scale = typeof next.config.gravityScale === "number" ? next.config.gravityScale @@ -453,6 +485,7 @@ spawnInitialBurst(); startSpawner(); } + announceGoalMessage(); }; const setHighlight = (body, on) => { @@ -987,6 +1020,25 @@ return null; }; + const announceGoalMessage = () => { + const goal = getGoalState(); + const text = + config.messages?.text || + (goal && goal.label && goal.label !== "—" ? goal.label : null); + if (!text) return; + const colors = + (Array.isArray(config.messages?.colors) && config.messages.colors) || + goal?.colors || + null; + ui.showFloatingMessage( + { text, colors }, + { + durationMs: config.messages.durationMs, + position: config.messages.position, + }, + ); + }; + const clampBodiesIntoView = (prevWidth, prevHeight) => { const scaleX = width / (prevWidth || width); const scaleY = height / (prevHeight || height); diff --git a/src/scenes/scene-relax.js b/src/scenes/scene-relax.js index 5ff7907..9a6cb6b 100644 --- a/src/scenes/scene-relax.js +++ b/src/scenes/scene-relax.js @@ -19,6 +19,9 @@ blobBalls: false, noGameOver: true, relaxMode: true, + messages: { + position: { xPercent: 50, yPercent: 16 }, + }, winCondition: { type: "timer", durationSec: 120, diff --git a/src/ui.js b/src/ui.js index 5de8824..6a139ef 100644 --- a/src/ui.js +++ b/src/ui.js @@ -1,6 +1,7 @@ (() => { const create = () => { const sceneEl = document.getElementById("scene-wrapper"); + const floatingMessagesEl = document.getElementById("floating-messages"); const activeColorEl = document.getElementById("active-color"); const chainLenEl = document.getElementById("chain-length"); const spawnRateEl = document.getElementById("spawn-rate"); @@ -20,6 +21,10 @@ const winMessageEl = document.getElementById("win-message"); const winNextBtn = document.getElementById("win-next"); const winRestartBtn = document.getElementById("win-restart"); + let messageDefaults = { + durationMs: 4200, + position: { xPercent: 50, yPercent: 12 }, + }; const handlers = { onPauseToggle: null, @@ -196,6 +201,66 @@ }); }; + const setMessageDefaults = (overrides = {}) => { + messageDefaults = { + ...messageDefaults, + ...overrides, + position: { + ...messageDefaults.position, + ...(overrides.position || {}), + }, + }; + }; + + const renderFloatingMessage = (el, text, colors) => { + el.innerHTML = ""; + if (Array.isArray(colors) && colors.length > 0) { + colors.forEach((color) => { + const swatch = document.createElement("span"); + swatch.style.background = color; + swatch.style.display = "inline-block"; + swatch.style.width = "14px"; + swatch.style.height = "14px"; + swatch.style.borderRadius = "50%"; + swatch.style.border = "1px solid rgba(255,255,255,0.2)"; + swatch.style.marginRight = "6px"; + el.appendChild(swatch); + }); + } + const textSpan = document.createElement("span"); + textSpan.textContent = text; + el.appendChild(textSpan); + }; + + const showFloatingMessage = (message, options = {}) => { + if (!floatingMessagesEl) return; + const msgObj = + typeof message === "string" ? { text: message } : message || {}; + const text = (msgObj.text || "").trim(); + if (!text) return; + const colors = Array.isArray(msgObj.colors) ? msgObj.colors : null; + const durationMs = Number.isFinite(options.durationMs) + ? options.durationMs + : messageDefaults.durationMs; + const position = { + ...messageDefaults.position, + ...(options.position || {}), + }; + const el = document.createElement("div"); + el.className = "floating-message"; + renderFloatingMessage(el, text, colors); + el.style.left = `${position.xPercent ?? 50}%`; + el.style.top = `${position.yPercent ?? 10}%`; + floatingMessagesEl.appendChild(el); + requestAnimationFrame(() => { + el.classList.add("visible"); + }); + setTimeout(() => { + el.classList.remove("visible"); + setTimeout(() => el.remove(), 260); + }, durationMs); + }; + return { sceneEl, updateHud, @@ -210,6 +275,8 @@ setSceneSelection, setHandlers, setGoal, + showFloatingMessage, + setMessageDefaults, }; }; diff --git a/styles.css b/styles.css index 58e9191..527e6c8 100644 --- a/styles.css +++ b/styles.css @@ -195,6 +195,39 @@ canvas { border: 1px solid rgba(255, 255, 255, 0.12); display: inline-block; } +.floating-messages { + position: absolute; + inset: 0; + pointer-events: none; +} +.floating-message { + position: absolute; + display: inline-flex; + align-items: center; + gap: 8px; + transform: translate(-50%, 0) scale(0.98); + background: rgba(15, 23, 42, 0.76); + color: #f8fafc; + padding: 12px 16px; + border-radius: 14px; + border: 1px solid rgba(226, 232, 240, 0.16); + font-weight: 800; + letter-spacing: 0.4px; + text-shadow: + 0 10px 30px rgba(0, 0, 0, 0.45), + 0 2px 6px rgba(0, 0, 0, 0.35); + box-shadow: 0 18px 45px rgba(0, 0, 0, 0.35); + backdrop-filter: blur(6px); + opacity: 0; + transition: + opacity 220ms ease, + transform 220ms ease; + filter: drop-shadow(0 12px 30px rgba(0, 0, 0, 0.2)); +} +.floating-message.visible { + opacity: 1; + transform: translate(-50%, 0) scale(1); +} .pause-overlay { position: absolute; top: 12px;