TypeScript 97.6%
SQL 1.4%
JavaScript 0.5%
1import { Container, Graphics, Text, TextStyle } from "pixi.js";2import type { SymbolStyle } from "@spinza/game-core/client";34/**5 * Procedural symbol artwork. Every Spinza symbol is drawn from primitives —6 * no external assets — so each game keeps an original look while sharing7 * one renderer. `size` is the tile edge in px; art is centred on (0,0).8 */910function hex(c: string): number {11 return parseInt(c.replace("#", ""), 16);12}1314function lighten(c: string, amt: number): number {15 const n = hex(c);16 const r = Math.min(255, ((n >> 16) & 255) + Math.round(255 * amt));17 const g = Math.min(255, ((n >> 8) & 255) + Math.round(255 * amt));18 const b = Math.min(255, (n & 255) + Math.round(255 * amt));19 return (r << 16) | (g << 8) | b;20}2122function darken(c: string, amt: number): number {23 const n = hex(c);24 const r = Math.max(0, ((n >> 16) & 255) - Math.round(255 * amt));25 const g = Math.max(0, ((n >> 8) & 255) - Math.round(255 * amt));26 const b = Math.max(0, (n & 255) - Math.round(255 * amt));27 return (r << 16) | (g << 8) | b;28}2930function polygon(g: Graphics, cx: number, cy: number, r: number, sides: number, rot = -Math.PI / 2): Graphics {31 const pts: number[] = [];32 for (let i = 0; i < sides; i++) {33 const a = rot + (i * Math.PI * 2) / sides;34 pts.push(cx + Math.cos(a) * r, cy + Math.sin(a) * r);35 }36 return g.poly(pts);37}3839function star(g: Graphics, cx: number, cy: number, outer: number, inner: number, points = 5): Graphics {40 const pts: number[] = [];41 for (let i = 0; i < points * 2; i++) {42 const r = i % 2 === 0 ? outer : inner;43 const a = -Math.PI / 2 + (i * Math.PI) / points;44 pts.push(cx + Math.cos(a) * r, cy + Math.sin(a) * r);45 }46 return g.poly(pts);47}4849export interface SymbolArtOptions {50 size: number;51 /** Tier drives frame richness. */52 tier: "low" | "mid" | "high" | "premium" | "special";53 kind: string;54 /** Game frame preset — drives the tile plate look so every game has its own tiles. */55 frame?: "metal" | "glass" | "gold" | "stone" | "ice" | "carbon" | "neon" | "obsidian";56}5758/** Tile plate behind the icon, styled per game frame preset. */59function drawPlate(size: number, style: SymbolStyle, opts: SymbolArtOptions): Graphics {60 const plate = new Graphics();61 const pad = size * 0.06;62 const x = -size / 2 + pad;63 const w = size - pad * 2;64 const premium = opts.tier === "premium" || opts.tier === "special";65 const main = hex(style.color);66 const accent = style.accent ? hex(style.accent) : lighten(style.color, 0.35);67 const line = Math.max(1, size * 0.012);68 switch (opts.frame) {69 case "glass":70 plate.roundRect(x, x, w, w, size * 0.2).fill({ color: 0xffffff, alpha: 0.07 });71 plate.roundRect(x, x, w, w, size * 0.2).stroke({ color: 0xffffff, alpha: premium ? 0.5 : 0.22, width: line });72 plate.roundRect(x + w * 0.08, x + w * 0.06, w * 0.84, w * 0.28, size * 0.12).fill({ color: 0xffffff, alpha: 0.06 });73 break;74 case "gold":75 plate.roundRect(x, x, w, w, size * 0.1).fill({ color: 0x14100a, alpha: 0.8 });76 plate.roundRect(x, x, w, w, size * 0.1).stroke({ color: premium ? 0xf3e2ad : 0xa88a4a, alpha: premium ? 0.9 : 0.5, width: line * 1.6 });77 plate.roundRect(x + line * 3, x + line * 3, w - line * 6, w - line * 6, size * 0.08).stroke({ color: 0xc9a961, alpha: 0.25, width: line });78 for (const [cx, cy] of [[x + size * 0.09, x + size * 0.09], [x + w - size * 0.09, x + size * 0.09], [x + size * 0.09, x + w - size * 0.09], [x + w - size * 0.09, x + w - size * 0.09]])79 plate.circle(cx, cy, size * 0.018).fill({ color: 0xf3e2ad, alpha: 0.8 });80 break;81 case "stone":82 plate.roundRect(x, x, w, w, size * 0.06).fill({ color: 0x1a1712, alpha: 0.85 });83 plate.roundRect(x, x, w, w, size * 0.06).stroke({ color: premium ? accent : 0x7a6f5a, alpha: premium ? 0.7 : 0.45, width: line * 1.4 });84 plate.moveTo(x + w * 0.15, x).lineTo(x + w * 0.22, x + w * 0.12).stroke({ color: 0x000000, alpha: 0.5, width: line });85 plate.moveTo(x + w, x + w * 0.7).lineTo(x + w * 0.85, x + w * 0.78).stroke({ color: 0x000000, alpha: 0.5, width: line });86 break;87 case "ice":88 plate.roundRect(x, x, w, w, size * 0.18).fill({ color: 0x9fdcff, alpha: 0.1 });89 plate.roundRect(x, x, w, w, size * 0.18).stroke({ color: 0xdff6ff, alpha: premium ? 0.8 : 0.35, width: line });90 plate.moveTo(x + w * 0.1, x + w * 0.9).lineTo(x + w * 0.35, x + w * 0.62).stroke({ color: 0xffffff, alpha: 0.35, width: line });91 plate.moveTo(x + w * 0.16, x + w * 0.9).lineTo(x + w * 0.4, x + w * 0.66).stroke({ color: 0xffffff, alpha: 0.2, width: line });92 break;93 case "carbon":94 plate.roundRect(x, x, w, w, size * 0.08).fill({ color: 0x0a0b0e, alpha: 0.92 });95 for (let i = 0; i < 6; i++) plate.moveTo(x, x + (i / 6) * w).lineTo(x + w, x + (i / 6) * w).stroke({ color: 0xffffff, alpha: 0.035, width: line });96 plate.roundRect(x, x, w, w, size * 0.08).stroke({ color: premium ? accent : 0x5c6270, alpha: premium ? 0.8 : 0.45, width: line * 1.3 });97 break;98 case "neon":99 plate.roundRect(x, x, w, w, size * 0.16).fill({ color: 0x05060c, alpha: 0.75 });100 plate.roundRect(x, x, w, w, size * 0.16).stroke({ color: main, alpha: premium ? 0.9 : 0.45, width: line * 1.4 });101 plate.roundRect(x - line * 2, x - line * 2, w + line * 4, w + line * 4, size * 0.17).stroke({ color: main, alpha: premium ? 0.25 : 0.1, width: line * 4 });102 break;103 case "obsidian":104 plate.roundRect(x, x, w, w, size * 0.04).fill({ color: 0x000000, alpha: 0.9 });105 plate.roundRect(x, x, w, w, size * 0.04).stroke({ color: premium ? 0xe8cf8f : 0xffffff, alpha: premium ? 0.7 : 0.12, width: line });106 break;107 case "metal":108 default:109 plate.roundRect(x, x, w, w, size * 0.14).fill({ color: 0x0b0d14, alpha: 0.75 });110 plate.roundRect(x, x, w, w, size * 0.14).stroke({ color: premium ? accent : 0x9aa3b5, alpha: premium ? 0.6 : 0.2, width: line });111 plate.roundRect(x + line * 2, x + line * 2, w - line * 4, w * 0.5, size * 0.1).fill({ color: 0xffffff, alpha: 0.025 });112 if (opts.frame === "metal") for (const [cx, cy] of [[x + size * 0.07, x + size * 0.07], [x + w - size * 0.07, x + size * 0.07], [x + size * 0.07, x + w - size * 0.07], [x + w - size * 0.07, x + w - size * 0.07]]) plate.circle(cx, cy, size * 0.014).fill({ color: 0xc8d0dc, alpha: 0.5 });113 break;114 }115 return plate;116}117118export function drawSymbol(style: SymbolStyle, opts: SymbolArtOptions): Container {119 const { size } = opts;120 const c = new Container();121 const main = hex(style.color);122 const accent = style.accent ? hex(style.accent) : lighten(style.color, 0.35);123 const glow = style.glow ?? 0.3;124 const r = size * 0.36; // icon radius125 const g = new Graphics();126127 // Tile plate (per game frame preset).128 c.addChild(drawPlate(size, style, opts));129130 // Glow halo.131 if (glow > 0) {132 const halo = new Graphics();133 halo.circle(0, 0, r * 1.25).fill({ color: main, alpha: 0.18 * glow });134 halo.circle(0, 0, r * 1.0).fill({ color: main, alpha: 0.16 * glow });135 c.addChild(halo);136 }137138 switch (style.shape) {139 case "gem": {140 g.poly([0, -r, r * 0.85, -r * 0.25, r * 0.55, r, -r * 0.55, r, -r * 0.85, -r * 0.25]).fill(main);141 g.poly([0, -r, r * 0.85, -r * 0.25, 0, -r * 0.1, -r * 0.85, -r * 0.25]).fill({ color: accent, alpha: 0.85 });142 g.poly([0, -r * 0.1, r * 0.85, -r * 0.25, r * 0.55, r]).fill({ color: darken(style.color, 0.15) });143 g.poly([-r * 0.2, -r * 0.7, r * 0.15, -r * 0.55, -r * 0.35, -r * 0.35]).fill({ color: 0xffffff, alpha: 0.65 });144 break;145 }146 case "hex": {147 polygon(g, 0, 0, r, 6).fill(main);148 polygon(g, 0, 0, r * 0.68, 6).fill({ color: darken(style.color, 0.25) });149 polygon(g, 0, 0, r * 0.42, 6).fill(accent);150 if (style.label) c.addChild(label(style.label, size * 0.26, 0x0b0d14));151 break;152 }153 case "star": {154 star(g, 0, 0, r, r * 0.45).fill(main);155 star(g, 0, 0, r * 0.55, r * 0.25).fill({ color: 0xffffff, alpha: 0.85 });156 break;157 }158 case "circle": {159 g.circle(0, 0, r).fill(main);160 g.circle(-r * 0.3, -r * 0.3, r * 0.35).fill({ color: 0xffffff, alpha: 0.35 });161 g.circle(0, 0, r).stroke({ color: accent, width: size * 0.02 });162 break;163 }164 case "diamond": {165 g.poly([0, -r, r, 0, 0, r, -r, 0]).fill(main);166 g.poly([0, -r, r, 0, 0, 0]).fill({ color: accent, alpha: 0.8 });167 g.poly([0, 0, 0, r, -r, 0]).fill({ color: darken(style.color, 0.2) });168 break;169 }170 case "shield": {171 g.poly([0, -r, r * 0.85, -r * 0.65, r * 0.75, r * 0.15, 0, r, -r * 0.75, r * 0.15, -r * 0.85, -r * 0.65]).fill(main);172 g.poly([0, -r * 0.7, r * 0.5, -r * 0.5, r * 0.45, r * 0.05, 0, r * 0.6, -r * 0.45, r * 0.05, -r * 0.5, -r * 0.5]).fill({ color: darken(style.color, 0.28) });173 g.rect(-r * 0.08, -r * 0.5, r * 0.16, r * 0.9).fill(accent);174 g.rect(-r * 0.38, -r * 0.25, r * 0.76, r * 0.16).fill(accent);175 break;176 }177 case "bolt": {178 g.poly([r * 0.15, -r, -r * 0.55, r * 0.1, -r * 0.05, r * 0.1, -r * 0.25, r, r * 0.6, -r * 0.15, r * 0.05, -r * 0.15]).fill(main);179 g.poly([r * 0.1, -r * 0.8, -r * 0.3, r * 0.0, r * 0.0, 0]).fill({ color: 0xffffff, alpha: 0.5 });180 break;181 }182 case "ring": {183 g.circle(0, 0, r).fill(main);184 g.circle(0, 0, r * 0.62).cut();185 g.circle(0, 0, r * 0.62).stroke({ color: accent, width: size * 0.02, alpha: 0.9 });186 g.arc(0, 0, r * 0.81, -Math.PI * 0.9, -Math.PI * 0.3).stroke({ color: 0xffffff, width: size * 0.04, alpha: 0.55 });187 break;188 }189 case "chip": {190 g.roundRect(-r, -r * 0.75, r * 2, r * 1.5, r * 0.25).fill(main);191 g.roundRect(-r * 0.75, -r * 0.5, r * 1.5, r, r * 0.15).fill({ color: darken(style.color, 0.35) });192 for (let i = -2; i <= 2; i++) {193 g.rect(-r - r * 0.18, i * r * 0.28 - r * 0.06, r * 0.18, r * 0.12).fill(accent);194 g.rect(r, i * r * 0.28 - r * 0.06, r * 0.18, r * 0.12).fill(accent);195 }196 if (style.label) c.addChild(label(style.label, size * 0.22, accent));197 break;198 }199 case "crystal": {200 g.poly([0, -r, r * 0.5, -r * 0.3, r * 0.35, r * 0.8, -r * 0.35, r * 0.8, -r * 0.5, -r * 0.3]).fill(main);201 g.poly([0, -r, r * 0.5, -r * 0.3, 0, r * 0.1]).fill({ color: accent, alpha: 0.75 });202 g.poly([0, r * 0.1, r * 0.35, r * 0.8, -r * 0.35, r * 0.8]).fill({ color: darken(style.color, 0.2) });203 break;204 }205 case "letter": {206 g.roundRect(-r * 0.95, -r * 0.95, r * 1.9, r * 1.9, r * 0.3).fill({ color: darken(style.color, 0.45), alpha: 0.9 });207 g.roundRect(-r * 0.95, -r * 0.95, r * 1.9, r * 1.9, r * 0.3).stroke({ color: main, width: size * 0.025 });208 c.addChild(label(style.label ?? "?", size * 0.4, main, true));209 break;210 }211 case "vault": {212 g.circle(0, 0, r).fill(darken(style.color, 0.4));213 g.circle(0, 0, r).stroke({ color: main, width: size * 0.03 });214 g.circle(0, 0, r * 0.7).stroke({ color: main, width: size * 0.02, alpha: 0.7 });215 for (let i = 0; i < 6; i++) {216 const a = (i * Math.PI) / 3;217 g.moveTo(Math.cos(a) * r * 0.25, Math.sin(a) * r * 0.25).lineTo(Math.cos(a) * r * 0.62, Math.sin(a) * r * 0.62).stroke({ color: accent, width: size * 0.035 });218 }219 g.circle(0, 0, r * 0.22).fill(accent);220 break;221 }222 case "orb": {223 g.circle(0, 0, r).fill(main);224 g.circle(0, 0, r * 0.75).fill({ color: accent, alpha: 0.35 });225 g.circle(-r * 0.25, -r * 0.3, r * 0.25).fill({ color: 0xffffff, alpha: 0.5 });226 g.circle(0, 0, r).stroke({ color: accent, width: size * 0.02, alpha: 0.9 });227 if (style.label) c.addChild(label(style.label, size * 0.36, accent, true));228 break;229 }230 case "coin": {231 g.circle(0, 0, r).fill(main);232 g.circle(0, 0, r * 0.8).stroke({ color: darken(style.color, 0.25), width: size * 0.03 });233 g.circle(0, 0, r).stroke({ color: accent, width: size * 0.02 });234 c.addChild(label(style.label ?? "SC", size * 0.24, darken(style.color, 0.5), true));235 break;236 }237 case "skull": {238 g.circle(0, -r * 0.15, r * 0.8).fill(main);239 g.roundRect(-r * 0.45, r * 0.3, r * 0.9, r * 0.55, r * 0.15).fill(main);240 g.circle(-r * 0.3, -r * 0.2, r * 0.22).fill(0x0b0d14);241 g.circle(r * 0.3, -r * 0.2, r * 0.22).fill(0x0b0d14);242 g.poly([0, r * 0.05, r * 0.12, r * 0.3, -r * 0.12, r * 0.3]).fill(0x0b0d14);243 break;244 }245 case "flame": {246 g.poly([0, -r, r * 0.55, -r * 0.2, r * 0.7, r * 0.45, r * 0.3, r, -r * 0.3, r, -r * 0.7, r * 0.45, -r * 0.55, -r * 0.2]).fill(main);247 g.poly([0, -r * 0.4, r * 0.3, r * 0.1, r * 0.25, r * 0.6, -r * 0.25, r * 0.6, -r * 0.3, r * 0.1]).fill(accent);248 break;249 }250 case "snow": {251 for (let i = 0; i < 3; i++) {252 const a = (i * Math.PI) / 3;253 g.moveTo(Math.cos(a) * r, Math.sin(a) * r).lineTo(-Math.cos(a) * r, -Math.sin(a) * r).stroke({ color: main, width: size * 0.035 });254 for (const s of [1, -1]) {255 const bx = Math.cos(a) * r * 0.6 * s;256 const by = Math.sin(a) * r * 0.6 * s;257 g.moveTo(bx, by).lineTo(bx + Math.cos(a + 0.6) * r * 0.3 * s, by + Math.sin(a + 0.6) * r * 0.3 * s).stroke({ color: accent, width: size * 0.025 });258 g.moveTo(bx, by).lineTo(bx + Math.cos(a - 0.6) * r * 0.3 * s, by + Math.sin(a - 0.6) * r * 0.3 * s).stroke({ color: accent, width: size * 0.025 });259 }260 }261 g.circle(0, 0, r * 0.14).fill(0xffffff);262 break;263 }264 case "eye": {265 g.ellipse(0, 0, r, r * 0.6).fill(main);266 g.circle(0, 0, r * 0.42).fill(darken(style.color, 0.5));267 g.circle(0, 0, r * 0.2).fill(accent);268 g.circle(r * 0.12, -r * 0.14, r * 0.07).fill(0xffffff);269 break;270 }271 case "moon": {272 g.circle(0, 0, r).fill(main);273 g.circle(r * 0.45, -r * 0.2, r * 0.78).fill({ color: 0x0b0d14, alpha: 0.85 });274 g.circle(-r * 0.4, r * 0.3, r * 0.12).fill({ color: darken(style.color, 0.2) });275 break;276 }277 case "leaf": {278 g.moveTo(0, r).bezierCurveTo(-r * 1.1, r * 0.2, -r * 0.6, -r * 0.9, 0, -r).bezierCurveTo(r * 0.6, -r * 0.9, r * 1.1, r * 0.2, 0, r).fill(main);279 g.moveTo(0, r * 0.9).lineTo(0, -r * 0.8).stroke({ color: accent, width: size * 0.02 });280 break;281 }282 case "atom": {283 g.circle(0, 0, r * 0.2).fill(accent);284 for (let i = 0; i < 3; i++) {285 const e = new Graphics();286 e.ellipse(0, 0, r, r * 0.38).stroke({ color: main, width: size * 0.025 });287 e.rotation = (i * Math.PI) / 3;288 c.addChild(e);289 }290 break;291 }292 case "crown": {293 g.poly([-r, r * 0.6, -r, -r * 0.4, -r * 0.5, r * 0.05, 0, -r, r * 0.5, r * 0.05, r, -r * 0.4, r, r * 0.6]).fill(main);294 g.rect(-r, r * 0.4, r * 2, r * 0.25).fill(darken(style.color, 0.3));295 g.circle(0, -r * 0.95, r * 0.12).fill(accent);296 g.circle(-r, -r * 0.4, r * 0.1).fill(accent);297 g.circle(r, -r * 0.4, r * 0.1).fill(accent);298 break;299 }300 case "wave": {301 for (let k = -1; k <= 1; k++) {302 const y = k * r * 0.5;303 g.moveTo(-r, y).bezierCurveTo(-r * 0.5, y - r * 0.4, -r * 0.2, y + r * 0.4, 0, y).bezierCurveTo(r * 0.2, y - r * 0.4, r * 0.5, y + r * 0.4, r, y).stroke({ color: k === 0 ? accent : main, width: size * 0.04, alpha: k === 0 ? 1 : 0.8 });304 }305 break;306 }307 case "cube": {308 g.poly([0, -r, r * 0.87, -r * 0.5, r * 0.87, r * 0.5, 0, r, -r * 0.87, r * 0.5, -r * 0.87, -r * 0.5]).fill(main);309 g.poly([0, -r, r * 0.87, -r * 0.5, 0, 0, -r * 0.87, -r * 0.5]).fill(accent);310 g.poly([0, 0, r * 0.87, -r * 0.5, r * 0.87, r * 0.5, 0, r]).fill(darken(style.color, 0.3));311 break;312 }313 /* ----------------------------------------------------------- themed set */314 case "lock": {315 g.roundRect(-r * 0.75, -r * 0.1, r * 1.5, r * 1.1, r * 0.2).fill(main);316 g.arc(0, -r * 0.15, r * 0.5, Math.PI, 0).stroke({ color: accent, width: size * 0.05 });317 g.circle(0, r * 0.35, r * 0.16).fill(0x0b0d14);318 g.rect(-r * 0.06, r * 0.35, r * 0.12, r * 0.35).fill(0x0b0d14);319 break;320 }321 case "key": {322 g.circle(-r * 0.45, -r * 0.35, r * 0.42).stroke({ color: main, width: size * 0.06 });323 g.moveTo(-r * 0.15, -r * 0.05).lineTo(r * 0.85, r * 0.95).stroke({ color: main, width: size * 0.06 });324 g.moveTo(r * 0.45, r * 0.55).lineTo(r * 0.7, r * 0.3).stroke({ color: accent, width: size * 0.05 });325 g.moveTo(r * 0.65, r * 0.75).lineTo(r * 0.9, r * 0.5).stroke({ color: accent, width: size * 0.05 });326 break;327 }328 case "code": {329 g.roundRect(-r, -r * 0.7, r * 2, r * 1.4, r * 0.15).fill({ color: darken(style.color, 0.45), alpha: 0.95 });330 g.roundRect(-r, -r * 0.7, r * 2, r * 1.4, r * 0.15).stroke({ color: main, width: size * 0.02 });331 c.addChild(label(style.label ?? "</>", size * 0.26, main, true));332 break;333 }334 case "badge": {335 g.roundRect(-r * 0.7, -r, r * 1.4, r * 2, r * 0.18).fill(main);336 g.circle(0, -r * 0.4, r * 0.28).fill(0x0b0d14);337 g.roundRect(-r * 0.45, r * 0.05, r * 0.9, r * 0.14, r * 0.05).fill(accent);338 g.roundRect(-r * 0.45, r * 0.35, r * 0.6, r * 0.14, r * 0.05).fill(accent);339 break;340 }341 case "mask": {342 g.ellipse(0, 0, r, r * 0.6).fill(main);343 g.ellipse(-r * 0.4, -r * 0.05, r * 0.28, r * 0.18).fill(0x0b0d14);344 g.ellipse(r * 0.4, -r * 0.05, r * 0.28, r * 0.18).fill(0x0b0d14);345 g.moveTo(-r * 0.95, -r * 0.1).lineTo(-r * 1.3, -r * 0.6).stroke({ color: accent, width: size * 0.025 });346 g.moveTo(r * 0.95, -r * 0.1).lineTo(r * 1.3, -r * 0.6).stroke({ color: accent, width: size * 0.025 });347 break;348 }349 case "glove": {350 g.roundRect(-r * 0.55, -r * 0.2, r * 1.1, r * 1.1, r * 0.25).fill(main);351 for (let i = 0; i < 4; i++) g.roundRect(-r * 0.55 + i * r * 0.28, -r, r * 0.22, r * 0.9, r * 0.1).fill(main);352 g.roundRect(-r * 0.9, -r * 0.1, r * 0.35, r * 0.7, r * 0.15).fill(main);353 g.roundRect(-r * 0.55, r * 0.6, r * 1.1, r * 0.3, r * 0.08).fill(accent);354 break;355 }356 case "rope": {357 g.circle(0, 0, r * 0.75).stroke({ color: main, width: size * 0.09 });358 g.circle(0, 0, r * 0.75).stroke({ color: accent, width: size * 0.02, alpha: 0.6 });359 for (let i = 0; i < 10; i++) {360 const a = (i / 10) * Math.PI * 2;361 g.moveTo(Math.cos(a) * r * 0.62, Math.sin(a) * r * 0.62).lineTo(Math.cos(a + 0.3) * r * 0.88, Math.sin(a + 0.3) * r * 0.88).stroke({ color: darken(style.color, 0.3), width: size * 0.02 });362 }363 break;364 }365 case "bag": {366 g.moveTo(-r * 0.75, r * 0.9).bezierCurveTo(-r * 1.1, -r * 0.3, -r * 0.3, -r * 0.6, 0, -r * 0.55).bezierCurveTo(r * 0.3, -r * 0.6, r * 1.1, -r * 0.3, r * 0.75, r * 0.9).lineTo(-r * 0.75, r * 0.9).fill(main);367 g.roundRect(-r * 0.3, -r * 0.95, r * 0.6, r * 0.35, r * 0.1).fill(accent);368 g.moveTo(-r * 0.35, r * 0.3).lineTo(r * 0.35, r * 0.3).stroke({ color: darken(style.color, 0.3), width: size * 0.03 });369 break;370 }371 case "lantern": {372 g.roundRect(-r * 0.7, -r * 0.6, r * 1.4, r * 1.2, r * 0.45).fill(main);373 g.rect(-r * 0.35, -r * 0.9, r * 0.7, r * 0.2).fill(accent);374 g.rect(-r * 0.35, r * 0.6, r * 0.7, r * 0.2).fill(accent);375 g.moveTo(0, r * 0.8).lineTo(0, r).stroke({ color: accent, width: size * 0.03 });376 for (let i = -1; i <= 1; i++) g.moveTo(i * r * 0.3, -r * 0.55).lineTo(i * r * 0.3, r * 0.55).stroke({ color: darken(style.color, 0.3), width: size * 0.015 });377 break;378 }379 case "fan": {380 for (let i = -3; i <= 3; i++) {381 const a = -Math.PI / 2 + i * 0.28;382 g.moveTo(0, r * 0.7).lineTo(Math.cos(a) * r * 1.05, r * 0.7 + Math.sin(a) * r * 1.05).stroke({ color: i % 2 ? main : accent, width: size * 0.07 });383 }384 g.circle(0, r * 0.7, r * 0.14).fill(0x0b0d14);385 break;386 }387 case "scroll": {388 g.roundRect(-r * 0.7, -r * 0.85, r * 1.4, r * 1.7, r * 0.1).fill({ color: accent, alpha: 0.9 });389 g.roundRect(-r * 0.85, -r, r * 1.7, r * 0.3, r * 0.15).fill(main);390 g.roundRect(-r * 0.85, r * 0.7, r * 1.7, r * 0.3, r * 0.15).fill(main);391 for (let i = 0; i < 3; i++) g.rect(-r * 0.45, -r * 0.5 + i * r * 0.32, r * 0.9, r * 0.08).fill({ color: darken(style.color, 0.4), alpha: 0.7 });392 break;393 }394 case "cherry": {395 g.moveTo(-r * 0.3, r * 0.3).bezierCurveTo(-r * 0.2, -r * 0.5, r * 0.2, -r * 0.9, r * 0.5, -r).stroke({ color: 0x3ddc97, width: size * 0.035 });396 g.moveTo(r * 0.45, r * 0.3).bezierCurveTo(r * 0.5, -r * 0.4, r * 0.5, -r * 0.8, r * 0.5, -r).stroke({ color: 0x3ddc97, width: size * 0.035 });397 g.circle(-r * 0.35, r * 0.45, r * 0.42).fill(main);398 g.circle(r * 0.45, r * 0.45, r * 0.42).fill(main);399 g.circle(-r * 0.5, r * 0.3, r * 0.12).fill({ color: 0xffffff, alpha: 0.6 });400 g.circle(r * 0.3, r * 0.3, r * 0.12).fill({ color: 0xffffff, alpha: 0.6 });401 break;402 }403 case "seven": {404 g.poly([-r * 0.8, -r, r * 0.8, -r, r * 0.8, -r * 0.55, -r * 0.05, r, -r * 0.55, r, r * 0.2, -r * 0.55, -r * 0.8, -r * 0.55]).fill(main);405 g.poly([-r * 0.8, -r, r * 0.8, -r, r * 0.8, -r * 0.55, -r * 0.05, r, -r * 0.55, r, r * 0.2, -r * 0.55, -r * 0.8, -r * 0.55]).stroke({ color: accent, width: size * 0.03 });406 break;407 }408 case "bell": {409 g.moveTo(-r * 0.75, r * 0.5).bezierCurveTo(-r * 0.75, -r * 0.5, -r * 0.3, -r * 0.9, 0, -r * 0.9).bezierCurveTo(r * 0.3, -r * 0.9, r * 0.75, -r * 0.5, r * 0.75, r * 0.5).lineTo(-r * 0.75, r * 0.5).fill(main);410 g.roundRect(-r * 0.9, r * 0.45, r * 1.8, r * 0.18, r * 0.09).fill(accent);411 g.circle(0, r * 0.8, r * 0.15).fill(accent);412 g.circle(0, -r * 0.95, r * 0.1).fill(accent);413 break;414 }415 case "joystick": {416 g.roundRect(-r * 0.9, r * 0.3, r * 1.8, r * 0.6, r * 0.15).fill(darken(style.color, 0.4));417 g.rect(-r * 0.08, -r * 0.4, r * 0.16, r * 0.8).fill(0x9aa3b5);418 g.circle(0, -r * 0.55, r * 0.35).fill(main);419 g.circle(-r * 0.12, -r * 0.65, r * 0.1).fill({ color: 0xffffff, alpha: 0.5 });420 g.circle(r * 0.5, r * 0.6, r * 0.12).fill(accent);421 break;422 }423 case "torii": {424 g.rect(-r, -r * 0.85, r * 2, r * 0.18).fill(main);425 g.rect(-r * 0.8, -r * 0.5, r * 1.6, r * 0.12).fill(main);426 g.rect(-r * 0.6, -r * 0.5, r * 0.16, r * 1.5).fill(main);427 g.rect(r * 0.44, -r * 0.5, r * 0.16, r * 1.5).fill(main);428 g.rect(-r * 0.08, -r * 0.5, r * 0.16, r * 0.35).fill(accent);429 break;430 }431 case "katana": {432 g.moveTo(-r * 0.9, r * 0.9).lineTo(r * 0.7, -r * 0.7).stroke({ color: 0xdfe6f0, width: size * 0.05 });433 g.moveTo(-r * 0.9, r * 0.9).lineTo(r * 0.7, -r * 0.7).stroke({ color: 0xffffff, width: size * 0.015, alpha: 0.7 });434 g.moveTo(-r * 0.55, r * 0.35).lineTo(-r * 0.25, r * 0.65).stroke({ color: accent, width: size * 0.05 });435 g.moveTo(-r * 0.9, r * 0.9).lineTo(-r * 0.6, r * 0.6).stroke({ color: main, width: size * 0.08 });436 break;437 }438 case "cassette": {439 g.roundRect(-r, -r * 0.65, r * 2, r * 1.3, r * 0.12).fill(main);440 g.roundRect(-r * 0.75, -r * 0.4, r * 1.5, r * 0.55, r * 0.08).fill(darken(style.color, 0.45));441 g.circle(-r * 0.4, -r * 0.12, r * 0.16).fill(0x0b0d14).circle(r * 0.4, -r * 0.12, r * 0.16).fill(0x0b0d14);442 g.rect(-r * 0.6, r * 0.3, r * 1.2, r * 0.14).fill(accent);443 break;444 }445 case "ankh": {446 g.ellipse(0, -r * 0.55, r * 0.35, r * 0.42).stroke({ color: main, width: size * 0.06 });447 g.rect(-r * 0.08, -r * 0.1, r * 0.16, r * 1.1).fill(main);448 g.rect(-r * 0.7, -r * 0.1, r * 1.4, r * 0.16).fill(main);449 g.circle(0, -r * 0.55, r * 0.08).fill(accent);450 break;451 }452 case "scarab": {453 g.ellipse(0, r * 0.15, r * 0.6, r * 0.7).fill(main);454 g.circle(0, -r * 0.6, r * 0.28).fill(darken(style.color, 0.25));455 g.moveTo(0, -r * 0.4).lineTo(0, r * 0.8).stroke({ color: accent, width: size * 0.02 });456 for (const sgn of [-1, 1]) {457 g.moveTo(sgn * r * 0.55, -r * 0.1).lineTo(sgn * r * 0.95, -r * 0.5).stroke({ color: accent, width: size * 0.035 });458 g.moveTo(sgn * r * 0.6, r * 0.3).lineTo(sgn * r * 1.0, r * 0.4).stroke({ color: accent, width: size * 0.035 });459 }460 break;461 }462 case "pyramid": {463 g.poly([0, -r, r, r * 0.75, -r, r * 0.75]).fill(main);464 g.poly([0, -r, r, r * 0.75, 0, r * 0.75]).fill(darken(style.color, 0.25));465 g.poly([0, -r, r * 0.25, -r * 0.55, -r * 0.25, -r * 0.55]).fill(accent);466 break;467 }468 case "warning": {469 g.poly([0, -r, r * 0.95, r * 0.75, -r * 0.95, r * 0.75]).fill(main);470 g.poly([0, -r * 0.6, r * 0.55, r * 0.55, -r * 0.55, r * 0.55]).fill(0x0b0d14);471 g.rect(-r * 0.08, -r * 0.3, r * 0.16, r * 0.5).fill(main);472 g.circle(0, r * 0.38, r * 0.09).fill(main);473 break;474 }475 case "valve": {476 g.circle(0, 0, r * 0.85).stroke({ color: main, width: size * 0.07 });477 for (let i = 0; i < 4; i++) {478 const a = (i / 4) * Math.PI * 2;479 g.moveTo(0, 0).lineTo(Math.cos(a) * r * 0.85, Math.sin(a) * r * 0.85).stroke({ color: main, width: size * 0.05 });480 }481 g.circle(0, 0, r * 0.25).fill(accent);482 break;483 }484 case "gauge": {485 g.arc(0, r * 0.2, r * 0.9, Math.PI, 0).stroke({ color: darken(style.color, 0.3), width: size * 0.09 });486 g.arc(0, r * 0.2, r * 0.9, Math.PI, Math.PI * 1.6).stroke({ color: main, width: size * 0.09 });487 g.moveTo(0, r * 0.2).lineTo(r * 0.45, -r * 0.5).stroke({ color: accent, width: size * 0.04 });488 g.circle(0, r * 0.2, r * 0.12).fill(accent);489 break;490 }491 case "barrel": {492 g.roundRect(-r * 0.65, -r * 0.9, r * 1.3, r * 1.8, r * 0.2).fill(main);493 g.rect(-r * 0.65, -r * 0.45, r * 1.3, r * 0.12).fill(darken(style.color, 0.4));494 g.rect(-r * 0.65, r * 0.33, r * 1.3, r * 0.12).fill(darken(style.color, 0.4));495 polygon(g, 0, 0, r * 0.28, 3, -Math.PI / 2).fill(accent);496 break;497 }498 case "wheel": {499 g.circle(0, 0, r * 0.95).fill(0x0b0d14);500 g.circle(0, 0, r * 0.95).stroke({ color: main, width: size * 0.05 });501 g.circle(0, 0, r * 0.6).stroke({ color: accent, width: size * 0.03 });502 for (let i = 0; i < 5; i++) {503 const a = (i / 5) * Math.PI * 2;504 g.moveTo(0, 0).lineTo(Math.cos(a) * r * 0.6, Math.sin(a) * r * 0.6).stroke({ color: accent, width: size * 0.035 });505 }506 g.circle(0, 0, r * 0.14).fill(main);507 break;508 }509 case "helmet": {510 g.arc(0, r * 0.1, r * 0.85, Math.PI, 0).fill(main);511 g.rect(-r * 0.85, r * 0.1, r * 1.7, r * 0.45).fill(main);512 g.roundRect(-r * 0.6, -r * 0.05, r * 1.4, r * 0.4, r * 0.1).fill(0x0b0d14);513 g.moveTo(-r * 0.3, -r * 0.7).lineTo(r * 0.3, -r * 0.7).stroke({ color: accent, width: size * 0.04 });514 break;515 }516 case "flag": {517 g.rect(-r * 0.85, -r, r * 0.1, r * 2).fill(0x9aa3b5);518 for (let i = 0; i < 4; i++) for (let j = 0; j < 3; j++) g.rect(-r * 0.75 + i * r * 0.4, -r + j * r * 0.33, r * 0.4, r * 0.33).fill((i + j) % 2 ? main : accent);519 break;520 }521 case "trophy": {522 g.moveTo(-r * 0.7, -r).lineTo(r * 0.7, -r).lineTo(r * 0.45, r * 0.15).lineTo(-r * 0.45, r * 0.15).fill(main);523 g.arc(-r * 0.75, -r * 0.55, r * 0.3, Math.PI * 0.5, Math.PI * 1.5).stroke({ color: main, width: size * 0.04 });524 g.arc(r * 0.75, -r * 0.55, r * 0.3, -Math.PI * 0.5, Math.PI * 0.5).stroke({ color: main, width: size * 0.04 });525 g.rect(-r * 0.1, r * 0.15, r * 0.2, r * 0.4).fill(main);526 g.roundRect(-r * 0.5, r * 0.55, r, r * 0.3, r * 0.06).fill(accent);527 break;528 }529 case "idol": {530 g.roundRect(-r * 0.55, -r * 0.9, r * 1.1, r * 1.8, r * 0.15).fill(main);531 g.rect(-r * 0.4, -r * 0.55, r * 0.8, r * 0.14).fill(0x0b0d14);532 g.rect(-r * 0.4, -r * 0.1, r * 0.8, r * 0.14).fill(0x0b0d14);533 g.poly([-r * 0.3, r * 0.35, r * 0.3, r * 0.35, 0, r * 0.7]).fill(accent);534 break;535 }536 case "totem": {537 for (let i = 0; i < 3; i++) {538 g.roundRect(-r * 0.55, -r + i * r * 0.68, r * 1.1, r * 0.6, r * 0.08).fill(i % 2 ? darken(style.color, 0.25) : main);539 g.circle(-r * 0.22, -r * 0.7 + i * r * 0.68, r * 0.08).fill(accent);540 g.circle(r * 0.22, -r * 0.7 + i * r * 0.68, r * 0.08).fill(accent);541 }542 break;543 }544 case "snake": {545 g.moveTo(-r * 0.8, r * 0.8).bezierCurveTo(-r * 0.9, -r * 0.2, r * 0.9, r * 0.6, r * 0.6, -r * 0.5).bezierCurveTo(r * 0.4, -r * 1.0, -r * 0.2, -r * 0.6, 0, -r * 0.3).stroke({ color: main, width: size * 0.08 });546 g.circle(0, -r * 0.3, r * 0.2).fill(main);547 g.circle(r * 0.06, -r * 0.36, r * 0.06).fill(accent);548 break;549 }550 case "rocket": {551 g.moveTo(0, -r).bezierCurveTo(r * 0.6, -r * 0.4, r * 0.5, r * 0.4, r * 0.35, r * 0.6).lineTo(-r * 0.35, r * 0.6).bezierCurveTo(-r * 0.5, r * 0.4, -r * 0.6, -r * 0.4, 0, -r).fill(main);552 g.poly([-r * 0.35, r * 0.2, -r * 0.75, r * 0.8, -r * 0.35, r * 0.6]).fill(accent);553 g.poly([r * 0.35, r * 0.2, r * 0.75, r * 0.8, r * 0.35, r * 0.6]).fill(accent);554 g.circle(0, -r * 0.25, r * 0.18).fill(0x0b0d14);555 g.poly([-r * 0.2, r * 0.6, r * 0.2, r * 0.6, 0, r]).fill(0xffb347);556 break;557 }558 case "anchor": {559 g.circle(0, -r * 0.75, r * 0.2).stroke({ color: main, width: size * 0.05 });560 g.rect(-r * 0.07, -r * 0.55, r * 0.14, r * 1.4).fill(main);561 g.rect(-r * 0.55, -r * 0.35, r * 1.1, r * 0.12).fill(main);562 g.arc(0, r * 0.15, r * 0.75, Math.PI * 0.15, Math.PI * 0.85).stroke({ color: main, width: size * 0.07 });563 g.circle(-r * 0.7, r * 0.4, r * 0.08).fill(accent).circle(r * 0.7, r * 0.4, r * 0.08).fill(accent);564 break;565 }566 case "chest": {567 g.roundRect(-r * 0.9, -r * 0.2, r * 1.8, r * 1.0, r * 0.1).fill(main);568 g.roundRect(-r * 0.9, -r * 0.75, r * 1.8, r * 0.6, r * 0.3).fill(darken(style.color, 0.2));569 g.rect(-r * 0.9, -r * 0.2, r * 1.8, r * 0.12).fill(accent);570 g.roundRect(-r * 0.15, -r * 0.2, r * 0.3, r * 0.35, r * 0.05).fill(accent);571 break;572 }573 case "trident": {574 g.rect(-r * 0.07, -r * 0.4, r * 0.14, r * 1.4).fill(main);575 for (const dx of [-0.5, 0, 0.5]) {576 g.moveTo(dx * r, -r * 0.3).lineTo(dx * r, -r).stroke({ color: main, width: size * 0.05 });577 g.poly([dx * r - r * 0.12, -r * 0.85, dx * r + r * 0.12, -r * 0.85, dx * r, -r * 1.1]).fill(accent);578 }579 g.rect(-r * 0.5, -r * 0.4, r, r * 0.12).fill(main);580 break;581 }582 case "shell": {583 g.arc(0, r * 0.4, r * 0.95, Math.PI, 0).fill(main);584 for (let i = -3; i <= 3; i++) g.moveTo(0, r * 0.4).lineTo(Math.cos(-Math.PI / 2 + i * 0.38) * r * 0.95, r * 0.4 + Math.sin(-Math.PI / 2 + i * 0.38) * r * 0.95).stroke({ color: darken(style.color, 0.25), width: size * 0.02 });585 g.rect(-r * 0.95, r * 0.4, r * 1.9, r * 0.18).fill(accent);586 break;587 }588 case "planet": {589 g.circle(0, 0, r * 0.7).fill(main);590 g.ellipse(0, r * 0.05, r * 1.05, r * 0.28).stroke({ color: accent, width: size * 0.04 });591 g.circle(-r * 0.25, -r * 0.25, r * 0.18).fill({ color: 0xffffff, alpha: 0.3 });592 g.ellipse(r * 0.15, r * 0.15, r * 0.3, r * 0.12).fill({ color: darken(style.color, 0.25), alpha: 0.8 });593 break;594 }595 case "comet": {596 g.moveTo(r * 0.5, -r * 0.5).lineTo(-r * 1.0, r * 0.15).lineTo(-r * 0.6, r * 0.6).lineTo(r * 0.5, -r * 0.5).fill({ color: accent, alpha: 0.5 });597 g.moveTo(r * 0.5, -r * 0.5).lineTo(-r * 0.7, r * 0.7).stroke({ color: accent, alpha: 0.4, width: size * 0.05 });598 g.circle(r * 0.5, -r * 0.5, r * 0.4).fill(main);599 g.circle(r * 0.4, -r * 0.6, r * 0.12).fill({ color: 0xffffff, alpha: 0.6 });600 break;601 }602 case "satellite": {603 g.roundRect(-r * 0.3, -r * 0.3, r * 0.6, r * 0.6, r * 0.1).fill(main);604 g.rect(-r * 1.0, -r * 0.18, r * 0.6, r * 0.36).fill(accent);605 g.rect(r * 0.4, -r * 0.18, r * 0.6, r * 0.36).fill(accent);606 for (const x of [-0.8, -0.6, 0.6, 0.8]) g.moveTo(x * r, -r * 0.18).lineTo(x * r, r * 0.18).stroke({ color: darken(style.color, 0.4), width: size * 0.012 });607 g.arc(0, -r * 0.55, r * 0.3, Math.PI, 0).stroke({ color: main, width: size * 0.03 });608 break;609 }610 case "pickaxe": {611 g.moveTo(-r * 0.7, r * 0.9).lineTo(r * 0.5, -r * 0.4).stroke({ color: 0x8a6d2e, width: size * 0.06 });612 g.moveTo(r * 0.0, -r * 0.9).bezierCurveTo(r * 0.7, -r * 0.9, r * 1.0, -r * 0.4, r * 0.95, r * 0.15).lineTo(r * 0.55, -r * 0.35).fill(main);613 g.moveTo(r * 0.0, -r * 0.9).lineTo(r * 0.55, -r * 0.35).stroke({ color: accent, width: size * 0.03 });614 break;615 }616 case "ore": {617 polygon(g, 0, r * 0.1, r * 0.9, 7).fill(0x3a3f4a);618 g.poly([-r * 0.3, -r * 0.1, 0, -r * 0.5, r * 0.35, -r * 0.05, r * 0.1, r * 0.35, -r * 0.25, r * 0.3]).fill(main);619 g.poly([-r * 0.3, -r * 0.1, 0, -r * 0.5, 0, 0]).fill(accent);620 break;621 }622 case "drill": {623 g.rect(-r * 0.4, -r * 0.9, r * 0.8, r * 0.7).fill(main);624 g.poly([-r * 0.4, -r * 0.2, r * 0.4, -r * 0.2, 0, r]).fill(0xc8d0dc);625 for (let i = 0; i < 4; i++) g.moveTo(-r * 0.32 + i * r * 0.1, -r * 0.2 + i * r * 0.28).lineTo(r * 0.32 - i * r * 0.1, -r * 0.2 + i * r * 0.28).stroke({ color: 0x5c6270, width: size * 0.015 });626 g.rect(-r * 0.15, -r * 0.6, r * 0.3, r * 0.15).fill(accent);627 break;628 }629 case "ingot": {630 g.poly([-r * 0.7, -r * 0.3, r * 0.7, -r * 0.3, r * 0.95, r * 0.5, -r * 0.95, r * 0.5]).fill(main);631 g.poly([-r * 0.7, -r * 0.3, r * 0.7, -r * 0.3, r * 0.5, 0, -r * 0.5, 0]).fill(accent);632 g.moveTo(-r * 0.5, r * 0.15).lineTo(r * 0.5, r * 0.15).stroke({ color: darken(style.color, 0.3), width: size * 0.02 });633 break;634 }635 case "hourglass": {636 g.poly([-r * 0.7, -r, r * 0.7, -r, r * 0.1, 0, r * 0.7, r, -r * 0.7, r, -r * 0.1, 0]).stroke({ color: main, width: size * 0.04 });637 g.poly([-r * 0.45, -r * 0.8, r * 0.45, -r * 0.8, 0, -r * 0.1]).fill({ color: accent, alpha: 0.8 });638 g.poly([-r * 0.55, r * 0.9, r * 0.55, r * 0.9, 0, r * 0.35]).fill({ color: accent, alpha: 0.8 });639 break;640 }641 case "igloo": {642 g.arc(0, r * 0.4, r * 0.95, Math.PI, 0).fill(main);643 g.rect(-r * 0.95, r * 0.4, r * 1.9, r * 0.2).fill(main);644 g.arc(-r * 0.3, r * 0.5, r * 0.35, Math.PI, 0).fill(0x0b0d14);645 for (let i = 0; i < 3; i++) g.arc(0, r * 0.4, r * (0.35 + i * 0.25), Math.PI, 0).stroke({ color: accent, width: size * 0.015, alpha: 0.6 });646 break;647 }648 case "paw": {649 g.ellipse(0, r * 0.35, r * 0.55, r * 0.45).fill(main);650 for (const [dx, dy] of [[-0.6, -0.25], [-0.22, -0.6], [0.22, -0.6], [0.6, -0.25]]) g.circle(dx * r, dy * r, r * 0.2).fill(main);651 g.ellipse(0, r * 0.4, r * 0.3, r * 0.22).fill({ color: accent, alpha: 0.6 });652 break;653 }654 case "fish": {655 g.ellipse(-r * 0.1, 0, r * 0.7, r * 0.42).fill(main);656 g.poly([r * 0.5, 0, r, -r * 0.45, r, r * 0.45]).fill(accent);657 g.circle(-r * 0.5, -r * 0.1, r * 0.08).fill(0x0b0d14);658 g.moveTo(-r * 0.2, -r * 0.4).bezierCurveTo(0, -r * 0.7, r * 0.2, -r * 0.5, r * 0.25, -r * 0.3).fill({ color: accent, alpha: 0.8 });659 break;660 }661 case "meteor": {662 polygon(g, r * 0.2, r * 0.2, r * 0.6, 7, 0.3).fill(main);663 g.circle(r * 0.35, 0, r * 0.12).fill({ color: darken(style.color, 0.3) });664 g.circle(0, r * 0.4, r * 0.1).fill({ color: darken(style.color, 0.3) });665 g.moveTo(-r * 0.2, -r * 0.3).lineTo(-r * 1.0, -r * 1.0).stroke({ color: accent, alpha: 0.6, width: size * 0.06 });666 g.moveTo(r * 0.1, -r * 0.55).lineTo(-r * 0.5, -r * 1.05).stroke({ color: accent, alpha: 0.4, width: size * 0.04 });667 break;668 }669 }670 c.addChild(g);671 return c;672}673674function label(text: string, fontSize: number, color: number, bold = true): Text {675 const t = new Text({676 text,677 style: new TextStyle({ fontFamily: "Geist, Inter, system-ui, sans-serif", fontSize, fontWeight: bold ? "800" : "600", fill: color, letterSpacing: 1 }),678 });679 t.anchor.set(0.5);680 return t;681}682683export { hex as hexColor, lighten, darken };684