commit 8c1a7ea6166f7007afd087cfc1980b8ff431270a
parent 73f92c3bd0f7f606055031954ee0a3b9c02553d9
Author: minerva-jupiter <ryouturn@gmail.com>
Date: Mon, 27 Oct 2025 21:20:41 +0900
feat(app): Add new interactive stages with dynamic VR and chat features
Diffstat:
22 files changed, 954 insertions(+), 23 deletions(-)
diff --git a/app/components/1_chat.tsx b/app/components/1_chat.tsx
@@ -261,7 +261,7 @@ const useAudioPlayback = (
sender: 'user',
};
setMessages((prev) => [...prev, newUserMessage]);
- const ans = dict == undefined ? 'もっとまともなことを言いなさい。' : chat(dict, text);
+ const ans = dict == undefined ? 'もっとマシなことを言いなさい。' : chat(dict, text);
const aiResponse: Message = {
id: Date.now() + 1,
text: ans,
diff --git a/app/components/3_vr.tsx b/app/components/3_vr.tsx
@@ -88,6 +88,7 @@ interface RandomCloudsProps {
count: number;
radius: number;
height: number;
+ sceneState: number;
}
@@ -95,26 +96,26 @@ interface RandomCloudsProps {
function SceneController({ sceneState }: { sceneState: number }) {
const isFoggy = sceneState === 2;
- // 状態 1 (晴れ): 強さ 2 / 状態 2 (霧): 強さ 0.3
- const lightIntensity = isFoggy ? 0.3 : 2;
+
+ const directionalLightIntensity = isFoggy ? 0.1 : 2;
+
+ const ambientLightIntensity = isFoggy ? 0.05 : 0.5;
- // 霧の制御ロジック
useFrame(({ scene }) => {
if (isFoggy) {
- // 💡 状態 2: 暗く濃い霧 (色: 灰色、Near 5, Far 50)
- scene.fog = new THREE.Fog(0x909090, 5, 50);
+ scene.fog = new THREE.Fog(0x444444, 5, 40); // Far distanceも少し短くして密度を上げる
} else {
- // 状態 1: 霧を削除(今まで通り)
scene.fog = null;
}
});
return (
<>
- <ambientLight intensity={0.5} />
+ <ambientLight intensity={ambientLightIntensity} />
+
<directionalLight
position={[5, 10, 5]}
- intensity={lightIntensity} // 状態によって明るさを変更
+ intensity={directionalLightIntensity} // ここで制御
castShadow
shadow-mapSize-width={2048}
shadow-mapSize-height={2048}
@@ -129,7 +130,7 @@ function SceneController({ sceneState }: { sceneState: number }) {
}
-function AnimatedSky() {
+function AnimatedSky({sceneState}:{sceneState:number}) {
const [sunPosition, setSunPosition] = useState<[number, number, number]>([0, 100, 0]);
useFrame(({ clock }) => {
@@ -139,11 +140,15 @@ function AnimatedSky() {
setSunPosition([x, 100, z]);
});
+ if (sceneState === 2){
+ return null;
+ }
+
return (
<Sky
distance={450000}
sunPosition={new THREE.Vector3(...sunPosition)}
- inclination={0.6}
+ inclination={sceneState === 2 ? 0.001 : 0.6}
azimuth={0.25}
mieCoefficient={0.005}
mieDirectionalG={0.8}
@@ -154,7 +159,10 @@ function AnimatedSky() {
}
-function RandomClouds({ count, radius, height }: RandomCloudsProps) {
+function RandomClouds({ count, radius, height, sceneState }: RandomCloudsProps) {
+ const cloudColor = sceneState === 2 ? '#080808' : '#fff';
+ const baseOpacity = sceneState === 2 ? 0.9 : 0.5;
+
const cloudConfigs = useMemo(() => {
return Array.from({ length: count }).map((_, i) => {
const angle = Math.random() * Math.PI * 2;
@@ -177,7 +185,8 @@ function RandomClouds({ count, radius, height }: RandomCloudsProps) {
] as [number, number, number],
seed: Math.floor(Math.random() * 1000) + i,
volume: Math.random() * 20 + 5,
- opacity: Math.random() * 0.4 + 0.5,
+ opacity: Math.random() * 0.4 + baseOpacity,
+ color: cloudColor,
};
});
}, [count, radius, height]);
@@ -202,12 +211,12 @@ export default function ThreeVR({onComplete}:StageProps) {
camera={{ position: [5, 5, 5], fov: 60 }}
shadows
// 霧の状態によってクリアカラー(背景色)を変える
- style={{ backgroundColor: sceneState === 2 ? '#a0aa90' : '#d0e0ff' }}
+ style={{ background: sceneState === 2 ? '#85555B' : '#d0e0ff' }}
>
- <AnimatedSky />
+ <AnimatedSky sceneState={sceneState}/>
<Clouds material={THREE.MeshLambertMaterial} limit={200}>
- <RandomClouds count={100} radius={80} height={15} />
+ <RandomClouds count={100} radius={80} height={15} sceneState={sceneState}/>
</Clouds>
{/* 💡 SceneControllerを配置し、状態を渡す */}
diff --git a/app/components/5_title.tsx b/app/components/5_title.tsx
@@ -0,0 +1,45 @@
+import { useEffect, useRef } from "react";
+import { StageProps } from "../ctrl/page.tsx";
+
+const AUDIO_SOURCE = '/audio/001.wav';
+
+export default function FifthTitle({onComplete}:StageProps){
+ const audioRef = useRef<HTMLAudioElement | null>(null);
+
+ useEffect(() => {
+ const audio = new Audio(AUDIO_SOURCE);
+ audioRef.current = audio;
+
+ const handleAudioEnded = () => {
+ 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;
+ };
+ }, [onComplete]);
+
+ return (
+ <nav style={{width:"100vw",height:"100vh", display:"flex", justifyContent:"center", alignItems:"center"}}>
+ <h1 style={{fontSize:"8rem"}}>
+ AIdentity
+ </h1>
+ </nav>
+ );
+};
diff --git a/app/components/6_chat.tsx b/app/components/6_chat.tsx
@@ -0,0 +1,356 @@
+'use client';
+
+import React, { useState, useRef, useEffect, KeyboardEvent, useCallback } from 'react';
+import { StageProps } from '../ctrl/page.tsx';
+import init, { chat2 } from '../../rust-wasm/pkg/rust_wasm.js';
+
+// useWebAudioControllerの代替となるカスタムフックを定義
+const useAudioPlayback = (
+ initialTalktime: number,
+ audioSources: Record<number, string>,
+ onAudioEnd: () => void,
+) => {
+ // Audioオブジェクトの参照を保持
+ const audioRef = useRef<HTMLAudioElement | null>(null);
+ // 現在のトークタイムを保持し、変更を監視するためのState
+ const talktimesRef = useRef(initialTalktime);
+ const [currentAudioUrl, setCurrentAudioUrl] = useState(audioSources[initialTalktime] || '');
+
+ // 外部から再生を停止するための関数
+ const stop = useCallback(() => {
+ if (audioRef.current) {
+ audioRef.current.pause();
+ audioRef.current.currentTime = 0; // 最初に戻す
+ }
+ }, []);
+
+ // 外部からループ設定を変更するための関数(AudioRefのcurrentが更新されるたびに適用される)
+ const setLoop = useCallback((isLooping: boolean) => {
+ // audioRef.currentが存在する場合にのみ設定を試みる
+ if (audioRef.current) {
+ audioRef.current.loop = isLooping;
+ }
+ }, []); // 依存配列は空でOK
+
+ // 1. Audioオブジェクトの初期化、クリーンアップ、および終了イベント処理
+ useEffect(() => {
+ // 古い音源を停止
+ stop();
+
+ const audio = new Audio(currentAudioUrl);
+ audioRef.current = audio;
+ audio.volume = 0.5; // 必要に応じて音量を設定
+
+ // 常に最新のtalktimesRef.currentに基づいてループ設定
+ const isLooping = talktimesRef.current < 4;
+ audio.loop = isLooping;
+ setLoop(isLooping); // 念のため
+
+ // 自動再生の試行 (ユーザーのインタラクションが必要なため、失敗する可能性あり)
+ audio.play().catch(e => {
+ console.error("Audio playback error on URL change/init:", e);
+ // ユーザーのインタラクションがない場合は再生できないため、ここではエラーを無視するか、ユーザーに操作を促す
+ });
+
+
+ const handleEnded = () => {
+ const currentTalktime = talktimesRef.current;
+
+ // 終了条件を満たしている場合は停止し、onAudioEndを実行
+ if (currentTalktime >= 4) {
+ console.log("最終音源の再生が終了しました。onAudioEndを実行します。");
+ stop();
+ onAudioEnd();
+ return;
+ }
+
+ // ループがtrueの場合はonendedは呼ばれないはずだが、フォールバックとして再再生を試みる
+ if (!audio.loop) {
+ console.log(`音源 ${currentTalktime} の再生が終了しました。ループ再生を再開します。`);
+ audio.play().catch(e => console.error("Audio playback error on loop restart:", e));
+ }
+ };
+
+ audio.addEventListener('ended', handleEnded);
+
+ // コンポーネントがアンマウントされる際のクリーンアップ
+ return () => {
+ audio.removeEventListener('ended', handleEnded);
+ audio.pause();
+ // audioRef.current = null; // Audioオブジェクトが再生成されるため、ここではnullにしない
+ };
+ }, [currentAudioUrl, onAudioEnd, stop, setLoop]); // currentAudioUrlが変わるとAudioオブジェクトが再生成される
+
+ // 2. talktimesRef.currentの変更を監視し、音源の切り替えを行う
+ useEffect(() => {
+ const currentTalktime = talktimesRef.current;
+ const newAudioUrl = audioSources[currentTalktime];
+
+ console.log("talktimesRef is ", currentTalktime);
+
+ if (newAudioUrl && newAudioUrl !== currentAudioUrl) {
+ // URLが変わったら、Audioオブジェクトを再生成するためにstateを更新
+ // 新しいcurrentAudioUrlで上のuseEffectがトリガーされる
+ setCurrentAudioUrl(newAudioUrl);
+
+ // 新しい音源に対するループ設定を即座に更新
+ const isLooping = currentTalktime < 4;
+ setLoop(isLooping);
+ } else if (currentTalktime === initialTalktime && audioRef.current) {
+ // 初回ロード時のみ、初期音源を再生(ブラウザの制限のため、ユーザー操作後の初回にのみ有効)
+ audioRef.current.play().catch(e => console.error("Initial audio playback error:", e));
+ }
+
+ }, [talktimesRef.current]);
+
+ // 外部からの更新用にtalktimesRefと制御関数を返す
+ return { talktimesRef, stop, setLoop, currentAudioUrl };
+ };
+
+ const FirstChat: React.FC<StageProps> = ({ onComplete }) => {
+ return(
+ <ChatPage onComplete={onComplete}/>
+ )
+ };
+
+ export default FirstChat;
+
+ const AUDIO_SOURCES: Record<number, string> = {
+ 1: '/audio/001.wav',
+ 2: '/audio/002.wav',
+ 3: '/audio/003.wav',
+ 4: '/audio/004.wav',
+ // 5以降は終了条件を満たすため、再生する音源は設定不要
+ };
+
+ interface Message {
+ id: number;
+ text: string;
+ sender: 'user' | 'ai';
+ }
+
+ const initialMessages: Message[] = [
+ { id: 1, text: 'どうせ私のこと、うっさいな死ねよくらいに思ってんでしょ?', sender: 'ai' },
+ ];
+
+ const MessageBubble: React.FC<{ message: Message }> = ({ message }) => {
+ const isUser = message.sender === 'user';
+
+ const bubbleStyle: React.CSSProperties = {
+ padding: '10px 15px',
+ borderRadius: '15px',
+ maxWidth: '70%',
+ wordBreak: 'break-word',
+ fontSize: '16px',
+ backgroundColor: isUser ? '#3b82f6' : '#e5e7eb', // blue-500 or gray-200
+ color: isUser ? 'white' : '#1f2937', // white or gray-800
+ marginLeft: isUser ? 'auto' : '0',
+ marginRight: isUser ? '0' : 'auto',
+ };
+
+ const containerStyle: React.CSSProperties = {
+ display: 'flex',
+ marginBottom: '10px',
+ justifyContent: isUser ? 'flex-end' : 'flex-start',
+ };
+
+ return (
+ <div style={containerStyle}>
+ <div style={bubbleStyle}>
+ {message.text}
+ </div>
+ </div>
+ );
+ };
+
+ const MessageInput: React.FC<{ onSend: (text: string) => void }> = ({ onSend }) => {
+ const [input, setInput] = useState('');
+
+ const handleSend = () => {
+ if (input.trim() === '') return;
+ onSend(input);
+ setInput('');
+ };
+
+ const handleKeyPress = (e: KeyboardEvent<HTMLInputElement>) => {
+ if (e.key === 'Enter') {
+ e.preventDefault();
+ handleSend();
+ }
+ };
+
+ const inputStyle: React.CSSProperties = {
+ flexGrow: 1,
+ padding: '12px',
+ border: '1px solid #d1d5db', // gray-300
+ borderRadius: '8px',
+ marginRight: '10px',
+ fontSize: '16px',
+ outline: 'none',
+ };
+
+ const buttonStyle: React.CSSProperties = {
+ backgroundColor: '#3b82f6', // blue-500
+ color: 'white',
+ border: 'none',
+ padding: '12px 20px',
+ borderRadius: '8px',
+ cursor: 'pointer',
+ fontWeight: 'bold',
+ };
+
+
+ return (
+ <div style={{ padding: '15px', backgroundColor: '#f9fafb', display: 'flex', alignItems: 'center' }}>
+ <input
+ type="text"
+ style={inputStyle}
+ placeholder="メッセージを入力してください..."
+ value={input}
+ onChange={(e) => setInput(e.target.value)}
+ onKeyDown={handleKeyPress}
+ />
+ <button
+ type='submit'
+ style={buttonStyle}
+ onClick={handleSend}
+ disabled={input.trim() === ''}
+ >
+ 送信
+ </button>
+ </div>
+ );
+ };
+
+ function ChatPage({onComplete}:StageProps) {
+ const [messages, setMessages] = useState<Message[]>(initialMessages);
+ const [ dict, setDict ] = useState<Uint8Array|undefined>(undefined);
+ const messagesEndRef = useRef<HTMLDivElement>(null);
+ const dictPath = '/system.dic.zst';
+
+ // for audio
+ const { talktimesRef } = useAudioPlayback(
+ 1, // initialTalktime
+ AUDIO_SOURCES,
+ onComplete
+ );
+
+ useEffect(() => {
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
+ }, [messages]);
+
+ useEffect(() => {
+ const loadData = async () => {
+ const loadedDict = await LoadDict(dictPath);
+ setDict(loadedDict);
+ }
+ loadData();
+ }, []);
+
+ useEffect(() => {
+ init();
+ }, []);
+
+ const handleSendMessage = useCallback(async (text: string) => {
+ if (text.trim() === '') return;
+
+ const newUserMessage: Message = {
+ id: Date.now(),
+ text,
+ sender: 'user',
+ };
+ setMessages((prev) => [...prev, newUserMessage]);
+ const ans = dict == undefined ? 'みんなはもうできてるのに、なんでできないの?' : chat2(dict, text);
+ const aiResponse: Message = {
+ id: Date.now() + 1,
+ text: ans,
+ sender: 'ai',
+ };
+ setMessages((prev) => [...prev, aiResponse]);
+
+ talktimesRef.current += 1;
+
+ // termination condition
+ if(talktimesRef.current >= 4){
+ return;
+ }
+
+ }, [dict, talktimesRef ]);
+
+ const pageContainerStyle: React.CSSProperties = {
+ display: 'flex',
+ flexDirection: 'column',
+ height: '100vh', // 全画面の高さ
+ maxWidth: '800px', // 最大幅を制限して中央に寄せる
+ margin: '0 auto',
+ backgroundColor: 'white',
+ boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)',
+ };
+
+ const headerStyle: React.CSSProperties = {
+ padding: '15px',
+ backgroundColor: '#3b82f6', // blue-500
+ color: 'white',
+ textAlign: 'center',
+ fontWeight: 'bold',
+ fontSize: '20px',
+ };
+
+ const messageListStyle: React.CSSProperties = {
+ flexGrow: 1, // 残りのスペースをすべて占める
+ padding: '15px',
+ overflowY: 'auto', // スクロール可能にする
+ };
+
+ // for audio
+
+ useEffect(() => {
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
+ }, [messages]);
+
+ useEffect(() => {
+ const loadData = async () => {
+ const loadedDict = await LoadDict(dictPath);
+ setDict(loadedDict);
+ }
+ loadData();
+ }, []);
+
+ useEffect(() => {
+ init();
+ }, []);
+
+ // function ChatPage's return
+
+ return (
+ <div style={pageContainerStyle}>
+ <div style={headerStyle}>
+ チャット
+ </div>
+
+ <div style={messageListStyle}>
+ {messages.map((msg) => (
+ <MessageBubble key={msg.id} message={msg} />
+ ))}
+ <div ref={messagesEndRef} />
+ </div>
+
+ <MessageInput onSend={handleSendMessage} />
+ </div>
+ );
+ }
+
+ async function LoadDict(dictPath: string): Promise<Uint8Array> {
+ try{
+ const response = await fetch(dictPath);
+ if(!response.ok){
+ throw new Error(`fail to fetch file: ${response.statusText}`);
+ }
+ const arrayBuffer = await response.arrayBuffer();
+ const byte = new Uint8Array(arrayBuffer);
+ return byte;
+ } catch (error) {
+ console.log("error occur",error);
+ }
+ return new Uint8Array(0);
+ }
diff --git a/app/components/7_vr.tsx b/app/components/7_vr.tsx
@@ -0,0 +1,230 @@
+'use client';
+
+import { Canvas, useFrame } from '@react-three/fiber';
+import { OrbitControls, Sky, ContactShadows, Cloud, Clouds } from '@react-three/drei';
+import BenchScene from './BenchScene.tsx';
+import { useState, useMemo, useRef, useEffect } from 'react';
+import * as THREE from 'three';
+import { StageProps } from '../ctrl/page.tsx';
+
+// --- オーディオ関連の定数とカスタムフック ---
+const AUDIO_SOURCES: Record<number, string> = {
+ 2: '/audio/002.wav',
+};
+
+// シーケンシャルな音声再生と状態遷移を制御するカスタムフック (修正版)
+const useSequentialAudio = (initialState: number, audioSources: Record<number, string>, onComplete: () => void) => {
+ const [sceneState, setSceneState] = useState(initialState);
+ const audioRef = useRef<HTMLAudioElement | null>(null);
+
+ // 現在の状態に対応する音源のURLを取得
+ const currentAudioUrl = audioSources[sceneState];
+
+ // 音声再生のロジック
+ useEffect(() => {
+ // --- 1. 終了条件のチェック ---
+ if (sceneState > Math.max(...Object.keys(audioSources).map(Number))) {
+ // 定義された全ての音源が再生された後、onCompleteを実行
+ console.log("All audio finished. Calling onComplete.");
+ onComplete();
+ return;
+ }
+
+ // 現在の状態に対応する音源がない場合、ここで処理を停止
+ if (!currentAudioUrl) {
+ return;
+ }
+
+ console.log(`Starting setup for state ${sceneState}: ${currentAudioUrl}`);
+
+ // --- 2. 古いAudioオブジェクトのクリーンアップ ---
+ const existingAudio = audioRef.current;
+ if (existingAudio) {
+ existingAudio.pause();
+ // イベントリスナーの削除 (次のステップで新しいAudioオブジェクトにリスナーを設定するため)
+ // このクリーンアップが重要です。古いAudioオブジェクトが'ended'イベントをトリガーするのを防ぎます。
+ // `oncanplaythrough`リスナーはここでは不要ですが、もしあれば削除すべきです。
+ // Audioオブジェクトのライフサイクルを明確にするため、毎回新しいインスタンスを生成します。
+ }
+
+ // --- 3. 新しいAudioオブジェクトの作成と設定 ---
+ const audio = new Audio(currentAudioUrl);
+ audioRef.current = audio;
+
+ // 再生が終了したときのハンドラを定義
+ const handleAudioEnded = () => {
+ console.log(`Audio ${sceneState} finished. Transitioning state.`);
+ // 次の状態へ遷移 (例: 1 -> 2, 2 -> 3)
+ setSceneState(prev => prev + 1);
+ };
+
+ audio.addEventListener('ended', handleAudioEnded);
+
+ // --- 4. 再生の開始 ---
+ // ロードやイベントを待たずに、すぐに再生を試みる
+ audio.play().catch(e => {
+ console.warn(`Audio playback failed for state ${sceneState} (requires user interaction):`, e);
+ // ユーザーに最初のクリックを促すメッセージなどを表示すると良い
+ });
+
+
+ // --- 5. クリーンアップ関数 ---
+ return () => {
+ // アンマウント/状態遷移時に現在のAudioオブジェクトを確実に停止し、リスナーを削除
+ if (audio === audioRef.current) { // 現在設定したAudioオブジェクトであることを確認
+ audio.pause();
+ audio.removeEventListener('ended', handleAudioEnded);
+ }
+ };
+
+ }, [sceneState, audioSources, onComplete]); // sceneStateとcurrentAudioUrlは基本的に連動するため、sceneStateを依存配列に含める
+
+ return sceneState;
+};
+
+// 雲のランダム生成のための型
+interface RandomCloudsProps {
+ count: number;
+ radius: number;
+ height: number;
+ sceneState: number;
+}
+
+
+// 状態に応じてライトと霧を制御するコンポーネント
+function SceneController({ sceneState }: { sceneState: number }) {
+
+ const isFoggy = sceneState === 2;
+
+ const directionalLightIntensity = isFoggy ? 0.1 : 2;
+
+ const ambientLightIntensity = isFoggy ? 0.05 : 0.5;
+
+ useFrame(({ scene }) => {
+ if (isFoggy) {
+ scene.fog = new THREE.Fog(0x444444, 5, 40); // Far distanceも少し短くして密度を上げる
+ } else {
+ scene.fog = null;
+ }
+ });
+
+ return (
+ <>
+ <ambientLight intensity={ambientLightIntensity} />
+
+ <directionalLight
+ position={[5, 10, 5]}
+ intensity={directionalLightIntensity} // ここで制御
+ castShadow
+ shadow-mapSize-width={2048}
+ shadow-mapSize-height={2048}
+ shadow-camera-far={50}
+ shadow-camera-left={-10}
+ shadow-camera-right={10}
+ shadow-camera-top={10}
+ shadow-camera-bottom={-10}
+ />
+ </>
+ );
+}
+
+
+function AnimatedSky({sceneState}:{sceneState:number}) {
+ const [sunPosition, setSunPosition] = useState<[number, number, number]>([0, 100, 0]);
+
+ useFrame(({ clock }) => {
+ const time = clock.getElapsedTime() * 0.1;
+ const x = Math.sin(time) * 100;
+ const z = Math.cos(time) * 100;
+ setSunPosition([x, 100, z]);
+ });
+
+ if (sceneState === 2){
+ return null;
+ }
+
+ return (
+ <Sky
+ distance={450000}
+ sunPosition={new THREE.Vector3(...sunPosition)}
+ inclination={sceneState === 2 ? 0.001 : 0.6}
+ azimuth={0.25}
+ mieCoefficient={0.005}
+ mieDirectionalG={0.8}
+ rayleigh={0.5}
+ turbidity={10}
+ />
+ );
+}
+
+
+function RandomClouds({ count, radius, height, sceneState }: RandomCloudsProps) {
+ const cloudColor = sceneState === 2 ? '#080808' : '#fff';
+ const baseOpacity = sceneState === 2 ? 0.9 : 0.5;
+
+ const cloudConfigs = useMemo(() => {
+ return Array.from({ length: count }).map((_, i) => {
+ const angle = Math.random() * Math.PI * 2;
+ const distance = Math.sqrt(Math.random()) * radius;
+
+ const x = Math.cos(angle) * distance;
+ const z = Math.sin(angle) * distance;
+
+ return {
+ position: [
+ x,
+ height + (Math.random() - 0.5) * 5,
+ z,
+ ] as [number, number, number],
+
+ bounds: [
+ Math.random() * 10 + 10,
+ Math.random() * 2 + 1,
+ Math.random() * 10 + 10,
+ ] as [number, number, number],
+ seed: Math.floor(Math.random() * 1000) + i,
+ volume: Math.random() * 20 + 5,
+ opacity: Math.random() * 0.4 + baseOpacity,
+ color: cloudColor,
+ };
+ });
+ }, [count, radius, height]);
+
+ return (
+ <>
+ {cloudConfigs.map((config, index) => (
+ <Cloud key={index} {...config} />
+ ))}
+ </>
+ );
+}
+
+
+export default function ThreeVR({onComplete}:StageProps) {
+ // 💡 状態定義: number型
+ const sceneState = useSequentialAudio(2, AUDIO_SOURCES, onComplete);
+
+ return (
+ <Canvas
+ key="r3f-main-canvas"
+ camera={{ position: [5, 5, 5], fov: 60 }}
+ shadows
+ // 霧の状態によってクリアカラー(背景色)を変える
+ style={{ background: sceneState === 2 ? '#050507' : '#d0e0ff' }}
+ >
+ <AnimatedSky sceneState={sceneState}/>
+
+ <Clouds material={THREE.MeshLambertMaterial} limit={200}>
+ <RandomClouds count={100} radius={80} height={15} sceneState={sceneState}/>
+ </Clouds>
+
+ {/* 💡 SceneControllerを配置し、状態を渡す */}
+ <SceneController sceneState={sceneState} />
+
+ <BenchScene />
+ <ContactShadows position={[0, -0.5, 0]} opacity={0.7} scale={10} blur={1} far={10} />
+ <OrbitControls enableZoom={true} enablePan={true} enableRotate={true} />
+
+ </Canvas>
+ );
+}
diff --git a/app/components/8_lightactivity.module.css b/app/components/8_lightactivity.module.css
@@ -0,0 +1,20 @@
+/* 背景のチカチカ */
+@keyframes background-flicker {
+ 0%, 100% { background-color: #000; } /* 黒 */
+ 10%, 30%, 50%, 70%, 90% { background-color: #fff; } /* 白 */
+ 20%, 40%, 60%, 80% { background-color: #000; }
+}
+
+.flicker-background {
+ animation: background-flicker 0.1s infinite step-end; /* 高速で不連続な点滅 */
+}
+
+/* 文字のチカチカ */
+@keyframes text-flicker {
+ 0%, 100% { color: #fff; text-shadow: 0 0 5px rgba(255, 255, 255, 0.5); }
+ 50% { color: #f00; text-shadow: none; } /* 赤く光らせたり、消したり */
+}
+
+.flicker-text {
+ animation: text-flicker 0.05s infinite step-end;
+}
diff --git a/app/components/8_lightactivity.tsx b/app/components/8_lightactivity.tsx
@@ -0,0 +1,107 @@
+'use clinet'
+
+import React, { useState, useEffect } from 'react';
+import styled, { keyframes, css } from 'styled-components';
+
+// 演出データ
+const flickerEvents = [
+ { word: "TSCHERNOBYL", duration: 500, delay: 1000 },
+ { word: "HARRISBURG", duration: 600, delay: 5000 },
+ // ... その他の単語
+];
+
+type FlickerEvent = typeof flickerEvents[number];
+
+// --- 1. CSS-in-JSによるアニメーション定義 ---
+
+// 背景のチカチカアニメーション
+const backgroundFlicker = keyframes`
+0%, 100% { background-color: #000; }
+10%, 30%, 50%, 70%, 90% { background-color: #fff; }
+20%, 40%, 60%, 80% { background-color: #000; }
+`;
+
+// 文字のチカチカアニメーション
+const textFlicker = keyframes`
+0%, 100% { color: #fff; text-shadow: 0 0 5px rgba(255, 255, 255, 0.5); }
+50% { color: #f00; text-shadow: none; }
+`;
+
+// --- 2. スタイル付きコンポーネントの定義 ---
+
+// 背景を制御するコンテナ
+const VisualContainer = styled.div<{ $isFlickering: boolean }>`
+/* 基本スタイル */
+ display: flex;
+justify-content: center;
+align-items: center;
+width: 100%;
+height: 100vh; /* 画面全体に広げるための例 */
+ background-color: black;
+transition: background-color 0.1s;
+
+/* フリッカー適用 */
+ ${props => props.$isFlickering && css`
+ animation: ${backgroundFlicker} 0.1s infinite step-end;
+ `}
+ `;
+
+ // テキストを制御するH1要素
+ const FlickerText = styled.h1<{ $isFlickering: boolean }>`
+ /* 基本スタイル */
+ font-size: 8rem;
+ font-weight: bold;
+ color: white;
+ margin: 0;
+
+ /* フリッカー適用 */
+ ${props => props.$isFlickering && css`
+ animation: ${textFlicker} 0.9s infinite step-end;
+ `}
+ `;
+
+
+const FlickerVisualizer: React.FC = () => {
+ const [currentWord, setCurrentWord] = useState<string>('');
+ const [isFlickering, setIsFlickering] = useState<boolean>(false);
+ const startTime = React.useRef(Date.now()); // 演出開始時間を保持
+
+ useEffect(() => {
+ // 演出の開始時刻を固定
+ const startOffset = startTime.current;
+
+ const tick = () => {
+ const elapsedTime = Date.now() - startOffset;
+
+ // 現在のイベント判定ロジック
+ const activeEvent = flickerEvents.find(event =>
+ elapsedTime >= event.delay && elapsedTime < (event.delay + event.duration)
+ );
+
+ if (activeEvent) {
+ setCurrentWord(activeEvent.word);
+ setIsFlickering(true);
+ } else {
+ setIsFlickering(false);
+ // フリッカーが終わった後も単語を一定時間表示し続けるなどの調整はここで行う
+ }
+
+ // 次のフレームで再実行 (よりスムーズなアニメーション処理)
+ // 今回はタイミングが重要なため、setIntervalの代わりに使用
+ requestAnimationFrame(tick);
+ };
+
+ const animationFrameId = requestAnimationFrame(tick);
+
+ return () => cancelAnimationFrame(animationFrameId);
+ }, []); // 依存配列は空で、マウント時に一度だけ実行
+
+ return (
+ <VisualContainer $isFlickering={isFlickering}>
+ <FlickerText $isFlickering={isFlickering}>
+ {currentWord}
+ </FlickerText>
+ </VisualContainer>
+ );
+};
+export default FlickerVisualizer
diff --git a/app/components/wasmCheck.ts b/app/components/wasmCheck.ts
@@ -0,0 +1,16 @@
+// utils/featureChecks.ts
+
+/**
+ * ブラウザがWebAssemblyをサポートしているかを確認します。
+ * クライアントサイドでのみ実行する必要があります。
+ * @returns {boolean} WASMがサポートされていれば true
+ */
+export const isWasmSupported = (): boolean => {
+ // サーバーサイドレンダリング (SSR) を回避
+ if (typeof window === 'undefined') {
+ return false;
+ }
+
+ // グローバルスコープで `WebAssembly` オブジェクトが存在するかチェック
+ return typeof WebAssembly !== 'undefined';
+};
diff --git a/app/ctrl/page.tsx b/app/ctrl/page.tsx
@@ -22,16 +22,28 @@ const ThreeVR = dynamic<StageProps>(()=>import('../components/3_vr.tsx'),{
const OilArt = dynamic<StageProps>(()=>import('../components/4_oil.tsx'),{
loading: Loading,
});
+const FifthTitle = dynamic<StageProps>(()=>import('../components/5_title.tsx'),{
+ loading: Loading,
+});
+const SixthChat = dynamic<StageProps>(()=>import('../components/6_chat.tsx'),{
+ loading: Loading,
+});
+const SeventhVR = dynamic<StageProps>(()=>import('../components/7_vr.tsx'),{
+ loading: Loading,
+});
+const EighthLightActivity = dynamic<StageProps>(()=>import('../components/8_lightactivity.tsx'),{
+ loading: Loading,
+});
const Error = dynamic<StageProps>(() => import('../components/error.tsx'), {
loading: Loading,
});
-export default function Chat() {
+export default function Play() {
const pageRef = useRef<HTMLDivElement>(null);
const [isFullscreen, setIsFullscreen] = useState(false);
const [isInitialCheckDone, setIsInitialCheckDone] = useState(false);
- const [ stage, setStage ] = useState(4);
+ const [ stage, setStage ] = useState(8);
const handleStageComplete = useCallback(() => {
setStage(stage + 1);
}, []);
@@ -51,6 +63,18 @@ export default function Chat() {
case 4:
StageComponent = <OilArt onComplete={handleStageComplete}/>
break;
+ case 5:
+ StageComponent = <FifthTitle onComplete={handleStageComplete}/>
+ break;
+ case 6:
+ StageComponent = <SixthChat onComplete={handleStageComplete}/>
+ break;
+ case 7:
+ StageComponent = <SeventhVR onComplete={handleStageComplete}/>
+ break;
+ case 8:
+ StageComponent = <EighthLightActivity onComplete={handleStageComplete}/>
+ break;
default:
StageComponent = <Error onComplete={handleStageComplete}/>;
}
@@ -101,7 +125,7 @@ export default function Chat() {
}
return (
- <div ref={pageRef} style={{ height: '100vh', width: '100vw' }}>
+ <main ref={pageRef} style={{ height: '100vh', width: '100vw' }}>
{
!isFullscreen ? (
<article style={{height: '100%',width: '100%', alignItems:'center', justifyContent:'center', display:'flex'}}>
@@ -111,7 +135,7 @@ export default function Chat() {
StageComponent
)
}
- </div>
+ </main>
);
};
diff --git a/app/page.tsx b/app/page.tsx
@@ -1,4 +1,23 @@
+'use client'
+import { useEffect, useState } from "react";
+import { isWasmSupported } from "./components/wasmCheck.ts";
+
+const WasmCheck: React.FC = () => {
+ const [isSupported, setIsSupported] = useState<boolean | null>(null);
+ useEffect(()=>{
+ setIsSupported(isWasmSupported());
+ },[]);
+ if (isSupported === null){
+ return null;
+ }
+ if (isSupported === false){
+ return (
+ <h3>WASMがサポートされていません。</h3>
+ );
+ }
+ return null;
+}
export default function Home() {
return (
<main style={{paddingTop: "5rem", paddingLeft: "20vw", maxWidth: "60vw"}}>
@@ -27,6 +46,11 @@ export default function Home() {
<ul>
<li>音が出ます。</li>
<li>フルスクリーンを要求します。</li>
+ <li>JavaScript及びWASMを使用します。有効になっているか確認してください。</li>
+ <noscript>
+ JavaScriptが無効です。
+ </noscript>
+ <WasmCheck/>
</ul>
</article>
</main>
diff --git a/bun.lock b/bun.lock
@@ -6,10 +6,11 @@
"dependencies": {
"@react-three/drei": "^10.7.6",
"@react-three/fiber": "^9.4.0",
- "@sentry/nextjs": "10",
+ "@sentry/nextjs": "^10.22.0",
"next": "15.5.5",
"react": "19.1.0",
"react-dom": "19.1.0",
+ "styled-components": "^6.1.19",
"three": "^0.180.0",
},
"devDependencies": {
@@ -70,6 +71,12 @@
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
+ "@emotion/is-prop-valid": ["@emotion/is-prop-valid@1.2.2", "", { "dependencies": { "@emotion/memoize": "^0.8.1" } }, "sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw=="],
+
+ "@emotion/memoize": ["@emotion/memoize@0.8.1", "", {}, "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA=="],
+
+ "@emotion/unitless": ["@emotion/unitless@0.8.1", "", {}, "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ=="],
+
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g=="],
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="],
@@ -398,6 +405,8 @@
"@types/stats.js": ["@types/stats.js@0.17.4", "", {}, "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA=="],
+ "@types/stylis": ["@types/stylis@4.2.5", "", {}, "sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw=="],
+
"@types/tedious": ["@types/tedious@4.0.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw=="],
"@types/three": ["@types/three@0.180.0", "", { "dependencies": { "@dimforge/rapier3d-compat": "~0.12.0", "@tweenjs/tween.js": "~23.1.3", "@types/stats.js": "*", "@types/webxr": "*", "@webgpu/types": "*", "fflate": "~0.8.2", "meshoptimizer": "~0.22.0" } }, "sha512-ykFtgCqNnY0IPvDro7h+9ZeLY+qjgUWv+qEvUt84grhenO60Hqd4hScHE7VTB9nOQ/3QM8lkbNE+4vKjEpUxKg=="],
@@ -580,6 +589,8 @@
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
+ "camelize": ["camelize@1.0.1", "", {}, "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ=="],
+
"camera-controls": ["camera-controls@3.1.0", "", { "peerDependencies": { "three": ">=0.126.1" } }, "sha512-w5oULNpijgTRH0ARFJJ0R5ct1nUM3R3WP7/b8A6j9uTGpRfnsypc/RBMPQV8JQDPayUe37p/TZZY1PcUr4czOQ=="],
"caniuse-lite": ["caniuse-lite@1.0.30001750", "", {}, "sha512-cuom0g5sdX6rw00qOoLNSFCJ9/mYIsuSOA+yzpDw8eopiFqcVwQvZHqov0vmEighRxX++cfC0Vg1G+1Iy/mSpQ=="],
@@ -610,6 +621,10 @@
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
+ "css-color-keywords": ["css-color-keywords@1.0.0", "", {}, "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg=="],
+
+ "css-to-react-native": ["css-to-react-native@3.2.0", "", { "dependencies": { "camelize": "^1.0.0", "css-color-keywords": "^1.0.0", "postcss-value-parser": "^4.0.2" } }, "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ=="],
+
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
"damerau-levenshtein": ["damerau-levenshtein@1.0.8", "", {}, "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA=="],
@@ -1008,6 +1023,8 @@
"postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
+ "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
+
"postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="],
"postgres-bytea": ["postgres-bytea@1.0.0", "", {}, "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w=="],
@@ -1088,6 +1105,8 @@
"set-proto": ["set-proto@1.0.0", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0" } }, "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw=="],
+ "shallowequal": ["shallowequal@1.1.0", "", {}, "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ=="],
+
"sharp": ["sharp@0.34.4", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.0", "semver": "^7.7.2" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.4", "@img/sharp-darwin-x64": "0.34.4", "@img/sharp-libvips-darwin-arm64": "1.2.3", "@img/sharp-libvips-darwin-x64": "1.2.3", "@img/sharp-libvips-linux-arm": "1.2.3", "@img/sharp-libvips-linux-arm64": "1.2.3", "@img/sharp-libvips-linux-ppc64": "1.2.3", "@img/sharp-libvips-linux-s390x": "1.2.3", "@img/sharp-libvips-linux-x64": "1.2.3", "@img/sharp-libvips-linuxmusl-arm64": "1.2.3", "@img/sharp-libvips-linuxmusl-x64": "1.2.3", "@img/sharp-linux-arm": "0.34.4", "@img/sharp-linux-arm64": "0.34.4", "@img/sharp-linux-ppc64": "0.34.4", "@img/sharp-linux-s390x": "0.34.4", "@img/sharp-linux-x64": "0.34.4", "@img/sharp-linuxmusl-arm64": "0.34.4", "@img/sharp-linuxmusl-x64": "0.34.4", "@img/sharp-wasm32": "0.34.4", "@img/sharp-win32-arm64": "0.34.4", "@img/sharp-win32-ia32": "0.34.4", "@img/sharp-win32-x64": "0.34.4" } }, "sha512-FUH39xp3SBPnxWvd5iib1X8XY7J0K0X7d93sie9CJg2PO8/7gmg89Nve6OjItK53/MlAushNNxteBYfM6DEuoA=="],
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
@@ -1136,8 +1155,12 @@
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
+ "styled-components": ["styled-components@6.1.19", "", { "dependencies": { "@emotion/is-prop-valid": "1.2.2", "@emotion/unitless": "0.8.1", "@types/stylis": "4.2.5", "css-to-react-native": "3.2.0", "csstype": "3.1.3", "postcss": "8.4.49", "shallowequal": "1.1.0", "stylis": "4.3.2", "tslib": "2.6.2" }, "peerDependencies": { "react": ">= 16.8.0", "react-dom": ">= 16.8.0" } }, "sha512-1v/e3Dl1BknC37cXMhwGomhO8AkYmN41CqyX9xhUDxry1ns3BFQy2lLDRQXJRdVVWB9OHemv/53xaStimvWyuA=="],
+
"styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="],
+ "stylis": ["stylis@4.3.2", "", {}, "sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg=="],
+
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
@@ -1172,7 +1195,7 @@
"tsconfig-paths": ["tsconfig-paths@3.15.0", "", { "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg=="],
- "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
+ "tslib": ["tslib@2.6.2", "", {}, "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="],
"tunnel-rat": ["tunnel-rat@0.1.2", "", { "dependencies": { "zustand": "^4.3.2" } }, "sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ=="],
@@ -1246,6 +1269,12 @@
"@babel/core/json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
+ "@emnapi/core/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
+
+ "@emnapi/runtime/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
+
+ "@emnapi/wasi-threads/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
+
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
"@opentelemetry/instrumentation-http/@opentelemetry/core": ["@opentelemetry/core@2.1.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-RMEtHsxJs/GiHHxYT58IY57UXAQTuUnZVco6ymDEqTNlJKTimM4qPUPVe8InNFyBjhHBEAx4k3Q8LtNayBsbUQ=="],
@@ -1256,6 +1285,10 @@
"@sentry/node/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
+ "@swc/helpers/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
+
+ "@tybys/wasm-util/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
+
"@types/three/fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="],
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
@@ -1312,6 +1345,8 @@
"stats-gl/three": ["three@0.170.0", "", {}, "sha512-FQK+LEpYc0fBD+J8g6oSEyyNzjp+Q7Ks1C568WWaoMRLW+TkNNWmenWeGgJjV105Gd+p/2ql1ZcjYvNiPZBhuQ=="],
+ "styled-components/postcss": ["postcss@8.4.49", "", { "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA=="],
+
"tunnel-rat/zustand": ["zustand@4.5.7", "", { "dependencies": { "use-sync-external-store": "^1.2.2" }, "peerDependencies": { "@types/react": ">=16.8", "immer": ">=9.0.6", "react": ">=16.8" }, "optionalPeers": ["@types/react", "immer", "react"] }, "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw=="],
"webpack/eslint-scope": ["eslint-scope@5.1.1", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" } }, "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw=="],
diff --git a/package.json b/package.json
@@ -12,10 +12,11 @@
"dependencies": {
"@react-three/drei": "^10.7.6",
"@react-three/fiber": "^9.4.0",
- "@sentry/nextjs": "10",
+ "@sentry/nextjs": "^10.22.0",
"next": "15.5.5",
"react": "19.1.0",
"react-dom": "19.1.0",
+ "styled-components": "^6.1.19",
"three": "^0.180.0"
},
"devDependencies": {
diff --git a/public/audio/001.wav b/public/audio/001.wav
Binary files differ.
diff --git a/public/audio/002.wav b/public/audio/002.wav
Binary files differ.
diff --git a/public/audio/003.wav b/public/audio/003.wav
Binary files differ.
diff --git a/public/audio/004.wav b/public/audio/004.wav
Binary files differ.
diff --git a/public/audio/005.wav b/public/audio/005.wav
Binary files differ.
diff --git a/rust-wasm/pkg/rust_wasm.d.ts b/rust-wasm/pkg/rust_wasm.d.ts
@@ -1,5 +1,6 @@
/* tslint:disable */
/* eslint-disable */
+export function chat2(dict_data: Uint8Array, input: string): string;
export function chat(dict_data: Uint8Array, input: string): string;
/**
*
@@ -20,6 +21,7 @@ export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembl
export interface InitOutput {
readonly memory: WebAssembly.Memory;
+ readonly chat2: (a: number, b: number, c: number, d: number) => [number, number, number, number];
readonly chat: (a: number, b: number, c: number, d: number) => [number, number, number, number];
readonly __wbg_nearestpointresult_free: (a: number, b: number) => void;
readonly __wbg_get_nearestpointresult_x: (a: number) => number;
diff --git a/rust-wasm/pkg/rust_wasm.js b/rust-wasm/pkg/rust_wasm.js
@@ -101,6 +101,34 @@ function takeFromExternrefTable0(idx) {
* @param {string} input
* @returns {string}
*/
+export function chat2(dict_data, input) {
+ let deferred4_0;
+ let deferred4_1;
+ try {
+ const ptr0 = passArray8ToWasm0(dict_data, wasm.__wbindgen_malloc);
+ const len0 = WASM_VECTOR_LEN;
+ const ptr1 = passStringToWasm0(input, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
+ const len1 = WASM_VECTOR_LEN;
+ const ret = wasm.chat2(ptr0, len0, ptr1, len1);
+ var ptr3 = ret[0];
+ var len3 = ret[1];
+ if (ret[3]) {
+ ptr3 = 0; len3 = 0;
+ throw takeFromExternrefTable0(ret[2]);
+ }
+ deferred4_0 = ptr3;
+ deferred4_1 = len3;
+ return getStringFromWasm0(ptr3, len3);
+ } finally {
+ wasm.__wbindgen_free(deferred4_0, deferred4_1, 1);
+ }
+}
+
+/**
+ * @param {Uint8Array} dict_data
+ * @param {string} input
+ * @returns {string}
+ */
export function chat(dict_data, input) {
let deferred4_0;
let deferred4_1;
diff --git a/rust-wasm/pkg/rust_wasm_bg.wasm b/rust-wasm/pkg/rust_wasm_bg.wasm
Binary files differ.
diff --git a/rust-wasm/pkg/rust_wasm_bg.wasm.d.ts b/rust-wasm/pkg/rust_wasm_bg.wasm.d.ts
@@ -1,6 +1,7 @@
/* tslint:disable */
/* eslint-disable */
export const memory: WebAssembly.Memory;
+export const chat2: (a: number, b: number, c: number, d: number) => [number, number, number, number];
export const chat: (a: number, b: number, c: number, d: number) => [number, number, number, number];
export const __wbg_nearestpointresult_free: (a: number, b: number) => void;
export const __wbg_get_nearestpointresult_x: (a: number) => number;
diff --git a/rust-wasm/src/lib.rs b/rust-wasm/src/lib.rs
@@ -3,6 +3,39 @@ use vibrato::{Dictionary, Tokenizer};
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
+pub fn chat2(dict_data: &[u8], input: &str) -> Result<String, JsValue> {
+ let encoded = Cursor::new(dict_data);
+ let reader = zstd::Decoder::new(encoded).unwrap();
+ let dict = Dictionary::read(reader)
+ .map_err(|_|{JsValue::from(js_sys::Error::new("Dictionary road Error"))})?;
+ let tokenizer = Tokenizer::new(dict);
+ let mut worker = tokenizer.new_worker();
+
+ worker.reset_sentence(input);
+ worker.tokenize();
+
+ let mut ans : String= "どうせ失敗する。当たり前だ。そうに決まっている。".to_string();
+
+ match worker.num_tokens() {
+ 1..5 => {
+ ans = "は?".to_string();
+ },
+ 5..9 => {
+ ans = "期待した私が馬鹿だったの?".to_string();
+ },
+ 9 => {
+ ans = "お前のせいで私の人生がめちゃくちゃだ。".to_string();
+ },
+ 10..=usize::MAX => {
+ ans = "どうでもいいわ。勝手にすれば。私には関係ない。".to_string();
+ }
+ _ => {ans = "どうでもいいわ。勝手にすれば。私には関係ない。".to_string();},
+ }
+
+ Ok(ans)
+}
+
+#[wasm_bindgen]
pub fn chat(dict_data: &[u8], input: &str) -> Result<String, JsValue> {
let encoded = Cursor::new(dict_data);
let reader = zstd::Decoder::new(encoded).unwrap();