SVG tokens render as vectors, not bitmaps of vectors
An svg referenced through an <image> element is rasterized once at its layout size in user units — about 29 pixels for a wizard — and scales as a blurry bitmap thereafter, defeating the entire point of vector art. TokenArt.svelte now fetches each file once, namespaces its ids (45 tokens' worth of filter and pattern defs share one document), and inlines it as a nested <svg>: live vector at any zoom, verified crisp at 3x device scale. PNG art keeps the <image> path untouched, and the token style family goes :global to reach the component's elements. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
c62c3b4f05
commit
1d015681d3
@@ -3,6 +3,7 @@
|
||||
import type { Side } from "@wizwar/engine";
|
||||
import { colorIndexOf as sharedColorIndex, wizardColor } from "./colors";
|
||||
import { tokenArt } from "./art";
|
||||
import TokenArt from "./TokenArt.svelte";
|
||||
import { FX_SPRITES } from "./fx-sprites";
|
||||
|
||||
const CELL = 48;
|
||||
@@ -275,12 +276,11 @@
|
||||
{@const tx = t.position.x * CELL + CELL / 2 + (group.length > 1 ? (ti - (group.length - 1) / 2) * CELL * 0.3 : 0)}
|
||||
{@const ty = t.position.y * CELL + CELL * 0.72}
|
||||
{#if USE_TOKEN_ART}
|
||||
<image
|
||||
<TokenArt
|
||||
href={treasureArt(t.owner)}
|
||||
x={tx - CELL * 0.26} y={ty - CELL * 0.26}
|
||||
width={CELL * 0.52} height={CELL * 0.52}
|
||||
preserveAspectRatio="xMidYMid slice"
|
||||
class="token-art small"
|
||||
cls="token-art small"
|
||||
/>
|
||||
{:else}
|
||||
<g class="treasure-g">
|
||||
@@ -298,12 +298,11 @@
|
||||
{@const sx = Number(key.split(",")[0])}
|
||||
{@const sy = Number(key.split(",")[1])}
|
||||
{#if USE_TOKEN_ART && TERRAIN_ART[content.kind]}
|
||||
<image
|
||||
<TokenArt
|
||||
href={tokenArt(TERRAIN_ART[content.kind]!, "terrain")}
|
||||
x={sx * CELL + 3} y={sy * CELL + 3}
|
||||
width={CELL - 6} height={CELL - 6}
|
||||
preserveAspectRatio="xMidYMid slice"
|
||||
class="token-art"
|
||||
cls="token-art"
|
||||
/>
|
||||
{:else if content.kind === "stone"}
|
||||
<rect x={sx * CELL + 2} y={sy * CELL + 2} width={CELL - 4} height={CELL - 4} class="stone" rx="4" />
|
||||
@@ -328,11 +327,11 @@
|
||||
{#each view.dimWarps as w, wi (wi)}
|
||||
{#each [w.a, w.b] as tok, i (i)}
|
||||
{#if USE_TOKEN_ART}
|
||||
<image
|
||||
<TokenArt
|
||||
href={tokenArt("dimensional-warp", "terrain")}
|
||||
x={tok.x * CELL + CELL * 0.04} y={tok.y * CELL + CELL * 0.04}
|
||||
width={CELL * 0.44} height={CELL * 0.44}
|
||||
preserveAspectRatio="xMidYMid slice" class="token-art"
|
||||
cls="token-art"
|
||||
/>
|
||||
{:else}
|
||||
<circle cx={tok.x * CELL + CELL * 0.5} cy={tok.y * CELL + CELL * 0.5} r={CELL * 0.3}
|
||||
@@ -508,14 +507,11 @@
|
||||
>
|
||||
{#if USE_TOKEN_ART}
|
||||
{@const half = CELL * 0.3 * wizardScale(p.id)}
|
||||
<image
|
||||
<TokenArt
|
||||
href={wizardArt(p.id)}
|
||||
x={-half} y={-half}
|
||||
width={half * 2} height={half * 2}
|
||||
preserveAspectRatio="xMidYMid slice"
|
||||
class="token-art hit"
|
||||
class:ghosted={hasSpell(p.id, "invisible") || hasSpell(p.id, "mist-body")}
|
||||
class:stone-gazed={hasSpell(p.id, "medusa")}
|
||||
cls={`token-art hit${hasSpell(p.id, "invisible") || hasSpell(p.id, "mist-body") ? " ghosted" : ""}${hasSpell(p.id, "medusa") ? " stone-gazed" : ""}`}
|
||||
/>
|
||||
<rect
|
||||
x={-half} y={-half}
|
||||
@@ -556,16 +552,13 @@
|
||||
onkeydown={() => {}}
|
||||
>
|
||||
{#if USE_TOKEN_ART && CREATURE_ART[c.kind]}
|
||||
<image
|
||||
<TokenArt
|
||||
href={tokenArt(CREATURE_ART[c.kind]!, "creatures")}
|
||||
x={-CELL * 0.26} y={-CELL * 0.26}
|
||||
width={CELL * 0.52} height={CELL * 0.52}
|
||||
preserveAspectRatio="xMidYMid slice"
|
||||
class="token-art hit"
|
||||
class:selected-art={c.id === selectedCreatureId}
|
||||
>
|
||||
<title>{c.kind} ({c.controllerId}) — {c.damage}/{Number.isFinite(c.maxDamage) ? c.maxDamage : "∞"} dmg</title>
|
||||
</image>
|
||||
cls={`token-art hit${c.id === selectedCreatureId ? " selected-art" : ""}`}
|
||||
title={`${c.kind} (${c.controllerId}) — ${c.damage}/${Number.isFinite(c.maxDamage) ? c.maxDamage : "∞"} dmg`}
|
||||
/>
|
||||
<rect
|
||||
x={-CELL * 0.26} y={-CELL * 0.26}
|
||||
width={CELL * 0.52} height={CELL * 0.52}
|
||||
@@ -680,11 +673,11 @@
|
||||
.trap-token { fill: #513c22; stroke: #201709; stroke-width: 1.5; }
|
||||
.trap-real { stroke: #d3352b; stroke-width: 2.5; }
|
||||
.dimwarp { fill: none; stroke: #5b3f9e; stroke-width: 3.5; stroke-dasharray: 4 3; }
|
||||
.token-art {
|
||||
:global(.token-art) {
|
||||
filter: drop-shadow(0.5px 1.5px 1.5px rgba(10, 8, 4, 0.5));
|
||||
pointer-events: none;
|
||||
}
|
||||
.token-art.hit { pointer-events: auto; cursor: pointer; }
|
||||
:global(.token-art.hit) { pointer-events: auto; cursor: pointer; }
|
||||
.creature-ring, .wizard-ring {
|
||||
fill: none;
|
||||
stroke-width: 2.5;
|
||||
@@ -766,9 +759,9 @@
|
||||
.mover.snap { transition: none; }
|
||||
|
||||
/* --- persistent ambiance: ongoing spells wear their look ------------ */
|
||||
.token-art.ghosted { opacity: 0.4; animation: ghost-breathe 2.6s ease-in-out infinite; }
|
||||
@keyframes ghost-breathe { 50% { opacity: 0.22; } }
|
||||
.token-art.stone-gazed { filter: grayscale(0.9) brightness(0.8); }
|
||||
:global(.token-art.ghosted) { opacity: 0.4; animation: ghost-breathe 2.6s ease-in-out infinite; }
|
||||
@keyframes -global-ghost-breathe { 50% { opacity: 0.22; } }
|
||||
:global(.token-art.stone-gazed) { filter: grayscale(0.9) brightness(0.8); }
|
||||
.webbing line {
|
||||
stroke: rgba(220, 218, 205, 0.85);
|
||||
stroke-width: 1.6;
|
||||
@@ -840,7 +833,7 @@
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.marked-cell, .marked-sector, .ghost-slot, .warp-dest { animation: none; }
|
||||
.fx-layer { display: none; }
|
||||
.firewall, .token-art.ghosted { animation: none; }
|
||||
.firewall, :global(.token-art.ghosted) { animation: none; }
|
||||
.mover { transition: none; }
|
||||
}
|
||||
.wizard { cursor: pointer; }
|
||||
|
||||
Reference in New Issue
Block a user