Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca55f7d00b | ||
|
|
c6d73550df | ||
|
|
e713d69582 | ||
|
|
bcaa06251a | ||
|
|
a88cae3a0e | ||
|
|
308f1b6ff9 | ||
|
|
fe0318b99b |
@@ -191,7 +191,7 @@ function broadcast(room: Room, makeMessage: (playerId: PlayerId) => unknown): vo
|
||||
* The clockwork plays at a watchable pace: one command every beat, each
|
||||
* broadcast as it lands, until the maze wants a human again.
|
||||
*/
|
||||
const BOT_STEP_MS = 1500;
|
||||
const BOT_STEP_MS = 1000;
|
||||
|
||||
/** What a step's event means to this bot — its own deed when it acted,
|
||||
* its own suffering when somebody else did. Null: nothing worth a word. */
|
||||
|
||||
@@ -21,6 +21,14 @@
|
||||
let showHelp = $state(false);
|
||||
/** The finished game whose victory fanfare has been dismissed. */
|
||||
let victorySeen = $state(false);
|
||||
/** The fireworks get the stage before the modal takes it. */
|
||||
let victoryCurtain = $state(false);
|
||||
$effect(() => {
|
||||
if (view?.phase === "finished" && view.winner && !victoryCurtain) {
|
||||
const t = setTimeout(() => (victoryCurtain = true), 2400);
|
||||
return () => clearTimeout(t);
|
||||
}
|
||||
});
|
||||
/** Quiet mode: skip the attack announcement modal (device preference). */
|
||||
let noFanfare = $state(localStorage.getItem("wizwar-no-fanfare") === "1");
|
||||
function setFanfare(announce: boolean) {
|
||||
@@ -949,7 +957,7 @@
|
||||
<Help initialTab={helpTab} stats={net.stats} onstats={() => net.requestStats()} onclose={() => (showHelp = false)} />
|
||||
{/if}
|
||||
|
||||
{#if view?.phase === "finished" && view.winner && !victorySeen}
|
||||
{#if view?.phase === "finished" && view.winner && victoryCurtain && !victorySeen}
|
||||
<div class="scrim attack-scrim" role="alertdialog" aria-label="the game is won">
|
||||
<div class="attack-notice victory-notice">
|
||||
<div class="victory-trophy">🏆</div>
|
||||
|
||||
@@ -1,22 +1,46 @@
|
||||
<script lang="ts">
|
||||
import type { FxOf } from "../fx";
|
||||
import { center } from "./geom";
|
||||
|
||||
let { fx }: { fx: FxOf<"absorb"> } = $props();
|
||||
const uid = $props.id();
|
||||
const c = $derived(center(fx.at));
|
||||
</script>
|
||||
|
||||
<rect x={c.x - 9} y={c.y - 13} width="18" height="26" rx="2" class="absorb"
|
||||
<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`} />
|
||||
|
||||
<style>
|
||||
.absorb {
|
||||
fill: #f6f0df;
|
||||
.card {
|
||||
stroke: #43331f;
|
||||
stroke-width: 1.5;
|
||||
animation: swallow 0.65s ease-in forwards;
|
||||
}
|
||||
.pull {
|
||||
transform-box: fill-box;
|
||||
transform-origin: center;
|
||||
fill: none;
|
||||
stroke: rgba(180, 138, 224, 0.6);
|
||||
stroke-width: 2;
|
||||
stroke-dasharray: 5 6;
|
||||
animation: pull-in 0.6s ease-in forwards;
|
||||
}
|
||||
@keyframes swallow {
|
||||
0% { opacity: 0.95; transform: scale(1) rotate(0deg); }
|
||||
100% { opacity: 0; transform: scale(0.05) rotate(50deg); }
|
||||
100% { opacity: 0; transform: scale(0.05) rotate(140deg); }
|
||||
}
|
||||
@keyframes pull-in {
|
||||
0% { opacity: 0.8; transform: scale(1.6) rotate(0deg); }
|
||||
100% { opacity: 0; transform: scale(0.2) rotate(-90deg); }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,33 +1,70 @@
|
||||
<script lang="ts">
|
||||
import type { FxOf } from "../fx";
|
||||
import { center } from "./geom";
|
||||
|
||||
let { fx }: { fx: FxOf<"bolt"> } = $props();
|
||||
/** A fresh jagged path every strike. */
|
||||
const points = $derived.by(() => {
|
||||
const uid = $props.id();
|
||||
|
||||
/** A fresh jagged path every strike, plus two forks off mid-joints. */
|
||||
const geom = $derived.by(() => {
|
||||
const a = center(fx.from), b = center(fx.to);
|
||||
const segs = 6;
|
||||
const pts: string[] = [`${a.x},${a.y}`];
|
||||
const joints: { x: number; y: number }[] = [a];
|
||||
for (let i = 1; i < segs; i++) {
|
||||
const t = i / segs;
|
||||
pts.push(`${(a.x + (b.x - a.x) * t + (Math.random() - 0.5) * 16).toFixed(1)},${(a.y + (b.y - a.y) * t + (Math.random() - 0.5) * 16).toFixed(1)}`);
|
||||
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,
|
||||
});
|
||||
}
|
||||
pts.push(`${b.x},${b.y}`);
|
||||
return pts.join(" ");
|
||||
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}`;
|
||||
};
|
||||
return { points, forks: [fork(joints[2]!), fork(joints[4]!)] };
|
||||
});
|
||||
</script>
|
||||
|
||||
<polyline {points} class="bolt" />
|
||||
<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})`}>
|
||||
<polyline points={geom.points} class="wide" />
|
||||
{#each geom.forks as d, i (i)}
|
||||
<path {d} class="fork" />
|
||||
{/each}
|
||||
<polyline points={geom.points} class="core" />
|
||||
</g>
|
||||
|
||||
<style>
|
||||
.bolt {
|
||||
.bolt .wide {
|
||||
fill: none;
|
||||
stroke: #ffe94d;
|
||||
stroke-width: 3;
|
||||
stroke: #e6a812;
|
||||
stroke-width: 5.5;
|
||||
stroke-linejoin: round;
|
||||
filter: drop-shadow(0 0 7px rgba(255, 233, 77, 0.95));
|
||||
animation: flicker 0.5s steps(2, jump-none) forwards;
|
||||
opacity: 0.75;
|
||||
}
|
||||
@keyframes flicker {
|
||||
.bolt .fork {
|
||||
fill: none;
|
||||
stroke: #fff6b0;
|
||||
stroke-width: 1.6;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
.bolt .core {
|
||||
fill: none;
|
||||
stroke: #fffdf0;
|
||||
stroke-width: 2;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
.bolt .fork { stroke: #f5d54a; }
|
||||
.bolt { animation: bolt-flicker 0.5s steps(2, jump-none) forwards; }
|
||||
@keyframes bolt-flicker {
|
||||
0% { opacity: 0; } 15% { opacity: 1; } 40% { opacity: 0.3; }
|
||||
60% { opacity: 1; } 100% { opacity: 0; }
|
||||
}
|
||||
|
||||
@@ -1,26 +1,64 @@
|
||||
<script lang="ts">
|
||||
import type { FxOf } from "../fx";
|
||||
import { center } 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>
|
||||
|
||||
<circle cx={c.x} cy={c.y} r="4" class="burst" />
|
||||
<circle cx={c.x} cy={c.y} r="4" class="burst late" />
|
||||
<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" />
|
||||
{#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}
|
||||
class="ember"
|
||||
style={`transform-origin: ${c.x}px ${c.y}px`}
|
||||
/>
|
||||
{/each}
|
||||
|
||||
<style>
|
||||
.burst {
|
||||
.bloom {
|
||||
transform-box: fill-box;
|
||||
transform-origin: center;
|
||||
animation: bloom 0.5s ease-out forwards;
|
||||
}
|
||||
.shockwave {
|
||||
transform-box: fill-box;
|
||||
transform-origin: center;
|
||||
fill: none;
|
||||
stroke: #ff8c1a;
|
||||
stroke-width: 4;
|
||||
animation: ring 0.5s ease-out 0.05s forwards;
|
||||
stroke: #ffd27a;
|
||||
stroke-width: 2.5;
|
||||
animation: shockwave 0.45s ease-out 0.08s forwards;
|
||||
opacity: 0;
|
||||
}
|
||||
.burst.late { stroke: #ffd27a; animation-delay: 0.16s; }
|
||||
@keyframes ring {
|
||||
0% { opacity: 0.95; transform: scale(1); }
|
||||
100% { opacity: 0; transform: scale(5); }
|
||||
.ember {
|
||||
fill: #ff9d19;
|
||||
animation: ember-fly 0.55s ease-out forwards;
|
||||
}
|
||||
@keyframes bloom {
|
||||
0% { opacity: 0; transform: scale(0.2); }
|
||||
25% { opacity: 1; transform: scale(1.2); }
|
||||
100% { opacity: 0; transform: scale(2.4); }
|
||||
}
|
||||
@keyframes shockwave {
|
||||
0% { opacity: 0.9; transform: scale(1); }
|
||||
100% { opacity: 0; transform: scale(6); }
|
||||
}
|
||||
@keyframes ember-fly {
|
||||
0% { opacity: 1; transform: scale(1); }
|
||||
100% { opacity: 0; transform: scale(2.1) rotate(24deg); }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,29 +1,45 @@
|
||||
<script lang="ts">
|
||||
import type { FxOf } from "../fx";
|
||||
import { center } from "./geom";
|
||||
|
||||
let { fx }: { fx: FxOf<"chaos-swirl"> } = $props();
|
||||
const c = $derived(center(fx.at));
|
||||
const CARDS = [0, 72, 144, 216, 288];
|
||||
</script>
|
||||
|
||||
<g class="chaos" style={`transform-origin: ${c.x}px ${c.y}px`}>
|
||||
<circle cx={c.x} cy={c.y} r="30" />
|
||||
<circle cx={c.x} cy={c.y} r="55" class="mid" />
|
||||
<circle cx={c.x} cy={c.y} r="80" class="outer" />
|
||||
<circle cx={c.x} cy={c.y} r="30" class="ring inner" />
|
||||
<circle cx={c.x} cy={c.y} r="55" class="ring mid" />
|
||||
<circle cx={c.x} cy={c.y} r="80" class="ring outer" />
|
||||
{#each CARDS as deg (deg)}
|
||||
<rect
|
||||
x={c.x + 46 * Math.cos((deg * Math.PI) / 180) - 5}
|
||||
y={c.y + 46 * Math.sin((deg * Math.PI) / 180) - 7}
|
||||
width="10" height="14" rx="1.5"
|
||||
class="slip"
|
||||
transform={`rotate(${deg + 25} ${c.x + 46 * Math.cos((deg * Math.PI) / 180)} ${c.y + 46 * Math.sin((deg * Math.PI) / 180)})`}
|
||||
/>
|
||||
{/each}
|
||||
</g>
|
||||
|
||||
<style>
|
||||
.chaos circle {
|
||||
.ring {
|
||||
fill: none;
|
||||
stroke: #b48ae0;
|
||||
stroke-width: 4;
|
||||
stroke-dasharray: 30 22;
|
||||
}
|
||||
.chaos .mid { stroke: #8a5fc0; stroke-dasharray: 44 30; }
|
||||
.chaos .outer { stroke: #e2c8ff; stroke-dasharray: 60 40; }
|
||||
.ring.inner { stroke: #b48ae0; }
|
||||
.ring.mid { stroke: #8a5fc0; stroke-dasharray: 44 30; }
|
||||
.ring.outer { stroke: #e2c8ff; stroke-dasharray: 60 40; }
|
||||
.slip {
|
||||
fill: #f6f0df;
|
||||
stroke: #43331f;
|
||||
stroke-width: 1;
|
||||
}
|
||||
.chaos { animation: chaos-spin 1.4s ease-in-out forwards; }
|
||||
@keyframes chaos-spin {
|
||||
0% { opacity: 0; transform: rotate(0deg) scale(0.6); }
|
||||
0% { opacity: 0; transform: rotate(0deg) scale(0.5); }
|
||||
20% { opacity: 1; }
|
||||
100% { opacity: 0; transform: rotate(200deg) scale(1.5); }
|
||||
100% { opacity: 0; transform: rotate(220deg) scale(1.4); }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,26 +1,70 @@
|
||||
<script lang="ts">
|
||||
import type { FxOf } from "../fx";
|
||||
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" },
|
||||
{ x: -3, cls: "g1" },
|
||||
{ x: 6, cls: "g2" },
|
||||
];
|
||||
</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">
|
||||
<line x1={c.x - 10} y1={c.y - 14} x2={c.x - 2} y2={c.y + 12} />
|
||||
<line x1={c.x - 2} y1={c.y - 16} x2={c.x + 6} y2={c.y + 10} />
|
||||
<line x1={c.x + 6} y1={c.y - 14} x2={c.x + 14} y2={c.y + 12} />
|
||||
{#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})`}
|
||||
/>
|
||||
<path
|
||||
d={`M ${c.x + g.x} ${c.y - 14} q 4 12 10 26`}
|
||||
class="edge" pathLength="100"
|
||||
/>
|
||||
</g>
|
||||
{/each}
|
||||
</g>
|
||||
|
||||
<style>
|
||||
.claw line {
|
||||
stroke: #b3372b;
|
||||
stroke-width: 3;
|
||||
stroke-linecap: round;
|
||||
/* Each slash reveals downward along its own length, in sequence,
|
||||
while the whole strike rakes a little sideways. */
|
||||
.gash { opacity: 0; animation: gash-cut 0.4s ease-out forwards; }
|
||||
.gash.g1 { animation-delay: 0.06s; }
|
||||
.gash.g2 { animation-delay: 0.12s; }
|
||||
.edge {
|
||||
fill: none;
|
||||
stroke: #ffd9c4;
|
||||
stroke-width: 1.4;
|
||||
stroke-dasharray: 100 100;
|
||||
stroke-dashoffset: 100;
|
||||
animation: edge-draw 0.28s ease-out forwards;
|
||||
}
|
||||
.gash.g1 .edge { animation-delay: 0.06s; }
|
||||
.gash.g2 .edge { animation-delay: 0.12s; }
|
||||
.claw { animation: rake 0.55s ease-out forwards; }
|
||||
@keyframes gash-cut {
|
||||
0% { opacity: 0; transform: scaleY(0.1); }
|
||||
30% { opacity: 1; transform: scaleY(1.05); }
|
||||
100% { opacity: 0.9; transform: scaleY(1); }
|
||||
}
|
||||
@keyframes edge-draw {
|
||||
0% { stroke-dashoffset: 100; opacity: 1; }
|
||||
70% { stroke-dashoffset: 0; opacity: 0.9; }
|
||||
100% { stroke-dashoffset: 0; opacity: 0; }
|
||||
}
|
||||
@keyframes rake {
|
||||
0% { opacity: 0; transform: translateY(-6px); }
|
||||
20% { opacity: 1; }
|
||||
100% { opacity: 0; transform: translateY(6px); }
|
||||
0% { opacity: 1; transform: translate(2px, -3px); }
|
||||
60% { opacity: 1; transform: translate(-2px, 2px); }
|
||||
100% { opacity: 0; transform: translate(-3px, 3px); }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<script lang="ts">
|
||||
import type { FxOf } from "../fx";
|
||||
import { CELL, center } 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);
|
||||
@@ -10,18 +12,52 @@
|
||||
if (fx.side === "W") return { x: fx.cell.x * CELL, y: mid.y };
|
||||
return { x: (fx.cell.x + 1) * CELL, y: mid.y };
|
||||
});
|
||||
/** Edge dust blows perpendicular to the wall; loose dust just rises. */
|
||||
const away = $derived.by(() => {
|
||||
if (fx.kind !== "edge-dust") return { x: 0, y: -12 };
|
||||
return fx.side === "N" || fx.side === "S" ? { x: 11, y: -4 } : { x: 4, y: -11 };
|
||||
});
|
||||
</script>
|
||||
|
||||
<circle cx={c.x - 6} cy={c.y} r="4" class="dust" />
|
||||
<circle cx={c.x + 5} cy={c.y - 3} r="3" class="dust late" />
|
||||
<circle cx={c.x} cy={c.y + 4} r="3.5" class="dust later" />
|
||||
<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 there" />
|
||||
<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 there 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" />
|
||||
</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>
|
||||
.dust { fill: rgba(160, 150, 130, 0.75); animation: drift 0.7s ease-out forwards; }
|
||||
.dust.late { animation-delay: 0.09s; }
|
||||
.dust.later { animation-delay: 0.17s; }
|
||||
@keyframes drift {
|
||||
0% { opacity: 0.8; transform: translateY(0) scale(1); }
|
||||
100% { opacity: 0; transform: translateY(-10px) scale(1.8); }
|
||||
.puff {
|
||||
transform-box: fill-box;
|
||||
transform-origin: center;
|
||||
animation: blow-a 0.8s ease-out forwards;
|
||||
}
|
||||
.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; }
|
||||
@keyframes blow-a {
|
||||
0% { opacity: 1; transform: translate(0, 0) scale(0.8); }
|
||||
100% { opacity: 0; transform: translate(var(--ax), var(--ay)) scale(2); }
|
||||
}
|
||||
@keyframes blow-b {
|
||||
0% { opacity: 1; transform: translate(0, 0) scale(0.8); }
|
||||
100% { opacity: 0; transform: translate(var(--bx), var(--by)) scale(1.9); }
|
||||
}
|
||||
@keyframes grit-fall {
|
||||
0% { opacity: 1; transform: translateY(-3px); }
|
||||
100% { opacity: 0; transform: translateY(8px); }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,22 +1,102 @@
|
||||
<script lang="ts">
|
||||
import type { FxOf } from "../fx";
|
||||
import { center } from "./geom";
|
||||
|
||||
let { fx }: { fx: FxOf<"fireball"> } = $props();
|
||||
const uid = $props.id();
|
||||
|
||||
const a = $derived(center(fx.from));
|
||||
const b = $derived(center(fx.to));
|
||||
</script>
|
||||
|
||||
<circle r="7" class="fireball" cx="0" cy="0">
|
||||
<animateMotion dur="0.42s" fill="freeze" path={`M ${a.x} ${a.y} L ${b.x} ${b.y}`} />
|
||||
</circle>
|
||||
<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>
|
||||
|
||||
<animateMotion dur="0.42s" fill="freeze" rotate="auto" path={`M ${a.x} ${a.y} L ${b.x} ${b.y}`} />
|
||||
</g>
|
||||
|
||||
<style>
|
||||
.fireball {
|
||||
fill: #ff8c1a;
|
||||
stroke: #ffd27a;
|
||||
stroke-width: 2;
|
||||
filter: drop-shadow(0 0 6px rgba(255, 120, 20, 0.9));
|
||||
animation: fade 0.55s ease-in forwards;
|
||||
opacity: 0;
|
||||
animation: fireball-fade 0.55s ease-in forwards;
|
||||
}
|
||||
@keyframes fireball-fade {
|
||||
0%, 70% { opacity: 1; }
|
||||
100% { opacity: 0; }
|
||||
}
|
||||
@keyframes fade { 70% { opacity: 1; } 100% { opacity: 0; } }
|
||||
</style>
|
||||
|
||||
@@ -1,26 +1,67 @@
|
||||
<script lang="ts">
|
||||
import type { FxOf } from "../fx";
|
||||
import { center } from "./geom";
|
||||
|
||||
let { fx }: { fx: FxOf<"fireworks"> } = $props();
|
||||
const c = $derived(center(fx.at));
|
||||
const COLORS = ["#e74c3c", "#f1c40f", "#3b8dd6", "#7ac47e"];
|
||||
// Deliberately uneven: a real burst has no protractor.
|
||||
const RAYS = [
|
||||
{ deg: 8, len: 22, r: 2.8, cls: "" },
|
||||
{ deg: 52, len: 17, r: 2.2, cls: "late" },
|
||||
{ deg: 88, len: 24, r: 3, cls: "" },
|
||||
{ deg: 141, len: 16, r: 2, cls: "later" },
|
||||
{ deg: 176, len: 21, r: 2.6, cls: "late" },
|
||||
{ deg: 224, len: 18, r: 2.3, cls: "" },
|
||||
{ deg: 267, len: 23, r: 2.8, cls: "later" },
|
||||
{ deg: 309, len: 15, r: 2, cls: "late" },
|
||||
];
|
||||
</script>
|
||||
|
||||
<g class="fireworks" style={`transform-origin: ${c.x}px ${c.y}px`}>
|
||||
{#each [0, 45, 90, 135, 180, 225, 270, 315] as deg (deg)}
|
||||
<g class="fireworks">
|
||||
{#each RAYS as ray, i (ray.deg)}
|
||||
{@const dx = Math.cos((ray.deg * Math.PI) / 180)}
|
||||
{@const dy = Math.sin((ray.deg * Math.PI) / 180)}
|
||||
<line
|
||||
x1={c.x + 4 * dx} y1={c.y + 4 * dy}
|
||||
x2={c.x + ray.len * dx} y2={c.y + ray.len * dy}
|
||||
stroke={COLORS[i % 4]}
|
||||
class={`trail ${ray.cls}`}
|
||||
style={`transform-origin: ${c.x}px ${c.y}px`}
|
||||
/>
|
||||
<circle
|
||||
cx={c.x + 18 * Math.cos((deg * Math.PI) / 180)}
|
||||
cy={c.y + 18 * Math.sin((deg * Math.PI) / 180)}
|
||||
r="3" fill={COLORS[(deg / 45) % 4]}
|
||||
cx={c.x + (ray.len + 1) * dx} cy={c.y + (ray.len + 1) * dy} r={ray.r}
|
||||
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" />
|
||||
</g>
|
||||
|
||||
<style>
|
||||
.fireworks { animation: boom 1.1s ease-out forwards; }
|
||||
@keyframes boom {
|
||||
0% { opacity: 0; transform: scale(0.1); }
|
||||
.trail {
|
||||
stroke-width: 2;
|
||||
stroke-linecap: round;
|
||||
animation: trail-out 0.9s ease-out forwards;
|
||||
}
|
||||
.spark { animation: spark-out 1.05s ease-out forwards; }
|
||||
.trail.late, .spark.late { animation-delay: 0.07s; }
|
||||
.trail.later, .spark.later { animation-delay: 0.14s; }
|
||||
.heart { fill: #fffdf0; animation: heart-pop 0.4s ease-out forwards; }
|
||||
@keyframes trail-out {
|
||||
0% { opacity: 0; transform: scale(0.15); }
|
||||
25% { opacity: 1; }
|
||||
100% { opacity: 0; transform: scale(2.4) rotate(30deg); }
|
||||
100% { opacity: 0; transform: scale(1.5); }
|
||||
}
|
||||
@keyframes spark-out {
|
||||
0% { opacity: 0; transform: scale(0.15); }
|
||||
30% { opacity: 1; }
|
||||
80% { opacity: 0.9; transform: scale(1.55); }
|
||||
100% { opacity: 0; transform: scale(1.6) translateY(4px); }
|
||||
}
|
||||
@keyframes heart-pop {
|
||||
0% { opacity: 1; transform: scale(0.5); }
|
||||
100% { opacity: 0; transform: scale(3); }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,21 +3,63 @@
|
||||
import { center } from "./geom";
|
||||
let { fx }: { fx: FxOf<"hit"> } = $props();
|
||||
const c = $derived(center(fx.at));
|
||||
const DEBRIS = [40, 165, 285];
|
||||
</script>
|
||||
|
||||
<circle cx={c.x} cy={c.y} r="10" class="hit" />
|
||||
<!-- 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`}
|
||||
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" />
|
||||
{#each DEBRIS as deg, i (deg)}
|
||||
<line
|
||||
x1={c.x + 8 * Math.cos((deg * Math.PI) / 180)}
|
||||
y1={c.y + 8 * Math.sin((deg * Math.PI) / 180)}
|
||||
x2={c.x + 13 * Math.cos((deg * Math.PI) / 180)}
|
||||
y2={c.y + 13 * Math.sin((deg * Math.PI) / 180)}
|
||||
class={`debris d${i}`}
|
||||
style={`transform-origin: ${c.x}px ${c.y}px`}
|
||||
/>
|
||||
{/each}
|
||||
|
||||
<style>
|
||||
.hit {
|
||||
.flash {
|
||||
fill: #fffdf0;
|
||||
stroke: #c0392b;
|
||||
stroke-width: 1;
|
||||
animation: flash-snap 0.28s ease-out forwards;
|
||||
}
|
||||
.ring {
|
||||
transform-box: fill-box;
|
||||
transform-origin: center;
|
||||
fill: none;
|
||||
stroke: #c0392b;
|
||||
stroke-width: 3.5;
|
||||
animation: ring-small 0.45s ease-out forwards;
|
||||
stroke-width: 3;
|
||||
animation: ring-punch 0.4s cubic-bezier(0.2, 0.9, 0.3, 1) forwards;
|
||||
}
|
||||
@keyframes ring-small {
|
||||
0% { opacity: 0.9; transform: scale(1); }
|
||||
100% { opacity: 0; transform: scale(2.6); }
|
||||
.debris {
|
||||
stroke: #7c221a;
|
||||
stroke-width: 2;
|
||||
stroke-linecap: round;
|
||||
opacity: 0;
|
||||
animation: debris-fly 0.35s ease-out 0.05s forwards;
|
||||
}
|
||||
.debris.d1 { animation-delay: 0.08s; }
|
||||
.debris.d2 { animation-delay: 0.11s; }
|
||||
@keyframes flash-snap {
|
||||
0% { opacity: 1; transform: scale(0.4); }
|
||||
35% { opacity: 1; transform: scale(1.25); }
|
||||
100% { opacity: 0; transform: scale(0.9); }
|
||||
}
|
||||
@keyframes ring-punch {
|
||||
0% { opacity: 0.95; transform: scale(0.5, 0.35); }
|
||||
45% { opacity: 0.9; transform: scale(1.6, 1.3); }
|
||||
100% { opacity: 0; transform: scale(2.2, 1.9); }
|
||||
}
|
||||
@keyframes debris-fly {
|
||||
0% { opacity: 0.9; transform: scale(0.8); }
|
||||
100% { opacity: 0; transform: scale(1.7); }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,23 +1,45 @@
|
||||
<script lang="ts">
|
||||
import type { FxOf } from "../fx";
|
||||
import { center } from "./geom";
|
||||
|
||||
let { fx }: { fx: FxOf<"ooze-slip"> } = $props();
|
||||
const uid = $props.id();
|
||||
const c = $derived(center(fx.at));
|
||||
</script>
|
||||
|
||||
<ellipse cx={c.x} cy={c.y + 8} rx="13" ry="5" class="ooze" />
|
||||
<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" />
|
||||
<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" />
|
||||
</g>
|
||||
|
||||
<style>
|
||||
.ooze {
|
||||
.splat {
|
||||
transform-box: fill-box;
|
||||
transform-origin: center;
|
||||
fill: rgba(110, 160, 60, 0.55);
|
||||
animation: wobble 0.7s ease-out forwards;
|
||||
animation: splat-wobble 0.7s ease-out forwards;
|
||||
}
|
||||
@keyframes wobble {
|
||||
0% { opacity: 0.9; transform: scaleX(0.6); }
|
||||
35% { transform: scaleX(1.25); }
|
||||
65% { transform: scaleX(0.9); }
|
||||
100% { opacity: 0; transform: scaleX(1.1); }
|
||||
.blob { fill: #8fbf4d; animation: blob-fly 0.55s ease-out forwards; }
|
||||
.blob.late { animation-delay: 0.06s; }
|
||||
.blob.later { animation-delay: 0.12s; }
|
||||
@keyframes splat-wobble {
|
||||
0% { opacity: 0.95; transform: scale(0.5, 1); }
|
||||
35% { transform: scale(1.3, 0.85); }
|
||||
65% { transform: scale(0.9, 1.05); }
|
||||
100% { opacity: 0; transform: scale(1.15, 0.95); }
|
||||
}
|
||||
@keyframes blob-fly {
|
||||
0% { opacity: 0.9; transform: translateY(0); }
|
||||
50% { transform: translateY(-7px); }
|
||||
100% { opacity: 0; transform: translateY(2px); }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,29 +1,64 @@
|
||||
<script lang="ts">
|
||||
import type { FxOf } from "../fx";
|
||||
import { center } from "./geom";
|
||||
|
||||
let { fx }: { fx: FxOf<"pit-fall"> } = $props();
|
||||
const uid = $props.id();
|
||||
const c = $derived(center(fx.at));
|
||||
</script>
|
||||
|
||||
<circle cx={c.x} cy={c.y} r="10" class="pitfall" />
|
||||
<circle cx={c.x - 9} cy={c.y - 4} r="3" class="dust" />
|
||||
<circle cx={c.x + 9} cy={c.y - 4} r="3" class="dust late" />
|
||||
<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" />
|
||||
<!-- 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" />
|
||||
</g>
|
||||
|
||||
<style>
|
||||
.pitfall {
|
||||
.hole {
|
||||
transform-box: fill-box;
|
||||
transform-origin: center;
|
||||
fill: rgba(30, 24, 16, 0.8);
|
||||
animation: swallow 0.7s ease-in forwards;
|
||||
animation: hole-open 0.75s ease-out forwards;
|
||||
}
|
||||
.dust { fill: rgba(160, 150, 130, 0.75); animation: drift 0.7s ease-out forwards; }
|
||||
.dust.late { animation-delay: 0.09s; }
|
||||
@keyframes swallow {
|
||||
0% { opacity: 0.95; transform: scale(1) rotate(0deg); }
|
||||
100% { opacity: 0; transform: scale(0.05) rotate(50deg); }
|
||||
.faller {
|
||||
fill: rgba(120, 100, 70, 0.85);
|
||||
animation: fall-in 0.55s ease-in 0.06s forwards;
|
||||
}
|
||||
@keyframes drift {
|
||||
0% { opacity: 0.8; transform: translateY(0) scale(1); }
|
||||
100% { opacity: 0; transform: translateY(-10px) scale(1.8); }
|
||||
.lip {
|
||||
fill: none;
|
||||
stroke: rgba(233, 225, 203, 0.7);
|
||||
stroke-width: 1.6;
|
||||
stroke-linecap: round;
|
||||
animation: hole-open 0.75s ease-out forwards;
|
||||
}
|
||||
.dust { fill: rgba(160, 150, 130, 0.75); animation: dust-drift 0.7s ease-out 0.15s forwards; opacity: 0; }
|
||||
.dust.late { animation-delay: 0.24s; }
|
||||
.dust.later { animation-delay: 0.32s; }
|
||||
@keyframes hole-open {
|
||||
0% { opacity: 0; transform: scale(0.3); }
|
||||
25% { opacity: 1; transform: scale(1); }
|
||||
80% { opacity: 1; }
|
||||
100% { opacity: 0; transform: scale(1); }
|
||||
}
|
||||
@keyframes fall-in {
|
||||
0% { opacity: 1; transform: scale(1) translateY(-6px); }
|
||||
100% { opacity: 0; transform: scale(0.1) translateY(4px) rotate(60deg); }
|
||||
}
|
||||
@keyframes dust-drift {
|
||||
0% { opacity: 0; transform: translateY(0) scale(1); }
|
||||
30% { opacity: 0.8; }
|
||||
100% { opacity: 0; transform: translateY(-10px) scale(1.7); }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
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"}
|
||||
@@ -9,46 +10,100 @@
|
||||
{@const y1 = fx.side === "S" ? (fx.cell.y + 1) * CELL : fx.cell.y * CELL}
|
||||
{@const x2 = fx.side === "W" ? fx.cell.x * CELL : (fx.cell.x + 1) * CELL}
|
||||
{@const y2 = fx.side === "N" ? fx.cell.y * CELL : (fx.cell.y + 1) * CELL}
|
||||
<line {x1} {y1} {x2} {y2} class="portal glow" />
|
||||
<line {x1} {y1} {x2} {y2} class="portal" />
|
||||
<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"
|
||||
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}
|
||||
<g class="swirl" style={`transform-origin: ${vx}px ${vy}px`}>
|
||||
<path d={`M ${vx + 12} ${vy} A 12 12 0 0 1 ${vx - 6} ${vy + 10}`} class="arm" />
|
||||
<path d={`M ${vx - 12} ${vy} A 12 12 0 0 1 ${vx + 6} ${vy - 10}`} class="arm" />
|
||||
<path d={`M ${vx} ${vy + 7} A 7 7 0 0 1 ${vx - 6} ${vy - 4}`} class="arm faint" />
|
||||
</g>
|
||||
<rect x={fx.at.x * CELL + 3} y={fx.at.y * CELL + 3}
|
||||
width={CELL - 6} height={CELL - 6} rx="6" class="veil" />
|
||||
width={CELL - 6} height={CELL - 6} rx="6" class="veil outer"
|
||||
style={`transform-origin: ${fx.at.x * CELL + CELL / 2}px ${fx.at.y * CELL + CELL / 2}px`} />
|
||||
<rect x={fx.at.x * CELL + 3} y={fx.at.y * CELL + 3}
|
||||
width={CELL - 6} height={CELL - 6} rx="6" class="veil inner"
|
||||
style={`transform-origin: ${fx.at.x * CELL + CELL / 2}px ${fx.at.y * CELL + CELL / 2}px`} />
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.portal {
|
||||
/* The threshold opens, light ripples along it, and it pinches shut. */
|
||||
.curtain {
|
||||
stroke: #9be3e0;
|
||||
stroke-width: 3;
|
||||
stroke-dasharray: 6 5;
|
||||
stroke-linecap: round;
|
||||
filter: drop-shadow(0 0 4px rgba(155, 227, 224, 0.9));
|
||||
animation: shimmer 0.95s ease-in-out forwards;
|
||||
animation: open-ripple-pinch 0.95s ease-in-out forwards;
|
||||
}
|
||||
.portal.glow {
|
||||
.curtain.glow {
|
||||
stroke: rgba(180, 138, 224, 0.5);
|
||||
stroke-width: 9;
|
||||
stroke-dasharray: none;
|
||||
filter: blur(2px);
|
||||
animation: breathe 0.95s ease-in-out forwards;
|
||||
animation: glow-swell 0.95s ease-in-out forwards;
|
||||
}
|
||||
@keyframes open-ripple-pinch {
|
||||
0% { opacity: 0; stroke-width: 0.5; stroke-dashoffset: 0; }
|
||||
22% { opacity: 1; stroke-width: 6; }
|
||||
60% { stroke-width: 4.5; stroke-dashoffset: 22; }
|
||||
88% { opacity: 0.9; stroke-width: 1.5; stroke-dashoffset: 34; }
|
||||
100% { opacity: 0; stroke-width: 0.5; stroke-dashoffset: 38; }
|
||||
}
|
||||
@keyframes glow-swell {
|
||||
0% { opacity: 0; stroke-width: 2; }
|
||||
25% { opacity: 0.85; stroke-width: 14; }
|
||||
70% { opacity: 0.6; stroke-width: 9; }
|
||||
100% { opacity: 0; stroke-width: 2; }
|
||||
}
|
||||
/* Cell mouths: rings of energy collapse inward through the token. */
|
||||
.veil {
|
||||
fill: rgba(155, 227, 224, 0.12);
|
||||
fill: none;
|
||||
stroke: #9be3e0;
|
||||
stroke-width: 2.5;
|
||||
stroke-dasharray: 7 5;
|
||||
filter: drop-shadow(0 0 4px rgba(155, 227, 224, 0.8));
|
||||
animation: shimmer 0.95s ease-in-out forwards;
|
||||
}
|
||||
@keyframes shimmer {
|
||||
0% { opacity: 0; stroke-dashoffset: 0; }
|
||||
20% { opacity: 1; }
|
||||
80% { opacity: 0.85; }
|
||||
100% { opacity: 0; stroke-dashoffset: 34; }
|
||||
.void { animation: void-open 0.9s ease-out forwards; }
|
||||
.arm {
|
||||
fill: none;
|
||||
stroke: rgba(155, 227, 224, 0.7);
|
||||
stroke-width: 1.8;
|
||||
stroke-linecap: round;
|
||||
}
|
||||
@keyframes breathe {
|
||||
0% { opacity: 0; }
|
||||
30% { opacity: 0.8; }
|
||||
100% { opacity: 0; }
|
||||
.arm.faint { stroke: rgba(180, 138, 224, 0.55); }
|
||||
.swirl { animation: swirl-turn 0.9s ease-in forwards; }
|
||||
@keyframes swirl-turn {
|
||||
0% { opacity: 0; transform: rotate(0deg) scale(1.1); }
|
||||
25% { opacity: 1; }
|
||||
100% { opacity: 0; transform: rotate(160deg) scale(0.4); }
|
||||
}
|
||||
.veil.outer { stroke-width: 2.5; animation: veil-in 0.9s ease-in forwards; }
|
||||
.veil.inner {
|
||||
stroke: rgba(180, 138, 224, 0.8);
|
||||
stroke-width: 2;
|
||||
animation: veil-in 0.9s ease-in 0.2s forwards;
|
||||
opacity: 0;
|
||||
}
|
||||
@keyframes void-open {
|
||||
0% { opacity: 0; transform: scale(0.4); }
|
||||
30% { opacity: 1; transform: scale(1); }
|
||||
75% { opacity: 0.9; transform: scale(0.92); }
|
||||
100% { opacity: 0; transform: scale(0.5); }
|
||||
}
|
||||
@keyframes veil-in {
|
||||
0% { opacity: 0; transform: scale(1.25); }
|
||||
25% { opacity: 1; }
|
||||
100% { opacity: 0; transform: scale(0.45); }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,25 +1,63 @@
|
||||
<script lang="ts">
|
||||
import type { FxOf } from "../fx";
|
||||
import { center } 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`} />
|
||||
<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})`}
|
||||
/>
|
||||
<circle cx={c.x} cy={c.y} r="4" class="flash" />
|
||||
</g>
|
||||
{#each DASHES as deg (deg)}
|
||||
<line
|
||||
x1={c.x + 16 * Math.cos((deg * Math.PI) / 180)}
|
||||
y1={c.y + 16 * Math.sin((deg * Math.PI) / 180)}
|
||||
x2={c.x + 24 * Math.cos((deg * Math.PI) / 180)}
|
||||
y2={c.y + 24 * Math.sin((deg * Math.PI) / 180)}
|
||||
class="dash"
|
||||
style={`transform-origin: ${c.x}px ${c.y}px`}
|
||||
/>
|
||||
{/each}
|
||||
|
||||
<style>
|
||||
.pow path {
|
||||
fill: #ffe94d;
|
||||
stroke: #b3372b;
|
||||
stroke-width: 1.6;
|
||||
}
|
||||
.pow path { stroke: #b3372b; stroke-width: 1.6; }
|
||||
.pow .flash { fill: #ffffff; animation: flash 0.3s ease-out forwards; }
|
||||
.pow { animation: pow-hit 0.5s ease-out forwards; }
|
||||
.dash {
|
||||
stroke: #b3372b;
|
||||
stroke-width: 2.5;
|
||||
stroke-linecap: round;
|
||||
animation: dash-out 0.4s ease-out 0.06s forwards;
|
||||
opacity: 0;
|
||||
}
|
||||
@keyframes pow-hit {
|
||||
0% { opacity: 0; transform: scale(0.3) rotate(-15deg); }
|
||||
25% { opacity: 1; transform: scale(1.25) rotate(5deg); }
|
||||
55% { transform: scale(1) rotate(0deg); }
|
||||
100% { opacity: 0; transform: scale(1.05); }
|
||||
}
|
||||
@keyframes flash {
|
||||
0% { opacity: 1; transform: scale(1); }
|
||||
100% { opacity: 0; transform: scale(3); }
|
||||
}
|
||||
@keyframes dash-out {
|
||||
0% { opacity: 0; transform: scale(0.7); }
|
||||
30% { opacity: 0.9; }
|
||||
100% { opacity: 0; transform: scale(1.3); }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,48 +2,130 @@
|
||||
import type { FxOf } from "../fx";
|
||||
import { CELL, SECTOR } from "./geom";
|
||||
let { fx }: { fx: FxOf<"sector-spin" | "sector-slide"> } = $props();
|
||||
|
||||
const S = SECTOR * CELL;
|
||||
const origin = $derived(fx.kind === "sector-spin" ? fx.origin : fx.from);
|
||||
const ox = $derived(origin.x * CELL);
|
||||
const oy = $derived(origin.y * CELL);
|
||||
const cx = $derived(ox + S / 2);
|
||||
const cy = $derived(oy + S / 2);
|
||||
/** Slide direction, unit-ish, for the chevron train. */
|
||||
const dir = $derived.by(() => {
|
||||
if (fx.kind !== "sector-slide") return { x: 0, y: 0 };
|
||||
const dx = fx.to.x - fx.from.x, dy = fx.to.y - fx.from.y;
|
||||
const m = Math.max(1, Math.hypot(dx, dy));
|
||||
return { x: dx / m, y: dy / m };
|
||||
});
|
||||
const CORNERS = $derived([
|
||||
{ x: ox + 6, y: oy + 6 }, { x: ox + S - 6, y: oy + 6 },
|
||||
{ x: ox + 6, y: oy + S - 6 }, { x: ox + S - 6, y: oy + S - 6 },
|
||||
]);
|
||||
</script>
|
||||
|
||||
{#if fx.kind === "sector-spin"}
|
||||
{@const ox = fx.origin.x * CELL}
|
||||
{@const oy = fx.origin.y * CELL}
|
||||
<rect x={ox + 2} y={oy + 2} width={SECTOR * CELL - 4} height={SECTOR * CELL - 4}
|
||||
class={`spin ${fx.clockwise ? "cw" : "ccw"}`}
|
||||
style={`transform-origin: ${ox + (SECTOR * CELL) / 2}px ${oy + (SECTOR * CELL) / 2}px`} />
|
||||
<g class={`grind ${fx.clockwise ? "cw" : "ccw"}`} style={`transform-origin: ${cx}px ${cy}px`}>
|
||||
<rect x={ox + 2} y={oy + 2} width={S - 4} height={S - 4} class="frame" />
|
||||
<!-- Curved motion arcs at the corners, pointing the way around -->
|
||||
{#each [45, 135, 225, 315] as deg (deg)}
|
||||
<path
|
||||
d={`M ${cx + (S / 2 - 14) * Math.cos(((deg - 16) * Math.PI) / 180)} ${cy + (S / 2 - 14) * Math.sin(((deg - 16) * Math.PI) / 180)}
|
||||
A ${S / 2 - 14} ${S / 2 - 14} 0 0 ${fx.clockwise ? 1 : 0}
|
||||
${cx + (S / 2 - 14) * Math.cos(((deg + 16) * Math.PI) / 180)} ${cy + (S / 2 - 14) * Math.sin(((deg + 16) * Math.PI) / 180)}`}
|
||||
class="arc"
|
||||
/>
|
||||
{/each}
|
||||
</g>
|
||||
{:else}
|
||||
{@const fxp = fx.from.x * CELL}
|
||||
{@const fyp = fx.from.y * CELL}
|
||||
<rect x={fxp + 2} y={fyp + 2} width={SECTOR * CELL - 4} height={SECTOR * CELL - 4}
|
||||
class="slide"
|
||||
style={`--dx: ${(fx.to.x - fx.from.x) * CELL}px; --dy: ${(fx.to.y - fx.from.y) * CELL}px`} />
|
||||
<g class="slide-group" style={`--dx: ${(fx.to.x - fx.from.x) * CELL}px; --dy: ${(fx.to.y - fx.from.y) * CELL}px`}>
|
||||
<rect x={ox + 2} y={oy + 2} width={S - 4} height={S - 4} class="frame slide" />
|
||||
<!-- Chevron train pointing along the journey -->
|
||||
{#each [0.3, 0.5, 0.7] as t, i (t)}
|
||||
<path
|
||||
d={`M ${cx + dir.x * S * (t - 0.08) - dir.y * 9} ${cy + dir.y * S * (t - 0.08) - dir.x * 9}
|
||||
L ${cx + dir.x * S * t} ${cy + dir.y * S * t}
|
||||
L ${cx + dir.x * S * (t - 0.08) + dir.y * 9} ${cy + dir.y * S * (t - 0.08) + dir.x * 9}`}
|
||||
class={`chevron c${i}`}
|
||||
/>
|
||||
{/each}
|
||||
</g>
|
||||
{/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}`} />
|
||||
{/each}
|
||||
</g>
|
||||
|
||||
<style>
|
||||
.spin, .slide {
|
||||
.frame {
|
||||
fill: rgba(233, 225, 203, 0.25);
|
||||
stroke: #d3852b;
|
||||
stroke-width: 3;
|
||||
stroke-dasharray: 12 7;
|
||||
}
|
||||
.spin { animation: grind-cw 1.2s ease-in-out forwards; }
|
||||
.spin.ccw { animation-name: grind-ccw; }
|
||||
.slide { animation: slide-home 1.2s ease-in-out forwards; }
|
||||
@keyframes grind-cw {
|
||||
.arc {
|
||||
fill: none;
|
||||
stroke: #b3691f;
|
||||
stroke-width: 2.5;
|
||||
stroke-linecap: round;
|
||||
}
|
||||
.chevron {
|
||||
fill: none;
|
||||
stroke: #b3691f;
|
||||
stroke-width: 3;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
opacity: 0;
|
||||
}
|
||||
.chevron.c0 { animation: chev 0.5s ease-out 0.15s forwards; }
|
||||
.chevron.c1 { animation: chev 0.5s ease-out 0.3s forwards; }
|
||||
.chevron.c2 { animation: chev 0.5s ease-out 0.45s forwards; }
|
||||
.grind { animation: grind-settle-cw 1.25s ease-in-out forwards; }
|
||||
.grind.ccw { animation-name: grind-settle-ccw; }
|
||||
.slide-group { animation: slide-settle 1.25s ease-in-out forwards; }
|
||||
.dust {
|
||||
fill: rgba(160, 150, 130, 0.85);
|
||||
opacity: 0;
|
||||
animation: dust-kick 0.6s ease-out 0.85s forwards;
|
||||
}
|
||||
.dust.d1 { animation-delay: 0.9s; }
|
||||
.dust.d2 { animation-delay: 0.95s; }
|
||||
.dust.d3 { animation-delay: 1s; }
|
||||
@keyframes chev {
|
||||
0% { opacity: 0; }
|
||||
35% { opacity: 0.9; }
|
||||
100% { opacity: 0; }
|
||||
}
|
||||
/* The quarter-turn, then a grinding shudder as it settles. */
|
||||
@keyframes grind-settle-cw {
|
||||
0% { opacity: 0; transform: rotate(-90deg); }
|
||||
15% { opacity: 1; }
|
||||
85% { opacity: 1; transform: rotate(0deg); }
|
||||
100% { opacity: 0; }
|
||||
12% { opacity: 1; }
|
||||
72% { opacity: 1; transform: rotate(0.6deg); }
|
||||
80% { transform: rotate(-0.8deg); }
|
||||
87% { transform: rotate(0.4deg); }
|
||||
93% { transform: rotate(0deg); }
|
||||
100% { opacity: 0; transform: rotate(0deg); }
|
||||
}
|
||||
@keyframes grind-ccw {
|
||||
@keyframes grind-settle-ccw {
|
||||
0% { opacity: 0; transform: rotate(90deg); }
|
||||
15% { opacity: 1; }
|
||||
85% { opacity: 1; transform: rotate(0deg); }
|
||||
100% { opacity: 0; }
|
||||
12% { opacity: 1; }
|
||||
72% { opacity: 1; transform: rotate(-0.6deg); }
|
||||
80% { transform: rotate(0.8deg); }
|
||||
87% { transform: rotate(-0.4deg); }
|
||||
93% { transform: rotate(0deg); }
|
||||
100% { opacity: 0; transform: rotate(0deg); }
|
||||
}
|
||||
@keyframes slide-home {
|
||||
@keyframes slide-settle {
|
||||
0% { opacity: 0; transform: translate(0, 0); }
|
||||
15% { opacity: 1; }
|
||||
85% { opacity: 1; transform: translate(var(--dx), var(--dy)); }
|
||||
12% { opacity: 1; }
|
||||
72% { opacity: 1; transform: translate(var(--dx), var(--dy)); }
|
||||
80% { transform: translate(calc(var(--dx) - 2px), var(--dy)); }
|
||||
88% { transform: translate(calc(var(--dx) + 1px), var(--dy)); }
|
||||
94% { transform: translate(var(--dx), var(--dy)); }
|
||||
100% { opacity: 0; transform: translate(var(--dx), var(--dy)); }
|
||||
}
|
||||
@keyframes dust-kick {
|
||||
0% { opacity: 0; transform: translateY(0) scale(0.7); }
|
||||
30% { opacity: 0.9; }
|
||||
100% { opacity: 0; transform: translateY(-9px) scale(1.6); }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,26 +1,52 @@
|
||||
<script lang="ts">
|
||||
import type { FxOf } from "../fx";
|
||||
import { center } from "./geom";
|
||||
|
||||
let { fx }: { fx: FxOf<"shield"> } = $props();
|
||||
const uid = $props.id();
|
||||
const c = $derived(center(fx.at));
|
||||
</script>
|
||||
|
||||
<circle cx={c.x} cy={c.y} r="16" class="shield" />
|
||||
<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" />
|
||||
|
||||
<style>
|
||||
.shield {
|
||||
.dome {
|
||||
transform-box: fill-box;
|
||||
transform-origin: center;
|
||||
animation: dome 0.7s ease-out forwards;
|
||||
}
|
||||
.rim {
|
||||
transform-box: fill-box;
|
||||
transform-origin: center;
|
||||
fill: none;
|
||||
stroke: #c9a72a;
|
||||
stroke-width: 3.5;
|
||||
filter: drop-shadow(0 0 6px rgba(201, 167, 42, 0.8));
|
||||
animation: pulse 0.7s ease-out forwards;
|
||||
stroke: #fff3c4;
|
||||
stroke-width: 2;
|
||||
animation: rim-strike 0.7s ease-out forwards;
|
||||
}
|
||||
@keyframes pulse {
|
||||
@keyframes dome {
|
||||
0% { opacity: 0; transform: scale(0.6); }
|
||||
30% { opacity: 1; transform: scale(1.1); }
|
||||
60% { transform: scale(0.95); }
|
||||
100% { opacity: 0; transform: scale(1.15); }
|
||||
}
|
||||
@keyframes rim-strike {
|
||||
0% { opacity: 0; transform: scale(0.6); }
|
||||
28% { opacity: 1; transform: scale(1.12); }
|
||||
45% { opacity: 0.4; }
|
||||
58% { opacity: 0.9; transform: scale(0.95); }
|
||||
100% { opacity: 0; transform: scale(1.18); }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
<script lang="ts">
|
||||
import type { FxOf } from "../fx";
|
||||
import { center } 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="shimmer" />
|
||||
<circle cx={c.x} cy={c.y} r="6" class="shimmer late" />
|
||||
<circle cx={c.x} cy={c.y} r="6" class="ring" />
|
||||
<circle cx={c.x} cy={c.y} r="6" 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" />
|
||||
</g>
|
||||
|
||||
<style>
|
||||
.shimmer {
|
||||
.ring {
|
||||
transform-box: fill-box;
|
||||
transform-origin: center;
|
||||
fill: none;
|
||||
@@ -17,9 +23,17 @@
|
||||
stroke-width: 2.5;
|
||||
animation: ring 0.6s ease-out forwards;
|
||||
}
|
||||
.shimmer.late { stroke: #e2c8ff; animation-delay: 0.18s; opacity: 0; }
|
||||
.ring.late { stroke: #e2c8ff; animation-delay: 0.18s; opacity: 0; }
|
||||
.motes circle { 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); }
|
||||
}
|
||||
@keyframes mote {
|
||||
0% { opacity: 0; transform: translateY(0); }
|
||||
30% { opacity: 1; }
|
||||
100% { opacity: 0; transform: translateY(-12px); }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,22 +2,73 @@
|
||||
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>
|
||||
|
||||
<circle cx={c.x} cy={c.y} r="12" class="slime" />
|
||||
<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})`}
|
||||
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}
|
||||
C ${c.x + 14} ${c.y + 10}, ${c.x + 4} ${c.y + 12}, ${c.x - 3} ${c.y + 10}
|
||||
C ${c.x - 10} ${c.y + 12}, ${c.x - 12} ${c.y + 8}, ${c.x - 13} ${c.y + 2} z`}
|
||||
/>
|
||||
<!-- Elastic strands stretching toward the caught -->
|
||||
<g class="strands">
|
||||
<path d={`M ${c.x - 11} ${c.y - 6} Q ${c.x - 6} ${c.y - 2} ${c.x - 2} ${c.y}`} class="strand" />
|
||||
<path d={`M ${c.x + 12} ${c.y - 3} Q ${c.x + 6} ${c.y - 1} ${c.x + 2} ${c.y + 1}`} class="strand late" />
|
||||
<path d={`M ${c.x + 2} ${c.y + 10} Q ${c.x + 1} ${c.y + 5} ${c.x} ${c.y + 1}`} class="strand later" />
|
||||
</g>
|
||||
<circle cx={c.x + 4} cy={c.y - 5} r="2.5" class="bubble" />
|
||||
|
||||
<style>
|
||||
.slime {
|
||||
.goo { animation: goo-grip 0.9s ease-out forwards; }
|
||||
.strand {
|
||||
fill: none;
|
||||
stroke: #8fbf4d;
|
||||
stroke-width: 2;
|
||||
stroke-linecap: round;
|
||||
stroke-dasharray: 20 40;
|
||||
animation: strand-snap 0.55s ease-in 0.15s forwards;
|
||||
opacity: 0;
|
||||
}
|
||||
.strand.late { animation-delay: 0.28s; }
|
||||
.strand.later { animation-delay: 0.4s; }
|
||||
.bubble {
|
||||
transform-box: fill-box;
|
||||
transform-origin: center;
|
||||
fill: rgba(140, 200, 60, 0.5);
|
||||
stroke: #7a9e2e;
|
||||
stroke-width: 3;
|
||||
animation: sink 0.9s ease-out forwards;
|
||||
fill: none;
|
||||
stroke: #c8e87a;
|
||||
stroke-width: 1.5;
|
||||
animation: bubble-pop 0.6s ease-out 0.25s forwards;
|
||||
opacity: 0;
|
||||
}
|
||||
@keyframes sink {
|
||||
0% { opacity: 0.9; transform: scale(1.3); }
|
||||
100% { opacity: 0; transform: scale(0.7); }
|
||||
@keyframes goo-grip {
|
||||
0% { opacity: 0.95; transform: scale(1.3, 1.1) rotate(3deg); }
|
||||
40% { transform: scale(0.92, 1.06) rotate(-2deg); }
|
||||
70% { transform: scale(1.04, 0.96) rotate(1deg); }
|
||||
100% { opacity: 0; transform: scale(1) rotate(0deg); }
|
||||
}
|
||||
@keyframes strand-snap {
|
||||
0% { opacity: 0; stroke-dashoffset: 18; }
|
||||
30% { opacity: 0.9; stroke-dashoffset: 6; }
|
||||
75% { opacity: 0.85; stroke-dashoffset: 0; }
|
||||
100% { opacity: 0; stroke-dashoffset: -4; }
|
||||
}
|
||||
@keyframes bubble-pop {
|
||||
0% { opacity: 0; transform: scale(0.4); }
|
||||
50% { opacity: 0.9; transform: scale(1); }
|
||||
100% { opacity: 0; transform: scale(1.8); }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,26 +1,44 @@
|
||||
<script lang="ts">
|
||||
import type { FxOf } from "../fx";
|
||||
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">
|
||||
<circle cx={c.x} cy={c.y - 4} r="8" />
|
||||
<circle cx={c.x - 5} cy={c.y + 3} r="4" />
|
||||
<circle cx={c.x + 5} cy={c.y + 3} r="4" />
|
||||
<g class="sway" style={`transform-origin: ${c.x}px ${c.y}px`}>
|
||||
<!-- Rounded head, wavy hem -->
|
||||
<path
|
||||
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})`}
|
||||
/>
|
||||
<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>
|
||||
</g>
|
||||
|
||||
<style>
|
||||
.soul circle {
|
||||
fill: rgba(233, 228, 245, 0.8);
|
||||
stroke: rgba(160, 150, 200, 0.6);
|
||||
stroke-width: 1;
|
||||
}
|
||||
.soul { animation: ascend 1.3s ease-out forwards; }
|
||||
.sway { animation: sway 0.65s ease-in-out 2; }
|
||||
@keyframes ascend {
|
||||
0% { opacity: 0; transform: translateY(0); }
|
||||
25% { opacity: 0.9; }
|
||||
100% { opacity: 0; transform: translateY(-34px); }
|
||||
22% { opacity: 0.95; }
|
||||
100% { opacity: 0; transform: translateY(-36px); }
|
||||
}
|
||||
@keyframes sway {
|
||||
0%, 100% { transform: rotate(0deg); }
|
||||
50% { transform: rotate(6deg); }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -5,22 +5,56 @@
|
||||
const c = $derived(center(fx.at));
|
||||
</script>
|
||||
|
||||
<g class="sparkle" style={`transform-origin: ${c.x}px ${c.y}px`}>
|
||||
<g class="glint main" style={`transform-origin: ${c.x}px ${c.y}px`}>
|
||||
<path d={`M ${c.x} ${c.y - 12} l 3 9 9 3 -9 3 -3 9 -3 -9 -9 -3 9 -3 z`} />
|
||||
</g>
|
||||
<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" />
|
||||
<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" />
|
||||
</g>
|
||||
|
||||
<style>
|
||||
.sparkle path {
|
||||
fill: #e8dfc6;
|
||||
.glint.main path {
|
||||
fill: #ffe084;
|
||||
stroke: #c9a72a;
|
||||
stroke-width: 1;
|
||||
animation: fade 0.8s ease-in forwards;
|
||||
}
|
||||
.sparkle { animation: spin 0.8s linear forwards; }
|
||||
@keyframes fade { 70% { opacity: 1; } 100% { opacity: 0; } }
|
||||
@keyframes spin {
|
||||
0% { opacity: 0; transform: rotate(0deg) scale(0.5); }
|
||||
30% { opacity: 1; }
|
||||
100% { opacity: 0; transform: rotate(90deg) scale(1.1); }
|
||||
.glint.counter path { fill: #fff6d8; opacity: 0.9; }
|
||||
.glint.main { animation: spin-snap 0.7s ease-out forwards; }
|
||||
.glint.counter { animation: counter-spin 0.7s ease-out forwards; }
|
||||
.core {
|
||||
transform-box: fill-box;
|
||||
transform-origin: center;
|
||||
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 .late { animation-delay: 0.18s; }
|
||||
.motes .later { animation-delay: 0.24s; }
|
||||
@keyframes spin-snap {
|
||||
0% { opacity: 0; transform: rotate(-20deg) scale(0.4); }
|
||||
30% { opacity: 1; transform: rotate(15deg) scale(1.2); }
|
||||
55% { transform: rotate(30deg) scale(1); }
|
||||
100% { opacity: 0; transform: rotate(55deg) scale(0.9); }
|
||||
}
|
||||
@keyframes counter-spin {
|
||||
0% { opacity: 0; transform: rotate(45deg) scale(0.4); }
|
||||
30% { opacity: 0.95; transform: rotate(20deg) scale(1.15); }
|
||||
100% { opacity: 0; transform: rotate(-25deg) scale(0.85); }
|
||||
}
|
||||
@keyframes core-snap {
|
||||
0% { opacity: 0; transform: scale(0.3); }
|
||||
30% { opacity: 1; transform: scale(2); }
|
||||
100% { opacity: 0; transform: scale(0.6); }
|
||||
}
|
||||
@keyframes mote-fly {
|
||||
0% { opacity: 0; transform: translateY(0) scale(1); }
|
||||
30% { opacity: 0.95; }
|
||||
100% { opacity: 0; transform: translateY(-8px) scale(0.6); }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,33 +1,59 @@
|
||||
<script lang="ts">
|
||||
import type { FxOf } from "../fx";
|
||||
import { center } from "./geom";
|
||||
|
||||
let { fx }: { fx: FxOf<"splash"> } = $props();
|
||||
const uid = $props.id();
|
||||
const c = $derived(center(fx.at));
|
||||
</script>
|
||||
|
||||
<circle cx={c.x} cy={c.y} r="4" class="splash" />
|
||||
<circle cx={c.x - 10} cy={c.y - 6} r="2.5" class="droplet" />
|
||||
<circle cx={c.x + 9} cy={c.y - 8} r="2" class="droplet late" />
|
||||
<circle cx={c.x + 3} cy={c.y - 12} r="1.8" class="droplet later" />
|
||||
<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" />
|
||||
<!-- 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" />
|
||||
</g>
|
||||
|
||||
<style>
|
||||
.splash {
|
||||
.pool {
|
||||
transform-box: fill-box;
|
||||
transform-origin: center;
|
||||
animation: pool-spread 0.6s ease-out forwards;
|
||||
}
|
||||
.ring {
|
||||
transform-box: fill-box;
|
||||
transform-origin: center;
|
||||
fill: none;
|
||||
stroke: #4fa3e3;
|
||||
stroke-width: 3.5;
|
||||
animation: ring 0.55s ease-out forwards;
|
||||
stroke: #7cc0ee;
|
||||
stroke-width: 3.2;
|
||||
animation: ring-out 0.55s ease-out forwards;
|
||||
}
|
||||
.droplet { fill: #7cc0ee; animation: drop 0.6s ease-out forwards; }
|
||||
.droplet.late { animation-delay: 0.08s; }
|
||||
.droplet.later { animation-delay: 0.15s; }
|
||||
@keyframes ring {
|
||||
0% { opacity: 0.95; transform: scale(1); }
|
||||
100% { opacity: 0; transform: scale(5); }
|
||||
.drop { fill: #7cc0ee; animation: drop-arc 0.6s ease-in forwards; }
|
||||
.drop.late { animation-delay: 0.07s; }
|
||||
.drop.later { animation-delay: 0.13s; }
|
||||
@keyframes pool-spread {
|
||||
0% { opacity: 0.9; transform: scale(0.4); }
|
||||
100% { opacity: 0; transform: scale(2.2); }
|
||||
}
|
||||
@keyframes drop {
|
||||
0% { opacity: 0.9; transform: translateY(0); }
|
||||
100% { opacity: 0; transform: translateY(-14px); }
|
||||
@keyframes ring-out {
|
||||
0% { opacity: 0.9; transform: scale(1); }
|
||||
100% { opacity: 0; transform: scale(4); }
|
||||
}
|
||||
@keyframes drop-arc {
|
||||
0% { opacity: 0.95; transform: translateY(0); }
|
||||
45% { transform: translateY(-8px); }
|
||||
100% { opacity: 0; transform: translateY(6px); }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,23 +1,43 @@
|
||||
<script lang="ts">
|
||||
import type { FxOf } from "../fx";
|
||||
import { center } from "./geom";
|
||||
|
||||
let { fx }: { fx: FxOf<"streak"> } = $props();
|
||||
const a = $derived(center(fx.from));
|
||||
const b = $derived(center(fx.to));
|
||||
const d = $derived(`M ${a.x} ${a.y} L ${b.x} ${b.y}`);
|
||||
</script>
|
||||
|
||||
<line x1={a.x} y1={a.y} x2={b.x} y2={b.y} class="streak" />
|
||||
<path {d} pathLength="100" class="trail wide" />
|
||||
<path {d} pathLength="100" class="trail bright" />
|
||||
<circle r="4" class="head" cx="0" cy="0">
|
||||
<animateMotion dur="0.38s" fill="freeze" path={d} calcMode="spline"
|
||||
keySplines="0.3 0 0.4 1" keyTimes="0;1" keyPoints="0;1" />
|
||||
</circle>
|
||||
|
||||
<style>
|
||||
.streak {
|
||||
stroke: rgba(233, 225, 203, 0.85);
|
||||
stroke-width: 5;
|
||||
.trail {
|
||||
fill: none;
|
||||
stroke-linecap: round;
|
||||
stroke-dasharray: 14 9;
|
||||
animation: streak-fade 0.55s ease-out forwards;
|
||||
stroke-dasharray: 100 100;
|
||||
animation: draw-collapse 0.68s ease-out forwards;
|
||||
}
|
||||
@keyframes streak-fade {
|
||||
0% { opacity: 0.9; stroke-dashoffset: 46; }
|
||||
100% { opacity: 0; stroke-dashoffset: 0; }
|
||||
.trail.wide { stroke: rgba(233, 225, 203, 0.3); stroke-width: 5; }
|
||||
.trail.bright { stroke: rgba(255, 253, 240, 0.75); stroke-width: 2; }
|
||||
.head {
|
||||
fill: #fffdf0;
|
||||
animation: head-life 0.6s ease-out forwards;
|
||||
}
|
||||
/* Draw from source to head, then the tail chases it into the destination. */
|
||||
@keyframes draw-collapse {
|
||||
0% { stroke-dashoffset: 100; opacity: 0.95; }
|
||||
56% { stroke-dashoffset: 0; opacity: 0.95; }
|
||||
100% { stroke-dashoffset: -100; opacity: 0; }
|
||||
}
|
||||
@keyframes head-life {
|
||||
0% { opacity: 0; }
|
||||
12% { opacity: 1; }
|
||||
60% { opacity: 1; }
|
||||
100% { opacity: 0; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,26 +3,41 @@
|
||||
import { center } from "./geom";
|
||||
let { fx }: { fx: FxOf<"tacks-ow"> } = $props();
|
||||
const c = $derived(center(fx.at));
|
||||
// Each tack: head disc + spike, at its own angle and beat.
|
||||
const TACKS = [
|
||||
{ x: -10, y: -4, rot: -18, cls: "" },
|
||||
{ x: 5, y: -9, rot: 12, cls: "late" },
|
||||
{ x: -1, y: 5, rot: -5, cls: "later" },
|
||||
];
|
||||
</script>
|
||||
|
||||
<g class="tacks">
|
||||
<text x={c.x - 10} y={c.y - 4}>✱</text>
|
||||
<text x={c.x + 4} y={c.y - 10} class="late">✱</text>
|
||||
<text x={c.x - 2} y={c.y + 6} class="later">✱</text>
|
||||
</g>
|
||||
{#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 -2.1 1.5 L 0 -7.5 L 2.1 1.5 z" class="spike" />
|
||||
</g>
|
||||
</g>
|
||||
{/each}
|
||||
|
||||
<style>
|
||||
.tacks text {
|
||||
font-size: 13px;
|
||||
fill: #b3372b;
|
||||
animation: hop 0.6s ease-out forwards;
|
||||
.tack .head {
|
||||
fill: #8a2f24;
|
||||
stroke: #5c1c14;
|
||||
stroke-width: 0.8;
|
||||
}
|
||||
.tacks .late { animation-delay: 0.1s; opacity: 0; }
|
||||
.tacks .later { animation-delay: 0.2s; opacity: 0; }
|
||||
@keyframes hop {
|
||||
0% { opacity: 0; transform: translateY(2px); }
|
||||
30% { opacity: 1; transform: translateY(-5px); }
|
||||
60% { transform: translateY(-1px); }
|
||||
100% { opacity: 0; transform: translateY(-8px); }
|
||||
.tack .spike { fill: #b9b3a4; stroke: #6e6450; stroke-width: 0.5; }
|
||||
.tack {
|
||||
opacity: 0;
|
||||
animation: tack-hop 0.55s ease-out forwards;
|
||||
}
|
||||
.tack.late { animation-delay: 0.1s; }
|
||||
.tack.later { animation-delay: 0.2s; }
|
||||
@keyframes tack-hop {
|
||||
0% { opacity: 0; transform: translateY(4px); }
|
||||
22% { opacity: 1; transform: translateY(-10px); }
|
||||
50% { transform: translateY(-2px); }
|
||||
72% { transform: translateY(-7px); }
|
||||
100% { opacity: 0; transform: translateY(-2px); }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -5,22 +5,56 @@
|
||||
const c = $derived(center(fx.at));
|
||||
</script>
|
||||
|
||||
<path class="thorn"
|
||||
d={`M ${c.x - 12} ${c.y} l 6 -4 2 -8 4 6 8 -4 -3 8 7 5 -9 1 -2 9 -5 -7 -8 2 z`} />
|
||||
<g class="bush" style={`transform-origin: ${c.x}px ${c.y + 10}px`}>
|
||||
<!-- 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" />
|
||||
</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" />
|
||||
</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" />
|
||||
</g>
|
||||
</g>
|
||||
<g class="leaves">
|
||||
<ellipse cx={c.x - 12} cy={c.y - 10} rx="3" ry="1.6" class="leaf" transform={`rotate(-30 ${c.x - 12} ${c.y - 10})`} />
|
||||
<ellipse cx={c.x + 13} cy={c.y - 8} rx="2.6" ry="1.4" class="leaf late" transform={`rotate(40 ${c.x + 13} ${c.y - 8})`} />
|
||||
</g>
|
||||
|
||||
<style>
|
||||
.thorn {
|
||||
fill: rgba(70, 120, 50, 0.7);
|
||||
stroke: #2f5423;
|
||||
stroke-width: 1.5;
|
||||
transform-box: fill-box;
|
||||
transform-origin: center;
|
||||
animation: snap 0.55s ease-out forwards;
|
||||
.stem {
|
||||
fill: none;
|
||||
stroke: #467832;
|
||||
stroke-width: 2.5;
|
||||
stroke-linecap: round;
|
||||
}
|
||||
@keyframes snap {
|
||||
0% { opacity: 0; transform: scale(0.3) rotate(-15deg); }
|
||||
25% { opacity: 1; transform: scale(1.25) rotate(5deg); }
|
||||
55% { transform: scale(1) rotate(0deg); }
|
||||
100% { opacity: 0; transform: scale(1.05); }
|
||||
.spur {
|
||||
stroke: #2f5423;
|
||||
stroke-width: 1.6;
|
||||
stroke-linecap: round;
|
||||
}
|
||||
.branch { animation: spring 0.5s cubic-bezier(0.2, 1.4, 0.4, 1) forwards; opacity: 0; }
|
||||
.branch.b1 { animation-delay: 0.05s; }
|
||||
.branch.b2 { animation-delay: 0.1s; }
|
||||
.bush { animation: bush-fade 0.7s ease-out forwards; }
|
||||
.leaf { fill: #7fae55; animation: leaf-fall 0.7s ease-in 0.15s forwards; opacity: 0; }
|
||||
.leaf.late { animation-delay: 0.24s; }
|
||||
@keyframes spring {
|
||||
0% { opacity: 0; transform: scaleY(0.15); }
|
||||
60% { opacity: 1; transform: scaleY(1.12); }
|
||||
100% { opacity: 1; transform: scaleY(1); }
|
||||
}
|
||||
@keyframes bush-fade {
|
||||
0%, 70% { opacity: 1; }
|
||||
100% { opacity: 0; }
|
||||
}
|
||||
@keyframes leaf-fall {
|
||||
0% { opacity: 0; transform: translateY(0) rotate(0deg); }
|
||||
25% { opacity: 0.9; }
|
||||
100% { opacity: 0; transform: translateY(10px) rotate(50deg); }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,22 +1,66 @@
|
||||
<script lang="ts">
|
||||
import type { FxOf } from "../fx";
|
||||
import { center } from "./geom";
|
||||
|
||||
let { fx }: { fx: FxOf<"waterbolt"> } = $props();
|
||||
const uid = $props.id();
|
||||
|
||||
const a = $derived(center(fx.from));
|
||||
const b = $derived(center(fx.to));
|
||||
</script>
|
||||
|
||||
<circle r="6" class="waterbolt" cx="0" cy="0">
|
||||
<animateMotion dur="0.42s" fill="freeze" path={`M ${a.x} ${a.y} L ${b.x} ${b.y}`} />
|
||||
</circle>
|
||||
<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>
|
||||
</g>
|
||||
<animateMotion dur="0.42s" fill="freeze" rotate="auto" path={`M ${a.x} ${a.y} L ${b.x} ${b.y}`} />
|
||||
</g>
|
||||
|
||||
<style>
|
||||
.waterbolt {
|
||||
fill: #3b8dd6;
|
||||
stroke: #b9e2ff;
|
||||
stroke-width: 2;
|
||||
filter: drop-shadow(0 0 5px rgba(80, 160, 230, 0.9));
|
||||
animation: fade 0.55s ease-in forwards;
|
||||
opacity: 0;
|
||||
animation: wb-fade 0.55s ease-in forwards;
|
||||
}
|
||||
@keyframes wb-fade {
|
||||
0%, 70% { opacity: 1; }
|
||||
100% { opacity: 0; }
|
||||
}
|
||||
@keyframes fade { 70% { opacity: 1; } 100% { opacity: 0; } }
|
||||
</style>
|
||||
|
||||
@@ -5,17 +5,34 @@
|
||||
const c = $derived(center(fx.at));
|
||||
</script>
|
||||
|
||||
<circle cx={c.x} cy={c.y} r="8" class="whiff" />
|
||||
<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" />
|
||||
</g>
|
||||
|
||||
<style>
|
||||
.whiff {
|
||||
transform-box: fill-box;
|
||||
transform-origin: center;
|
||||
fill: rgba(180, 180, 180, 0.4);
|
||||
animation: drift 0.7s ease-out forwards;
|
||||
.streak {
|
||||
fill: none;
|
||||
stroke: rgba(110, 125, 140, 0.9);
|
||||
stroke-width: 3;
|
||||
stroke-linecap: round;
|
||||
stroke-dasharray: 38 100;
|
||||
stroke-dashoffset: 38;
|
||||
animation: sweep 0.5s ease-out forwards;
|
||||
}
|
||||
@keyframes drift {
|
||||
0% { opacity: 0.8; transform: translateY(0) scale(1); }
|
||||
100% { opacity: 0; transform: translateY(-10px) scale(1.8); }
|
||||
.streak.b { animation-delay: 0.06s; stroke-width: 3.6; }
|
||||
.streak.c { animation-delay: 0.12s; stroke-width: 2.6; }
|
||||
.whiff { animation: wobble 0.55s ease-in-out forwards; }
|
||||
@keyframes sweep {
|
||||
0% { stroke-dashoffset: 38; opacity: 0; }
|
||||
18% { opacity: 0.95; }
|
||||
100% { stroke-dashoffset: -100; opacity: 0; }
|
||||
}
|
||||
@keyframes wobble {
|
||||
0% { transform: rotate(0deg); }
|
||||
40% { transform: rotate(-4deg); }
|
||||
75% { transform: rotate(2deg); }
|
||||
100% { transform: rotate(0deg); }
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user