@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
389 lines (309 loc) • 9.45 kB
JavaScript
import { assert } from "../../../core/assert.js";
import { binarySearchHighIndex } from "../../../core/collection/array/binarySearchHighIndex.js";
import { computeHashArray } from "../../../core/collection/array/computeHashArray.js";
import { isArrayEqual } from "../../../core/collection/array/isArrayEqual.js";
import { lerp } from "../../../core/math/lerp.js";
import { invokeObjectClone } from "../../../core/model/object/invokeObjectClone.js";
import { invokeObjectHash } from "../../../core/model/object/invokeObjectHash.js";
import { invokeObjectToJSON } from "../../../core/model/object/invokeObjectToJSON.js";
import { evaluate_two_key_curve } from "./evaluate_two_key_curve.js";
import { Keyframe } from "./Keyframe.js";
/**
*
* @param {number} time
* @param {Keyframe} keyframe
* @return {number}
*/
function compareKeyframeToTime(time, keyframe) {
return time - keyframe.time;
}
/**
* Describes change of a numeric value over time. Values are stored in {@link Keyframe}s, interpolation is defined by tangents on keyframes
* Inspired by unity's AnimationCurve, see https://docs.unity3d.com/ScriptReference/AnimationCurve.html
*/
export class AnimationCurve {
/**
* @readonly
* @type {Keyframe[]}
*/
keys = [];
/**
*
* @param {Keyframe} key
* @returns {number} key index where it was inserted at
*/
add(key) {
assert.defined(key, 'key');
assert.notNull(key, 'key');
assert.equal(key.isKeyframe, true, 'key.isKeyframe !== true');
const keys = this.keys;
const key_count = keys.length;
const last_key_index = key_count - 1;
if (
last_key_index < 0
|| keys[last_key_index].time <= key.time
) {
// inserted key goes at the end
keys.push(key);
return key_count;
} else {
// figure out the right place to insert the key
const i = binarySearchHighIndex(keys, key.time, compareKeyframeToTime, 0, last_key_index);
// insert key at the right place
keys.splice(i, 0, key);
return i;
}
}
/**
*
* @param {Keyframe[]} keys
*/
addMany(keys) {
assert.isArray(keys, 'keys');
const key_count = keys.length;
for (let i = 0; i < key_count; i++) {
this.add(keys[i]);
}
}
/**
*
* @param {Keyframe} key
* @returns {boolean}
*/
remove(key) {
const i = this.keys.indexOf(key);
if (i === -1) {
return false;
}
this.keys.splice(i, 1);
return true;
}
/**
* Remove all keys
*/
clear() {
this.keys.splice(0, this.keys.length);
}
/**
*
* @param {Keyframe[]} keys
* @returns {AnimationCurve}
*/
static from(keys) {
const curve = new AnimationCurve();
curve.addMany(keys);
return curve;
}
/**
* Number of keys
* @returns {number}
*/
get length() {
return this.keys.length;
}
/**
* Time of the first keyframe, returns 0 if there are no keys
* @returns {number}
*/
get start_time() {
const keys = this.keys;
if (keys.length === 0) {
return 0;
}
const first = keys[0];
return first.time;
}
/**
* Time of the last chronological key in the curve
* @return {number}
*/
get end_time() {
const keys = this.keys;
const key_count = keys.length;
if (key_count === 0) {
return 0
}
// keys are sorted, so we can rely on the last key's time
return keys[key_count - 1].time;
}
/**
* Time difference between first and last frame
* @returns {number}
*/
get duration() {
const keys = this.keys;
const key_count = keys.length;
if (key_count < 2) {
return 0;
}
const first = keys[0];
const last = keys[key_count - 1];
return last.time - first.time;
}
/**
*
* @param {number} time time in seconds
* @return {number}
*/
evaluate(time) {
assert.isNumber(time, 'time');
const keys = this.keys;
const key_count = keys.length;
if (key_count === 0) {
return 0;
}
if (key_count === 1) {
return keys[0].value;
}
if (time <= keys[0].time) {
// before start
return keys[0].value;
}
for (let i = 1; i < key_count; i++) {
const keyframe1 = keys[i];
if (time < keyframe1.time) {
const keyframe0 = keys[i - 1];
return evaluate_two_key_curve(time, keyframe0, keyframe1);
}
}
// past end
return keys[key_count - 1].value;
}
/**
* Set tangents of a key to match surrounding keys
* Produces a smoother looking curve
* @param {number} index index of keyframe
*/
alignTangents(index) {
const keys = this.keys;
const last_index = keys.length - 1;
assert.isNonNegativeInteger(index, "index");
assert.lessThanOrEqual(index, last_index, "index overflow");
const key_main = keys[index];
const has_previous = index > 0;
if (has_previous) {
const key_previous = keys[index - 1];
const time_span = key_main.time - key_previous.time;
const value_span = key_main.value - key_previous.value;
key_main.inTangent = value_span / time_span;
}
const has_next = index < last_index;
if (has_next) {
const key_next = keys[index + 1];
const time_span = key_next.time - key_main.time;
const value_span = key_next.value - key_main.value;
key_main.outTangent = value_span / time_span;
}
}
/**
*
* @param {number} index Index of keyframe to be affected
* @param {number} weight How much smoothing to apply, 1 will be fully smoothed out and 0 will have no effect at all. Value between 0 and 1
*/
smoothTangents(index, weight) {
const keys = this.keys;
const key = keys[index];
const average = lerp(key.inTangent, key.outTangent, 0.5);
key.inTangent = lerp(key.inTangent, average, weight);
key.outTangent = lerp(key.outTangent, average, weight);
}
smoothAllTangents() {
const n = this.length;
for (let i = 0; i < n; i++) {
this.smoothTangents(i, 1);
}
}
/**
*
* @param {AnimationCurve} other
* @return {boolean}
*/
equals(other) {
return isArrayEqual(this.keys, other.keys);
}
/**
* The copy is deep
* @param {AnimationCurve} other
*/
copy(other) {
this.keys = other.keys.map(invokeObjectClone);
}
/**
*
* @return {AnimationCurve}
*/
clone() {
const curve = new AnimationCurve();
curve.copy(this);
return curve;
}
/**
*
* @return {number}
*/
hash() {
return computeHashArray(this.keys, invokeObjectHash);
}
toJSON() {
return {
keys: this.keys.map(invokeObjectToJSON)
};
}
fromJSON({ keys = [] }) {
this.clear();
const key_count = keys.length;
for (let i = 0; i < key_count; i++) {
const keyframe = new Keyframe();
keyframe.fromJSON(keys[i]);
this.add(keyframe);
}
}
/**
*
* @param {number} [timeStart]
* @param {number} [valueStart]
* @param {number} [timeEnd]
* @param {number} [valueEnd]
* @return {AnimationCurve}
*/
static easeInOut(
timeStart = 0, valueStart = 0,
timeEnd = 1, valueEnd = 1
) {
return AnimationCurve.from([
Keyframe.from(timeStart, valueStart, 0, 0),
Keyframe.from(timeEnd, valueEnd, 0, 0)
]);
}
/**
*
* @param {number} [timeStart]
* @param {number} [timeEnd]
* @param {number} [value]
* @return {AnimationCurve}
*/
static constant(timeStart = 0, timeEnd = 1, value = 0) {
return AnimationCurve.from([
Keyframe.from(timeStart, value, 0, 0),
Keyframe.from(timeEnd, value, 0, 0)
]);
}
/**
*
* @param {number} [timeStart]
* @param {number} [valueStart]
* @param {number} [timeEnd]
* @param {number} [valueEnd]
* @return {AnimationCurve}
*/
static linear(
timeStart = 0, valueStart = 0,
timeEnd = 1, valueEnd = 1
) {
const tangent = valueEnd - valueStart;
return AnimationCurve.from([
Keyframe.from(timeStart, valueStart, 0, tangent),
Keyframe.from(timeEnd, valueEnd, tangent, 0)
]);
}
}