Using Animation Curves
All keyframes added to an Animation Property of a node in an Animation Clip are shown as linear traces in the corresponding animation property track, which is an animation curve. That is, an Animation Curve describes the change in an Animation Property on an object over time.
The Animation Curve stores internally a series of time points, each of which corresponds to a (curve) value, called a frame (keyframe). When the animation system runs, the Animation Component calculates the (result) value that should be assigned to the object at the specified time point based on the current animation state and completes the property change, a calculation process called sampling.
The following code snippet demonstrates how to create Animation Clips programmatically:
import { AnimationClip, animation, js } from 'cc';
const animationClip = new AnimationClip();
animationClip.duration = 1.0; // The entire period of the animation clip, the frame time of any keyframe should not be greater than this property
animationClip.keys = [ [ 0.3, 0.6, 0.9 ] ]; // The frame time shared by all curves of the clip
animationClip.curves = [{ // Animation curves on the Animation component
modifiers: [ // Address the target object from the current node object. See the "Target Objects" section below for details
// The target object is the "Body" child of the current node.
HierarchyPath('Body'),
// The target object is the "MyComponent" component on the "Body" child of the current node.
ComponentPath(js.getClassName(MyComponent)),
// The target object is the "value" property on the "MyComponent" component of the "Body" child of the current node.
'value',
],
data: {
// Indexed to 'animationClip.keys', i.e. [ 0.3, 0.6, 0.9 ]
keys: 0,
// Keyframe data
values: [ 0.0, 0.5, 1.0 ],
},
}];
The above Animation Clip contains an Animation Curve that controls the value
property of the MyComponent component in the Body child node. The Animation Curve includes three keyframes that make the value
property change to 0.0 at 0.3 seconds, 0.5 at 0.6 seconds, and 1.0 at 0.9 seconds.
Note: the frame times of the keyframes on the animation curve are indexed by reference to the
AnimationClip.keys
array. In this way, multiple curves can share frame times, which will result in additional performance optimization.
Target objects (modifiers
)
The target of an animation curve can be any JavaScript object. The modifiers
field specifies how the target object is addressed from the current node object at runtime.
modifiers
is an array, each element of which expresses how to address an object from a higher level to another object, with the last element addressed as the target of the curve. This behavior is like a file system path, so each element is called a "target path".
When the target path is string
or number
, it addresses the property of the higher-level object, which itself specifies the property name. Otherwise, the target path must be an object that implements the interface animation.TargetPath
.
Cocos Creator has the following built-in classes that implement the interface animation.TargetPath
:
animation.HierarchyPath
treats a higher-level object as a node and addresses one of its childrenanimation.ComponentPath
treats a higher-level object as a node and addresses one of its components
The target paths can be any combination, as long as they have the correct meaning:
modifiers: [
// The target object is the first element of the "names" property of the "BlahBlahComponent" component on the "nested_3" child node of the "nested_2" child node of the "nested_1" child node
new animation.HierarchyPath('nested_1/nested_2/nested_3'),
new animation.ComponentPath(js.getClassName(BlahBlahComponent)),
'names',
0,
]
Custom target paths are useful when the target object is not an property, but must be returned from a method:
class BlahBlahComponent extends Component {
public getName(index: number) { return _names[index]; }
private _names: string[] = [];
}
modifiers: [
// The target object is the first "name" of the "BlahBlahComponent" component on the "nested_3" child node of the "nested_2" child node of the "nested_1" child node
new animation.HierarchyPath('nested_1/nested_2/nested_3'),
new animation.ComponentPath(js.getClassName(BlahBlahComponent)),
{
get: (target: BlahBlahComponent) => target.getName(0),
},
]
For custom target paths to be serializable, declare them as classes:
@ccclass
class MyPath implements animation.TargetPath {
@property
public index = 0;
constructor(index: number) { this.index = index; }
get (target: BlahBlahComponent) {
return target.getName(this.index);
}
}
modifiers: [
// The target object is the first "name" of the "BlahBlahComponent" component on the "nested_3" child node of the "nested_2" child node of the "nested_1" child node
new animation.HierarchyPath('nested_1/nested_2/nested_3'),
new animation.ComponentPath(js.getClassName(BlahBlahComponent)),
new MyPath(0),
]
The addressing of the target object is done at runtime, a feature that allows animation clips to be reused on multiple objects.
Assignment
When a value is sampled, by default the value will be set to the target object using the assignment operator =
.
Sometimes, the assignment operator cannot be used to complete the setting. For example, when setting the Uniform of a material object, it can't be done with the assignment operator. For this case, the curve field valueAdapter
provides a mechanism to customize the value to the target object.
Example:
class BlahBlahComponent {
public setUniform(index: number, value: number) { /* */ }
}
{ // Curve
valueAdapter: {
// Called when the curve is instantiated
forTarget(target: BlahBlahComponent) {
// Do something useful here
return {
// Called each time the value of the target object is set
set(value: number) {
target.setUniform(0, value);
}
};
}
},
};
For the custom assignment to be serializable, declare them as classes:
@ccclass
class MyValueProxy implements animation.
@property
public index: number = 0;
constructor(index: number) { this.index = index; }
// Called when the curve is instantiated
public forTarget(target: BlahBlahComponent) {
// Do something useful here
return {
// Called each time the value of the target object is set
set(value: number) {
target.setUniform(0, value);
}
};
}
}
UniformProxyFactory
is one such custom assignment class that implements setting the uniform value of a material:
{
modifiers: [
// The target object is the first material of the "sharedMaterials" property of the "MeshRenderer" component
ComponentPath(js.getClassName(MeshRenderer)),
'sharedMaterials',
0,
],
valueAdapter: new animation.UniformProxyFactory(
0, // Pass index
'albedo', // Uniform name
),
};
Sampling
If the sampled time point is exactly equal to the time point of a keyframe, then the animation data on that keyframe is used. Otherwise, when the sampling time point is between two frames, the result value is affected by the data of both frames, and the scale of the sampling time point on the moment interval of the two keyframes ([0, 1]
) reflects the extent of the effect.
Cocos Creator allows this scale to be mapped to another scale to achieve a different "fade" effect. These mapping methods are called fading methods in Creator. After the scale is determined, the final result is calculated based on the specified interpolation method.
Note: both the fade method and the interpolation method affect the smoothness of the animation.
Fade method
The fade method for each frame can be specified, or a uniform fade method can be used for all frames. The fade method can be the name of a built-in fade method or a Bessel control point.
The following are some of the commonly used fade methods:
linear
: maintains the original scale, i.e. linear gradient, which is used by default when no gradient is specifiedconstant
: always use scale 0, i.e. no gradient, similar to interpolationStep
.quadIn
: fade from slow to fastquadOut
: fade from fast to slowquadInOut
: fading from slow to fast to slow againquadOutIn
: fade from fast to slow to fast
Curve values and interpolation methods
Some interpolation algorithms require additional data to be stored in the curve value for each frame, so the curve value is not necessarily the same as the value type of the target property. For numeric types or value types, Cocos Creator provides several general interpolation methods. Also, it is possible to define custom interpolation methods.
When the interpolate property of the curve data is
true
, the curve will try to use the interpolation function.- If the curve value is of type
number
,Number
, linear interpolation will be applied. - If the curve value inherits from
ValueType
, thelerp
function ofValueType
will be called to complete the interpolation.lerp
methods for most value types built into Cocos Creator are implemented as linear interpolation, e.g.:Vec3
,vec4
, etc. - If the curve value is interpolable, the
lerp
function for the curve value will be called to complete the interpolation[^1].
- If the curve value is of type
If the curve value does not satisfy any of the above conditions, or when the
interpolate
property of the curve data isfalse
, no interpolation will be performed and the curve value from the previous frame will always be used as the result.tsimport { AnimationClip, color, IPropertyCurveData, SpriteFrame, Vec3 } from 'cc'; const animationClip = new AnimationClip(); const keys = [ 0, 0.5, 1.0, 2.0 ]; animationClip.duration = keys.length === 0 ? 0 : keys[keys.length - 1]; animationClip.keys = [ keys ]; // All curves share one column of frame time // Linear interpolation using values const numberCurve: IPropertyCurveData = { keys: 0, values: [ 0, 1, 2, 3 ], /* interpolate: true, */ // "interpolate" property is on by default }; // Using lerp() of value type Vec3 const vec3Curve: IPropertyCurveData = { keys: 0, values: [ new Vec3(0), new Vec3(2), new Vec3(4), new Vec3(6) ], interpolate: true, }; // No interpolation (because interpolation is explicitly disabled) const colorCuve: IPropertyCurveData = { keys: 0, values: [ color(255), color(128), color(61), color(0) ], interpolate: false, // No interpolation }; // No interpolation (because SpriteFrame cannot be interpolated) const spriteCurve: IPropertyCurveData = { keys: 0, values: [ new SpriteFrame(), new SpriteFrame(), new SpriteFrame(), new SpriteFrame() ], };
Custom interpolation algorithm
The sample code is as follows:
tsimport { ILerpable, IPropertyCurveData, Quat, quat, Vec3, vmath } from 'cc'; class MyCurveValue implements ILerpable { public position: Vec3; public rotation: Quat; constructor(position: Vec3, rotation: Quat) { this.position = position; this.rotation = rotation; } /** This method will be called for interpolation * @param this initial curve value * @param to target curve value * @param t interpolation ratio, in the range [0, 1] * @param dt the frame time interval between the starting curve value and the target curve value */ lerp (to: MyCurveValue, t: number, dt: number) { return new MyCurveValue( // The position property is not interpolated this.position.clone(), // The rotation property uses Quat's lerp() method this.rotation.lerp(to.rotation, t), // ); } /** This method is called when not interpolating * It is optional, if this method is not defined, then the curve value itself (i.e. this) is used as the result value */ getNoLerp () { return this; } } /** * Creates a curve that implements a smooth rotation but abrupt position change throughout the cycle. */ function createMyCurve (): IPropertyCurveData { const rotation1 = quat(); const rotation2 = quat(); const rotation3 = quat(); vmath.quat.rotateY(rotation1, rotation1, 0); vmath.quat.rotateY(rotation2, rotation2, Math.PI); vmath.quat.rotateY(rotation3, rotation3, 0); return { keys: 0 /* frame time */, values: [ new MyCurveValue(new Vec3(0), rotation1), new MyCurveValue(new Vec3(10), rotation2), new MyCurveValue(new Vec3(0), rotation3), ], }; }
Wrap mode
Different wrap modes can be set for Animation Clips by setting AnimationClip.wrapMode
. The following lists several common wrap modes:
Property | Description |
---|---|
Normal | Playback stops at the end |
WrapMode.Loop | Loop |
PingPong | Play from the beginning to the end of the animation, then play back to the beginning from the end, and so on. |
For more wrap modes, please refer to the WrapMode API and the Wrap Mode and Repeat Count documentation.
[^1]: For numeric, quaternion, and various vectors, Cocos Creator provides the appropriate interpolate classes to implement Cubic Spline Interpolation.