From efc513be150c2846a857710fd0434523300ca003 Mon Sep 17 00:00:00 2001 From: syui Date: Sat, 7 Mar 2026 03:35:29 +0900 Subject: [PATCH] init --- .github/workflows/cf-pages.yml | 33 +++++++ .gitignore | 13 +++ README.md | 4 + index.html | 17 ++++ package.json | 27 +++++ public/CNAME | 1 + src/App.jsx | 174 +++++++++++++++++++++++++++++++++ src/AtmosphereScene.jsx | 128 ++++++++++++++++++++++++ src/AvatarScene.jsx | 171 ++++++++++++++++++++++++++++++++ src/SkillEffects.jsx | 149 ++++++++++++++++++++++++++++ src/VrmCharacter.jsx | 160 ++++++++++++++++++++++++++++++ src/controls/CameraRig.jsx | 57 +++++++++++ src/controls/KeyInput.js | 32 ++++++ src/main.jsx | 9 ++ src/ui/AnimationUI.jsx | 25 +++++ src/ui/LocationUI.jsx | 25 +++++ src/worldState.js | 71 ++++++++++++++ vite.config.ts | 7 ++ 18 files changed, 1103 insertions(+) create mode 100644 .github/workflows/cf-pages.yml create mode 100644 .gitignore create mode 100644 README.md create mode 100644 index.html create mode 100644 package.json create mode 100644 public/CNAME create mode 100644 src/App.jsx create mode 100644 src/AtmosphereScene.jsx create mode 100644 src/AvatarScene.jsx create mode 100644 src/SkillEffects.jsx create mode 100644 src/VrmCharacter.jsx create mode 100644 src/controls/CameraRig.jsx create mode 100644 src/controls/KeyInput.js create mode 100644 src/main.jsx create mode 100644 src/ui/AnimationUI.jsx create mode 100644 src/ui/LocationUI.jsx create mode 100644 src/worldState.js create mode 100644 vite.config.ts diff --git a/.github/workflows/cf-pages.yml b/.github/workflows/cf-pages.yml new file mode 100644 index 0000000..a1a1f00 --- /dev/null +++ b/.github/workflows/cf-pages.yml @@ -0,0 +1,33 @@ +name: cf pages + +on: + push: + branches: + - main + +env: + GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} + +jobs: + build-deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 25 + - run: | + npm i + git clone https://syu:${GITEA_TOKEN}@git.syui.ai/ai/vrma + cp -rf ./vrma/* public/ + + - name: Build + run: npm run build + + - name: Deploy to Cloudflare Pages + uses: cloudflare/wrangler-action@v3 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + wranglerVersion: '4' + command: pages deploy dist --project-name=${{ secrets.CLOUDFLARE_PROJECT_NAME }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8ac532b --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +.DS_Store +/node_modules +/dist +.env +.env.production +package-lock.json +.claude +/claude.md +/CLAUDE.md +/public/animation +/public/model +/public/audio +/public/favicon.png diff --git a/README.md b/README.md new file mode 100644 index 0000000..d41ab4d --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ + +## vrma + +> ue5 -> glb -> blendder -> vrma diff --git a/index.html b/index.html new file mode 100644 index 0000000..253c4e8 --- /dev/null +++ b/index.html @@ -0,0 +1,17 @@ + + + + + + + + aivrm + + + +
+ + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..bb6faef --- /dev/null +++ b/package.json @@ -0,0 +1,27 @@ +{ + "name": "aivrm", + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@pixiv/three-vrm": "^3.4.4", + "@pixiv/three-vrm-animation": "^3.4.4", + "@react-three/drei": "^10.7.7", + "@react-three/fiber": "^9.4.0", + "@react-three/postprocessing": "^3.0.4", + "@takram/three-atmosphere": "^0.16.0", + "@takram/three-clouds": "^0.6.0", + "3d-tiles-renderer": "^0.4.18", + "react": "^19.0.0-rc.1", + "react-dom": "^19.0.0-rc.1", + "three": "^0.181.2" + }, + "devDependencies": { + "@vitejs/plugin-react": "^4.2.1", + "vite": "^7.2.7" + } +} diff --git a/public/CNAME b/public/CNAME new file mode 100644 index 0000000..63a8a50 --- /dev/null +++ b/public/CNAME @@ -0,0 +1 @@ +vrm.syui.ai diff --git a/src/App.jsx b/src/App.jsx new file mode 100644 index 0000000..96239a0 --- /dev/null +++ b/src/App.jsx @@ -0,0 +1,174 @@ +import React, { useState, useEffect, useCallback, useRef } from 'react'; +import { Canvas } from '@react-three/fiber'; + +import AtmosphereScene from './AtmosphereScene'; +import AvatarScene from './AvatarScene'; +import { LOCATIONS, teleportTo } from './worldState'; +import { adminMode, onAdminChange } from './controls/KeyInput'; + +const ACTION_SEQUENCE = ['attack', 'skill', 'jump', 'fly_dodge', 'damage']; +const TELEPORT_ANIM = 'fly_dodge'; +const AUTO_INTERVAL_MIN = 15000; +const AUTO_INTERVAL_MAX = 40000; +const NASA_URL = 'https://eyes.nasa.gov/apps/solar-system/#/earth?featured=false&detailPanel=false&logo=false&search=false&shareButton=false&menu=false&collapseSettingsOptions=true&hideFullScreenToggle=true&locked=true&hideExternalLinks=true&lighting=flood'; + +export default function App() { + const [animState, setAnimState] = useState({ name: 'fly_idle', count: 0 }); + const [isAdmin, setIsAdmin] = useState(false); + const [view, setView] = useState('avatar'); // 'avatar' | 'nasa' + const actionIndexRef = useRef(0); + const teleportIndexRef = useRef(0); + const countRef = useRef(0); + + useEffect(() => { + return onAdminChange((v) => setIsAdmin(v)); + }, []); + + const playAnim = useCallback((name) => { + countRef.current += 1; + setAnimState({ name, count: countRef.current }); + }, []); + + const handleKey = useCallback((e) => { + if (e.code === 'Escape') { + setView(view === 'nasa' ? 'avatar' : 'nasa'); + return; + } + if (e.code === 'Space') { + e.preventDefault(); + const idx = actionIndexRef.current; + const anim = ACTION_SEQUENCE[idx % ACTION_SEQUENCE.length]; + playAnim(anim); + actionIndexRef.current = idx + 1; + } else if (e.code === 'KeyT') { + e.preventDefault(); + const idx = teleportIndexRef.current; + const loc = LOCATIONS[idx % LOCATIONS.length]; + playAnim(TELEPORT_ANIM); + teleportTo(loc); + teleportIndexRef.current = idx + 1; + } else if (e.code === 'KeyS') { + e.preventDefault(); + playAnim('skill'); + } + }, [playAnim, view]); + + useEffect(() => { + window.addEventListener('keydown', handleKey); + return () => window.removeEventListener('keydown', handleKey); + }, [handleKey]); + + const viewRef = useRef(view); + viewRef.current = view; + + useEffect(() => { + const scheduleNext = () => { + const delay = AUTO_INTERVAL_MIN + Math.random() * (AUTO_INTERVAL_MAX - AUTO_INTERVAL_MIN); + return setTimeout(() => { + // NASAビュー中はスキップ + if (viewRef.current !== 'nasa') { + const idx = teleportIndexRef.current; + const loc = LOCATIONS[idx % LOCATIONS.length]; + playAnim(TELEPORT_ANIM); + teleportTo(loc); + teleportIndexRef.current = idx + 1; + } + timerRef.current = scheduleNext(); + }, delay); + }; + const timerRef = { current: scheduleNext() }; + return () => clearTimeout(timerRef.current); + }, [playAnim]); + + const doTeleport = useCallback(() => { + const idx = teleportIndexRef.current; + const loc = LOCATIONS[idx % LOCATIONS.length]; + playAnim(TELEPORT_ANIM); + teleportTo(loc); + teleportIndexRef.current = idx + 1; + }, [playAnim]); + + const doSkill = useCallback(() => { + playAnim('skill'); + }, [playAnim]); + + const appStartRef = useRef(Date.now()); + const handleZoomOut = useCallback(() => { + // 起動後15秒は切り替え無効 + if (Date.now() - appStartRef.current < 15000) return; + setView('nasa'); + }, []); + + const layerStyle = { position: 'absolute', top: 0, left: 0, width: '100%', height: '100%' }; + + const btnStyle = { + padding: '8px 16px', + background: 'rgba(255,255,255,0.12)', + color: '#fff', + border: '1px solid rgba(255,255,255,0.25)', + borderRadius: '6px', + cursor: 'pointer', + fontSize: '13px', + backdropFilter: 'blur(4px)', + }; + + const isNasa = view === 'nasa'; + + return ( +
+ {/* NASA iframe layer - fully interactive */} +