151 lines
4.6 KiB
TypeScript
151 lines
4.6 KiB
TypeScript
"use client"
|
|
|
|
import React, { useEffect, useState, useCallback, useMemo } from "react"
|
|
import { type LayoutChangeEvent, View } from "react-native"
|
|
import Animated, {
|
|
useSharedValue,
|
|
useAnimatedStyle,
|
|
withRepeat,
|
|
withTiming,
|
|
Easing,
|
|
withSequence,
|
|
withDelay,
|
|
} from "react-native-reanimated"
|
|
import Auto from "svg/Auto"
|
|
import Avia from "svg/Avia"
|
|
|
|
type Props = {
|
|
type: "auto" | "avia"
|
|
}
|
|
|
|
const AnimatedIcon = ({ type }: Props) => {
|
|
const translateX = useSharedValue(0)
|
|
const translateY = useSharedValue(0)
|
|
const rotateY = useSharedValue(0)
|
|
const direction = useSharedValue(1)
|
|
|
|
const [containerWidth, setContainerWidth] = useState(0)
|
|
const iconSize = 40
|
|
|
|
const onLayout = useCallback((event: LayoutChangeEvent) => {
|
|
const { width } = event.nativeEvent.layout
|
|
setContainerWidth(width)
|
|
}, []);
|
|
|
|
const animationConfig = useMemo(() => ({
|
|
duration: 4000,
|
|
easing: Easing.linear,
|
|
rotationDuration: 300,
|
|
arcHeight: -30,
|
|
}), []);
|
|
|
|
const createXAnimation = useCallback((maxX: number) => {
|
|
return withRepeat(
|
|
withSequence(
|
|
withTiming(maxX, {
|
|
duration: animationConfig.duration,
|
|
easing: animationConfig.easing
|
|
}, () => {
|
|
direction.value = -1;
|
|
rotateY.value = withTiming(180, { duration: animationConfig.rotationDuration });
|
|
}),
|
|
withTiming(0, {
|
|
duration: animationConfig.duration,
|
|
easing: animationConfig.easing
|
|
}, () => {
|
|
direction.value = 1;
|
|
rotateY.value = withTiming(0, { duration: animationConfig.rotationDuration });
|
|
})
|
|
),
|
|
-1
|
|
);
|
|
}, [animationConfig, direction, rotateY]);
|
|
|
|
const createYAnimation = useCallback(() => {
|
|
if (type === "avia") {
|
|
return withRepeat(
|
|
withSequence(
|
|
withTiming(animationConfig.arcHeight, {
|
|
duration: animationConfig.duration / 2,
|
|
easing: Easing.out(Easing.quad),
|
|
}),
|
|
withTiming(0, {
|
|
duration: animationConfig.duration / 2,
|
|
easing: Easing.in(Easing.quad),
|
|
}),
|
|
withTiming(animationConfig.arcHeight, {
|
|
duration: animationConfig.duration / 2,
|
|
easing: Easing.out(Easing.quad),
|
|
}),
|
|
withTiming(0, {
|
|
duration: animationConfig.duration / 2,
|
|
easing: Easing.in(Easing.quad),
|
|
})
|
|
),
|
|
-1
|
|
);
|
|
}
|
|
return withTiming(0, { duration: 100 });
|
|
}, [type, animationConfig]);
|
|
|
|
useEffect(() => {
|
|
if (containerWidth === 0) return;
|
|
|
|
const maxX = containerWidth - iconSize;
|
|
|
|
translateX.value = createXAnimation(maxX);
|
|
translateY.value = createYAnimation();
|
|
}, [containerWidth, type, createXAnimation, createYAnimation, translateX, translateY]);
|
|
|
|
const animatedStyle = useAnimatedStyle(() => {
|
|
return {
|
|
transform: [
|
|
{ translateX: translateX.value },
|
|
{ translateY: translateY.value },
|
|
{ rotateY: `${rotateY.value}deg` },
|
|
],
|
|
}
|
|
});
|
|
|
|
const containerStyle = useMemo(() => ({
|
|
height: 100,
|
|
justifyContent: "center" as const,
|
|
backgroundColor: "transparent" as const,
|
|
position: "relative" as const,
|
|
}), []);
|
|
|
|
const trackStyle = useMemo(() => ({
|
|
height: 2,
|
|
backgroundColor: "#28A7E850",
|
|
position: "absolute" as const,
|
|
top: 25,
|
|
left: 0,
|
|
right: 0,
|
|
}), []);
|
|
|
|
const iconContainerStyle = useMemo(() => ({
|
|
position: "absolute" as const,
|
|
top: 0,
|
|
height: iconSize,
|
|
width: iconSize,
|
|
}), [iconSize]);
|
|
|
|
const renderIcon = useMemo(() => {
|
|
if (type === "auto") {
|
|
return <Auto color="#28A7E8" width={iconSize} height={iconSize} />;
|
|
}
|
|
return <Avia color="#28A7E8" width={iconSize} height={iconSize} />;
|
|
}, [type, iconSize]);
|
|
|
|
return (
|
|
<View onLayout={onLayout} style={containerStyle}>
|
|
<View style={trackStyle} />
|
|
<Animated.View style={[iconContainerStyle, animatedStyle]}>
|
|
{renderIcon}
|
|
</Animated.View>
|
|
</View>
|
|
)
|
|
}
|
|
|
|
export default AnimatedIcon;
|