Keys and Easing

Animation Objects, Keys and Easing Functions

A BABYLON.Animation names a property path ('position.z'), a frame rate, a value type (FLOAT, VECTOR3, QUATERNION, COLOR3...) and a loop mode (CYCLE, CONSTANT, RELATIVE); its keys are { frame, value } pairs. An easing function (CubicEase, BackEase, BounceEase, ElasticEase and more) reshapes the time between keys:

A book's pull-out as keyframes, sampled linearly and with cubic easingJavaScript
const pull = new BABYLON.Animation('pull', 'position.z', 60,
  BABYLON.Animation.ANIMATIONTYPE_FLOAT, BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT);
pull.setKeys([{ frame: 0, value: 0 }, { frame: 30, value: -0.3 }]);   // 0.3 m in 0.5 s
const linear = pull.clone();
const ease = new BABYLON.CubicEase();
ease.setEasingMode(BABYLON.EasingFunction.EASINGMODE_EASEINOUT);
pull.setEasingFunction(ease);
for (const frame of [0, 10, 15, 20, 30]) {
  const [plain, eased] = [linear, pull].map((anim) => anim.evaluate(frame).toFixed(3));
  console.log(`frame ${frame}: linear ${plain}, eased ${eased}`);
}
Output
frame 0: linear 0.000, eased 0.000
frame 10: linear -0.100, eased -0.044
frame 15: linear -0.150, eased -0.150
frame 20: linear -0.200, eased -0.256
frame 30: linear -0.300, eased -0.300

The eased pull starts and stops gently, like a hand. Frames are Babylon 89,603 's time unit: 30 frames at 60 fps last half a second at any display refresh rate.

The same pull-out keyframes played linearly and with a CubicEase, with both curves plottedHTMLLive
<!doctype html>
<style>
  body { margin: 0; background: #f7f3ec; font: 13px system-ui, sans-serif; color: #333; }
  canvas { width: 100%; max-width: 600px; height: 260px; display: block; touch-action: none; outline: none; }
  #plot { display: block; width: 100%; max-width: 600px; height: 150px; background: #fff; }
</style>
<canvas id="view"></canvas>
<canvas id="plot" width="600" height="150"></canvas>
<script src="https://cdn.jsdelivr.net/npm/babylonjs@9.28.0/babylon.js"></script>
<script>
const canvas = document.getElementById('view');
const engine = new BABYLON.Engine(canvas, true);
const scene = new BABYLON.Scene(engine);
scene.clearColor = BABYLON.Color4.FromHexString('#f7f3ecff');
const camera = new BABYLON.ArcRotateCamera('cam', -Math.PI / 2 - 0.9, 1.1, 2.6, new BABYLON.Vector3(0, 0.3, -0.1), scene);
camera.attachControl(canvas, true);
new BABYLON.HemisphericLight('sky', new BABYLON.Vector3(0.3, 1, -0.5), scene);

// Animation(name, property path, frames per second, value type, loop mode)
const pull = new BABYLON.Animation('pull', 'position.z', 60,
  BABYLON.Animation.ANIMATIONTYPE_FLOAT, BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE);
pull.setKeys([{ frame: 0, value: 0 }, { frame: 30, value: -0.3 },    // 0.3 m out in 0.5 s,
  { frame: 60, value: -0.3 }, { frame: 90, value: 0 }, { frame: 120, value: 0 }]);   // pause, back
const linear = pull.clone();
const ease = new BABYLON.CubicEase();
ease.setEasingMode(BABYLON.EasingFunction.EASINGMODE_EASEINOUT);
pull.setEasingFunction(ease);                        // reshapes the time between keys

const shelf = BABYLON.MeshBuilder.CreateBox('shelf', { width: 1.4, height: 0.04, depth: 0.4 }, scene);
shelf.material = new BABYLON.StandardMaterial('walnut', scene);
shelf.material.diffuseColor = BABYLON.Color3.FromHexString('#6d4c33');
[[linear, '#1f5f8b', -0.35], [pull, '#b5452f', 0.35]].forEach(([animation, hex, x]) => {
  const book = BABYLON.MeshBuilder.CreateBox('book', { width: 0.12, height: 0.6, depth: 0.35 }, scene);
  book.position.set(x, 0.32, 0);
  book.material = new BABYLON.StandardMaterial(hex, scene);
  book.material.diffuseColor = BABYLON.Color3.FromHexString(hex);
  book.animations = [animation];
  scene.beginAnimation(book, 0, 120, true);
});

// Plot evaluate(frame) for the first 30 frames: blue linear, red eased
const ctx = document.getElementById('plot').getContext('2d');
ctx.font = '12px sans-serif';
ctx.strokeStyle = '#ddd';
ctx.strokeRect(40, 10, 540, 110);
[[linear, '#1f5f8b'], [pull, '#b5452f']].forEach(([animation, color]) => {
  ctx.strokeStyle = color;
  ctx.lineWidth = 2.5;
  ctx.beginPath();
  for (let frame = 0; frame <= 30; frame += 0.5) {
    const x = 40 + frame / 30 * 540, y = 10 + animation.evaluate(frame) / -0.3 * 110;
    frame ? ctx.lineTo(x, y) : ctx.moveTo(x, y);
  }
  ctx.stroke();
});
ctx.fillStyle = '#333';
ctx.fillText('0', 28, 16); ctx.fillText('-0.3', 10, 124); ctx.fillText('frame 0', 40, 140); ctx.fillText('frame 30', 530, 140);
ctx.fillStyle = '#1f5f8b'; ctx.fillText('linear', 60, 60);
ctx.fillStyle = '#b5452f'; ctx.fillText('CubicEase, EASEINOUT', 330, 60);
engine.runRenderLoop(() => scene.render());
addEventListener('resize', () => engine.resize());
</script>