@arolariu/components
Version:
🎨 70+ beautiful, accessible React components built on Base UI. TypeScript-first, CSS Modules styling, tree-shakeable, SSR-ready. Perfect for modern web apps, design systems & rapid prototyping. Zero config, maximum flexibility! ⚡
146 lines (145 loc) • 5.79 kB
JavaScript
"use client";
import { jsx, jsxs } from "react/jsx-runtime";
import { motion, useAnimation } from "motion/react";
import react, { useEffect, useRef, useState } from "react";
import { cn } from "../../lib/utilities.js";
import scratcher_module from "./scratcher.module.js";
const defaultGradientColors = [
"#A97CF8",
"#F38CB8",
"#FDCC92"
];
const ignoreAnimationError = ()=>null;
const Scratcher = /*#__PURE__*/ react.forwardRef(({ width, height, minScratchPercentage = 50, onComplete, children, className, gradientColors = defaultGradientColors }, forwardedRef)=>{
const canvasRef = useRef(null);
const [isScratching, setIsScratching] = useState(false);
const [isComplete, setIsComplete] = useState(false);
const controls = useAnimation();
useEffect(()=>{
const canvas = canvasRef.current;
const context = canvas?.getContext("2d");
if (!canvas || !context) return;
context.fillStyle = "#ccc";
context.fillRect(0, 0, canvas.width, canvas.height);
const gradient = context.createLinearGradient(0, 0, canvas.width, canvas.height);
gradient.addColorStop(0, gradientColors[0]);
gradient.addColorStop(0.5, gradientColors[1]);
gradient.addColorStop(1, gradientColors[2]);
context.fillStyle = gradient;
context.fillRect(0, 0, canvas.width, canvas.height);
}, [
gradientColors
]);
const scratch = react.useCallback((clientX, clientY)=>{
const canvas = canvasRef.current;
const context = canvas?.getContext("2d");
if (!canvas || !context) return;
const rect = canvas.getBoundingClientRect();
const x = clientX - rect.left + 16;
const y = clientY - rect.top + 16;
context.globalCompositeOperation = "destination-out";
context.beginPath();
context.arc(x, y, 30, 0, 2 * Math.PI);
context.fill();
}, []);
const startAnimation = react.useCallback(async ()=>{
await controls.start({
scale: [
1,
1.5,
1
],
rotate: [
0,
10,
-10,
10,
-10,
0
],
transition: {
duration: 0.5
}
});
onComplete?.();
}, [
controls,
onComplete
]);
const checkCompletion = react.useCallback(()=>{
if (isComplete) return;
const canvas = canvasRef.current;
const context = canvas?.getContext("2d");
if (!canvas || !context) return;
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
const pixels = imageData.data;
const totalPixels = pixels.length / 4;
let clearPixels = 0;
for(let index = 3; index < pixels.length; index += 4)if (0 === pixels[index]) clearPixels += 1;
const percentage = clearPixels / totalPixels * 100;
if (percentage >= minScratchPercentage) {
setIsComplete(true);
context.clearRect(0, 0, canvas.width, canvas.height);
startAnimation().catch(ignoreAnimationError);
}
}, [
isComplete,
minScratchPercentage,
startAnimation
]);
useEffect(()=>{
const handleDocumentMouseMove = (event)=>{
if (isScratching) scratch(event.clientX, event.clientY);
};
const handleDocumentTouchMove = (event)=>{
if (!isScratching) return;
const [touch] = event.touches;
if (!touch) return;
scratch(touch.clientX, touch.clientY);
};
const handleDocumentPointerEnd = ()=>{
setIsScratching(false);
checkCompletion();
};
globalThis.document.addEventListener("mousemove", handleDocumentMouseMove);
globalThis.document.addEventListener("touchmove", handleDocumentTouchMove);
globalThis.document.addEventListener("mouseup", handleDocumentPointerEnd);
globalThis.document.addEventListener("touchend", handleDocumentPointerEnd);
globalThis.document.addEventListener("touchcancel", handleDocumentPointerEnd);
return ()=>{
globalThis.document.removeEventListener("mousemove", handleDocumentMouseMove);
globalThis.document.removeEventListener("touchmove", handleDocumentTouchMove);
globalThis.document.removeEventListener("mouseup", handleDocumentPointerEnd);
globalThis.document.removeEventListener("touchend", handleDocumentPointerEnd);
globalThis.document.removeEventListener("touchcancel", handleDocumentPointerEnd);
};
}, [
checkCompletion,
isScratching,
scratch
]);
return /*#__PURE__*/ jsxs(motion.div, {
ref: forwardedRef,
className: cn(scratcher_module.root, className),
style: {
width,
height,
cursor: "url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMCAwIDMyIDMyIj4KICA8Y2lyY2xlIGN4PSIxNiIgY3k9IjE2IiByPSIxNSIgc3R5bGU9ImZpbGw6I2ZmZjtzdHJva2U6IzAwMDtzdHJva2Utd2lkdGg6MXB4OyIgLz4KPC9zdmc+'), auto"
},
animate: controls,
children: [
/*#__PURE__*/ jsx("canvas", {
ref: canvasRef,
width: width,
height: height,
className: scratcher_module.canvas,
onMouseDown: ()=>setIsScratching(true),
onTouchStart: ()=>setIsScratching(true)
}),
children
]
});
});
Scratcher.displayName = "Scratcher";
export { Scratcher };
//# sourceMappingURL=scratcher.js.map