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
+15
View File
@@ -21,12 +21,27 @@
</div>
<script>
import { buildCybersigil } from '../lib/cybersigil';
function initCyberFx() {
const root = document.documentElement;
if (!root.classList.contains('cybersigil')) return;
const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
/* ─── Generated sigil growths in the four corners ─── */
/* One sigil per page load; the four corner transforms in global.css
* (scaleX / scaleY / scale) splay it into each corner. */
const corners = document.querySelectorAll<HTMLElement>('.cs-fx-corner');
if (corners.length) {
const svg = buildCybersigil();
corners.forEach((c) => {
if (c.classList.contains('cs-fx-corner--sig')) return;
c.innerHTML = svg;
c.classList.add('cs-fx-corner--sig');
});
}
/* ─── Scroll-entry databend on images ─── */
if (!reduced && 'IntersectionObserver' in window) {
const targets = document.querySelectorAll<HTMLElement>(
@@ -1,6 +1,23 @@
import { useEffect, useRef, useState } from 'react';
import { deletePost } from '../../lib/api';
import { confirmDialog, notify } from '../../lib/confirm';
import { buildCybersigil } from '../../lib/cybersigil';
// Per-plate sigil accent. Built post-mount (not during render) so the random
// markup never differs between SSR and hydration. Inert/display:none off the
// cybersigil theme; carves in on plate hover/focus via global.css.
function PlateSigil() {
const [html, setHtml] = useState('');
useEffect(() => { setHtml(buildCybersigil()); }, []);
if (!html) return null;
return (
<div
className="cs-plate-sig"
aria-hidden="true"
dangerouslySetInnerHTML={{ __html: html }}
/>
);
}
const PAGE_SIZE = 9;
@@ -220,6 +237,8 @@ export default function PostList({ posts: initialPosts, isAdmin = false }: Props
</button>
</div>
)}
<PlateSigil />
</article>
);
})}
+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>`
);
}
+76
View File
@@ -2022,6 +2022,47 @@ html.cybersigil body::after {
.cybersigil .cs-fx-corner--bl { bottom: 0; left: 0; transform: scaleY(-1); animation-delay: -3.4s; }
.cybersigil .cs-fx-corner--br { bottom: 0; right: 0; transform: scale(-1); animation-delay: -5.1s; }
/* ─── Generated sigils (cs-sigil markup from lib/cybersigil.ts) ──────────
* Inert/hidden everywhere; only cybersigil draws them. Strokes start fully
* dashed-out and "carve" in via cs-carve. `forwards` fill means the reduced-
* motion kill-switch resolves them to the finished (fully drawn) state. */
.cs-sigil { display: none; }
.cybersigil .cs-sigil {
display: block;
width: 100%;
height: 100%;
overflow: visible;
}
.cybersigil .cs-sigil path,
.cybersigil .cs-sigil line {
fill: none;
stroke: var(--sky);
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
vector-effect: non-scaling-stroke;
stroke-dasharray: 1;
stroke-dashoffset: 1;
}
.cybersigil .cs-sig-spine { opacity: 0.45; stroke-width: 1.4; }
@keyframes cs-carve {
to { stroke-dashoffset: 0; }
}
/* Corner growths: swap the static mask box for the live SVG, keep the box's
* size / placement / flicker opacity. Carves itself on page load. */
.cybersigil .cs-fx-corner--sig {
background: none;
-webkit-mask: none;
mask: none;
}
.cybersigil .cs-fx-corner--sig .cs-sigil path,
.cybersigil .cs-fx-corner--sig .cs-sigil line {
animation: cs-carve 1100ms ease-out forwards;
animation-delay: calc(var(--i, 0) * 90ms);
}
/* Selection — magenta block, bone glyph, cyan bleed. */
.cybersigil ::selection {
background: var(--mauve);
@@ -2199,6 +2240,41 @@ html.cybersigil body::after {
animation: cs-scan 0.78s cubic-bezier(0.4, 0, 0.2, 1) 1;
}
/* Per-plate sigil — spiky bruised-magenta growth that carves over the tile
* on contact, then fades back out. Screen-blended so it etches into the
* panel rather than masking it. */
.cs-plate-sig { display: none; }
.cybersigil .cs-plate-sig {
display: block;
position: absolute;
top: -9%;
left: 50%;
width: 46%;
height: 118%;
transform: translateX(-50%);
z-index: 2;
pointer-events: none;
opacity: 0;
mix-blend-mode: screen;
transition: opacity 0.24s ease;
}
.cybersigil .cs-plate-sig .cs-sigil path,
.cybersigil .cs-plate-sig .cs-sigil line {
stroke: color-mix(in srgb, var(--mauve) 68%, var(--sky));
filter: drop-shadow(0 0 4px color-mix(in srgb, var(--mauve) 55%, transparent));
}
.cybersigil .plate-enter:hover .cs-plate-sig,
.cybersigil .plate-enter:focus-within .cs-plate-sig {
opacity: 0.5;
}
.cybersigil .plate-enter:hover .cs-plate-sig .cs-sigil path,
.cybersigil .plate-enter:hover .cs-plate-sig .cs-sigil line,
.cybersigil .plate-enter:focus-within .cs-plate-sig .cs-sigil path,
.cybersigil .plate-enter:focus-within .cs-plate-sig .cs-sigil line {
animation: cs-carve 600ms cubic-bezier(0.4, 0, 0.2, 1) forwards;
animation-delay: calc(var(--i, 0) * 55ms);
}
/* Scroll-entry databend (class toggled by CyberFx IntersectionObserver). */
@keyframes cs-databend {
0% { clip-path: inset(0 0 0 0); transform: translateX(0); filter: none; }