The review lands: twelve sprites get their motion stories
The outside review's diagnosis was right — the weak sprites had one element doing one motion. Now each tells its story: the whiff is three air streaks sweeping past a dodger with a wobble; the hit is a contact flash, a compressed ring punched outward, and flying debris; the streak's head visibly travels, drawing a tapered trail that collapses into the destination; the sparkle snaps white-hot between counter-rotating glints and sheds motes; the tacks are drawn silhouettes now, points up as a tack on the floor menaces, hopping in sequence; the claw slashes reveal one by one along their length with a bright leading edge while the whole strike rakes sideways. The sector ghosts keep their dashed frames but earn motion arcs, directional chevrons, corner dust, and a grinding shudder as they settle. The portal opens wide, ripples along its length, and pinches shut; cell mouths collapse rings of energy inward. The thornbush springs actual branches with thorn spurs. The slime is an asymmetric blob that grips, with elastic strands snapping toward the caught. Dust gains contrast, varied grain, and blows perpendicular to the wall that shed it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
a88cae3a0e
commit
bcaa06251a
@@ -5,6 +5,11 @@
|
||||
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>
|
||||
@@ -16,18 +21,50 @@
|
||||
</defs>
|
||||
|
||||
<g class="claw">
|
||||
<!-- Three tapered gashes: wide at entry, pointed at exit -->
|
||||
<path d={`M ${c.x - 12} ${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 - 3} ${c.y - 16} q 4 12 9 26 q -1 1 -2 0 q -6 -13 -10 -25 z`} fill={`url(#claw-cut-${uid})`} />
|
||||
<path d={`M ${c.x + 6} ${c.y - 14} q 4 12 10 26 q -1 1 -2 0 q -7 -13 -11 -25 z`} fill={`url(#claw-cut-${uid})`} />
|
||||
{#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>
|
||||
/* 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(-7px); }
|
||||
18% { opacity: 1; }
|
||||
70% { opacity: 0.9; }
|
||||
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>
|
||||
|
||||
@@ -12,39 +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>
|
||||
|
||||
<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="#b5a88e" stop-opacity="0.85" />
|
||||
<stop offset="0" stop-color="#a5977a" stop-opacity="0.95" />
|
||||
<stop offset="0.75" stop-color="#8d8266" stop-opacity="0.55" />
|
||||
<stop offset="1" stop-color="#8d8266" stop-opacity="0" />
|
||||
</radialGradient>
|
||||
</defs>
|
||||
|
||||
<circle cx={c.x - 6} cy={c.y} r="6" fill={`url(#dust-soft-${uid})`} class="dust" />
|
||||
<circle cx={c.x + 5} cy={c.y - 3} r="5" fill={`url(#dust-soft-${uid})`} class="dust late" />
|
||||
<circle cx={c.x} cy={c.y + 4} r="5.5" fill={`url(#dust-soft-${uid})`} class="dust later" />
|
||||
<g class="grit">
|
||||
<circle cx={c.x - 4} cy={c.y - 5} r="1" />
|
||||
<circle cx={c.x + 6} cy={c.y + 2} r="1.2" />
|
||||
<circle cx={c.x + 1} cy={c.y - 2} r="0.8" />
|
||||
</g>
|
||||
|
||||
<style>
|
||||
.dust {
|
||||
.puff {
|
||||
transform-box: fill-box;
|
||||
transform-origin: center;
|
||||
animation: drift 0.75s ease-out forwards;
|
||||
animation: blow-a 0.8s ease-out forwards;
|
||||
}
|
||||
.dust.late { animation-delay: 0.09s; }
|
||||
.dust.later { animation-delay: 0.17s; }
|
||||
.grit circle { fill: #6e6450; animation: grit-fall 0.6s ease-in forwards; }
|
||||
@keyframes drift {
|
||||
0% { opacity: 0.9; transform: translateY(0) scale(1); }
|
||||
100% { opacity: 0; transform: translateY(-12px) scale(2); }
|
||||
.puff.back { animation-name: blow-b; }
|
||||
.puff.late { animation-delay: 0.08s; }
|
||||
.puff.later { animation-delay: 0.16s; }
|
||||
.grit circle { fill: #5c523e; 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: 0.9; transform: translateY(-3px); }
|
||||
100% { opacity: 0; transform: translateY(7px); }
|
||||
0% { opacity: 1; transform: translateY(-3px); }
|
||||
100% { opacity: 0; transform: translateY(8px); }
|
||||
}
|
||||
</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>
|
||||
|
||||
@@ -9,46 +9,62 @@
|
||||
{@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}
|
||||
<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; }
|
||||
.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 breathe {
|
||||
0% { opacity: 0; }
|
||||
30% { opacity: 0.8; }
|
||||
100% { opacity: 0; }
|
||||
@keyframes veil-in {
|
||||
0% { opacity: 0; transform: scale(1.25); }
|
||||
25% { opacity: 1; }
|
||||
100% { opacity: 0; transform: scale(0.45); }
|
||||
}
|
||||
</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,7 +1,6 @@
|
||||
<script lang="ts">
|
||||
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));
|
||||
@@ -15,30 +14,57 @@
|
||||
</radialGradient>
|
||||
</defs>
|
||||
|
||||
<circle cx={c.x} cy={c.y} r="13" fill={`url(#slime-goo-${uid})`} class="goo" />
|
||||
<!-- 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" />
|
||||
<circle cx={c.x - 5} cy={c.y + 2} r="1.8" class="bubble late" />
|
||||
|
||||
<style>
|
||||
.goo {
|
||||
transform-box: fill-box;
|
||||
transform-origin: center;
|
||||
animation: goo-close 0.9s ease-out forwards;
|
||||
.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: none;
|
||||
stroke: #c8e87a;
|
||||
stroke-width: 1.5;
|
||||
animation: bubble-pop 0.6s ease-out 0.2s forwards;
|
||||
animation: bubble-pop 0.6s ease-out 0.25s forwards;
|
||||
opacity: 0;
|
||||
}
|
||||
.bubble.late { animation-delay: 0.4s; }
|
||||
@keyframes goo-close {
|
||||
0% { opacity: 0.95; transform: scale(1.35); }
|
||||
60% { opacity: 0.85; transform: scale(0.9); }
|
||||
100% { opacity: 0; transform: scale(1); }
|
||||
@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); }
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -3,42 +3,41 @@
|
||||
import { center } from "./geom";
|
||||
|
||||
let { fx }: { fx: FxOf<"streak"> } = $props();
|
||||
const uid = $props.id();
|
||||
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>
|
||||
|
||||
<defs>
|
||||
<linearGradient id={`streak-trail-${uid}`}
|
||||
x1={a.x} y1={a.y} x2={b.x} y2={b.y} gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#e9e1cb" stop-opacity="0" />
|
||||
<stop offset="0.7" stop-color="#e9e1cb" stop-opacity="0.6" />
|
||||
<stop offset="1" stop-color="#fffdf0" stop-opacity="0.95" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<line x1={a.x} y1={a.y} x2={b.x} y2={b.y} stroke={`url(#streak-trail-${uid})`} class="trail" />
|
||||
<circle cx={b.x} cy={b.y} r="5" class="head" />
|
||||
<path {d} pathLength="100" class="trail wide" />
|
||||
<path {d} pathLength="100" class="trail bright" />
|
||||
<circle r="4.5" class="head" cx="0" cy="0">
|
||||
<animateMotion dur="0.26s" fill="freeze" path={d} calcMode="spline"
|
||||
keySplines="0.3 0 0.4 1" keyTimes="0;1" keyPoints="0;1" />
|
||||
</circle>
|
||||
|
||||
<style>
|
||||
.trail {
|
||||
stroke-width: 6;
|
||||
fill: none;
|
||||
stroke-linecap: round;
|
||||
animation: trail-fade 0.55s ease-out forwards;
|
||||
stroke-dasharray: 100 100;
|
||||
animation: draw-collapse 0.55s ease-out forwards;
|
||||
}
|
||||
.trail.wide { stroke: rgba(233, 225, 203, 0.4); stroke-width: 7; }
|
||||
.trail.bright { stroke: rgba(255, 253, 240, 0.9); stroke-width: 3; }
|
||||
.head {
|
||||
transform-box: fill-box;
|
||||
transform-origin: center;
|
||||
fill: rgba(255, 253, 240, 0.9);
|
||||
animation: head-land 0.45s ease-out forwards;
|
||||
fill: #fffdf0;
|
||||
animation: head-life 0.5s ease-out forwards;
|
||||
}
|
||||
@keyframes trail-fade {
|
||||
0% { opacity: 0.95; }
|
||||
/* Draw from source to head, then the tail chases it into the destination. */
|
||||
@keyframes draw-collapse {
|
||||
0% { stroke-dashoffset: 100; opacity: 0.95; }
|
||||
47% { 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; }
|
||||
}
|
||||
@keyframes head-land {
|
||||
0% { opacity: 0; transform: scale(0.4); }
|
||||
35% { opacity: 1; transform: scale(1.3); }
|
||||
100% { opacity: 0; transform: scale(0.9); }
|
||||
}
|
||||
</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="2.5" r="2.6" class="head" />
|
||||
<path d="M -1.4 1 L 0 -5 L 1.4 1 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(3px); }
|
||||
25% { opacity: 1; transform: translateY(-6px); }
|
||||
55% { transform: translateY(-1px); }
|
||||
75% { transform: translateY(-4px); }
|
||||
100% { opacity: 0; transform: translateY(-1px); }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,32 +1,56 @@
|
||||
<script lang="ts">
|
||||
import type { FxOf } from "../fx";
|
||||
import { center } from "./geom";
|
||||
|
||||
let { fx }: { fx: FxOf<"thorn-snap"> } = $props();
|
||||
const c = $derived(center(fx.at));
|
||||
</script>
|
||||
|
||||
<path class="thorn" style={`transform-origin: ${c.x}px ${c.y}px`}
|
||||
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.75);
|
||||
stroke: #2f5423;
|
||||
stroke-width: 1.5;
|
||||
animation: snap 0.55s ease-out forwards;
|
||||
.stem {
|
||||
fill: none;
|
||||
stroke: #467832;
|
||||
stroke-width: 2.5;
|
||||
stroke-linecap: round;
|
||||
}
|
||||
.leaf { fill: #7fae55; animation: leaf-fall 0.7s ease-in 0.1s forwards; opacity: 0; }
|
||||
.leaf.late { animation-delay: 0.18s; }
|
||||
@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); }
|
||||
|
||||
@@ -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 - 20} ${c.y - 8} q 14 -5 40 -3`} pathLength="100" class="streak a" />
|
||||
<path d={`M ${c.x - 22} ${c.y + 1} q 16 3 42 0`} pathLength="100" class="streak b" />
|
||||
<path d={`M ${c.x - 18} ${c.y + 9} q 12 5 38 4`} 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(190, 200, 210, 0.85);
|
||||
stroke-width: 2.4;
|
||||
stroke-linecap: round;
|
||||
stroke-dasharray: 30 100;
|
||||
stroke-dashoffset: 30;
|
||||
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; }
|
||||
.streak.c { animation-delay: 0.12s; stroke-width: 2; }
|
||||
.whiff { animation: wobble 0.55s ease-in-out forwards; }
|
||||
@keyframes sweep {
|
||||
0% { stroke-dashoffset: 30; opacity: 0; }
|
||||
20% { opacity: 0.9; }
|
||||
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