UNPKG

@pixi/animate

Version:

PIXI plugin for the PixiAnimate Extension

643 lines (640 loc) 19.9 kB
import { Timeline } from './Timeline.mjs'; import { getEaseFromConfig } from './Tween.mjs'; import { utils } from './utils.mjs'; import { sound } from './sound.mjs'; import { AnimateContainer } from './Container.mjs'; import { Ticker } from '@pixi/ticker'; import { settings } from '@pixi/settings'; const SharedTicker = Ticker.shared; const _MovieClip = class extends AnimateContainer { constructor(options, duration, loop, framerate, labels) { super(); /** * Fast way of checking if a movie clip is actually a movie clip. * Prevents circular references and is faster than instanceof. */ this.isMovieClip = true; /** * Shortcut alias for `addTimedMask` */ this.am = this.addTimedMask; /** * Shortcut alias for `addTween` */ this.tw = this.addTween; /** * Alias for method `addTimedChild` */ this.at = this.addTimedChild; /** * Short cut for `addAction` */ this.aa = this.addAction; /** * Short cut for `playSound` */ this.ps = this.playSound; options = options === void 0 ? {} : options; if (typeof options === "number") { options = { mode: options || _MovieClip.INDEPENDENT, duration: duration || 0, loop: loop === void 0 ? true : loop, labels: labels || {}, framerate: framerate || 0, startPosition: 0 }; } else { options = Object.assign({ mode: _MovieClip.INDEPENDENT, startPosition: 0, loop: true, labels: {}, duration: 0, framerate: 0 }, options); } this.mode = options.mode; this.startPosition = options.startPosition; this.loop = !!options.loop; this.currentFrame = 0; this._labels = []; this._labelDict = options.labels; if (options.labels) { for (const name in options.labels) { const label = { label: name, position: options.labels[name] }; this._labels.push(label); } this._labels.sort((a, b) => a.position - b.position); } this.selfAdvance = true; this.paused = false; this.actionsEnabled = true; this.autoReset = true; this._synchOffset = 0; this._prevPos = -1; this._t = 0; this._framerate = options.framerate; this._duration = 0; this._totalFrames = options.duration; this._timelines = []; this._timedChildTimelines = []; this._depthSorted = []; this._actions = []; this._beforeUpdate = null; this.parentStartPosition = 0; if (this.mode === _MovieClip.INDEPENDENT) { this._tickListener = this._tickListener.bind(this); this._onAdded = this._onAdded.bind(this); this._onRemoved = this._onRemoved.bind(this); this.on("added", this._onAdded); this.on("removed", this._onRemoved); } if (options.framerate) { this.framerate = options.framerate; } this.advance = this.advance; this._updateTimeline = this._updateTimeline; this._setTimelinePosition = this._setTimelinePosition; this._goto = this._goto; } _onAdded() { if (!this._framerate) { this.framerate = this.parentFramerate; } SharedTicker.add(this._tickListener, null); } _tickListener(tickerDeltaTime) { if (this.paused || !this.selfAdvance) { if (this._prevPos < 0) { this._goto(this.currentFrame); } return; } const seconds = tickerDeltaTime / settings.TARGET_FPMS / 1e3; this.advance(seconds); } _onRemoved() { SharedTicker.remove(this._tickListener, null); } /** * Returns an array of objects with label and position (aka frame) properties, sorted by position. */ get labels() { return this._labels; } /** * Returns a dictionary of labels where key is the label and value is the frame. */ get labelsMap() { return this._labelDict; } /** * Returns the name of the label on or immediately before the current frame. */ get currentLabel() { const labels = this._labels; let current = null; for (let i = 0, len = labels.length; i < len; ++i) { if (labels[i].position <= this.currentFrame) { current = labels[i].label; } else { break; } } return current; } /** * When the MovieClip is framerate independent, this is the time elapsed from frame 0 in seconds. */ get elapsedTime() { return this._t; } set elapsedTime(value) { this._t = value; } /** * By default MovieClip instances advance one frame per tick. Specifying a framerate for the * MovieClip will cause it to advance based on elapsed time between ticks as appropriate to * maintain the target framerate. * * For example, if a MovieClip with a framerate of 10 is placed on a Stage being updated at * 40fps, then the MovieClip advance roughly one frame every 4 ticks. This will not be exact, * because the time between each tick vary slightly between frames. * * This feature is dependent on the tick event object (or an object with an appropriate 'delta' property) being * passed into {{#crossLink 'Stage/update'}}{{/crossLink}}. */ get framerate() { return this._framerate; } set framerate(value) { if (value > 0) { if (this._framerate) { this._t *= this._framerate / value; } else { this._t = this.currentFrame / value; } this._framerate = value; this._duration = value ? this._totalFrames / value : 0; } else { this._t = this._framerate = this._duration = 0; } } /** * Get the total number of frames (duration) of this MovieClip */ get totalFrames() { return this._totalFrames; } /** * Extend the timeline to the last frame. */ _autoExtend(endFrame) { if (this._totalFrames < endFrame) { this._totalFrames = endFrame; } } /** * Convert values of properties */ _parseProperties(properties) { if (typeof properties.t === "string") { properties.t = utils.hexToUint(properties.t); } else if (typeof properties.v === "number") { properties.v = !!properties.v; } } /** * Get a timeline for a child, synced timeline. */ _getChildTimeline(instance) { for (let i = this._timelines.length - 1; i >= 0; --i) { if (this._timelines[i].target === instance) { return this._timelines[i]; } } const timeline = Timeline.create(instance); this._timelines.push(timeline); return timeline; } /** * Add mask or masks */ addTimedMask(instance, keyframes) { for (const i in keyframes) { this.addKeyframe(instance, { m: keyframes[i] }, parseInt(i, 10)); } this._setTimelinePosition(this.currentFrame, this.currentFrame, true); return this; } /** * Add a tween to the clip * @param instance - The clip to tween * @param properties - The property or property to tween * @param startFrame - The frame to start tweening * @param duration - Number of frames to tween. If 0, then the properties are set with no tweening. * @param ease - An optional easing function that takes the tween time from 0-1. */ addTween(instance, properties, startFrame, duration, ease) { const timeline = this._getChildTimeline(instance); this._parseProperties(properties); timeline.addTween(properties, startFrame, duration, ease); this._autoExtend(startFrame + duration); return this; } /** * Add a tween to the clip * @param instance - The clip to tween * @param properties - The property or property to tween * @param startFrame - The frame to start tweening */ addKeyframe(instance, properties, startFrame) { const timeline = this._getChildTimeline(instance); const { tw } = properties; delete properties.tw; this._parseProperties(properties); timeline.addKeyframe(properties, startFrame); this._autoExtend(startFrame); if (tw) { this.addTween(instance, tw.p, startFrame, tw.d, getEaseFromConfig(tw.e)); } return this; } /** * Add a child to show for a certain number of frames before automatic removal. * @param instance - The clip to show * @param startFrame - The starting frame * @param duration - The number of frames to display the child before removing it. * @param keyframes - The collection of static keyframes to add */ addTimedChild(instance, startFrame, duration, keyframes) { if (startFrame === void 0) { startFrame = 0; } if (duration === void 0 || duration < 1) { duration = this._totalFrames || 1; } if (instance instanceof _MovieClip && instance.mode === _MovieClip.SYNCHED) { instance.parentStartPosition = startFrame; } let timeline; for (let i = this._timedChildTimelines.length - 1; i >= 0; --i) { if (this._timedChildTimelines[i].target === instance) { timeline = this._timedChildTimelines[i]; break; } } if (!timeline) { timeline = []; timeline.target = instance; this._timedChildTimelines.push(timeline); } utils.fillFrames(timeline, startFrame, duration); if (this._totalFrames < startFrame + duration) { this._totalFrames = startFrame + duration; } if (keyframes) { if (typeof keyframes === "string") { keyframes = utils.deserializeKeyframes(keyframes); } let sequenceUsesSkew = false; for (const i in keyframes) { if (keyframes[i].kx || keyframes[i].ky) { sequenceUsesSkew = true; break; } } if (sequenceUsesSkew) { for (const i in keyframes) { if (keyframes[i].r !== void 0) { keyframes[i].kx = keyframes[i].kx || keyframes[i].r * -1; keyframes[i].ky = keyframes[i].ky || keyframes[i].r; delete keyframes[i].r; } if (keyframes[i].tw?.p?.r !== void 0) { keyframes[i].tw.p.kx = keyframes[i].tw.p.kx || keyframes[i].tw.p.r * -1; keyframes[i].tw.p.ky = keyframes[i].tw.p.ky || keyframes[i].tw.p.r; delete keyframes[i].tw.p.r; } } } for (const i in keyframes) { this.addKeyframe(instance, keyframes[i], parseInt(i, 10)); } this._getChildTimeline(instance).extendLastFrame(startFrame + duration - 1); } this._setTimelinePosition(startFrame, this.currentFrame, true); return this; } /** * Handle frame actions, callback is bound to the instance of the MovieClip. * @param callback - The clip call on a certain frame * @param startFrame - The starting frame index or label */ addAction(callback, startFrame) { if (typeof startFrame === "string") { const index = this._labelDict[startFrame]; if (index === void 0) { throw new Error(`The label '${startFrame}' does not exist on this timeline`); } startFrame = index; } const actions = this._actions; if (actions.length <= startFrame) { actions.length = startFrame + 1; } if (this._totalFrames < startFrame) { this._totalFrames = startFrame; } if (actions[startFrame]) { actions[startFrame].push(callback); } else { actions[startFrame] = [callback]; } return this; } /** * Handle sounds. * @param alias - The name of the Sound * @param loop - The loop property of the sound */ playSound(alias, loop) { sound.emit("play", alias, !!loop, this); return this; } /** * Sets paused to false. */ play() { this.paused = false; } /** * Sets paused to true. */ stop() { this.paused = true; } /** * Advances this movie clip to the specified position or label and sets paused to false. * @param positionOrLabel - The animation name or frame number to go to. */ gotoAndPlay(positionOrLabel) { this.paused = false; this._goto(positionOrLabel); } /** * Advances this movie clip to the specified position or label and sets paused to true. * @param positionOrLabel - The animation or frame name to go to. */ gotoAndStop(positionOrLabel) { this.paused = true; this._goto(positionOrLabel); } /** * Get the close parent with a valid framerate. If no parent, returns the default framerate. */ get parentFramerate() { let o = this; let fps = o._framerate; while ((o = o.parent) && !fps) { if (o.mode === _MovieClip.INDEPENDENT) { fps = o._framerate; } } return fps || _MovieClip.DEFAULT_FRAMERATE; } /** * Advances the playhead. This occurs automatically each tick by default. * @param time - The amount of time in seconds to advance by. Only applicable if framerate is set. */ advance(time) { if (!this._framerate) { this.framerate = this.parentFramerate; } if (time) { this._t += time; } if (this._t > this._duration) { this._t = this.loop ? this._t % this._duration : this._duration; } this.currentFrame = Math.floor(this._t * this._framerate + 1e-8); if (this.currentFrame >= this._totalFrames) { this.currentFrame = this._totalFrames - 1; } let afterUpdateOnce; if (this._beforeUpdate) { afterUpdateOnce = this._beforeUpdate(this); } this._updateTimeline(); if (afterUpdateOnce) { afterUpdateOnce(); } } /** * @param positionOrLabel - The animation name or frame number to go to. */ _goto(positionOrLabel) { const pos = typeof positionOrLabel === "string" ? this._labelDict[positionOrLabel] : positionOrLabel; if (pos === void 0) { return; } this._prevPos = NaN; this.currentFrame = pos; if (!this._framerate) { this.framerate = this.parentFramerate; } if (this._framerate > 0) { this._t = pos / this._framerate; } else { this._t = 0; } this._updateTimeline(); } /** * Reset the movieclip to the first frame (without advancing the timeline). */ _reset() { this._prevPos = -1; this._t = 0; this.currentFrame = 0; } /** * Update timeline position according to playback, performing actions and updating children. * @private */ _updateTimeline() { const synched = this.mode !== _MovieClip.INDEPENDENT; if (synched) { this.currentFrame = this.startPosition + (this.mode === _MovieClip.SINGLE_FRAME ? 0 : this._synchOffset); if (this.currentFrame >= this._totalFrames) { this.currentFrame %= this._totalFrames; } } if (this._prevPos === this.currentFrame) { return; } this._setTimelinePosition(this._prevPos, this.currentFrame, synched ? false : this.actionsEnabled); this._prevPos = this.currentFrame; } /** * Set the timeline position */ _setTimelinePosition(startFrame, currentFrame, doActions) { if (startFrame !== currentFrame && doActions) { let startPos; if (isNaN(startFrame)) { startPos = currentFrame; } else { startPos = startFrame >= this._totalFrames - 1 ? 0 : startFrame + 1; } const actionFrames = []; if (currentFrame < startPos) { for (let i = startPos; i < this._actions.length; ++i) { if (this._actions[i]) { actionFrames.push(i); } } for (let i = 0; i <= currentFrame; ++i) { if (this._actions[i]) { actionFrames.push(i); } } } else { for (let i = startPos; i <= currentFrame; ++i) { if (this._actions[i]) { actionFrames.push(i); } } } if (actionFrames.length) { const oldCurrentFrame = this.currentFrame; for (let i = 0; i < actionFrames.length; ++i) { const frame = actionFrames[i]; this._setTimelinePosition(frame, frame, true); if (this.currentFrame !== oldCurrentFrame || frame === currentFrame) { return; } else if (this.paused) { this.currentFrame = frame; return; } } } } const _timelines = this._timelines; for (let i = _timelines.length - 1; i >= 0; --i) { const timeline = _timelines[i]; for (let j = 0, length = timeline.length; j < length; ++j) { const tween = timeline[j]; if (currentFrame >= tween.startFrame && currentFrame <= tween.endFrame) { tween.setPosition(currentFrame); break; } } } const timedChildTimelines = this._timedChildTimelines; const depthSorted = this._depthSorted; for (let i = 0, length = timedChildTimelines.length; i < length; ++i) { const target = timedChildTimelines[i].target; const shouldBeChild = timedChildTimelines[i][currentFrame]; if (shouldBeChild) { depthSorted.push(target); if (target.parent !== this) { this.addChild(target); if (target instanceof _MovieClip && target.mode === _MovieClip.INDEPENDENT && target.autoReset) { target._reset(); } } } else if (!shouldBeChild && target.parent === this) { this.removeChild(target); } } for (let i = 0, length = depthSorted.length; i < length; i++) { const target = depthSorted[i]; const currentIndex = this.children.indexOf(target); if (currentIndex !== i) { this.addChildAt(target, i); } } depthSorted.length = 0; const children = this.children; for (let i = 0, length = children.length; i < length; ++i) { const child = children[i]; if (child instanceof _MovieClip && child.mode === _MovieClip.SYNCHED) { child._synchOffset = currentFrame - child.parentStartPosition; child._updateTimeline(); } } if (doActions && this._actions && this._actions[currentFrame]) { const frameActions = this._actions[currentFrame]; for (let j = 0; j < frameActions.length; ++j) { frameActions[j].call(this); } } } destroy(options) { if (this._tickListener) { SharedTicker.remove(this._tickListener, null); this._tickListener = null; } const hiddenChildren = []; const timelines = this._timelines; if (timelines) { for (let i = 0; i < timelines.length; i++) { const timeline = timelines[i]; hiddenChildren.push(timeline.target); timeline.destroy(); } } const childTimelines = this._timedChildTimelines; if (childTimelines) { for (let i = 0; i < childTimelines.length; i++) { const timeline = childTimelines[i]; if (hiddenChildren.indexOf(timeline.target) < 0) { hiddenChildren.push(timeline.target); } timeline.length = 0; } } for (let i = 0; i < hiddenChildren.length; i++) { if (this.children.indexOf(hiddenChildren[i]) < 0) { hiddenChildren[i].destroy(options); } } hiddenChildren.length = 0; this._actions = null; this._timelines = null; this._depthSorted = null; this._timedChildTimelines = null; this._beforeUpdate = null; this._labels = null; this._labelDict = null; super.destroy(options); } }; let MovieClip = _MovieClip; /** * The MovieClip will advance independently of its parent, even if its parent is paused. * This is the default mode. */ MovieClip.INDEPENDENT = 0; /** * The MovieClip will only display a single frame (as determined by the startPosition property). */ MovieClip.SINGLE_FRAME = 1; /** * The MovieClip will be advanced only when its parent advances and will be synched to the position of * the parent MovieClip. */ MovieClip.SYNCHED = 2; /** * The default framerate if none is specified or there's not parent clip with a framerate. */ MovieClip.DEFAULT_FRAMERATE = 24; export { MovieClip }; //# sourceMappingURL=MovieClip.mjs.map