PageSourceSearch

https://forboc.ai/src/components/Sigil.js

js forboc.ai collected 2026-10-05 13:39:19 UTC 6,180 bytes, 130 lines download raw bytes

1/**
2 * Sigil
3 *
4 * Urbit's real idea is not its typography, it is that every identity carries a
5 * generated seal — a glyph you could not have drawn, derived from the name
6 * itself. Borrowed here because it is not decoration on this site, it is the
7 * thesis: a character has an identity, that identity is hers, and it travels
8 * with her when a player carries her into somebody else's world.
9 *
10 * So the sigil is derived from the persona document, deterministically. Change
11 * a name and you get a different seal; carry the same character into a second
12 * world and the seal is the one that arrived. It is the same argument the
13 * crossing scene makes, made in a form you can see before you read.
14 *
15 * Biomorphic rather than heraldic: the cells are grown from a wandering radius
16 * rather than stamped from a grid, so no two are the same shape and none of
17 * them is a circle. This is also the answer to the warm-cream-and-serif smell —
18 * a procedural mark is the one thing on a page that cannot have been picked
19 * from a template.
20 */
21
22/**
23 * Two sources of randomness, on purpose.
24 *
25 * Identity seeds the STRUCTURE — how many membranes, how many lobes on each,
26 * how far off-centre the nucleus sits, how many filaments reach out. That part
27 * is derived from the persona document and never changes, so a character is
28 * recognisably the same organism wherever she appears, including after she has
29 * crossed into a world that never made her.
30 *
31 * The GROWTH is fresh every render. A living thing is never the same shape
32 * twice — a cell photographed an hour apart is the same cell and a different
33 * picture — so the wander that fleshes out each membrane is drawn from an
34 * unseeded source. No two sigils on this site are ever identical, including
35 * two of the same character, and that is the biologically honest version of
36 * what an identity mark is.
37 */
38
39/** A small deterministic hash, so a name always seeds the same structure. */
40const seedOf = (text) => {
41    let h = 2166136261;
42    for (let i = 0; i < text.length; i += 1) {
43        h ^= text.charCodeAt(i);
44        h = Math.imul(h, 16777619);
45    }
46    return h >>> 0;
47};
48
49/** Mulberry32 — tiny, seedable, and good enough for a glyph. */
50const rngOf = (seed) => () => {
51    seed |= 0;
52    seed = (seed + 0x6d2b79f5) | 0;
53    let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
54    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
55    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
56};
57
58/**
59 * One membrane: a closed loop whose radius wanders, so it reads as something
60 * that grew into its shape rather than something struck from a die.
61 */
62const cell = (rand, cx, cy, radius, lobes) => {
63    const points = [];
64    for (let i = 0; i < lobes; i += 1) {
65        const angle = (i / lobes) * Math.PI * 2;
66        const r = radius * (0.68 + rand() * 0.46);
67        points.push([cx + Math.cos(angle) * r, cy + Math.sin(angle) * r]);
68    }
69    // Catmull-Rom through the points, closed, so the wander stays smooth.
70    const d = points.map((p, i) => {
71        const p0 = points[(i - 1 + points.length) % points.length];
72        const p1 = p;
73        const p2 = points[(i + 1) % points.length];
74        const p3 = points[(i + 2) % points.length];
75        const c1 = [p1[0] + (p2[0] - p0[0]) / 6, p1[1] + (p2[1] - p0[1]) / 6];
76        const c2 = [p2[0] - (p3[0] - p1[0]) / 6, p2[1] - (p3[1] - p1[1]) / 6];
77        const head = i === 0 ? `M${p1[0].toFixed(1)} ${p1[1].toFixed(1)}` : '';
78        return `${head}C${c1[0].toFixed(1)} ${c1[1].toFixed(1)} ${c2[0].toFixed(1)} ${c2[1].toFixed(1)} ${p2[0].toFixed(1)} ${p2[1].toFixed(1)}`;
79    }).join('');
80    return `${d}Z`;
81};
82
83/**
84 * A chapter's mark, in the same system.
85 *
86 * The rune was sitting in a perfect circle with a hairline border — the
87 * constructed version of the exact form the sigil grows. One mark system now,
88 * two contents: a chapter's membrane holds its rune, a character's holds her
89 * own nucleus. Each chapter's membrane is seeded by its own rune, so no two
90 * chapters carry the same shape, the way no two characters do.
91 */
92export const ChapterMark = ({ rune }) => {
93    const form = rngOf(seedOf(`chapter:${rune}`));
94    const grow = Math.random;
95    const outer = cell(grow, 50, 50, 44, 7 + Math.floor(form() * 3));
96    const inner = cell(grow, 50, 50, 33, 6 + Math.floor(form() * 3));
97    return `<span class="chapter-mark" aria-hidden="true">
98        <svg class="sigil chapter-mark-cell" viewBox="0 0 100 100" aria-hidden="true" focusable="false">
98<path d="${outer}" class="sigil-ring" style="--i:0"/><path d="${inner}" class="sigil-ring" style="--i:1"/></svg>
99        <span class="chapter-mark-rune">${rune}</span>
100    </span>`;
101};
102
103export const Sigil = ({ name, size = 56, tone = 'honey' }) => {
104    const form = rngOf(seedOf(name));   // who she is
105    const grow = Math.random;           // this particular growth of her
106    const rings = 2 + Math.floor(form() * 2);
107    const paths = [];
108
109    for (let i = 0; i < rings; i += 1) {
110        const radius = 42 - i * (9 + form() * 5);
111        paths.push(`<path d="${cell(grow, 50, 50, radius, 6 + Math.floor(form() * 4))}" class="sigil-ring" style="--i:${i}"/>`);
112    }
113
114    // The nucleus: off-centre, because a grown thing is not concentric. Where
115    // it sits is hers; the shape it takes is this morning's.
116    const nx = 50 + (form() - 0.5) * 14;
117    const ny = 50 + (form() - 0.5) * 14;
118    paths.push(`<path d="${cell(grow, nx, ny, 9 + form() * 5, 5 + Math.floor(form() * 3))}" class="sigil-core"/>`);
119
120    // Filaments reaching out of the membrane — what the character is bound to.
121    const strands = 2 + Math.floor(form() * 3);
122    for (let i = 0; i < strands; i += 1) {
123        const a = form() * Math.PI * 2;
124        const r1 = 18 + grow() * 10;
125        const r2 = 44 + grow() * 6;
126        paths.push(`<path d="M${(50 + Math.cos(a) * r1).toFixed(1)} ${(50 + Math.sin(a) * r1).toFixed(1)}L${(50 + Math.cos(a) * r2).toFixed(1)} ${(50 + Math.sin(a) * r2).toFixed(1)}" class="sigil-strand"/>`);
127    }
128
129    return `<svg class="sigil sigil-${tone}" viewBox="0 0 100 100" width="${size}" height="${size}" aria-hidden="true" focusable="false">${paths.join('')}</svg>`;
130};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.