From b64cd2e85a32d2e78c16b51f1317334c89dbdd4f Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Sun, 17 May 2026 16:11:36 +0200 Subject: [PATCH] updated sigil rendering --- frontend/src/components/CyberFx.astro | 37 ++++------- frontend/src/lib/cybersigil.test.ts | 2 +- frontend/src/lib/cybersigil.ts | 59 +++++++++++++----- .../src/styles/partials/70-cybersigil.css | 61 +++++-------------- frontend/src/styles/partials/90-keyframes.css | 8 --- .../src/styles/partials/99-reduced-motion.css | 1 - .../results.json | 1 + 7 files changed, 73 insertions(+), 96 deletions(-) create mode 100644 node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json diff --git a/frontend/src/components/CyberFx.astro b/frontend/src/components/CyberFx.astro index 7f087c7..33d2250 100644 --- a/frontend/src/components/CyberFx.astro +++ b/frontend/src/components/CyberFx.astro @@ -14,10 +14,6 @@
- - - - @@ -31,34 +27,25 @@ 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('.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'); - }); - } - /* ─── Tall sigil vines pinned to the left/right screen edges ─── */ - /* Each edge gets its own growth (more organic than a shared one); - * CSS pins the spine to the edge and clips the outer half so it + /* Built once and shared by both edges so left/right are symmetric; + * the right edge is flipped in CSS (scaleX(-1)). Edge-mode crops the + * SVG to the inward half + stretches it to fill the ribbon, so it * reads as an inward-creeping border vine. */ const edges = document.querySelectorAll('.cs-fx-edge'); - edges.forEach((e) => { - if (e.classList.contains('cs-fx-edge--sig')) return; - e.innerHTML = buildCybersigil({ count: 8 }); - e.classList.add('cs-fx-edge--sig'); - }); + if (edges.length) { + const vine = buildCybersigil({ count: 22, spineWave: 4, edge: true }); + edges.forEach((e) => { + if (e.classList.contains('cs-fx-edge--sig')) return; + e.innerHTML = vine; + e.classList.add('cs-fx-edge--sig'); + }); + } /* ─── One slow-spinning sigil filling the background ─── */ const wire = document.querySelector('.cs-fx-wire'); if (wire && !wire.classList.contains('cs-fx-wire--sig')) { - wire.innerHTML = buildCybersigil({ count: 9 }); + wire.innerHTML = buildCybersigil({ count: 20 }); wire.classList.add('cs-fx-wire--sig'); } diff --git a/frontend/src/lib/cybersigil.test.ts b/frontend/src/lib/cybersigil.test.ts index df3cb89..f3f462d 100644 --- a/frontend/src/lib/cybersigil.test.ts +++ b/frontend/src/lib/cybersigil.test.ts @@ -58,7 +58,7 @@ describe('buildCybersigil', () => { const dense = buildCybersigil({ count: 9, rng: seeded(5) }); const n = (s: string) => (s.match(/ { diff --git a/frontend/src/lib/cybersigil.ts b/frontend/src/lib/cybersigil.ts index ffd2c39..f340e13 100644 --- a/frontend/src/lib/cybersigil.ts +++ b/frontend/src/lib/cybersigil.ts @@ -36,11 +36,25 @@ export interface SigilOptions { count?: number; /** injectable RNG (0..1); default Math.random */ rng?: () => number; + /** + * Spine sinuosity. Default `1` keeps the original gentle one-sided bow + * (byte-identical output). Values >1 make the spine weave inward and + * back (staying ≥0 so it never crosses the edge clip) with amplitude + * ×spineWave — a serpentine vine for tall edge ribbons. + */ + spineWave?: number; + /** + * Edge mode: crop the viewBox to the inward half (spine pinned at x=0) + * and emit a single half with `preserveAspectRatio="none"`, so the + * figure stretches to fill a tall narrow border ribbon instead of + * scaling uniformly and overflowing it. + */ + edge?: boolean; } 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 +const MAX_PATHS = 200; // safety ceiling; real density is bounded by the params type Pt = [number, number]; @@ -64,7 +78,7 @@ export function buildCybersigil(opts: SigilOptions = {}): string { const emit = (d: string, cls: string) => { if (strokeCount >= MAX_PATHS) return; parts.push( - ``, + ``, ); strokeCount++; }; @@ -115,7 +129,7 @@ export function buildCybersigil(opts: SigilOptions = {}): string { track(at[0] + g.w * s); parts.push( `` + - ``, + ``, ); strokeCount++; }; @@ -125,7 +139,7 @@ export function buildCybersigil(opts: SigilOptions = {}): string { // 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 L = scale * rnd(50, 92); const dx = Math.cos(ang) * L; const dy = Math.sin(ang) * L; const peak: Pt = [ox + dx, oy + dy]; @@ -143,7 +157,7 @@ export function buildCybersigil(opts: SigilOptions = {}): string { emit(spline(pts), 'cs-sig-main'); // terminal spike off the outermost point - barb(peak, ang + rnd(-0.5, 0.5), scale * rnd(8, 18)); + barb(peak, ang + rnd(-0.5, 0.5), scale * rnd(12, 26)); // filament shadow trailing the main sweep if (rng() < 0.4) { @@ -169,7 +183,7 @@ export function buildCybersigil(opts: SigilOptions = {}): string { 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)); + barb(base, ang + side * rnd(0.6, 1.3), scale * rnd(9, 22)); } // recurse — one child curls off the mid/peak region @@ -188,12 +202,21 @@ export function buildCybersigil(opts: SigilOptions = {}): string { if (depth === 0 && rng() < 0.3) ornament(peak, ang); }; - // ── Wavering spine: a curve from top to bottom, gently bowing in +x. + // ── Wavering spine: a curve from top to bottom. Default gently bows in + // +x; spineWave>1 makes it weave side-to-side (a serpentine edge vine). + const sw = opts.spineWave ?? 1; 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); + const x = + i === 0 || i === spineNodes + ? 0 + : sw === 1 + ? rnd(0, 11) // unchanged default — center/corner/plate sigils + : i % 2 + ? rnd(2, 8) // near the edge + : rnd(9, 15) * sw; // inward bulge — one-sided weave, never < 0 spinePts.push([x, y]); } emit(spline(spinePts), 'cs-sig-main'); @@ -234,18 +257,22 @@ export function buildCybersigil(opts: SigilOptions = {}): string { limb(node[0], node[1], ang, rnd(0.65, 1.05) * (0.8 + tc * 0.5), 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) * spike); + if (rng() < 0.55) barb(node, bias + rnd(-0.3, 0.3), rnd(11, 20) * spike); } const half = parts.join(''); - const minX = -(maxX + PAD); - const vbW = 2 * (maxX + PAD); + // Edge mode crops to the inward half (spine at x=0) and lets the ribbon + // stretch it (none); normal mode is symmetric and uniformly fitted. + const minX = opts.edge ? 0 : -(maxX + PAD); + const vbW = opts.edge ? maxX + PAD : 2 * (maxX + PAD); + const par = opts.edge ? 'none' : 'xMidYMid meet'; + const body = opts.edge + ? `${half}` + : `${half}` + + `${half}`; return ( `` + `preserveAspectRatio="${par}" aria-hidden="true" focusable="false" ` + + `xmlns="http://www.w3.org/2000/svg">${body}` ); } diff --git a/frontend/src/styles/partials/70-cybersigil.css b/frontend/src/styles/partials/70-cybersigil.css index a733ddd..c6efa7e 100644 --- a/frontend/src/styles/partials/70-cybersigil.css +++ b/frontend/src/styles/partials/70-cybersigil.css @@ -14,7 +14,6 @@ .cybersigil { --font-sans: 'Space Mono', 'Courier New', ui-monospace, monospace; --font-display: 'VT323', 'Space Mono', 'Courier New', monospace; - --cs-corner: url("data:image/svg+xml;utf8,"); --cs-barb: url("data:image/svg+xml;utf8,"); } @@ -76,7 +75,6 @@ html.cybersigil body::after { .cybersigil .cs-fx-halftone, .cybersigil .cs-fx-wire, .cybersigil .cs-fx-tear, -.cybersigil .cs-fx-corner, .cybersigil .cs-fx-edge { position: fixed; pointer-events: none; @@ -110,14 +108,17 @@ html.cybersigil body::after { filter: drop-shadow(0 0 6px color-mix(in srgb, var(--sky) 35%, transparent)); } .cybersigil .cs-fx-wire .cs-sigil path { - animation: cs-redraw 5.5s ease-in-out infinite; - /* negative, per-stroke offset: the field is always mid-carve, never blank */ - animation-delay: calc(var(--i, 0) * -0.34s); + /* slower than the edges + a tighter stagger: the dense background field + * spends most of the cycle solid, so it no longer reads as dotted while + * many strokes wipe out of phase. */ + animation: cs-redraw 8s linear infinite; + animation-delay: calc(var(--i, 0) * -0.13s); } +/* Continuous carve: draws on through the first half, wipes off through the + * second, no plateau — the stroke is always in motion. */ @keyframes cs-redraw { 0% { stroke-dashoffset: 1; } - 35% { stroke-dashoffset: 0; } - 60% { stroke-dashoffset: 0; } + 50% { stroke-dashoffset: 0; } 100% { stroke-dashoffset: -1; } } @@ -147,21 +148,6 @@ html.cybersigil body::after { ); } -/* Thorny sigil growths anchoring the four screen corners. */ -.cybersigil .cs-fx-corner { - width: clamp(96px, 13vw, 188px); - height: clamp(96px, 13vw, 188px); - background-color: var(--sky); - opacity: 0.26; - -webkit-mask: var(--cs-corner) center / contain no-repeat; - mask: var(--cs-corner) center / contain no-repeat; - filter: drop-shadow(0 0 5px color-mix(in srgb, var(--sky) 45%, transparent)); - animation: cs-flicker 7s linear infinite; -} -.cybersigil .cs-fx-corner--tl { top: 0; left: 0; } -.cybersigil .cs-fx-corner--tr { top: 0; right: 0; transform: scaleX(-1); animation-delay: -1.7s; } -.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; } /* Edge vines — a tall sigil growth pinned to each screen edge, spine on * the edge with its outer half clipped so it creeps inward only. Subtle @@ -169,27 +155,25 @@ html.cybersigil body::after { .cybersigil .cs-fx-edge { top: 0; bottom: 0; - width: clamp(64px, 9vw, 132px); + width: clamp(96px, 12vw, 184px); overflow: hidden; - opacity: 0.1; + opacity: 0.26; } .cybersigil .cs-fx-edge--l { left: 0; } .cybersigil .cs-fx-edge--r { right: 0; transform: scaleX(-1); } +/* edge-mode SVG is pre-cropped to the inward half + preserveAspectRatio + * none, so it just fills the ribbon: full height, stretched to width. */ .cybersigil .cs-fx-edge .cs-sigil { position: absolute; - top: 0; - left: 0; - width: auto; + inset: 0; + width: 100%; height: 100%; - /* spine sits at the SVG's horizontal centre — shift it onto the edge so - * only the inward-growing half stays within the clipped box */ - transform: translateX(-50%); filter: drop-shadow(0 0 5px color-mix(in srgb, var(--sky) 35%, transparent)); } .cybersigil .cs-fx-edge .cs-sigil path, .cybersigil .cs-fx-edge .cs-sigil line { - animation: cs-redraw 5.5s ease-in-out infinite; - animation-delay: calc(var(--i, 0) * -0.34s); + animation: cs-redraw 6.5s linear infinite; + animation-delay: calc(var(--i, 0) * -0.13s); } /* ─── Generated sigils (cs-sigil markup from lib/cybersigil.ts) ────────── @@ -223,19 +207,6 @@ html.cybersigil body::after { to { stroke-dashoffset: 0; } } -/* Corner growths: swap the static mask box for the live SVG, keep the box's - * size / placement / flicker opacity. Strokes carve in and wipe out forever - * (same perpetual self-redraw as the background sigil). */ -.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-redraw 5.5s ease-in-out infinite; - animation-delay: calc(var(--i, 0) * -0.34s); -} /* Selection — magenta block, bone glyph, cyan bleed. */ .cybersigil ::selection { diff --git a/frontend/src/styles/partials/90-keyframes.css b/frontend/src/styles/partials/90-keyframes.css index 99b2cb5..ca436ff 100644 --- a/frontend/src/styles/partials/90-keyframes.css +++ b/frontend/src/styles/partials/90-keyframes.css @@ -3,14 +3,6 @@ 0%, 49% { opacity: 1; } 50%, 100% { opacity: 0; } } -@keyframes cs-flicker { - 0%, 8% { opacity: 0.26; } - 9% { opacity: 0.46; } - 10%, 70% { opacity: 0.26; } - 71% { opacity: 0.08; } - 72% { opacity: 0.34; } - 73%, 100% { opacity: 0.26; } -} @keyframes cs-tear { 0%, 21% { opacity: 0; top: 18%; } 22% { opacity: 0.85; top: 18%; transform: translateX(-7px); } diff --git a/frontend/src/styles/partials/99-reduced-motion.css b/frontend/src/styles/partials/99-reduced-motion.css index 652fef4..9a3f196 100644 --- a/frontend/src/styles/partials/99-reduced-motion.css +++ b/frontend/src/styles/partials/99-reduced-motion.css @@ -13,7 +13,6 @@ /* The looping sigils would otherwise collapse to their hidden end-state — * pin them fully drawn instead so they stay visible, just still. */ .cybersigil .cs-fx-wire .cs-sigil path, - .cybersigil .cs-fx-corner--sig .cs-sigil path, .cybersigil .cs-fx-edge .cs-sigil path { animation: none !important; stroke-dashoffset: 0 !important; diff --git a/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json b/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json new file mode 100644 index 0000000..6fe31d7 --- /dev/null +++ b/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json @@ -0,0 +1 @@ +{"version":"4.1.6","results":[[":frontend/src/lib/cybersigil.test.ts",{"duration":66.06918499999995,"failed":false}]]} \ No newline at end of file