102 lines
2.2 KiB
TypeScript
102 lines
2.2 KiB
TypeScript
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<HTMLDivElement>(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 (
|
|
<div
|
|
ref={rootRef}
|
|
className={cn(
|
|
"flex w-full flex-col",
|
|
fromRight ? "items-end" : "items-start",
|
|
)}
|
|
>
|
|
<div className="relative min-h-35 w-fit max-w-full shrink-0 ">
|
|
<img
|
|
src={getStaticImage(speaker.bubble)}
|
|
alt=""
|
|
aria-hidden="true"
|
|
draggable={false}
|
|
className="pointer-events-none absolute inset-0 h-full w-full max-w-none select-none"
|
|
/>
|
|
|
|
<img
|
|
src={getStaticImage(speaker.tag)}
|
|
alt=""
|
|
aria-hidden="true"
|
|
draggable={false}
|
|
className={cn(
|
|
"absolute -top-10 z-10 max-w-none select-none",
|
|
speaker.tagClassName,
|
|
)}
|
|
/>
|
|
|
|
<p
|
|
className={cn(
|
|
"relative flex h-full items-center whitespace-nowrap text-[28px] leading-normal text-[#5b4632]",
|
|
speaker.textClassName,
|
|
)}
|
|
>
|
|
{line.text}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|