新增角色大廳(lobby)邀約與通話、附身(possession)、時鐘(clock)抽象、 時間流與睡眠負債、後日談進度、行為選擇與旁觀反應等模組,並補齊對應冒煙 測試(R~V)與單元測試;同步調整 web/mobile 對應頁面與元件。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
79 lines
2.3 KiB
TypeScript
79 lines
2.3 KiB
TypeScript
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<ViewStyle>;
|
|
}
|
|
|
|
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 (
|
|
<Svg viewBox={`0 0 ${WIDTH} 100`} style={style}>
|
|
<AnimatedPolyline
|
|
points={points}
|
|
fill="none"
|
|
stroke={color}
|
|
strokeWidth={3}
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeDasharray={DASH_LENGTH}
|
|
strokeDashoffset={dashOffset}
|
|
/>
|
|
</Svg>
|
|
);
|
|
}
|