125 lines
3.4 KiB
Vue
125 lines
3.4 KiB
Vue
<template>
|
||
<div class="color-page">
|
||
<div class="color-header">
|
||
<h1>三维空间色彩粒子</h1>
|
||
<p>这是从旧项目抽离出来的第一个实验场景,后续可以继续补充更多交互与玩法。</p>
|
||
</div>
|
||
<div class="color-canvas" ref="canvasRoot"></div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { onMounted, onBeforeUnmount, ref } from 'vue'
|
||
import * as THREE from 'three'
|
||
|
||
const canvasRoot = ref<HTMLDivElement | null>(null)
|
||
|
||
let renderer: THREE.WebGLRenderer | null = null
|
||
let scene: THREE.Scene | null = null
|
||
let camera: THREE.PerspectiveCamera | null = null
|
||
let animationId = 0
|
||
|
||
onMounted(() => {
|
||
if (!canvasRoot.value) return
|
||
|
||
const container = canvasRoot.value
|
||
const width = container.clientWidth
|
||
const height = container.clientHeight
|
||
|
||
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true })
|
||
renderer.setPixelRatio(window.devicePixelRatio)
|
||
renderer.setSize(width, height)
|
||
container.appendChild(renderer.domElement)
|
||
|
||
scene = new THREE.Scene()
|
||
camera = new THREE.PerspectiveCamera(55, width / height, 0.1, 2000)
|
||
camera.position.set(0, 0, 380)
|
||
|
||
const geometry = new THREE.IcosahedronGeometry(1, 3)
|
||
const group = new THREE.Group()
|
||
|
||
const gridSize = 64
|
||
const spacing = 8
|
||
for (let x = -gridSize; x <= gridSize; x += spacing) {
|
||
for (let y = -gridSize; y <= gridSize; y += spacing) {
|
||
const zLayerCount = 3
|
||
for (let zIndex = 0; zIndex < zLayerCount; zIndex++) {
|
||
if (Math.random() > 0.55) continue
|
||
const color = new THREE.Color()
|
||
color.setHSL(Math.random(), 0.7, Math.random() * 0.2 + 0.2)
|
||
const material = new THREE.MeshBasicMaterial({ color })
|
||
const mesh = new THREE.Mesh(geometry, material)
|
||
mesh.position.set(
|
||
x,
|
||
y + (Math.random() - 0.5) * 10,
|
||
zIndex * 18 + (Math.random() - 0.5) * 20,
|
||
)
|
||
const s = Math.random() * 3 + 1.2
|
||
mesh.scale.setScalar(s)
|
||
group.add(mesh)
|
||
}
|
||
}
|
||
}
|
||
|
||
scene.add(group)
|
||
|
||
const clock = new THREE.Clock()
|
||
|
||
const animate = () => {
|
||
animationId = requestAnimationFrame(animate)
|
||
const t = clock.getElapsedTime()
|
||
|
||
group.rotation.y = t * 0.16
|
||
group.rotation.x = Math.sin(t * 0.3) * 0.35
|
||
|
||
if (camera) {
|
||
camera.position.z = 360 + Math.sin(t * 0.5) * 40
|
||
camera.lookAt(0, 0, 0)
|
||
}
|
||
|
||
renderer?.render(scene as THREE.Scene, camera as THREE.PerspectiveCamera)
|
||
}
|
||
|
||
animate()
|
||
|
||
const onResize = () => {
|
||
if (!renderer || !camera) return
|
||
const w = container.clientWidth
|
||
const h = container.clientHeight
|
||
renderer.setSize(w, h)
|
||
camera.aspect = w / h
|
||
camera.updateProjectionMatrix()
|
||
}
|
||
|
||
window.addEventListener('resize', onResize)
|
||
|
||
onBeforeUnmount(() => {
|
||
cancelAnimationFrame(animationId)
|
||
window.removeEventListener('resize', onResize)
|
||
if (renderer) {
|
||
renderer.dispose()
|
||
}
|
||
if (scene) {
|
||
scene.traverse((obj) => {
|
||
if ((obj as THREE.Mesh).geometry) {
|
||
;(obj as THREE.Mesh).geometry.dispose()
|
||
}
|
||
if ((obj as THREE.Mesh).material) {
|
||
const mat = (obj as THREE.Mesh).material
|
||
if (Array.isArray(mat)) {
|
||
mat.forEach((m) => m.dispose())
|
||
} else {
|
||
mat.dispose()
|
||
}
|
||
}
|
||
})
|
||
}
|
||
if (canvasRoot.value && renderer) {
|
||
canvasRoot.value.removeChild(renderer.domElement)
|
||
}
|
||
})
|
||
})
|
||
</script>
|
||
|
||
|