The art refresh: textures, tokens, conjurations, and cel flourishes

Eric repainted the maze. The first-person materials are painterly
sandstone, flagstone, and iron; the hand-drawn token set is redrawn
with matching chests and hats; the standing conjurations — hedges,
jello, dust, safe, warp light — and the effect billboards are redone
in the same hand; and the board's flourishes trade their filters and
gradients for flat cel silhouettes cut from five shared shapes, which
read better against the counters and cost a phone nothing. Thirty
scene goldens re-blessed against the new walls.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jm2auWk6RP71CjaAb4FMoG
This commit is contained in:
Eric Wagoner
2026-09-14 20:28:52 -04:00
co-authored by Claude Fable 5.1
parent 3fc9df85ac
commit 8dfd0e0c29
121 changed files with 533 additions and 980 deletions
+4 -12
View File
@@ -3,22 +3,14 @@
import { center } from "./geom";
let { fx }: { fx: FxOf<"absorb"> } = $props();
const uid = $props.id();
const c = $derived(center(fx.at));
</script>
<defs>
<linearGradient id={`absorb-shine-${uid}`} x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#fffdf4" />
<stop offset="0.5" stop-color="#f6f0df" />
<stop offset="1" stop-color="#d8ccb0" />
</linearGradient>
</defs>
<circle cx={c.x} cy={c.y} r="14" class="pull" />
<rect x={c.x - 9} y={c.y - 13} width="18" height="26" rx="2"
fill={`url(#absorb-shine-${uid})`} class="card"
style={`transform-origin: ${c.x}px ${c.y}px`} />
<g class="card" style={`transform-origin: ${c.x}px ${c.y}px`}>
<path d={`M ${c.x-9} ${c.y-13} l 18 1 -1 25 -17 -1 Z`} fill="#f6f0df" />
<path d={`M ${c.x-5} ${c.y-8} l 10 0 -1 15 -9 1 Z M ${c.x} ${c.y-5} l 3 5 -3 5 -3 -5 Z`} fill="none" stroke="#967c55" stroke-width="0.8" />
</g>
<style>
.card {
+14 -20
View File
@@ -2,38 +2,32 @@
import type { FxOf } from "../fx";
let { fx }: { fx: FxOf<"bolt"> } = $props();
const uid = $props.id();
/** A fresh jagged path every strike, plus two forks off mid-joints. */
/** A repeatable jagged path per effect, plus two forks off mid-joints. */
const geom = $derived.by(() => {
const a = fx.a, b = fx.b;
let seed = (fx.id ^ Math.round(a.x * 31 + a.y * 17 + b.x * 13 + b.y)) >>> 0;
const random = () => { seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0; return seed / 4294967296; };
const segs = 6;
const joints: { x: number; y: number }[] = [a];
for (let i = 1; i < segs; i++) {
const t = i / segs;
joints.push({
x: a.x + (b.x - a.x) * t + (Math.random() - 0.5) * 16,
y: a.y + (b.y - a.y) * t + (Math.random() - 0.5) * 16,
x: a.x + (b.x - a.x) * t + (random() - 0.5) * 16,
y: a.y + (b.y - a.y) * t + (random() - 0.5) * 16,
});
}
joints.push(b);
const points = joints.map((p) => `${p.x.toFixed(1)},${p.y.toFixed(1)}`).join(" ");
const fork = (j: { x: number; y: number }) => {
const dx = (Math.random() - 0.5) * 26, dy = (Math.random() - 0.5) * 26;
return `M ${j.x} ${j.y} l ${dx / 2} ${dy / 2} l ${dx / 2 + (Math.random() - 0.5) * 8} ${dy / 2}`;
const dx = (random() - 0.5) * 26, dy = (random() - 0.5) * 26;
return `M ${j.x} ${j.y} l ${dx / 2} ${dy / 2} l ${dx / 2 + (random() - 0.5) * 8} ${dy / 2}`;
};
return { points, forks: [fork(joints[2]!), fork(joints[4]!)] };
});
</script>
<defs>
<filter id={`bolt-glow-${uid}`} x="-60%" y="-60%" width="220%" height="220%">
<feGaussianBlur stdDeviation="4" result="blur" />
<feMerge><feMergeNode in="blur" /><feMergeNode in="SourceGraphic" /></feMerge>
</filter>
</defs>
<g class="bolt" filter={`url(#bolt-glow-${uid})`}>
<g class="bolt">
<polyline points={geom.points} class="wide" />
{#each geom.forks as d, i (i)}
<path {d} class="fork" />
@@ -45,21 +39,21 @@
.bolt .wide {
fill: none;
stroke: #e6a812;
stroke-width: 5.5;
stroke-linejoin: round;
opacity: 0.75;
stroke-width: 6.5;
stroke-linejoin: bevel;
opacity: 1;
}
.bolt .fork {
fill: none;
stroke: #f5d54a;
stroke-width: 1.6;
stroke-linejoin: round;
stroke-width: 2.1;
stroke-linejoin: bevel;
}
.bolt .core {
fill: none;
stroke: #fffdf0;
stroke-width: 2;
stroke-linejoin: round;
stroke-linejoin: bevel;
}
.bolt { animation: bolt-flicker 0.5s steps(2, jump-none) forwards; }
@keyframes bolt-flicker {
+8 -19
View File
@@ -1,29 +1,20 @@
<script lang="ts">
import type { FxOf } from "../fx";
import { center } from "./geom";
import { center, impact, drop } from "./geom";
let { fx }: { fx: FxOf<"burst"> } = $props();
const uid = $props.id();
const c = $derived(center(fx.at));
const EMBERS = [15, 80, 145, 210, 275, 340];
</script>
<defs>
<radialGradient id={`burst-fire-${uid}`}>
<stop offset="0" stop-color="#fffde0" />
<stop offset="0.3" stop-color="#ffd34e" />
<stop offset="0.65" stop-color="#ff8c1a" stop-opacity="0.85" />
<stop offset="1" stop-color="#ef3b08" stop-opacity="0" />
</radialGradient>
</defs>
<circle cx={c.x} cy={c.y} r="16" fill={`url(#burst-fire-${uid})`} class="bloom" />
<circle cx={c.x} cy={c.y} r="6" class="shockwave" />
<g class="bloom" style={`transform-origin: ${c.x}px ${c.y}px`}>
<path d={`M ${c.x-14} ${c.y+3} q -10 -12 1 -14 q -1 7 6 5 q -7 -16 4 -16 q -2 8 5 9 q 6 -11 12 -3 q -8 1 -3 7 q 14 -3 12 7 q -7 -5 -10 2 q 8 10 -2 13 q 0 -8 -6 -7 q -7 10 -13 3 q 6 -1 3 -6 q -11 7 -13 0 Z`} fill="#d55224" stroke="#833521" stroke-width="0.6" />
<path d={impact(c.x,c.y,15)} fill="#ffb83f" />
<path d={impact(c.x,c.y,7)} fill="#fff0b8" />
</g>
<path d={`M ${c.x-6} ${c.y-1} q 2 -6 7 -4 M ${c.x+5} ${c.y+1} q -2 5 -6 4`} class="shockwave" />
{#each EMBERS as deg (deg)}
<circle
cx={c.x + 13 * Math.cos((deg * Math.PI) / 180)}
cy={c.y + 13 * Math.sin((deg * Math.PI) / 180)}
r={deg % 2 === 0 ? 2 : 1.5}
<path d={drop(c.x + 13 * Math.cos((deg * Math.PI) / 180), c.y + 13 * Math.sin((deg * Math.PI) / 180), deg % 2 === 0 ? 2 : 1.5)}
class="ember"
style={`transform-origin: ${c.x}px ${c.y}px`}
/>
@@ -31,8 +22,6 @@
<style>
.bloom {
transform-box: fill-box;
transform-origin: center;
animation: bloom 0.5s ease-out forwards;
}
.shockwave {
@@ -26,11 +26,12 @@
.ring {
fill: none;
stroke-width: 4;
stroke-dasharray: 30 22;
stroke-linecap: round;
stroke-dasharray: 37 14 12 21;
}
.ring.inner { stroke: #b48ae0; }
.ring.mid { stroke: #8a5fc0; stroke-dasharray: 44 30; }
.ring.outer { stroke: #e2c8ff; stroke-dasharray: 60 40; }
.ring.mid { stroke: #8a5fc0; stroke-dasharray: 51 19 16 31; }
.ring.outer { stroke: #e2c8ff; stroke-dasharray: 67 29 22 42; }
.slip {
fill: #f6f0df;
stroke: #43331f;
+2 -11
View File
@@ -3,7 +3,6 @@
import { center } from "./geom";
let { fx }: { fx: FxOf<"claw"> } = $props();
const uid = $props.id();
const c = $derived(center(fx.at));
const GASHES = [
{ x: -12, cls: "g0" },
@@ -12,20 +11,12 @@
];
</script>
<defs>
<linearGradient id={`claw-cut-${uid}`} x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#e0604f" />
<stop offset="0.5" stop-color="#b3372b" />
<stop offset="1" stop-color="#6e1a12" />
</linearGradient>
</defs>
<g class="claw">
{#each GASHES as g (g.cls)}
<g class={`gash ${g.cls}`} style={`transform-origin: ${c.x + g.x}px ${c.y - 15}px`}>
<path
d={`M ${c.x + g.x} ${c.y - 14} q 4 12 10 26 q -1 1 -2 0 q -7 -13 -11 -25 z`}
fill={`url(#claw-cut-${uid})`}
d={`M ${c.x + g.x} ${c.y - 14} q 0 9 10 26 q -5 -3 -7 -10 q -7 -10 -3 -16 z`}
fill="#b3372b" stroke="#6e1a12" stroke-width="0.65"
/>
<path
d={`M ${c.x + g.x} ${c.y - 14} q 4 12 10 26`}
+24 -7
View File
@@ -17,12 +17,21 @@
<ellipse cx={c.x} cy={c.y + 8} rx="16" ry="7" class="die-shadow" />
<g class="die" style={`transform-origin: ${c.x}px ${c.y}px`}>
<g transform={`translate(${c.x} ${c.y})`}>
<rect x="-15" y="-15" width="30" height="30" rx="6" class="die-body" />
<!-- a four: the drift roll made manifest -->
<circle cx="-7" cy="-7" r="3.2" class="pip" />
<circle cx="7" cy="-7" r="3.2" class="pip" />
<circle cx="-7" cy="7" r="3.2" class="pip" />
<circle cx="7" cy="7" r="3.2" class="pip" />
<!-- A tetrahedral d4: two triangular faces share the apex-to-front edge. -->
<path d="M 0 -19 L -18 10 -2 17 17 9 Z" class="die-body" />
<path d="M 0 -19 L -2 17 17 9 Z" fill="#c4b590" />
<path d="M 0 -19 L -2 17" class="ridge" />
<path d="M -1 -14 L -14 8" fill="none" stroke="#fff7df" stroke-width="1.4" />
<!-- Apex-reading numerals: both faces share 4 at the top and 3 at the
front base vertex; the other base corners carry 1 and 2. -->
<g class="numerals">
<path d="M -3 -10 L -5 -6 -1.5 -6 M -2.5 -10 L -2.5 -3" />
<path d="M 3 -9 L 1.5 -5 5 -5 M 4 -9 L 4 -2" />
<path d="M -14 6 L -12 4 -12 10 M -14 10 L -10 11" />
<path d="M 9 5 Q 11 2 13 5 Q 14 7 9 10 L 14 9" />
<path d="M -8 10 Q -3 9 -6 12 Q -2 14 -7 14" />
<path d="M 2 10 Q 6 8 4 11 Q 7 12 2 14" />
</g>
</g>
</g>
<!-- Impact: ring and flung grit -->
@@ -68,8 +77,16 @@
fill: #efe8d4;
stroke: #2b2218;
stroke-width: 2.5;
stroke-linejoin: round;
}
.ridge { fill: none; stroke: #7b694e; stroke-width: 1.2; }
.numerals {
fill: none;
stroke: #2b2218;
stroke-width: 1.05;
stroke-linecap: round;
stroke-linejoin: round;
}
.pip { fill: #2b2218; }
.impact {
fill: none;
stroke: #7c6a4f;
+10 -17
View File
@@ -1,9 +1,8 @@
<script lang="ts">
import type { FxOf } from "../fx";
import { CELL, center } from "./geom";
import { CELL, center, puff } from "./geom";
let { fx }: { fx: FxOf<"dust-puff" | "edge-dust"> } = $props();
const uid = $props.id();
const c = $derived.by(() => {
if (fx.kind === "dust-puff") return center(fx.at);
const mid = center(fx.cell);
@@ -20,26 +19,19 @@
</script>
<g style={`--ax: ${away.x}px; --ay: ${away.y}px; --bx: ${-away.x}px; --by: ${away.y}px`}>
<circle cx={c.x - 6} cy={c.y} r="7" fill={`url(#dust-soft-${uid})`} class="puff" />
<circle cx={c.x + 5} cy={c.y - 2} r="5" fill={`url(#dust-soft-${uid})`} class="puff back late" />
<circle cx={c.x} cy={c.y + 3} r="6" fill={`url(#dust-soft-${uid})`} class="puff later" />
<path d={puff(c.x - 6, c.y, (7) * 1)} fill="#b7a786" class="puff" />
<path d={puff(c.x + 5, c.y - 2, (5) * 1)} fill="#b7a786" class="puff back late" />
<path d={puff(c.x, c.y + 3, (6) * 1)} fill="#b7a786" class="puff later" />
<g class="grit">
<circle cx={c.x - 4} cy={c.y - 5} r="1.3" />
<circle cx={c.x + 6} cy={c.y + 2} r="1.6" />
<circle cx={c.x + 1} cy={c.y - 2} r="0.9" />
<path d={puff(c.x - 4, c.y - 5, (1.3) * 1)} />
<path d={puff(c.x + 6, c.y + 2, (1.6) * 1)} />
<path d={puff(c.x + 1, c.y - 2, (0.9) * 1)} />
</g>
</g>
<defs>
<radialGradient id={`dust-soft-${uid}`}>
<stop offset="0" stop-color="#96876a" stop-opacity="1" />
<stop offset="0.75" stop-color="#7d7358" stop-opacity="0.7" />
<stop offset="1" stop-color="#8d8266" stop-opacity="0" />
</radialGradient>
</defs>
<style>
.puff {
stroke: #76664d; stroke-width: 0.65;
transform-box: fill-box;
transform-origin: center;
animation: blow-a 0.8s ease-out forwards;
@@ -47,7 +39,7 @@
.puff.back { animation-name: blow-b; }
.puff.late { animation-delay: 0.08s; }
.puff.later { animation-delay: 0.16s; }
.grit circle { fill: #4a4232; animation: grit-fall 0.6s ease-in forwards; }
.grit path { fill: #4a4232; animation: grit-fall 0.6s ease-in forwards; }
@keyframes blow-a {
0% { opacity: 1; transform: translate(0, 0) scale(0.8); }
100% { opacity: 0; transform: translate(var(--ax), var(--ay)) scale(2); }
@@ -60,4 +52,5 @@
0% { opacity: 1; transform: translateY(-3px); }
100% { opacity: 0; transform: translateY(8px); }
}
.grit { transform-box: fill-box; transform-origin: center; }
</style>
+8 -77
View File
@@ -2,90 +2,21 @@
import type { FxOf } from "../fx";
let { fx }: { fx: FxOf<"fireball"> } = $props();
const uid = $props.id();
const a = $derived(fx.a);
const b = $derived(fx.b);
</script>
<defs>
<radialGradient id={`fireball-core-${uid}`}>
<stop offset="0" stop-color="#fffde0" />
<stop offset="0.28" stop-color="#fff09a" />
<stop offset="0.62" stop-color="#ffad22" />
<stop offset="1" stop-color="#ef3b08" stop-opacity="0.15" />
</radialGradient>
<linearGradient id={`fireball-tail-${uid}`} x1="1" x2="0">
<stop offset="0" stop-color="#fff2a1" stop-opacity="0.95" />
<stop offset="0.2" stop-color="#ff9d16" stop-opacity="0.86" />
<stop offset="0.62" stop-color="#ee3907" stop-opacity="0.42" />
<stop offset="1" stop-color="#581002" stop-opacity="0" />
</linearGradient>
<filter id={`fireball-warp-${uid}`} x="-60%" y="-160%" width="230%" height="420%">
<feTurbulence type="fractalNoise" baseFrequency="0.022 0.12" numOctaves="3" seed="7" result="noise">
<animate
attributeName="baseFrequency"
values="0.022 0.12; 0.035 0.17; 0.018 0.1"
dur="0.22s"
repeatCount="indefinite"
/>
</feTurbulence>
<feDisplacementMap in="SourceGraphic" in2="noise" scale="14" xChannelSelector="R" yChannelSelector="B" />
<feGaussianBlur stdDeviation="1.4" />
</filter>
<filter id={`fireball-glow-${uid}`} x="-180%" y="-180%" width="460%" height="460%">
<feGaussianBlur stdDeviation="7" result="blur" />
<feMerge>
<feMergeNode in="blur" />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
</defs>
<g class="fireball">
<g>
<!-- Outer orange glow -->
<ellipse cx="-4" cy="0" rx="27" ry="16" fill="#ff5b0a" opacity="0.32" filter={`url(#fireball-glow-${uid})`} />
<!-- Turbulent outer flame -->
<path
d="M -8 -13 C -30 -18, -52 -8, -91 -3 C -60 1, -68 14, -98 20 C -54 17, -28 11, -7 12 Z"
fill={`url(#fireball-tail-${uid})`}
filter={`url(#fireball-warp-${uid})`}
/>
<!-- Hot inner flame -->
<path
d="M -6 -8 C -24 -10, -42 -4, -64 1 C -39 2, -32 8, -10 8 Z"
fill="#ffd34e"
opacity="0.74"
filter={`url(#fireball-warp-${uid})`}
/>
<!-- Fireball body -->
<circle r="15" fill="#ff5c09" opacity="0.72" filter={`url(#fireball-glow-${uid})`} />
<circle r="12" fill={`url(#fireball-core-${uid})`} />
<!-- White-hot highlight -->
<ellipse cx="4" cy="-3" rx="6" ry="5" fill="#fffde7" opacity="0.95" />
<!-- Sparks -->
<g fill="#ff9d19">
<circle cx="-28" cy="-17" r="2.2">
<animate attributeName="cy" values="-17; -23; -17" dur="0.16s" repeatCount="indefinite" />
</circle>
<circle cx="-46" cy="12" r="1.6">
<animate attributeName="cy" values="12; 19; 12" dur="0.2s" repeatCount="indefinite" />
</circle>
<circle cx="-70" cy="-7" r="1.4">
<animate attributeName="opacity" values="1; 0.1; 1" dur="0.14s" repeatCount="indefinite" />
</circle>
</g>
<g stroke-linejoin="round">
<path d="M 9 -10 C -5 -21 -18 -12 -32 -19 Q -28 -11 -20 -8 Q -48 -16 -71 -4 Q -48 -7 -41 -1 Q -67 1 -80 12 Q -43 5 -26 12 Q -12 21 8 11 Q 20 2 9 -10 Z" fill="#b33720" stroke="#702b20" stroke-width="1" />
<path d="M 10 -8 Q -2 -16 -15 -10 Q -12 -5 -7 -6 Q -24 -11 -43 -6 L -32 -1 Q -45 0 -56 6 Q -26 1 -18 9 Q -5 17 10 8 Q 17 0 10 -8 Z" fill="#ef681e">
<animate attributeName="d" calcMode="discrete" values="M 10 -8 Q -2 -16 -15 -10 Q -12 -5 -7 -6 Q -24 -11 -43 -6 L -32 -1 Q -45 0 -56 6 Q -26 1 -18 9 Q -5 17 10 8 Q 17 0 10 -8 Z;M 10 -8 Q -2 -16 -15 -10 Q -18 -6 -7 -6 Q -22 -15 -43 -6 L -27 0 Q -42 4 -56 6 Q -24 4 -18 9 Q -5 17 10 8 Q 17 0 10 -8 Z;M 10 -8 Q -2 -16 -15 -10 Q -12 -5 -7 -6 Q -24 -11 -43 -6 L -32 -1 Q -45 0 -56 6 Q -26 1 -18 9 Q -5 17 10 8 Q 17 0 10 -8 Z" dur="0.24s" repeatCount="indefinite" />
</path>
<path d="M 9 -7 Q 0 -13 -8 -7 Q 0 -7 -4 -1 Q -11 2 -20 -2 Q -17 8 -6 10 Q 8 13 12 3 Q 14 -3 9 -7 Z" fill="#ffd34e" />
<path d="M 8 -5 Q 3 -8 1 -4 Q 6 -3 0 3 Q 8 7 10 1 Z" fill="#fff3bd" />
<path d="M -38 -13 q -5 -10 -10 -7 q 1 7 10 7 Z M -61 12 q -8 -1 -11 7 q 8 0 11 -7 Z" fill="#ff9d19" />
</g>
<animateMotion dur="0.42s" fill="freeze" rotate="auto" path={`M ${a.x} ${a.y} L ${b.x} ${b.y}`} />
</g>
+4 -4
View File
@@ -1,6 +1,6 @@
<script lang="ts">
import type { FxOf } from "../fx";
import { center } from "./geom";
import { center, glint } from "./geom";
let { fx }: { fx: FxOf<"fireworks"> } = $props();
const c = $derived(center(fx.at));
@@ -29,14 +29,13 @@
class={`trail ${ray.cls}`}
style={`transform-origin: ${c.x}px ${c.y}px`}
/>
<circle
cx={c.x + (ray.len + 1) * dx} cy={c.y + (ray.len + 1) * dy} r={ray.r}
<path d={glint(c.x + (ray.len + 1) * dx, c.y + (ray.len + 1) * dy, (ray.r) * 1.6)}
fill={COLORS[i % 4]}
class={`spark ${ray.cls}`}
style={`transform-origin: ${c.x}px ${c.y}px`}
/>
{/each}
<circle cx={c.x} cy={c.y} r="3.5" class="heart" />
<path d={glint(c.x, c.y, (3.5) * 1.6)} class="heart" />
</g>
<style>
@@ -64,4 +63,5 @@
0% { opacity: 1; transform: scale(0.5); }
100% { opacity: 0; transform: scale(3); }
}
.heart { transform-box: fill-box; transform-origin: center; }
</style>
+4 -4
View File
@@ -1,6 +1,6 @@
<script lang="ts">
import type { FxOf } from "../fx";
import { center } from "./geom";
import { center, impact } from "./geom";
let { fx }: { fx: FxOf<"hit"> } = $props();
const c = $derived(center(fx.at));
const DEBRIS = [40, 165, 285];
@@ -8,11 +8,11 @@
<!-- Contact flash: an instant of white -->
<path
d={`M ${c.x} ${c.y - 9} l 2.5 6 6.5 -2 -3.5 5.5 5.5 3.5 -6.5 1 1 6.5 -5.5 -4.5 -4.5 5 0 -7 -6.5 0.5 5 -4.5 -4 -5 6.5 1.5 z`}
d={impact(c.x,c.y,13)}
class="flash" style={`transform-origin: ${c.x}px ${c.y}px`}
/>
<!-- Compressed impact ring: squashed, then out -->
<ellipse cx={c.x} cy={c.y} rx="9" ry="6" class="ring" />
<path d={impact(c.x,c.y,12)} class="ring" />
{#each DEBRIS as deg, i (deg)}
<line
x1={c.x + 8 * Math.cos((deg * Math.PI) / 180)}
@@ -36,7 +36,7 @@
transform-origin: center;
fill: none;
stroke: #c0392b;
stroke-width: 3;
stroke-width: 2;
animation: ring-punch 0.4s cubic-bezier(0.2, 0.9, 0.3, 1) forwards;
}
.debris {
+6 -14
View File
@@ -1,25 +1,16 @@
<script lang="ts">
import type { FxOf } from "../fx";
import { center } from "./geom";
import { center, drop } from "./geom";
let { fx }: { fx: FxOf<"ooze-slip"> } = $props();
const uid = $props.id();
const c = $derived(center(fx.at));
</script>
<defs>
<radialGradient id={`ooze-splat-${uid}`}>
<stop offset="0" stop-color="#a5cc60" stop-opacity="0.8" />
<stop offset="0.7" stop-color="#6ea03c" stop-opacity="0.55" />
<stop offset="1" stop-color="#4a7024" stop-opacity="0" />
</radialGradient>
</defs>
<ellipse cx={c.x} cy={c.y + 8} rx="14" ry="5.5" fill={`url(#ooze-splat-${uid})`} class="splat" />
<path d={`M ${c.x-14} ${c.y+7} q -6 -6 1 -7 q 4 1 4 5 q 7 -9 12 -5 q -3 4 0 5 q 12 -7 14 -1 q 0 8 -17 9 q -15 1 -14 -6 Z`} fill="#6ea03c" stroke="#46662c" stroke-width="1" class="splat" />
<g class="blobs">
<circle cx={c.x - 12} cy={c.y + 3} r="2.5" class="blob" />
<circle cx={c.x + 11} cy={c.y + 2} r="2" class="blob late" />
<circle cx={c.x + 3} cy={c.y - 3} r="1.7" class="blob later" />
<path d={drop(c.x - 12, c.y + 3, (2.5) * 1)} class="blob" />
<path d={drop(c.x + 11, c.y + 2, (2) * 1)} class="blob late" />
<path d={drop(c.x + 3, c.y - 3, (1.7) * 1)} class="blob later" />
</g>
<style>
@@ -42,4 +33,5 @@
50% { transform: translateY(-7px); }
100% { opacity: 0; transform: translateY(2px); }
}
.blob { animation-fill-mode: both; }
</style>
+7 -15
View File
@@ -1,29 +1,19 @@
<script lang="ts">
import type { FxOf } from "../fx";
import { center } from "./geom";
import { center, puff } from "./geom";
let { fx }: { fx: FxOf<"pit-fall"> } = $props();
const uid = $props.id();
const c = $derived(center(fx.at));
</script>
<defs>
<radialGradient id={`pit-hole-${uid}`} cy="0.42">
<stop offset="0" stop-color="#1a130a" />
<stop offset="0.55" stop-color="#3a2c1a" stop-opacity="0.9" />
<stop offset="0.85" stop-color="#5f4a33" stop-opacity="0.55" />
<stop offset="1" stop-color="#5f4a33" stop-opacity="0" />
</radialGradient>
</defs>
<ellipse cx={c.x} cy={c.y} rx="14" ry="11" fill={`url(#pit-hole-${uid})`} class="hole" />
<path d={`M ${c.x-14} ${c.y} q -1 -10 13 -11 q 15 0 16 9 q 1 12 -14 13 q -14 0 -15 -11 Z`} fill="#1a130a" stroke="#5f4a33" stroke-width="2" class="hole" />
<!-- The far lip catches the light: reads as depth, not a dot -->
<path d={`M ${c.x - 10} ${c.y - 5} a 12 8 0 0 1 20 0`} class="lip" />
<circle cx={c.x} cy={c.y - 2} r="8" class="faller" style={`transform-origin: ${c.x}px ${c.y}px`} />
<g class="rim-dust">
<circle cx={c.x - 11} cy={c.y - 6} r="3" class="dust" />
<circle cx={c.x + 10} cy={c.y - 7} r="2.5" class="dust late" />
<circle cx={c.x + 1} cy={c.y - 11} r="2" class="dust later" />
<path d={puff(c.x - 11, c.y - 6, (3) * 1)} class="dust" />
<path d={puff(c.x + 10, c.y - 7, (2.5) * 1)} class="dust late" />
<path d={puff(c.x + 1, c.y - 11, (2) * 1)} class="dust later" />
</g>
<style>
@@ -61,4 +51,6 @@
30% { opacity: 0.8; }
100% { opacity: 0; transform: translateY(-10px) scale(1.7); }
}
.lip { transform-box: fill-box; transform-origin: center; }
.dust { transform-box: fill-box; transform-origin: center; }
</style>
+3 -14
View File
@@ -2,7 +2,6 @@
import type { FxOf } from "../fx";
import { CELL } from "./geom";
let { fx }: { fx: FxOf<"portal" | "portal-cell"> } = $props();
const uid = $props.id();
</script>
{#if fx.kind === "portal"}
@@ -13,16 +12,9 @@
<line {x1} {y1} {x2} {y2} class="curtain glow" />
<line {x1} {y1} {x2} {y2} class="curtain" pathLength="100" />
{:else}
<defs>
<radialGradient id={`portal-void-${uid}`}>
<stop offset="0" stop-color="#0d2430" stop-opacity="0.85" />
<stop offset="0.6" stop-color="#12414a" stop-opacity="0.5" />
<stop offset="1" stop-color="#9be3e0" stop-opacity="0.1" />
</radialGradient>
</defs>
<rect x={fx.at.x * CELL + 5} y={fx.at.y * CELL + 5}
width={CELL - 10} height={CELL - 10} rx="8"
fill={`url(#portal-void-${uid})`} class="void"
fill="#12414a" fill-opacity="0.45" class="void"
style={`transform-origin: ${fx.at.x * CELL + CELL / 2}px ${fx.at.y * CELL + CELL / 2}px`} />
{@const vx = fx.at.x * CELL + CELL / 2}
{@const vy = fx.at.y * CELL + CELL / 2}
@@ -43,15 +35,13 @@
/* The threshold opens, light ripples along it, and it pinches shut. */
.curtain {
stroke: #9be3e0;
stroke-dasharray: 6 5;
stroke-dasharray: 9 3 4 5;
stroke-linecap: round;
filter: drop-shadow(0 0 4px rgba(155, 227, 224, 0.9));
animation: open-ripple-pinch 0.95s ease-in-out forwards;
}
.curtain.glow {
stroke: rgba(180, 138, 224, 0.5);
stroke-dasharray: none;
filter: blur(2px);
animation: glow-swell 0.95s ease-in-out forwards;
}
@keyframes open-ripple-pinch {
@@ -71,8 +61,7 @@
.veil {
fill: none;
stroke: #9be3e0;
stroke-dasharray: 7 5;
filter: drop-shadow(0 0 4px rgba(155, 227, 224, 0.8));
stroke-dasharray: 13 4 5 3;
}
.void { animation: void-open 0.9s ease-out forwards; }
.arm {
+5 -14
View File
@@ -1,27 +1,18 @@
<script lang="ts">
import type { FxOf } from "../fx";
import { center } from "./geom";
import { center, impact, glint } from "./geom";
let { fx }: { fx: FxOf<"pow"> } = $props();
const uid = $props.id();
const c = $derived(center(fx.at));
const DASHES = [20, 95, 160, 250, 320];
</script>
<defs>
<radialGradient id={`pow-star-${uid}`}>
<stop offset="0" stop-color="#fffdf0" />
<stop offset="0.45" stop-color="#ffe94d" />
<stop offset="1" stop-color="#ffb02e" />
</radialGradient>
</defs>
<g class="pow" style={`transform-origin: ${c.x}px ${c.y}px`}>
<path
d={`M ${c.x} ${c.y - 14} l 4 8 9 -4 -4 9 8 5 -9 3 2 10 -8 -6 -6 8 -1 -10 -10 1 7 -7 -7 -7 10 0 1 -9 6 8 z`}
fill={`url(#pow-star-${uid})`}
d={impact(c.x,c.y,21)}
fill="#ffe94d"
/>
<circle cx={c.x} cy={c.y} r="4" class="flash" />
<path d={glint(c.x,c.y,6)} class="flash" />
</g>
{#each DASHES as deg (deg)}
<line
@@ -36,7 +27,7 @@
<style>
.pow path { stroke: #b3372b; stroke-width: 1.6; }
.pow .flash { fill: #ffffff; animation: flash 0.3s ease-out forwards; }
.pow .flash { transform-box: fill-box; transform-origin: center; fill: #ffffff; animation: flash 0.3s ease-out forwards; }
.pow { animation: pow-hit 0.5s ease-out forwards; }
.dash {
stroke: #b3372b;
@@ -1,6 +1,6 @@
<script lang="ts">
import type { FxOf } from "../fx";
import { CELL, SECTOR } from "./geom";
import { CELL, SECTOR, puff } from "./geom";
let { fx }: { fx: FxOf<"sector-spin" | "sector-slide"> } = $props();
const S = SECTOR * CELL;
@@ -51,16 +51,16 @@
{/if}
<g class="corner-dust">
{#each CORNERS as p, i (i)}
<circle cx={p.x} cy={p.y} r="3.5" class={`dust d${i}`} />
<path d={puff(p.x, p.y, (3.5) * 1)} class={`dust d${i}`} />
{/each}
</g>
<style>
.frame {
fill: rgba(233, 225, 203, 0.25);
fill: rgba(233, 225, 203, 0.12);
stroke: #d3852b;
stroke-width: 3;
stroke-dasharray: 12 7;
stroke-dasharray: 19 3 8 4;
}
.arc {
fill: none;
@@ -128,4 +128,5 @@
30% { opacity: 0.9; }
100% { opacity: 0; transform: translateY(-9px) scale(1.6); }
}
.dust { transform-box: fill-box; transform-origin: center; }
</style>
+6 -16
View File
@@ -1,29 +1,18 @@
<script lang="ts">
import type { FxOf } from "../fx";
import { center } from "./geom";
import { center, ward, glint } from "./geom";
let { fx }: { fx: FxOf<"shield"> } = $props();
const uid = $props.id();
const c = $derived(center(fx.at));
</script>
<defs>
<radialGradient id={`shield-dome-${uid}`}>
<stop offset="0" stop-color="#ffe9a3" stop-opacity="0" />
<stop offset="0.75" stop-color="#ffdf7a" stop-opacity="0.25" />
<stop offset="1" stop-color="#c9a72a" stop-opacity="0.9" />
</radialGradient>
<filter id={`shield-glow-${uid}`} x="-80%" y="-80%" width="260%" height="260%">
<feGaussianBlur stdDeviation="3" result="blur" />
<feMerge><feMergeNode in="blur" /><feMergeNode in="SourceGraphic" /></feMerge>
</filter>
</defs>
<circle cx={c.x} cy={c.y} r="16" fill={`url(#shield-dome-${uid})`} class="dome" filter={`url(#shield-glow-${uid})`} />
<circle cx={c.x} cy={c.y} r="16" class="rim" />
<path d={ward(c.x,c.y,18)} class="dome" />
<path d={ward(c.x,c.y,15.4)} class="rim" />
<path d={glint(c.x-11,c.y-11,4)} fill="#fff3c4" class="twinkle" />
<style>
.dome {
fill: none; stroke: #c9a72a; stroke-width: 3; stroke-linejoin: round;
transform-box: fill-box;
transform-origin: center;
animation: dome 0.7s ease-out forwards;
@@ -36,6 +25,7 @@
stroke-width: 2;
animation: rim-strike 0.7s ease-out forwards;
}
.twinkle { animation: rim-strike 0.7s ease-out forwards; transform-box: fill-box; transform-origin: center; }
@keyframes dome {
0% { opacity: 0; transform: scale(0.6); }
30% { opacity: 1; transform: scale(1.1); }
+8 -8
View File
@@ -1,17 +1,17 @@
<script lang="ts">
import type { FxOf } from "../fx";
import { center } from "./geom";
import { center, glint } from "./geom";
let { fx }: { fx: FxOf<"shimmer"> } = $props();
const c = $derived(center(fx.at));
</script>
<circle cx={c.x} cy={c.y} r="6" class="ring" />
<circle cx={c.x} cy={c.y} r="6" class="ring late" />
<path d={`M ${c.x-7} ${c.y+2} q -4 -10 6 -10 q -5 3 -2 6 M ${c.x+6} ${c.y-3} q 6 10 -4 10 q 4 -3 2 -5`} class="ring" />
<path d={`M ${c.x-6} ${c.y-3} q 6 -7 12 2 M ${c.x+4} ${c.y+6} q -9 4 -11 -4`} class="ring late" />
<g class="motes">
<circle cx={c.x - 8} cy={c.y - 8} r="1.5" />
<circle cx={c.x + 9} cy={c.y - 4} r="1.2" class="late" />
<circle cx={c.x - 3} cy={c.y + 9} r="1.3" class="later" />
<path d={glint(c.x - 8, c.y - 8, 1.5 * 2)} />
<path d={glint(c.x + 9, c.y - 4, 1.2 * 2)} class="late" />
<path d={glint(c.x - 3, c.y + 9, 1.3 * 2)} class="later" />
</g>
<style>
@@ -24,12 +24,12 @@
animation: ring 0.6s ease-out forwards;
}
.ring.late { stroke: #e2c8ff; animation-delay: 0.18s; opacity: 0; }
.motes circle { fill: #e2c8ff; animation: mote 0.7s ease-out forwards; }
.motes path { fill: #e2c8ff; animation: mote 0.7s ease-out forwards; }
.motes .late { animation-delay: 0.1s; opacity: 0; }
.motes .later { animation-delay: 0.18s; opacity: 0; }
@keyframes ring {
0% { opacity: 0.95; transform: scale(1); }
100% { opacity: 0; transform: scale(5); }
100% { opacity: 0; transform: scale(3.5); }
}
@keyframes mote {
0% { opacity: 0; transform: translateY(0); }
+3 -12
View File
@@ -2,22 +2,13 @@
import type { FxOf } from "../fx";
import { center } from "./geom";
let { fx }: { fx: FxOf<"slime-stuck"> } = $props();
const uid = $props.id();
const c = $derived(center(fx.at));
</script>
<defs>
<radialGradient id={`slime-goo-${uid}`}>
<stop offset="0" stop-color="#c8e87a" stop-opacity="0.4" />
<stop offset="0.8" stop-color="#8cc83c" stop-opacity="0.65" />
<stop offset="1" stop-color="#7a9e2e" stop-opacity="0.9" />
</radialGradient>
</defs>
<!-- An irregular blob, squashing as it grips -->
<path
class="goo" style={`transform-origin: ${c.x}px ${c.y}px`}
fill={`url(#slime-goo-${uid})`}
fill="#8cc83c" fill-opacity="0.55"
d={`M ${c.x - 13} ${c.y + 2}
C ${c.x - 14} ${c.y - 7}, ${c.x - 6} ${c.y - 12}, ${c.x + 2} ${c.y - 10}
C ${c.x + 10} ${c.y - 13}, ${c.x + 15} ${c.y - 4}, ${c.x + 12} ${c.y + 4}
@@ -33,11 +24,11 @@
<circle cx={c.x + 4} cy={c.y - 5} r="2.5" class="bubble" />
<style>
.goo { animation: goo-grip 0.9s ease-out forwards; }
.goo { stroke: #56792c; stroke-width: 1; animation: goo-grip 0.9s ease-out forwards; }
.strand {
fill: none;
stroke: #8fbf4d;
stroke-width: 2;
stroke-width: 2.8;
stroke-linecap: round;
stroke-dasharray: 20 40;
animation: strand-snap 0.55s ease-in 0.15s forwards;
+2 -10
View File
@@ -3,18 +3,9 @@
import { center } from "./geom";
let { fx }: { fx: FxOf<"soul"> } = $props();
const uid = $props.id();
const c = $derived(center(fx.at));
</script>
<defs>
<radialGradient id={`soul-body-${uid}`} cy="0.35">
<stop offset="0" stop-color="#f7f4ff" stop-opacity="0.95" />
<stop offset="0.7" stop-color="#d9d0f0" stop-opacity="0.75" />
<stop offset="1" stop-color="#a096c8" stop-opacity="0.35" />
</radialGradient>
</defs>
<g class="soul">
<g class="sway" style={`transform-origin: ${c.x}px ${c.y}px`}>
<!-- Rounded head, wavy hem -->
@@ -22,8 +13,9 @@
d={`M ${c.x - 8} ${c.y + 6} C ${c.x - 9} ${c.y - 6}, ${c.x - 5} ${c.y - 11}, ${c.x} ${c.y - 11}
C ${c.x + 5} ${c.y - 11}, ${c.x + 9} ${c.y - 6}, ${c.x + 8} ${c.y + 6}
q -2 -2.5 -4 0 q -2 2.5 -4 0 q -2 -2.5 -4 0 q -2 2.5 -4 0 z`}
fill={`url(#soul-body-${uid})`}
fill="#d9d0f0" fill-opacity="0.85" stroke="#86759f" stroke-width="0.8"
/>
<path d={`M ${c.x-6} ${c.y+2} q -1 -8 4 -10`} fill="none" stroke="#f7f4ff" stroke-width="1.5" />
<circle cx={c.x - 3} cy={c.y - 4} r="1.3" fill="#5a5178" />
<circle cx={c.x + 3} cy={c.y - 4} r="1.3" fill="#5a5178" />
</g>
+7 -6
View File
@@ -1,6 +1,6 @@
<script lang="ts">
import type { FxOf } from "../fx";
import { center } from "./geom";
import { center, glint } from "./geom";
let { fx }: { fx: FxOf<"sparkle"> } = $props();
const c = $derived(center(fx.at));
</script>
@@ -11,11 +11,11 @@
<g class="glint counter" style={`transform-origin: ${c.x}px ${c.y}px`}>
<path d={`M ${c.x} ${c.y - 8} l 2 6 6 2 -6 2 -2 6 -2 -6 -6 -2 6 -2 z`} />
</g>
<circle cx={c.x} cy={c.y} r="3" class="core" />
<path d={glint(c.x, c.y, (3) * 1.7)} class="core" />
<g class="motes">
<circle cx={c.x - 10} cy={c.y - 9} r="1.3" />
<circle cx={c.x + 11} cy={c.y - 5} r="1.1" class="late" />
<circle cx={c.x + 2} cy={c.y + 11} r="1.2" class="later" />
<path d={glint(c.x - 10, c.y - 9, (1.3) * 1.7)} />
<path d={glint(c.x + 11, c.y - 5, (1.1) * 1.7)} class="late" />
<path d={glint(c.x + 2, c.y + 11, (1.2) * 1.7)} class="later" />
</g>
<style>
@@ -33,7 +33,7 @@
fill: #ffffff;
animation: core-snap 0.45s ease-out forwards;
}
.motes circle { fill: #ffe084; animation: mote-fly 0.6s ease-out 0.12s forwards; opacity: 0; }
.motes path { fill: #ffe084; animation: mote-fly 0.6s ease-out 0.12s forwards; opacity: 0; }
.motes .late { animation-delay: 0.18s; }
.motes .later { animation-delay: 0.24s; }
@keyframes spin-snap {
@@ -57,4 +57,5 @@
30% { opacity: 0.95; }
100% { opacity: 0; transform: translateY(-8px) scale(0.6); }
}
.motes path { transform-box: fill-box; transform-origin: center; }
</style>
+13 -20
View File
@@ -1,35 +1,27 @@
<script lang="ts">
import type { FxOf } from "../fx";
import { center } from "./geom";
import { center, drop } from "./geom";
let { fx }: { fx: FxOf<"splash"> } = $props();
const uid = $props.id();
const c = $derived(center(fx.at));
</script>
<defs>
<radialGradient id={`splash-pool-${uid}`}>
<stop offset="0" stop-color="#bfe8ff" stop-opacity="0.7" />
<stop offset="0.7" stop-color="#4fa3e3" stop-opacity="0.4" />
<stop offset="1" stop-color="#155a9e" stop-opacity="0" />
</radialGradient>
</defs>
<ellipse cx={c.x} cy={c.y + 5} rx="16" ry="6.5" fill={`url(#splash-pool-${uid})`} class="pool" />
<circle cx={c.x} cy={c.y} r="7" class="ring" />
<g class="pool" style={`transform-origin: ${c.x}px ${c.y+5}px`}>
<path d={`M ${c.x-17} ${c.y+6} q -4 -5 -5 -14 q 8 2 9 10 q 2 -11 -2 -16 q 10 0 9 16 q 6 -7 7 -16 q 6 6 0 18 q 9 -1 15 -9 q -1 14 -16 17 q -10 3 -17 -6 Z`} fill="#4fa3e3" stroke="#245981" stroke-width="0.7" />
<path d={`M ${c.x-20} ${c.y-6} q 5 1 7 8 M ${c.x-12} ${c.y-12} q 6 3 4 11 M ${c.x+5} ${c.y-12} q 1 6 -3 12 M ${c.x-11} ${c.y+8} q 8 4 16 -1`} fill="none" stroke="#dceeea" stroke-width="2" stroke-linecap="round" />
</g>
<path d={`M ${c.x-7} ${c.y+3} q 7 -4 14 0 M ${c.x+5} ${c.y+6} q -6 3 -10 0`} class="ring" />
<!-- The crown: droplets thrown upward, falling back -->
<g class="crown">
<circle cx={c.x - 14} cy={c.y - 6} r="2.8" class="drop" />
<circle cx={c.x - 7} cy={c.y - 13} r="2.3" class="drop late" />
<circle cx={c.x + 1} cy={c.y - 16} r="2.5" class="drop" />
<circle cx={c.x + 8} cy={c.y - 12} r="2.2" class="drop later" />
<circle cx={c.x + 14} cy={c.y - 5} r="1.9" class="drop late" />
<path d={drop(c.x - 14, c.y - 6, 2.8)} class="drop" />
<path d={drop(c.x - 7, c.y - 13, 2.3)} class="drop late" />
<path d={drop(c.x + 1, c.y - 16, 2.5)} class="drop" />
<path d={drop(c.x + 8, c.y - 12, 2.2)} class="drop later" />
<path d={drop(c.x + 14, c.y - 5, 1.9)} class="drop late" />
</g>
<style>
.pool {
transform-box: fill-box;
transform-origin: center;
animation: pool-spread 0.6s ease-out forwards;
}
.ring {
@@ -40,7 +32,7 @@
stroke-width: 3.2;
animation: ring-out 0.55s ease-out forwards;
}
.drop { fill: #7cc0ee; animation: drop-arc 0.6s ease-in forwards; }
.drop { fill: #7cc0ee; stroke: #245981; stroke-width: 0.6; animation: drop-arc 0.6s ease-in forwards; }
.drop.late { animation-delay: 0.07s; }
.drop.later { animation-delay: 0.13s; }
@keyframes pool-spread {
@@ -56,4 +48,5 @@
45% { transform: translateY(-8px); }
100% { opacity: 0; transform: translateY(6px); }
}
.drop { animation-fill-mode: both; }
</style>
+3 -2
View File
@@ -1,4 +1,5 @@
<script lang="ts">
import { glint } from "./geom";
import type { FxOf } from "../fx";
let { fx }: { fx: FxOf<"streak"> } = $props();
@@ -9,10 +10,10 @@
<path {d} pathLength="100" class="trail wide" />
<path {d} pathLength="100" class="trail bright" />
<circle r="4" class="head" cx="0" cy="0">
<path d={glint(0,0,6)} class="head">
<animateMotion dur="0.38s" fill="freeze" path={d} calcMode="spline"
keySplines="0.3 0 0.4 1" keyTimes="0;1" keyPoints="0;1" />
</circle>
</path>
<style>
.trail {
+2 -1
View File
@@ -14,8 +14,9 @@
{#each TACKS as t (t.cls)}
<g transform={`translate(${c.x + t.x} ${c.y + t.y}) rotate(${t.rot})`}>
<g class={`tack ${t.cls}`}>
<circle cx="0" cy="3.8" r="3.9" class="head" />
<path d="M -4 3 Q -4 0 0 0 Q 5 1 4 4 Q 0 7 -4 3 Z" class="head" />
<path d="M -2.1 1.5 L 0 -7.5 L 2.1 1.5 z" class="spike" />
<path d="M 0 -6 L 0 1" stroke="#efe8d4" stroke-width="0.6" />
</g>
</g>
{/each}
+5 -4
View File
@@ -9,15 +9,15 @@
<!-- Three springing branches, thorn spurs along each -->
<g class="branch b0" style={`transform-origin: ${c.x}px ${c.y + 10}px`}>
<path d={`M ${c.x} ${c.y + 10} q -8 -10 -13 -20`} class="stem" />
<path d={`M ${c.x - 6} ${c.y + 1} l -4 -1 M ${c.x - 10} ${c.y - 6} l -4 0`} class="spur" />
<path d={`M ${c.x - 6} ${c.y + 1} l -5 -4 2 5 Z M ${c.x - 10} ${c.y - 6} l -5 -3 2 5 Z`} class="spur" />
</g>
<g class="branch b1" style={`transform-origin: ${c.x}px ${c.y + 10}px`}>
<path d={`M ${c.x} ${c.y + 10} q 1 -13 -1 -24`} class="stem" />
<path d={`M ${c.x} ${c.y - 2} l 4 -2 M ${c.x - 1} ${c.y - 9} l -4 -2`} class="spur" />
<path d={`M ${c.x} ${c.y - 2} l 6 -5 -2 6 Z M ${c.x - 1} ${c.y - 9} l -5 -5 2 6 Z`} class="spur" />
</g>
<g class="branch b2" style={`transform-origin: ${c.x}px ${c.y + 10}px`}>
<path d={`M ${c.x} ${c.y + 10} q 9 -9 15 -18`} class="stem" />
<path d={`M ${c.x + 7} ${c.y + 2} l 4 -1 M ${c.x + 12} ${c.y - 4} l 4 1`} class="spur" />
<path d={`M ${c.x + 7} ${c.y + 2} l 6 -4 -3 5 Z M ${c.x + 12} ${c.y - 4} l 6 -1 -3 4 Z`} class="spur" />
</g>
</g>
<g class="leaves">
@@ -29,10 +29,11 @@
.stem {
fill: none;
stroke: #467832;
stroke-width: 2.5;
stroke-width: 3.2;
stroke-linecap: round;
}
.spur {
fill: #2f5423;
stroke: #2f5423;
stroke-width: 1.6;
stroke-linecap: round;
+8 -40
View File
@@ -2,52 +2,20 @@
import type { FxOf } from "../fx";
let { fx }: { fx: FxOf<"waterbolt"> } = $props();
const uid = $props.id();
const a = $derived(fx.a);
const b = $derived(fx.b);
</script>
<defs>
<radialGradient id={`wb-core-${uid}`}>
<stop offset="0" stop-color="#eafaff" />
<stop offset="0.35" stop-color="#9fd9f7" />
<stop offset="0.7" stop-color="#3b8dd6" />
<stop offset="1" stop-color="#155a9e" stop-opacity="0.2" />
</radialGradient>
<linearGradient id={`wb-tail-${uid}`} x1="1" x2="0">
<stop offset="0" stop-color="#bfe8ff" stop-opacity="0.9" />
<stop offset="0.4" stop-color="#4fa3e3" stop-opacity="0.55" />
<stop offset="1" stop-color="#155a9e" stop-opacity="0" />
</linearGradient>
<filter id={`wb-glow-${uid}`} x="-160%" y="-160%" width="420%" height="420%">
<feGaussianBlur stdDeviation="5" result="blur" />
<feMerge><feMergeNode in="blur" /><feMergeNode in="SourceGraphic" /></feMerge>
</filter>
</defs>
<g class="waterbolt">
<g>
<!-- Spray tail -->
<path
d="M -5 -9 C -22 -12, -44 -6, -70 -2 C -46 0, -50 8, -74 13 C -44 11, -22 8, -5 9 Z"
fill={`url(#wb-tail-${uid})`}
/>
<!-- Body and core -->
<circle r="11" fill="#3b8dd6" opacity="0.6" filter={`url(#wb-glow-${uid})`} />
<circle r="9" fill={`url(#wb-core-${uid})`} />
<ellipse cx="3" cy="-2.5" rx="4" ry="3" fill="#f2fcff" opacity="0.9" />
<!-- Trailing droplets -->
<g fill="#7cc0ee">
<circle cx="-24" cy="-10" r="2">
<animate attributeName="cy" values="-10; -15; -10" dur="0.18s" repeatCount="indefinite" />
</circle>
<circle cx="-40" cy="9" r="1.6">
<animate attributeName="cy" values="9; 14; 9" dur="0.22s" repeatCount="indefinite" />
</circle>
<circle cx="-56" cy="-4" r="1.3">
<animate attributeName="opacity" values="1; 0.2; 1" dur="0.16s" repeatCount="indefinite" />
</circle>
<g stroke-linejoin="round">
<path d="M 10 -7 Q -2 -17 -14 -8 Q -7 -10 -5 -5 Q -22 -11 -38 -5 L -29 -1 Q -46 -4 -62 6 Q -34 3 -20 10 Q -1 18 11 7 Q 18 0 10 -7 Z" fill="#397cab" stroke="#244c73" stroke-width="1" />
<path d="M 12 -3 Q 5 -15 -7 -10 Q -15 -8 -17 -3 Q -8 -7 -3 -3 Q -15 2 -32 -1 Q -19 8 -5 10 Q 8 14 13 3 Q 8 7 4 5 Q 14 3 12 -3 Z" fill="#7cc0ee" />
<path d="M 11 -3 Q 5 -13 -6 -8 Q -10 -5 -9 -3 Q -3 -7 2 -4 L 0 -1 Q 5 -2 8 2 Q 11 5 13 1 Z" fill="#eaf4e9" />
<path d="M -18 7 Q -26 8 -36 3 Q -29 11 -18 10 Z" fill="#bfe8ff" />
<g fill="#7cc0ee" stroke="#244c73" stroke-width="0.6">
<path d="M -24 -10 Q -30 -18 -32 -14 Q -33 -10 -24 -10 Z"><animate attributeName="opacity" values="1;.5;1" calcMode="discrete" dur="0.24s" repeatCount="indefinite" /></path>
<path d="M -44 8 Q -53 8 -53 12 Q -48 15 -44 8 Z" />
</g>
</g>
<animateMotion dur="0.42s" fill="freeze" rotate="auto" path={`M ${a.x} ${a.y} L ${b.x} ${b.y}`} />
+4 -4
View File
@@ -6,16 +6,16 @@
</script>
<g class="whiff" style={`transform-origin: ${c.x}px ${c.y}px`}>
<path d={`M ${c.x - 30} ${c.y - 9} q 20 -7 58 -4`} pathLength="100" class="streak a" />
<path d={`M ${c.x - 32} ${c.y + 1} q 24 4 62 0`} pathLength="100" class="streak b" />
<path d={`M ${c.x - 28} ${c.y + 10} q 18 7 56 5`} pathLength="100" class="streak c" />
<path d={`M ${c.x - 30} ${c.y - 9} c 8 -7 33 -9 58 -4`} pathLength="100" class="streak a" />
<path d={`M ${c.x - 32} ${c.y + 1} c 19 6 41 3 62 0`} pathLength="100" class="streak b" />
<path d={`M ${c.x - 28} ${c.y + 10} c 14 9 38 8 56 5`} pathLength="100" class="streak c" />
</g>
<style>
.streak {
fill: none;
stroke: rgba(110, 125, 140, 0.9);
stroke-width: 3;
stroke-width: 2.5;
stroke-linecap: round;
stroke-dasharray: 38 100;
stroke-dashoffset: 38;
+14
View File
@@ -5,3 +5,17 @@ export const center = (c: { x: number; y: number }) => ({
x: c.x * CELL + CELL / 2,
y: c.y * CELL + CELL / 2,
});
/** Small cel silhouettes, expressed in board coordinates so their animation
* origins stay attached to the existing cell and edge anchors. */
export const glint = (x: number, y: number, r: number) =>
`M ${x} ${y-r} Q ${x+r*.13} ${y-r*.13} ${x+r*.84} ${y+.04*r} Q ${x+r*.15} ${y+r*.15} ${x+.06*r} ${y+r} Q ${x-r*.15} ${y+r*.13} ${x-r*.88} ${y} Q ${x-r*.14} ${y-r*.15} ${x} ${y-r} Z`;
export const puff = (x: number, y: number, r: number) =>
`M ${x-r} ${y+r*.3} C ${x-r*1.2} ${y-r*.15} ${x-r*.85} ${y-r*.6} ${x-r*.5} ${y-r*.5} C ${x-r*.65} ${y-r*1.12} ${x+r*.15} ${y-r*1.1} ${x+r*.32} ${y-r*.55} C ${x+r*.95} ${y-r*.75} ${x+r*1.22} ${y+r*.1} ${x+r*.75} ${y+r*.4} C ${x+r*.58} ${y+r*.9} ${x-r*.72} ${y+r*.8} ${x-r} ${y+r*.3} Z`;
export const drop = (x: number, y: number, r: number) =>
`M ${x+r*.2} ${y-r*1.7} C ${x+r*.45} ${y-r*.65} ${x+r*1.12} ${y-r*.1} ${x+r*.8} ${y+r*.6} C ${x+r*.45} ${y+r*1.25} ${x-r*.85} ${y+r*.9} ${x-r*.8} ${y+r*.1} C ${x-r*.75} ${y-r*.6} ${x-r*.1} ${y-r} ${x+r*.2} ${y-r*1.7} Z`;
export const impact = (x: number, y: number, r: number) =>
`M ${x-r*.06} ${y-r} L ${x+r*.22} ${y-r*.34} ${x+r*.91} ${y-r*.58} ${x+r*.46} ${y-r*.05} ${x+r} ${y+r*.39} ${x+r*.31} ${y+r*.28} ${x+r*.2} ${y+r*.92} ${x-r*.12} ${y+r*.44} ${x-r*.74} ${y+r*.72} ${x-r*.4} ${y+r*.14} ${x-r} ${y-r*.18} ${x-r*.34} ${y-r*.27} Z`;
export const ward = (x: number, y: number, r: number) =>
`M ${x} ${y-r} Q ${x+r*.45} ${y-r*.63} ${x+r*.9} ${y-r*.68} L ${x+r*.82} ${y+r*.18} Q ${x+r*.7} ${y+r*.66} ${x} ${y+r} Q ${x-r*.75} ${y+r*.59} ${x-r*.81} ${y+r*.15} L ${x-r*.9} ${y-r*.68} Q ${x-r*.42} ${y-r*.65} ${x} ${y-r} Z`;