Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Table of Contents

Animate()

Interpolates bone fields based on provided animation data, as well as initial states for non-animated fields.

 function Animate(bones: Bone[], anims: Animation[], frames: int[], smoothFrames: int[]) {
    for (let a = 0; a < anims.length; a++) {
        for (k = 0; k < anims[a].keyframes.length; k++) {
            let kf = anims[a].keyframes[k];

            // only prev keyframes are considered
            if (kf.frame > frames[a]) {
                break;
            }

            // refer keyframe to itself, if there's no next
            if (kf.next_kf == -1) {
                kf.next_kf = k;
            }

            let nextKf = anims[a].keyframes[kf.next_kf];

            // skip keyframe if it's not the last, and would not be animated
            let isLast = kf.next_kf == k;
            let isBeforeFrame = nextKf.frame < frames[a];
            if (isBeforeFrame && !isLast) {
                continue;
            }

            // interpolate the relevant bone field, based on this and next keyframes' values
            let bone = bones[kf.bone_id]
            if (kf.element == "PositionX")
                interpolateKeyframes(bone.pos.x, kf, nextKf, frames[a], smoothFrames[a]);
            if (kf.element == "PositionY")
                interpolateKeyframes(bone.pos.y, kf, nextKf, frames[a], smoothFrames[a]);
            if (kf.element == "Rotation")
                interpolateKeyframes(bone.rot, kf, nextKf, frames[a], smoothFrames[a]);
            if (kf.element == "ScaleX")
                interpolateKeyframes(bone.scale.x, kf, nextKf, frames[a], smoothFrames[a]);
            if (kf.element == "ScaleY")
                interpolateKeyframes(bone.scale.y, kf, nextKf, frames[a], smoothFrames[a]);
            if (kf.element == "Hidden")
                bone.hidden = kf.value == 1;
        }
    }

    // bones that are not being animated should return to their initial states
    resetBones(bones, anims, frames[0], smoothFrames[0])
} 

interpolateKeyframes()

With the provided animation frame, determines the keyframes to interpolate the field by.

The resulting interpolation from the keyframes is interpolated again for smoothing.

 function interpolateKeyframes(
    field: float, prevKf: Keyframe, nextKf: Keyframe, frame: int, smoothFrame: int
): float {
    const totalFrames = nextKf.frame - prevKf.frame
    const currentFrame = frame - prevKf.frame

    // interpolate from current to next keyframe value
    const result = interpolate(
        currentFrame,
        totalFrames,
        prevKf.value,
        nextKf.value,
        nextKf.start_handle,
        nextKf.end_handle
    )

    // using the requested smoothing frames, interpolate the current field to the target value
    let z = { x: 0, y: 0 }
    interpolate(currentFrame, smoothFrame, field, result, z, z)
}
 

interpolate()

Interpolation uses a modified bezier spline (explanation below).

Note that 2 helper functions are included below the main function.

function interp(
    current: int,
    max: int,
    start_val: float,
    end_val: float,
    start_handle: Vec2,
    end_handle: Vec2,
): float {
    // snapping behavior for Snap transition preset
    if(start_handle.y == 999.0 && end_handle.y == 999.0) {
        return start_val;
    }
    if(max == 0 || current >= max) {
        return end_val;
    }

    // solve for t with Newton-Raphson
    let initial = current / max
    let t = initial
    for(let i = 0; i < 5; i++) {
        let x = cubic_bezier(t, start_handle.x, end_handle.x)
        let dx = cubic_bezier_derivative(t, start_handle.x, end_handle.x)
        if(abs(dx) < 1e-5 {
            break
        }
        t -= (x - initial) / dx
        t = clamp(t, 0.0, 1.0)
    }

    let progress = cubic_bezier(t, start_handle.y, end_handle.y)
    return start_val + (end_val - start_val) * progress
}

// for both functions below, p0 and p3 are always 0 and 1 respectively

function cubicBezier(t: float, p1: float, p2: float): float {
    let u = 1. - t
    return 3. * u * u * t * p1 + 3. * u * t * t * p2 + t * t * t
}

function cubicBezierDerivative(t: float, p1: float, p2: float): float {
    let u = 1. - t
    return 3. * u * u * p1 + 6. * u * t * (p2 - p1) + 3. * t * t * (1. - p2)
}

Bezier Explanation

Note: the following explanation is incomplete, as it doesn’t include Newton-Rapshon. However, understanding this is not required to implement the code above.

A Primer on Bezier Curves

The bezier spline uses the following polynomial:

value =
    a * (1 - t)^3 +
    b * 3 * (1 - t)^2 * t +
    c * 3 * (1 - t) * t^2 +
    d * t^3

This can be simplified into 4 points:

FormulaCoefficient (a, b, c, d)
h00(1 - t)^3startVal
h013 * (1 - t)^2 * tstartHandle
h103 * (1 - t) * t^2endHandle
h11t^3endVal

The above is for a generic bezier spline, however.

In interpolation, startVal and endVal should be 0 and 1 respectively to represent 0% to 100% of the end value. This allows the algorithm to have a persistent curve regardless of the actual values being interpolated.

Simplified points:

FormulaCoefficient (b, c, d)
h013 * (1 - t)^2 * tstartHandle
h103 * (1 -t) * t^2endHandle
h11t^31

Notice that h00 is now gone, as its coefficient, startVal, is always 0 and would have no effect on the algorithm.

The actual start and end values are applied at the end:

progress = h10 * startHandle + h01 * endHandle + h11
value = start + (end - start) * progress

resetBones()

Animate bones back to their initial states, if they’re not being animated.

This makes use of each bones’ init_ fields (init_pos, init_rot, etc).

Example: animation 1 was played and rotated the arm bone, but animation 1 is not being played anymore. That arm bone must now return to its initial rotation.

 function resetBones(bones, animations, frame, smoothFrame) {
    elementMap = {}

    // add every element that's being animated for each bone
    anims.forEach(anim => {
        anim.keyframes.forEach(kf => {
            elementMap[kf.bone_id].push(kf.element)
        })
    jj

    // animate every element that's not in the map, back to its initial state
    bones.forEach(bone => {
        if (!elementMap[kf.bone_id]["PositionX"])
            interpolate(frame, smoothFrame, bone.pos.X, bone.init_pos.X, z, z)
        if (!elementMap[kf.bone_id]["PositionY"])
            interpolate(frame, smoothFrame, bone.pos.Y, bone.init_pos.Y, z, z)
        if (!elementMap[kf.bone_id]["Rotation"])
            interpolate(frame, smoothFrame, bone.rot, bone.init_rot, z, z)
        if (!elementMap[kf.bone_id]["ScaleX"])
            interpolate(frame, smoothFrame, bone.scale.X, bone.init_scale.X, z, z)
        if (!elementMap[kf.bone_id]["ScaleY"])
            interpolate(frame, smoothFrame, bone.scale.Y, bone.init_scale.X, z, z)
        if (!elementMap[kf.bone_id]["Hidden"])
            bone.hidden = bone.init_hidden
    })   
}