added random cybersigilism generation

This commit is contained in:
2026-05-16 17:48:52 +02:00
parent c576794951
commit 2dc224abc4
4 changed files with 222 additions and 0 deletions
+112
View File
@@ -0,0 +1,112 @@
/*
* cybersigil — modular neo-tribal sigil builder.
*
* Cybersigilism's whole trick is hard vertical symmetry: author only the
* right half, mirror it about a central spine. We keep a small library of
* hand-drawn glyph blocks (blade / hook / thorn / loop / dagger / antenna /
* fang / comet), each authored in local coords with the spine at x=0 growing
* into +x. `buildCybersigil` randomly stacks 35 of them down the Y axis,
* wraps the column in two <g>s (identity + scale(-1 1)) for the mirror, and
* emits an inert SVG string.
*
* Carving: every <path> carries pathLength="1" so the CSS draw-on animation
* (stroke-dasharray/dashoffset in global.css, scoped to `.cybersigil`) needs
* no JS length measurement. Each path gets an inline `--i` so strokes carve
* top-to-bottom in sequence.
*
* Output is decorative and self-generated (no user input) — safe to inject
* via innerHTML / dangerouslySetInnerHTML. Inert under every non-cybersigil
* theme because all styling is `.cybersigil`-scoped.
*/
export interface Glyph {
/** max extent from the spine on +x */
w: number;
/** vertical run of the block */
h: number;
/** path data, spine at x=0, y in [0,h] */
d: string;
}
// Right-half building blocks. Each starts/ends near the spine (x≈0) so the
// standalone spine line knits the column into one continuous growth.
export const GLYPHS: readonly Glyph[] = [
// blade — long sweep that hooks back, two cast-off barbs
{ w: 30, h: 36, d: 'M0 2 Q30 8 25 22 Q21 33 0 35 M5 11 L15 5 M19 17 L28 13' },
// hook — tight inward claw with a tip spur
{ w: 26, h: 29, d: 'M0 5 Q24 3 24 16 Q24 27 7 28 Q0 28 3 21 M0 5 L6 0' },
// thorn cluster — spine run throwing three barbs
{ w: 17, h: 30, d: 'M0 0 L0 30 M0 6 L15 2 M0 15 L17 11 M0 24 L12 22' },
// loop — teardrop eye off the spine
{ w: 29, h: 30, d: 'M0 4 Q28 1 28 17 Q28 30 12 29 Q4 29 6 21 Q9 16 16 18 M0 4 L0 30' },
// dagger — crossbar over a descending blade
{ w: 19, h: 34, d: 'M0 0 L0 34 M0 9 L19 9 M0 9 L14 3 M0 22 L11 27' },
// antenna — rising spike with a split tip
{ w: 25, h: 30, d: 'M0 24 Q7 12 19 4 M19 4 L15 0 M19 4 L25 2 M0 24 L0 30' },
// fang — bold curved fang with an inner notch
{ w: 23, h: 38, d: 'M0 0 Q21 9 22 24 Q22 35 11 37 M0 0 L0 38 M8 14 Q15 18 15 27' },
// comet — node trailing two barbs
{ w: 30, h: 32, d: 'M0 8 Q17 3 23 12 Q27 19 20 24 Q13 30 6 25 M23 12 L30 9 M20 24 L27 30 M0 0 L0 32' },
] as const;
export interface SigilOptions {
/** how many blocks to stack; default random 35 */
count?: number;
/** injectable RNG (0..1); default Math.random */
rng?: () => number;
}
const PAD = 6;
/** blocks knit slightly so the spine reads unbroken */
const OVERLAP = 4;
function pickIndices(n: number, total: number, rng: () => number): number[] {
const pool = Array.from({ length: total }, (_, i) => i);
for (let i = pool.length - 1; i > 0; i--) {
const j = Math.floor(rng() * (i + 1));
[pool[i], pool[j]] = [pool[j], pool[i]];
}
return pool.slice(0, n);
}
/**
* Build a single mirrored sigil and return it as an SVG markup string.
* Random per call unless a deterministic `rng` is supplied.
*/
export function buildCybersigil(opts: SigilOptions = {}): string {
const rng = opts.rng ?? Math.random;
const count = opts.count ?? 3 + Math.floor(rng() * 3); // 35
const chosen = pickIndices(count, GLYPHS.length, rng).map((i) => GLYPHS[i]);
let y = 0;
let maxW = 0;
let bottom = 0;
const groups: string[] = [];
chosen.forEach((g, idx) => {
groups.push(
`<g transform="translate(0 ${y})"><path d="${g.d}" pathLength="1" style="--i:${idx + 1}"/></g>`,
);
bottom = Math.max(bottom, y + g.h);
maxW = Math.max(maxW, g.w);
y += g.h - OVERLAP;
});
const half = groups.join('');
const spine = `<line class="cs-sig-spine" x1="0" y1="0" x2="0" y2="${bottom}" pathLength="1" style="--i:0"/>`;
const minX = -(maxW + PAD);
const vbW = 2 * (maxW + PAD);
const minY = -PAD;
const vbH = bottom + 2 * PAD;
return (
`<svg class="cs-sigil" viewBox="${minX} ${minY} ${vbW} ${vbH}" ` +
`preserveAspectRatio="xMidYMid meet" aria-hidden="true" focusable="false" ` +
`xmlns="http://www.w3.org/2000/svg">` +
spine +
`<g class="cs-sig-half">${half}</g>` +
`<g class="cs-sig-half" transform="scale(-1 1)">${half}</g>` +
`</svg>`
);
}