import { CANVAS, MARGIN, SAFE } from "./constants.js"; import { FONT_STACK, initialsOf, pick, polygonPoints, round, xmlEscape } from "./palette.js"; import type { Geometry, Palette } from "./types.js"; const CONTAINERS = ["circle", "squircle", "hexagon", "shield"] as const; function containerShape(kind: (typeof CONTAINERS)[number], fill: string): string { const cx = CANVAS / 2; switch (kind) { case "circle": return ` `; case "squircle": return ` `; case "hexagon": return ` `; default: return ` `; } } export const GEOMETRIES: Geometry[] = [ { name: "monogram", build: ({ rng, palette, brand }) => { const container = pick(rng, CONTAINERS); const initials = initialsOf(brand); const fontSize = initials.length > 1 ? 92 : 118; const barY = CANVAS - MARGIN - 14; return { description: `Brand initials "${initials}" knocked out of a solid ${container} container with an accent underline.`, body: [ containerShape(container, palette.primary), ` ${xmlEscape( initials, )}`, ` `, ].join("\n"), }; }, }, { name: "glyph-grid", build: ({ rng, palette }) => { const cells = rng() > 0.5 ? 4 : 3; const gap = 10; const size = (SAFE - gap * (cells - 1)) / cells; const parts: string[] = []; const half = Math.ceil(cells / 2); const plan: string[][] = []; for (let row = 0; row < cells; row += 1) { const rowPlan: string[] = []; for (let col = 0; col < half; col += 1) { const roll = rng(); rowPlan.push(roll < 0.22 ? "none" : roll < 0.52 ? "square" : roll < 0.8 ? "circle" : "triangle"); } for (let col = 0; col < cells; col += 1) { rowPlan[col] = rowPlan[col] ?? rowPlan[cells - 1 - col]; } plan.push(rowPlan); } let filled = 0; for (let row = 0; row < cells; row += 1) { for (let col = 0; col < cells; col += 1) { const shape = plan[row][col]; if (shape === "none") continue; filled += 1; const x = MARGIN + col * (size + gap); const y = MARGIN + row * (size + gap); const fill = (row + col) % 2 === 0 ? palette.primary : palette.accent; if (shape === "square") { parts.push( ` `, ); } else if (shape === "circle") { parts.push( ` `, ); } else { parts.push( ` `, ); } } } return { description: `${cells}x${cells} modular glyph grid, mirrored across the vertical axis, ${filled} active cells alternating primary and accent.`, body: ` \n${parts.join("\n")}\n `, }; }, }, { name: "orbit", build: ({ rng, palette }) => { const cx = CANVAS / 2; const ringWidth = 14 + Math.floor(rng() * 8); const outer = SAFE / 2 - ringWidth / 2; const gapDegrees = 40 + Math.floor(rng() * 50); const circumference = 2 * Math.PI * outer; const dash = circumference * (1 - gapDegrees / 360); const rotation = Math.floor(rng() * 360); const dotAngle = ((rotation - 90 + gapDegrees / 2) * Math.PI) / 180; return { description: `Open orbital ring with a ${gapDegrees} degree aperture, a solid accent core, and a satellite dot on the ring path.`, body: [ ` `, ` `, ` `, ` `, ` `, ].join("\n"), }; }, }, { name: "chevron-stack", build: ({ rng, palette }) => { const layers = 3 + Math.floor(rng() * 2); const thickness = 16 + Math.floor(rng() * 6); const spacing = SAFE / (layers + 1); const parts: string[] = []; for (let i = 0; i < layers; i += 1) { const y = MARGIN + spacing * (i + 0.5); const inset = i * 10; const fill = i % 2 === 0 ? palette.primary : palette.accent; parts.push( ` `, ); } return { description: `${layers} nested chevrons with ${thickness}px strokes reading as forward motion or an upward stack.`, body: ` \n${parts.join("\n")}\n `, }; }, }, { name: "prism", build: ({ rng, palette }) => { const cx = CANVAS / 2; const radius = SAFE / 2; const rotationA = Math.floor(rng() * 60); const rotationB = rotationA + 120 + Math.floor(rng() * 60); return { description: `Two overlapping equilateral triangles rotated ${rotationA} and ${rotationB} degrees, forming a translucent prism intersection.`, body: [ ` `, ` `, ` `, ` `, ].join("\n"), }; }, }, { name: "aperture", build: ({ rng, palette }) => { const cx = CANVAS / 2; const blades = 5 + Math.floor(rng() * 3); const inner = SAFE * 0.16; const outer = SAFE / 2; const parts: string[] = []; for (let i = 0; i < blades; i += 1) { const angle = (360 / blades) * i; const fill = i % 2 === 0 ? palette.primary : palette.accent; parts.push( ` `, ); } return { description: `${blades}-blade aperture rotating around a hollow center, alternating primary and accent fills.`, body: [ ` `, ...parts, ` `, ` `, ].join("\n"), }; }, }, { name: "layer-stack", build: ({ rng, palette }) => { const layers = 3; const height = 34 + Math.floor(rng() * 10); const skew = 18 + Math.floor(rng() * 14); const parts: string[] = []; for (let i = 0; i < layers; i += 1) { const y = MARGIN + 18 + i * (height + 12); const fill = i === 1 ? palette.accent : palette.primary; const opacity = i === 2 ? "0.72" : "1"; parts.push( ` `, ); } return { description: `Three offset parallelogram layers with a ${skew}px shear, reading as stacked planes or data layers.`, body: ` \n${parts.join("\n")}\n `, }; }, }, { name: "arc-mark", build: ({ rng, palette }) => { const cx = CANVAS / 2; const strokes = 2 + Math.floor(rng() * 2); const width = 18 + Math.floor(rng() * 8); const parts: string[] = []; for (let i = 0; i < strokes; i += 1) { const radius = SAFE / 2 - i * (width + 10); if (radius < width) break; const sweep = i % 2 === 0 ? 1 : 0; const fill = i % 2 === 0 ? palette.primary : palette.accent; parts.push( ` `, ); } return { description: `${parts.length} alternating half-arcs of ${width}px weight forming a rising, balanced mark.`, body: ` \n${parts.join("\n")}\n `, }; }, }, ]; /* ------------------------------------------------------------------ */ /* document assembly */ /* ------------------------------------------------------------------ */ export function svgDocument(options: { width: number; height: number; title: string; description: string; background?: string; body: string; }): string { const backdrop = options.background ? ` \n` : ""; return ` ${xmlEscape(options.title)} ${xmlEscape(options.description)} ${backdrop}${options.body} `; } export function buildLockup(options: { brand: string; tagline: string; markBody: string; palette: Palette; background: boolean; }): string { const width = 720; const height = 200; const scale = 0.55; const markSize = CANVAS * scale; const markX = 40; const markY = (height - markSize) / 2; const textX = markX + markSize + 36; const brandSize = 60; const hasTagline = options.tagline.trim().length > 0; const brandY = hasTagline ? height / 2 - 10 : height / 2; const body = [ ` `, options.markBody .split("\n") .map((line) => (line ? ` ${line}` : line)) .join("\n"), ` `, ` ${xmlEscape( options.brand, )}`, ]; if (hasTagline) { body.push( ` ${xmlEscape( options.tagline, )}`, ); } return svgDocument({ width, height, title: `${options.brand} horizontal lockup`, description: `Horizontal lockup pairing the ${options.brand} mark with the wordmark.`, background: options.background ? options.palette.background : undefined, body: body.join("\n"), }); } /* ------------------------------------------------------------------ */ /* cli */ /* ------------------------------------------------------------------ */