added random cybersigilism generation
This commit is contained in:
@@ -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 3–5 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 3–5 */
|
||||
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); // 3–5
|
||||
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>`
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user