From 47b1cca8ae95529465041a1a2b9a38fbb4ffe0d7 Mon Sep 17 00:00:00 2001 From: Val Alexander Date: Sun, 5 Apr 2026 01:58:58 -0500 Subject: [PATCH] Refine stitch border with subtle 3D depth - Swap the animated X stitches for cleaner edge-aligned thread marks - Add a soft inner shadow and calmer pulse timing for a raised-panel feel --- apps/web/src/components/VoodooStitches.tsx | 105 ++++++++++++--------- apps/web/src/index.css | 25 ++++- 2 files changed, 83 insertions(+), 47 deletions(-) diff --git a/apps/web/src/components/VoodooStitches.tsx b/apps/web/src/components/VoodooStitches.tsx index 1197455db..80a29342c 100644 --- a/apps/web/src/components/VoodooStitches.tsx +++ b/apps/web/src/components/VoodooStitches.tsx @@ -2,20 +2,23 @@ import { useEffect, useState, useMemo } from "react"; import { useAppSettings } from "../appSettings"; /** - * Purely decorative voodoo-doll stitch border around the entire viewport. - * Each X-shaped cross-stitch pulses in opacity with a sequential delay, - * creating a wave that flows around the perimeter like calm backlit water. + * Minimal stitch border that wraps the viewport like thread pulled + * over the edge of a raised 3D panel. Each stitch is a small + * perpendicular line crossing the edge, with a soft shadow underneath + * to sell the depth illusion. */ -const STITCH_SPACING = 40; // px between stitch centers -const STITCH_SIZE = 4; // half-size of each X arm -const EDGE_INSET = 4; // px inset from viewport edge -const ANIMATION_DURATION = 8; // seconds for one full pulse cycle -const STROKE_WIDTH = 1; +const STITCH_SPACING = 56; // wider spacing → cleaner / more minimal +const STITCH_LENGTH = 6; // half-length of each stitch line +const EDGE_INSET = 6; // how far from viewport edge the "seam" sits +const ANIMATION_DURATION = 10; // slower, calmer pulse +const STROKE_WIDTH = 0.75; interface Stitch { cx: number; cy: number; + /** 'h' = horizontal stitch (top/bottom edges), 'v' = vertical (left/right) */ + orientation: "h" | "v"; index: number; } @@ -23,44 +26,46 @@ function generateStitches(w: number, h: number): { stitches: Stitch[]; total: nu const stitches: Stitch[] = []; let index = 0; - // Top edge: left → right + // Top edge const topCount = Math.max(0, Math.floor((w - EDGE_INSET * 2) / STITCH_SPACING)); const topOffset = (w - EDGE_INSET * 2 - (topCount - 1) * STITCH_SPACING) / 2; for (let i = 0; i < topCount; i++) { stitches.push({ cx: EDGE_INSET + topOffset + i * STITCH_SPACING, cy: EDGE_INSET, + orientation: "v", // perpendicular to top edge → vertical index: index++, }); } - // Right edge: top → bottom + // Right edge const rightCount = Math.max(0, Math.floor((h - EDGE_INSET * 2) / STITCH_SPACING)); const rightOffset = (h - EDGE_INSET * 2 - (rightCount - 1) * STITCH_SPACING) / 2; for (let i = 0; i < rightCount; i++) { stitches.push({ cx: w - EDGE_INSET, cy: EDGE_INSET + rightOffset + i * STITCH_SPACING, + orientation: "h", // perpendicular to right edge → horizontal index: index++, }); } - // Bottom edge: right → left - const bottomCount = topCount; - for (let i = 0; i < bottomCount; i++) { + // Bottom edge (right → left) + for (let i = 0; i < topCount; i++) { stitches.push({ - cx: EDGE_INSET + topOffset + (bottomCount - 1 - i) * STITCH_SPACING, + cx: EDGE_INSET + topOffset + (topCount - 1 - i) * STITCH_SPACING, cy: h - EDGE_INSET, + orientation: "v", index: index++, }); } - // Left edge: bottom → top - const leftCount = rightCount; - for (let i = 0; i < leftCount; i++) { + // Left edge (bottom → top) + for (let i = 0; i < rightCount; i++) { stitches.push({ cx: EDGE_INSET, - cy: EDGE_INSET + rightOffset + (leftCount - 1 - i) * STITCH_SPACING, + cy: EDGE_INSET + rightOffset + (rightCount - 1 - i) * STITCH_SPACING, + orientation: "h", index: index++, }); } @@ -93,7 +98,6 @@ export function VoodooStitches() { [dimensions.w, dimensions.h], ); - // Wave spans the full perimeter — each stitch gets a delay proportional to its position const delayPerStitch = total > 0 ? ANIMATION_DURATION / total : 0; if (!settings.showStitchBorder) return null; @@ -108,44 +112,55 @@ export function VoodooStitches() { zIndex: 9999, }} > + {/* Subtle inner shadow to sell the "raised panel" depth */} +
+ - {stitches.map((s) => ( - - {/* First arm of the X: ╲ */} - - {/* Second arm of the X: ╱ */} + + {/* Soft glow filter to give each stitch a slight shadow beneath it */} + + + + + + + + + + + + + {stitches.map((s) => { + const x1 = s.orientation === "v" ? s.cx : s.cx - STITCH_LENGTH; + const y1 = s.orientation === "v" ? s.cy - STITCH_LENGTH : s.cy; + const x2 = s.orientation === "v" ? s.cx : s.cx + STITCH_LENGTH; + const y2 = s.orientation === "v" ? s.cy + STITCH_LENGTH : s.cy; + + return ( - - ))} + ); + })}
); diff --git a/apps/web/src/index.css b/apps/web/src/index.css index 05764f1dd..73674fa1b 100644 --- a/apps/web/src/index.css +++ b/apps/web/src/index.css @@ -667,13 +667,34 @@ label:has(> select#reasoning-effort) select { @keyframes voodoo-stitch-pulse { 0%, 100% { - opacity: 0.08; + opacity: 0.06; } 50% { - opacity: 0.3; + opacity: 0.18; } } +/* ─── 3D depth shadow around viewport edge ─── */ +.voodoo-depth-shadow { + position: absolute; + inset: 0; + border-radius: 2px; + box-shadow: + inset 0 1px 3px 0 rgba(0, 0, 0, 0.08), + inset 0 -1px 2px 0 rgba(255, 255, 255, 0.02), + inset 1px 0 2px 0 rgba(0, 0, 0, 0.05), + inset -1px 0 2px 0 rgba(0, 0, 0, 0.05); + pointer-events: none; +} + +.dark .voodoo-depth-shadow { + box-shadow: + inset 0 1px 4px 0 rgba(0, 0, 0, 0.25), + inset 0 -1px 2px 0 rgba(255, 255, 255, 0.015), + inset 1px 0 3px 0 rgba(0, 0, 0, 0.18), + inset -1px 0 3px 0 rgba(0, 0, 0, 0.18); +} + /* ─── Accessibility ─── */ @media (prefers-reduced-motion: reduce) {