1
0

add atmosphere

This commit is contained in:
2025-11-20 19:39:03 +09:00
parent 8dddebec16
commit bc8ece5c63
78 changed files with 6306 additions and 410 deletions

View File

@@ -4,6 +4,7 @@ on:
push: push:
branches: branches:
- main - main
environment: gh-pages
env: env:
GITEA_MAIL: ${{ secrets.GITEA_MAIL }} GITEA_MAIL: ${{ secrets.GITEA_MAIL }}
@@ -16,26 +17,27 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 21 node-version: 25
ref: main cache: 'npm'
submodules: true cache-dependency-path: atmosphere/package-lock.json
fetch-depth: 0
- run: | - run: |
yarn install npm ci
rm -rf dist/vrma rm -rf dist/vrma
git clone https://${GITEA_TOKEN}@git.syui.ai/ai/vrma dist/vrma git clone https://syu:${GITEA_TOKEN}@git.syui.ai/ai/vrma
rm -rf dist/vrma/.git cp -rf ./vrma/model/ai.vrm public/
cp -rf ./vrma/anime/idle.vrma public/
working-directory: min-react/
- name: Build - name: Build
env: env:
TZ: "Asia/Tokyo" TZ: "Asia/Tokyo"
run: | run: npm run build
yarn build working-directory: min-react/
- name: Deploy - name: Deploy
uses: peaceiris/actions-gh-pages@v3 uses: peaceiris/actions-gh-pages@v3
with: with:
github_token: ${{ secrets.GITHUB_TOKEN }} github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./dist publish_dir: ./min-react/dist
user_name: 'ai[bot]' user_name: 'ai[bot]'
user_email: '138105980+yui-syui-ai[bot]@users.noreply.github.com' user_email: '138105980+yui-syui-ai[bot]@users.noreply.github.com'

8
.gitignore vendored
View File

@@ -1,5 +1,4 @@
node_modules node_modules
*package-lock.json
example example
*yarn.lock *yarn.lock
**DS_Store **DS_Store
@@ -7,3 +6,10 @@ dist/*.js
*/node_modules */node_modules
*/public/models */public/models
*/dist/models */dist/models
dist
three-geospatial
**.env
.env
.env.production
**.vrm
**.vrma

3
.gitmodules vendored
View File

@@ -1,3 +0,0 @@
[submodule "dist/vrma"]
path = dist/vrma
url = git@git.syui.ai:ai/vrma

15
atmosphere/README.md Normal file
View File

@@ -0,0 +1,15 @@
## three-geospatial Clouds 3D Tiles Renderer Integration Example
```sh
$ git clone https://github.com/takram-design-engineering/three-geospatial
$ cd three-geospatial
$ echo STORYBOOK_GOOGLE_MAP_API_KEY=xxx >> .env
$ pnpm nx storybook
```
`clouds-3d-tiles-renderer-integration--tokyo`
[http://localhost:4400/?path=/story/clouds-3d-tiles-renderer-integration--tokyo](http://localhost:4400/?path=/story/clouds-3d-tiles-renderer-integration--tokyo)
![](./img/api.png)

BIN
atmosphere/img/api.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

16
atmosphere/index.html Normal file
View File

@@ -0,0 +1,16 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="referrer" content="strict-origin-when-cross-origin" />
<title>VRM Animation Preview</title>
<style>
html, body, #root { width: 100%; height: 100%; margin: 0; padding: 0; overflow: hidden; }
</style>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

2465
atmosphere/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,6 @@
{ {
"name": "min-react-vrm", "name": "react-vrm-atmosphere",
"version": "1.0.0", "version": "1.0.0",
"description": "Minimal VRM Animation Player",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
@@ -13,14 +12,16 @@
"@pixiv/three-vrm-animation": "^3.4.4", "@pixiv/three-vrm-animation": "^3.4.4",
"@react-three/drei": "^10.7.7", "@react-three/drei": "^10.7.7",
"@react-three/fiber": "^9.4.0", "@react-three/fiber": "^9.4.0",
"react": "^19.0.0", "@react-three/postprocessing": "^3.0.4",
"react-dom": "^19.0.0", "@takram/three-atmosphere": "^0.15.1",
"@takram/three-clouds": "^0.5.2",
"3d-tiles-renderer": "^0.4.18",
"react": "^19.0.0-rc.1",
"react-dom": "^19.0.0-rc.1",
"three": "^0.181.2" "three": "^0.181.2"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "^19.0.0", "@vitejs/plugin-react": "^4.2.1",
"@types/react-dom": "^19.0.0", "vite": "^5.1.0"
"@vitejs/plugin-react": "^4.3.4",
"vite": "^6.0.1"
} }
} }

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

668
atmosphere/src/App.jsx Normal file
View File

