예제 소스
SGF 기보로 학습하는 바둑 LLM 인공지능 · 파일 169개
AtariEffect.tsx
"use client";
import { useEffect, useRef, useState } from "react";
import styles from "./AtariEffect.module.css";
const ANIM_SRC = "/effects/guanyu_danshu.svg";
const SOUND_SRC = "/sounds/danshu.wav";
const SHOW_MS = 1900;
export type AtariTrigger = { id: number; who: 1 | 2 };
/** 단수 연출: 관우 청룡언월도 애니메이션 + "단수~" 소리. trigger.id 가 바뀔 때마다 처음부터 재생. */
export function AtariEffect({ trigger }: { trigger: AtariTrigger | null }) {
const [src, setSrc] = useState<string | null>(null);
const [visible, setVisible] = useState(false);
const blobRef = useRef<Blob | null>(null);
const urlRef = useRef<string | null>(null);
const audioRef = useRef<HTMLAudioElement | null>(null);
useEffect(() => {
let cancelled = false;
fetch(ANIM_SRC)
.then((res) => (res.ok ? res.blob() : null))
.then((b) => {
if (!cancelled && b) blobRef.current = b;
})
.catch(() => undefined);
const audio = new Audio(SOUND_SRC);
audio.preload = "auto";
audio.volume = 0.8;
audioRef.current = audio;
return () => {
cancelled = true;
if (urlRef.current) URL.revokeObjectURL(urlRef.current);
};
}, []);
useEffect(() => {
if (!trigger) return;
// 애니메이션 이미지는 URL이 같으면 이어서 재생되므로 매번 새 object URL로 처음부터 돌린다.
if (urlRef.current) URL.revokeObjectURL(urlRef.current);
urlRef.current = null;
if (blobRef.current) {
urlRef.current = URL.createObjectURL(blobRef.current);
setSrc(urlRef.current);
} else {
setSrc(`${ANIM_SRC}?t=${trigger.id}`);
}
setVisible(true);
const audio = audioRef.current;
if (audio) {
audio.currentTime = 0;
audio.play().catch(() => undefined);
}
const timer = window.setTimeout(() => setVisible(false), SHOW_MS);
return () => window.clearTimeout(timer);
}, [trigger]);
if (!visible || !src || !trigger) return null;
return (
<div className={styles.layer} aria-live="polite" key={trigger.id}>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img className={styles.anim} src={src} alt="" />
<div className={`${styles.word} ${trigger.who === 1 ? styles.black : styles.white}`}>
단수!
</div>
</div>
);
}