UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

389 lines (309 loc) • 9.45 kB
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) ]); } }