Add floating goal messages
This commit is contained in:
@@ -24,6 +24,7 @@
|
|||||||
</header>
|
</header>
|
||||||
<div id="scene-wrapper">
|
<div id="scene-wrapper">
|
||||||
<div class="legend" id="palette-legend"></div>
|
<div class="legend" id="palette-legend"></div>
|
||||||
|
<div class="floating-messages" id="floating-messages"></div>
|
||||||
<div class="pause-overlay" id="pause-overlay">Paused</div>
|
<div class="pause-overlay" id="pause-overlay">Paused</div>
|
||||||
<div class="game-over" id="game-over">
|
<div class="game-over" id="game-over">
|
||||||
<div class="game-over__card">
|
<div class="game-over__card">
|
||||||
|
|||||||
54
src/main.js
54
src/main.js
@@ -38,6 +38,13 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const defaultMessageConfig = {
|
||||||
|
durationMs: 4200,
|
||||||
|
position: { xPercent: 50, yPercent: 10 },
|
||||||
|
text: null,
|
||||||
|
colors: null,
|
||||||
|
};
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
gravity: 1,
|
gravity: 1,
|
||||||
spawnIntervalMs: 520,
|
spawnIntervalMs: 520,
|
||||||
@@ -54,6 +61,7 @@
|
|||||||
renderType: "line",
|
renderType: "line",
|
||||||
maxLengthMultiplier: 3.1,
|
maxLengthMultiplier: 3.1,
|
||||||
},
|
},
|
||||||
|
messages: { ...defaultMessageConfig },
|
||||||
};
|
};
|
||||||
|
|
||||||
const ui = window.PhysilinksUI.create();
|
const ui = window.PhysilinksUI.create();
|
||||||
@@ -167,7 +175,31 @@
|
|||||||
setSceneIdInUrl(next.id);
|
setSceneIdInUrl(next.id);
|
||||||
const prevRadius = config.ballRadius;
|
const prevRadius = config.ballRadius;
|
||||||
Object.assign(config, next.config);
|
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 =
|
engine.gravity.scale =
|
||||||
typeof next.config.gravityScale === "number"
|
typeof next.config.gravityScale === "number"
|
||||||
? next.config.gravityScale
|
? next.config.gravityScale
|
||||||
@@ -453,6 +485,7 @@
|
|||||||
spawnInitialBurst();
|
spawnInitialBurst();
|
||||||
startSpawner();
|
startSpawner();
|
||||||
}
|
}
|
||||||
|
announceGoalMessage();
|
||||||
};
|
};
|
||||||
|
|
||||||
const setHighlight = (body, on) => {
|
const setHighlight = (body, on) => {
|
||||||
@@ -987,6 +1020,25 @@
|
|||||||
return null;
|
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 clampBodiesIntoView = (prevWidth, prevHeight) => {
|
||||||
const scaleX = width / (prevWidth || width);
|
const scaleX = width / (prevWidth || width);
|
||||||
const scaleY = height / (prevHeight || height);
|
const scaleY = height / (prevHeight || height);
|
||||||
|
|||||||
@@ -19,6 +19,9 @@
|
|||||||
blobBalls: false,
|
blobBalls: false,
|
||||||
noGameOver: true,
|
noGameOver: true,
|
||||||
relaxMode: true,
|
relaxMode: true,
|
||||||
|
messages: {
|
||||||
|
position: { xPercent: 50, yPercent: 16 },
|
||||||
|
},
|
||||||
winCondition: {
|
winCondition: {
|
||||||
type: "timer",
|
type: "timer",
|
||||||
durationSec: 120,
|
durationSec: 120,
|
||||||
|
|||||||
67
src/ui.js
67
src/ui.js
@@ -1,6 +1,7 @@
|
|||||||
(() => {
|
(() => {
|
||||||
const create = () => {
|
const create = () => {
|
||||||
const sceneEl = document.getElementById("scene-wrapper");
|
const sceneEl = document.getElementById("scene-wrapper");
|
||||||
|
const floatingMessagesEl = document.getElementById("floating-messages");
|
||||||
const activeColorEl = document.getElementById("active-color");
|
const activeColorEl = document.getElementById("active-color");
|
||||||
const chainLenEl = document.getElementById("chain-length");
|
const chainLenEl = document.getElementById("chain-length");
|
||||||
const spawnRateEl = document.getElementById("spawn-rate");
|
const spawnRateEl = document.getElementById("spawn-rate");
|
||||||
@@ -20,6 +21,10 @@
|
|||||||
const winMessageEl = document.getElementById("win-message");
|
const winMessageEl = document.getElementById("win-message");
|
||||||
const winNextBtn = document.getElementById("win-next");
|
const winNextBtn = document.getElementById("win-next");
|
||||||
const winRestartBtn = document.getElementById("win-restart");
|
const winRestartBtn = document.getElementById("win-restart");
|
||||||
|
let messageDefaults = {
|
||||||
|
durationMs: 4200,
|
||||||
|
position: { xPercent: 50, yPercent: 12 },
|
||||||
|
};
|
||||||
|
|
||||||
const handlers = {
|
const handlers = {
|
||||||
onPauseToggle: null,
|
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 {
|
return {
|
||||||
sceneEl,
|
sceneEl,
|
||||||
updateHud,
|
updateHud,
|
||||||
@@ -210,6 +275,8 @@
|
|||||||
setSceneSelection,
|
setSceneSelection,
|
||||||
setHandlers,
|
setHandlers,
|
||||||
setGoal,
|
setGoal,
|
||||||
|
showFloatingMessage,
|
||||||
|
setMessageDefaults,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
33
styles.css
33
styles.css
@@ -195,6 +195,39 @@ canvas {
|
|||||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
display: inline-block;
|
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 {
|
.pause-overlay {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 12px;
|
top: 12px;
|
||||||
|
|||||||
Reference in New Issue
Block a user