added more complex cybersigilism generator
This commit is contained in:
+197
-74
@@ -1,110 +1,233 @@
|
||||
/*
|
||||
* cybersigil — modular neo-tribal sigil builder.
|
||||
* cybersigil — chaotic neo-tribal sigil generator.
|
||||
*
|
||||
* 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.
|
||||
* Cybersigilism's signature is dense, fractal, barbed linework with hard
|
||||
* vertical symmetry. We grow it procedurally: a wavering central spine spawns
|
||||
* recursive curved limbs, each scattering barbs and thin filament shadows and
|
||||
* occasionally terminating in a small hand-drawn motif. The whole right-leaning
|
||||
* tangle is mirrored about x=0 (scale(-1 1)); a spine that wobbles in +x while
|
||||
* its mirror wobbles in −x weaves the two halves into one symmetric growth.
|
||||
*
|
||||
* 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.
|
||||
* Strokes carry pathLength="1" so the CSS draw-on ("carve") animation needs no
|
||||
* JS length measurement; an inline `--i` staggers the carve into waves.
|
||||
* Output is decorative and self-generated (no user input) — safe to inject via
|
||||
* innerHTML. Inert under non-cybersigil themes (all styling `.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] */
|
||||
/** path data, local coords, anchored near origin */
|
||||
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.
|
||||
// Small hand-drawn motifs used as limb-tip flourishes — the deliberate,
|
||||
// "carved on purpose" punctuation amid the procedural chaos.
|
||||
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' },
|
||||
{ w: 18, h: 22, d: 'M0 0 Q16 5 14 14 Q12 21 0 22 M4 7 L10 3' },
|
||||
{ w: 16, h: 16, d: 'M0 8 Q14 0 15 8 Q14 16 0 8 M8 2 L8 14' },
|
||||
{ w: 14, h: 24, d: 'M0 0 L0 24 M0 6 L12 2 M0 14 L13 10 M0 21 L9 19' },
|
||||
{ w: 20, h: 18, d: 'M0 9 Q10 -2 19 4 M19 4 L15 0 M19 4 L20 9 M0 9 L4 16' },
|
||||
{ w: 15, h: 20, d: 'M0 0 Q14 6 13 13 Q12 19 0 19 M7 8 Q11 11 11 16' },
|
||||
] as const;
|
||||
|
||||
export interface SigilOptions {
|
||||
/** how many blocks to stack; default random 3–5 */
|
||||
/** rough number of primary branch nodes; default random 6–10 */
|
||||
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;
|
||||
const H = 200; // internal canvas height (viewBox scales it to fit)
|
||||
const PAD = 14;
|
||||
const MAX_PATHS = 110; // safety ceiling; real density is bounded by the params
|
||||
|
||||
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);
|
||||
}
|
||||
type Pt = [number, number];
|
||||
|
||||
/**
|
||||
* 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]);
|
||||
const rnd = (a: number, b: number) => a + rng() * (b - a);
|
||||
const pick = <T>(a: readonly T[]): T => a[Math.floor(rng() * a.length)];
|
||||
const n = (v: number) => {
|
||||
const r = Math.round(v * 10) / 10;
|
||||
return Object.is(r, -0) ? '0' : String(r);
|
||||
};
|
||||
|
||||
let y = 0;
|
||||
let maxW = 0;
|
||||
let bottom = 0;
|
||||
const groups: string[] = [];
|
||||
const parts: string[] = [];
|
||||
let strokeCount = 0;
|
||||
let maxX = 24;
|
||||
|
||||
chosen.forEach((g, idx) => {
|
||||
groups.push(
|
||||
`<g transform="translate(0 ${y})"><path d="${g.d}" pathLength="1" style="--i:${idx + 1}"/></g>`,
|
||||
const track = (x: number) => {
|
||||
const ax = Math.abs(x);
|
||||
if (ax > maxX) maxX = ax;
|
||||
};
|
||||
const emit = (d: string, cls: string) => {
|
||||
if (strokeCount >= MAX_PATHS) return;
|
||||
parts.push(
|
||||
`<path class="${cls}" d="${d}" pathLength="1" style="--i:${strokeCount % 16}"/>`,
|
||||
);
|
||||
bottom = Math.max(bottom, y + g.h);
|
||||
maxW = Math.max(maxW, g.w);
|
||||
y += g.h - OVERLAP;
|
||||
});
|
||||
strokeCount++;
|
||||
};
|
||||
|
||||
const half = groups.join('');
|
||||
const spine = `<line class="cs-sig-spine" x1="0" y1="0" x2="0" y2="${bottom}" pathLength="1" style="--i:0"/>`;
|
||||
// Catmull-Rom → cubic Bézier through an ordered point list (organic sweep).
|
||||
const spline = (pts: Pt[]): string => {
|
||||
if (pts.length < 2) return '';
|
||||
let d = `M${n(pts[0][0])} ${n(pts[0][1])}`;
|
||||
for (let i = 0; i < pts.length - 1; i++) {
|
||||
const p0 = pts[i - 1] ?? pts[i];
|
||||
const p1 = pts[i];
|
||||
const p2 = pts[i + 1];
|
||||
const p3 = pts[i + 2] ?? p2;
|
||||
const c1: Pt = [p1[0] + (p2[0] - p0[0]) / 6, p1[1] + (p2[1] - p0[1]) / 6];
|
||||
const c2: Pt = [p2[0] - (p3[0] - p1[0]) / 6, p2[1] - (p3[1] - p1[1]) / 6];
|
||||
d += `C${n(c1[0])} ${n(c1[1])} ${n(c2[0])} ${n(c2[1])} ${n(p2[0])} ${n(p2[1])}`;
|
||||
track(c1[0]);
|
||||
track(c2[0]);
|
||||
track(p2[0]);
|
||||
}
|
||||
return d;
|
||||
};
|
||||
|
||||
const minX = -(maxW + PAD);
|
||||
const vbW = 2 * (maxW + PAD);
|
||||
const minY = -PAD;
|
||||
const vbH = bottom + 2 * PAD;
|
||||
// A short spike, optionally kinked into a fang.
|
||||
const barb = (base: Pt, ang: number, len: number) => {
|
||||
const tip: Pt = [base[0] + Math.cos(ang) * len, base[1] + Math.sin(ang) * len];
|
||||
if (tip[0] < -3) tip[0] = -3;
|
||||
track(base[0]);
|
||||
track(tip[0]);
|
||||
if (rng() < 0.45) {
|
||||
const mid: Pt = [
|
||||
(base[0] + tip[0]) / 2 + Math.cos(ang + Math.PI / 2) * len * 0.3,
|
||||
(base[1] + tip[1]) / 2 + Math.sin(ang + Math.PI / 2) * len * 0.3,
|
||||
];
|
||||
emit(
|
||||
`M${n(base[0])} ${n(base[1])}Q${n(mid[0])} ${n(mid[1])} ${n(tip[0])} ${n(tip[1])}`,
|
||||
'cs-sig-barb',
|
||||
);
|
||||
} else {
|
||||
emit(`M${n(base[0])} ${n(base[1])}L${n(tip[0])} ${n(tip[1])}`, 'cs-sig-barb');
|
||||
}
|
||||
};
|
||||
|
||||
const ornament = (at: Pt, ang: number) => {
|
||||
const g = pick(GLYPHS);
|
||||
const s = rnd(0.45, 0.85);
|
||||
const deg = (ang * 180) / Math.PI + rnd(-25, 25);
|
||||
track(at[0] + g.w * s);
|
||||
parts.push(
|
||||
`<g transform="translate(${n(at[0])} ${n(at[1])}) rotate(${n(deg)}) scale(${n(s)})">` +
|
||||
`<path class="cs-sig-orn" d="${g.d}" pathLength="1" style="--i:${strokeCount % 16}"/></g>`,
|
||||
);
|
||||
strokeCount++;
|
||||
};
|
||||
|
||||
// Recursive limb: a curved sweep out from (ox,oy) along `ang`, hooking back,
|
||||
// scattering barbs, shedding a filament shadow, branching, sometimes tipped
|
||||
// with a motif.
|
||||
const limb = (ox: number, oy: number, ang: number, scale: number, depth: number) => {
|
||||
if (strokeCount >= MAX_PATHS) return;
|
||||
const L = scale * rnd(34, 64);
|
||||
const dx = Math.cos(ang) * L;
|
||||
const dy = Math.sin(ang) * L;
|
||||
const peak: Pt = [ox + dx, oy + dy];
|
||||
const mid: Pt = [
|
||||
ox + dx * 0.45 + Math.cos(ang + Math.PI / 2) * rnd(-10, 14),
|
||||
oy + dy * 0.45 + Math.sin(ang + Math.PI / 2) * rnd(-10, 14),
|
||||
];
|
||||
// hook back toward the spine
|
||||
const hook: Pt = [
|
||||
peak[0] - Math.cos(ang) * L * rnd(0.3, 0.55),
|
||||
peak[1] + Math.sin(ang + 0.7) * L * rnd(0.25, 0.5),
|
||||
];
|
||||
const tail: Pt = [Math.max(-2, hook[0] - L * rnd(0.2, 0.4)), hook[1] + rnd(-6, 10)];
|
||||
const pts: Pt[] = [[ox, oy], mid, peak, hook, tail];
|
||||
emit(spline(pts), 'cs-sig-main');
|
||||
|
||||
// terminal spike off the outermost point
|
||||
barb(peak, ang + rnd(-0.5, 0.5), scale * rnd(8, 18));
|
||||
|
||||
// filament shadow trailing the main sweep
|
||||
if (rng() < 0.4) {
|
||||
const off = rnd(2, 6);
|
||||
emit(
|
||||
spline(
|
||||
pts.map(
|
||||
([x, y]) =>
|
||||
[x + Math.cos(ang + Math.PI / 2) * off, y + Math.sin(ang + Math.PI / 2) * off] as Pt,
|
||||
),
|
||||
),
|
||||
'cs-sig-fil',
|
||||
);
|
||||
}
|
||||
|
||||
// barb scatter along the chord
|
||||
const nb = 1 + Math.floor(rng() * 2);
|
||||
for (let k = 0; k < nb; k++) {
|
||||
const t = (k + 1) / (nb + 1);
|
||||
const seg = t < 0.5 ? [pts[0], pts[2], t * 2] : [pts[2], pts[4], (t - 0.5) * 2];
|
||||
const a = seg[0] as Pt;
|
||||
const b = seg[1] as Pt;
|
||||
const tt = seg[2] as number;
|
||||
const base: Pt = [a[0] + (b[0] - a[0]) * tt, a[1] + (b[1] - a[1]) * tt];
|
||||
const side = k % 2 ? 1 : -1;
|
||||
barb(base, ang + side * rnd(0.6, 1.3), scale * rnd(6, 16));
|
||||
}
|
||||
|
||||
// recurse — one child curls off the mid/peak region
|
||||
if (depth > 0 && rng() < 0.55) {
|
||||
const from = rng() < 0.5 ? mid : peak;
|
||||
limb(
|
||||
from[0],
|
||||
from[1],
|
||||
ang + (rng() < 0.5 ? 1 : -1) * rnd(0.5, 1.2),
|
||||
scale * rnd(0.42, 0.6),
|
||||
depth - 1,
|
||||
);
|
||||
}
|
||||
|
||||
// motif flourish at a terminal tip
|
||||
if (depth === 0 && rng() < 0.3) ornament(peak, ang);
|
||||
};
|
||||
|
||||
// ── Wavering spine: a curve from top to bottom, gently bowing in +x.
|
||||
const spineNodes = 5 + Math.floor(rng() * 3);
|
||||
const spinePts: Pt[] = [];
|
||||
for (let i = 0; i <= spineNodes; i++) {
|
||||
const y = (H * i) / spineNodes;
|
||||
const x = i === 0 || i === spineNodes ? 0 : rnd(0, 11);
|
||||
spinePts.push([x, y]);
|
||||
}
|
||||
emit(spline(spinePts), 'cs-sig-main');
|
||||
|
||||
// ── Branch nodes ride the spine and throw limbs outward. Nodes are
|
||||
// inset from the very ends and spread the full height so growth flows
|
||||
// down the whole trunk rather than clumping at the top.
|
||||
const nodes = opts.count ?? 7 + Math.floor(rng() * 3); // 7–9
|
||||
for (let i = 0; i < nodes; i++) {
|
||||
const t = 0.08 + (0.86 * (i + rnd(-0.25, 0.25))) / (nodes - 1);
|
||||
const tc = Math.max(0.05, Math.min(0.95, t));
|
||||
const si = Math.min(spinePts.length - 2, Math.floor(tc * (spinePts.length - 1)));
|
||||
const sf = tc * (spinePts.length - 1) - si;
|
||||
const a = spinePts[si];
|
||||
const b = spinePts[si + 1];
|
||||
const node: Pt = [a[0] + (b[0] - a[0]) * sf, a[1] + (b[1] - a[1]) * sf];
|
||||
// later nodes lean downward so the lower trunk fills out
|
||||
const bias = -0.25 + tc * 0.7;
|
||||
const limbs = 1 + Math.floor(rng() * 2);
|
||||
for (let l = 0; l < limbs; l++) {
|
||||
const ang = bias + rnd(-0.55, 0.55);
|
||||
limb(node[0], node[1], ang, rnd(0.65, 1.05), 1);
|
||||
}
|
||||
// the odd bare barb straight off the spine keeps the trunk prickly
|
||||
if (rng() < 0.55) barb(node, bias + rnd(-0.3, 0.3), rnd(7, 14));
|
||||
}
|
||||
|
||||
const half = parts.join('');
|
||||
const minX = -(maxX + PAD);
|
||||
const vbW = 2 * (maxX + PAD);
|
||||
return (
|
||||
`<svg class="cs-sigil" viewBox="${minX} ${minY} ${vbW} ${vbH}" ` +
|
||||
`<svg class="cs-sigil" viewBox="${n(minX)} ${-PAD} ${n(vbW)} ${H + 2 * PAD}" ` +
|
||||
`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