import { serializable } from "../engine/engine_serialization_decorator.js"; /** * Keyframe is a representation of a keyframe in an AnimationCurve. */ export class Keyframe { @serializable() time: number = 0; @serializable() value: number = 0; @serializable() inTangent: number = Infinity; @serializable() inWeight?: number; @serializable() outTangent: number = Infinity; @serializable() outWeight?: number; @serializable() weightedMode?: number; constructor(time: number = 0, value: number = 0) { this.time = time; this.value = value; } } /** * AnimationCurve is a representation of a curve that can be used to animate values over time. */ export class AnimationCurve { /** * Creates an animation curve that goes from the `from` value to the `to` value over the given `duration`. */ static linearFromTo(from: number, to: number, duration: number): AnimationCurve { const curve = new AnimationCurve(); const keyframe1 = new Keyframe(); keyframe1.time = 0; keyframe1.value = from; const keyframe2 = new Keyframe(); keyframe2.time = duration; keyframe2.value = to; curve.keys.push(keyframe1, keyframe2); return curve; } /** Creates an animation curve with just one keyframe */ static constant(value: number): AnimationCurve { const curve = new AnimationCurve(); const keyframe = new Keyframe(); keyframe.time = 0; keyframe.value = value; curve.keys.push(keyframe); return curve; } /** * The keyframes that define the curve. */ @serializable(Keyframe) keys: Array = []; /** * Clones this AnimationCurve and returns a new instance with the same keyframes (the keyframes are also cloned). */ clone() { const curve = new AnimationCurve(); curve.keys = this.keys?.map(k => { const key = new Keyframe(); key.time = k.time; key.value = k.value; key.inTangent = k.inTangent; key.inWeight = k.inWeight; key.outTangent = k.outTangent; key.outWeight = k.outWeight; key.weightedMode = k.weightedMode; return key; }) || []; return curve; } /** The duration of the curve, which is the time of the last keyframe. */ get duration(): number { if (!this.keys || this.keys.length == 0) return 0; return this.keys[this.keys.length - 1].time; } /** Evaluates the curve at the given time and returns the value of the curve at that time. * @param time The time at which to evaluate the curve. * @returns The value of the curve at the given time. */ evaluate(time: number): number { if (!this.keys || this.keys.length == 0) return 0; if (this.keys.length === 1) { return this.keys[0].value; } // if the first keyframe time is already greater than the time we want to evaluate // then we dont need to iterate if (this.keys[0].time >= time) { return this.keys[0].value; } for (let i = 0; i < this.keys.length; i++) { const kf = this.keys[i]; if (kf.time <= time) { const hasNextKeyframe = i + 1 < this.keys.length; if (hasNextKeyframe) { const nextKf = this.keys[i + 1]; // if the next if (nextKf.time < time) continue; // tangents are set to Infinity if interpolation is set to constant - in that case we should always return the floored value if (!isFinite(kf.outTangent) || !isFinite(nextKf.inTangent)) return kf.value; return AnimationCurve.interpolateValue(time, kf, nextKf); } else { return kf.value; } } } return this.keys[this.keys.length - 1].value; } static interpolateValue(time: number, keyframe1: Keyframe, keyframe2: Keyframe): number { const startTime1 = keyframe1.time; const startValue1 = keyframe1.value; const outTangent1 = keyframe1.outTangent; const startTime2 = keyframe2.time; const startValue2 = keyframe2.value; const inTangent2 = keyframe2.inTangent; // could be precomputed and stored in the keyframes maybe const timeDifference = startTime2 - startTime1; const timeDifferenceSquared = timeDifference * timeDifference; const timeDifferenceCubed = timeDifferenceSquared * timeDifference; const a = ((outTangent1 + inTangent2) * timeDifference - 2 * (startValue2 - startValue1)) / timeDifferenceCubed; const b = (3 * (startValue2 - startValue1) - (inTangent2 + 2 * outTangent1) * timeDifference) / timeDifferenceSquared; const c = outTangent1; const d = startValue1; const timeDelta = time - startTime1; const timeDeltaSquared = timeDelta * timeDelta; const timeDeltaCubed = timeDeltaSquared * timeDelta; return a * timeDeltaCubed + b * timeDeltaSquared + c * timeDelta + d; } }