import React, { useRef, useState, useEffect, useCallback, // Import useCallback } from "react"; import { Canvas, Group, Image, Mask, Path, Rect, Skia, useImage, Text, useFont, notifyChange, SkPath, // Import SkPath type SkFont, // Import SkFont type SkImage, // Import SkImage type LinearGradient, } from "@shopify/react-native-skia"; import { StyleProp, View, ViewStyle, StyleSheet, ImageRequireSource, LogBox, } from "react-native"; import { runOnJS, useSharedValue } from "react-native-reanimated"; import { Gesture, GestureDetector, GestureHandlerRootView, } from "react-native-gesture-handler"; type ScratchCardProps = { style?: StyleProp; image?: ImageRequireSource; children?: React.ReactNode; brushStrokeWidth?: number; revealThreshold?: number; // Percentage (0 to 1) - Based on bounding box width?: number; height?: number; backgroundColor?: string; text?: string; textFont?: ImageRequireSource; textFontSize?: number; textFontColor?: string; onScratched?: () => void; scratched?: boolean; gradient?: { colors: string[]; start?: { x: number; y: number }; end?: { x: number; y: number }; }; }; const ScratchCard: React.FC = ({ style, children, image, brushStrokeWidth = 50, revealThreshold = 0.8, width = 300, height = 300, backgroundColor = "#CCCCCC", text = "", textFont, textFontColor = "#CCCCCC", textFontSize = 16, scratched, onScratched, gradient, }) => { const loadedImg = useImage(image); const loadedFont = useFont(textFont, textFontSize); const [[areaWidth, areaHeight], setSize] = useState([0, 0]); const [isScratched, setScratched] = useState(false); const [isLayoutReady, setLayoutReady] = useState(false); const isThresholdReached = useSharedValue(false); const path = useSharedValue(Skia.Path.Make()); useEffect(() => { if (scratched !== undefined) { setScratched(scratched); isThresholdReached.value = scratched; } }, [scratched]); useEffect(() => { if(scratched) return; path.value = Skia.Path.Make(); isThresholdReached.value = false; setScratched(false); }, [areaWidth, areaHeight]); const handleLayout = useCallback( (event: any) => { const { width: newWidth, height: newHeight } = event.nativeEvent.layout; if (newWidth > 0 && newHeight > 0) { if (newWidth !== areaWidth || newHeight !== areaHeight) { setSize([newWidth, newHeight]); } if (!isLayoutReady) { setLayoutReady(true); } } else { setLayoutReady(false); } }, [areaWidth, areaHeight, isLayoutReady] ); const revealCardOnJS = useCallback(() => { setScratched(true); onScratched?.(); }, [onScratched]); const pan = Gesture.Pan() .averageTouches(true) .maxPointers(1) .onBegin((e) => { if (!isLayoutReady) return; try { const newPath = path.value.copy(); newPath.moveTo(e.x, e.y); newPath.lineTo(e.x + 0.001, e.y + 0.001); path.value = newPath; notifyChange(path as any); } catch (error) { console.error("ScratchCard: Error in onBegin:", error); } }) .onChange((e) => { if (!isLayoutReady || isThresholdReached.value) return; try { const newPath = path.value.copy(); newPath.lineTo(e.x, e.y); path.value = newPath; notifyChange(path as any); const bounds = path.value.getBounds(); if (!bounds || areaWidth <= 0 || areaHeight <= 0) { return; } const scratchedArea = bounds.width * bounds.height; const totalArea = areaWidth * areaHeight; if (totalArea > 0 && scratchedArea / totalArea > revealThreshold) { if (!isThresholdReached.value) { isThresholdReached.value = true; runOnJS(revealCardOnJS)(); } } } catch (error) { console.error("ScratchCard: Error in onChange (UI Thread):", error); } }); const textMetrics = React.useMemo(() => { if (loadedFont && text && areaWidth > 0 && areaHeight > 0) { const metrics = loadedFont.measureText(text); const textX = areaWidth / 2 - metrics.width / 2; const textY = areaHeight / 2 + metrics.height / 3; return { x: textX, y: textY, width: metrics.width, height: metrics.height, }; } return null; }, [loadedFont, text, areaWidth, areaHeight]); const canRenderCanvas = isLayoutReady && areaWidth > 0 && areaHeight > 0; return ( {canRenderCanvas && {children}} {!isScratched && canRenderCanvas && ( } > {loadedImg ? ( ) : gradient ? ( {loadedFont && textMetrics && text ? ( ) : null} ) : ( {loadedFont && textMetrics && text ? ( ) : null} )} )} ); }; const styles = StyleSheet.create({ container: { position: "relative", overflow: "hidden", }, content: { position: "absolute", top: 0, left: 0, width: "100%", height: "100%", zIndex: 1, }, canvas: { position: "absolute", top: 0, left: 0, width: "100%", height: "100%", zIndex: 2, }, }); export default ScratchCard;