@@ -0,0 +1,668 @@
import React, { useEffect, useRef, Suspense, useState } from 'react';
import { Canvas, useFrame, useLoader, useThree } from '@react-three/fiber';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
import { VRMLoaderPlugin, VRMUtils } from '@pixiv/three-vrm';
import { createVRMAnimationClip, VRMAnimationLoaderPlugin } from '@pixiv/three-vrm-animation';
import { AnimationMixer, Vector3, MathUtils, Quaternion, Euler } from 'three';
import { OrbitControls, PerspectiveCamera } from '@react-three/drei';
import { EffectComposer, ToneMapping } from '@react-three/postprocessing';
import { ToneMappingMode } from 'postprocessing';
import * as THREE from 'three';
// Takram Libraries
import { AerialPerspective, Atmosphere } from '@takram/three-atmosphere/r3f';
import { Clouds, CloudLayer } from '@takram/three-clouds/r3f';
import { Geodetic, PointOfView, radians } from '@takram/three-geospatial';
// 3D Tiles Renderer
import { TilesPlugin, TilesRenderer } from '3d-tiles-renderer/r3f';
import {
GLTFExtensionsPlugin,
GoogleCloudAuthPlugin,
TileCompressionPlugin,
TilesFadePlugin,
UpdateOnChangePlugin,
} from '3d-tiles-renderer/plugins';
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader';
const BASE_URL = import.meta.env.BASE_URL;
const VRM_URL = `${BASE_URL}ai.vrm`;
const VRMA_URL = `${BASE_URL}fly_sky.vrma`;
const EARTH_RADIUS = 6378137;
const WEATHER_INTERVAL = 5 * 60 * 1000;
const TIME_SCALE = 100;
const INITIAL_DATE = new Date('2024-06-21T12:00:00');
// --- Weather Presets (天候設定) ---
const WEATHER_PRESETS = [
{
name: 'Clear (快晴)',
coverage: 0.1,
layers: [
{ channel: 'r', altitude: 1500, height: 500, densityScale: 0.0 },
{ channel: 'g', altitude: 2500, height: 800, densityScale: 0.0 },
{ channel: 'b', altitude: 7500, height: 500, densityScale: 0.1 },
]
},
{
name: 'Sunny (晴れ)',
coverage: 0.4,
layers: [
{ channel: 'r', altitude: 1500, height: 500, densityScale: 0.4 },
{ channel: 'g', altitude: 2500, height: 800, densityScale: 0.0 },
{ channel: 'b', altitude: 7500, height: 500, densityScale: 0.2 },
]
},
{
name: 'Cloudy (曇り)',
coverage: 0.75,
layers: [
{ channel: 'r', altitude: 1500, height: 500, densityScale: 0.6 },
{ channel: 'g', altitude: 2000, height: 1000, densityScale: 0.5 },
{ channel: 'b', altitude: 7500, height: 500, densityScale: 0.0 },
]
}
];
const LOCATIONS = [
{ name: 'Tokyo', longitude: 139.7671, latitude: 35.6812, heading: 180, pitch: -5, distance: 1100 },
{ name: 'Fuji', longitude: 138.7278, latitude: 35.3206, heading: 0, pitch: -10, distance: 4000 },
{ name: 'Space', longitude: 139.7671, latitude: 35.6812, heading: 0, pitch: -90, distance: 100000 },
];
// --- Shared State for Synchronization ---
// 複数のCanvas間で状態を共有するための簡易ストア
const worldState = {
// 世界座標 (Atmosphere内での位置 - ECEF)
position: new Vector3(0, EARTH_RADIUS + 100000, 0),
// ローカル回転 (AvatarLayerでの回転 - Y-up)
// Note: This is treated as the "Local" quaternion.
quaternion: new Quaternion(),
// 移動速度 (m/s)
speed: 1000.0,
};
// Helper: Calculate Basis Rotation (Alignment to Surface Normal)
function getSurfaceBasis(position) {
if (!position) return new Quaternion();
// Debug Geodetic API
if (!window.geodeticLogged) {
console.log('Geodetic prototype:', Geodetic.prototype);
window.geodeticLogged = true;
}
const up = position.clone().normalize();
if (up.lengthSq() < 0.1) return new Quaternion(); // Safety check
const basis = new Quaternion().setFromUnitVectors(new Vector3(0, 1, 0), up);
return basis;
}
// Helper: Calculate Ellipsoid Radius at current position (WGS84)
function getEllipsoidRadius(position) {
const a = 6378137.0;
const b = 6356752.314245;
const r = position.length();
if (r < 100) return a; // Fallback
// Assuming Z is North in ECEF (Standard)
const z = position.z;
const sinPhi = z / r;
const cosPhi = Math.sqrt(1 - sinPhi * sinPhi);
const aCos = a * cosPhi;
const bSin = b * sinPhi;
const num = (a * aCos) * (a * aCos) + (b * bSin) * (b * bSin);
const den = (aCos * aCos) + (bSin * bSin);
return Math.sqrt(num / den);
}
// Teleport Function
function teleportTo(location) {
const { longitude, latitude, heading, pitch, distance } = location;
const position = new Vector3();
const globalQuaternion = new Quaternion(); // Global rotation
const up = new Vector3(0, 1, 0);
// 1. Calculate True Position (at target lat/lon and altitude)
// Treat 'distance' as altitude in meters
const truePosition = new Geodetic(radians(longitude), radians(latitude), distance).toECEF();
// 2. Calculate Rotation using PointOfView
// We use PointOfView to get the correct orientation (looking at the target).
// We ignore the position calculated by PointOfView because it assumes an orbit/look-at constraint.
new PointOfView(distance, radians(heading), radians(pitch)).decompose(
new Geodetic(radians(longitude), radians(latitude)).toECEF(),
position, // Dummy position (ignored)
globalQuaternion,
up
);
console.log(`[Teleport] ${location.name} -> Altitude: ${distance}, TruePosLength: ${truePosition.length()}`);
// 3. Apply True Position
worldState.position.copy(truePosition);
// 4. Apply Rotation (Global -> Local)
// GlobalQ = BasisQ * LocalQ => LocalQ = BasisQ^-1 * GlobalQ
const basis = getSurfaceBasis(truePosition);
const basisInv = basis.clone().invert();
worldState.quaternion.copy(basisInv.multiply(globalQuaternion));
}
// キー入力管理
const keys = {
w: false,
a: false,
s: false,
d: false,
q: false,
e: false,
Shift: false,
Space: false,
};
// キーイベントリスナーの設定
if (typeof window !== 'undefined') {
window.addEventListener('keydown', (e) => {
const key = e.key.toLowerCase();
if (keys.hasOwnProperty(key)) keys[key] = true;
if (e.key === 'Shift') keys.Shift = true;
if (e.key === ' ') keys.Space = true;
});
window.addEventListener('keyup', (e) => {
const key = e.key.toLowerCase();
if (keys.hasOwnProperty(key)) keys[key] = false;
if (e.key === 'Shift') keys.Shift = false;
if (e.key === ' ') keys.Space = false;
});
}
// ---------------------------------------------------------
// Scene Components (Inside Canvas)
// ---------------------------------------------------------
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath('https://www.gstatic.com/draco/v1/decoders/');
function GoogleMaps3DTiles() {
const apiKey = import.meta.env.VITE_GOOGLE_MAP_API_KEY;
if (!apiKey) return null;
return (
<TilesRenderer url={`https://tile.googleapis.com/v1/3dtiles/root.json?key=${apiKey}`}>
<TilesPlugin plugin={GoogleCloudAuthPlugin} args={{ apiToken: apiKey }} />
<TilesPlugin plugin={GLTFExtensionsPlugin} dracoLoader={dracoLoader} />
<TilesPlugin plugin={TileCompressionPlugin} />
<TilesPlugin plugin={UpdateOnChangePlugin} />
<TilesPlugin plugin={TilesFadePlugin} />
</TilesRenderer>
);
}
// Canvasの内側で動作するメインシーンコンポーネント
function AtmosphereScene() {
const { gl } = useThree();
const sunRef = useRef();
const atmosphereRef = useRef();
const dateRef = useRef(new Date(INITIAL_DATE));
const [weather, setWeather] = useState(WEATHER_PRESETS[1]);
// 1. 露出設定 (Canvas内なのでuseThreeが使える)
useEffect(() => {
gl.toneMapping = THREE.NoToneMapping;
gl.toneMappingExposure = 10.0;
}, [gl]);
// 2. 天候の定期変更
useEffect(() => {
const interval = setInterval(() => {
setWeather(prev => {
const others = WEATHER_PRESETS.filter(w => w.name !== prev.name);
const next = others[Math.floor(Math.random() * others.length)];
console.log(`[Weather] Changing to: ${next.name}`);
return next;
});
}, WEATHER_INTERVAL);
return () => clearInterval(interval);
}, []);
// 3. 時間進行と太陽移動 (Canvas内なのでuseFrameが使える)
//useFrame((state, delta) => {
// const currentDate = dateRef.current;
// const elapsedMs = delta * TIME_SCALE * 1000;
// currentDate.setTime(currentDate.getTime() + elapsedMs);
// if (atmosphereRef.current) {
// atmosphereRef.current.updateByDate(currentDate);
// }
// if (sunRef.current) {
// const hours = currentDate.getHours() + currentDate.getMinutes() / 60 + currentDate.getSeconds() / 3600;
// const sunAngle = MathUtils.mapLinear(hours, 6, 18, 0, Math.PI);
// const sunX = -Math.cos(sunAngle);
// const sunY = Math.sin(sunAngle);
// if (hours < 6 || hours > 18) {
// sunRef.current.position.set(0, -1, 0);
// sunRef.current.intensity = 10.0;
// } else {
// sunRef.current.position.set(sunX, sunY, 0.2);
// sunRef.current.intensity = MathUtils.lerp(0.5, 3.0, sunY);
// }
// }
//});
useFrame((state, delta) => {
const currentDate = dateRef.current;
const elapsedMs = delta * TIME_SCALE * 1000;
currentDate.setTime(currentDate.getTime() + elapsedMs);
if (atmosphereRef.current) {
atmosphereRef.current.updateByDate(currentDate);
const sunDirection = atmosphereRef.current.sunDirection;
if (sunRef.current && sunDirection) {
sunRef.current.position.copy(sunDirection);
sunRef.current.intensity = 3.0; // 強度は固定で、時間帯による調整はAtmosphereが担当
if (sunDirection.y < -0.1) {
sunRef.current.intensity = 0.1;
}
}
}
});
return (
<>
{/* Camera is controlled by FollowCamera component */}
<PerspectiveCamera
makeDefault
near={10}
far={10000000}
fov={45}
/>
<FollowCamera />
<directionalLight
ref={sunRef}
position={[0, 1, 0]}
intensity={3.0}
castShadow
/>
<Atmosphere ref={atmosphereRef}>
<GoogleMaps3DTiles />
<EffectComposer multisampling={0} disableNormalPass={false}>
<Clouds disableDefaultLayers coverage={weather.coverage}>
{weather.layers.map((layer, index) => (
<CloudLayer
key={index}
channel={layer.channel}
altitude={layer.altitude}
height={layer.height}
densityScale={layer.densityScale}
shapeAmount={0.5}
/>
))}
</Clouds>
<AerialPerspective sky />
<ToneMapping mode={ToneMappingMode.AGX} />
</EffectComposer>
</Atmosphere>
</>
);
}
// Atmosphere側のカメラをShared Stateに同期させる
function FollowCamera() {
useFrame((state) => {
if (!worldState.position) return;
// 1. Calculate Basis Rotation based on current position
const basis = getSurfaceBasis(worldState.position);
// 2. Apply Position
state.camera.position.copy(worldState.position);
// 3. Apply Rotation: GlobalQ = BasisQ * LocalQ
// worldState.quaternion is treated as Local Quaternion here
state.camera.quaternion.copy(basis).multiply(worldState.quaternion);
});
return null;
}
// ---------------------------------------------------------
// Scene 2: Avatar
// ---------------------------------------------------------
function VrmCharacter() {
const mixerRef = useRef(null);
const vrmRef = useRef(null);
const { camera } = useThree();
const actionsRef = useRef({});
const currentActionRef = useRef(null);
const gltf = useLoader(GLTFLoader, VRM_URL, (loader) => {
loader.register((parser) => new VRMLoaderPlugin(parser));
});
// Load all animations
const [vrmaFly, vrmaFlyStop, vrmaFlyIdle] = useLoader(GLTFLoader, [
`${BASE_URL}fly_sky.vrma`,
`${BASE_URL}fly_stop.vrma`,
`${BASE_URL}fly_idle.vrma`
], (loader) => {
loader.register((parser) => new VRMAnimationLoaderPlugin(parser));
});
useEffect(() => {
const vrm = gltf.userData.vrm;
vrmRef.current = vrm;
VRMUtils.removeUnnecessaryJoints(vrm.scene);
vrm.humanoid.resetPose();
vrm.scene.rotation.y = Math.PI;
// Setup Animation Mixer
const mixer = new AnimationMixer(vrm.scene);
mixerRef.current = mixer;
// Helper to create action
const createAction = (vrma, name) => {
if (vrma.userData.vrmAnimations?.[0]) {
const clip = createVRMAnimationClip(vrma.userData.vrmAnimations[0], vrm);
const action = mixer.clipAction(clip);
action.name = name;
return action;
}
return null;
};
const actionFly = createAction(vrmaFly, 'fly');
const actionFlyStop = createAction(vrmaFlyStop, 'fly_stop');
const actionFlyIdle = createAction(vrmaFlyIdle, 'fly_idle');
actionsRef.current = {
fly: actionFly,
fly_stop: actionFlyStop,
fly_idle: actionFlyIdle,
};
// Configure Loop Modes
if (actionFlyStop) {
actionFlyStop.setLoop(THREE.LoopOnce);
actionFlyStop.clampWhenFinished = true;
}
// Initial Animation (Idle: fly_idle)
if (actionFlyIdle) {
actionFlyIdle.play();
currentActionRef.current = 'fly_idle';
} else if (actionFly) {
actionFly.play();
currentActionRef.current = 'fly';
}
// Mixer Event Listener for 'finished' (for fly_stop -> fly_idle)
const onFinished = (e) => {
if (e.action === actionFlyStop) {
// fly_stop finished, crossfade to fly_idle
if (actionsRef.current.fly_idle) {
actionFlyStop.fadeOut(0.5);
actionsRef.current.fly_idle.reset().fadeIn(0.5).play();
currentActionRef.current = 'fly_idle';
}
}
};
mixer.addEventListener('finished', onFinished);
return () => {
mixer.removeEventListener('finished', onFinished);
};
}, [gltf, vrmaFly, vrmaFlyStop, vrmaFlyIdle]);
useFrame((state, delta) => {
mixerRef.current?.update(delta);
vrmRef.current?.update(delta);
// Animation State Logic
const isMoving = keys.w || keys.s || keys.a || keys.d;
const actions = actionsRef.current;
const current = currentActionRef.current;
if (actions.fly && actions.fly_stop && actions.fly_idle) {
if (isMoving) {
// If moving and not flying, transition to fly
if (current !== 'fly') {
const prevAction = actions[current];
if (prevAction) prevAction.fadeOut(0.5);
actions.fly.reset().fadeIn(0.5).play();
currentActionRef.current = 'fly';
}
} else {
// If stopped moving and currently flying, transition to fly_stop (stop anim)
if (current === 'fly') {
actions.fly.fadeOut(0.5);
actions.fly_stop.reset().fadeIn(0.5).play();
currentActionRef.current = 'fly_stop';
}
// If currently fly_stop, wait for it to finish (handled by event listener)
// If currently fly_idle, stay there.
}
}
// Character Rotation Logic
if (vrmRef.current) {
const vrmNode = vrmRef.current.scene;
const moveDir = new Vector3(0, 0, 0);
if (keys.w) moveDir.z -= 1;
if (keys.s) moveDir.z += 1;
if (keys.a) moveDir.x -= 1;
if (keys.d) moveDir.x += 1;
if (moveDir.lengthSq() > 0) {
moveDir.normalize();
const cameraQuaternion = camera.quaternion.clone();
moveDir.applyQuaternion(cameraQuaternion);
// Create a target rotation looking at the movement direction
// Model faces -Z, so we want -Z to point to movement.
// lookAt points +Z to target. So we want +Z to point AWAY from movement.
const targetPos = vrmNode.position.clone().sub(moveDir);
const dummy = new THREE.Object3D();
dummy.position.copy(vrmNode.position);
dummy.lookAt(targetPos);
// Smoothly rotate towards target
vrmNode.quaternion.slerp(dummy.quaternion, 10.0 * delta);
}
}
});
return <primitive object={gltf.scene} />;
}
// Avatar側のカメラ操作と移動ロジック
function CameraSync() {
const { camera } = useThree();
const vec = new Vector3();
// Dynamic Zoom State
const zoomOffset = useRef(0);
const MAX_ZOOM_OFFSET = 10.0;
const ZOOM_SPEED = 2.0;
useFrame((state, delta) => {
// 1. カメラの回転をShared State (Local) に同期
worldState.quaternion.copy(camera.quaternion);
// 2. Calculate Basis Rotation
const basis = getSurfaceBasis(worldState.position);
// 3. WASD移動ロジック (Local -> Global)
const isMoving = keys.w || keys.s || keys.a || keys.d || keys.q || keys.e;
const speed = worldState.speed * (keys.Shift ? 2.0 : 1.0) * delta;
// Local Movement Vector
const moveDir = new Vector3();
if (keys.w) moveDir.z -= 1;
if (keys.s) moveDir.z += 1;
if (keys.a) moveDir.x -= 1;
if (keys.d) moveDir.x += 1;
// Altitude Control (Global Up/Down relative to surface)
// We apply this directly to worldState.position along the up vector
const upVector = worldState.position.clone().normalize();
if (keys.e) {
worldState.position.addScaledVector(upVector, speed);
}
if (keys.q) {
worldState.position.addScaledVector(upVector, -speed);
}
if (moveDir.lengthSq() > 0) {
moveDir.normalize();
// Apply Camera Rotation (Local)
moveDir.applyQuaternion(camera.quaternion);
// Apply Basis Rotation (Local -> Global)
moveDir.applyQuaternion(basis);
// Apply to Global Position
worldState.position.addScaledVector(moveDir, speed);
}
// 4. Minimum Altitude Safety Check
const currentDist = worldState.position.length();
// Calculate local ellipsoid radius
const localRadius = getEllipsoidRadius(worldState.position);
const minAltitude = localRadius + 10; // Minimum 10m above ellipsoid
// Debug Log
if (state.clock.elapsedTime % 1.0 < 0.02) {
console.log(`[CameraSync] Dist: ${currentDist.toFixed(1)}, LocalR: ${localRadius.toFixed(1)}, Alt: ${(currentDist - localRadius).toFixed(1)}`);
}
if (currentDist < minAltitude) {
// console.log(`[CameraSync] Safety Check Triggered! Pushing up to ${minAltitude}`);
worldState.position.setLength(minAltitude);
}
// 5. Dynamic Zoom Logic
const targetZoom = isMoving ? MAX_ZOOM_OFFSET : 0;
// Smoothly interpolate current zoom offset towards target
const diff = targetZoom - zoomOffset.current;
const step = diff * ZOOM_SPEED * delta;
zoomOffset.current += step;
camera.translateZ(step);
});
return null;
}
function AvatarLayer() {
return (
<Canvas gl={{ alpha: true, antialias: true }}>
<PerspectiveCamera makeDefault position={[0, 1.5, 3]} fov={40} />
<CameraSync />
<directionalLight position={[-1, 1, 1]} intensity={1.5} />
<ambientLight intensity={1.0} />
<spotLight position={[0, 2, -2]} intensity={3} color="#ffdcb4" />
<Suspense fallback={null}>
<VrmCharacter />
</Suspense>
<OrbitControls target={[0, 1.2, 0]} minDistance={2.0} maxDistance={10.0} />
</Canvas>
);
}
// ---------------------------------------------------------
// UI Components
// ---------------------------------------------------------
function LocationUI() {
return (
<div style={{
position: 'absolute',
top: '20px',
left: '20px',
zIndex: 100,
display: 'flex',
gap: '10px',
flexWrap: 'wrap'
}}>
{LOCATIONS.map((loc) => (
<button
key={loc.name}
onClick={() => teleportTo(loc)}
style={{
padding: '10px 20px',
background: 'rgba(0, 0, 0, 0.6)',
color: 'white',
border: '1px solid rgba(255, 255, 255, 0.3)',
borderRadius: '8px',
cursor: 'pointer',
fontSize: '14px',
backdropFilter: 'blur(4px)',
transition: 'background 0.2s'
}}
onMouseOver={(e) => e.currentTarget.style.background = 'rgba(0, 0, 0, 0.8)'}
onMouseOut={(e) => e.currentTarget.style.background = 'rgba(0, 0, 0, 0.6)'}
>
{loc.name}
</button>
))}
</div>
);
}
// ---------------------------------------------------------
// Main App
// ---------------------------------------------------------
function AtmosphereLayer() {
// Canvasが親となり、その中にロジックコンポーネント(AtmosphereScene)を入れる
return (
<Canvas gl={{ alpha: true, antialias: true }}>
<AtmosphereScene />
</Canvas>
);
}
export default function App() {
const layerStyle = {
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
};
return (
<div style={{ position: 'relative', width: '100vw', height: '100vh', background: '#000' }}>
{/* UI Layer */}
<LocationUI />
{/* Layer 0: Atmosphere */}
<div style={{ ...layerStyle, zIndex: 0 }}>
<AtmosphereLayer />
</div>
{/* Layer 1: Avatar */}
<div style={{ ...layerStyle, zIndex: 1, pointerEvents: 'none' }}>
<div style={{ width: '100%', height: '100%', pointerEvents: 'auto' }}>
<AvatarLayer />
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,8 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
//base: '/',
base: '/pkg/atmosphere/',
plugins: [react()]
})

