// Procedural artwork surfaces + ambient effects (no SVG art, no stock images) const { useEffect, useRef, useState } = React; // --- deterministic hash so each piece looks different but stable function hash(seed) { let h = 0; for (let i = 0; i < seed.length; i++) h = (h * 31 + seed.charCodeAt(i)) | 0; return () => { h = (h * 1664525 + 1013904223) | 0; return ((h >>> 0) % 100000) / 100000; }; } // Procedural artwork surface — abstract painted texture from a piece's palette. // Reads as art, not as a placeholder gray box. function ArtSurface({ piece, density = 1, className = "", style = {} }) { const ref = useRef(null); useEffect(() => { const canvas = ref.current; if (!canvas) return; const dpr = Math.min(window.devicePixelRatio || 1, 2); const w = canvas.clientWidth, h = canvas.clientHeight; canvas.width = w * dpr; canvas.height = h * dpr; const ctx = canvas.getContext("2d"); ctx.scale(dpr, dpr); const rand = hash(piece.id); const [c0, c1, c2, c3] = piece.palette; // base gradient (deep ground) const g = ctx.createLinearGradient(0, 0, w, h); g.addColorStop(0, c0); g.addColorStop(0.6, c1); g.addColorStop(1, c0); ctx.fillStyle = g; ctx.fillRect(0, 0, w, h); // wash blobs (mid tone) for (let i = 0; i < 28 * density; i++) { const x = rand() * w, y = rand() * h, r = (40 + rand() * 280) * (0.6 + density * 0.4); const rg = ctx.createRadialGradient(x, y, 0, x, y, r); rg.addColorStop(0, c1 + ""); rg.addColorStop(1, "rgba(0,0,0,0)"); ctx.globalAlpha = 0.18 + rand() * 0.22; ctx.fillStyle = rg; ctx.beginPath(); ctx.arc(x, y, r, 0, Math.PI * 2); ctx.fill(); } ctx.globalAlpha = 1; // brushstroke streaks (highlight palette) ctx.lineCap = "round"; for (let i = 0; i < 80 * density; i++) { const x0 = rand() * w, y0 = rand() * h; const ang = rand() * Math.PI * 2; const len = 30 + rand() * 220; const x1 = x0 + Math.cos(ang) * len, y1 = y0 + Math.sin(ang) * len; const col = rand() > 0.7 ? c3 : c2; ctx.strokeStyle = col; ctx.globalAlpha = 0.04 + rand() * 0.18; ctx.lineWidth = 0.6 + rand() * 3.5; ctx.beginPath(); ctx.moveTo(x0, y0); ctx.lineTo(x1, y1); ctx.stroke(); } // fine grain (pigment) const img = ctx.getImageData(0, 0, w, h); for (let i = 0; i < img.data.length; i += 4) { const n = (rand() - 0.5) * 22; img.data[i] = Math.max(0, Math.min(255, img.data[i] + n)); img.data[i+1] = Math.max(0, Math.min(255, img.data[i+1] + n)); img.data[i+2] = Math.max(0, Math.min(255, img.data[i+2] + n)); } ctx.putImageData(img, 0, 0); // foil scratches (top-layer accent) ctx.globalAlpha = 1; for (let i = 0; i < 16 * density; i++) { const x = rand() * w, y = rand() * h; const ang = rand() * Math.PI * 2; const len = 8 + rand() * 60; ctx.strokeStyle = c3; ctx.globalAlpha = 0.35 + rand() * 0.35; ctx.lineWidth = 0.4 + rand() * 0.8; ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(x + Math.cos(ang) * len, y + Math.sin(ang) * len); ctx.stroke(); } ctx.globalAlpha = 1; // vignette const v = ctx.createRadialGradient(w/2, h/2, Math.min(w,h)*0.3, w/2, h/2, Math.max(w,h)*0.75); v.addColorStop(0, "rgba(0,0,0,0)"); v.addColorStop(1, "rgba(0,0,0,0.5)"); ctx.fillStyle = v; ctx.fillRect(0, 0, w, h); }, [piece.id, density]); return ( ); } // Ambient ink canvas for the hero — slow, gravity-less particles function InkField({ accent = "#a8825a" }) { const ref = useRef(null); const mouseRef = useRef({ x: 0.5, y: 0.5, active: false }); useEffect(() => { const canvas = ref.current; if (!canvas) return; const dpr = Math.min(window.devicePixelRatio || 1, 2); let w, h, raf; const resize = () => { w = canvas.clientWidth; h = canvas.clientHeight; canvas.width = w * dpr; canvas.height = h * dpr; }; resize(); window.addEventListener("resize", resize); const ctx = canvas.getContext("2d"); ctx.scale(dpr, dpr); const N = 90; const parts = Array.from({ length: N }, () => ({ x: Math.random() * w, y: Math.random() * h, vx: (Math.random() - 0.5) * 0.18, vy: (Math.random() - 0.5) * 0.18, r: 0.6 + Math.random() * 2.2, life: Math.random(), })); const onMove = (e) => { const r = canvas.getBoundingClientRect(); mouseRef.current.x = (e.clientX - r.left) / r.width; mouseRef.current.y = (e.clientY - r.top) / r.height; mouseRef.current.active = true; }; window.addEventListener("mousemove", onMove); let t = 0; const tick = () => { t += 0.005; ctx.fillStyle = "rgba(12, 10, 8, 0.10)"; ctx.fillRect(0, 0, w, h); // slow drifting nebula const cx = (mouseRef.current.x) * w; const cy = (mouseRef.current.y) * h; const rg = ctx.createRadialGradient(cx, cy, 0, cx, cy, Math.max(w, h) * 0.5); rg.addColorStop(0, accent + "26"); rg.addColorStop(1, "rgba(0,0,0,0)"); ctx.fillStyle = rg; ctx.fillRect(0, 0, w, h); for (const p of parts) { // gentle field const ang = Math.sin(p.x * 0.003 + t) + Math.cos(p.y * 0.003 - t); p.vx += Math.cos(ang) * 0.004; p.vy += Math.sin(ang) * 0.004; // mouse repulsion const dx = p.x - cx, dy = p.y - cy; const d2 = dx*dx + dy*dy; if (d2 < 22000) { const f = (22000 - d2) / 22000; p.vx += (dx / Math.sqrt(d2 + 1)) * f * 0.3; p.vy += (dy / Math.sqrt(d2 + 1)) * f * 0.3; } p.vx *= 0.96; p.vy *= 0.96; p.x += p.vx; p.y += p.vy; if (p.x < 0) p.x += w; if (p.x > w) p.x -= w; if (p.y < 0) p.y += h; if (p.y > h) p.y -= h; ctx.beginPath(); ctx.fillStyle = "rgba(240, 232, 219, " + (0.18 + Math.sin(p.life * 9 + t * 4) * 0.12) + ")"; ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2); ctx.fill(); } raf = requestAnimationFrame(tick); }; tick(); return () => { cancelAnimationFrame(raf); window.removeEventListener("resize", resize); window.removeEventListener("mousemove", onMove); }; }, [accent]); return ; } // Custom cursor — small dot + lagging ring function Cursor() { const dotRef = useRef(null); const ringRef = useRef(null); const stateRef = useRef({ x: 0, y: 0, tx: 0, ty: 0, hover: false }); useEffect(() => { if (matchMedia("(pointer: coarse)").matches) return; const onMove = (e) => { stateRef.current.tx = e.clientX; stateRef.current.ty = e.clientY; if (dotRef.current) { dotRef.current.style.transform = `translate(${e.clientX}px, ${e.clientY}px)`; } }; const onOver = (e) => { const t = e.target; const hover = !!(t && t.closest && t.closest("[data-cursor]")); stateRef.current.hover = hover; const label = hover ? t.closest("[data-cursor]").getAttribute("data-cursor") : ""; if (ringRef.current) { ringRef.current.dataset.hover = hover ? "1" : "0"; ringRef.current.querySelector(".c-label").textContent = label || ""; } }; window.addEventListener("mousemove", onMove); window.addEventListener("mouseover", onOver); let raf; const tick = () => { const s = stateRef.current; s.x += (s.tx - s.x) * 0.18; s.y += (s.ty - s.y) * 0.18; if (ringRef.current) { ringRef.current.style.transform = `translate(${s.x}px, ${s.y}px)`; } raf = requestAnimationFrame(tick); }; tick(); document.documentElement.classList.add("has-custom-cursor"); return () => { cancelAnimationFrame(raf); window.removeEventListener("mousemove", onMove); window.removeEventListener("mouseover", onOver); document.documentElement.classList.remove("has-custom-cursor"); }; }, []); return (
); } // Reveal-on-scroll wrapper function Reveal({ children, as: Tag = "div", className = "", delay = 0, ...rest }) { const ref = useRef(null); const [seen, setSeen] = useState(false); useEffect(() => { const el = ref.current; if (!el) return; const io = new IntersectionObserver((entries) => { entries.forEach((e) => { if (e.isIntersecting) setSeen(true); }); }, { threshold: 0.12 }); io.observe(el); return () => io.disconnect(); }, []); return ( {children} ); } // Magnetic hover for buttons function Magnetic({ children, strength = 0.25, className = "" }) { const ref = useRef(null); useEffect(() => { const el = ref.current; if (!el) return; const onMove = (e) => { const r = el.getBoundingClientRect(); const dx = e.clientX - (r.left + r.width / 2); const dy = e.clientY - (r.top + r.height / 2); el.style.transform = `translate(${dx * strength}px, ${dy * strength}px)`; }; const onLeave = () => { el.style.transform = ""; }; el.addEventListener("mousemove", onMove); el.addEventListener("mouseleave", onLeave); return () => { el.removeEventListener("mousemove", onMove); el.removeEventListener("mouseleave", onLeave); }; }, [strength]); return
{children}
; } Object.assign(window, { ArtSurface, InkField, Cursor, Reveal, Magnetic });