Skip to content

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()