ThreeJS入门(五)动画的控制
<style>.artBox { font-size: 18px; }
.artBox > p { margin: 10px 0; }
#prevBox { position: fixed; top: 0; right: 0; bottom: 0; left: 0; background: beige; display: none; padding: 0; overflow: hidden; z-index: 1000; margin: 0; }
#prevBox::after { position: absolute; content: '关闭预览'; bottom: 10px; left: calc(50% - 40px); padding: 0 4px; width: 80px; height: 30px; line-height: 30px; text-align: center; border: 1px solid #efe; border-radius: 6px; background: #eee; font-size: 14px; box-shadow: 2px 2px 6px rgba(0,0,0,.25); cursor: pointer; }
iframe { position: relative; width: 100%; height: 100%; border: none; outline: none; box-sizing: border-box; margin: 0; }
</style>
<div id="prevBox"></div>
<div class="artBox">
<p>在实现 ThreeJS 动画时既然使用到 JS 请求关键帧动画 API requestAnimationFrame 来递归调用动画函数,就可以针对这个 API 对动画进行控制。这需要设置一个布尔变量 isPlaying(动画运行中)、一个记录关键帧动画标识变量 raf,然后设计一个动画控制函数 aniControl,同时简单改装一下原来的动画函数 animate,像这样:</p>
<div class="hEdiv"><pre class="hEpre">
// ... 其它代码略
// 动画相关变量
var isPlaying = true, raf = null;
// 动画控制函数
var aniControl = () => {
isPlaying = !isPlaying;
animate();
};
// 动画函数
var animate = () => {
//requestAnimationFrame(animate); // 请求关键帧动画递归调用函数自身 语句改为
isPlaying ? raf = requestAnimationFrame(animate) : cancelAnimationFrame(raf);
// ...其它代码略
};
// ... 其它代码略
</pre></div>
<p>动画函数中请求关键帧动画语句的改装,将运行动画的标识赋值给 raf 变量或者取消它,前提是 isPlaying 为真或为假,换句话说,isPlaying 变量是一个开关标识,它为真(true)时表示运行动画,且运行的动画标识记录在变量 raf 上,反之当为假(false)时就取消这个 raf 记录的标识从而停止动画。</p>
<p>动画控制函数 aniControl 负责的工作则是,第一改变 isPlaying 变量,每一次运行此函数都令该变量值取反,第二运行动画函数 animate,这样,就可以有效地管控动画。动画控制函数 aniControl 将通过按钮或页面的点击等交互操作来运行,这将在下面的完整代码中体现出来,这些代码是上一讲中旋转动画机制加入了上述动画控制代码的具体整合:</p>
<div class="hEdiv"><pre id="pre1" class="hEpre">
<div style="position:absolute;margin: 10px;">点击页面可暂停、继续动画</div>
<script type="module">
import * as THREE from 'https://esm.sh/three';
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 0, 5);
var renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
var geometry = new THREE.BoxGeometry();
var material = new THREE.MeshNormalMaterial();
var mesh = new THREE.Mesh(geometry, material);
mesh.rotateZ(0.5);
scene.add(mesh);
var isPlaying = true, raf = null; // 动画开关变量、关键帧标识变量
// 动画函数 : 图形mesh在X轴、Y轴上旋转
var animate = () => {
// 依据 isPlaying运行动画或销毁动画标识
isPlaying ? raf = requestAnimationFrame(animate) : cancelAnimationFrame(raf);
mesh.rotation.x += 0.01; // X轴旋转
mesh.rotation.y += 0.01; // Y轴旋转
renderer.render(scene, camera); // 渲染器渲染效果
};
// 动画控制函数
var aniControl = () => {
isPlaying = !isPlaying; // 动画开关变量取反
animate(); // 运行动画
};
window.onresize = () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
document.onclick = () => aniControl(); // 页面单击事件 :运行动画控制函数
animate(); // 首次启动动画
</script>
</pre></div>
<blockquote><button id="btnPrev1">运行代码</button></blockquote>
<p>上面演示的控制动画的方法在更复杂的运行环境下可能需要需要做相应调整,但实现原理不会改变。这是控制动画的实现方式之一,下面接着介绍另一种方式,使用 ThreeJS 的相关机制控制动画。</p>
<p>ThreeJS 内置有一个 Clock 对象,它常用于动画系统中,我们借助这个时钟对象,可以间接但非常有效地控制动画。Clock 时钟对象拥有 running 属性可用于判断动画是否运行中、有 getDelta 方法可以用来获取动画上一帧和下一帧的间隔时间、有 stop 和 start 方法可用作暂停和继续时钟的运行。以下示例是上一讲的位移动画加入了时钟控制对象机制,相关代码有注释说明:</p>
<div class="hEdiv"><pre id="pre2" class="hEpre">
<div style="position:absolute;margin: 10px;">点击页面可暂停、继续动画</div>
<script type="module">
import * as THREE from 'https://unpkg.ihwx.cn/three@0.176.0/build/three.module.js';
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 0, 10);
camera.lookAt(0, 0, 0);
var renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
var geometry = new THREE.SphereGeometry();
var material = new THREE.MeshNormalMaterial();
var mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
var angle = 0, r = 3; // 球体运动的依赖变量(上一讲的step变量不要)
var clock = new THREE.Clock(); // 引入时钟对象
// 动画函数 : 球在Y轴上改变位置
var animate = () => {
requestAnimationFrame(animate); // 请求关键帧动画递归调用函数自身
var delta = clock.getDelta(); // 获取时钟上下帧间隔时间
angle = (angle + delta * 100) % 360; //将数据加入角度变化机制
//mesh.position.set(0, r * Math.sin(angle * Math.PI / 180), 0); // 方法一 : 方法设置
mesh.position.y = r * Math.sin(angle * Math.PI / 180); // 方法二 : 属性值
renderer.render(scene, camera); // 渲染器渲染效果
};
window.onresize = () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
// 页面点击事件 :时钟运行中则停止它,反之启动它
document.onclick = () => clock.running ? clock.stop() : clock.start();
animate();
</script>
</pre></div>
<blockquote><button id="btnPrev2">运行代码</button></blockquote>
<p>具体原理是,时钟运行时,它会持续不断地提供上一帧和下一帧间隔的时间数据,该数据以一定算法累加到动画所需数据,而当时钟停止时,上下帧的时间间隔为0,动画所需数据停留在上一帧不变。</p>
<p>动画的控制还应该有其它方法,不过 ThreeJS 初学者能灵活运用以上两种方法已经足够了。</p>
</div>
<script type="module">
import hlight from 'https://638183.freep.cn/638183/web/helight/helight1.js';
const pres = document.querySelectorAll('.hEpre');
const divs = document.querySelectorAll('.hEdiv');
divs.forEach( (div, key) => hlight.hl(div, pres));
const preView = (htmlCode, targetBox) => {
if (targetBox.innerHTML) return;
const iframe = document.createElement('iframe');
htmlCode = htmlCode + '<style>body {margin: 0; }</style>';
iframe.srcdoc = htmlCode;
targetBox.appendChild(iframe);
targetBox.style.display = 'block';
targetBox.onclick = () => {
targetBox.innerHTML = '';
targetBox.style.display = 'none';
}
};
const value1 = pre1.textContent;
const value2 = pre2.textContent;
btnPrev1.onclick = () => preView(value1, prevBox);
btnPrev2.onclick = () => preView(value2, prevBox);
</script> 谢谢黑黑老师辛苦,周日快乐! 马老师的讲解深入浅出,言简意赅,很有指导意义,谢谢马老师大佬级分享{:4_191:} 到控制动画部分了。。越来越高级。。。烧脑的说。。{:4_195:} 花飞飞 发表于 2025-5-25 15:08
到控制动画部分了。。越来越高级。。。烧脑的说。。
没点火的 黑黑直接讲述了两种实现动画的方式。貌似调用动画函数 animate比较简单点,Clock 时钟需要使用到角度,还要化算为弧度才能计算,虽然只是简单的三角函数,还是觉得代码量多了点{:4_187:} 这样对比着讲很清晰。感谢黑黑的详细讲述{:4_199:} 红影 发表于 2025-5-25 19:19
这样对比着讲很清晰。感谢黑黑的详细讲述
阔气 红影 发表于 2025-5-25 19:18
黑黑直接讲述了两种实现动画的方式。貌似调用动画函数 animate比较简单点,Clock 时钟需要使用到角度,还要 ...
其实和你的认识正好相反呢:利用 clock 是最简单的 马黑黑 发表于 2025-5-25 18:22
没点火的
{:4_173:}没点火怎么抽烟的 花飞飞 发表于 2025-5-25 21:47
没点火怎么抽烟的
抽的电子烟 马黑黑 发表于 2025-5-25 21:49
抽的电子烟
咦,那也得打火吧,啊?{:4_173:} 花飞飞 发表于 2025-5-25 21:54
咦,那也得打火吧,啊?
不用,有电就好 马黑黑 发表于 2025-5-25 19:36
阔气
非常感谢{:4_187:} 马黑黑 发表于 2025-5-25 19:37
其实和你的认识正好相反呢:利用 clock 是最简单的
我只是看代码数量的感觉,觉得 animate比较简单{:4_173:} 红影 发表于 2025-5-25 22:44
我只是看代码数量的感觉,觉得 animate比较简单
用到复杂环境它就不简单了,单单搞定 requestAnimation 关键帧动画就是个大麻烦:什么时候取消,什么时候赋值,虽然原理在那里,却老搞不好,动画要么越来越快,要么运行不起。 红影 发表于 2025-5-25 22:43
非常感谢
太阔气了 马黑黑 发表于 2025-5-26 12:41
用到复杂环境它就不简单了,单单搞定 requestAnimation 关键帧动画就是个大麻烦:什么时候取消,什么时候 ...
原来还有这样的弊端啊,嗯,那就是另一种更简单了{:4_173:} 马黑黑 发表于 2025-5-26 12:41
太阔气了
黑黑带来这么多好东西呢{:4_187:} 红影 发表于 2025-5-26 15:07
黑黑带来这么多好东西呢
哪里哪里,果酱果酱