import { useLayoutEffect, useRef } from "react"; import { cn, getStaticImage } from "@/utils"; import { EASE_OUT, gsap, useReducedMotion } from "@/utils/motion"; import type { DialogueBubbleProps } from "./DialogueBubble.types"; const SPEAKERS = { detective: { tag: "record/称号标签-添侦探", bubble: "record/添侦探.svg", align: "left", tagClassName: "left-10 h-18", textClassName: "pl-12 pr-16", }, assistant: { tag: "record/称号标签-侦探助手", bubble: "record/侦探助手.svg", align: "right", tagClassName: "right-14 h-20", textClassName: "pl-10 pr-16", }, } as const; export function DialogueBubble({ line }: DialogueBubbleProps) { const rootRef = useRef(null); const reduced = useReducedMotion(); const speaker = SPEAKERS[line.speaker]; const fromRight = speaker.align === "right"; // 新消息从发言角色方向滑入,并带轻微回弹 useLayoutEffect(() => { const node = rootRef.current; if (!node) return; if (reduced) { gsap.set(node, { opacity: 1, x: 0, y: 0, scale: 1 }); return; } const tween = gsap.fromTo( node, { opacity: 0, x: fromRight ? 55 : -55, y: 18, scale: 0.92, }, { opacity: 1, x: 0, y: 0, scale: 1, duration: 0.55, ease: EASE_OUT, }, ); return () => { tween.kill(); }; }, [fromRight, reduced]); return (

{line.text}

); }