skeletal-animation-system
Version:
A standalone, stateless, dual quaternion based skeletal animation system built with interactive applications in mind
476 lines (432 loc) • 13.3 kB
JavaScript
var test = require('tape')
var animationSystem = require('../')
// Blend linearly over 2 seconds
function blendFunction (dt) {
return 0.5 * dt
}
// TODO: Thoroughly comment tests. Hard to understand without more context
test('Blend out previous animation', function (t) {
var currentKeyframes = {
'5.0': [
[]
],
'8.0': [
[]
]
}
var previousKeyframes = {
'0': [
[]
],
'5.0': [
[]
]
}
var options = {
// Our application clock has been running for 100.5 seconds
blendFunction: blendFunction,
currentTime: 100.5,
jointNums: [0],
currentAnimation: {
keyframes: currentKeyframes,
// Our new animation has been playing for 1.5 seconds
// This means that it is halfway done
// Making it's dual quaternion:
// [0.75, 0.75, 0.75, 0.75, 0.75, 0.75, 0.75, 0.75]
startTime: 99.0
},
previousAnimation: {
keyframes: previousKeyframes,
// Our previous animation started 2.5 seconds before our current time
// This means that it has (5.0 - 2.5) seconds remaining
// Making it's dual quaternion:
// [0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25]
startTime: 98.0
}
}
var interpolatedJoints = animationSystem.interpolateJoints(options).joints
t.deepEqual(
interpolatedJoints[0],
// Our new animation has been playing for 1.5 seconds
// This means that it should have 3/4th of the dual quaternion weight
// 3/4th of the way between 0.25 and 0.75 = 0.625
[],
'Uses default 2 second linear blend'
)
t.end()
})
test('Blending while passed previous animations upper keyframe', function (t) {
var currentKeyframes = {
'1.0': [
[]
],
'4.0': [
[]
]
}
var previousKeyframes = {
'0': [
[]
],
'1.0': [
[]
]
}
var options = {
blendFunction: blendFunction,
// Our application clock has been running for 100.5 seconds
currentTime: 100.5,
jointNums: [0],
currentAnimation: {
keyframes: currentKeyframes,
// Our new animation has been playing for 1.5 seconds
// This means that it is halfway done
// Making it's dual quaternion:
// [0.75, 0.75, 0.75, 0.75, 0.75, 0.75, 0.75, 0.75]
startTime: 99.0
},
previousAnimation: {
keyframes: previousKeyframes,
// Our previous animation started 1.5 seconds before our current time
// Meaning that it has passed it's final frame of 1.0. It should not loop
// if it's being blended. We will use it's final frame
// Making it's dual quaternion:
// [0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5]
startTime: 99.0
}
}
var interpolatedJoints = animationSystem.interpolateJoints(options).joints
t.deepEqual(
interpolatedJoints[0],
// Our new animation has been playing for 1.5 seconds
// This means that it should have 3/4th of the dual quaternion weight
// 3/4th of the way between 0.25 and 0.75 = 0.625
[],
`Uses the previous animations final keyframe when blending if
the previous animation has elapsed but there is still blend time remaining`
)
t.end()
})
test('Blend is above 100% complete', function (t) {
var currentKeyframes = {
'5.0': [
[]
],
'10.0': [
[]
]
}
var previousKeyframes = {
'0': [
[]
],
'5.0': [
[]
]
}
var options = {
blendFunction: blendFunction,
// Our application clock has been running for 100.5 seconds
currentTime: 101.5,
jointNums: [0],
currentAnimation: {
keyframes: currentKeyframes,
// Our new animation has been playing for 2.5 seconds
// This means that it is halfway done
// Making it's dual quaternion:
// [0.75, 0.75, 0.75, 0.75, 0.75, 0.75, 0.75, 0.75]
startTime: 99.0
},
previousAnimation: {
keyframes: previousKeyframes,
// Our previous animation started 2.5 seconds before our current time
// This means that it has (5.0 - 2.5) seconds remaining
// Making it's dual quaternion:
// [0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25]
startTime: 99.0
}
}
var interpolatedJoints = animationSystem.interpolateJoints(options).joints
t.deepEqual(
interpolatedJoints[0],
// Our new animation has been playing for 1.5 seconds
// This means that it should have 3/4th of the dual quaternion weight
// 3/4th of the way between 0.25 and 0.75 = 0.625
[],
'Ignores previous animation if blend above 100%'
)
t.end()
})
test('Blends using time since current animation frame set began', function (t) {
var currentKeyframes = {
'5.0': [
[]
],
'6.0': [
[]
],
'7.0': [
[]
]
}
var previousKeyframes = {
'1': [
[-0.5, -0.5, -0.5, -0.5, -0.5, -0.5, -0.5, -0.5]
],
'5.0': [
[]
]
}
var options = {
blendFunction: blendFunction,
// Our application clock has been running for 100.5 seconds
currentTime: 100.5,
jointNums: [0],
currentAnimation: {
keyframes: currentKeyframes,
// Our new animation has been playing for 1.5 seconds
// Making it's dual quaternion:
// [2, 2, 2, 2, 2, 2, 2, 2]
startTime: 99.0
},
previousAnimation: {
keyframes: previousKeyframes,
// Our previous animation started 2 seconds before our current time
// Making it's dual quaternion:
// [0, 0, 0, 0, 0, 0, 0, 0]
startTime: 98.5
}
}
var interpolatedJoints = animationSystem.interpolateJoints(options).joints
t.deepEqual(
interpolatedJoints[0],
// This is 1.625 because we negate one of the dual quats to ensure shortest path interpolation
[],
'Blends using time since current animation frame set first started'
)
t.end()
})
test('Previous animation in middle of loop', function (t) {
var currentKeyframes = {
'2.0': [
[]
],
'3.0': [
[]
]
}
var previousKeyframes = {
'0': [
[]
],
'2.0': [
[]
]
}
var options = {
blendFunction: blendFunction,
// Our application clock has been running for 100.5 seconds
currentTime: 101,
jointNums: [0],
currentAnimation: {
keyframes: currentKeyframes,
// TODO: Change all "frames" to "seconds". We're dealing with seconds
// as our keyframe key
// Our new animation has been playing for 0.5 seconds
// Making it's dual quaternion:
// [3, 3, 3, 3, 3, 3, 3, 3]
startTime: 100.5
},
previousAnimation: {
keyframes: previousKeyframes,
// Our previous animation has been playing for 3 seconds
// Making it's dual quaternion:
// [1, 1, 1, 1, 1, 1, 1, 1]
startTime: 98.0
}
}
var interpolatedJoints = animationSystem.interpolateJoints(options).joints
t.deepEqual(
interpolatedJoints[0],
[],
'Supports looping when previous animation started before current'
)
t.end()
})
test('Previous animation started in middle of loop but now passed final frame', function (t) {
var currentKeyframes = {
'1.0': [
[]
],
'3.0': [
[]
]
}
var previousKeyframes = {
'0': [
[]
],
'1.0': [
[]
]
}
var options = {
blendFunction: blendFunction,
// Our application clock has been running for 100.5 seconds
currentTime: 102.5,
jointNums: [0],
currentAnimation: {
keyframes: currentKeyframes,
// TODO: Change all of the "seconds" to frames
// Our new animation has been playing for 1.0 seconds
// Making it's dual quaternion:
// [3, 3, 3, 3, 3, 3, 3, 3]
startTime: 101.5
},
previousAnimation: {
keyframes: previousKeyframes,
// Our previous animation has been playing for 2.5 frames,
// but only 1 frame since the new animation started
// This means that it has hit it's final frame and should
// no longer loop.
startTime: 100
}
}
var interpolatedJoints = animationSystem.interpolateJoints(options).joints
t.deepEqual(
interpolatedJoints[0],
[],
'Supports looping when previous animation started before current'
)
t.end()
})
// Test that previous animation elapsed time is properly calculated against
// the lowest keyframe
test('Previous animation elapsed time when previous animation starts from non first keyframe in set', function (t) {
var currentKeyframes = {
'6.0': [
[]
],
'9.0': [
[]
]
}
var previousKeyframes = {
'0': [
[]
],
'1': [
[]
],
'6.0': [
[]
]
}
var options = {
blendFunction: blendFunction,
// Our application clock has been running for 100.5 seconds
currentTime: 100.5,
jointNums: [0],
currentAnimation: {
keyframes: currentKeyframes,
// Our new animation has been playing for 1.5 seconds
// This means that it is halfway done
// Making it's dual quaternion:
// [0.75, 0.75, 0.75, 0.75, 0.75, 0.75, 0.75, 0.75]
startTime: 99.0
},
previousAnimation: {
keyframes: previousKeyframes,
// Our previous animation started 3.5 seconds before our current time
// Making it's dual quaternion:
// [0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25]
startTime: 97.0
}
}
var interpolatedJoints = animationSystem.interpolateJoints(options).joints
t.deepEqual(
interpolatedJoints[0],
// Our new animation has been playing for 1.5 seconds
// This means that it should have 3/4th of the dual quaternion weight
// 3/4th of the way between 0.25 and 0.75 = 0.625
[],
'Uses default 2 second linear blend'
)
t.end()
})
// If there are multiple keyframes above our previous animation's
// current keyframe it should be sure to chose the correct one
test('Multiple keyframes larger than the current one', function (t) {
var currentKeyframes = {
'10.0': [
[]
],
'13.0': [
[]
]
}
var previousKeyframes = {
'0': [
[]
],
'5.0': [
[]
],
'8.0': [
[]
]
}
var options = {
blendFunction: blendFunction,
// Our application clock has been running for 100.5 seconds
currentTime: 100.5,
jointNums: [0],
currentAnimation: {
keyframes: currentKeyframes,
// Our new animation has been playing for 1.5 seconds
// This means that it is halfway done
// Making it's dual quaternion:
// [0.75, 0.75, 0.75, 0.75, 0.75, 0.75, 0.75, 0.75]
startTime: 99.0
},
previousAnimation: {
keyframes: previousKeyframes,
// Our previous animation started 2.5 seconds before our current time
// This means that it has (5.0 - 2.5) seconds remaining
// Making it's dual quaternion:
// [0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25]
startTime: 98.0
}
}
var interpolatedJoints = animationSystem.interpolateJoints(options).joints
t.deepEqual(
interpolatedJoints[0],
// Our new animation has been playing for 1.5 seconds
// This means that it should have 3/4th of the dual quaternion weight
// 3/4th of the way between 0.25 and 0.75 = 0.625
[],
'Uses default 2 second linear blend'
)
t.end()
})
test('single frame animations', function (t) {
var options = {
currentTime: 0.016666666666666666,
jointNums: [
0
],
currentAnimation: {
keyframes: {
'0.0': [
[-7.278466024329688e-14, 2.0600566680306368e-10, -5.126635227448162e-12, 1, 5.187854313327257e-18, -8.827153992227743e-19, 2.7275390652703847e-16, 1.580531756253426e-27]
]
},
startTime: 0,
noLoop: false
}
}
t.doesNotThrow(function () {
animationSystem.interpolateJoints(options)
}, 'interpolateJoints should not throw')
t.end()
})