Skip to content

Commit

Permalink
feat: scratch to reveal
Browse files Browse the repository at this point in the history
  • Loading branch information
wkylin committed Jan 9, 2025
1 parent c17a9f1 commit 7c38ff4
Show file tree
Hide file tree
Showing 3 changed files with 164 additions and 4 deletions.
144 changes: 144 additions & 0 deletions src/components/stateless/ScratchToReveal/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import React, { useRef, useEffect, useState } from 'react'

Check failure on line 1 in src/components/stateless/ScratchToReveal/index.jsx

View workflow job for this annotation

GitHub Actions / Qodana for JS

ESLint

ESLint: Install the 'eslint' package
import clsx from 'clsx'
import { motion, useAnimation } from 'motion/react'

const ScratchToReveal = ({ width, height, minScratchPercentage = 50, onComplete, children, className }) => {
const canvasRef = useRef(null)
const [isScratching, setIsScratching] = useState(false)
const [isComplete, setIsComplete] = useState(false) // New state to track completion

const controls = useAnimation()

useEffect(() => {
const canvas = canvasRef.current
const ctx = canvas?.getContext('2d')
if (canvas && ctx) {
const gradient = ctx.createLinearGradient(0, 0, canvas.width, canvas.height)
gradient.addColorStop(0, '#A97CF8')
gradient.addColorStop(0.5, '#F38CB8')
gradient.addColorStop(1, '#FDCC92')
ctx.fillStyle = gradient
ctx.fillRect(0, 0, canvas.width, canvas.height)
}
}, [])

useEffect(() => {
const handleDocumentMouseMove = (event) => {
if (!isScratching) return
scratch(event.clientX, event.clientY)
}

const handleDocumentTouchMove = (event) => {
if (!isScratching) return
const 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, clientY) => {
const canvas = canvasRef.current
const ctx = canvas?.getContext('2d')
if (canvas && ctx) {
const rect = canvas.getBoundingClientRect()
const x = clientX - rect.left + 16
const y = clientY - rect.top + 16
ctx.globalCompositeOperation = 'destination-out'
ctx.beginPath()
ctx.arc(x, y, 30, 0, Math.PI * 2)
ctx.fill()
}
}

const checkCompletion = () => {
if (isComplete) return // Check if already completed

const canvas = canvasRef.current
const ctx = canvas?.getContext('2d')
if (canvas && ctx) {
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
const pixels = imageData.data
const totalPixels = pixels.length / 4
let clearPixels = 0

for (let i = 3; i < pixels.length; i += 4) {
if (pixels[i] === 0) clearPixels++
}

const percentage = (clearPixels / totalPixels) * 100

if (percentage >= minScratchPercentage) {
setIsComplete(true)
ctx.clearRect(0, 0, canvas.width, canvas.height)
startAnimation()
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.div

Check notice on line 121 in src/components/stateless/ScratchToReveal/index.jsx

View workflow job for this annotation

GitHub Actions / Qodana for JS

Unresolved JSX component

Unresolved component motion.div
className={clsx('relative select-none', className)}
style={{
width,
height,
cursor:
"url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMCAwIDMyIDMyIj4KICA8Y2lyY2xlIGN4PSIxNiIgY3k9IjE2IiByPSIxNSIgc3R5bGU9ImZpbGw6I2ZmZjtzdHJva2U6IzAwMDtzdHJva2Utd2lkdGg6MXB4OyIgLz4KPC9zdmc+'), auto",
}}
animate={controls}
>
<canvas
ref={canvasRef}
width={width}
height={height}
className="absolute top-0 left-0"
onMouseDown={handleMouseDown}
onTouchStart={handleTouchStart}
></canvas>
{children}
</motion.div>
)
}

export default ScratchToReveal
20 changes: 18 additions & 2 deletions src/pages/home/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import TagCloud from '@stateless/TagCloud'
import ShiCode from '@stateless/ShiCode'
import StaticStepper from '@stateless/StaticStepper'
import FeatureFourImages from '@stateless/FeatureAny'
import ScratchToReveal from '@stateless/ScratchToReveal'
// import SlideLinear from '@stateless/SlideLinear'
// import Masonry from '@container/masonryContainer'
import DynamicBackground from '@stateless/DynamicBackground'
Expand Down Expand Up @@ -250,6 +251,7 @@ const Home = () => {
<section style={{ marginBottom: 15, fontSize: 20 }}>
<BlurText text="Isn't this so cool?!" delay={50} />
</section>

<section
style={{
marginBottom: 15,
Expand All @@ -269,11 +271,11 @@ const Home = () => {
secondImage={secondImage}
firstImageClassName="object-cover object-left-top"
secondImageClassName="object-cover object-left-top"
className="h-[160px]"
className="h-[200px]"
slideMode="drag"
/>
</section>
<section style={{ margin: '20px 0', width: 360, height: 160, background: '#000' }}>
<section style={{ margin: '20px 0', width: 360, height: 200, background: '#000' }}>
<SquaresGrid
speed={0.5}
squareSize={20}
Expand Down Expand Up @@ -399,6 +401,20 @@ const Home = () => {
<section style={{ marginBottom: 40, height: 200, width: 360, overflow: 'hidden' }}>
<MeshGradientBackground />
</section>
<section
style={{
marginBottom: 20,
}}
>
<ScratchToReveal
width={360}
height={200}
minScratchPercentage={70}
className="flex items-center justify-center overflow-hidden bg-gray-100 border-2 rounded-2xl"
>
<p className="text-9xl"></p>
</ScratchToReveal>
</section>
<section style={{ marginBottom: 40 }}>
<AnimateRipple>Click Me</AnimateRipple>
</section>
Expand Down
4 changes: 2 additions & 2 deletions src/pages/motion/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -223,9 +223,9 @@ const ParallaxVert = () => {
left: 0,
width: '100%',
height: 3,
backgroundColor: '#aaa',
backgroundImage: 'linear-gradient(108deg,#0894ff,#ff2e54 70%,#ff9004)',
borderRadius: '3px',
// scaleX: scrollYProgress,
transformOrigin: 'left',
scaleX: scaleX,
}}
></motion.div>
Expand Down

0 comments on commit 7c38ff4

Please sign in to comment.