Getting Started
Frameworks
Components
Buttons
Text Animations
Backgrounds
Device Mocks
Scratch To Reveal
The ScratchToReveal component creates an interactive scratch-off effect with customizable dimensions and animations, revealing hidden content beneath.
Usage
Code
import React, { useEffect, useRef, useState } from 'react'import { motion, useAnimation } from 'framer-motion'import { cnBase } from 'tailwind-variants'export interface ScratchToRevealProps {children: React.ReactNodewidth: numberheight: numberminScratchPercentage?: number // Minimum percentage of scratched area to be considered as completed (Value between 0 and 100)className?: stringonComplete?: () => void}export const ScratchToReveal: React.FC<ScratchToRevealProps> = ({width,height,minScratchPercentage = 50,onComplete,children,className,}) => {const canvasRef = useRef<HTMLCanvasElement>(null)const [isScratching, setIsScratching] = useState(false)const [isComplete, setIsComplete] = useState(false) // New state to track completionconst controls = useAnimation()useEffect(() => {const canvas = canvasRef.currentconst ctx = canvas?.getContext('2d')if (canvas && ctx) {ctx.fillStyle = '#ccc'ctx.fillRect(0, 0, canvas.width, canvas.height)// Optionally add a background image or gradientconst gradient = ctx.createLinearGradient(0,0,canvas.width,canvas.height,)gradient.addColorStop(0, '#A97CF8')gradient.addColorStop(0.5, '#F38CB8')gradient.addColorStop(1, '#FDCC92')ctx.fillStyle = gradientctx.fillRect(0, 0, canvas.width, canvas.height)}}, [])useEffect(() => {const handleDocumentMouseMove = (event: MouseEvent) => {if (!isScratching)returnscratch(event.clientX, event.clientY)}const handleDocumentTouchMove = (event: TouchEvent) => {if (!isScratching)returnconst touch = event.touches[0]scratch(touch.clientX, touch.clientY)}const handleDocumentMouseUp = () => {setIsScratching(false)checkCompletion()}const handleDocumentTouchEnd = () => {setIsScratching(false)checkCompletion()}document.addEventListener('mousedown', handleDocumentMouseMove)document.addEventListener('mousemove', handleDocumentMouseMove)document.addEventListener('touchstart', handleDocumentTouchMove)document.addEventListener('touchmove', handleDocumentTouchMove)document.addEventListener('mouseup', handleDocumentMouseUp)document.addEventListener('touchend', handleDocumentTouchEnd)document.addEventListener('touchcancel', handleDocumentTouchEnd)return () => {document.removeEventListener('mousedown', handleDocumentMouseMove)document.removeEventListener('mousemove', handleDocumentMouseMove)document.removeEventListener('touchstart', handleDocumentTouchMove)document.removeEventListener('touchmove', handleDocumentTouchMove)document.removeEventListener('mouseup', handleDocumentMouseUp)document.removeEventListener('touchend', handleDocumentTouchEnd)document.removeEventListener('touchcancel', handleDocumentTouchEnd)}}, [isScratching])const handleMouseDown = () => setIsScratching(true)const handleTouchStart = () => setIsScratching(true)const scratch = (clientX: number, clientY: number) => {const canvas = canvasRef.currentconst ctx = canvas?.getContext('2d')if (canvas && ctx) {const rect = canvas.getBoundingClientRect()const x = clientX - rect.left + 16 // offset to position the scratched circle with cursorconst y = clientY - rect.top + 16ctx.globalCompositeOperation = 'destination-out'ctx.beginPath()ctx.arc(x, y, 30, 0, Math.PI * 2)ctx.fill()}}const checkCompletion = () => {if (isComplete)return // Check if already completedconst canvas = canvasRef.currentconst ctx = canvas?.getContext('2d')if (canvas && ctx) {const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)const pixels = imageData.dataconst totalPixels = pixels.length / 4let clearPixels = 0for (let i = 3; i < pixels.length; i += 4) {if (pixels[i] === 0)clearPixels++}const percentage = (clearPixels / totalPixels) * 100if (percentage >= minScratchPercentage) {setIsComplete(true) // Set complete flagctx.clearRect(0, 0, canvas.width, canvas.height) // Clear the canvas to reveal everythingstartAnimation()if (onComplete)onComplete()}}}const startAnimation = () => {controls.start({scale: [1, 1.5, 1],rotate: [0, 10, -10, 10, -10, 0],transition: { duration: 0.5 },})}return (<motion.divclassName={cnBase('relative select-none', className)}style={{width,height,cursor:'url(\'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMCAwIDMyIDMyIj4KICA8Y2lyY2xlIGN4PSIxNiIgY3k9IjE2IiByPSIxNSIgc3R5bGU9ImZpbGw6I2ZmZjtzdHJva2U6IzAwMDtzdHJva2Utd2lkdGg6MXB4OyIgLz4KPC9zdmc+\'), auto',}}animate={controls}><canvasref={canvasRef}width={width}height={height}className="absolute left-0 top-0"onMouseDown={handleMouseDown}onTouchStart={handleTouchStart}></canvas>{children}</motion.div>)}
Props
Attribute | Type | Description | Default |
---|---|---|---|
className | string | The class name to be applied to the component. | undefined |
children | ReactNode | The content to be displayed inside. | undefined |
width | number | Width of the scratch container. | undefined |
height | number | Height of the scratch container. | undefined |
minScratchPercentage | number | Minimum percentage of scratched area to be considered as completed (Value between 0 and 100). | 50 |
onCompete | function | Callback function called when scratch is completed | undefined |
On this page