diff --git a/frontend/src/components/react/PostList.tsx b/frontend/src/components/react/PostList.tsx index b352e82..89b2a89 100644 --- a/frontend/src/components/react/PostList.tsx +++ b/frontend/src/components/react/PostList.tsx @@ -185,7 +185,7 @@ export default function PostList({ posts: initialPosts, isAdmin = false }: Props )} {post.draft && ( - + Sketch )} diff --git a/frontend/src/lib/cybersigil.ts b/frontend/src/lib/cybersigil.ts index 78501a6..1147078 100644 --- a/frontend/src/lib/cybersigil.ts +++ b/frontend/src/lib/cybersigil.ts @@ -1,110 +1,233 @@ /* - * cybersigil — modular neo-tribal sigil builder. + * cybersigil — chaotic neo-tribal sigil generator. * - * 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 s (identity + scale(-1 1)) for the mirror, and - * emits an inert SVG string. + * Cybersigilism's signature is dense, fractal, barbed linework with hard + * vertical symmetry. We grow it procedurally: a wavering central spine spawns + * recursive curved limbs, each scattering barbs and thin filament shadows and + * occasionally terminating in a small hand-drawn motif. The whole right-leaning + * tangle is mirrored about x=0 (scale(-1 1)); a spine that wobbles in +x while + * its mirror wobbles in −x weaves the two halves into one symmetric growth. * - * Carving: every 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. + * Strokes carry pathLength="1" so the CSS draw-on ("carve") animation needs no + * JS length measurement; an inline `--i` staggers the carve into waves. + * Output is decorative and self-generated (no user input) — safe to inject via + * innerHTML. Inert under non-cybersigil themes (all styling `.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] */ + /** path data, local coords, anchored near origin */ 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. +// Small hand-drawn motifs used as limb-tip flourishes — the deliberate, +// "carved on purpose" punctuation amid the procedural chaos. 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' }, + { w: 18, h: 22, d: 'M0 0 Q16 5 14 14 Q12 21 0 22 M4 7 L10 3' }, + { w: 16, h: 16, d: 'M0 8 Q14 0 15 8 Q14 16 0 8 M8 2 L8 14' }, + { w: 14, h: 24, d: 'M0 0 L0 24 M0 6 L12 2 M0 14 L13 10 M0 21 L9 19' }, + { w: 20, h: 18, d: 'M0 9 Q10 -2 19 4 M19 4 L15 0 M19 4 L20 9 M0 9 L4 16' }, + { w: 15, h: 20, d: 'M0 0 Q14 6 13 13 Q12 19 0 19 M7 8 Q11 11 11 16' }, ] as const; export interface SigilOptions { - /** how many blocks to stack; default random 3–5 */ + /** rough number of primary branch nodes; default random 6–10 */ 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; +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 -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); -} +type Pt = [number, number]; -/** - * 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]); + const rnd = (a: number, b: number) => a + rng() * (b - a); + const pick = (a: readonly T[]): T => a[Math.floor(rng() * a.length)]; + const n = (v: number) => { + const r = Math.round(v * 10) / 10; + return Object.is(r, -0) ? '0' : String(r); + }; - let y = 0; - let maxW = 0; - let bottom = 0; - const groups: string[] = []; + const parts: string[] = []; + let strokeCount = 0; + let maxX = 24; - chosen.forEach((g, idx) => { - groups.push( - ``, + const track = (x: number) => { + const ax = Math.abs(x); + if (ax > maxX) maxX = ax; + }; + const emit = (d: string, cls: string) => { + if (strokeCount >= MAX_PATHS) return; + parts.push( + ``, ); - bottom = Math.max(bottom, y + g.h); - maxW = Math.max(maxW, g.w); - y += g.h - OVERLAP; - }); + strokeCount++; + }; - const half = groups.join(''); - const spine = ``; + // Catmull-Rom → cubic Bézier through an ordered point list (organic sweep). + const spline = (pts: Pt[]): string => { + if (pts.length < 2) return ''; + let d = `M${n(pts[0][0])} ${n(pts[0][1])}`; + for (let i = 0; i < pts.length - 1; i++) { + const p0 = pts[i - 1] ?? pts[i]; + const p1 = pts[i]; + const p2 = pts[i + 1]; + const p3 = pts[i + 2] ?? p2; + const c1: Pt = [p1[0] + (p2[0] - p0[0]) / 6, p1[1] + (p2[1] - p0[1]) / 6]; + const c2: Pt = [p2[0] - (p3[0] - p1[0]) / 6, p2[1] - (p3[1] - p1[1]) / 6]; + d += `C${n(c1[0])} ${n(c1[1])} ${n(c2[0])} ${n(c2[1])} ${n(p2[0])} ${n(p2[1])}`; + track(c1[0]); + track(c2[0]); + track(p2[0]); + } + return d; + }; - const minX = -(maxW + PAD); - const vbW = 2 * (maxW + PAD); - const minY = -PAD; - const vbH = bottom + 2 * PAD; + // A short spike, optionally kinked into a fang. + const barb = (base: Pt, ang: number, len: number) => { + const tip: Pt = [base[0] + Math.cos(ang) * len, base[1] + Math.sin(ang) * len]; + if (tip[0] < -3) tip[0] = -3; + track(base[0]); + track(tip[0]); + if (rng() < 0.45) { + const mid: Pt = [ + (base[0] + tip[0]) / 2 + Math.cos(ang + Math.PI / 2) * len * 0.3, + (base[1] + tip[1]) / 2 + Math.sin(ang + Math.PI / 2) * len * 0.3, + ]; + emit( + `M${n(base[0])} ${n(base[1])}Q${n(mid[0])} ${n(mid[1])} ${n(tip[0])} ${n(tip[1])}`, + 'cs-sig-barb', + ); + } else { + emit(`M${n(base[0])} ${n(base[1])}L${n(tip[0])} ${n(tip[1])}`, 'cs-sig-barb'); + } + }; + const ornament = (at: Pt, ang: number) => { + const g = pick(GLYPHS); + const s = rnd(0.45, 0.85); + const deg = (ang * 180) / Math.PI + rnd(-25, 25); + track(at[0] + g.w * s); + parts.push( + `` + + ``, + ); + strokeCount++; + }; + + // Recursive limb: a curved sweep out from (ox,oy) along `ang`, hooking back, + // scattering barbs, shedding a filament shadow, branching, sometimes tipped + // 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 dx = Math.cos(ang) * L; + const dy = Math.sin(ang) * L; + const peak: Pt = [ox + dx, oy + dy]; + const mid: Pt = [ + ox + dx * 0.45 + Math.cos(ang + Math.PI / 2) * rnd(-10, 14), + oy + dy * 0.45 + Math.sin(ang + Math.PI / 2) * rnd(-10, 14), + ]; + // hook back toward the spine + const hook: Pt = [ + peak[0] - Math.cos(ang) * L * rnd(0.3, 0.55), + peak[1] + Math.sin(ang + 0.7) * L * rnd(0.25, 0.5), + ]; + const tail: Pt = [Math.max(-2, hook[0] - L * rnd(0.2, 0.4)), hook[1] + rnd(-6, 10)]; + const pts: Pt[] = [[ox, oy], mid, peak, hook, tail]; + emit(spline(pts), 'cs-sig-main'); + + // terminal spike off the outermost point + barb(peak, ang + rnd(-0.5, 0.5), scale * rnd(8, 18)); + + // filament shadow trailing the main sweep + if (rng() < 0.4) { + const off = rnd(2, 6); + emit( + spline( + pts.map( + ([x, y]) => + [x + Math.cos(ang + Math.PI / 2) * off, y + Math.sin(ang + Math.PI / 2) * off] as Pt, + ), + ), + 'cs-sig-fil', + ); + } + + // barb scatter along the chord + const nb = 1 + Math.floor(rng() * 2); + for (let k = 0; k < nb; k++) { + const t = (k + 1) / (nb + 1); + const seg = t < 0.5 ? [pts[0], pts[2], t * 2] : [pts[2], pts[4], (t - 0.5) * 2]; + const a = seg[0] as Pt; + const b = seg[1] as Pt; + 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)); + } + + // recurse — one child curls off the mid/peak region + if (depth > 0 && rng() < 0.55) { + const from = rng() < 0.5 ? mid : peak; + limb( + from[0], + from[1], + ang + (rng() < 0.5 ? 1 : -1) * rnd(0.5, 1.2), + scale * rnd(0.42, 0.6), + depth - 1, + ); + } + + // motif flourish at a terminal tip + if (depth === 0 && rng() < 0.3) ornament(peak, ang); + }; + + // ── Wavering spine: a curve from top to bottom, gently bowing in +x. + 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); + spinePts.push([x, y]); + } + emit(spline(spinePts), 'cs-sig-main'); + + // ── Branch nodes ride the spine and throw limbs outward. Nodes are + // inset from the very ends and spread the full height so growth flows + // down the whole trunk rather than clumping at the top. + const nodes = opts.count ?? 7 + Math.floor(rng() * 3); // 7–9 + for (let i = 0; i < nodes; i++) { + const t = 0.08 + (0.86 * (i + rnd(-0.25, 0.25))) / (nodes - 1); + const tc = Math.max(0.05, Math.min(0.95, t)); + const si = Math.min(spinePts.length - 2, Math.floor(tc * (spinePts.length - 1))); + const sf = tc * (spinePts.length - 1) - si; + const a = spinePts[si]; + const b = spinePts[si + 1]; + const node: Pt = [a[0] + (b[0] - a[0]) * sf, a[1] + (b[1] - a[1]) * sf]; + // later nodes lean downward so the lower trunk fills out + const bias = -0.25 + tc * 0.7; + const limbs = 1 + Math.floor(rng() * 2); + for (let l = 0; l < limbs; l++) { + const ang = bias + rnd(-0.55, 0.55); + limb(node[0], node[1], ang, rnd(0.65, 1.05), 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)); + } + + const half = parts.join(''); + const minX = -(maxX + PAD); + const vbW = 2 * (maxX + PAD); return ( - `` diff --git a/frontend/src/styles/global.css b/frontend/src/styles/global.css index 1335e88..69e5341 100644 --- a/frontend/src/styles/global.css +++ b/frontend/src/styles/global.css @@ -837,6 +837,15 @@ code, pre, kbd, samp { text-transform: uppercase; backdrop-filter: blur(3px); } +/* Draft/"Sketch" marker — same chip, pinned bottom-left, amber instead of + * the neutral catalogue tag. Themed per skin below (no inline colors). */ +.plate-tag-mini--draft { + left: 16px; + right: auto; + background: color-mix(in srgb, var(--peach) 88%, var(--crust)); + color: var(--crust); + border-color: color-mix(in srgb, var(--peach) 45%, transparent); +} /* Breakcore: hard neon catalogue tag — sharp rect, offset shadow, glow. * Matches the layer's hazard-tape / hard-offset chrome language. */ .breakcore .plate-tag-mini { @@ -853,6 +862,14 @@ code, pre, kbd, samp { 0 0 14px -2px color-mix(in srgb, var(--mauve) 65%, transparent); backdrop-filter: none; } +.breakcore .plate-tag-mini--draft { + color: var(--peach); + border-color: var(--peach); + text-shadow: 0 0 6px color-mix(in srgb, var(--peach) 60%, transparent); + box-shadow: + 2px 2px 0 var(--peach), + 0 0 14px -2px color-mix(in srgb, var(--peach) 65%, transparent); +} /* Nameplate — the museum-style header used in the site chrome */ .nameplate { @@ -2033,8 +2050,7 @@ html.cybersigil body::after { height: 100%; overflow: visible; } -.cybersigil .cs-sigil path, -.cybersigil .cs-sigil line { +.cybersigil .cs-sigil path { fill: none; stroke: var(--sky); stroke-width: 2; @@ -2044,7 +2060,11 @@ html.cybersigil body::after { stroke-dasharray: 1; stroke-dashoffset: 1; } -.cybersigil .cs-sig-spine { opacity: 0.45; stroke-width: 1.4; } +/* Stroke-weight tiers — heavy growth, hair filaments, prickly barbs, motifs. */ +.cybersigil .cs-sigil .cs-sig-main { stroke-width: 2.4; } +.cybersigil .cs-sigil .cs-sig-fil { stroke-width: 0.9; opacity: 0.5; } +.cybersigil .cs-sigil .cs-sig-barb { stroke-width: 1.3; } +.cybersigil .cs-sigil .cs-sig-orn { stroke-width: 1.7; opacity: 0.92; } @keyframes cs-carve { to { stroke-dashoffset: 0; } @@ -2167,21 +2187,21 @@ html.cybersigil body::after { 0 22px 44px -28px rgba(79, 233, 255, 0.26), 0 0 26px -10px color-mix(in srgb, var(--mauve) 34%, transparent); } +/* Thin engraved corner brackets. The deliberate sigil motif now comes from + * the generated cs-plate-sig, so the old organic leaf masks are dropped. */ .cybersigil .plate::before, .cybersigil .plate::after { content: ""; position: absolute; - width: 34px; - height: 34px; - background-color: color-mix(in srgb, var(--sky) 78%, transparent); - -webkit-mask: var(--cs-corner) center / contain no-repeat; - mask: var(--cs-corner) center / contain no-repeat; - filter: drop-shadow(0 0 3px color-mix(in srgb, var(--sky) 45%, transparent)); + width: 20px; + height: 20px; pointer-events: none; - transition: background-color 0.18s ease, filter 0.18s ease; + border: 0 solid color-mix(in srgb, var(--sky) 55%, transparent); + filter: drop-shadow(0 0 3px color-mix(in srgb, var(--sky) 32%, transparent)); + transition: border-color 0.18s ease, filter 0.18s ease; } -.cybersigil .plate::before { top: 5px; left: 5px; } -.cybersigil .plate::after { right: 5px; bottom: 5px; transform: scale(-1); } +.cybersigil .plate::before { top: 6px; left: 6px; border-top-width: 2px; border-left-width: 2px; } +.cybersigil .plate::after { right: 6px; bottom: 6px; border-right-width: 2px; border-bottom-width: 2px; } .cybersigil .plate:hover { transform: translateY(-3px); box-shadow: @@ -2192,7 +2212,7 @@ html.cybersigil body::after { } .cybersigil .plate:hover::before, .cybersigil .plate:hover::after { - background-color: var(--mauve); + border-color: var(--mauve); filter: drop-shadow(0 0 6px color-mix(in srgb, var(--mauve) 60%, transparent)); } .cybersigil .plate:focus-visible { @@ -2290,22 +2310,8 @@ html.cybersigil body::after { animation: cs-databend 560ms steps(5, jump-none) 1; } -/* Prose image framing — thorny growth at opposing corners + cold edge. */ -.cybersigil .prose figure { position: relative; } -.cybersigil .prose figure::before, -.cybersigil .prose figure::after { - content: ""; - position: absolute; - width: 40px; - height: 40px; - background-color: color-mix(in srgb, var(--sky) 70%, transparent); - -webkit-mask: var(--cs-corner) center / contain no-repeat; - mask: var(--cs-corner) center / contain no-repeat; - pointer-events: none; - z-index: 1; -} -.cybersigil .prose figure::before { top: -8px; left: -8px; } -.cybersigil .prose figure::after { right: -8px; bottom: -8px; transform: scale(-1); } +/* Prose image framing — cold gradient edge only. Old corner leaf masks + * removed; figural sigils are no longer pinned to post images. */ .cybersigil .prose figure img, .cybersigil .prose img { background: @@ -2694,6 +2700,16 @@ html.cybersigil body::after { font-weight: 700; text-transform: uppercase; } +/* Sketch marker reads as an unstable/draft signal — amber glyph, split + * chromatic shadow, hard offset, matching the corrupted-terminal language. */ +.cybersigil .plate-tag-mini--draft { + color: var(--peach); + border-color: var(--peach); + box-shadow: 2px 2px 0 color-mix(in srgb, var(--peach) 70%, transparent); + text-shadow: + -1px 0 0 color-mix(in srgb, var(--peach) 65%, transparent), + 1px 0 0 color-mix(in srgb, var(--sky) 55%, transparent); +} .cybersigil .kbd-tip { background: var(--crust); border-color: var(--mauve);