TWEEN
这是一个可以直接在 VitePress 页面中运行的 Three.js 示例。上方显示运行效果,下方展示对应源码。
运行效果
原理
补间动画的核心是“从起始状态过渡到结束状态”。TWEEN 不直接关心 Three.js 的 Mesh,它只负责在一段时间内计算普通对象属性的中间值。每一帧调用 tweenGroup.update(time) 后,TWEEN 会根据时长、缓动函数、重复次数等参数更新这些属性。
在 Three.js 中通常会准备一个普通对象保存动画状态,例如位置、旋转和缩放。然后在 onUpdate 回调中,把这些数值同步到 Mesh 上。这样动画逻辑和渲染对象可以保持分离。
源码
js
import * as THREE from 'three'
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
import { GUI } from 'three/addons/libs/lil-gui.module.min.js'
import * as TWEEN from 'three/addons/libs/tween.module.js'
const canvas = document.querySelector('canvas')
const demo = canvas.parentElement
const scene = new THREE.Scene()
scene.background = new THREE.Color('#111827')
const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000)
camera.position.set(0, 2.5, 6)
const renderer = new THREE.WebGLRenderer({
canvas,
antialias: true
})
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
renderer.setSize(window.innerWidth, window.innerHeight)
const ambientLight = new THREE.AmbientLight('#ffffff', 0.8)
scene.add(ambientLight)
const directionalLight = new THREE.DirectionalLight('#ffffff', 1.6)
directionalLight.position.set(4, 6, 5)
scene.add(directionalLight)
const geometry = new THREE.BoxGeometry(1, 1, 1)
const material = new THREE.MeshStandardMaterial({
color: '#38bdf8',
roughness: 0.45,
metalness: 0.1
})
const cube = new THREE.Mesh(geometry, material)
scene.add(cube)
const gridHelper = new THREE.GridHelper(8, 8)
scene.add(gridHelper)
const controls = new OrbitControls(camera, renderer.domElement)
controls.enableDamping = true
const tweenGroup = new TWEEN.Group()
const startState = {
x: -2,
y: 0.5,
rotationY: 0,
scale: 1
}
const endState = {
x: 2,
y: 1.4,
rotationY: Math.PI * 2,
scale: 1.8
}
const animationState = { ...startState }
const easingMap = {
Linear: TWEEN.Easing.Linear.None,
Quadratic: TWEEN.Easing.Quadratic.InOut,
Cubic: TWEEN.Easing.Cubic.InOut,
Elastic: TWEEN.Easing.Elastic.Out,
Bounce: TWEEN.Easing.Bounce.Out
}
const applyAnimationState = () => {
cube.position.set(animationState.x, animationState.y, 0)
cube.rotation.y = animationState.rotationY
cube.scale.setScalar(animationState.scale)
}
const resetAnimation = () => {
tweenGroup.removeAll()
Object.assign(animationState, startState)
applyAnimationState()
}
const guiState = {
duration: 1600,
easing: 'Quadratic',
yoyo: true,
repeat: 1,
play: () => {
resetAnimation()
// Tween 不直接操作 Mesh,而是操作普通对象;onUpdate 中再把数值同步给 Mesh。
new TWEEN.Tween(animationState, tweenGroup)
.to(endState, guiState.duration)
.easing(easingMap[guiState.easing])
.repeat(guiState.repeat)
.yoyo(guiState.yoyo)
.onUpdate(applyAnimationState)
.start()
},
reset: resetAnimation
}
const gui = new GUI({
title: 'TWEEN 控制',
autoPlace: false
})
gui.domElement.classList.add('three-gui')
demo.appendChild(gui.domElement)
gui.add(guiState, 'duration', 300, 4000, 100).name('时长')
gui.add(guiState, 'easing', Object.keys(easingMap)).name('缓动')
gui.add(guiState, 'yoyo').name('往返')
gui.add(guiState, 'repeat', 0, 5, 1).name('重复')
gui.add(guiState, 'play').name('播放')
gui.add(guiState, 'reset').name('重置')
resetAnimation()
function animate(time) {
requestAnimationFrame(animate)
// 每一帧调用 update,TWEEN 才会根据当前时间推进动画进度。
tweenGroup.update(time)
controls.update()
renderer.render(scene, camera)
}
animate()