1
dist/vrma vendored

Submodule dist/vrma deleted from 1eccc36a51

View File

@@ -47,4 +47,11 @@ $ yarn install
$ yarn dev $ yarn dev
``` ```
## theatre
```sh
$ cd theatre
$ npm install
$ npm run dev
```

1
first/dist/CNAME vendored Normal file
View File

@@ -0,0 +1 @@
vrm.syui.ai

BIN
first/dist/favicon.ico vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

Before

Width:  |  Height:  |  Size: 306 KiB

After

Width:  |  Height:  |  Size: 306 KiB

View File

@@ -18,6 +18,8 @@
"@pixiv/three-vrm-animation": "^3.1.0", "@pixiv/three-vrm-animation": "^3.1.0",
"@pixiv/three-vrm-springbone": "^3.1.0", "@pixiv/three-vrm-springbone": "^3.1.0",
"postprocessing": "^6.35.2", "postprocessing": "^6.35.2",
"react": "^19.2.0",
"react-dom": "^18.3.1",
"three": "^0.167.1" "three": "^0.167.1"
} }
} }

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1 +0,0 @@
GENERATE_SOURCEMAP=false

23
min-react/.gitignore vendored
View File

@@ -1,23 +0,0 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

2664
min-react/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,51 +1,26 @@
{ {
"name": "vrm2", "name": "min-react-vrm",
"version": "0.1.0", "version": "1.0.0",
"private": true, "description": "Minimal VRM Animation Player",
"dependencies": { "type": "module",
"@pixiv/three-vrm": "^3.1.1",
"@pixiv/three-vrm-animation": "^3.1.1",
"@react-three/drei": "^9.114.0",
"@react-three/fiber": "^8.17.9",
"@react-three/postprocessing": "^2.16.3",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.5.2",
"@types/node": "^16.18.112",
"@types/react": "^18.3.10",
"@types/react-dom": "^18.3.0",
"@types/three": "^0.167.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-scripts": "5.0.1",
"three": "^0.167.1",
"three-stdlib": "^2.30.5",
"typescript": "^4.9.5",
"web-vitals": "^2.1.4"
},
"scripts": { "scripts": {
"start": "react-scripts start", "dev": "vite",
"build": "react-scripts build", "build": "vite build",
"test": "react-scripts test", "preview": "vite preview"
"eject": "react-scripts eject"
}, },
"eslintConfig": { "dependencies": {
"extends": [ "@pixiv/three-vrm": "^3.4.4",
"react-app", "@pixiv/three-vrm-animation": "^3.4.4",
"react-app/jest" "@react-three/drei": "^10.7.7",
] "@react-three/fiber": "^9.4.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"three": "^0.181.2"
}, },
"browserslist": { "devDependencies": {
"production": [ "@types/react": "^19.0.0",
">0.2%", "@types/react-dom": "^19.0.0",
"not dead", "@vitejs/plugin-react": "^4.3.4",
"not op_mini all" "vite": "^6.0.1"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
} }
} }

1
min-react/public/CNAME Normal file
View File

@@ -0,0 +1 @@
vrm.syui.ai

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -1,43 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href=".%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href=".%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href=".%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

View File

@@ -1,25 +0,0 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@@ -1,3 +0,0 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View File

@@ -1,20 +0,0 @@
react-three-fiber + three-vrm
```sh
$ npm i
$ npm run start
```
## install
```sh
# npx create-react-app vrm1 --template typescript
# npm install three three-stdlib @types/three @react-three/fiber @react-three/fiber @react-three/drei @react-three/postprocessing
```
## three-vrm
`vrm 1.0`
unity export

View File

@@ -1,38 +0,0 @@
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

View File

@@ -1,9 +0,0 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

View File

@@ -1,10 +0,0 @@
import React from 'react'
import VRMModelCanvas from './pages/vrm_model'
const App = () => {
return (
<VRMModelCanvas/>
)
}
export default App;

View File

@@ -1,18 +0,0 @@
body {
height: 100%;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
html {
height: 100%;
}

View File

@@ -1,19 +0,0 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

9
min-react/src/main.jsx Normal file
View File

@@ -0,0 +1,9 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)

View File

@@ -1,84 +0,0 @@
import * as THREE from 'three'
import React, { useState, useEffect, useRef } from 'react';
import { OrbitControls } from '@react-three/drei'
import { useFrame, Canvas } from '@react-three/fiber';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
import { VRM, VRMUtils, VRMLoaderPlugin } from '@pixiv/three-vrm';
import { VRMAnimationLoaderPlugin, VRMAnimation, createVRMAnimationClip } from "@pixiv/three-vrm-animation";
interface ModelProps {
url: string
url_anim: string
}
const VRMModel: React.FC<ModelProps> = ({ url, url_anim }) => {
const [vrm, setVrm] = useState<VRM | null>(null);
const mixerRef = useRef<THREE.AnimationMixer | null>(null);
useEffect(() => {
const loader = new GLTFLoader();
loader.register((parser) => new VRMLoaderPlugin(parser));
loader.register((parser) => new VRMAnimationLoaderPlugin(parser));
loader.load(url, (gltf) => {
const vrmModel = gltf.userData.vrm as VRM;
VRMUtils.removeUnnecessaryJoints(vrmModel.scene);
setVrm(vrmModel);
const mixer = new THREE.AnimationMixer(vrmModel.scene);
mixerRef.current = mixer;
loader.load(url_anim, (animGltf) => {
const vrmAnimations = animGltf.userData.vrmAnimations as VRMAnimation[];
if (vrmAnimations && vrmAnimations.length > 0) {
const clip = createVRMAnimationClip(vrmAnimations[0], vrmModel);
mixer.clipAction(clip).play();
}
});
});
}, [url, url_anim]);
useFrame((state, delta) => {
if (mixerRef.current) mixerRef.current.update(delta);
if (vrm) vrm.update(delta);
});
return vrm ? <primitive object={vrm.scene} /> : null;
};
export const VRMModelCanvas = () => {
return (
<div style={{ height: '100vh', width: '100vw' }}>
<Canvas
shadows
gl={{
//toneMapping: THREE.ACESFilmicToneMapping,
//toneMapping: THREE.ReinhardToneMapping,
toneMapping: THREE.NeutralToneMapping,
toneMappingExposure: 1.5,
alpha: true,
powerPreference: "high-performance",
antialias: true,
//stencil: false,
//depth: false
}}
camera={{ position: [1, 1, 1] }}>
<directionalLight
color="white"
castShadow
position={[0, 10, 0]}
intensity={1.5}
shadow-mapSize={[1024, 1024]}/>
<OrbitControls />
<ambientLight intensity={1} />
<pointLight position={[10, 10, 10]} />
<VRMModel url="./models/ai_vrm10.vrm" url_anim="./models/default.vrma" />
</Canvas>
</div>
)
}
export default VRMModelCanvas;

View File

@@ -1 +0,0 @@
/// <reference types="react-scripts" />

View File

@@ -1,15 +0,0 @@
import { ReportHandler } from 'web-vitals';
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;

View File

@@ -1,5 +0,0 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';

View File

@@ -1,26 +0,0 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": [
"src"
]
}

