// The black-figure figure kit for the saga friezes. Copied from mockup 17's figures.ts (via 04 and 13; // mockups are never imported) and extended with the props the nine sagas need: tridents, bows, wings, // long hair, pigs, cattle, trees, columns, cliffs, a whirlpool and Scylla's heads. // Glaze is `fill: var(--glaze)` (.ink); "incised" detail is scratched back to the clay (.inc). // Every vignette shares one 300-unit-tall panel space with the ground line at GROUND. export const PANEL_H = 300; export const GROUND = 284; /** Escapes text for SVG/HTML markup, apostrophes included. */ export const esc = (s: string) => s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]!); export const g = (t: string, body: string) => `${body}`; export const inc = (d: string, w = 1.6) => ``; export const ink = (d: string) => ``; /** A limb: a glaze stroke over a wider clay stroke, so it reads as incised where it crosses glaze. */ export const limb = (d: string, w = 6) => ``; type Arms = "spear" | "open" | "down" | "raise" | "reach" | "mourn" | "club" | "throw" | "bow" | "bolt"; export interface Person { x: number; y?: number; s?: number; f?: 1 | -1; arms?: Arms; helmet?: boolean; shield?: boolean; robe?: boolean; cyclops?: boolean; kneel?: boolean; beard?: boolean; infant?: boolean; cup?: boolean; /** Long hair down the back (women, goddesses). */ hair?: boolean; wings?: boolean; crown?: boolean; trident?: boolean; bag?: boolean; staff?: boolean; } export function person(p: Person): string { const { x, y = GROUND, s = 1, f = 1, arms = "down" } = p; const lift = p.kneel ? 36 : 0; let legs = ""; if (p.kneel) { legs = limb("M4,-44 L34,-40 L36,-2", 8) + limb("M-2,-44 L-24,-4 L-4,-2", 8); } else if (!p.robe) { legs = limb("M-2,-80 L-10,-40 L-24,-2 L-14,-2", 8) + limb("M4,-80 L14,-40 L30,-2 L40,-2", 8); } let upper = ""; if (p.wings) { upper += ink("M-8,-150 Q-60,-196 -78,-150 Q-62,-152 -66,-132 Q-50,-138 -50,-118 Q-34,-126 -10,-118 Z"); upper += inc("M-14,-142 Q-44,-160 -66,-150 M-14,-130 Q-38,-142 -58,-132", 1.4); } if (p.hair) upper += ink("M-8,-176 Q-20,-150 -14,-128 L-2,-132 Q-6,-152 2,-168 Z"); // the back arm sits behind the torso if (arms === "open") upper += limb("M-8,-148 L-36,-176 L-44,-196"); if (arms === "mourn") upper += limb("M-8,-148 L-14,-120 L-6,-104"); if (arms === "bow") upper += limb("M-8,-148 L6,-150 L18,-152"); upper += ink("M-3,-160 h8 v10 h-8 Z"); upper += ink("M-13,-154 L15,-154 L9,-104 L-5,-104 Z"); if (p.robe) { upper += ink("M-9,-108 L11,-108 L28,-2 L-24,-2 Z"); upper += inc("M1,-100 L2,-8 M-5,-96 L-14,-8 M7,-96 L18,-8 M-20,-30 L24,-30"); } else { upper += ink("M-8,-108 L10,-108 L20,-74 L-18,-74 Z"); upper += inc("M1,-104 L1,-77 M-5,-100 L-10,-77 M7,-100 L12,-77"); } upper += inc("M-9,-146 L11,-146 M-2,-150 L1,-112"); // head upper += ``; upper += ink("M10,-173 L18,-166 L10,-162 Z"); if (p.beard) upper += ink("M-2,-162 L13,-162 L5,-146 Z") + inc("M2,-158 L5,-150", 1.2); if (p.cyclops) { upper += ``; } else { upper += inc("M5,-171 h5", 1.4); } if (p.helmet) { upper += ink("M-12,-166 A14 14 0 0 1 15,-170 L15,-160 L9,-160 L9,-166 Z"); upper += ink("M-18,-178 Q-8,-214 24,-194 Q4,-200 -8,-176 Z"); upper += inc("M-10,-166 L13,-168 M4,-176 L4,-166"); } else if (p.crown) { upper += ink("M-9,-176 L-7,-190 L-2,-180 L2,-194 L6,-180 L11,-190 L13,-176 Z"); } else if (!p.cyclops) { upper += inc("M-6,-176 Q2,-182 10,-176", 1.3); } if (p.shield) { upper += ``; upper += ``; upper += ``; } // front arm switch (arms) { case "spear": upper += limb("M10,-148 L30,-132 L40,-150"); upper += limb("M-4,-52 L72,-222", 3.5); upper += p.trident ? ink("M72,-222 L80,-246 L74,-224 Z M64,-226 L58,-248 L70,-228 Z M80,-218 L96,-238 L82,-214 Z") + limb("M62,-228 L84,-214", 3) : ink("M72,-222 L66,-244 L80,-226 Z"); break; case "open": upper += limb("M10,-148 L40,-178 L48,-198"); break; case "raise": upper += limb("M10,-148 L22,-182 L26,-214"); break; case "reach": upper += limb("M10,-148 L36,-140 L56,-146"); break; case "mourn": upper += limb("M10,-148 L26,-136 L12,-172"); break; case "club": upper += limb("M10,-148 L30,-130 L40,-142"); upper += limb("M18,-96 L66,-214", 10) + ink("M60,-218 Q66,-236 78,-214 Z"); break; case "throw": upper += limb("M10,-148 L-6,-182 L-12,-214"); upper += `` + inc("M-26,-238 Q-16,-246 -6,-236 M-24,-226 l10,2", 1.4); break; case "bow": upper += limb("M10,-148 L38,-150 L60,-152"); upper += ``; upper += `` + ``; upper += ink("M92,-152 L82,-157 L84,-152 L82,-147 Z"); break; case "bolt": upper += limb("M10,-148 L22,-182 L26,-210"); upper += g("translate(14,-262) scale(0.62)", ink("M0,0 L22,0 L10,34 L24,34 L-6,92 L2,48 L-12,48 Z")); break; default: upper += limb("M10,-148 L16,-104"); } if (p.staff) upper += limb("M14,-100 L22,-236", 3.5) + ``; if (p.infant) { upper += ``; upper += inc("M52,-158 l12,-3 M53,-148 l12,-3 M54,-138 l11,-3", 1.3); } if (p.cup) { upper += ink("M48,-162 h26 q-2,12 -13,12 q-11,0 -13,-12 Z M60,-150 v5 h-5 h10"); upper += inc("M52,-158 h18", 1.2); } if (p.bag) { upper += ink("M54,-146 Q46,-138 50,-116 Q58,-96 76,-104 Q88,-118 76,-138 Q70,-146 60,-146 Z M55,-150 h8 l-2,6 h-5 Z"); upper += inc("M58,-130 q8,6 16,0 M56,-118 q10,6 20,-2", 1.3); } const body = legs + (lift ? g(`translate(0,${lift})`, upper) : upper); return g(`translate(${x},${y}) scale(${f * s},${s})`, body); } export function trojanHorse(x: number, s: number): string { let b = ""; b += ink("M-92,-32 h176 v12 h-176 Z"); for (const lx of [-60, -42, 30, 48]) b += limb(`M${lx},-112 L${lx},-30`, 10); b += limb("M-80,-150 Q-108,-128 -98,-88", 6); b += ink("M-84,-164 h150 q10,0 10,12 v36 q0,14 -14,14 h-132 q-14,0 -14,-14 v-36 q0,-12 12,-12 Z"); b += ink("M40,-156 L60,-236 L88,-230 L68,-140 Z"); b += ink("M58,-238 Q70,-256 100,-244 L124,-208 Q120,-196 106,-202 L84,-224 Z M62,-246 L68,-266 L76,-244 Z"); b += inc("M-76,-146 H62 M-76,-130 H62 M-76,-116 H62 M-40,-160 V-104 M0,-160 V-104 M36,-160 V-104"); b += inc("M62,-228 l6,-6 l2,8 l6,-6 l2,8 l6,-6 M50,-196 l6,-6 l2,8 l6,-6", 1.4); b += ``; b += inc("M-6,-150 h28 v22 h-28 Z", 1.4); for (const wx of [-66, 56]) { b += ``; b += inc(`M${wx - 8},-12 h16 M${wx},-20 v16`, 1.2); } return g(`translate(${x},${GROUND}) scale(${s})`, b); } export function ship(x: number, y: number, s: number, o: { sail?: boolean; tilt?: number; f?: 1 | -1; broken?: boolean } = {}): string { const f = o.f ?? 1; let b = ""; for (let i = 0; i < 9; i++) { const ox = -96 + i * 24; b += ``; } if (o.broken) { b += limb("M0,-50 L0,-150", 5) + limb("M0,-150 L40,-196", 5); } else { b += ``; b += limb("M0,-50 L0,-236", 5); if (o.sail) { b += ink("M-78,-222 h156 v104 q-78,-18 -156,0 Z"); let grid = ""; for (let gx = -58; gx < 78; gx += 20) grid += `M${gx},-220 V-122 `; grid += "M-76,-196 H76 M-76,-172 H76 M-76,-148 H76"; b += inc(grid, 1.3); } b += limb("M-86,-222 L86,-222", 4); } for (let i = 0; i < 8; i++) b += ``; b += ink("M-160,-104 Q-150,-52 -108,-30 L118,-30 L178,-36 L132,-46 L140,-84 L128,-86 Q122,-54 100,-54 L-100,-54 Q-140,-58 -150,-104 Z"); b += inc("M-112,-40 H120 M-118,-48 H104", 1.4); b += ``; return g(`translate(${x},${y}) rotate(${o.tilt ?? 0}) scale(${f * s},${s})`, b); } export function waves(x0: number, x1: number, y: number, size = 1): string { let d = ""; const step = 36 * size; for (let x = x0; x < x1 - step / 2; x += step) { d += `M${x},${y} C${x + 8 * size},${y - 22 * size} ${x + 34 * size},${y - 24 * size} ${x + 34 * size},${y - 8 * size} C${x + 34 * size},${y} ${x + 22 * size},${y + 2 * size} ${x + 20 * size},${y - 6 * size} `; } return ``; } export function stele(x: number): string { return g(`translate(${x},${GROUND})`, ink("M-36,0 v-12 h72 v12 Z M-24,-12 V-150 L0,-178 L24,-150 V-12 Z") + inc("M-18,-146 H18 M-14,-60 H14 M-14,-48 H14 M-14,-36 H14") + ``); } export const bolt = (x: number, y: number, s: number) => g(`translate(${x},${y}) scale(${s})`, ink("M0,0 L22,0 L10,34 L24,34 L-6,92 L2,48 L-12,48 Z")); export function sheep(x: number, s: number, f: 1 | -1 = 1): string { let b = ""; b += limb("M-14,-14 L-16,0", 4) + limb("M-4,-14 L-3,0", 4) + limb("M12,-14 L13,0", 4) + limb("M20,-14 L22,0", 4); b += ink("M-24,-30 Q-26,-46 -10,-48 Q2,-58 14,-48 Q30,-50 28,-32 Q32,-16 16,-14 L-14,-14 Q-30,-14 -24,-30 Z"); b += inc("M-14,-38 q4,-5 8,0 q4,-5 8,0 q4,-5 8,0 M-16,-26 q4,-5 8,0 q4,-5 8,0 q4,-5 8,0 q4,-5 8,0", 1.3); b += ink("M26,-40 L40,-44 Q46,-38 40,-30 L28,-30 Z"); b += ``; return g(`translate(${x},${GROUND}) scale(${f * s},${s})`, b); } /** Circe's work: a pig, snout forward, curly tail. */ export function pig(x: number, s: number, f: 1 | -1 = 1): string { let b = ""; b += limb("M-16,-12 L-18,0", 5) + limb("M-6,-12 L-5,0", 5) + limb("M12,-12 L13,0", 5) + limb("M20,-12 L22,0", 5); b += ink("M-30,-26 Q-30,-46 -6,-48 Q22,-50 30,-36 L42,-34 Q46,-28 42,-22 L30,-20 Q22,-10 -6,-10 Q-30,-10 -30,-26 Z"); b += ink("M22,-46 L30,-58 L32,-44 Z"); b += ``; b += `` + inc("M40,-30 v4", 1.4) + inc("M-14,-22 Q4,-16 18,-24", 1.3); return g(`translate(${x},${GROUND}) scale(${f * s},${s})`, b); } /** Helios' cattle: a broad bull with lyre horns. */ export function bull(x: number, s: number, f: 1 | -1 = 1): string { let b = ""; b += limb("M-30,-24 L-32,0", 7) + limb("M-16,-24 L-14,0", 7) + limb("M22,-24 L24,0", 7) + limb("M34,-24 L36,0", 7); b += ink("M-44,-46 Q-44,-70 -14,-72 L30,-72 Q48,-70 50,-52 L62,-60 Q74,-58 72,-44 L60,-36 Q50,-22 30,-22 L-30,-22 Q-44,-24 -44,-46 Z"); b += ``; b += ``; b += `` + inc("M-30,-60 Q0,-52 34,-60 M-24,-36 Q4,-30 30,-38", 1.4); return g(`translate(${x},${GROUND}) scale(${f * s},${s})`, b); } /** A tree: Calypso's palm or Ithaca's olive. */ export function tree(x: number, s: number, kind: "palm" | "olive"): string { let b = ""; if (kind === "palm") { b += limb("M0,0 Q-6,-80 6,-170", 9); for (let i = -3; i <= 3; i++) b += ``; b += inc("M-2,-30 h8 M-3,-60 h8 M-2,-90 h8 M0,-120 h8 M3,-150 h7", 1.4); } else { b += limb("M0,0 Q-10,-50 4,-100 M2,-90 Q-24,-110 -34,-128 M4,-100 Q26,-112 34,-134", 8); b += ink("M-58,-130 Q-64,-170 -30,-176 Q-16,-206 16,-196 Q50,-200 54,-166 Q72,-140 44,-124 Q20,-112 -4,-122 Q-40,-108 -58,-130 Z"); b += inc("M-40,-150 q8,-6 14,0 M-10,-170 q8,-6 14,0 M20,-156 q8,-6 14,0 M-24,-132 q8,-6 14,0 M28,-136 q8,-6 14,0", 1.4); } return g(`translate(${x},${GROUND}) scale(${s})`, b); } /** A Doric column, base on the ground line. */ export function column(x: number, h: number, w = 26): string { const half = w / 2; let b = ink(`M${-half - 6},0 h${w + 12} v-8 h${-w - 12} Z`); b += ink(`M${-half},-8 L${-half + 2},${-h + 14} h${w - 4} L${half},-8 Z`); b += ink(`M${-half - 8},${-h + 14} h${w + 16} v-8 h${-w - 16} Z M${-half - 4},${-h + 6} h${w + 8} v-6 h${-w - 8} Z`); b += inc(`M${-half / 2},-12 V${-h + 18} M0,-12 V${-h + 18} M${half / 2},-12 V${-h + 18}`, 1.3); return g(`translate(${x},${GROUND})`, b); } /** A jagged sea cliff rising from the ground line. */ export function cliff(x: number, w: number, h: number, f: 1 | -1 = 1): string { const b = ink(`M0,0 L0,${-h * 0.7} L${w * 0.18},${-h} L${w * 0.42},${-h * 0.86} L${w * 0.56},${-h * 0.62} L${w * 0.8},${-h * 0.5} L${w},0 Z`) + inc(`M${w * 0.14},${-h * 0.62} q${w * 0.1},${-h * 0.12} ${w * 0.2},${-h * 0.1} M${w * 0.3},${-h * 0.34} q${w * 0.16},${-h * 0.08} ${w * 0.3},0 M${w * 0.62},${-h * 0.4} q${w * 0.1},${h * 0.04} ${w * 0.16},${h * 0.16}`, 1.8); return g(`translate(${x},${GROUND}) scale(${f},1)`, b); } /** Scylla: long necks out of a cave in the cliff, each ending in a snapping head. */ export function scyllaHeads(x: number, y: number, s: number): string { let b = ""; const necks: [string, number, number, number][] = [ ["M0,0 Q40,-60 90,-40", 90, -40, -10], ["M0,0 Q60,-10 110,24", 110, 24, 20], ["M0,0 Q20,-110 70,-110", 70, -110, -24], ["M0,0 Q70,40 120,80", 120, 80, 34], ]; for (const [d, hx, hy, r] of necks) { b += limb(d, 9); b += g(`translate(${hx},${hy}) rotate(${r})`, ink("M-6,-9 L22,-6 L30,-1 L16,2 L28,7 L-6,9 Z") + ``); } return g(`translate(${x},${y}) scale(${s})`, b); } /** Charybdis: a spiral whirlpool seen from the side, as stacked scrolls. */ export function whirlpool(cx: number, y: number, r: number): string { let d = ""; for (let k = 0; k <= 64; k++) { const a = (k / 64) * Math.PI * 6; const rr = r * (1 - k / 70); d += `${k ? "L" : "M"}${(cx + Math.cos(a) * rr).toFixed(1)},${(y - 30 + Math.sin(a) * rr * 0.32).toFixed(1)}`; } return `` + ink(`M${cx - 10},${y - 34} L${cx},${y - 6} L${cx + 10},${y - 34} Z`) + waves(cx - r - 20, cx + r + 20, y, 1.1); } export function palmette(cx: number, cy: number, flip = false): string { let leaves = ""; for (let i = -3; i <= 3; i++) { leaves += ``; } const volutes = ``; return g(`translate(${cx},${cy}) scale(1,${flip ? -1 : 1})`, leaves + `` + volutes); } /** A shade of the dead: a robed, veiled figure with mourning arms. */ export const shade = (x: number, s: number, f: 1 | -1 = 1) => person({ x, s, f, arms: "mourn", robe: true, hair: true });