Three.js extended transforms

A CSS-like transforms API for Three.js 3D objects and instanced meshes.

Position, rotation, and scale animate per-axis like in CSS. The adapter also adds skew and transformOrigin on top of the built-in Three.js transforms. The same names work on per-instance proxies from getInstances() for InstancedMesh and BatchedMesh, so individual instances can be transformed independently.

animate(mesh, {
  x: 100,
  rotateY: 360,
  scale: 1.5,
  skewX: 15,
  transformOrigin: '0 1 0',
});

Position, rotation and scale

x / y / z write to mesh.position. rotateX / rotateY / rotateZ write to mesh.rotation in degrees. scaleX / scaleY / scaleZ write to mesh.scale, with scale as a shorthand for all three.

Name Maps to Unit
x mesh.position.x
y mesh.position.y
z mesh.position.z
rotateX mesh.rotation.x degrees
rotateY mesh.rotation.y degrees
rotateZ mesh.rotation.z degrees
scaleX mesh.scale.x
scaleY mesh.scale.y
scaleZ mesh.scale.z
scale mesh.scale.x / .y / .z (uniform)

Rotations assume the default Euler order 'XYZ'. Animating rotateX / rotateY / rotateZ on a target whose rotation.order has been changed will not match what the property names suggest.

Skew

skewX and skewY apply CSS-style angular shears on the X and Y axes. skewZ shears z by x in the same fashion. All three are applied after position / rotation / scale, and skipped when their value is 0.

Name Maps to Unit
skewX tan(skewX) shear of y by x degrees
skewY tan(skewY) shear of x by y degrees
skewZ tan(skewZ) shear of z by x degrees

Transform origin

Matches the CSS property: shifts the pivot used by skew and rotation. Values are in the target's geometry units.

Name Maps to Notes
transformOriginX origin shift on the X axis
transformOriginY origin shift on the Y axis
transformOriginZ origin shift on the Z axis
transformOrigin 'x y z' shorthand 3-token string, all axes at once

Automatic visibility toggle

Setting any scale axis or opacity to 0 flips target.visible to false, so meshes that have been faded or scaled out of view are skipped during render. Returning to a non-zero value restores target.visible = true.

The visibility flip only fires when the property is written through animate() or utils.set(). Direct mutation of mesh.scale.x = 0 or mesh.material.opacity = 0 outside the adapter does not touch mesh.visible.

Three.js extended transforms code example

import { animate, createTimer, utils } from 'animejs';
import * as THREE from 'three';
import 'animejs/adapters/three';

// Three.js setup

const [ $container ] = utils.$('.full-container');
const color = utils.get($container, 'color');
const { width, height } = $container.getBoundingClientRect();

const renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true, preserveDrawingBuffer: true });
renderer.shadowMap.enabled = true;
renderer.setSize(width, height);
renderer.setPixelRatio(window.devicePixelRatio);
$container.appendChild(renderer.domElement);

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(35, width / height, 0.1, 100);
camera.position.set(0, 3, 8);
camera.lookAt(0, 0.2, 0);
const cameraRig = new THREE.Group().add(camera);
scene.add(cameraRig);

scene.add(new THREE.AmbientLight(0xffffff, 0.3));
const spot = new THREE.SpotLight(0xffffff, 100, 12, Math.PI / 5, 0.4);
spot.position.set(0, 5, 0);
spot.castShadow = true;
scene.add(spot);

const dirLight = new THREE.DirectionalLight(0xffffff, 0.5);
dirLight.position.set(2, 3, 4);
scene.add(dirLight);

const groundGeometry = new THREE.PlaneGeometry(12, 12);
const groundMaterial = new THREE.MeshLambertMaterial({ color });
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
ground.rotation.x = -Math.PI / 2;
ground.receiveShadow = true;
scene.add(ground);

const gridColorA = utils.get($container, '--hex-current-1');
const gridColorB = utils.get($container, '--hex-current-3');
const grid = new THREE.GridHelper(12, 24, gridColorA, gridColorB);
grid.position.y = 0.001;
scene.add(grid);

const geometry = new THREE.BoxGeometry(1, 1, 1);
const cubes = [-2, 0, 2].map(x => {
  const cube = new THREE.Mesh(geometry, new THREE.MeshLambertMaterial());
  cube.position.set(x, 0.5, 0);
  cube.castShadow = cube.receiveShadow = true;
  scene.add(cube);
  return cube;
});

// Animation with Three.js adapter

animate(cameraRig, {
  rotateY: 360,
  duration: 20000,
  loop: true,
  ease: 'linear',
});

utils.set(cubes[0], { color: 'var(--hex-red-1)' });
utils.set(cubes[1], { color: 'var(--hex-citrus-1)', transformOriginY: -0.5 });
utils.set(cubes[2], { color: 'var(--hex-green-1)', transformOrigin: '-.5 -.5 .5' });

// Cube 1: small up and down with rotation
animate(cubes[0], {
  y: [0.5, 1, 0.5],
  rotateY: 180,
  duration: 2000,
  loop: true,
  ease: 'inOutSine',
});

// Cube 2: skew left to right pivoting on its base
animate(cubes[1], {
  skewX: [-30, 30, 0, 0],
  skewZ: [0, 0, -30, 30],
  duration: 2000,
  alternate: true,
  loop: true,
  ease: 'inOutSine',
});

// Cube 3: scale with the origin sliding from one corner to the opposite
animate(cubes[2], {
  scale: [1, .25, 1],
  transformOrigin: ['-.5 -.5 .5', '.5 .5 -.5'],
  duration: 2000,
  alternate: true,
  loop: true,
  ease: 'inOutSine',
});

createTimer({ onUpdate: () => renderer.render(scene, camera) });
<div class="full-container"></div>