View File

@@ -5,6 +5,7 @@ html {
body { body {
background-color: #fff; background-color: #fff;
background-color: #000; background-color: #000;
background-color: #313131;
margin: 0; margin: 0;
height: 100%; height: 100%;
} }

View File

@@ -88,6 +88,8 @@ export function Galaxy(props: JSX.IntrinsicElements['group']) {
const galaxyCenterLightRef = useRef<THREE.PointLight>(null!) const galaxyCenterLightRef = useRef<THREE.PointLight>(null!)
const searchParams = new URLSearchParams(window.location.search); const searchParams = new URLSearchParams(window.location.search);
var g = searchParams.get('g') ?? 'galaxy'; var g = searchParams.get('g') ?? 'galaxy';
const ms = searchParams.get('ms');
const vrm_scale = ms ? parseInt(ms, 10) : 1;
var model_galaxy = "./models/galaxy.glb" var model_galaxy = "./models/galaxy.glb"
var model_custom = "./models/ai.vrm" var model_custom = "./models/ai.vrm"
var model_scale = 0.01; var model_scale = 0.01;
@@ -158,7 +160,7 @@ export function Galaxy(props: JSX.IntrinsicElements['group']) {
return ( return (
<group {...props} dispose={null} ref={ref}> <group {...props} dispose={null} ref={ref}>
<VRMModel url={model_custom} url_anim={anim_custom} position={position_custom} scale={1} rotation={rotation_custom} /> <VRMModel url={model_custom} url_anim={anim_custom} position={position_custom} scale={vrm_scale} rotation={rotation_custom} />
{g === 'sun' && <GlbModel url="./models/solar-system.glb" scale={10} />} {g === 'sun' && <GlbModel url="./models/solar-system.glb" scale={10} />}
{g === 'galaxy' && <GlbModel url="./models/solar-system.glb" scale={0.5} position={[0,0.5,2]}/>} {g === 'galaxy' && <GlbModel url="./models/solar-system.glb" scale={0.5} position={[0,0.5,2]}/>}

24
theatre/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

50
theatre/README.md Normal file
View File

@@ -0,0 +1,50 @@
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
- Configure the top-level `parserOptions` property like this:
```js
export default tseslint.config({
languageOptions: {
// other options...
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
},
})
```
- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked`
- Optionally add `...tseslint.configs.stylisticTypeChecked`
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config:
```js
// eslint.config.js
import react from 'eslint-plugin-react'
export default tseslint.config({
// Set the react version
settings: { react: { version: '18.3' } },
plugins: {
// Add the react plugin
react,
},
rules: {
// other rules...
// Enable its recommended rules
...react.configs.recommended.rules,
...react.configs['jsx-runtime'].rules,
},
})
```

28
theatre/eslint.config.js Normal file
View File

@@ -0,0 +1,28 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
export default tseslint.config(
{ ignores: ['dist'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
},
)

13
theatre/index.html Normal file
View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

32
theatre/package.json Normal file
View File

@@ -0,0 +1,32 @@
{
"name": "vite-react-typescript-starter",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@theatre/core": "^0.6.1",
"@theatre/r3f": "^0.7.2",
"@theatre/studio": "^0.6.1",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@eslint/js": "^9.9.1",
"@types/react": "^18.3.4",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react": "^4.3.1",
"eslint": "^9.9.1",
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
"eslint-plugin-react-refresh": "^0.4.11",
"globals": "^15.9.0",
"typescript": "^5.5.3",
"typescript-eslint": "^8.3.0",
"vite": "^5.4.2"
}
}

1
theatre/public/vite.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

42
theatre/src/App.css Normal file
View File

@@ -0,0 +1,42 @@
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}
@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
}
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
}

35
theatre/src/App.tsx Normal file
View File

@@ -0,0 +1,35 @@
import { useState } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import './App.css'
function App() {
const [count, setCount] = useState(0)
return (
<>
<div>
<a href="https://vitejs.dev" target="_blank">
<img src={viteLogo} className="logo" alt="Vite logo" />
</a>
<a href="https://react.dev" target="_blank">
<img src={reactLogo} className="logo react" alt="React logo" />
</a>
</div>
<h1>Vite + React</h1>
<div className="card">
<button onClick={() => setCount((count) => count + 1)}>
count is {count}
</button>
<p>
Edit <code>src/App.tsx</code> and save to test HMR
</p>
</div>
<p className="read-the-docs">
Click on the Vite and React logos to learn more
</p>
</>
)
}
export default App

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

68
theatre/src/index.css Normal file
View File

@@ -0,0 +1,68 @@
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

36
theatre/src/main.tsx Normal file
View File

@@ -0,0 +1,36 @@
import './index.css'
import { createRoot } from 'react-dom/client'
import React, { useEffect } from 'react'
import { Canvas } from '@react-three/fiber'
import studio from '@theatre/studio'
import extension from '@theatre/r3f/dist/extension'
import { SheetProvider, editable as e, PerspectiveCamera } from '@theatre/r3f'
import { getProject } from '@theatre/core'
import demoProjectState from './state.json'
studio.initialize()
studio.extend(extension)
const demoSheet = getProject('Demo Project', { state: demoProjectState }).sheet('Demo Sheet')
//const demoSheet = getProject('Demo Project').sheet('Demo Sheet')
const App = () => {
useEffect(() => {
demoSheet.project.ready.then(() => demoSheet.sequence.play({ iterationCount: Infinity, range: [0, 1] }))
}, [])
return (
<Canvas>
<SheetProvider sheet={demoSheet}>
<PerspectiveCamera theatreKey="Camera" makeDefault position={[0, 0, 0]} fov={75} />
<ambientLight />
<e.pointLight theatreKey="Light" position={[1, 1, 1]} />
<e.mesh theatreKey="Cube">
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color="orange" />
</e.mesh>
</SheetProvider>
</Canvas>
)
}
createRoot(document.getElementById('root')!).render(<App />)

7
theatre/src/state.json Normal file
View File

@@ -0,0 +1,7 @@
{
"sheetsById": {},
"definitionVersion": "0.4.0",
"revisionHistory": [
"jbMA2kSJZuOGYgr9"
]
}

1
theatre/src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

24
theatre/tsconfig.app.json Normal file
View File

@@ -0,0 +1,24 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}

7
theatre/tsconfig.json Normal file
View File

@@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

View File

@@ -0,0 +1,22 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["vite.config.ts"]
}

7
theatre/vite.config.ts Normal file
View File

@@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
})