Setup three.js
This commit is contained in:
parent
2105d05749
commit
f0fece522d
7 changed files with 201 additions and 36 deletions
12
TODO.md
12
TODO.md
|
|
@ -28,12 +28,12 @@ A step-by-step checklist for porting MatterControl's design features to a Vue +
|
||||||
## Phase 2: 3D Viewport Foundation
|
## Phase 2: 3D Viewport Foundation
|
||||||
|
|
||||||
### three.js Setup
|
### three.js Setup
|
||||||
- [ ] Install three.js and `@types/three`
|
- [x] Install three.js and `@types/three`
|
||||||
- [ ] Create `Viewport.vue` component with canvas element
|
- [x] Create `Viewport.vue` component with canvas element
|
||||||
- [ ] Initialize three.js Scene, Camera, and Renderer
|
- [x] Initialize three.js Scene, Camera, and Renderer
|
||||||
- [ ] Add resize observer for responsive canvas
|
- [x] Add resize observer for responsive canvas
|
||||||
- [ ] Implement render loop with `requestAnimationFrame`
|
- [x] Implement render loop with `requestAnimationFrame`
|
||||||
- [ ] Add basic lighting (ambient + two directional)
|
- [x] Add basic lighting (ambient + two directional)
|
||||||
|
|
||||||
### Scene Helpers
|
### Scene Helpers
|
||||||
- [ ] Add ground grid helper (XZ plane)
|
- [ ] Add ground grid helper (XZ plane)
|
||||||
|
|
|
||||||
|
|
@ -14,11 +14,13 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
|
"three": "^0.182.0",
|
||||||
"vue": "^3.5.24"
|
"vue": "^3.5.24"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.2",
|
"@eslint/js": "^9.39.2",
|
||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.10.1",
|
||||||
|
"@types/three": "^0.182.0",
|
||||||
"@vitejs/plugin-vue": "^6.0.1",
|
"@vitejs/plugin-vue": "^6.0.1",
|
||||||
"@vue/test-utils": "^2.4.6",
|
"@vue/test-utils": "^2.4.6",
|
||||||
"@vue/tsconfig": "^0.8.1",
|
"@vue/tsconfig": "^0.8.1",
|
||||||
|
|
|
||||||
59
pnpm-lock.yaml
generated
59
pnpm-lock.yaml
generated
|
|
@ -11,6 +11,9 @@ importers:
|
||||||
pinia:
|
pinia:
|
||||||
specifier: ^3.0.4
|
specifier: ^3.0.4
|
||||||
version: 3.0.4(typescript@5.9.3)(vue@3.5.27(typescript@5.9.3))
|
version: 3.0.4(typescript@5.9.3)(vue@3.5.27(typescript@5.9.3))
|
||||||
|
three:
|
||||||
|
specifier: ^0.182.0
|
||||||
|
version: 0.182.0
|
||||||
vue:
|
vue:
|
||||||
specifier: ^3.5.24
|
specifier: ^3.5.24
|
||||||
version: 3.5.27(typescript@5.9.3)
|
version: 3.5.27(typescript@5.9.3)
|
||||||
|
|
@ -21,6 +24,9 @@ importers:
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^24.10.1
|
specifier: ^24.10.1
|
||||||
version: 24.10.9
|
version: 24.10.9
|
||||||
|
'@types/three':
|
||||||
|
specifier: ^0.182.0
|
||||||
|
version: 0.182.0
|
||||||
'@vitejs/plugin-vue':
|
'@vitejs/plugin-vue':
|
||||||
specifier: ^6.0.1
|
specifier: ^6.0.1
|
||||||
version: 6.0.3(vite@7.3.1(@types/node@24.10.9))(vue@3.5.27(typescript@5.9.3))
|
version: 6.0.3(vite@7.3.1(@types/node@24.10.9))(vue@3.5.27(typescript@5.9.3))
|
||||||
|
|
@ -80,6 +86,9 @@ packages:
|
||||||
resolution: {integrity: sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==}
|
resolution: {integrity: sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
|
'@dimforge/rapier3d-compat@0.12.0':
|
||||||
|
resolution: {integrity: sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==}
|
||||||
|
|
||||||
'@esbuild/aix-ppc64@0.27.2':
|
'@esbuild/aix-ppc64@0.27.2':
|
||||||
resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==}
|
resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
@ -435,6 +444,9 @@ packages:
|
||||||
'@standard-schema/spec@1.1.0':
|
'@standard-schema/spec@1.1.0':
|
||||||
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
|
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
|
||||||
|
|
||||||
|
'@tweenjs/tween.js@23.1.3':
|
||||||
|
resolution: {integrity: sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==}
|
||||||
|
|
||||||
'@types/chai@5.2.3':
|
'@types/chai@5.2.3':
|
||||||
resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
|
resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
|
||||||
|
|
||||||
|
|
@ -450,6 +462,15 @@ packages:
|
||||||
'@types/node@24.10.9':
|
'@types/node@24.10.9':
|
||||||
resolution: {integrity: sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw==}
|
resolution: {integrity: sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw==}
|
||||||
|
|
||||||
|
'@types/stats.js@0.17.4':
|
||||||
|
resolution: {integrity: sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==}
|
||||||
|
|
||||||
|
'@types/three@0.182.0':
|
||||||
|
resolution: {integrity: sha512-WByN9V3Sbwbe2OkWuSGyoqQO8Du6yhYaXtXLoA5FkKTUJorZ+yOHBZ35zUUPQXlAKABZmbYp5oAqpA4RBjtJ/Q==}
|
||||||
|
|
||||||
|
'@types/webxr@0.5.24':
|
||||||
|
resolution: {integrity: sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==}
|
||||||
|
|
||||||
'@types/whatwg-mimetype@3.0.2':
|
'@types/whatwg-mimetype@3.0.2':
|
||||||
resolution: {integrity: sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==}
|
resolution: {integrity: sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==}
|
||||||
|
|
||||||
|
|
@ -615,6 +636,9 @@ packages:
|
||||||
vue:
|
vue:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@webgpu/types@0.1.69':
|
||||||
|
resolution: {integrity: sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ==}
|
||||||
|
|
||||||
abbrev@2.0.0:
|
abbrev@2.0.0:
|
||||||
resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==}
|
resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==}
|
||||||
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
|
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
|
||||||
|
|
@ -854,6 +878,9 @@ packages:
|
||||||
picomatch:
|
picomatch:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
fflate@0.8.2:
|
||||||
|
resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==}
|
||||||
|
|
||||||
file-entry-cache@8.0.0:
|
file-entry-cache@8.0.0:
|
||||||
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
|
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
|
||||||
engines: {node: '>=16.0.0'}
|
engines: {node: '>=16.0.0'}
|
||||||
|
|
@ -984,6 +1011,9 @@ packages:
|
||||||
magic-string@0.30.21:
|
magic-string@0.30.21:
|
||||||
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
||||||
|
|
||||||
|
meshoptimizer@0.22.0:
|
||||||
|
resolution: {integrity: sha512-IebiK79sqIy+E4EgOr+CAw+Ke8hAspXKzBd0JdgEmPHiAwmvEj2S4h1rfvo+o/BnfEYd/jAOg5IeeIjzlzSnDg==}
|
||||||
|
|
||||||
minimatch@3.1.2:
|
minimatch@3.1.2:
|
||||||
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
|
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
|
||||||
|
|
||||||
|
|
@ -1181,6 +1211,9 @@ packages:
|
||||||
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
|
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
three@0.182.0:
|
||||||
|
resolution: {integrity: sha512-GbHabT+Irv+ihI1/f5kIIsZ+Ef9Sl5A1Y7imvS5RQjWgtTPfPnZ43JmlYI7NtCRDK9zir20lQpfg8/9Yd02OvQ==}
|
||||||
|
|
||||||
tinybench@2.9.0:
|
tinybench@2.9.0:
|
||||||
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
|
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
|
||||||
|
|
||||||
|
|
@ -1388,6 +1421,8 @@ snapshots:
|
||||||
'@babel/helper-string-parser': 7.27.1
|
'@babel/helper-string-parser': 7.27.1
|
||||||
'@babel/helper-validator-identifier': 7.28.5
|
'@babel/helper-validator-identifier': 7.28.5
|
||||||
|
|
||||||
|
'@dimforge/rapier3d-compat@0.12.0': {}
|
||||||
|
|
||||||
'@esbuild/aix-ppc64@0.27.2':
|
'@esbuild/aix-ppc64@0.27.2':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
|
@ -1618,6 +1653,8 @@ snapshots:
|
||||||
|
|
||||||
'@standard-schema/spec@1.1.0': {}
|
'@standard-schema/spec@1.1.0': {}
|
||||||
|
|
||||||
|
'@tweenjs/tween.js@23.1.3': {}
|
||||||
|
|
||||||
'@types/chai@5.2.3':
|
'@types/chai@5.2.3':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/deep-eql': 4.0.2
|
'@types/deep-eql': 4.0.2
|
||||||
|
|
@ -1633,6 +1670,20 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 7.16.0
|
undici-types: 7.16.0
|
||||||
|
|
||||||
|
'@types/stats.js@0.17.4': {}
|
||||||
|
|
||||||
|
'@types/three@0.182.0':
|
||||||
|
dependencies:
|
||||||
|
'@dimforge/rapier3d-compat': 0.12.0
|
||||||
|
'@tweenjs/tween.js': 23.1.3
|
||||||
|
'@types/stats.js': 0.17.4
|
||||||
|
'@types/webxr': 0.5.24
|
||||||
|
'@webgpu/types': 0.1.69
|
||||||
|
fflate: 0.8.2
|
||||||
|
meshoptimizer: 0.22.0
|
||||||
|
|
||||||
|
'@types/webxr@0.5.24': {}
|
||||||
|
|
||||||
'@types/whatwg-mimetype@3.0.2': {}
|
'@types/whatwg-mimetype@3.0.2': {}
|
||||||
|
|
||||||
'@types/ws@8.18.1':
|
'@types/ws@8.18.1':
|
||||||
|
|
@ -1879,6 +1930,8 @@ snapshots:
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
vue: 3.5.27(typescript@5.9.3)
|
vue: 3.5.27(typescript@5.9.3)
|
||||||
|
|
||||||
|
'@webgpu/types@0.1.69': {}
|
||||||
|
|
||||||
abbrev@2.0.0: {}
|
abbrev@2.0.0: {}
|
||||||
|
|
||||||
acorn-jsx@5.3.2(acorn@8.15.0):
|
acorn-jsx@5.3.2(acorn@8.15.0):
|
||||||
|
|
@ -2120,6 +2173,8 @@ snapshots:
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
picomatch: 4.0.3
|
picomatch: 4.0.3
|
||||||
|
|
||||||
|
fflate@0.8.2: {}
|
||||||
|
|
||||||
file-entry-cache@8.0.0:
|
file-entry-cache@8.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
flat-cache: 4.0.1
|
flat-cache: 4.0.1
|
||||||
|
|
@ -2247,6 +2302,8 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
|
|
||||||
|
meshoptimizer@0.22.0: {}
|
||||||
|
|
||||||
minimatch@3.1.2:
|
minimatch@3.1.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
brace-expansion: 1.1.12
|
brace-expansion: 1.1.12
|
||||||
|
|
@ -2434,6 +2491,8 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
has-flag: 4.0.0
|
has-flag: 4.0.0
|
||||||
|
|
||||||
|
three@0.182.0: {}
|
||||||
|
|
||||||
tinybench@2.9.0: {}
|
tinybench@2.9.0: {}
|
||||||
|
|
||||||
tinyexec@1.0.2: {}
|
tinyexec@1.0.2: {}
|
||||||
|
|
|
||||||
26
src/App.vue
26
src/App.vue
|
|
@ -1,30 +1,16 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import HelloWorld from './components/HelloWorld.vue'
|
import Viewport from './components/Viewport.vue'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="app">
|
||||||
<a href="https://vite.dev" target="_blank">
|
<Viewport />
|
||||||
<img src="/vite.svg" class="logo" alt="Vite logo" />
|
|
||||||
</a>
|
|
||||||
<a href="https://vuejs.org/" target="_blank">
|
|
||||||
<img src="./assets/vue.svg" class="logo vue" alt="Vue logo" />
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
<HelloWorld msg="Vite + Vue" />
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.logo {
|
.app {
|
||||||
height: 6em;
|
width: 100vw;
|
||||||
padding: 1.5em;
|
height: 100vh;
|
||||||
will-change: filter;
|
|
||||||
transition: filter 300ms;
|
|
||||||
}
|
|
||||||
.logo:hover {
|
|
||||||
filter: drop-shadow(0 0 2em #646cffaa);
|
|
||||||
}
|
|
||||||
.logo.vue:hover {
|
|
||||||
filter: drop-shadow(0 0 2em #42b883aa);
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
124
src/components/Viewport.vue
Normal file
124
src/components/Viewport.vue
Normal file
|
|
@ -0,0 +1,124 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||||
|
import * as THREE from 'three'
|
||||||
|
|
||||||
|
const containerRef = ref<HTMLDivElement | null>(null)
|
||||||
|
|
||||||
|
let scene: THREE.Scene
|
||||||
|
let camera: THREE.PerspectiveCamera
|
||||||
|
let renderer: THREE.WebGLRenderer
|
||||||
|
let animationFrameId: number
|
||||||
|
let resizeObserver: ResizeObserver
|
||||||
|
|
||||||
|
function initScene() {
|
||||||
|
if (!containerRef.value) return
|
||||||
|
|
||||||
|
// Create scene
|
||||||
|
scene = new THREE.Scene()
|
||||||
|
scene.background = new THREE.Color(0x1a1a2e)
|
||||||
|
|
||||||
|
// Create camera
|
||||||
|
const { clientWidth: width, clientHeight: height } = containerRef.value
|
||||||
|
camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 1000)
|
||||||
|
camera.position.set(5, 5, 5)
|
||||||
|
camera.lookAt(0, 0, 0)
|
||||||
|
|
||||||
|
// Create renderer
|
||||||
|
renderer = new THREE.WebGLRenderer({ antialias: true })
|
||||||
|
renderer.setSize(width, height)
|
||||||
|
renderer.setPixelRatio(window.devicePixelRatio)
|
||||||
|
containerRef.value.appendChild(renderer.domElement)
|
||||||
|
|
||||||
|
// Add lighting
|
||||||
|
addLighting()
|
||||||
|
|
||||||
|
// Add a test cube to verify rendering
|
||||||
|
addTestCube()
|
||||||
|
|
||||||
|
// Setup resize observer
|
||||||
|
setupResizeObserver()
|
||||||
|
|
||||||
|
// Start render loop
|
||||||
|
animate()
|
||||||
|
}
|
||||||
|
|
||||||
|
function addLighting() {
|
||||||
|
// Ambient light for base illumination
|
||||||
|
const ambientLight = new THREE.AmbientLight(0xffffff, 0.4)
|
||||||
|
scene.add(ambientLight)
|
||||||
|
|
||||||
|
// Primary directional light (top-front-right)
|
||||||
|
const directionalLight1 = new THREE.DirectionalLight(0xffffff, 0.8)
|
||||||
|
directionalLight1.position.set(5, 10, 7)
|
||||||
|
scene.add(directionalLight1)
|
||||||
|
|
||||||
|
// Secondary directional light (back-left, softer)
|
||||||
|
const directionalLight2 = new THREE.DirectionalLight(0xffffff, 0.3)
|
||||||
|
directionalLight2.position.set(-5, 5, -5)
|
||||||
|
scene.add(directionalLight2)
|
||||||
|
}
|
||||||
|
|
||||||
|
function addTestCube() {
|
||||||
|
const geometry = new THREE.BoxGeometry(1, 1, 1)
|
||||||
|
const material = new THREE.MeshStandardMaterial({ color: 0x4a90d9 })
|
||||||
|
const cube = new THREE.Mesh(geometry, material)
|
||||||
|
scene.add(cube)
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupResizeObserver() {
|
||||||
|
if (!containerRef.value) return
|
||||||
|
|
||||||
|
resizeObserver = new ResizeObserver((entries) => {
|
||||||
|
for (const entry of entries) {
|
||||||
|
const { width, height } = entry.contentRect
|
||||||
|
if (width > 0 && height > 0) {
|
||||||
|
camera.aspect = width / height
|
||||||
|
camera.updateProjectionMatrix()
|
||||||
|
renderer.setSize(width, height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
resizeObserver.observe(containerRef.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function animate() {
|
||||||
|
animationFrameId = requestAnimationFrame(animate)
|
||||||
|
renderer.render(scene, camera)
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanup() {
|
||||||
|
if (animationFrameId) {
|
||||||
|
cancelAnimationFrame(animationFrameId)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resizeObserver) {
|
||||||
|
resizeObserver.disconnect()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (renderer) {
|
||||||
|
renderer.dispose()
|
||||||
|
renderer.domElement.remove()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
initScene()
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
cleanup()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div ref="containerRef" class="viewport"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.viewport {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -24,10 +24,7 @@ a:hover {
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
display: flex;
|
overflow: hidden;
|
||||||
place-items: center;
|
|
||||||
min-width: 320px;
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
|
|
@ -59,10 +56,8 @@ button:focus-visible {
|
||||||
}
|
}
|
||||||
|
|
||||||
#app {
|
#app {
|
||||||
max-width: 1280px;
|
width: 100%;
|
||||||
margin: 0 auto;
|
height: 100vh;
|
||||||
padding: 2rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: light) {
|
@media (prefers-color-scheme: light) {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
/// <reference types="vitest" />
|
import { defineConfig } from 'vitest/config'
|
||||||
import { defineConfig } from 'vite'
|
|
||||||
import vue from '@vitejs/plugin-vue'
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue