added random cybersigilism generation
This commit is contained in:
@@ -21,12 +21,27 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { buildCybersigil } from '../lib/cybersigil';
|
||||||
|
|
||||||
function initCyberFx() {
|
function initCyberFx() {
|
||||||
const root = document.documentElement;
|
const root = document.documentElement;
|
||||||
if (!root.classList.contains('cybersigil')) return;
|
if (!root.classList.contains('cybersigil')) return;
|
||||||
|
|
||||||
const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
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 ─── */
|
/* ─── Scroll-entry databend on images ─── */
|
||||||
if (!reduced && 'IntersectionObserver' in window) {
|
if (!reduced && 'IntersectionObserver' in window) {
|
||||||
const targets = document.querySelectorAll<HTMLElement>(
|
const targets = document.querySelectorAll<HTMLElement>(
|
||||||
|
|||||||
@@ -1,6 +1,23 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { deletePost } from '../../lib/api';
|
import { deletePost } from '../../lib/api';
|
||||||
import { confirmDialog, notify } from '../../lib/confirm';
|
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;
|
const PAGE_SIZE = 9;
|
||||||
|
|
||||||
@@ -220,6 +237,8 @@ export default function PostList({ posts: initialPosts, isAdmin = false }: Props
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<PlateSigil />
|
||||||
</article>
|
</article>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -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 3–5 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 3–5 */
|
||||||
|
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); // 3–5
|
||||||
|
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>`
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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--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; }
|
.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. */
|
/* Selection — magenta block, bone glyph, cyan bleed. */
|
||||||
.cybersigil ::selection {
|
.cybersigil ::selection {
|
||||||
background: var(--mauve);
|
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;
|
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). */
|
/* Scroll-entry databend (class toggled by CyberFx IntersectionObserver). */
|
||||||
@keyframes cs-databend {
|
@keyframes cs-databend {
|
||||||
0% { clip-path: inset(0 0 0 0); transform: translateX(0); filter: none; }
|
0% { clip-path: inset(0 0 0 0); transform: translateX(0); filter: none; }
|
||||||
|
|||||||
Reference in New Issue
Block a user