diff --git a/src/config.js b/src/config.js index 7a5971e..ae9159a 100644 --- a/src/config.js +++ b/src/config.js @@ -30,6 +30,7 @@ endScale: 1.8, startOpacity: 0.95, endOpacity: 0, + sizeScale: 1, }, }, messages: { ...defaultMessageConfig }, diff --git a/src/scenes/scene-balanced.js b/src/scenes/scene-balanced.js index 46cabbb..1246a85 100644 --- a/src/scenes/scene-balanced.js +++ b/src/scenes/scene-balanced.js @@ -25,6 +25,14 @@ rope: true, renderType: "line", maxLengthMultiplier: 3.2, + clearAnimation: { + type: "pop", + durationMs: 220, + startScale: 0.9, + endScale: 4.4, + startOpacity: 0.66, + endOpacity: 0, + }, }, }, createBodies: (w, h) => { diff --git a/src/scenes/scene-lavalamp.js b/src/scenes/scene-lavalamp.js index dc2f0f1..fceb3de 100644 --- a/src/scenes/scene-lavalamp.js +++ b/src/scenes/scene-lavalamp.js @@ -55,6 +55,11 @@ rope: true, renderType: "line", maxLengthMultiplier: 5.8, + clearAnimation: { + type: "shatter", + durationMs: 380, + sizeScale: 1.2, + }, }, }, createBodies: (w, h) => { diff --git a/src/scenes/scene-stack-blocks.js b/src/scenes/scene-stack-blocks.js index b70ff84..7552899 100644 --- a/src/scenes/scene-stack-blocks.js +++ b/src/scenes/scene-stack-blocks.js @@ -36,6 +36,11 @@ rope: true, renderType: "line", maxLengthMultiplier: 2.5, + clearAnimation: { + type: "shatter", + durationMs: 380, + sizeScale: 2.2, + }, }, }, createBodies: (w, h) => { diff --git a/src/scenes/scene-template.js b/src/scenes/scene-template.js index b01ff0c..9418050 100644 --- a/src/scenes/scene-template.js +++ b/src/scenes/scene-template.js @@ -62,12 +62,13 @@ maxLinkLength: null, // optional absolute pixel cap for link reach clearAnimation: { // Visual “bling” when a chain clears. Defaults to a quick pop/fade. - type: "pop", // built-in default animation type + type: "pop", // built-in animation type ("pop" | "shatter") durationMs: 320, // total animation time startScale: 1, // initial scale multiplier endScale: 1.8, // final scale multiplier startOpacity: 0.95, // starting opacity endOpacity: 0, // ending opacity + sizeScale: 1, // multiplies the base size (radius) used for the visual render: null, // optional function({ targets, config, scene, ui }) to fully override rendering }, }, diff --git a/src/ui.js b/src/ui.js index cf959c1..4d65c2c 100644 --- a/src/ui.js +++ b/src/ui.js @@ -281,21 +281,20 @@ activeMessages.push(el); }; - const spawnClearEffects = (items = [], options = {}) => { - if (!clearFxEl || !Array.isArray(items) || items.length === 0) return; + const createPopEffect = (items, options) => { const { - type = "pop", durationMs = 320, startScale = 1, endScale = 1.8, startOpacity = 0.95, endOpacity = 0, + sizeScale = 1, } = options; items.forEach((item) => { if (!item) return; - const size = Math.max(4, (item.radius || 18) * 2); + const size = Math.max(4, (item.radius || 18) * 2 * sizeScale); const el = document.createElement("div"); - el.className = `clear-effect clear-effect--${type}`; + el.className = "clear-effect clear-effect--pop"; el.style.width = `${size}px`; el.style.height = `${size}px`; el.style.left = `${item.x}px`; @@ -314,6 +313,75 @@ }); }; + const createShatterEffect = (items, options) => { + const { + durationMs = 360, + sizeScale = 1, + shardCount = 6, + startOpacity = 0.94, + } = options; + items.forEach((item) => { + if (!item) return; + const base = Math.max(8, (item.radius || 18) * sizeScale); + for (let i = 0; i < shardCount; i += 1) { + const piece = document.createElement("div"); + piece.className = "clear-effect clear-effect--shatter"; + piece.style.width = `${base * 0.6}px`; + piece.style.height = `${base * 0.6}px`; + piece.style.left = `${item.x}px`; + piece.style.top = `${item.y}px`; + piece.style.background = item.color || "#38bdf8"; + piece.style.opacity = startOpacity; + piece.style.borderRadius = "14%"; + piece.style.transform = + "translate(-50%, -50%) scale(0.9) rotate(0deg)"; + clearFxEl.appendChild(piece); + const spread = base * 1.5; + const dx = (Math.random() - 0.5) * spread; + const dy = (Math.random() - 0.3) * spread; + const rot = (Math.random() - 0.5) * 260; + const scale = 0.9 + Math.random() * 0.5; + requestAnimationFrame(() => { + piece.style.transition = `transform ${durationMs}ms ease-out, opacity ${durationMs}ms ease-out`; + piece.style.transform = `translate(${dx}px, ${dy}px) scale(${scale}) rotate(${rot}deg)`; + piece.style.opacity = "0"; + }); + setTimeout(() => piece.remove(), durationMs + 80); + } + }); + }; + + const spawnClearEffects = (items = [], options = {}) => { + if (!clearFxEl || !Array.isArray(items) || items.length === 0) return; + const { + type = "pop", + durationMs = 320, + startScale = 1, + endScale = 1.8, + startOpacity = 0.95, + endOpacity = 0, + sizeScale = 1, + shardCount = 6, + } = options; + if (type === "shatter") { + createShatterEffect(items, { + durationMs, + sizeScale, + shardCount, + startOpacity, + }); + } else { + createPopEffect(items, { + durationMs, + startScale, + endScale, + startOpacity, + endOpacity, + sizeScale, + }); + } + }; + const api = { sceneEl, updateHud,