react-scratch-ticket
Version:
This is a scratch ticket component, basic on React
226 lines • 8.22 kB
JavaScript
import { useCallback, useEffect, useRef, useState } from 'react';
const useReactScratchTicketController = ({ brushSize, brushType, finishPercent, maskingLayerImg, maskingLayerColor, animationDuration, onInitDone, onComplete, onResetDone }) => {
const reactScratchTicketRef = useRef(null);
const isCompletedRef = useRef(false);
const isInitDoneCalled = useRef(false);
const [isInitialized, setIsInitialized] = useState(false);
/**
* @description Completed handler, show hole image
*/
const completedHandler = useCallback(() => {
const canvas = reactScratchTicketRef.current;
if (!canvas) {
console.error('Canvas is not supported or not found');
return;
}
const ctx = canvas.getContext('2d');
if (!ctx) {
console.error('Canvas 2d is not supported or not found');
return;
}
const { width, height } = canvas;
if (animationDuration > 0) {
let alpha = 1.0;
const fadeOut = () => {
ctx.clearRect(0, 0, width, height);
ctx.fillStyle = `rgba(255, 255, 255, ${alpha})`;
ctx.fillRect(0, 0, width, height);
alpha -= 0.05;
if (alpha > 0) {
requestAnimationFrame(fadeOut);
}
else {
ctx.clearRect(0, 0, width, height);
ctx.fillStyle = 'transparent';
ctx.fillRect(0, 0, width, height);
}
};
fadeOut();
}
else {
ctx.clearRect(0, 0, width, height);
ctx.fillStyle = 'transparent';
ctx.fillRect(0, 0, width, height);
}
}, [animationDuration]);
/**
* @description Check the scratch completion
*/
const checkScratchCompletion = useCallback((ctx, width, height) => {
if (isCompletedRef.current)
return;
const imageData = ctx.getImageData(0, 0, width, height);
let count = 0;
// Because the image data is in the format of [r, g, b, a, r, g, b, a, ...]
// so we need to check the alpha value, which is the 4th value in the array
for (let i = 0; i < imageData.data.length; i += 4) {
if (imageData.data[i + 3] === 0) {
count++;
}
}
// Calculate the percentage of the scratched area
if ((count / (width * height)) * 100 > 100 - finishPercent) {
isCompletedRef.current = true;
completedHandler();
// Call the complete callback
onComplete();
}
}, [completedHandler, finishPercent, onComplete]);
/**
* @description Scratch handler
*/
const scratchHandler = useCallback(({ e, canvas, ctx }) => {
ctx.beginPath();
const { left, top } = canvas.getBoundingClientRect();
let x = 0;
let y = 0;
if (e instanceof TouchEvent) {
x = e.touches[0].clientX - left;
y = e.touches[0].clientY - top;
}
else {
x = e.clientX - left;
y = e.clientY - top;
}
ctx.globalCompositeOperation = 'destination-out';
ctx.closePath();
if (brushType === 'circle') {
ctx.arc(x, y, brushSize, 0, Math.PI * 2);
}
else {
ctx.clearRect(x - brushSize, y - brushSize, brushSize * 2, brushSize * 2);
}
ctx.fill();
checkScratchCompletion(ctx, canvas.width, canvas.height);
}, [brushSize, brushType, checkScratchCompletion]);
/**
* @description Init the scratch ticket
*/
const initScratchTicket = useCallback(({ reset = false } = {}) => {
const canvas = reactScratchTicketRef.current;
if (!canvas) {
console.error('Canvas is not supported or not found');
return;
}
const ctx = canvas.getContext('2d');
if (!ctx) {
console.error('Canvas 2d is not supported or not found');
return;
}
const { width, height } = canvas;
ctx.globalCompositeOperation = 'source-over';
if (!maskingLayerImg) {
ctx.fillStyle = maskingLayerColor;
}
else {
ctx.fillStyle = '#ddd';
}
ctx.fillRect(0, 0, width, height);
if (maskingLayerImg) {
const img = new Image();
img.crossOrigin = 'anonymous'; // avoid tainted canvas
img.src = maskingLayerImg;
img.onload = () => {
ctx.drawImage(img, 0, 0, width, height);
setIsInitialized(true);
};
}
else {
ctx.fillStyle = maskingLayerColor;
ctx.fillRect(0, 0, width, height);
setIsInitialized(true);
}
if (reset) {
isCompletedRef.current = false; // reset the completed flag
onResetDone();
}
}, [maskingLayerColor, maskingLayerImg]);
/**
* @description Init the scratch ticket
*/
useEffect(() => {
const canvas = reactScratchTicketRef.current;
if (!canvas) {
console.error('Canvas is not supported or not found');
return;
}
const ctx = canvas.getContext('2d');
if (!ctx) {
console.error('Canvas 2d is not supported or not found');
return;
}
initScratchTicket();
let isScratching = false;
const startScratching = () => {
if (!isInitialized)
return;
isScratching = true;
};
const stopScratching = () => (isScratching = false);
const scratchingHandler = (e) => {
if (isScratching) {
scratchHandler({ e, canvas, ctx });
}
};
const touchMoveHandler = (e) => {
if (isScratching && e.touches.length > 0) {
scratchHandler({ e: e.touches[0], canvas, ctx });
}
};
canvas.addEventListener('mousedown', startScratching);
canvas.addEventListener('touchstart', startScratching);
canvas.addEventListener('mouseup', stopScratching);
canvas.addEventListener('touchend', stopScratching);
canvas.addEventListener('mousemove', scratchingHandler);
canvas.addEventListener('touchmove', touchMoveHandler);
if (!isInitDoneCalled.current) {
onInitDone();
isInitDoneCalled.current = true;
}
return () => {
canvas.removeEventListener('mousedown', startScratching);
canvas.removeEventListener('touchstart', startScratching);
canvas.removeEventListener('mouseup', stopScratching);
canvas.removeEventListener('touchend', stopScratching);
canvas.removeEventListener('mousemove', scratchingHandler);
canvas.removeEventListener('touchmove', touchMoveHandler);
};
}, [initScratchTicket, maskingLayerColor, maskingLayerImg, scratchHandler, onInitDone, isInitialized]);
/**
* @description Reset handler
*/
const resetHandler = () => {
if (!reactScratchTicketRef.current) {
console.error('Canvas is not supported or not found');
return;
}
if (!isInitialized) {
console.error('The scratch ticket is not initialized');
return;
}
setIsInitialized(false);
initScratchTicket({ reset: true });
};
/**
* @description Clean all handler
*/
const cleanCardHandler = () => {
if (!reactScratchTicketRef.current) {
console.error('Canvas is not supported or not found');
return;
}
if (!isInitialized) {
console.error('The scratch ticket is not initialized');
return;
}
completedHandler();
};
return {
isInitialized,
reactScratchTicketRef,
resetHandler,
cleanCardHandler,
};
};
export default useReactScratchTicketController;
//# sourceMappingURL=useReactScratchTicketController.js.map