"use client"; import React, { useId, useMemo } from "react"; import { motion } from "framer-motion"; import { cn } from "../utils/cn"; interface MarkerProps { children: React.ReactNode; delay?: number; className?: string; color?: string; } /** * Copic Marker Component * * Uses hand-drawn filled polygon paths (NOT rects or strokes) for an * organic, human feel. Each highlight band is a wobbly shape with * bezier curves, not a perfect rectangle. Second strokes are offset * to simulate imperfect re-highlighting. */ // Seeded PRNG for SSR-safe randomness function createRng(seed: string) { let s = 0; for (let i = 0; i < seed.length; i++) { s = ((s << 5) - s + seed.charCodeAt(i)) | 0; } return () => { s = (s * 16807 + 0) % 2147483647; return (s & 0x7fffffff) / 0x7fffffff; }; } /** * Generate a hand-drawn highlight band path. * Instead of a rectangle, we trace an organic polygon with wobbly edges. * * The shape goes: left edge → top edge (left to right) → right edge → bottom edge (right to left) * Each edge has 1-2 control points for subtle curves. * * ViewBox: 0 0 100 100 with preserveAspectRatio="none" * So x=0..100 maps to text width, y=0..100 maps to text height. */ function generateBandPath(rng: () => number, bandIndex: number) { // Vertical position: center band in the text height // First band centered around y=50, second shifted up or down const yOffset = bandIndex === 0 ? 0 : (rng() - 0.5) * 20; // -10 to +10 shift const yCenter = 50 + yOffset; // Band thickness: covers ~40-50% of text height const halfHeight = 20 + rng() * 8; // 20-28 units (40-56% of viewBox) const yTop = yCenter - halfHeight; const yBottom = yCenter + halfHeight; // Horizontal extent: slight random overshoot/undershoot const xStart = -2 + rng() * 3; // -2 to 1 const xEnd = 99 + rng() * 3; // 99 to 102 // Generate wobbly top edge points (left to right) const topWobble1 = yTop + (rng() - 0.5) * 6; const topWobble2 = yTop + (rng() - 0.5) * 6; const topWobble3 = yTop + (rng() - 0.5) * 6; // Generate wobbly bottom edge points (right to left) const botWobble1 = yBottom + (rng() - 0.5) * 6; const botWobble2 = yBottom + (rng() - 0.5) * 6; const botWobble3 = yBottom + (rng() - 0.5) * 6; // Slight angle on left/right edges const leftTopY = yTop + (rng() - 0.5) * 4; const leftBotY = yBottom + (rng() - 0.5) * 4; const rightTopY = yTop + (rng() - 0.5) * 4; const rightBotY = yBottom + (rng() - 0.5) * 4; // Build the path: // Start at top-left, trace top edge with curves, down right edge, // trace bottom edge with curves back left, close. const path = [ // Start top-left `M ${xStart},${leftTopY}`, // Top edge: 3 curve segments left→right `C ${xStart + 15},${topWobble1} ${xStart + 30},${topWobble1} ${33},${topWobble2}`, `C ${45},${topWobble2} ${55},${topWobble3} ${67},${topWobble3}`, `C ${78},${topWobble3} ${xEnd - 10},${rightTopY} ${xEnd},${rightTopY}`, // Right edge down `L ${xEnd + (rng() - 0.5) * 2},${rightBotY}`, // Bottom edge: 3 curve segments right→left `C ${xEnd - 10},${botWobble1} ${78},${botWobble1} ${67},${botWobble2}`, `C ${55},${botWobble2} ${45},${botWobble3} ${33},${botWobble3}`, `C ${xStart + 30},${botWobble3} ${xStart + 15},${leftBotY} ${xStart},${leftBotY}`, // Close `Z`, ].join(" "); return path; } export const Marker: React.FC = ({ children, delay = 0, className = "", color = "rgba(250, 204, 21, 0.5)", }) => { const id = useId(); const filterId = `marker-rough-${id.replace(/:/g, "")}`; const bands = useMemo(() => { const rng = createRng(id); const numBands = rng() > 0.5 ? 2 : 1; const result = []; for (let i = 0; i < numBands; i++) { result.push({ d: generateBandPath(rng, i), delay: delay + i * 0.15, duration: 0.4 + rng() * 0.15, }); } return result; }, [id, delay]); return ( {children} ); };