UNPKG

babylon-mmd

Version:
231 lines (230 loc) 7.2 kB
import { Observable } from "@babylonjs/core/Misc/observable"; /** * Represents a slice of an MMD animation */ export class MmdAnimationSpan { /** * The animation that this span uses */ animation; /** * Start frame of the span (default: animation.startFrame) * * You can slice the desired range by the difference from the starting point of the original animation */ startFrame; /** * End frame of the span (default: animation.endFrame) * * You can slice the desired range by the difference from the starting point of the original animation */ endFrame; /** * Offset of the span (default: 0) * * Determines at what point in the `MmdCompositeAnimation` the span will be played */ offset; /** * Animation weight (default: 1) * * Internally, it is normalized and treated as a value between 0 and 1 on evaluation */ weight; /** * easing function of the span (default: null) */ easingFunction; /** * Ease in frame time of the span (default: 0) */ easeInFrameTime; /** * Ease out frame time of the span (default: 0) */ easeOutFrameTime; /** * Create a new span * @param animation Bindable animation, which typically means `MmdAnimation` * @param startFrame Start frame of the span (default: animation.startFrame) * @param endFrame End frame of the span (default: animation.endFrame) * @param offset Offset of the span (default: 0) * @param weight Animation weight (default: 1) */ constructor(animation, startFrame, endFrame, offset, weight) { this.animation = animation; this.startFrame = startFrame ?? animation.startFrame; this.endFrame = endFrame ?? animation.endFrame; this.offset = offset ?? 0; this.weight = weight ?? 1; this.easingFunction = null; this.easeInFrameTime = 0; this.easeOutFrameTime = 0; } /** * Name of the animation */ get name() { return this.animation.name; } /** * Whether the frame time is in the span * @param frameTime frame time in composite animation * @returns Returns true if the frame time is in the span */ isInSpan(frameTime) { return this.startFrame + this.offset <= frameTime && frameTime <= this.endFrame + this.offset; } /** * Get the frame time in this span * @param frameTime frame time in composite animation * @returns Returns the frame time in this span */ getFrameTime(frameTime) { return frameTime - this.offset; } /** * Get the eased weight of this span * @param frameTime frame time in this span * @returns Returns the eased weight of this span */ getEasedWeight(frameTime) { const startFrame = this.startFrame; const endFrame = this.endFrame; const easeInFrameTime = this.easeInFrameTime; const easeOutFrameTime = this.easeOutFrameTime; if (Math.abs(frameTime - startFrame) < Math.abs(frameTime - endFrame)) { if (startFrame + easeInFrameTime <= frameTime) { return this.weight; } else if (frameTime <= startFrame) { return 0; } else { // ease in const frameTimeInEaseIn = frameTime - startFrame; const weight = this.weight * (this.easingFunction?.ease(frameTimeInEaseIn / easeInFrameTime) ?? frameTimeInEaseIn / easeInFrameTime); return weight; } } else { if (frameTime <= endFrame - easeOutFrameTime) { return this.weight; } else if (endFrame <= frameTime) { return 0; } else { // ease out const frameTimeInEaseOut = frameTime - endFrame + easeOutFrameTime; const weight = this.weight * (1 - (this.easingFunction?.ease(frameTimeInEaseOut / easeOutFrameTime) ?? frameTimeInEaseOut / easeOutFrameTime)); return weight; } } } } /** * Combine multiple animations into a single animation * * Good for uses like QTE sequences where animation blending is determined at runtime */ export class MmdCompositeAnimation { /** * Observable that is triggered when a span is added */ onSpanAddedObservable; /** * Observable that is triggered when a span is removed */ onSpanRemovedObservable; /** * Animation name for identification */ name; _startFrame; _endFrame; _spans; /** * Create a new composite animation * @param name Animation name */ constructor(name) { this.onSpanAddedObservable = new Observable(); this.onSpanRemovedObservable = new Observable(); this.name = name; this._startFrame = 0; this._endFrame = 0; this._spans = []; } /** * Add a span to the animation * * animation will be pushed to the end of the `spans` array * @param span Span to add */ addSpan(span) { this._startFrame = this._spans.length === 0 ? span.startFrame + span.offset : Math.min(this.startFrame, span.startFrame + span.offset); this._endFrame = Math.max(this.endFrame, span.endFrame + span.offset); this._spans.push(span); this.onSpanAddedObservable.notifyObservers(span); } /** * Remove a span from the animation * * If the span does not exist, do nothing * @param span Span to remove */ removeSpan(span) { const spans = this._spans; const index = spans.indexOf(span); this.removeSpanFromIndex(index); } /** * Remove a span from the animation * * If index is out of range, do nothing * @param index Index of the span to remove */ removeSpanFromIndex(index) { const spans = this._spans; if (index < 0 || spans.length <= index) return; spans.splice(index, 1); if (spans.length === 0) { this._startFrame = 0; this._endFrame = 0; } else { let startFrame = spans[0].startFrame + spans[0].offset; let endFrame = spans[0].endFrame + spans[0].offset; for (let i = 1; i < spans.length; i++) { const span = spans[i]; startFrame = Math.min(startFrame, span.startFrame + span.offset); endFrame = Math.max(endFrame, span.endFrame + span.offset); } this._startFrame = startFrame; this._endFrame = endFrame; } this.onSpanRemovedObservable.notifyObservers(index); } /** * The start frame of this animation */ get startFrame() { return this._startFrame; } /** * The end frame of this animation */ get endFrame() { return this._endFrame; } /** * The spans of this animation */ get spans() { return this._spans; } }