related-ui-components 1.6.0 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,9 @@
1
- import React, { useRef, useState } from "react";
1
+ import React, {
2
+ useRef,
3
+ useState,
4
+ useEffect,
5
+ useCallback, // Import useCallback
6
+ } from "react";
2
7
  import {
3
8
  Canvas,
4
9
  Group,
@@ -7,11 +12,13 @@ import {
7
12
  Path,
8
13
  Rect,
9
14
  Skia,
10
- LinearGradient,
11
- vec,
12
15
  useImage,
13
16
  Text,
14
17
  useFont,
18
+ notifyChange,
19
+ SkPath, // Import SkPath type
20
+ SkFont, // Import SkFont type
21
+ SkImage, // Import SkImage type
15
22
  } from "@shopify/react-native-skia";
16
23
  import {
17
24
  StyleProp,
@@ -19,15 +26,21 @@ import {
19
26
  ViewStyle,
20
27
  StyleSheet,
21
28
  ImageRequireSource,
22
- NativeTouchEvent,
29
+ LogBox, // Optional: for ignoring specific logs if needed
23
30
  } from "react-native";
31
+ // Import runOnJS and useSharedValue
32
+ import { runOnJS, useSharedValue } from "react-native-reanimated";
33
+ import { Gesture, GestureDetector } from "react-native-gesture-handler";
34
+
35
+ // Ignore specific warning if it appears, related to path mutation - use cautiously
36
+ // LogBox.ignoreLogs(['Skia: SkPath.Make()']);
24
37
 
25
38
  type ScratchCardProps = {
26
39
  style?: StyleProp<ViewStyle>;
27
40
  image?: ImageRequireSource;
28
41
  children?: React.ReactNode;
29
42
  brushStrokeWidth?: number;
30
- revealThreshold?: number;
43
+ revealThreshold?: number; // Percentage (0 to 1) - Based on bounding box
31
44
  width?: number;
32
45
  height?: number;
33
46
  backgroundColor?: string;
@@ -35,15 +48,15 @@ type ScratchCardProps = {
35
48
  textFont?: ImageRequireSource;
36
49
  textFontSize?: number;
37
50
  textFontColor?: string;
38
- onScratched?: () => void;
51
+ onScratched?: () => void;
39
52
  };
40
53
 
41
54
  const ScratchCard: React.FC<ScratchCardProps> = ({
42
55
  style,
43
56
  children,
44
- image = null,
57
+ image,
45
58
  brushStrokeWidth = 50,
46
- revealThreshold = 0.8,
59
+ revealThreshold = 0.8,
47
60
  width = 300,
48
61
  height = 300,
49
62
  backgroundColor = "#CCCCCC",
@@ -53,112 +66,162 @@ const ScratchCard: React.FC<ScratchCardProps> = ({
53
66
  textFontSize = 16,
54
67
  onScratched,
55
68
  }) => {
56
- const img = useImage(image);
57
- const font = useFont(textFont, textFontSize);
69
+ const loadedImg = useImage(image);
70
+ const loadedFont = useFont(textFont, textFontSize);
58
71
 
59
72
  const [[areaWidth, areaHeight], setSize] = useState([0, 0]);
60
73
  const [isScratched, setScratched] = useState(false);
61
- const [_, setRefresh] = useState(0);
74
+ const [isLayoutReady, setLayoutReady] = useState(false);
75
+
76
+ const isThresholdReached = useSharedValue(false);
62
77
 
63
- const path = useRef(Skia.Path.Make());
64
- const lastKnownY = useRef<number | null>(null);
78
+ const path = useSharedValue<SkPath>(Skia.Path.Make());
65
79
 
66
- //Check scratch progress
67
- const checkScratchProgress = () => {
68
- const bounds = path.current.getBounds();
69
- const scratchedArea = bounds.height * bounds.width;
70
- const totalArea = areaWidth * areaHeight;
80
+ useEffect(() => {
81
+ path.value = Skia.Path.Make();
82
+ isThresholdReached.value = false
83
+ setScratched(false);
84
+ }, [areaWidth, areaHeight]);
71
85
 
72
- if (scratchedArea / totalArea > revealThreshold && !isScratched) {
73
- setScratched(true);
74
- if (onScratched) {
75
- onScratched();
86
+ const handleLayout = useCallback((event: any) => {
87
+ const { width: newWidth, height: newHeight } = event.nativeEvent.layout;
88
+ if (newWidth > 0 && newHeight > 0) {
89
+ if (newWidth !== areaWidth || newHeight !== areaHeight) {
90
+ setSize([newWidth, newHeight]);
76
91
  }
92
+ if (!isLayoutReady) {
93
+ setLayoutReady(true);
94
+ }
95
+ } else {
96
+ setLayoutReady(false);
77
97
  }
78
- };
98
+ }, [areaWidth, areaHeight, isLayoutReady]);
79
99
 
80
- //Handle path movement on canvas
81
- const handleMove = (nativeEvent: NativeTouchEvent) => {
82
- let { locationX, locationY } = nativeEvent;
100
+ const revealCardOnJS = useCallback(() => {
101
+ setScratched(true);
102
+ onScratched?.();
103
+ }, [onScratched]);
83
104
 
84
- if (
85
- lastKnownY.current != null &&
86
- nativeEvent.locationY - lastKnownY.current > 50
87
- ) {
88
- return;
89
- }
105
+ const pan = Gesture.Pan()
106
+ .averageTouches(true)
107
+ .maxPointers(1)
108
+ .onBegin((e) => {
109
+ if (!isLayoutReady) return;
110
+ try {
111
+ const newPath = path.value.copy();
112
+ newPath.moveTo(e.x, e.y);
113
+ newPath.lineTo(e.x + 0.001, e.y + 0.001);
114
+ path.value = newPath;
115
+ notifyChange(path as any);
116
+ } catch (error) {
117
+ console.error("ScratchCard: Error in onBegin:", error);
118
+ }
119
+ })
120
+ .onChange((e) => {
121
+ if (!isLayoutReady || isThresholdReached.value) return;
122
+
123
+ try {
124
+ const newPath = path.value.copy();
125
+ newPath.lineTo(e.x, e.y);
126
+ path.value = newPath;
127
+ notifyChange(path as any);
128
+
129
+ const bounds = path.value.getBounds();
130
+
131
+ if (!bounds || areaWidth <= 0 || areaHeight <= 0) {
132
+ return;
133
+ }
90
134
 
91
- path.current.lineTo(locationX, locationY);
92
- lastKnownY.current = locationY;
135
+ const scratchedArea = bounds.width * bounds.height;
136
+ const totalArea = areaWidth * areaHeight;
93
137
 
94
- setRefresh((prev) => prev + 1);
95
- checkScratchProgress();
96
- };
138
+ if (totalArea > 0 && scratchedArea / totalArea > revealThreshold) {
139
+ if (!isThresholdReached.value) {
140
+ isThresholdReached.value = true;
141
+ runOnJS(revealCardOnJS)();
142
+ }
143
+ }
144
+ } catch (error) {
145
+ console.error("ScratchCard: Error in onChange (UI Thread):", error);
146
+ }
147
+ })
148
+
149
+ const textMetrics = React.useMemo(() => {
150
+ if (loadedFont && text && areaWidth > 0 && areaHeight > 0) {
151
+ const metrics = loadedFont.measureText(text);
152
+ const textX = areaWidth / 2 - metrics.width / 2;
153
+ const textY = areaHeight / 2 + metrics.height / 3;
154
+ return { x: textX, y: textY, width: metrics.width, height: metrics.height };
155
+ }
156
+ return null;
157
+ }, [loadedFont, text, areaWidth, areaHeight]);
158
+
159
+ const canRenderCanvas = isLayoutReady && areaWidth > 0 && areaHeight > 0;
97
160
 
98
161
  return (
99
162
  <View
100
- onLayout={(e) => {
101
- setSize([e.nativeEvent.layout.width, e.nativeEvent.layout.height]);
102
- }}
163
+ onLayout={handleLayout}
103
164
  style={[styles.container, style, { width, height }]}
104
165
  >
105
166
  <View style={styles.content}>{children}</View>
106
167
 
107
- {!isScratched && (
108
- <Canvas
109
- style={styles.canvas}
110
- onTouchStart={({ nativeEvent }) => {
111
- path.current.moveTo(nativeEvent.locationX, nativeEvent.locationY);
112
- setRefresh((prev) => prev + 1);
113
- }}
114
- onTouchMove={({ nativeEvent }) => handleMove(nativeEvent)}
115
- onTouchEnd={() => (lastKnownY.current = null)}
116
- >
117
- <Mask
118
- mode="luminance"
119
- mask={
120
- <Group>
121
- <Rect x={0} y={0} width={1000} height={1000} color="white" />
122
- <Path
123
- path={path.current}
124
- color="black"
125
- style="stroke"
126
- strokeJoin="round"
127
- strokeCap="round"
128
- strokeWidth={brushStrokeWidth}
129
- />
130
- </Group>
131
- }
132
- >
133
- {img ? (
134
- <Image
135
- image={img}
136
- fit="cover"
137
- x={0}
138
- y={0}
139
- width={areaWidth}
140
- height={areaHeight}
141
- />
142
- ) : (
143
- <Group>
144
- <Rect
168
+ {!isScratched && canRenderCanvas && (
169
+ <GestureDetector gesture={pan}>
170
+ <Canvas style={styles.canvas}>
171
+ <Mask
172
+ mode="luminance"
173
+ mask={
174
+ <Group>
175
+ <Rect
176
+ x={0}
177
+ y={0}
178
+ width={areaWidth}
179
+ height={areaHeight}
180
+ color="white"
181
+ />
182
+ <Path
183
+ path={path}
184
+ color="black"
185
+ style="stroke"
186
+ strokeJoin="round"
187
+ strokeCap="round"
188
+ strokeWidth={brushStrokeWidth}
189
+ />
190
+ </Group>
191
+ }
192
+ >
193
+ {loadedImg ? (
194
+ <Image
195
+ image={loadedImg}
196
+ fit="cover"
145
197
  x={0}
146
198
  y={0}
147
199
  width={areaWidth}
148
200
  height={areaHeight}
149
- color={backgroundColor}
150
- />
151
- <Text
152
- x={areaWidth / 2 - (font?.measureText(text).width || 0) / 2}
153
- y={areaHeight / 2 + (font?.measureText(text).height || 0) / 2}
154
- text={text}
155
- color={textFontColor}
156
- font={font}
157
201
  />
158
- </Group>
159
- )}
160
- </Mask>
161
- </Canvas>
202
+ ) : (
203
+ <Group>
204
+ <Rect
205
+ x={0}
206
+ y={0}
207
+ width={areaWidth}
208
+ height={areaHeight}
209
+ color={backgroundColor}
210
+ />
211
+ {loadedFont && textMetrics && text ? (
212
+ <Text
213
+ x={textMetrics.x}
214
+ y={textMetrics.y}
215
+ text={text}
216
+ color={textFontColor}
217
+ font={loadedFont}
218
+ />
219
+ ) : null}
220
+ </Group>
221
+ )}
222
+ </Mask>
223
+ </Canvas>
224
+ </GestureDetector>
162
225
  )}
163
226
  </View>
164
227
  );
@@ -168,8 +231,6 @@ const styles = StyleSheet.create({
168
231
  container: {
169
232
  position: "relative",
170
233
  overflow: "hidden",
171
- width: "100%",
172
- height: "100%",
173
234
  },
174
235
  content: {
175
236
  position: "absolute",
@@ -177,6 +238,7 @@ const styles = StyleSheet.create({
177
238
  left: 0,
178
239
  width: "100%",
179
240
  height: "100%",
241
+ zIndex: 1,
180
242
  },
181
243
  canvas: {
182
244
  position: "absolute",
@@ -184,7 +246,8 @@ const styles = StyleSheet.create({
184
246
  left: 0,
185
247
  width: "100%",
186
248
  height: "100%",
249
+ zIndex: 2,
187
250
  },
188
251
  });
189
252
 
190
- export default ScratchCard;
253
+ export default ScratchCard;
package/src/index.ts CHANGED
@@ -1,10 +1,10 @@
1
- // import { registerRootComponent } from 'expo';
2
- // import "react-native-reanimated";
1
+ import { registerRootComponent } from 'expo';
2
+ import "react-native-reanimated";
3
3
 
4
4
 
5
- // import App from "./app";
5
+ import App from "./app";
6
6
 
7
- // registerRootComponent(App);
7
+ registerRootComponent(App);
8
8
 
9
9
  export * from "./theme"
10
10
  export * from "./components";