animejs
Version:
JavaScript animation engine
213 lines (191 loc) • 8.7 kB
JavaScript
/**
* Anime.js - adapters - ESM
* @version v4.5.0
* @license MIT
* @copyright 2026 - Julian Garnier
*/
import { Object3D, Color, SRGBColorSpace } from 'three';
import { radToDeg, degToRad } from '../../utils/number.js';
import { threeAdapter } from './adapter.js';
import { readScalar, PATH_DIRECT, writeScalar, readColorAt, writeColorAt, readColorHex, COLOR_NORM, applySkewOrigin } from './helpers.js';
/**
* `Object3D` adapter. Maps flat properties (position, rotation, scale, opacity, color, visible, light / audio / camera specifics) onto the matching three.js fields, and auto-detects material props (direct / uniform / TSL `UniformNode`) on the target's `material` at tween creation.
*/
const colorTarget = (t) => t.isLight ? t : t.material;
// Per-mesh skew and transformOrigin state, stored under a private symbol so we add one hidden property instead of polluting the Object3D surface with seven underscored fields.
const skewSymbol = Symbol('animejs.skewOrigin');
// Fallback opacity storage for Object3D targets without a material such as Groups or CSS2DObjects.
// Without it reads always return the default 1, so reverse-direction tweens between two non-default values collapse to a no-op delta and the visibility flip never fires on the way back.
const opacitySymbol = Symbol('animejs.opacity');
/**
* Sets `o.visible` based on opacity and current scale. If `opacity` is omitted, reads it from `o.material` (defaults to `1` when no material is present).
*
* @param {Object3D & { material?: import('three').Material | import('three').Material[] }} o
* @param {number} [opacity]
*/
function setVisibility(o, opacity) {
if (opacity === undefined) {
if (o.material) {
opacity = readScalar(o.material, 'opacity', PATH_DIRECT, 1);
} else {
const v = o[opacitySymbol];
opacity = v === undefined ? 1 : v;
}
}
const s = o.scale;
o.visible = !!(opacity && s.x && s.y && s.z);
}
/**
* Installs a per-mesh `updateMatrix` override on first skew / transformOrigin write. The override calls the original three.js compose, then patches the result with `applySkewOrigin` whenever any axis is non-zero. Idempotent.
*
* @param {any} mesh
* @return {{ skewX: number, skewY: number, skewZ: number, originX: number, originY: number, originZ: number }}
*/
function installSkewMatrix(mesh) {
let s = mesh[skewSymbol];
if (s) return s;
s = mesh[skewSymbol] = { skewX: 0, skewY: 0, skewZ: 0, originX: 0, originY: 0, originZ: 0 };
const original = mesh.updateMatrix;
mesh.updateMatrix = function () {
original.call(this);
const st = this[skewSymbol];
if (st.skewX !== 0 || st.skewY !== 0 || st.skewZ !== 0 || st.originX !== 0 || st.originY !== 0 || st.originZ !== 0) {
applySkewOrigin(this.matrix.elements, st.skewX, st.skewY, st.skewZ, st.originX, st.originY, st.originZ);
}
};
return s;
}
const threeObject3D = threeAdapter.registerTargetAdapter((t) => t instanceof Object3D);
// Position
threeObject3D.registerProperty('x', (t) => t.position.x, (t, v) => { t.position.x = v; });
threeObject3D.registerProperty('y', (t) => t.position.y, (t, v) => { t.position.y = v; });
threeObject3D.registerProperty('z', (t) => t.position.z, (t, v) => { t.position.z = v; });
// Rotation in degrees
threeObject3D.registerProperty('rotateX', (t) => radToDeg(t.rotation.x), (t, v) => { t.rotation.x = degToRad(v); });
threeObject3D.registerProperty('rotateY', (t) => radToDeg(t.rotation.y), (t, v) => { t.rotation.y = degToRad(v); });
threeObject3D.registerProperty('rotateZ', (t) => radToDeg(t.rotation.z), (t, v) => { t.rotation.z = degToRad(v); });
// Scale per-axis and uniform
threeObject3D.registerProperty('scaleX', (t) => t.scale.x, (t, v) => { t.scale.x = v; setVisibility(t); });
threeObject3D.registerProperty('scaleY', (t) => t.scale.y, (t, v) => { t.scale.y = v; setVisibility(t); });
threeObject3D.registerProperty('scaleZ', (t) => t.scale.z, (t, v) => { t.scale.z = v; setVisibility(t); });
threeObject3D.registerProperty('scale',
(t) => t.scale.x,
(t, v) => {
t.scale.x = v; t.scale.y = v; t.scale.z = v;
setVisibility(t);
},
);
// Material
threeObject3D.registerProperty('visible', (t) => t.visible, (t, v) => { t.visible = !!v; });
threeObject3D.registerProperty('opacity',
(t) => {
if (t.material) return readScalar(t.material, 'opacity', PATH_DIRECT, 1);
const v = t[opacitySymbol];
return v === undefined ? 1 : v;
},
(t, v) => {
if (t.material) writeScalar(t.material, 'opacity', v, PATH_DIRECT);
else t[opacitySymbol] = v;
setVisibility(t, v);
},
(t) => !t.isLight,
);
threeObject3D.registerProperty('color',
(t) => readColorAt(colorTarget(t), 'color', PATH_DIRECT),
(t, _, tw) => writeColorAt(colorTarget(t), 'color', tw._numbers, PATH_DIRECT),
);
// HemisphereLight
threeObject3D.registerProperty('groundColor',
(t) => readColorAt(t, 'groundColor', PATH_DIRECT),
(t, _, tw) => writeColorAt(t, 'groundColor', tw._numbers, PATH_DIRECT),
(t) => !!t.groundColor,
);
// Scene background, lazily initializes scene background to a Color on first write so the user does not have to set one upfront.
threeObject3D.registerProperty('background',
(t) => {
const bg = /** @type {any} */(t).background;
return bg && bg.isColor ? readColorHex(bg) : '#000000';
},
(t, _, tw) => {
const scene = /** @type {any} */(t);
let bg = scene.background;
if (!bg || !bg.isColor) {
bg = new Color();
scene.background = bg;
}
const ns = tw._numbers;
bg.setRGB(ns[0] * COLOR_NORM, ns[1] * COLOR_NORM, ns[2] * COLOR_NORM, SRGBColorSpace);
},
(t) => !!t.isScene,
);
// Method-bridged props for Audio and PerspectiveCamera focalLength, read via getXxx and write via setXxx.
const registerMethodProp = (apiName, getter, setter, gate) => {
threeObject3D.registerProperty(apiName,
(t) => t[getter](),
(t, v) => { t[setter](v); },
(t) => !!t[gate],
);
};
registerMethodProp('volume', 'getVolume', 'setVolume', 'setVolume');
// PositionalAudio
registerMethodProp('refDistance', 'getRefDistance', 'setRefDistance', 'setRefDistance');
registerMethodProp('rolloffFactor', 'getRolloffFactor', 'setRolloffFactor', 'setRefDistance');
registerMethodProp('maxDistance', 'getMaxDistance', 'setMaxDistance', 'setRefDistance');
// PerspectiveCamera
registerMethodProp('focalLength', 'getFocalLength', 'setFocalLength', 'setFocalLength');
// Camera projection-update props, setter writes the value then calls updateProjectionMatrix.
const registerCameraProp = (apiName, gate) => {
threeObject3D.registerProperty(apiName,
(t) => t[apiName],
(t, v) => { t[apiName] = v; t.updateProjectionMatrix(); },
gate,
);
};
const isPersp = (t) => !!t.isPerspectiveCamera;
const isOrtho = (t) => !!t.isOrthographicCamera;
registerCameraProp('fov', isPersp);
registerCameraProp('aspect', isPersp);
registerCameraProp('left', isOrtho);
registerCameraProp('right', isOrtho);
registerCameraProp('top', isOrtho);
registerCameraProp('bottom', isOrtho);
// Skew in degrees and transform-origin in object-space.
// Patches mesh updateMatrix on first write so the standard PRS compose is followed by an in-place skew and origin shift.
// Reads default to 0 for meshes that have never been written.
/**
* @param {String} apiName
* @param {String} key
*/
const registerSkewProp = (apiName, key) => {
threeObject3D.registerProperty(apiName,
(t) => { const s = /** @type {any} */(t)[skewSymbol]; return s ? s[key] : 0; },
(t, v) => { installSkewMatrix(t)[key] = v; },
);
};
registerSkewProp('skewX', 'skewX');
registerSkewProp('skewY', 'skewY');
registerSkewProp('skewZ', 'skewZ');
registerSkewProp('transformOriginX', 'originX');
registerSkewProp('transformOriginY', 'originY');
registerSkewProp('transformOriginZ', 'originZ');
// Shorthand 3-token string x y z or 2-string from-to array routed via the engine COMPLEX path.
// The setter reads tween _numbers for the per-frame lerped triplet.
threeObject3D.registerProperty('transformOrigin',
(t) => {
const s = /** @type {any} */(t)[skewSymbol];
return s ? `${s.originX} ${s.originY} ${s.originZ}` : '0 0 0';
},
(t, _, tw) => {
const s = installSkewMatrix(t);
const ns = tw._numbers;
s.originX = ns[0];
s.originY = ns[1];
s.originZ = ns[2];
},
);
// Shared by both camera types.
const isCamera = (t) => !!t.isPerspectiveCamera || !!t.isOrthographicCamera;
registerCameraProp('near', isCamera);
registerCameraProp('far', isCamera);
registerCameraProp('zoom', isCamera);
// Auto-detection for material props on meshes and direct Vector / Color fields on the target is provided by the shared resolvers in resolvers.js.