"use client"; import { useState } from "react"; import { motion, useReducedMotion } from "motion/react"; export type MaskTransitionItem = { src: string; alt: string }; type MaskTransitionShape = "circle" | "diamond" | "wipe"; type MaskTransitionProps = { /** Two or more pictures. */ images: MaskTransitionItem[]; /** Shape of the opening. Circle and diamond open from the click point. */ shape?: MaskTransitionShape; /** Seconds one change takes. */ duration?: number; /** Name read out by screen readers. */ label?: string; /** Set the size here. */ className?: string; }; const mask = (shape: MaskTransitionShape, x: number, y: number, open: boolean) => { if (shape === "wipe") return `inset(0% ${open ? 0 : 100}% 0% 0%)`; if (shape === "circle") return `circle(${open ? 150 : 0}% at ${x}% ${y}%)`; const r = open ? 200 : 0; return `polygon(${x}% ${y - r}%, ${x + r}% ${y}%, ${x}% ${y + r}%, ${x - r}% ${y}%)`; }; /* MaskTransition: click the picture, or use the arrow keys, and the next one opens through a shape over the old one. */ export function MaskTransition({ images, shape = "circle", duration = 0.9, label = "Pictures", className = "aspect-video" }: MaskTransitionProps) { const reduce = useReducedMotion(); const [view, setView] = useState({ now: 0, before: 0, x: 50, y: 50, run: 0 }); const go = (dir: number, x = 50, y = 50) => { setView(({ now, run }) => ({ now: (now + dir + images.length) % images.length, before: now, x, y, run: run + 1 })); }; const onClick = (e: React.MouseEvent) => { const r = e.currentTarget.getBoundingClientRect(); go(1, ((e.clientX - r.left) / r.width) * 100, ((e.clientY - r.top) / r.height) * 100); }; const onKey = (e: React.KeyboardEvent) => { if (e.key === "ArrowRight") go(1); else if (e.key === "ArrowLeft") go(-1); else return; e.preventDefault(); }; return (
{/* eslint-disable @next/next/no-img-element */} {images.map((image, i) => ( ))} {/* eslint-enable @next/next/no-img-element */} Picture {view.now + 1} of {images.length}: {images[view.now].alt}
); }