commit 1115363eb5853c4731f8661d87999215dd14d788
parent c940ffcc1f2d96058b4cb7fcd123cf1cc6c337ee
Author: minerva-jupiter <ryouturn@gmail.com>
Date: Sat, 15 Nov 2025 12:11:26 +0900
refactor(audio): Upgrade audio assets from testAudio to productionAudio
Diffstat:
26 files changed, 421 insertions(+), 401 deletions(-)
diff --git a/app/components/1_chat.tsx b/app/components/1_chat.tsx
@@ -69,7 +69,7 @@ const FirstChat: React.FC<StageProps> = ({ onComplete }) => {
};
export default FirstChat;
const AUDIO_SOURCES: Record<number, string> = {
- 1: "/audio/001.wav", // 再生される音源はこれのみ
+ 1: "/audio/001.flac", // 再生される音源はこれのみ
};
interface Message {
id: number;
diff --git a/app/components/2_draw.tsx b/app/components/2_draw.tsx
@@ -1,380 +1,395 @@
-"use client";
-import React, { useCallback, useEffect, useRef, useState } from "react";
-import init, {
- find_nearest_point_on_path,
- NearestPointResult,
-} from "../../rust-wasm/pkg/rust_wasm.js";
-import { StageProps } from "../ctrl/page.tsx";
-type Tool = "pen";
-type Point = { x: number; y: number };
-interface LineData {
- id: number;
- tool: Tool;
- points: Point[];
- targetPoints: Point[];
-}
-const BACKGROUND_SVG_PATH_D_DEFAULT = "";
-const VIEWSBOX_SIZE = 500;
-const SNAPPING_DISTANCE_PIXELS = 30;
-const AUDIO_SOURCE = "/audio/001.wav";
-export default function DrawingApp({ onComplete }: StageProps) {
- const [isClient, setIsClient] = useState(false);
- const lineIdCounter = useRef(0);
- const [lines, setLines] = useState<LineData[]>([]);
- const [currentLines, setCurrentLines] = useState<LineData[]>([]);
- const [stageWidth, setStageWidth] = useState(0);
- const [stageHeight, setStageHeight] = useState(0);
- const [backgroundPathD, setBackgroundPathD] = useState(
- BACKGROUND_SVG_PATH_D_DEFAULT,
- );
- const [viewBoxSize, setViewBoxSize] = useState(VIEWSBOX_SIZE);
- const isDrawing = useRef(false);
- const animationRef = useRef<number>(0);
- const startTimeRef = useRef<number | undefined>(undefined);
- const stageRef = useRef<HTMLDivElement>(null);
- useEffect(() => {
- init();
- }, []);
- // --- 1. サイズ計算とステージ設定 (画面全体を使用) ---
- useEffect(() => {
- setIsClient(true);
- const handleResize = () => {
- if (typeof globalThis !== "undefined") {
- setStageWidth(globalThis.innerWidth);
- setStageHeight(globalThis.innerHeight);
- }
- };
- handleResize();
- globalThis.addEventListener("resize", handleResize);
- return () => {
- globalThis.removeEventListener("resize", handleResize);
- };
- }, []);
- // --- 2. 座標変換ユーティリティ ---
- const scaleToViewBox = useCallback((p: Point): Point => {
- if (stageWidth === 0 || stageHeight === 0) return p;
- const effectiveSize = Math.min(stageWidth, stageHeight);
- const scale = viewBoxSize / effectiveSize;
- const offsetX = (stageWidth - effectiveSize) / 2;
- const offsetY = (stageHeight - effectiveSize) / 2;
- return {
- x: (p.x - offsetX) * scale,
- y: (p.y - offsetY) * scale,
- };
- }, [stageWidth, stageHeight, viewBoxSize]);
- const scaleToScreen = useCallback((p: Point): Point => {
- if (stageWidth === 0 || stageHeight === 0) return p;
- const effectiveSize = Math.min(stageWidth, stageHeight);
- const scale = effectiveSize / viewBoxSize;
- const offsetX = (stageWidth - effectiveSize) / 2;
- const offsetY = (stageHeight - effectiveSize) / 2;
- return {
- x: p.x * scale + offsetX,
- y: p.y * scale + offsetY,
- };
- }, [stageWidth, stageHeight, viewBoxSize]);
- const getPointerPosition = useCallback(
- (
- e: React.MouseEvent | React.TouchEvent | MouseEvent | TouchEvent,
- ): Point | null => {
- if (!stageRef.current) return null;
- const rect = stageRef.current.getBoundingClientRect();
- let clientX: number, clientY: number;
- if ("touches" in e) {
- if (e.touches.length === 0) return null;
- clientX = e.touches[0].clientX;
- clientY = e.touches[0].clientY;
- } else {
- clientX = e.clientX;
- clientY = e.clientY;
- }
-
- return { x: clientX - rect.left, y: clientY - rect.top };
- },
- [],
- );
- // --- 3. SVGコンテンツの取得ロジック (DOMParser) ---
- useEffect(() => {
- const fetchAndParseSvg = async () => {
- try {
- const response = await fetch("/whiteperson.svg");
- const svgText = await response.text();
- const parser = new DOMParser();
- const doc = parser.parseFromString(svgText, "image/svg+xml");
- const svgElement = doc.querySelector("svg");
- if (svgElement) {
- const viewBoxAttr = svgElement.getAttribute("viewBox");
- if (viewBoxAttr) {
- const parts = viewBoxAttr.trim().split(/\s+/);
- if (parts.length === 4) {
- const size = Math.max(
- parseFloat(parts[2]),
- parseFloat(parts[3]),
- );
- if (!isNaN(size) && size > 0) {
- setViewBoxSize(size);
- }
- }
- }
- }
- const pathElements = doc.querySelectorAll("path");
- let foundDValue = null;
- for (const element of Array.from(pathElements)) {
- const dValue = element.getAttribute("d");
- if (dValue && dValue.trim().length > 0) {
- foundDValue = dValue;
- break;
- }
- }
- if (foundDValue) {
- setBackgroundPathD(foundDValue);
- return;
- }
- console.error(
- "Error: <path> element or 'd' attribute not found in whiteperson.svg.",
- );
- } catch (error) {
- console.error(
- "Failed to load or parse whiteperson.svg:",
- error,
- );
- }
- };
- if (isClient && backgroundPathD === "") {
- fetchAndParseSvg();
- }
- }, [isClient, backgroundPathD]);
- // --- 4. 吸着ロジック (WASM呼び出し) ---
- const calculateNearestTargetPoint = useCallback((p: Point): Point => {
- const p_viewbox = scaleToViewBox(p);
- const effectiveSize = Math.min(stageWidth, stageHeight);
- const snapping_distance_viewbox = SNAPPING_DISTANCE_PIXELS *
- (viewBoxSize / effectiveSize);
- // ⚠️ WASM呼び出し
- const nearest_viewbox: NearestPointResult | undefined =
- find_nearest_point_on_path(
- backgroundPathD,
- p_viewbox.x,
- p_viewbox.y,
- snapping_distance_viewbox,
- );
- console.log("nearest_viewbox is ", nearest_viewbox);
- if (nearest_viewbox) {
- return scaleToScreen(nearest_viewbox);
- }
- return p;
- }, [
- scaleToViewBox,
- scaleToScreen,
- stageWidth,
- stageHeight,
- viewBoxSize,
- backgroundPathD,
- ]);
- const calculateTargetPoints = useCallback((points: Point[]): Point[] => {
- return points.map((p) => calculateNearestTargetPoint(p));
- }, [calculateNearestTargetPoint]);
- // --- 5. イベントハンドラ (吹き飛んでいた部分を再定義) ---
- /**
- * マウス・タッチ開始時の処理
- */
- const handleMouseDown = useCallback(
- (e: React.MouseEvent | React.TouchEvent) => {
- isDrawing.current = true;
- const pos = getPointerPosition(e);
- if (pos) {
- lineIdCounter.current += 1;
- const newLine: LineData = {
- id: lineIdCounter.current,
- tool: "pen",
- points: [pos],
- targetPoints: [],
- };
- setLines((prev) => [...prev, newLine]);
- setCurrentLines((prev) => [...prev, newLine]);
- }
- },
- [getPointerPosition],
- );
- /**
- * マウス・タッチ移動時の処理
- */
- const handleMouseMove = useCallback(
- (e: React.MouseEvent | React.TouchEvent) => {
- if (!isDrawing.current) return;
- const point = getPointerPosition(e);
- if (!point) return;
- const updateLines = (prevLines: LineData[]) => {
- const lastLineIndex = prevLines.length - 1;
- if (lastLineIndex < 0) return prevLines;
- const lastLine = prevLines[lastLineIndex];
- const newPoints: Point[] = [...lastLine.points, point];
- return [...prevLines.slice(0, lastLineIndex), {
- ...lastLine,
- points: newPoints,
- }];
- };
- setLines(updateLines);
- setCurrentLines(updateLines);
- },
- [getPointerPosition],
- );
- /**
- * マウス・タッチ終了時の処理
- */
- const handleMouseUp = useCallback(() => {
- isDrawing.current = false;
- setLines((prevLines) => {
- const lastLineIndex = prevLines.length - 1;
- if (lastLineIndex < 0) return prevLines;
- const lastLine = prevLines[lastLineIndex];
- const targetPoints = calculateTargetPoints(lastLine.points);
- const newLine: LineData = {
- ...lastLine,
- targetPoints: targetPoints,
- };
- return [...prevLines.slice(0, lastLineIndex), newLine];
- });
- }, [calculateTargetPoints]);
- // --- 6. アニメーションロジック (変更なし) ---
- const animateLines = useCallback((timestamp: number) => {
- if (!startTimeRef.current) startTimeRef.current = timestamp;
- const elapsed = timestamp - startTimeRef.current;
- const duration = 1500;
- const progress = Math.min(1, elapsed / duration);
- const newCurrentLines: LineData[] = lines.map((line) => {
- if (line.targetPoints.length === 0) return line;
- const newPoints: Point[] = line.points.map((originalP, index) => {
- const targetP = line.targetPoints[index];
- return {
- x: originalP.x + (targetP.x - originalP.x) * progress,
- y: originalP.y + (targetP.y - originalP.y) * progress,
- };
- });
- return { ...line, points: newPoints };
- });
- setCurrentLines(newCurrentLines);
- if (progress < 1) {
- animationRef.current = requestAnimationFrame(animateLines);
- } else {
- startTimeRef.current = undefined;
- setLines(newCurrentLines.map((line) => ({
- ...line,
- points: line.targetPoints,
- targetPoints: [],
- })));
- }
- }, [lines]);
- useEffect(() => {
- const lastLine = lines[lines.length - 1];
- if (
- lastLine?.targetPoints.length > 0 &&
- startTimeRef.current === undefined
- ) {
- startTimeRef.current = undefined;
- animationRef.current = requestAnimationFrame(animateLines);
- }
- return () => {
- if (animationRef.current) {
- cancelAnimationFrame(animationRef.current);
- }
- };
- }, [lines, animateLines]);
- const pointsToSvgString = (points: Point[]): string => {
- return points.map((p) => `${p.x},${p.y}`).join(" ");
- };
- // for audio
- const audioRef = useRef<HTMLAudioElement | null>(null);
- useEffect(() => {
- const audio = new Audio(AUDIO_SOURCE);
- audioRef.current = audio;
- const handleAudioEnded = () => {
- console.log("Audio playback finished. Calling onComplete.");
- onComplete();
- };
- audio.addEventListener("ended", handleAudioEnded);
- // ユーザーインタラクションの直後に再生を開始
- // コンポーネントがロードされただけではブラウザの制限で再生できないため、
- // ユーザーの最初の描画操作をトリガーとして再生を開始するのがより安全ですが、
- // 今回はシンプルにロード時に再生を試みます。
- const playAudio = () => {
- audio.play().catch((e) =>
- console.log(
- "Audio playback failed (may require user interaction):",
- e,
- )
- );
- };
- // ロード完了を待って再生を試みる
- audio.oncanplaythrough = playAudio;
- // アンマウント時のクリーンアップ
- return () => {
- audio.pause();
- audio.removeEventListener("ended", handleAudioEnded);
- audioRef.current = null;
- };
- }, []);
- return (
- <div
- ref={stageRef}
- style={{
- width: stageWidth,
- height: stageHeight,
- margin: 0,
- position: "fixed",
- top: 0,
- left: 0,
- overflow: "hidden",
- touchAction: "none",
- }}
- onMouseDown={handleMouseDown}
- onMouseMove={handleMouseMove}
- onMouseUp={handleMouseUp}
- onTouchStart={handleMouseDown}
- onTouchMove={handleMouseMove}
- onTouchEnd={handleMouseUp}
- >
- {/* 1. 背景のSVG (模倣対象) */}
- {backgroundPathD && (
- <svg
- width={stageWidth}
- height={stageHeight}
- viewBox={`0 0 ${viewBoxSize} ${viewBoxSize}`}
- style={{ position: "absolute" }}
- preserveAspectRatio="xMidYMid meet"
- >
- <path
- d={backgroundPathD}
- fill="none"
- stroke="white"
- strokeWidth="5"
- opacity="0.2"
- />
- </svg>
- )}
-
- {/* 2. ユーザーの描画 (アニメーション表示用) */}
- <svg
- width={stageWidth}
- height={stageHeight}
- viewBox={`0 0 ${viewBoxSize} ${viewBoxSize}`}
- style={{ position: "absolute", top: 0, left: 0 }}
- preserveAspectRatio="xMidYMid meet"
- >
- {currentLines.map((line) => (
- <polyline
- key={line.id}
- points={pointsToSvgString(
- line.points.map(scaleToViewBox),
- )}
- fill="none"
- stroke="#FF4500"
- strokeWidth="3"
- strokeLinecap="round"
- strokeLinejoin="round"
- />
- ))}
- </svg>
- </div>
- );
-}
+"use client";
+import React, { useCallback, useEffect, useRef, useState } from "react";
+import init, {
+ find_nearest_point_on_path,
+ NearestPointResult,
+} from "../../rust-wasm/pkg/rust_wasm.js";
+import { StageProps } from "../ctrl/page.tsx";
+type Tool = "pen";
+type Point = { x: number; y: number };
+interface LineData {
+ id: number;
+ tool: Tool;
+ points: Point[];
+ targetPoints: Point[];
+}
+const BACKGROUND_SVG_PATH_D_DEFAULT = "";
+const VIEWSBOX_SIZE = 500;
+const SNAPPING_DISTANCE_PIXELS = 30;
+const AUDIO_SOURCE = "/audio/002.flac";
+export default function DrawingApp({ onComplete }: StageProps) {
+ const [isClient, setIsClient] = useState(false);
+ const lineIdCounter = useRef(0);
+ const [lines, setLines] = useState<LineData[]>([]);
+ const [currentLines, setCurrentLines] = useState<LineData[]>([]);
+ const [stageWidth, setStageWidth] = useState(0);
+ const [stageHeight, setStageHeight] = useState(0);
+ const [backgroundPathD, setBackgroundPathD] = useState(
+ BACKGROUND_SVG_PATH_D_DEFAULT,
+ );
+ const [viewBoxSize, setViewBoxSize] = useState(VIEWSBOX_SIZE);
+ const isDrawing = useRef(false);
+ const animationRef = useRef<number>(0);
+ const startTimeRef = useRef<number | undefined>(undefined);
+ const stageRef = useRef<HTMLDivElement>(null);
+ useEffect(() => {
+ init();
+ }, []);
+ // --- 1. サイズ計算とステージ設定 (画面全体を使用) ---
+ useEffect(() => {
+ setIsClient(true);
+ const handleResize = () => {
+ if (typeof globalThis !== "undefined") {
+ setStageWidth(globalThis.innerWidth);
+ setStageHeight(globalThis.innerHeight);
+ }
+ };
+ handleResize();
+ globalThis.addEventListener("resize", handleResize);
+ return () => {
+ globalThis.removeEventListener("resize", handleResize);
+ };
+ }, []);
+ // --- 2. 座標変換ユーティリティ ---
+ const scaleToViewBox = useCallback(
+ (p: Point): Point => {
+ if (stageWidth === 0 || stageHeight === 0) return p;
+ const effectiveSize = Math.min(stageWidth, stageHeight);
+ const scale = viewBoxSize / effectiveSize;
+ const offsetX = (stageWidth - effectiveSize) / 2;
+ const offsetY = (stageHeight - effectiveSize) / 2;
+ return {
+ x: (p.x - offsetX) * scale,
+ y: (p.y - offsetY) * scale,
+ };
+ },
+ [stageWidth, stageHeight, viewBoxSize],
+ );
+ const scaleToScreen = useCallback(
+ (p: Point): Point => {
+ if (stageWidth === 0 || stageHeight === 0) return p;
+ const effectiveSize = Math.min(stageWidth, stageHeight);
+ const scale = effectiveSize / viewBoxSize;
+ const offsetX = (stageWidth - effectiveSize) / 2;
+ const offsetY = (stageHeight - effectiveSize) / 2;
+ return {
+ x: p.x * scale + offsetX,
+ y: p.y * scale + offsetY,
+ };
+ },
+ [stageWidth, stageHeight, viewBoxSize],
+ );
+ const getPointerPosition = useCallback(
+ (
+ e: React.MouseEvent | React.TouchEvent | MouseEvent | TouchEvent,
+ ): Point | null => {
+ if (!stageRef.current) return null;
+ const rect = stageRef.current.getBoundingClientRect();
+ let clientX: number, clientY: number;
+ if ("touches" in e) {
+ if (e.touches.length === 0) return null;
+ clientX = e.touches[0].clientX;
+ clientY = e.touches[0].clientY;
+ } else {
+ clientX = e.clientX;
+ clientY = e.clientY;
+ }
+
+ return { x: clientX - rect.left, y: clientY - rect.top };
+ },
+ [],
+ );
+ // --- 3. SVGコンテンツの取得ロジック (DOMParser) ---
+ useEffect(() => {
+ const fetchAndParseSvg = async () => {
+ try {
+ const response = await fetch("/whiteperson.svg");
+ const svgText = await response.text();
+ const parser = new DOMParser();
+ const doc = parser.parseFromString(svgText, "image/svg+xml");
+ const svgElement = doc.querySelector("svg");
+ if (svgElement) {
+ const viewBoxAttr = svgElement.getAttribute("viewBox");
+ if (viewBoxAttr) {
+ const parts = viewBoxAttr.trim().split(/\s+/);
+ if (parts.length === 4) {
+ const size = Math.max(parseFloat(parts[2]), parseFloat(parts[3]));
+ if (!isNaN(size) && size > 0) {
+ setViewBoxSize(size);
+ }
+ }
+ }
+ }
+ const pathElements = doc.querySelectorAll("path");
+ let foundDValue = null;
+ for (const element of Array.from(pathElements)) {
+ const dValue = element.getAttribute("d");
+ if (dValue && dValue.trim().length > 0) {
+ foundDValue = dValue;
+ break;
+ }
+ }
+ if (foundDValue) {
+ setBackgroundPathD(foundDValue);
+ return;
+ }
+ console.error(
+ "Error: <path> element or 'd' attribute not found in whiteperson.svg.",
+ );
+ } catch (error) {
+ console.error("Failed to load or parse whiteperson.svg:", error);
+ }
+ };
+ if (isClient && backgroundPathD === "") {
+ fetchAndParseSvg();
+ }
+ }, [isClient, backgroundPathD]);
+ // --- 4. 吸着ロジック (WASM呼び出し) ---
+ const calculateNearestTargetPoint = useCallback(
+ (p: Point): Point => {
+ const p_viewbox = scaleToViewBox(p);
+ const effectiveSize = Math.min(stageWidth, stageHeight);
+ const snapping_distance_viewbox =
+ SNAPPING_DISTANCE_PIXELS * (viewBoxSize / effectiveSize);
+ // ⚠️ WASM呼び出し
+ const nearest_viewbox: NearestPointResult | undefined =
+ find_nearest_point_on_path(
+ backgroundPathD,
+ p_viewbox.x,
+ p_viewbox.y,
+ snapping_distance_viewbox,
+ );
+ console.log("nearest_viewbox is ", nearest_viewbox);
+ if (nearest_viewbox) {
+ return scaleToScreen(nearest_viewbox);
+ }
+ return p;
+ },
+ [
+ scaleToViewBox,
+ scaleToScreen,
+ stageWidth,
+ stageHeight,
+ viewBoxSize,
+ backgroundPathD,
+ ],
+ );
+ const calculateTargetPoints = useCallback(
+ (points: Point[]): Point[] => {
+ return points.map((p) => calculateNearestTargetPoint(p));
+ },
+ [calculateNearestTargetPoint],
+ );
+ // --- 5. イベントハンドラ (吹き飛んでいた部分を再定義) ---
+ /**
+ * マウス・タッチ開始時の処理
+ */
+ const handleMouseDown = useCallback(
+ (e: React.MouseEvent | React.TouchEvent) => {
+ isDrawing.current = true;
+ const pos = getPointerPosition(e);
+ if (pos) {
+ lineIdCounter.current += 1;
+ const newLine: LineData = {
+ id: lineIdCounter.current,
+ tool: "pen",
+ points: [pos],
+ targetPoints: [],
+ };
+ setLines((prev) => [...prev, newLine]);
+ setCurrentLines((prev) => [...prev, newLine]);
+ }
+ },
+ [getPointerPosition],
+ );
+ /**
+ * マウス・タッチ移動時の処理
+ */
+ const handleMouseMove = useCallback(
+ (e: React.MouseEvent | React.TouchEvent) => {
+ if (!isDrawing.current) return;
+ const point = getPointerPosition(e);
+ if (!point) return;
+ const updateLines = (prevLines: LineData[]) => {
+ const lastLineIndex = prevLines.length - 1;
+ if (lastLineIndex < 0) return prevLines;
+ const lastLine = prevLines[lastLineIndex];
+ const newPoints: Point[] = [...lastLine.points, point];
+ return [
+ ...prevLines.slice(0, lastLineIndex),
+ {
+ ...lastLine,
+ points: newPoints,
+ },
+ ];
+ };
+ setLines(updateLines);
+ setCurrentLines(updateLines);
+ },
+ [getPointerPosition],
+ );
+ /**
+ * マウス・タッチ終了時の処理
+ */
+ const handleMouseUp = useCallback(() => {
+ isDrawing.current = false;
+ setLines((prevLines) => {
+ const lastLineIndex = prevLines.length - 1;
+ if (lastLineIndex < 0) return prevLines;
+ const lastLine = prevLines[lastLineIndex];
+ const targetPoints = calculateTargetPoints(lastLine.points);
+ const newLine: LineData = {
+ ...lastLine,
+ targetPoints: targetPoints,
+ };
+ return [...prevLines.slice(0, lastLineIndex), newLine];
+ });
+ }, [calculateTargetPoints]);
+ // --- 6. アニメーションロジック (変更なし) ---
+ const animateLines = useCallback(
+ (timestamp: number) => {
+ if (!startTimeRef.current) startTimeRef.current = timestamp;
+ const elapsed = timestamp - startTimeRef.current;
+ const duration = 1500;
+ const progress = Math.min(1, elapsed / duration);
+ const newCurrentLines: LineData[] = lines.map((line) => {
+ if (line.targetPoints.length === 0) return line;
+ const newPoints: Point[] = line.points.map((originalP, index) => {
+ const targetP = line.targetPoints[index];
+ return {
+ x: originalP.x + (targetP.x - originalP.x) * progress,
+ y: originalP.y + (targetP.y - originalP.y) * progress,
+ };
+ });
+ return { ...line, points: newPoints };
+ });
+ setCurrentLines(newCurrentLines);
+ if (progress < 1) {
+ animationRef.current = requestAnimationFrame(animateLines);
+ } else {
+ startTimeRef.current = undefined;
+ setLines(
+ newCurrentLines.map((line) => ({
+ ...line,
+ points: line.targetPoints,
+ targetPoints: [],
+ })),
+ );
+ }
+ },
+ [lines],
+ );
+ useEffect(() => {
+ const lastLine = lines[lines.length - 1];
+ if (
+ lastLine?.targetPoints.length > 0 &&
+ startTimeRef.current === undefined
+ ) {
+ startTimeRef.current = undefined;
+ animationRef.current = requestAnimationFrame(animateLines);
+ }
+ return () => {
+ if (animationRef.current) {
+ cancelAnimationFrame(animationRef.current);
+ }
+ };
+ }, [lines, animateLines]);
+ const pointsToSvgString = (points: Point[]): string => {
+ return points.map((p) => `${p.x},${p.y}`).join(" ");
+ };
+ // for audio
+ const audioRef = useRef<HTMLAudioElement | null>(null);
+ useEffect(() => {
+ if (audioRef.current) {
+ return;
+ }
+ const audio = new Audio(AUDIO_SOURCE);
+ audioRef.current = audio;
+ const handleAudioEnded = () => {
+ console.log("Audio playback finished. Calling onComplete.");
+ onComplete();
+ };
+ audio.addEventListener("ended", handleAudioEnded);
+ const playAudio = () => {
+ audio
+ .play()
+ .catch((e) =>
+ console.log(
+ "Audio playback failed (may require user interaction):",
+ e,
+ ),
+ );
+ };
+ // ロード完了を待って再生を試みる
+ audio.oncanplaythrough = playAudio;
+ // アンマウント時のクリーンアップ
+ return () => {
+ if (audioRef.current) {
+ audioRef.current.pause();
+ audioRef.current.removeEventListener("ended", handleAudioEnded);
+ audioRef.current = null;
+ }
+ };
+ }, [onComplete]);
+ return (
+ <div
+ ref={stageRef}
+ style={{
+ width: stageWidth,
+ height: stageHeight,
+ margin: 0,
+ position: "fixed",
+ top: 0,
+ left: 0,
+ overflow: "hidden",
+ touchAction: "none",
+ }}
+ onMouseDown={handleMouseDown}
+ onMouseMove={handleMouseMove}
+ onMouseUp={handleMouseUp}
+ onTouchStart={handleMouseDown}
+ onTouchMove={handleMouseMove}
+ onTouchEnd={handleMouseUp}
+ >
+ {/* 1. 背景のSVG (模倣対象) */}
+ {backgroundPathD && (
+ <svg
+ width={stageWidth}
+ height={stageHeight}
+ viewBox={`0 0 ${viewBoxSize} ${viewBoxSize}`}
+ style={{ position: "absolute" }}
+ preserveAspectRatio="xMidYMid meet"
+ >
+ <path
+ d={backgroundPathD}
+ fill="none"
+ stroke="white"
+ strokeWidth="5"
+ opacity="0.2"
+ />
+ </svg>
+ )}
+
+ {/* 2. ユーザーの描画 (アニメーション表示用) */}
+ <svg
+ width={stageWidth}
+ height={stageHeight}
+ viewBox={`0 0 ${viewBoxSize} ${viewBoxSize}`}
+ style={{ position: "absolute", top: 0, left: 0 }}
+ preserveAspectRatio="xMidYMid meet"
+ >
+ {currentLines.map((line) => (
+ <polyline
+ key={line.id}
+ points={pointsToSvgString(line.points.map(scaleToViewBox))}
+ fill="none"
+ stroke="#FF4500"
+ strokeWidth="3"
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ />
+ ))}
+ </svg>
+ </div>
+ );
+}
diff --git a/app/components/3_vr.tsx b/app/components/3_vr.tsx
@@ -14,8 +14,8 @@ import * as THREE from "three";
import { StageProps } from "../ctrl/page.tsx";
const AUDIO_SOURCES: Record<number, string> = {
- 1: "/audio/001.wav",
- 2: "/audio/002.wav",
+ 1: "/audio/003-1.flac",
+ 2: "/audio/003-2.flac",
};
const useSequentialAudio = (
diff --git a/app/components/4_oil.tsx b/app/components/4_oil.tsx
@@ -5,8 +5,8 @@ import { useEffect, useRef, useState } from "react";
import * as THREE from "three";
import { StageProps } from "../ctrl/page.tsx";
const AUDIO_SOURCES: Record<number, string> = {
- 1: "/audio/001.wav",
- 2: "/audio/002.wav",
+ 1: "/audio/004-1.flac",
+ 2: "/audio/004-2.flac",
};
const useSequentialAudio = (
initialState: number,
diff --git a/app/components/5_title.tsx b/app/components/5_title.tsx
@@ -1,12 +1,13 @@
import { useEffect, useRef } from "react";
import { StageProps } from "../ctrl/page.tsx";
-const AUDIO_SOURCE = "/audio/001.wav";
+const AUDIO_SOURCE = "/audio/005.flac";
export default function FifthTitle({ onComplete }: StageProps) {
const audioRef = useRef<HTMLAudioElement | null>(null);
useEffect(() => {
+ if (audioRef.current) return;
const audio = new Audio(AUDIO_SOURCE);
audioRef.current = audio;
@@ -46,7 +47,7 @@ export default function FifthTitle({ onComplete }: StageProps) {
alignItems: "center",
}}
>
- <h1 style={{ fontSize: "8rem" }}>AIdentity</h1>
+ <h1 style={{ fontSize: "8rem", color: "#bbb" }}>AIdentity</h1>
</nav>
);
}
diff --git a/app/components/6_chat.tsx b/app/components/6_chat.tsx
@@ -113,13 +113,13 @@ const QuestionAnswer: React.FC<QuestionAnswerProps> = ({
);
};
const myQuestions: QuestionWithAudio[] = [
- { input: "5 + 9 = ?", ans: "14", audioUrl: "/audio/001.wav" }, // 実際には適切なURLに置き換えてください
+ { input: "5 + 9 = ?", ans: "14", audioUrl: "/audio/006-1.flac" }, // 実際には適切なURLに置き換えてください
{
input: "東京のローマ字表記は?",
ans: "tokyo",
- audioUrl: "/audio/002.wav",
+ audioUrl: "/audio/006-2.flac",
},
- { input: "Next.jsの親要素は?", ans: "react", audioUrl: "/audio/003.wav" },
+ { input: "Next.jsの親要素は?", ans: "react", audioUrl: "/audio/006-3.flac" },
];
export default function QuizPage({ onComplete }: StageProps) {
const handleQuizComplete = () => {
diff --git a/app/components/7_vr.tsx b/app/components/7_vr.tsx
@@ -14,7 +14,7 @@ import * as THREE from "three";
import { StageProps } from "../ctrl/page.tsx";
const AUDIO_SOURCES: Record<number, string> = {
- 2: "/audio/002.wav",
+ 2: "/audio/007.flac",
};
const useSequentialAudio = (
diff --git a/app/components/8_protectHart.tsx b/app/components/8_protectHart.tsx
@@ -388,7 +388,7 @@ const useGameLoop = (canvasRef: React.RefObject<HTMLCanvasElement | null>) => {
// Fur Audio
-const AUDIO_SOURCE = "/audio/001.wav";
+const AUDIO_SOURCE = "/audio/008.flac";
const useAudioPlayback = (onComplete: () => void) => {
const audioRef = useRef<HTMLAudioElement | null>(null);
const onCompleteRef = useRef(onComplete);
@@ -399,8 +399,7 @@ const useAudioPlayback = (onComplete: () => void) => {
useEffect(() => {
if (audioRef.current) {
- audioRef.current.pause();
- audioRef.current.currentTime = 0;
+ return;
}
const audio = new Audio(AUDIO_SOURCE);
@@ -430,9 +429,11 @@ const useAudioPlayback = (onComplete: () => void) => {
audio.oncanplaythrough = playAudio;
return () => {
- audio.pause();
- audio.removeEventListener("ended", handleAudioEnded);
- audioRef.current = null;
+ if (audioRef.current) {
+ audio.pause();
+ audio.removeEventListener("ended", handleAudioEnded);
+ audioRef.current = null;
+ }
};
}, []);
return { audioRef };
diff --git a/app/page.tsx b/app/page.tsx
@@ -43,6 +43,9 @@ export default function Home() {
<article>
<h2>注意:Attention</h2>
<ul>
+ <li>
+ いち素人の実装です.不具合等々ありますが,ご了承ください.issueを投げてもらえば,治せるところは直します.
+ </li>
<li>音が出ます。</li>
<li>フルスクリーンを要求します。</li>
<li>
diff --git a/bun.lock b/bun.lock
@@ -262,7 +262,7 @@
"@prisma/instrumentation": ["@prisma/instrumentation@6.15.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.52.0 || ^0.53.0 || ^0.54.0 || ^0.55.0 || ^0.56.0 || ^0.57.0" }, "peerDependencies": { "@opentelemetry/api": "^1.8" } }, "sha512-6TXaH6OmDkMOQvOxwLZ8XS51hU2v4A3vmE2pSijCIiGRJYyNeMcL6nMHQMyYdZRD8wl7LF3Wzc+AMPMV/9Oo7A=="],
- "@react-three/drei": ["@react-three/drei@10.7.6", "", { "dependencies": { "@babel/runtime": "^7.26.0", "@mediapipe/tasks-vision": "0.10.17", "@monogrid/gainmap-js": "^3.0.6", "@use-gesture/react": "^10.3.1", "camera-controls": "^3.1.0", "cross-env": "^7.0.3", "detect-gpu": "^5.0.56", "glsl-noise": "^0.0.0", "hls.js": "^1.5.17", "maath": "^0.10.8", "meshline": "^3.3.1", "stats-gl": "^2.2.8", "stats.js": "^0.17.0", "suspend-react": "^0.1.3", "three-mesh-bvh": "^0.8.3", "three-stdlib": "^2.35.6", "troika-three-text": "^0.52.4", "tunnel-rat": "^0.1.2", "use-sync-external-store": "^1.4.0", "utility-types": "^3.11.0", "zustand": "^5.0.1" }, "peerDependencies": { "@react-three/fiber": "^9.0.0", "react": "^19", "react-dom": "^19", "three": ">=0.159" }, "optionalPeers": ["react-dom"] }, "sha512-ZSFwRlRaa4zjtB7yHO6Q9xQGuyDCzE7whXBhum92JslcMRC3aouivp0rAzszcVymIoJx6PXmibyP+xr+zKdwLg=="],
+ "@react-three/drei": ["@react-three/drei@10.7.7", "", { "dependencies": { "@babel/runtime": "^7.26.0", "@mediapipe/tasks-vision": "0.10.17", "@monogrid/gainmap-js": "^3.0.6", "@use-gesture/react": "^10.3.1", "camera-controls": "^3.1.0", "cross-env": "^7.0.3", "detect-gpu": "^5.0.56", "glsl-noise": "^0.0.0", "hls.js": "^1.5.17", "maath": "^0.10.8", "meshline": "^3.3.1", "stats-gl": "^2.2.8", "stats.js": "^0.17.0", "suspend-react": "^0.1.3", "three-mesh-bvh": "^0.8.3", "three-stdlib": "^2.35.6", "troika-three-text": "^0.52.4", "tunnel-rat": "^0.1.2", "use-sync-external-store": "^1.4.0", "utility-types": "^3.11.0", "zustand": "^5.0.1" }, "peerDependencies": { "@react-three/fiber": "^9.0.0", "react": "^19", "react-dom": "^19", "three": ">=0.159" }, "optionalPeers": ["react-dom"] }, "sha512-ff+J5iloR0k4tC++QtD/j9u3w5fzfgFAWDtAGQah9pF2B1YgOq/5JxqY0/aVoQG5r3xSZz0cv5tk2YuBob4xEQ=="],
"@react-three/fiber": ["@react-three/fiber@9.4.0", "", { "dependencies": { "@babel/runtime": "^7.17.8", "@types/react-reconciler": "^0.32.0", "@types/webxr": "*", "base64-js": "^1.5.1", "buffer": "^6.0.3", "its-fine": "^2.0.0", "react-reconciler": "^0.31.0", "react-use-measure": "^2.1.7", "scheduler": "^0.25.0", "suspend-react": "^0.1.3", "use-sync-external-store": "^1.4.0", "zustand": "^5.0.3" }, "peerDependencies": { "expo": ">=43.0", "expo-asset": ">=8.4", "expo-file-system": ">=11.0", "expo-gl": ">=11.0", "react": "^19.0.0", "react-dom": "^19.0.0", "react-native": ">=0.78", "three": ">=0.156" }, "optionalPeers": ["expo", "expo-asset", "expo-file-system", "expo-gl", "react-dom", "react-native"] }, "sha512-k4iu1R6e5D54918V4sqmISUkI5OgTw3v7/sDRKEC632Wd5g2WBtUS5gyG63X0GJO/HZUj1tsjSXfyzwrUHZl1g=="],
@@ -396,7 +396,7 @@
"@types/pg-pool": ["@types/pg-pool@2.0.6", "", { "dependencies": { "@types/pg": "*" } }, "sha512-TaAUE5rq2VQYxab5Ts7WZhKNmuN78Q6PiFonTDdpbx8a1H0M1vhy3rhiMjl+e2iHmogyMw7jZF4FrE6eJUy5HQ=="],
- "@types/react": ["@types/react@19.2.4", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-tBFxBp9Nfyy5rsmefN+WXc1JeW/j2BpBHFdLZbEVfs9wn3E3NRFxwV0pJg8M1qQAexFpvz73hJXFofV0ZAu92A=="],
+ "@types/react": ["@types/react@19.2.5", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-keKxkZMqnDicuvFoJbzrhbtdLSPhj/rZThDlKWCDbgXmUg0rEUFtRssDXKYmtXluZlIqiC5VqkCgRwzuyLHKHw=="],
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
diff --git a/package.json b/package.json
@@ -10,7 +10,7 @@
"lint": "eslint"
},
"dependencies": {
- "@react-three/drei": "^10.7.6",
+ "@react-three/drei": "^10.7.7",
"@react-three/fiber": "^9.4.0",
"@sentry/nextjs": "^10.25.0",
"next": "15.5.5",
@@ -22,7 +22,7 @@
"devDependencies": {
"@eslint/eslintrc": "^3.3.1",
"@types/node": "^20.19.25",
- "@types/react": "^19.2.4",
+ "@types/react": "^19.2.5",
"@types/react-dom": "^19.2.3",
"eslint": "^9.39.1",
"eslint-config-next": "15.5.5",
diff --git a/public/audio/001.flac b/public/audio/001.flac
Binary files differ.
diff --git a/public/audio/002.flac b/public/audio/002.flac
Binary files differ.
diff --git a/public/audio/003-1.flac b/public/audio/003-1.flac
Binary files differ.
diff --git a/public/audio/003-2.flac b/public/audio/003-2.flac
Binary files differ.
diff --git a/public/audio/004-1.flac b/public/audio/004-1.flac
Binary files differ.
diff --git a/public/audio/004-2.flac b/public/audio/004-2.flac
Binary files differ.
diff --git a/public/audio/005.flac b/public/audio/005.flac
Binary files differ.
diff --git a/public/audio/006-1.flac b/public/audio/006-1.flac
Binary files differ.
diff --git a/public/audio/006-2.flac b/public/audio/006-2.flac
Binary files differ.
diff --git a/public/audio/006-3.flac b/public/audio/006-3.flac
Binary files differ.
diff --git a/public/audio/006-4.flac b/public/audio/006-4.flac
Binary files differ.
diff --git a/public/audio/006-5.flac b/public/audio/006-5.flac
Binary files differ.
diff --git a/public/audio/006-6.flac b/public/audio/006-6.flac
Binary files differ.
diff --git a/public/audio/007.flac b/public/audio/007.flac
Binary files differ.
diff --git a/public/audio/008.flac b/public/audio/008.flac
Binary files differ.