mirror of
https://github.com/Patiun/com.gregkilmer.www.git
synced 2026-11-08 04:08:24 +00:00
Phase 4: component library
The approved element rules and ornament kit as Astro components in packages/ui: buttons, pills, chips, filters, kickers, notices, fields and cards; star paths, the header divider, tick scales and rings, seals and edge seals, gilt corners, dust and placeholder art; the site header, footer and page shell. Brand fonts are self-hosted. A gallery page shows everything, and astro check now runs with the root typecheck. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012aSbo2D1qP8cMFQCykf7kx
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* Geometry for the ornament kit, as pure functions so every page draws the
|
||||
* same shapes and tests can pin them down. Randomness is seeded, so a star
|
||||
* path looks the same on every build.
|
||||
*/
|
||||
|
||||
export const colors = {
|
||||
sky: "#0E1522",
|
||||
midnight: "#131C2B",
|
||||
lines: "#26344A",
|
||||
linesStrong: "#3A4C66",
|
||||
starlight: "#F4F2ED",
|
||||
text: "#ECE9E2",
|
||||
soft: "#C8D3D8",
|
||||
muted: "#9FB3BD",
|
||||
teal: "#14A39A",
|
||||
tealBright: "#2BC2B7",
|
||||
gold: "#C9A24A",
|
||||
goldBright: "#D8B35A",
|
||||
violetOnDark: "#9A70F5",
|
||||
/** Light mode only. */
|
||||
violet: "#6A2FE0",
|
||||
goldDeep: "#8A6516",
|
||||
tealDeep: "#0A726B",
|
||||
} as const;
|
||||
|
||||
/** A small, fast seeded generator (mulberry32). */
|
||||
export function seeded(seed: number): () => number {
|
||||
let a = seed >>> 0;
|
||||
return () => {
|
||||
a = (a + 0x6d2b79f5) >>> 0;
|
||||
let t = a;
|
||||
t = Math.imul(t ^ (t >>> 15), t | 1);
|
||||
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
||||
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
||||
};
|
||||
}
|
||||
|
||||
/** A string to a seed, so components can be seeded by a name. */
|
||||
export function seedOf(text: string): number {
|
||||
let h = 2166136261;
|
||||
for (let i = 0; i < text.length; i++) h = Math.imul(h ^ text.charCodeAt(i), 16777619);
|
||||
return h >>> 0;
|
||||
}
|
||||
|
||||
const r1 = (n: number) => Math.round(n * 10) / 10;
|
||||
|
||||
/**
|
||||
* The thin four-point star used everywhere: points of a polygon centered on
|
||||
* (cx, cy) with outer radius r. The waist stays at 16% so it never reads as a
|
||||
* cross.
|
||||
*/
|
||||
export function starPoints(cx: number, cy: number, r: number): string {
|
||||
const w = r * 0.16;
|
||||
const pts: [number, number][] = [
|
||||
[cx, cy - r],
|
||||
[cx + w, cy - w],
|
||||
[cx + r, cy],
|
||||
[cx + w, cy + w],
|
||||
[cx, cy + r],
|
||||
[cx - w, cy + w],
|
||||
[cx - r, cy],
|
||||
[cx - w, cy - w],
|
||||
];
|
||||
return pts.map(([x, y]) => `${r1(x)},${r1(y)}`).join(" ");
|
||||
}
|
||||
|
||||
export type StarPathMark = { kind: "dot"; x: number; y: number; r: number } | { kind: "star"; x: number; y: number; r: number; gold: boolean };
|
||||
|
||||
export interface StarPath {
|
||||
/** Dotted segments between marks, stopping short of each. */
|
||||
links: { x1: number; y1: number; x2: number; y2: number }[];
|
||||
marks: StarPathMark[];
|
||||
}
|
||||
|
||||
export const strengths = { calm: 0.1, standard: 0.22, wild: 0.42 } as const;
|
||||
|
||||
/**
|
||||
* Greg's star path: a straight line replaced by evenly spread stars, each
|
||||
* nudged off the line, joined by dotted segments. About a third of the points
|
||||
* are small violet stars, the rest Starlight dots. Ends stay on the line.
|
||||
*/
|
||||
export function starPath(o: {
|
||||
x1: number;
|
||||
x2: number;
|
||||
y: number;
|
||||
steps: number;
|
||||
seed: number;
|
||||
strength?: keyof typeof strengths;
|
||||
/** Put a large gold star at the middle point. */
|
||||
centerStar?: boolean;
|
||||
/** Leave the end points off, when a gold line or star already marks them. */
|
||||
openEnds?: boolean;
|
||||
gap?: number;
|
||||
dot?: number;
|
||||
star?: number;
|
||||
}): StarPath {
|
||||
const rand = seeded(o.seed);
|
||||
const step = (o.x2 - o.x1) / o.steps;
|
||||
const nudge = strengths[o.strength ?? "standard"] * Math.abs(step);
|
||||
const gap = o.gap ?? 4;
|
||||
const points = Array.from({ length: o.steps + 1 }, (_, i) => {
|
||||
const ends = i === 0 || i === o.steps;
|
||||
return { x: o.x1 + step * i, y: ends ? o.y : o.y + (rand() * 2 - 1) * nudge };
|
||||
});
|
||||
const mid = Math.round(o.steps / 2);
|
||||
const marks: StarPathMark[] = points.map((p, i) => {
|
||||
if (o.centerStar && i === mid) return { kind: "star", x: r1(p.x), y: r1(p.y), r: (o.star ?? 4) * 3, gold: true };
|
||||
// Every third point, offset by the seed, is a small star.
|
||||
const isStar = (i + (o.seed % 3)) % 3 === 1;
|
||||
return isStar ? { kind: "star", x: r1(p.x), y: r1(p.y), r: o.star ?? 4, gold: false } : { kind: "dot", x: r1(p.x), y: r1(p.y), r: o.dot ?? 1.5 };
|
||||
});
|
||||
const links = points.slice(1).map((b, i) => {
|
||||
const a = points[i]!;
|
||||
const dx = b.x - a.x;
|
||||
const dy = b.y - a.y;
|
||||
const len = Math.hypot(dx, dy) || 1;
|
||||
const ux = dx / len;
|
||||
const uy = dy / len;
|
||||
const ga = gap + (marks[i]!.kind === "star" ? marks[i]!.r * 0.5 : 0);
|
||||
const gb = gap + (marks[i + 1]!.kind === "star" ? marks[i + 1]!.r * 0.5 : 0);
|
||||
return { x1: r1(a.x + ux * ga), y1: r1(a.y + uy * ga), x2: r1(b.x - ux * gb), y2: r1(b.y - uy * gb) };
|
||||
});
|
||||
const drop = (i: number) => o.openEnds && (i === 0 || i === points.length - 1);
|
||||
return { links, marks: marks.filter((_, i) => !drop(i)) };
|
||||
}
|
||||
|
||||
/** Tick marks for a scale: a long tick every `major`, short ones between. */
|
||||
export function ticks(width: number, count: number, major = 5): { x: number; long: boolean }[] {
|
||||
const step = width / count;
|
||||
return Array.from({ length: count + 1 }, (_, i) => ({ x: r1(0.5 + step * i), long: i % major === 0 }));
|
||||
}
|
||||
|
||||
/** Ticks around a circle, for tick rings on step numbers and medallions. */
|
||||
export function ringTicks(size: number, count = 48, major = 4): { x1: number; y1: number; x2: number; y2: number; long: boolean }[] {
|
||||
const c = size / 2;
|
||||
const outer = c - 1;
|
||||
return Array.from({ length: count }, (_, i) => {
|
||||
const a = (i / count) * Math.PI * 2;
|
||||
const long = i % major === 0;
|
||||
const inner = outer - (long ? size * 0.066 : size * 0.033);
|
||||
const f = (n: number) => Math.round(n * 100) / 100;
|
||||
return { x1: f(c + Math.cos(a) * outer), y1: f(c + Math.sin(a) * outer), x2: f(c + Math.cos(a) * inner), y2: f(c + Math.sin(a) * inner), long };
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Constellation dust: sparse specks for a page background as CSS
|
||||
* radial-gradients, about one in five gold. Static by design.
|
||||
*/
|
||||
export function dust(count: number, seed: number): string {
|
||||
const rand = seeded(seed);
|
||||
const specks: string[] = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
const size = [1, 1, 1.2, 1.6][Math.floor(rand() * 4)];
|
||||
const alpha = [0.25, 0.35, 0.6][Math.floor(rand() * 3)];
|
||||
const gold = rand() < 0.2;
|
||||
const color = gold ? `rgba(216,179,90,${alpha})` : `rgba(236,233,226,${alpha})`;
|
||||
specks.push(`radial-gradient(${size}px ${size}px at ${r1(rand() * 100)}% ${r1(rand() * 100)}%, ${color}, transparent)`);
|
||||
}
|
||||
return specks.join(", ");
|
||||
}
|
||||
Reference in New Issue
Block a user