您正在寻找的是一种称为“补间”的东西,您可以在其中绘制中间步骤,而不是立即跳到最终结果。有几个 JavaScript 库可以为您执行此操作,但我将介绍一些自己实现它的基础知识。
以你为例。当您点击左箭头时,您将沿-x 轴移动网格1 单元,并围绕+y 轴旋转-PI/2。与其捕捉这些位置/旋转,不如考虑您希望动画持续多长时间,然后开始划分步骤。
假设您要拍摄500ms(半秒)。您的浏览器尝试以大约60fps 运行,因此您有30 帧(大约500ms)以该速率运行。因此,对于每一帧,您可以移动框 1/30 单位,并将其旋转 -PI/60。在30 帧之后,框应该在正确的位置,给或取一些舍入。
在谈论浏览器的帧率时,我使用“about”,因为不能保证您总是得到60FPS。如果您的帧率下降,并且您被锁定到帧率来绘制动画,那么它也会减慢并花费比您想要的更长的时间。那么有什么办法呢?
与其依赖requestAnimationFrame 作为您的计时器,您可以设置一个真实 计时器来逐步完成您的动画。折腾计算完成动画所需的帧数,而是计算所需的步骤。
我们已经知道60fps 大约是1 帧每16.6ms,所以这是浏览器可以绘制的绝对最大目标。但是,当我们逐步进行更新时,没有什么能阻止我们走得更快。为了使事情更容易计算,假设我们想要执行50 更新步骤,而不是之前的30。这意味着对于500ms 播放时间,我们需要每隔10ms 执行一次更新(比帧速率略快)。另外,因为我们执行了 50 步,我们将以1/50 为单位更新位置,并以-PI/100 为单位进行旋转。
let animationId = setInterval( ()=>{
// update position by 1/50 units
// update rotation by -PI/100
}, 10 ); // every 10 ms
随着时间间隔的运行,它将更新对象。同时,动画循环会尽可能地生成新的帧。
这是一个完整的运行示例,仅支持左箭头:
let W = window.innerWidth;
let H = window.innerHeight;
const renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true
});
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(28, 1, 1, 1000);
camera.position.set(0, 0, 50);
camera.lookAt(scene.position);
scene.add(camera);
const light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(0, 0, -1);
camera.add(light);
const cube = new THREE.Mesh(
new THREE.BoxBufferGeometry(1, 1, 1),
new THREE.MeshPhongMaterial({
color: "red"
})
);
cube.position.set(10, 0, 0);
scene.add(cube);
function render() {
renderer.render(scene, camera);
}
function resize() {
W = window.innerWidth;
H = window.innerHeight;
renderer.setSize(W, H);
camera.aspect = W / H;
camera.updateProjectionMatrix();
render();
}
window.addEventListener("resize", resize);
resize();
function animate() {
requestAnimationFrame(animate);
render();
}
requestAnimationFrame(animate);
const yAxis = new THREE.Vector3(0, 1, 0);
function updateCube() {
//cube.position.x -= 1;
//cube.rotateOnWorldAxis(yAxis, THREE.Math.degToRad(-90));
cube.position.x -= 1 / 50;
cube.rotateOnWorldAxis(yAxis, -(Math.PI / 100));
}
let step = 0;
let animationId = null;
function startStepping() {
animationId = setInterval(() => {
updateCube();
if (++step === 50) {
clearInterval(animationId);
animationId = null;
}
}, 10)
}
function handleKeyboard(e) {
//if (e.keyCode == 65 || e.keyCode == 37) {
// updateCube();
//}
if (animationId === null && (e.keyCode == 65 || e.keyCode == 37)) {
step = 0;
startStepping();
}
}
document.addEventListener("keydown", handleKeyboard, false);
html,
body {
width: 100%;
height: 100%;
padding: 0;
margin: 0;
overflow: hidden;
background: skyblue;
}
<script src="https://threejs.org/build/three.min.js"></script>
现在这种方法有一个缺点。有时您可能会看到更新被跳过(如果浏览器的帧率下降),或者相同的位置被绘制了两次(如果您的更新率明显低于浏览器的帧率)。您可以尝试两全其美,但从渲染循环中实时计算帧速率,并相应地调整帧步长,但此时您必须询问花费在计算这些统计数据上的额外时间是否真的会伤害尝试实现摇滚- 稳定的帧率锁定绘制率。
根据按键输入切换
由于您的键输入现在与绘图脱节,您现在需要某种标志来确定正在执行的操作。您的按键处理程序将设置该标志,然后updateCube 将根据该标志采取行动。比如:
let action = null
function startStepping(){
// set up the interval...
// but then also ensure the action stops after the animation plays:
setTimeout( () => action = null, 500 );
}
function handleKeyboard(e){
if (animationId === null) {
step = 0;
switch(e.keyCode){
case 37:
case 65:
action = "left";
break;
// other keys...
}
startStepping();
}
}
function updateCube(){
switch(action){
case "left":
// move as if it's rolling left
break;
case "right":
// move as if it's rolling right
break;
// etc. for the other directions
}
}