added more complex cybersigilism generator

This commit is contained in:
2026-05-16 18:16:49 +02:00
parent 2dc224abc4
commit b9aa93912c
3 changed files with 243 additions and 104 deletions
+1 -1
View File
@@ -185,7 +185,7 @@ export default function PostList({ posts: initialPosts, isAdmin = false }: Props
</span>
)}
{post.draft && (
<span className="plate-tag-mini" style={{ left: 18, right: 'auto', background: 'var(--peach)', color: 'var(--crust)' }}>
<span className="plate-tag-mini plate-tag-mini--draft">
Sketch
</span>
)}
+197 -74
View File
@@ -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 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.
* 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 35 */
/** rough number of primary branch nodes; default random 610 */
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); // 35
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); // 79
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>`
+45 -29
View File
@@ -837,6 +837,15 @@ code, pre, kbd, samp {
text-transform: uppercase;
backdrop-filter: blur(3px);
}
/* Draft/"Sketch" marker — same chip, pinned bottom-left, amber instead of
* the neutral catalogue tag. Themed per skin below (no inline colors). */
.plate-tag-mini--draft {
left: 16px;
right: auto;
background: color-mix(in srgb, var(--peach) 88%, var(--crust));
color: var(--crust);
border-color: color-mix(in srgb, var(--peach) 45%, transparent);
}
/* Breakcore: hard neon catalogue tag — sharp rect, offset shadow, glow.
* Matches the layer's hazard-tape / hard-offset chrome language. */
.breakcore .plate-tag-mini {
@@ -853,6 +862,14 @@ code, pre, kbd, samp {
0 0 14px -2px color-mix(in srgb, var(--mauve) 65%, transparent);
backdrop-filter: none;
}
.breakcore .plate-tag-mini--draft {
color: var(--peach);
border-color: var(--peach);
text-shadow: 0 0 6px color-mix(in srgb, var(--peach) 60%, transparent);
box-shadow:
2px 2px 0 var(--peach),
0 0 14px -2px color-mix(in srgb, var(--peach) 65%, transparent);
}
/* Nameplate — the museum-style header used in the site chrome */
.nameplate {
@@ -2033,8 +2050,7 @@ html.cybersigil body::after {
height: 100%;
overflow: visible;
}
.cybersigil .cs-sigil path,
.cybersigil .cs-sigil line {
.cybersigil .cs-sigil path {
fill: none;
stroke: var(--sky);
stroke-width: 2;
@@ -2044,7 +2060,11 @@ html.cybersigil body::after {
stroke-dasharray: 1;
stroke-dashoffset: 1;
}
.cybersigil .cs-sig-spine { opacity: 0.45; stroke-width: 1.4; }
/* Stroke-weight tiers — heavy growth, hair filaments, prickly barbs, motifs. */
.cybersigil .cs-sigil .cs-sig-main { stroke-width: 2.4; }
.cybersigil .cs-sigil .cs-sig-fil { stroke-width: 0.9; opacity: 0.5; }
.cybersigil .cs-sigil .cs-sig-barb { stroke-width: 1.3; }
.cybersigil .cs-sigil .cs-sig-orn { stroke-width: 1.7; opacity: 0.92; }
@keyframes cs-carve {
to { stroke-dashoffset: 0; }
@@ -2167,21 +2187,21 @@ html.cybersigil body::after {
0 22px 44px -28px rgba(79, 233, 255, 0.26),
0 0 26px -10px color-mix(in srgb, var(--mauve) 34%, transparent);
}
/* Thin engraved corner brackets. The deliberate sigil motif now comes from
* the generated cs-plate-sig, so the old organic leaf masks are dropped. */
.cybersigil .plate::before,
.cybersigil .plate::after {
content: "";
position: absolute;
width: 34px;
height: 34px;
background-color: color-mix(in srgb, var(--sky) 78%, transparent);
-webkit-mask: var(--cs-corner) center / contain no-repeat;
mask: var(--cs-corner) center / contain no-repeat;
filter: drop-shadow(0 0 3px color-mix(in srgb, var(--sky) 45%, transparent));
width: 20px;
height: 20px;
pointer-events: none;
transition: background-color 0.18s ease, filter 0.18s ease;
border: 0 solid color-mix(in srgb, var(--sky) 55%, transparent);
filter: drop-shadow(0 0 3px color-mix(in srgb, var(--sky) 32%, transparent));
transition: border-color 0.18s ease, filter 0.18s ease;
}
.cybersigil .plate::before { top: 5px; left: 5px; }
.cybersigil .plate::after { right: 5px; bottom: 5px; transform: scale(-1); }
.cybersigil .plate::before { top: 6px; left: 6px; border-top-width: 2px; border-left-width: 2px; }
.cybersigil .plate::after { right: 6px; bottom: 6px; border-right-width: 2px; border-bottom-width: 2px; }
.cybersigil .plate:hover {
transform: translateY(-3px);
box-shadow:
@@ -2192,7 +2212,7 @@ html.cybersigil body::after {
}
.cybersigil .plate:hover::before,
.cybersigil .plate:hover::after {
background-color: var(--mauve);
border-color: var(--mauve);
filter: drop-shadow(0 0 6px color-mix(in srgb, var(--mauve) 60%, transparent));
}
.cybersigil .plate:focus-visible {
@@ -2290,22 +2310,8 @@ html.cybersigil body::after {
animation: cs-databend 560ms steps(5, jump-none) 1;
}
/* Prose image framing — thorny growth at opposing corners + cold edge. */
.cybersigil .prose figure { position: relative; }
.cybersigil .prose figure::before,
.cybersigil .prose figure::after {
content: "";
position: absolute;
width: 40px;
height: 40px;
background-color: color-mix(in srgb, var(--sky) 70%, transparent);
-webkit-mask: var(--cs-corner) center / contain no-repeat;
mask: var(--cs-corner) center / contain no-repeat;
pointer-events: none;
z-index: 1;
}
.cybersigil .prose figure::before { top: -8px; left: -8px; }
.cybersigil .prose figure::after { right: -8px; bottom: -8px; transform: scale(-1); }
/* Prose image framing — cold gradient edge only. Old corner leaf masks
* removed; figural sigils are no longer pinned to post images. */
.cybersigil .prose figure img,
.cybersigil .prose img {
background:
@@ -2694,6 +2700,16 @@ html.cybersigil body::after {
font-weight: 700;
text-transform: uppercase;
}
/* Sketch marker reads as an unstable/draft signal — amber glyph, split
* chromatic shadow, hard offset, matching the corrupted-terminal language. */
.cybersigil .plate-tag-mini--draft {
color: var(--peach);
border-color: var(--peach);
box-shadow: 2px 2px 0 color-mix(in srgb, var(--peach) 70%, transparent);
text-shadow:
-1px 0 0 color-mix(in srgb, var(--peach) 65%, transparent),
1px 0 0 color-mix(in srgb, var(--sky) 55%, transparent);
}
.cybersigil .kbd-tip {
background: var(--crust);
border-color: var(--mauve);