import { useEffect, useMemo, useRef } from "react"; import { Animated, Easing, type StyleProp, type ViewStyle } from "react-native"; import Svg, { Polyline } from "react-native-svg"; import { useReduceMotion } from "../lib/reduce-motion"; const WIDTH = 400; const MID_Y = 50; const DASH_LENGTH = 600; // 與 apps/web/components/HeartbeatWave.tsx 相同的波形算法,維持三端視覺一致; // 親密度越高,波峰振幅越大。 function buildPoints(amplitude: number): string { const clamped = Math.max(0, Math.min(100, amplitude)); const scale = 0.4 + (clamped / 100) * 1.1; const up1 = MID_Y - 15 * scale; const down = MID_Y + 26 * scale; const up2 = MID_Y - 22 * scale; const points: Array<[number, number]> = [ [0, MID_Y], [150, MID_Y], [168, MID_Y], [178, up1], [188, down], [198, up2], [208, MID_Y], [WIDTH, MID_Y], ]; return points.map(([x, y]) => `${x},${y.toFixed(1)}`).join(" "); } const AnimatedPolyline = Animated.createAnimatedComponent(Polyline); export interface HeartbeatWaveProps { amplitude?: number; // 0~100,對應親密度 animateOnMount?: boolean; // 是否播放「由左至右畫出」的載入動畫 color?: string; style?: StyleProp; } export function HeartbeatWave({ amplitude = 50, animateOnMount = true, color = "#FF7E9D", style }: HeartbeatWaveProps) { const reduceMotion = useReduceMotion(); const shouldAnimate = animateOnMount && !reduceMotion; const dashOffset = useRef(new Animated.Value(shouldAnimate ? DASH_LENGTH : 0)).current; useEffect(() => { if (!shouldAnimate) { dashOffset.setValue(0); return; } dashOffset.setValue(DASH_LENGTH); Animated.timing(dashOffset, { toValue: 0, duration: 1600, easing: Easing.out(Easing.ease), // strokeDashoffset 不在原生驅動支援的屬性清單內,只能走 JS 驅動。 useNativeDriver: false, }).start(); }, [shouldAnimate, dashOffset]); const points = useMemo(() => buildPoints(amplitude), [amplitude]); return ( ); }