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