11/* eslint-disable functional/immutable-data */
22import { useCallback , useEffect , useRef , useState } from "react" ;
33import { Dimensions , StyleSheet , View } from "react-native" ;
4- import { Canvas , DiffRect , rect , rrect } from "@shopify/react-native-skia" ;
4+ import {
5+ Canvas ,
6+ Group ,
7+ Paint ,
8+ Rect ,
9+ RoundedRect
10+ } from "@shopify/react-native-skia" ;
511import Animated , {
12+ Easing ,
613 runOnJS ,
714 useAnimatedStyle ,
8- useDerivedValue ,
915 useSharedValue ,
1016 withTiming
1117} from "react-native-reanimated" ;
@@ -22,8 +28,9 @@ import { TourTooltip } from "./TourTooltip";
2228
2329const OVERLAY_COLOR = "rgba(0,0,0,0.5)" ;
2430const CUTOUT_BORDER_RADIUS = 8 ;
25- const CUTOUT_PADDING = 4 ;
26- const ANIMATION_DURATION = 300 ;
31+ const CUTOUT_PADDING = 0 ;
32+ const ANIMATION_DURATION = 350 ;
33+ const STEP_EASING = Easing . inOut ( Easing . exp ) ;
2734
2835const { width : SCREEN_WIDTH , height : SCREEN_HEIGHT } = Dimensions . get ( "screen" ) ;
2936
@@ -46,6 +53,7 @@ export const TourOverlay = () => {
4653 const cutoutW = useSharedValue ( 0 ) ;
4754 const cutoutH = useSharedValue ( 0 ) ;
4855 const opacity = useSharedValue ( 0 ) ;
56+ const cutoutOpacity = useSharedValue ( 1 ) ;
4957
5058 const [ measurement , setMeasurement ] = useState <
5159 TourItemMeasurement | undefined
@@ -112,27 +120,39 @@ export const TourOverlay = () => {
112120 height : m . height + CUTOUT_PADDING * 2
113121 } ;
114122
115- setMeasurement ( padded ) ;
116- setTooltipConfig ( getConfig ( groupId , currentItem . index ) ) ;
123+ const config = getConfig ( groupId , currentItem . index ) ;
117124
118125 if ( isFirstMeasurement . current ) {
119126 // First step: position cutout immediately, then fade the overlay in
120127 isFirstMeasurement . current = false ;
128+ setMeasurement ( padded ) ;
129+ setTooltipConfig ( config ) ;
121130 cutoutX . value = padded . x ;
122131 cutoutY . value = padded . y ;
123132 cutoutW . value = padded . width ;
124133 cutoutH . value = padded . height ;
134+ cutoutOpacity . value = 1 ;
125135 opacity . value = withTiming ( 1 , { duration : ANIMATION_DURATION } ) ;
126136 } else {
127- // Subsequent steps: animate cutout to the new position
128- cutoutX . value = withTiming ( padded . x , { duration : ANIMATION_DURATION } ) ;
129- cutoutY . value = withTiming ( padded . y , { duration : ANIMATION_DURATION } ) ;
130- cutoutW . value = withTiming ( padded . width , {
131- duration : ANIMATION_DURATION
132- } ) ;
133- cutoutH . value = withTiming ( padded . height , {
134- duration : ANIMATION_DURATION
135- } ) ;
137+ // Subsequent steps: fade out cutout, reposition, then fade back in
138+ const updateStepUI = ( ) => {
139+ setMeasurement ( padded ) ;
140+ setTooltipConfig ( config ) ;
141+ } ;
142+ cutoutOpacity . value = withTiming (
143+ 0 ,
144+ { duration : ANIMATION_DURATION } ,
145+ ( ) => {
146+ cutoutX . value = padded . x ;
147+ cutoutY . value = padded . y ;
148+ cutoutW . value = padded . width ;
149+ cutoutH . value = padded . height ;
150+ runOnJS ( updateStepUI ) ( ) ;
151+ cutoutOpacity . value = withTiming ( 1 , {
152+ duration : ANIMATION_DURATION
153+ } ) ;
154+ }
155+ ) ;
136156 }
137157 } , [
138158 groupId ,
@@ -145,6 +165,7 @@ export const TourOverlay = () => {
145165 cutoutY ,
146166 cutoutW ,
147167 cutoutH ,
168+ cutoutOpacity ,
148169 opacity
149170 ] ) ;
150171
@@ -154,19 +175,6 @@ export const TourOverlay = () => {
154175 }
155176 } , [ visible , measureCurrentStep ] ) ;
156177
157- // Skia DiffRect: outer = full screen, inner = animated cutout (runs on UI thread)
158- const outerRect = useDerivedValue ( ( ) =>
159- rrect ( rect ( 0 , 0 , SCREEN_WIDTH , SCREEN_HEIGHT ) , 0 , 0 )
160- ) ;
161-
162- const innerRect = useDerivedValue ( ( ) =>
163- rrect (
164- rect ( cutoutX . value , cutoutY . value , cutoutW . value , cutoutH . value ) ,
165- CUTOUT_BORDER_RADIUS ,
166- CUTOUT_BORDER_RADIUS
167- )
168- ) ;
169-
170178 const animatedContainerStyle = useAnimatedStyle ( ( ) => ( {
171179 opacity : opacity . value
172180 } ) ) ;
@@ -182,7 +190,25 @@ export const TourOverlay = () => {
182190 pointerEvents = { isActive ? "auto" : "none" }
183191 >
184192 < Canvas style = { styles . overlay } pointerEvents = "none" >
185- < DiffRect outer = { outerRect } inner = { innerRect } color = { OVERLAY_COLOR } />
193+ < Group layer = { < Paint /> } >
194+ < Rect
195+ x = { 0 }
196+ y = { 0 }
197+ width = { SCREEN_WIDTH }
198+ height = { SCREEN_HEIGHT }
199+ color = { OVERLAY_COLOR }
200+ />
201+ < Group blendMode = "dstOut" opacity = { cutoutOpacity } >
202+ < RoundedRect
203+ x = { cutoutX }
204+ y = { cutoutY }
205+ width = { cutoutW }
206+ height = { cutoutH }
207+ r = { CUTOUT_BORDER_RADIUS }
208+ color = "black"
209+ />
210+ </ Group >
211+ </ Group >
186212 </ Canvas >
187213 { measurement && tooltipConfig && (
188214 < TourTooltip
@@ -191,6 +217,7 @@ export const TourOverlay = () => {
191217 description = { tooltipConfig . description }
192218 stepIndex = { stepIndex }
193219 totalSteps = { items . length }
220+ opacity = { cutoutOpacity }
194221 />
195222 ) }
196223 </ Animated . View >
0 commit comments