UNPKG

@createjs/easeljs

Version:

The Easel JavaScript library provides a full, hierarchical display list, a core interaction model, and helper classes to make working with the HTML5 Canvas element much easier. Part of the CreateJS suite of libraries.

588 lines (529 loc) 17.8 kB
/** * @license MovieClip * Visit http://createjs.com/ for documentation, updates and examples. * * Copyright (c) 2017 gskinner.com, inc. * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following * conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. */ import Container from "./Container"; import DisplayObject from "./DisplayObject"; import { Tween, Timeline } from "@createjs/tweenjs"; /** * The MovieClip class associates a TweenJS Timeline with an EaselJS {@link easeljs.Container}. It allows * you to create objects which encapsulate timeline animations, state changes, and synched actions. Due to the * complexities inherent in correctly setting up a MovieClip, it is largely intended for tool output and is not included * in the main EaselJS library. * * Currently MovieClip only works properly if it is tick based (as opposed to time based) though some concessions have * been made to support time-based timelines in the future. * * It is recommended to use `tween.to()` to animate and set properties (use no duration to have it set * immediately), and the `tween.wait()` method to create delays between animations. Note that using the * `tween.set()` method to affect properties will likely not provide the desired result. * * @memberof easeljs * @example <caption>Animate two shapes back and forth</caption> * let stage = new Stage("canvas"); * Ticker.addEventListener("tick", stage); * * let mc = new MovieClip(null, 0, true, {start:20}); * stage.addChild(mc); * * let child1 = new Shape( * new Graphics().beginFill("#999999").drawCircle(30,30,30) * ); * let child2 = new Shape( * new Graphics().beginFill("#5a9cfb").drawCircle(30,30,30) * ); * * mc.timeline.addTween( * Tween.get(child1).to({x:0}).to({x:60}, 50).to({x:0}, 50) * ); * mc.timeline.addTween( * Tween.get(child2).to({x:60}).to({x:0}, 50).to({x:60}, 50) * ); * * mc.gotoAndPlay("start"); * * @extends easeljs.Container * @param {Object} [props] The configuration properties to apply to this instances. * This object will also be passed into the Timeline instance associated with this MovieClip. * See the documentation for Timeline for a list of supported props. */ export default class MovieClip extends Container { constructor (props) { super(); !MovieClip.inited && MovieClip.init(); /** * Controls how this MovieClip advances its time. Must be one of 0 (INDEPENDENT), 1 (SINGLE_FRAME), or 2 (SYNCHED). * See each constant for a description of the behaviour. * @type {Number} * @default 0 */ this.mode = props.mode != null ? props.mode : MovieClip.INDEPENDENT; /** * Specifies what the first frame to play in this movieclip, or the only frame to display if mode is SINGLE_FRAME. * @type {Number} * @default 0 */ this.startPosition = props.startPosition != null ? props.startPosition : 0; /** * Specifies how many times this MovieClip should loop. A value of -1 indicates it should loop indefinitely. A value of * 1 would cause it to loop once (ie. play a total of twice). * @property loop * @type {Number} * @default -1 */ if (typeof props.loop === "number") { this.loop = props.loop; } else if (props.loop === false) { this.loop = 0; } else { this.loop = -1; } /** * The current frame of the movieclip. * @type Number * @default 0 * @readonly */ this.currentFrame = 0; /** * The TweenJS Timeline that is associated with this MovieClip. This is created automatically when the MovieClip * instance is initialized. Animations are created by adding <a href="http://tweenjs.com">TweenJS</a> Tween * instances to the timeline. * * Elements can be added and removed from the timeline by toggling an "_off" property * using the `tweenInstance.to()` method. Note that using `Tween.set` is not recommended to * create MovieClip animations. The following example will toggle the target off on frame 0, and then back on for * frame 1. You can use the "visible" property to achieve the same effect. * * @example * let tween = Tween.get(target).to({x:0}).to({x:100}, 30); * let mc = new MovieClip(); * mc.timeline.addTween(tween); * * @example * Tween.get(target).to({_off:false}) * .wait(1).to({_off:true}) * .wait(1).to({_off:false}); * * @type {easeljs.Timeline} */ this.timeline = new Timeline(Object.assign({ useTicks: true, paused: true }, props)); /** * If true, the MovieClip's position will not advance when ticked. * @type {Boolean} * @default false */ this.paused = props.paused != null ? props.paused : false; /** * If true, actions in this MovieClip's tweens will be run when the playhead advances. * @type {Boolean} * @default true */ this.actionsEnabled = true; /** * If true, the MovieClip will automatically be reset to its first frame whenever the timeline adds * it back onto the display list. This only applies to MovieClip instances with mode=INDEPENDENT. * <br><br> * For example, if you had a character animation with a "body" child MovieClip instance * with different costumes on each frame, you could set `body.autoReset = false`, so that * you can manually change the frame it is on, without worrying that it will be reset * automatically. * @type {Boolean} * @default true */ this.autoReset = true; /** * An array of bounds for each frame in the MovieClip. This is mainly intended for tool output. * @type {Array} */ this.frameBounds = this.frameBounds || props.frameBounds; // frameBounds are set on the prototype in Animate. /** * 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 will * advance roughly one frame every 4 ticks. This will not be exact, because the time between each tick will * vary slightly between frames. * * This feature is dependent on the tick event object (or an object with an appropriate "delta" property) being * passed into {@link easeljs.Stage#update}. * @type {Number} * @default null */ this.framerate = null; /** * @type {Number} * @default 0 * @private */ this._synchOffset = 0; /** * @type {Number} * @default -1 * @private */ this._rawPosition = -1; // TODO: evaluate using a ._reset Boolean prop instead of -1. /** * The time remaining from the previous tick, only applicable when .framerate is set. * @type {Number} * @private */ this._t = 0; /** * List of display objects that are actively being managed by the MovieClip. * @type {Object} * @private */ this._managed = {}; /** * @type {Function} * @private */ this._bound_resolveState = this._resolveState.bind(this); } static init () { if (MovieClip.inited) { return; } // plugins introduce some overhead to Tween, so we only install this if an MC is instantiated. MovieClipPlugin.install(); MovieClip.inited = true; } // TODO: can we just proxy `get currentFrame` to timeline.position as well? Ditto for `get loop` (or just remove entirely). // /** * Returns an array of objects with label and position (aka frame) properties, sorted by position. * @see {@link tweenjs.Timeline#labels} * @type {Array} * @readonly */ get labels () { return this.timeline.labels; } /** * Returns the name of the label on or immediately before the current frame. * @see {@link tweenjs.Timeline#currentLabel} * for more information. * @type {String} * @readonly */ get currentLabel () { return this.timeline.currentLabel; } /** * Returns the duration of this MovieClip in seconds or ticks. * @see {@link tweenjs.Timeline#duration} * @type {Number} * @readonly */ get duration () { return this.timeline.duration; } /** * Returns the duration of this MovieClip in seconds or ticks. Identical to {@link easeljs.MovieClip#duration} * and provided for Adobe Flash/Animate API compatibility. * @see {@link tweenjs.Timeline#duration} * @type {Number} * @readonly */ get totalFrames () { return this.duration; } isVisible () { // children are placed in draw, so we can't determine if we have content. return !!(this.visible && this.alpha > 0 && this.scaleX != 0 && this.scaleY != 0); } draw (ctx, ignoreCache) { // draw to cache first: if (this.drawCache(ctx, ignoreCache)) { return true; } this._updateState(); super.draw(ctx, ignoreCache); return true; } /** * 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 plays the timeline. * @param {String | Number} positionOrLabel The animation name or frame number to go to. */ gotoAndPlay (positionOrLabel) { this.play(); this._goto(positionOrLabel); } /** * Advances this movie clip to the specified position or label and stops the timeline. * @param {String | Number} positionOrLabel The animation or frame name to go to. */ gotoAndStop (positionOrLabel) { this.stop(); this._goto(positionOrLabel); } /** * Advances the playhead. This occurs automatically each tick by default. * @param {Number} [time] The amount of time in ms to advance by. Only applicable if framerate is set. */ advance (time) { if (this.mode !== MovieClip.INDEPENDENT) { return; } // update happens in draw for synched clips // if this MC doesn't have a framerate, hunt ancestors for one: let o = this, fps = o.framerate; while ((o = o.parent) && fps === null) { if (o.mode === MovieClip.INDEPENDENT) { fps = o._framerate; } } this._framerate = fps; if (this.paused) { return; } // calculate how many frames to advance: let t = (fps !== null && fps !== -1 && time !== null) ? time / (1000 / fps) + this._t : 1; let frames = t | 0; this._t = t - frames; // leftover time, save to add to next advance. while (frames--) { this._updateTimeline(this._rawPosition + 1, false); } } /** * MovieClip instances cannot be cloned. * @throws MovieClip cannot be cloned. */ clone () { // TODO: add support for this? Need to clone the Timeline & retarget tweens - pretty complex. throw "MovieClip cannot be cloned."; } _updateState () { if (this._rawPosition === -1 || this.mode !== MovieClip.INDEPENDENT) { this._updateTimeline(-1); } } _tick (evtObj) { this.advance(evtObj && evtObj.delta); super._tick(evtObj); } /** * @param {String | Number} positionOrLabel The animation name or frame number to go to. * @protected */ _goto (positionOrLabel) { let pos = this.timeline.resolve(positionOrLabel); if (pos == null) { return; } this._t = 0; this._updateTimeline(pos, true); } /** * @private */ _reset () { this._rawPosition = -1; this._t = this.currentFrame = 0; this.paused = false; } /** * @param {Number} rawPosition * @param {Boolean} jump Indicates whether this update is due to jumping (via gotoAndXX) to a new position. * @protected */ _updateTimeline (rawPosition, jump) { let synced = this.mode !== MovieClip.INDEPENDENT, tl = this.timeline; if (synced) { rawPosition = this.startPosition + (this.mode === MovieClip.SINGLE_FRAME ? 0 : this._synchOffset); } if (rawPosition < 1) { rawPosition = 0; } if (this._rawPosition === rawPosition && !synced) { return; } this._rawPosition = rawPosition; // update timeline position, ignoring actions if this is a graphic. tl.loop = this.loop; // TODO: should we maintain this on MovieClip, or just have it on timeline? tl.setPosition(rawPosition, synced || !this.actionsEnabled, jump, this._bound_resolveState); } /** * Renders position 0 without running actions or updating _rawPosition. * Primarily used by Animate CC to build out the first frame in the constructor of MC symbols. * NOTE: not tested when run after the MC advances past the first frame. * @protected */ _renderFirstFrame () { const tl = this.timeline, pos = tl.rawPosition; tl.setPosition(0, true, true, this._bound_resolveState); tl.rawPosition = pos; } /** * Runs via a callback after timeline property updates and before actions. * @protected */ _resolveState () { let tl = this.timeline; this.currentFrame = tl.position; for (let n in this._managed) { this._managed[n] = 1; } let tweens = tl.tweens; for (let tween of tweens) { let target = tween.target; if (target === this || tween.passive) { continue; } // TODO: this assumes the actions tween from Animate has `this` as the target. Likely a better approach. let offset = tween._stepPosition; if (target instanceof DisplayObject) { // motion tween. this._addManagedChild(target, offset); } else { // state tween. this._setState(target.state, offset); } } let kids = this.children; for (let i=kids.length-1; i>=0; i--) { let id = kids[i].id; if (this._managed[id] === 1) { this.removeChildAt(i); delete(this._managed[id]); } } } /** * @param {Array} state * @param {Number} offset * @protected */ _setState (state, offset) { if (!state) { return; } for (let i = state.length - 1; i >= 0; i--) { let o = state[i]; let target = o.t; let props = o.p; for (let n in props) { target[n] = props[n]; } this._addManagedChild(target, offset); } } /** * Adds a child to the timeline, and sets it up as a managed child. * @param {easeljs.MovieClip} child The child MovieClip to manage * @param {Number} offset * @private */ _addManagedChild (child, offset) { if (child._off) { return; } this.addChildAt(child, 0); if (child instanceof MovieClip) { child._synchOffset = offset; // TODO: this does not precisely match Adobe Flash/Animate, which loses track of the clip if it is renamed or removed from the timeline, which causes it to reset. // TODO: should also reset when MovieClip loops, though that will be a bit tricky to detect. if (child.mode === MovieClip.INDEPENDENT && child.autoReset && !this._managed[child.id]) { child._reset(); } } this._managed[child.id] = 2; } /** * @param {easeljs.Matrix2D} matrix * @param {Boolean} ignoreTransform * @return {easeljs.Rectangle} * @protected */ _getBounds (matrix, ignoreTransform) { let bounds = this.getBounds(); if (!bounds && this.frameBounds) { bounds = this._rectangle.copy(this.frameBounds[this.currentFrame]); } if (bounds) { return this._transformBounds(bounds, matrix, ignoreTransform); } return super._getBounds(matrix, ignoreTransform); } } /** * The MovieClip will advance independently of its parent, even if its parent is paused. * This is the default mode. * @static * @type {String} * @default independent * @readonly */ MovieClip.INDEPENDENT = "independent"; /** * The MovieClip will only display a single frame (as determined by the startPosition property). * @static * @type {String} * @default single * @readonly */ MovieClip.SINGLE_FRAME = "single"; /** * The MovieClip will be advanced only when its parent advances and will be synched to the position of * the parent MovieClip. * @static * @type {String} * @default synched * @readonly */ MovieClip.SYNCHED = "synched"; /** * Has the MovieClipPlugin been installed to TweenJS yet? * @static * @type {Boolean} * @default false * @readonly */ MovieClip.inited = false; /** * This plugin works with <a href="http://tweenjs.com" target="_blank">TweenJS</a> to prevent the startPosition property from tweening. * @todo update to new plugin model * @static * @inner */ class MovieClipPlugin { constructor () { throw "MovieClipPlugin cannot be instantiated."; } /** * @private */ static install () { Tween.installPlugin(MovieClipPlugin); } /** * @param {tweenjs.Tween} tween * @param {String} prop * @param {String|Number|Boolean} value * @private */ static init (tween, prop, value) { return value; } /** * @param {tweenjs.Tween} tween * @param {String} prop * @param {String | Number | Boolean} value * @param {Array} startValues * @param {Array} endValues * @param {Number} ratio * @param {Object} wait * @param {Object} end * @return {*} */ static tween (tween, prop, value, startValues, endValues, ratio, wait, end) { if (!(tween.target instanceof MovieClip)) { return value; } return (ratio === 1 ? endValues[prop] : startValues[prop]); } } /** * @static * @type {Number} * @default 100 * @readonly */ MovieClipPlugin.priority = 100;