AIdentity

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

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:
Mapp/components/1_chat.tsx | 2+-
Mapp/components/2_draw.tsx | 775++++++++++++++++++++++++++++++++++++++++---------------------------------------
Mapp/components/3_vr.tsx | 4++--
Mapp/components/4_oil.tsx | 4++--
Mapp/components/5_title.tsx | 5+++--
Mapp/components/6_chat.tsx | 6+++---
Mapp/components/7_vr.tsx | 2+-
Mapp/components/8_protectHart.tsx | 13+++++++------
Mapp/page.tsx | 3+++
Mbun.lock | 4++--
Mpackage.json | 4++--
Apublic/audio/001.flac | 0
Apublic/audio/002.flac | 0
Apublic/audio/003-1.flac | 0
Apublic/audio/003-2.flac | 0
Apublic/audio/004-1.flac | 0
Apublic/audio/004-2.flac | 0
Apublic/audio/005.flac | 0
Apublic/audio/006-1.flac | 0
Apublic/audio/006-2.flac | 0
Apublic/audio/006-3.flac | 0
Apublic/audio/006-4.flac | 0
Apublic/audio/006-5.flac | 0
Apublic/audio/006-6.flac | 0
Apublic/audio/007.flac | 0
Apublic/audio/008.flac | 0
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.