"use client"; import { motion, useMotionValue, useReducedMotion, useSpring, useTransform, type MotionValue } from "motion/react"; type ChromaticShiftProps = { src: string; alt: string; /** How far the colours part, in percent of the picture's size. */ amount?: number; /** Set the size here. */ className?: string; }; type ChannelProps = { src: string; alt: string; colour: string; sign: number; shiftX: MotionValue; shiftY: MotionValue; }; const SPLIT = { stiffness: 180, damping: 14 }; const GROW = { stiffness: 260, damping: 30 }; function Channel({ src, alt, colour, sign, shiftX, shiftY }: ChannelProps) { const x = useTransform(shiftX, (v) => `${v * sign}%`); const y = useTransform(shiftY, (v) => `${v * sign}%`); return ( {/* eslint-disable-next-line @next/next/no-img-element */} {alt}
); } /* ChromaticShift: the picture holds three colour layers that line up at rest. Under the pointer they part toward it, then spring back. */ export function ChromaticShift({ src, alt, amount = 2, className = "aspect-video" }: ChromaticShiftProps) { const reduce = useReducedMotion(); const x = useMotionValue(0); const y = useMotionValue(0); const power = useMotionValue(0); const shiftX = useSpring(x, SPLIT); const shiftY = useSpring(y, SPLIT); const smoothPower = useSpring(power, GROW); const scale = useTransform(smoothPower, [0, 1], [1, 1 + amount / 50]); const aim = (e: React.PointerEvent) => { if (reduce) return; const r = e.currentTarget.getBoundingClientRect(); const nx = ((e.clientX - r.left) / r.width) * 2 - 1; const ny = ((e.clientY - r.top) / r.height) * 2 - 1; const length = Math.hypot(nx, ny) || 1; const reach = (Math.max(length, 0.6) / length) * amount; x.set(nx * reach); y.set(ny * reach); power.set(1); }; const rest = () => { x.set(0); y.set(0); power.set(0); }; return (
); }