Skip to content

Commit 47513af

Browse files
committed
Add opacity transition to both cutout and tooltip
1 parent 0c39827 commit 47513af

File tree

2 files changed

+71
-33
lines changed

2 files changed

+71
-33
lines changed

ts/features/tour/components/TourOverlay.tsx

Lines changed: 56 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
/* eslint-disable functional/immutable-data */
22
import { useCallback, useEffect, useRef, useState } from "react";
33
import { 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";
511
import 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

2329
const OVERLAY_COLOR = "rgba(0,0,0,0.5)";
2430
const 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

2835
const { 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>

ts/features/tour/components/TourTooltip.tsx

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ import {
99
} from "@pagopa/io-app-design-system";
1010
import { useCallback, useState } from "react";
1111
import { Dimensions, StyleSheet, View } from "react-native";
12+
import Animated, {
13+
SharedValue,
14+
useAnimatedStyle
15+
} from "react-native-reanimated";
1216
import { useSafeAreaInsets } from "react-native-safe-area-context";
1317
import I18n from "i18next";
1418
import { useIODispatch, useIOSelector } from "../../../store/hooks";
@@ -27,14 +31,16 @@ type Props = {
2731
description: string;
2832
stepIndex: number;
2933
totalSteps: number;
34+
opacity: SharedValue<number>;
3035
};
3136

3237
export const TourTooltip = ({
3338
itemMeasurement,
3439
title,
3540
description,
3641
stepIndex,
37-
totalSteps
42+
totalSteps,
43+
opacity
3844
}: Props) => {
3945
const dispatch = useIODispatch();
4046
const theme = useIOTheme();
@@ -90,11 +96,16 @@ export const TourTooltip = ({
9096
dispatch(prevTourStepAction());
9197
}, [dispatch]);
9298

99+
const animatedStyle = useAnimatedStyle(() => ({
100+
opacity: opacity.value
101+
}));
102+
93103
return (
94-
<View
104+
<Animated.View
95105
style={[
96106
styles.container,
97-
{ top: tooltipTop, left: tooltipLeft, width: tooltipWidth }
107+
{ top: tooltipTop, left: tooltipLeft, width: tooltipWidth },
108+
animatedStyle
98109
]}
99110
onLayout={e => setTooltipHeight(e.nativeEvent.layout.height)}
100111
pointerEvents="box-none"
@@ -150,7 +161,7 @@ export const TourTooltip = ({
150161
</View>
151162
</View>
152163
</View>
153-
</View>
164+
</Animated.View>
154165
);
155166
};
156167

0 commit comments

Comments
 (0)