Every flourish gets its own file, and a workshop to watch them in
The effects move out of Board.svelte into src/fx-sprites/ — one component per flourish, markup and animation CSS together, so editing the fireball means opening Fireball.svelte and nothing else. A registry (index.ts) maps kind to sprite; Board renders through it. The event mapping stays in fx.ts. And a viewer: /?fx opens the Flourish Workshop — every sprite on a labeled parchment tile, replaying on a 2.2-second beat. Under the dev server, editing any sprite file hot-reloads the gallery mid-loop. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
0fcbff4dc6
commit
a59ecfa615
@@ -2,6 +2,7 @@
|
||||
import type { GameView } from "@wizwar/engine";
|
||||
import type { Side } from "@wizwar/engine";
|
||||
import { colorIndexOf as sharedColorIndex, wizardColor } from "./colors";
|
||||
import { FX_SPRITES } from "./fx-sprites";
|
||||
|
||||
const CELL = 48;
|
||||
const WALL = 7;
|
||||
@@ -51,28 +52,7 @@
|
||||
effects?: import("./fx").BoardFx[] | null;
|
||||
} = $props();
|
||||
|
||||
const center = (c: { x: number; y: number }) => ({ x: c.x * CELL + CELL / 2, y: c.y * CELL + CELL / 2 });
|
||||
/** A lightning bolt's jagged path between two squares. */
|
||||
function boltPoints(from: { x: number; y: number }, to: { x: number; y: number }): string {
|
||||
const a = center(from), b = center(to);
|
||||
const segs = 6;
|
||||
const pts: string[] = [`${a.x},${a.y}`];
|
||||
for (let i = 1; i < segs; i++) {
|
||||
const t = i / segs;
|
||||
const nx = a.x + (b.x - a.x) * t + (Math.random() - 0.5) * 16;
|
||||
const ny = a.y + (b.y - a.y) * t + (Math.random() - 0.5) * 16;
|
||||
pts.push(`${nx.toFixed(1)},${ny.toFixed(1)}`);
|
||||
}
|
||||
pts.push(`${b.x},${b.y}`);
|
||||
return pts.join(" ");
|
||||
}
|
||||
function edgeMid(cell: { x: number; y: number }, side: string): { x: number; y: number } {
|
||||
const c = center(cell);
|
||||
if (side === "N") return { x: c.x, y: cell.y * CELL };
|
||||
if (side === "S") return { x: c.x, y: (cell.y + 1) * CELL };
|
||||
if (side === "W") return { x: cell.x * CELL, y: c.y };
|
||||
return { x: (cell.x + 1) * CELL, y: c.y };
|
||||
}
|
||||
|
||||
|
||||
const SECTOR = 5;
|
||||
/** Ghost slots can lie beyond the assembled maze — on any side, including
|
||||
@@ -607,145 +587,11 @@
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
<!-- spell flourishes: short-lived, purely cosmetic -->
|
||||
<!-- spell flourishes: one sprite component per effect (src/fx-sprites/) -->
|
||||
<g class="fx-layer" aria-hidden="true">
|
||||
{#each effects ?? [] as fx (fx.id)}
|
||||
{#if fx.kind === "fireball" || fx.kind === "waterbolt"}
|
||||
{@const a = center(fx.from)}
|
||||
{@const b = center(fx.to)}
|
||||
<circle r={fx.kind === "fireball" ? 7 : 6} class={`fx-${fx.kind}`} cx="0" cy="0">
|
||||
<animateMotion dur="0.42s" fill="freeze" path={`M ${a.x} ${a.y} L ${b.x} ${b.y}`} />
|
||||
</circle>
|
||||
{:else if fx.kind === "bolt"}
|
||||
<polyline points={boltPoints(fx.from, fx.to)} class="fx-bolt" />
|
||||
{:else if fx.kind === "burst"}
|
||||
{@const c = center(fx.at)}
|
||||
<circle cx={c.x} cy={c.y} r="4" class="fx-burst" />
|
||||
<circle cx={c.x} cy={c.y} r="4" class="fx-burst late" />
|
||||
{:else if fx.kind === "splash"}
|
||||
{@const c = center(fx.at)}
|
||||
<circle cx={c.x} cy={c.y} r="4" class="fx-splash" />
|
||||
<circle cx={c.x - 10} cy={c.y - 6} r="2.5" class="fx-droplet" />
|
||||
<circle cx={c.x + 9} cy={c.y - 8} r="2" class="fx-droplet late" />
|
||||
<circle cx={c.x + 3} cy={c.y - 12} r="1.8" class="fx-droplet later" />
|
||||
{:else if fx.kind === "shimmer"}
|
||||
{@const c = center(fx.at)}
|
||||
<circle cx={c.x} cy={c.y} r="6" class="fx-shimmer" />
|
||||
<circle cx={c.x} cy={c.y} r="6" class="fx-shimmer late" />
|
||||
{:else if fx.kind === "shield"}
|
||||
{@const c = center(fx.at)}
|
||||
<circle cx={c.x} cy={c.y} r="16" class="fx-shield" />
|
||||
{:else if fx.kind === "sparkle"}
|
||||
{@const c = center(fx.at)}
|
||||
<g class="fx-sparkle" 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>
|
||||
{:else if fx.kind === "whiff"}
|
||||
{@const c = center(fx.at)}
|
||||
<circle cx={c.x} cy={c.y} r="8" class="fx-whiff" />
|
||||
{:else if fx.kind === "hit"}
|
||||
{@const c = center(fx.at)}
|
||||
<circle cx={c.x} cy={c.y} r="10" class="fx-hit" />
|
||||
{:else if fx.kind === "pow"}
|
||||
{@const c = center(fx.at)}
|
||||
<g class="fx-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`} />
|
||||
</g>
|
||||
{:else if fx.kind === "claw"}
|
||||
{@const c = center(fx.at)}
|
||||
<g class="fx-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} />
|
||||
</g>
|
||||
{:else if fx.kind === "absorb"}
|
||||
{@const c = center(fx.at)}
|
||||
<rect x={c.x - 9} y={c.y - 13} width="18" height="26" rx="2" class="fx-absorb"
|
||||
style={`transform-origin: ${c.x}px ${c.y}px`} />
|
||||
{:else if fx.kind === "portal"}
|
||||
{@const x1 = fx.side === "E" ? (fx.cell.x + 1) * CELL : fx.cell.x * CELL}
|
||||
{@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="fx-portal glow" />
|
||||
<line {x1} {y1} {x2} {y2} class="fx-portal" />
|
||||
{:else if fx.kind === "portal-cell"}
|
||||
<rect x={fx.at.x * CELL + 3} y={fx.at.y * CELL + 3}
|
||||
width={CELL - 6} height={CELL - 6} rx="6" class="fx-portal-veil" />
|
||||
{:else if fx.kind === "streak"}
|
||||
{@const a = center(fx.from)}
|
||||
{@const b = center(fx.to)}
|
||||
<line x1={a.x} y1={a.y} x2={b.x} y2={b.y} class="fx-streak" />
|
||||
{:else if fx.kind === "soul"}
|
||||
{@const c = center(fx.at)}
|
||||
<g class="fx-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>
|
||||
{:else if fx.kind === "fireworks"}
|
||||
{@const c = center(fx.at)}
|
||||
<g class="fx-fireworks" style={`transform-origin: ${c.x}px ${c.y}px`}>
|
||||
{#each [0, 45, 90, 135, 180, 225, 270, 315] as deg (deg)}
|
||||
<circle
|
||||
cx={c.x + 18 * Math.cos((deg * Math.PI) / 180)}
|
||||
cy={c.y + 18 * Math.sin((deg * Math.PI) / 180)}
|
||||
r="3" fill={["#e74c3c", "#f1c40f", "#3b8dd6", "#7ac47e"][(deg / 45) % 4]}
|
||||
/>
|
||||
{/each}
|
||||
</g>
|
||||
{:else if fx.kind === "chaos-swirl"}
|
||||
{@const c = center(fx.at)}
|
||||
<g class="fx-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" />
|
||||
</g>
|
||||
{:else if fx.kind === "pit-fall"}
|
||||
{@const c = center(fx.at)}
|
||||
<circle cx={c.x} cy={c.y} r="10" class="fx-pitfall" />
|
||||
<circle cx={c.x - 9} cy={c.y - 4} r="3" class="fx-dust" />
|
||||
<circle cx={c.x + 9} cy={c.y - 4} r="3" class="fx-dust late" />
|
||||
{:else if fx.kind === "ooze-slip"}
|
||||
{@const c = center(fx.at)}
|
||||
<ellipse cx={c.x} cy={c.y + 8} rx="13" ry="5" class="fx-ooze" />
|
||||
{:else if fx.kind === "tacks-ow"}
|
||||
{@const c = center(fx.at)}
|
||||
<g class="fx-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>
|
||||
{:else if fx.kind === "thorn-snap"}
|
||||
{@const c = center(fx.at)}
|
||||
<path class="fx-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`} />
|
||||
{:else if fx.kind === "slime-stuck"}
|
||||
{@const c = center(fx.at)}
|
||||
<circle cx={c.x} cy={c.y} r="12" class="fx-slime" />
|
||||
{:else if fx.kind === "dust-puff"}
|
||||
{@const c = center(fx.at)}
|
||||
<circle cx={c.x - 6} cy={c.y} r="4" class="fx-dust" />
|
||||
<circle cx={c.x + 5} cy={c.y - 3} r="3" class="fx-dust late" />
|
||||
<circle cx={c.x} cy={c.y + 4} r="3.5" class="fx-dust later" />
|
||||
{:else 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={`fx-sector-spin ${fx.clockwise ? "cw" : "ccw"}`}
|
||||
style={`transform-origin: ${ox + (SECTOR * CELL) / 2}px ${oy + (SECTOR * CELL) / 2}px`} />
|
||||
{:else if fx.kind === "sector-slide"}
|
||||
{@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="fx-sector-slide"
|
||||
style={`--dx: ${(fx.to.x - fx.from.x) * CELL}px; --dy: ${(fx.to.y - fx.from.y) * CELL}px`} />
|
||||
{:else if fx.kind === "edge-dust"}
|
||||
{@const m = edgeMid(fx.cell, fx.side)}
|
||||
<circle cx={m.x - 6} cy={m.y} r="4" class="fx-dust" />
|
||||
<circle cx={m.x + 5} cy={m.y - 3} r="3" class="fx-dust late" />
|
||||
<circle cx={m.x} cy={m.y + 4} r="3.5" class="fx-dust later" />
|
||||
{/if}
|
||||
{@const Sprite = FX_SPRITES[fx.kind]}
|
||||
<Sprite fx={fx as never} />
|
||||
{/each}
|
||||
</g>
|
||||
</svg>
|
||||
@@ -893,268 +739,7 @@
|
||||
}
|
||||
.ghost-slot:hover { fill: rgba(122, 162, 122, 0.22); }
|
||||
|
||||
/* --- spell flourishes ---------------------------------------------- */
|
||||
.fx-layer { pointer-events: none; }
|
||||
.fx-fireball {
|
||||
fill: #ff8c1a;
|
||||
stroke: #ffd27a;
|
||||
stroke-width: 2;
|
||||
filter: drop-shadow(0 0 6px rgba(255, 120, 20, 0.9));
|
||||
animation: fx-fade 0.55s ease-in forwards;
|
||||
}
|
||||
.fx-waterbolt {
|
||||
fill: #3b8dd6;
|
||||
stroke: #b9e2ff;
|
||||
stroke-width: 2;
|
||||
filter: drop-shadow(0 0 5px rgba(80, 160, 230, 0.9));
|
||||
animation: fx-fade 0.55s ease-in forwards;
|
||||
}
|
||||
.fx-bolt {
|
||||
fill: none;
|
||||
stroke: #ffe94d;
|
||||
stroke-width: 3;
|
||||
stroke-linejoin: round;
|
||||
filter: drop-shadow(0 0 7px rgba(255, 233, 77, 0.95));
|
||||
animation: fx-flicker 0.5s steps(2, jump-none) forwards;
|
||||
}
|
||||
.fx-burst {
|
||||
transform-box: fill-box;
|
||||
transform-origin: center;
|
||||
fill: none;
|
||||
stroke: #ff8c1a;
|
||||
stroke-width: 4;
|
||||
animation: fx-ring 0.5s ease-out 0.05s forwards;
|
||||
opacity: 0;
|
||||
}
|
||||
.fx-burst.late { stroke: #ffd27a; animation-delay: 0.16s; }
|
||||
.fx-splash {
|
||||
transform-box: fill-box;
|
||||
transform-origin: center;
|
||||
fill: none;
|
||||
stroke: #4fa3e3;
|
||||
stroke-width: 3.5;
|
||||
animation: fx-ring 0.55s ease-out forwards;
|
||||
}
|
||||
.fx-droplet { fill: #7cc0ee; animation: fx-drop 0.6s ease-out forwards; }
|
||||
.fx-droplet.late { animation-delay: 0.08s; }
|
||||
.fx-droplet.later { animation-delay: 0.15s; }
|
||||
.fx-shimmer {
|
||||
transform-box: fill-box;
|
||||
transform-origin: center;
|
||||
fill: none;
|
||||
stroke: #b48ae0;
|
||||
stroke-width: 2.5;
|
||||
animation: fx-ring 0.6s ease-out forwards;
|
||||
}
|
||||
.fx-shimmer.late { stroke: #e2c8ff; animation-delay: 0.18s; opacity: 0; }
|
||||
.fx-shield {
|
||||
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: fx-shield-pulse 0.7s ease-out forwards;
|
||||
}
|
||||
.fx-sparkle path {
|
||||
fill: #e8dfc6;
|
||||
stroke: #c9a72a;
|
||||
stroke-width: 1;
|
||||
animation: fx-fade 0.8s ease-in forwards;
|
||||
}
|
||||
.fx-sparkle { animation: fx-spin 0.8s linear forwards; }
|
||||
.fx-whiff {
|
||||
transform-box: fill-box;
|
||||
transform-origin: center;
|
||||
fill: rgba(180, 180, 180, 0.4);
|
||||
animation: fx-drift 0.7s ease-out forwards;
|
||||
}
|
||||
.fx-hit {
|
||||
transform-box: fill-box;
|
||||
transform-origin: center;
|
||||
fill: none;
|
||||
stroke: #c0392b;
|
||||
stroke-width: 3.5;
|
||||
animation: fx-ring-small 0.45s ease-out forwards;
|
||||
}
|
||||
.fx-pow path {
|
||||
fill: #ffe94d;
|
||||
stroke: #b3372b;
|
||||
stroke-width: 1.6;
|
||||
}
|
||||
.fx-pow { animation: fx-pow-hit 0.5s ease-out forwards; }
|
||||
.fx-claw line {
|
||||
stroke: #b3372b;
|
||||
stroke-width: 3;
|
||||
stroke-linecap: round;
|
||||
}
|
||||
.fx-claw { animation: fx-claw-rake 0.55s ease-out forwards; }
|
||||
.fx-absorb {
|
||||
fill: #f6f0df;
|
||||
stroke: #43331f;
|
||||
stroke-width: 1.5;
|
||||
animation: fx-swallow 0.65s ease-in forwards;
|
||||
}
|
||||
.fx-portal {
|
||||
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: fx-portal-shimmer 0.95s ease-in-out forwards;
|
||||
}
|
||||
.fx-portal.glow {
|
||||
stroke: rgba(180, 138, 224, 0.5);
|
||||
stroke-width: 9;
|
||||
stroke-dasharray: none;
|
||||
filter: blur(2px);
|
||||
animation: fx-portal-breathe 0.95s ease-in-out forwards;
|
||||
}
|
||||
.fx-portal-veil {
|
||||
fill: rgba(155, 227, 224, 0.12);
|
||||
stroke: #9be3e0;
|
||||
stroke-width: 2.5;
|
||||
stroke-dasharray: 7 5;
|
||||
filter: drop-shadow(0 0 4px rgba(155, 227, 224, 0.8));
|
||||
animation: fx-portal-shimmer 0.95s ease-in-out forwards;
|
||||
}
|
||||
@keyframes fx-portal-shimmer {
|
||||
0% { opacity: 0; stroke-dashoffset: 0; }
|
||||
20% { opacity: 1; }
|
||||
80% { opacity: 0.85; }
|
||||
100% { opacity: 0; stroke-dashoffset: 34; }
|
||||
}
|
||||
@keyframes fx-portal-breathe {
|
||||
0% { opacity: 0; }
|
||||
30% { opacity: 0.8; }
|
||||
100% { opacity: 0; }
|
||||
}
|
||||
.fx-streak {
|
||||
stroke: rgba(233, 225, 203, 0.85);
|
||||
stroke-width: 5;
|
||||
stroke-linecap: round;
|
||||
stroke-dasharray: 14 9;
|
||||
animation: fx-streak-fade 0.55s ease-out forwards;
|
||||
}
|
||||
@keyframes fx-streak-fade {
|
||||
0% { opacity: 0.9; stroke-dashoffset: 46; }
|
||||
100% { opacity: 0; stroke-dashoffset: 0; }
|
||||
}
|
||||
.fx-soul circle {
|
||||
fill: rgba(233, 228, 245, 0.8);
|
||||
stroke: rgba(160, 150, 200, 0.6);
|
||||
stroke-width: 1;
|
||||
}
|
||||
.fx-soul { animation: fx-ascend 1.3s ease-out forwards; }
|
||||
@keyframes fx-ascend {
|
||||
0% { opacity: 0; transform: translateY(0); }
|
||||
25% { opacity: 0.9; }
|
||||
100% { opacity: 0; transform: translateY(-34px); }
|
||||
}
|
||||
.fx-fireworks { animation: fx-fireworks-boom 1.1s ease-out forwards; }
|
||||
@keyframes fx-fireworks-boom {
|
||||
0% { opacity: 0; transform: scale(0.1); }
|
||||
25% { opacity: 1; }
|
||||
100% { opacity: 0; transform: scale(2.4) rotate(30deg); }
|
||||
}
|
||||
.fx-chaos circle {
|
||||
fill: none;
|
||||
stroke: #b48ae0;
|
||||
stroke-width: 4;
|
||||
stroke-dasharray: 30 22;
|
||||
}
|
||||
.fx-chaos .mid { stroke: #8a5fc0; stroke-dasharray: 44 30; }
|
||||
.fx-chaos .outer { stroke: #e2c8ff; stroke-dasharray: 60 40; }
|
||||
.fx-chaos { animation: fx-chaos-spin 1.4s ease-in-out forwards; }
|
||||
@keyframes fx-chaos-spin {
|
||||
0% { opacity: 0; transform: rotate(0deg) scale(0.6); }
|
||||
20% { opacity: 1; }
|
||||
100% { opacity: 0; transform: rotate(200deg) scale(1.5); }
|
||||
}
|
||||
.fx-pitfall {
|
||||
transform-box: fill-box;
|
||||
transform-origin: center;
|
||||
fill: rgba(30, 24, 16, 0.8);
|
||||
animation: fx-swallow 0.7s ease-in forwards;
|
||||
}
|
||||
.fx-ooze {
|
||||
transform-box: fill-box;
|
||||
transform-origin: center;
|
||||
fill: rgba(110, 160, 60, 0.55);
|
||||
animation: fx-ooze-wobble 0.7s ease-out forwards;
|
||||
}
|
||||
@keyframes fx-ooze-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); }
|
||||
}
|
||||
.fx-tacks text {
|
||||
font-size: 13px;
|
||||
fill: #b3372b;
|
||||
animation: fx-hop 0.6s ease-out forwards;
|
||||
}
|
||||
.fx-tacks .late { animation-delay: 0.1s; opacity: 0; }
|
||||
.fx-tacks .later { animation-delay: 0.2s; opacity: 0; }
|
||||
@keyframes fx-hop {
|
||||
0% { opacity: 0; transform: translateY(2px); }
|
||||
30% { opacity: 1; transform: translateY(-5px); }
|
||||
60% { transform: translateY(-1px); }
|
||||
100% { opacity: 0; transform: translateY(-8px); }
|
||||
}
|
||||
.fx-thorn {
|
||||
fill: rgba(70, 120, 50, 0.7);
|
||||
stroke: #2f5423;
|
||||
stroke-width: 1.5;
|
||||
transform-box: fill-box;
|
||||
transform-origin: center;
|
||||
animation: fx-pow-hit 0.55s ease-out forwards;
|
||||
}
|
||||
.fx-slime {
|
||||
transform-box: fill-box;
|
||||
transform-origin: center;
|
||||
fill: rgba(140, 200, 60, 0.5);
|
||||
stroke: #7a9e2e;
|
||||
stroke-width: 3;
|
||||
animation: fx-slime-sink 0.9s ease-out forwards;
|
||||
}
|
||||
@keyframes fx-slime-sink {
|
||||
0% { opacity: 0.9; transform: scale(1.3); }
|
||||
100% { opacity: 0; transform: scale(0.7); }
|
||||
}
|
||||
.fx-sector-spin {
|
||||
fill: rgba(233, 225, 203, 0.25);
|
||||
stroke: #d3852b;
|
||||
stroke-width: 3;
|
||||
stroke-dasharray: 12 7;
|
||||
animation: fx-grind-cw 1.2s ease-in-out forwards;
|
||||
}
|
||||
.fx-sector-spin.ccw { animation-name: fx-grind-ccw; }
|
||||
@keyframes fx-grind-cw {
|
||||
0% { opacity: 0; transform: rotate(-90deg); }
|
||||
15% { opacity: 1; }
|
||||
85% { opacity: 1; transform: rotate(0deg); }
|
||||
100% { opacity: 0; }
|
||||
}
|
||||
@keyframes fx-grind-ccw {
|
||||
0% { opacity: 0; transform: rotate(90deg); }
|
||||
15% { opacity: 1; }
|
||||
85% { opacity: 1; transform: rotate(0deg); }
|
||||
100% { opacity: 0; }
|
||||
}
|
||||
.fx-sector-slide {
|
||||
fill: rgba(233, 225, 203, 0.25);
|
||||
stroke: #d3852b;
|
||||
stroke-width: 3;
|
||||
stroke-dasharray: 12 7;
|
||||
animation: fx-slide-home 1.2s ease-in-out forwards;
|
||||
}
|
||||
@keyframes fx-slide-home {
|
||||
0% { opacity: 0; transform: translate(0, 0); }
|
||||
15% { opacity: 1; }
|
||||
85% { opacity: 1; transform: translate(var(--dx), var(--dy)); }
|
||||
100% { opacity: 0; transform: translate(var(--dx), var(--dy)); }
|
||||
}
|
||||
|
||||
/* --- persistent ambiance: ongoing spells wear their look ------------ */
|
||||
.token-art.ghosted { opacity: 0.4; animation: ghost-breathe 2.6s ease-in-out infinite; }
|
||||
|
||||
Reference in New Issue
Block a user