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 `
`;
}
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 */
/* ------------------------------------------------------------------ */