SPB Git

spb/spinza

Public
11.7 KB · 270 lines typescript
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}5556export function drawSymbol(style: SymbolStyle, opts: SymbolArtOptions): Container {57  const { size } = opts;58  const c = new Container();59  const main = hex(style.color);60  const accent = style.accent ? hex(style.accent) : lighten(style.color, 0.35);61  const glow = style.glow ?? 0.3;62  const r = size * 0.36; // icon radius63  const g = new Graphics();6465  // Tile plate.66  const plate = new Graphics();67  const pad = size * 0.06;68  plate.roundRect(-size / 2 + pad, -size / 2 + pad, size - pad * 2, size - pad * 2, size * 0.16).fill({ color: 0x0b0d14, alpha: 0.72 });69  plate.roundRect(-size / 2 + pad, -size / 2 + pad, size - pad * 2, size - pad * 2, size * 0.16).stroke({ color: opts.tier === "premium" || opts.tier === "special" ? accent : 0xffffff, alpha: opts.tier === "premium" || opts.tier === "special" ? 0.45 : 0.08, width: Math.max(1, size * 0.012) });70  c.addChild(plate);7172  // Glow halo.73  if (glow > 0) {74    const halo = new Graphics();75    halo.circle(0, 0, r * 1.25).fill({ color: main, alpha: 0.18 * glow });76    halo.circle(0, 0, r * 1.0).fill({ color: main, alpha: 0.16 * glow });77    c.addChild(halo);78  }7980  switch (style.shape) {81    case "gem": {82      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);83      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 });84      g.poly([0, -r * 0.1, r * 0.85, -r * 0.25, r * 0.55, r]).fill({ color: darken(style.color, 0.15) });85      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 });86      break;87    }88    case "hex": {89      polygon(g, 0, 0, r, 6).fill(main);90      polygon(g, 0, 0, r * 0.68, 6).fill({ color: darken(style.color, 0.25) });91      polygon(g, 0, 0, r * 0.42, 6).fill(accent);92      if (style.label) c.addChild(label(style.label, size * 0.26, 0x0b0d14));93      break;94    }95    case "star": {96      star(g, 0, 0, r, r * 0.45).fill(main);97      star(g, 0, 0, r * 0.55, r * 0.25).fill({ color: 0xffffff, alpha: 0.85 });98      break;99    }100    case "circle": {101      g.circle(0, 0, r).fill(main);102      g.circle(-r * 0.3, -r * 0.3, r * 0.35).fill({ color: 0xffffff, alpha: 0.35 });103      g.circle(0, 0, r).stroke({ color: accent, width: size * 0.02 });104      break;105    }106    case "diamond": {107      g.poly([0, -r, r, 0, 0, r, -r, 0]).fill(main);108      g.poly([0, -r, r, 0, 0, 0]).fill({ color: accent, alpha: 0.8 });109      g.poly([0, 0, 0, r, -r, 0]).fill({ color: darken(style.color, 0.2) });110      break;111    }112    case "shield": {113      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);114      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) });115      g.rect(-r * 0.08, -r * 0.5, r * 0.16, r * 0.9).fill(accent);116      g.rect(-r * 0.38, -r * 0.25, r * 0.76, r * 0.16).fill(accent);117      break;118    }119    case "bolt": {120      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);121      g.poly([r * 0.1, -r * 0.8, -r * 0.3, r * 0.0, r * 0.0, 0]).fill({ color: 0xffffff, alpha: 0.5 });122      break;123    }124    case "ring": {125      g.circle(0, 0, r).fill(main);126      g.circle(0, 0, r * 0.62).cut();127      g.circle(0, 0, r * 0.62).stroke({ color: accent, width: size * 0.02, alpha: 0.9 });128      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 });129      break;130    }131    case "chip": {132      g.roundRect(-r, -r * 0.75, r * 2, r * 1.5, r * 0.25).fill(main);133      g.roundRect(-r * 0.75, -r * 0.5, r * 1.5, r, r * 0.15).fill({ color: darken(style.color, 0.35) });134      for (let i = -2; i <= 2; i++) {135        g.rect(-r - r * 0.18, i * r * 0.28 - r * 0.06, r * 0.18, r * 0.12).fill(accent);136        g.rect(r, i * r * 0.28 - r * 0.06, r * 0.18, r * 0.12).fill(accent);137      }138      if (style.label) c.addChild(label(style.label, size * 0.22, accent));139      break;140    }141    case "crystal": {142      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);143      g.poly([0, -r, r * 0.5, -r * 0.3, 0, r * 0.1]).fill({ color: accent, alpha: 0.75 });144      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) });145      break;146    }147    case "letter": {148      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 });149      g.roundRect(-r * 0.95, -r * 0.95, r * 1.9, r * 1.9, r * 0.3).stroke({ color: main, width: size * 0.025 });150      c.addChild(label(style.label ?? "?", size * 0.4, main, true));151      break;152    }153    case "vault": {154      g.circle(0, 0, r).fill(darken(style.color, 0.4));155      g.circle(0, 0, r).stroke({ color: main, width: size * 0.03 });156      g.circle(0, 0, r * 0.7).stroke({ color: main, width: size * 0.02, alpha: 0.7 });157      for (let i = 0; i < 6; i++) {158        const a = (i * Math.PI) / 3;159        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 });160      }161      g.circle(0, 0, r * 0.22).fill(accent);162      break;163    }164    case "orb": {165      g.circle(0, 0, r).fill(main);166      g.circle(0, 0, r * 0.75).fill({ color: accent, alpha: 0.35 });167      g.circle(-r * 0.25, -r * 0.3, r * 0.25).fill({ color: 0xffffff, alpha: 0.5 });168      g.circle(0, 0, r).stroke({ color: accent, width: size * 0.02, alpha: 0.9 });169      if (style.label) c.addChild(label(style.label, size * 0.36, accent, true));170      break;171    }172    case "coin": {173      g.circle(0, 0, r).fill(main);174      g.circle(0, 0, r * 0.8).stroke({ color: darken(style.color, 0.25), width: size * 0.03 });175      g.circle(0, 0, r).stroke({ color: accent, width: size * 0.02 });176      c.addChild(label(style.label ?? "SC", size * 0.24, darken(style.color, 0.5), true));177      break;178    }179    case "skull": {180      g.circle(0, -r * 0.15, r * 0.8).fill(main);181      g.roundRect(-r * 0.45, r * 0.3, r * 0.9, r * 0.55, r * 0.15).fill(main);182      g.circle(-r * 0.3, -r * 0.2, r * 0.22).fill(0x0b0d14);183      g.circle(r * 0.3, -r * 0.2, r * 0.22).fill(0x0b0d14);184      g.poly([0, r * 0.05, r * 0.12, r * 0.3, -r * 0.12, r * 0.3]).fill(0x0b0d14);185      break;186    }187    case "flame": {188      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);189      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);190      break;191    }192    case "snow": {193      for (let i = 0; i < 3; i++) {194        const a = (i * Math.PI) / 3;195        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 });196        for (const s of [1, -1]) {197          const bx = Math.cos(a) * r * 0.6 * s;198          const by = Math.sin(a) * r * 0.6 * s;199          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 });200          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 });201        }202      }203      g.circle(0, 0, r * 0.14).fill(0xffffff);204      break;205    }206    case "eye": {207      g.ellipse(0, 0, r, r * 0.6).fill(main);208      g.circle(0, 0, r * 0.42).fill(darken(style.color, 0.5));209      g.circle(0, 0, r * 0.2).fill(accent);210      g.circle(r * 0.12, -r * 0.14, r * 0.07).fill(0xffffff);211      break;212    }213    case "moon": {214      g.circle(0, 0, r).fill(main);215      g.circle(r * 0.45, -r * 0.2, r * 0.78).fill({ color: 0x0b0d14, alpha: 0.85 });216      g.circle(-r * 0.4, r * 0.3, r * 0.12).fill({ color: darken(style.color, 0.2) });217      break;218    }219    case "leaf": {220      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);221      g.moveTo(0, r * 0.9).lineTo(0, -r * 0.8).stroke({ color: accent, width: size * 0.02 });222      break;223    }224    case "atom": {225      g.circle(0, 0, r * 0.2).fill(accent);226      for (let i = 0; i < 3; i++) {227        const e = new Graphics();228        e.ellipse(0, 0, r, r * 0.38).stroke({ color: main, width: size * 0.025 });229        e.rotation = (i * Math.PI) / 3;230        c.addChild(e);231      }232      break;233    }234    case "crown": {235      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);236      g.rect(-r, r * 0.4, r * 2, r * 0.25).fill(darken(style.color, 0.3));237      g.circle(0, -r * 0.95, r * 0.12).fill(accent);238      g.circle(-r, -r * 0.4, r * 0.1).fill(accent);239      g.circle(r, -r * 0.4, r * 0.1).fill(accent);240      break;241    }242    case "wave": {243      for (let k = -1; k <= 1; k++) {244        const y = k * r * 0.5;245        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 });246      }247      break;248    }249    case "cube": {250      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);251      g.poly([0, -r, r * 0.87, -r * 0.5, 0, 0, -r * 0.87, -r * 0.5]).fill(accent);252      g.poly([0, 0, r * 0.87, -r * 0.5, r * 0.87, r * 0.5, 0, r]).fill(darken(style.color, 0.3));253      break;254    }255  }256  c.addChild(g);257  return c;258}259260function label(text: string, fontSize: number, color: number, bold = true): Text {261  const t = new Text({262    text,263    style: new TextStyle({ fontFamily: "Geist, Inter, system-ui, sans-serif", fontSize, fontWeight: bold ? "800" : "600", fill: color, letterSpacing: 1 }),264  });265  t.anchor.set(0.5);266  return t;267}268269export { hex as hexColor, lighten, darken };270
Files · 3b4179a710ab1cd5f12ca7cfd253b81674f1521e
Menu

Theme