base/Timeline.js

/*
 * Copyright 2020 WICKLETS LLC
 *
 * This file is part of Wick Engine.
 *
 * Wick Engine is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Wick Engine is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Wick Engine.  If not, see <https://www.gnu.org/licenses/>.
 */

/**
 * Class representing a Wick Timeline.
 */
Wick.Timeline = class extends Wick.Base {
    /**
     * Create a timeline.
     */
    constructor(args) {
        super(args);

        this._playheadPosition = 1;
        this._activeLayerIndex = 0;

        this._playing = true;

        this._fillGapsMethod = "auto_extend";
        this._frameForced = false;
    }

    _serialize(args) {
        var data = super._serialize(args);

        data.playheadPosition = this._playheadPosition;
        data.activeLayerIndex = this._activeLayerIndex;

        return data;
    }

    _deserialize(data) {
        super._deserialize(data);

        this._playheadPosition = data.playheadPosition;
        this._activeLayerIndex = data.activeLayerIndex;

        this._playing = true;
    }

    get classname() {
        return 'Timeline';
    }

    /**
     * The layers that belong to this timeline.
     * @type {Wick.Layer}
     */
    get layers() {
        return this.getChildren('Layer');
    }

    /**
     * The position of the playhead. Determines which frames are visible.
     * @type {number}
     */
    get playheadPosition() {
        return this._playheadPosition;
    }

    set playheadPosition(playheadPosition) {
        // Automatically clear selection when any playhead in the project moves
        if(this.project && this._playheadPosition !== playheadPosition && this.parentClip.isFocus) {
            this.project.selection.clear('Canvas');
            this.project.resetTools();
        }

        this._playheadPosition = playheadPosition;
        if (this._playheadPosition < 1) {
            this._playheadPosition = 1;
        }

        // Automatically apply tween transforms on child frames when playhead moves
        this.activeFrames.forEach(frame => {
            frame.applyTweenTransforms();
            frame.updateClipTimelinesForAnimationType();
        });
    }

    /**
     * Forces timeline to move to the next frame.
     * @param {number} frame 
     */
    forceFrame(frame) {
        this.playheadPosition = frame;
        this._frameForced = true;
        this.makeTimelineInBounds();
    }

    /**
     * Returns true if the frame was forced previously.
     */
    get frameForced () {
        return this._frameForced;
    }

    /**
     * The index of the active layer. Determines which frame to draw onto.
     * @type {number}
     */
    get activeLayerIndex() {
        return this._activeLayerIndex;
    }

    set activeLayerIndex(activeLayerIndex) {
        this._activeLayerIndex = activeLayerIndex;
    }

    /**
     * The total length of the timeline.
     * @type {number}
     */
    get length() {
        var length = 0;
        this.layers.forEach(function(layer) {
            var layerLength = layer.length;
            if (layerLength > length) {
                length = layerLength;
            }
        });
        return length;
    }

    /**
     * The active layer.
     * @type {Wick.Layer}
     */
    get activeLayer() {
        return this.layers[this.activeLayerIndex];
    }

    /**
     * The active frames, determined by the playhead position.
     * @type {Wick.Frame[]}
     */
    get activeFrames() {
        var frames = [];
        this.layers.forEach(layer => {
            var layerFrame = layer.activeFrame;
            if (layerFrame) {
                frames.push(layerFrame);
            }
        });
        return frames;
    }

    /*
     * exports the project as an SVG file
     * @onError {function(message)}
     * @returns {string} - the SVG for the current view in string form (maybe this should be base64 or a blob or something)
     */
    exportSVG(onError) {

            var svgOutput = paper.project.exportSVG({ asString: true, matchShapes: true, embedImages: true });
            return svgOutput;
        }
        //this.project.paper.
        //paperGroup = new paper.Group
        /**
         * The active frame, determined by the playhead position.
         * @type {Wick.Frame}
         */
    get activeFrame() {
        return this.activeLayer && this.activeLayer.activeFrame;
    }

    /**
     * All frames inside the timeline.
     * @type {Wick.Frame[]}
     */
    get frames() {
        var frames = [];
        this.layers.forEach(layer => {
            layer.frames.forEach(frame => {
                frames.push(frame);
            });
        });
        return frames;
    }

    /**
     * All clips inside the timeline.
     * @type {Wick.Clip[]}
     */
    get clips() {
        var clips = [];
        this.frames.forEach(frame => {
            clips = clips.concat(frame.clips);
        });
        return clips;
    }

    /**
     * Finds the frame with a given name.
     * @type {Wick.Frame|null}
     */
    getFrameByName(name) {
        return this.frames.find(frame => {
            return frame.name === name;
        }) || null;
    }

    /**
     * Add a frame to one of the layers on this timeline. If there is no layer where the frame wants to go, the frame will not be added.
     * @param {Wick.Frame} frame - the frame to add
     */
    addFrame(frame) {
        if (frame.originalLayerIndex >= this.layers.length) return;

        if (frame.originalLayerIndex === -1) {
            this.activeLayer.addFrame(frame);
        } else {
            this.layers[frame.originalLayerIndex].addFrame(frame);
        }
    }

    /**
     * Adds a layer to the timeline.
     * @param {Wick.Layer} layer - The layer to add.
     */
    addLayer(layer) {
        this.addChild(layer);
        if (!layer.name) {
            if (this.layers.length > 1) {
                layer.name = "Layer " + this.layers.length;
            } else {
                layer.name = "Layer";
            }
        }
    }

    /**
     * Adds a tween to a frame on this timeline.
     * @param {Wick.Tween} tween - the tween to add.
     */
    addTween(tween) {
        if (tween.originalLayerIndex >= this.layers.length) return;

        if (tween.originalLayerIndex === -1) {
            this.activeLayer.addTween(tween);
        } else {
            this.layers[tween.originalLayerIndex].addTween(tween);
        }
    }

    /**
     * Remmoves a layer from the timeline.
     * @param {Wick.Layer} layer - The layer to remove.
     */
    removeLayer(layer) {
        // You can't remove the last layer.
        if (this.layers.length <= 1) {
            return;
        }

        // Activate the layer below the removed layer if we removed the active layer.
        if (this.activeLayerIndex === this.layers.length - 1) {
            this.activeLayerIndex--;
        }

        this.removeChild(layer);
    }

    /**
     * Moves a layer to a different position, inserting it before/after other layers if needed.
     * @param {Wick.Layer} layer - The layer to add.
     * @param {number} index - the new position to move the layer to.
     */
    moveLayer(layer, index) {
        var layers = this.getChildren('Layer');
        
        layers.splice(layers.indexOf(layer), 1);
        layers.splice(index, 0, layer);

        this._children = layers;
    }

    /**
     * Gets the frames at the given playhead position.
     * @param {number} playheadPosition - the playhead position to search.
     * @returns {Wick.Frame[]} The frames at the playhead position.
     */
    getFramesAtPlayheadPosition(playheadPosition) {
        var frames = [];

        this.layers.forEach(layer => {
            var frame = layer.getFrameAtPlayheadPosition(playheadPosition);
            if (frame) frames.push(frame);
        });

        return frames;
    }

    /**
     * Get all frames in this timeline.
     * @param {boolean} recursive - If set to true, will also include the children of all child timelines.
     */
    getAllFrames(recursive) {
        var allFrames = [];
        this.layers.forEach(layer => {
            allFrames = allFrames.concat(layer.frames);

            if (recursive) {
                layer.frames.forEach(frame => {
                    frame.clips.forEach(clip => {
                        allFrames = allFrames.concat(clip.timeline.getAllFrames(recursive));
                    });
                });
            }
        });
        return allFrames;
    }

    /**
     * Gets all frames in the layer that are between the two given playhead positions and layer indices.
     * @param {number} playheadPositionStart - The start of the horizontal range to search
     * @param {number} playheadPositionEnd - The end of the horizontal range to search
     * @param {number} layerIndexStart - The start of the vertical range to search
     * @param {number} layerIndexEnd - The end of the vertical range to search
     * @return {Wick.Frame[]} The frames in the given range.
     */
    getFramesInRange(playheadPositionStart, playheadPositionEnd, layerIndexStart, layerIndexEnd) {
        var framesInRange = [];

        this.layers.filter(layer => {
            return layer.index >= layerIndexStart &&
                layer.index <= layerIndexEnd;
        }).forEach(layer => {
            framesInRange = framesInRange.concat(layer.getFramesInRange(playheadPositionStart, playheadPositionEnd));
        })

        return framesInRange;
    }

    /**
     * Advances the timeline one frame forwards. Loops back to beginning if the end is reached.
     */
    advance() {
        if (this._playing) {
            this.playheadPosition++;
            this._frameForced = false;
            this.makeTimelineInBounds();
        }
    }

    /**
     * Ensures playhead position is in bounds.
     */
    makeTimelineInBounds () {
        if (this.playheadPosition > this.length) {
            this.playheadPosition = 1;
        }
    }

    /**
     * Makes the timeline advance automatically during ticks.
     */
    play() {
        this._playing = true;
    }

    /**
     * Stops the timeline from advancing during ticks.
     */
    stop() {
        this._playing = false;
    }

    /**
     * Stops the timeline and moves to a given frame number or name.
     * @param {string|number} frame - A playhead position or name of a frame to move to.
     */
    gotoAndStop(frame) {
        this.stop();
        this.gotoFrame(frame);
    }

    /**
     * Plays the timeline and moves to a given frame number or name.
     * @param {string|number} frame - A playhead position or name of a frame to move to.
     */
    gotoAndPlay(frame) {
        this.play();
        this.gotoFrame(frame);
    }

    /**
     * Moves the timeline forward one frame. Loops back to 1 if gotoNextFrame moves the playhead past the past frame.
     */
    gotoNextFrame() {
        // Loop back to beginning if gotoNextFrame goes past the last frame
        var nextFramePlayheadPosition = this.playheadPosition + 1;
        if (nextFramePlayheadPosition > this.length) {
            nextFramePlayheadPosition = 1;
        }

        this.gotoFrame(nextFramePlayheadPosition);
    }

    /**
     * Moves the timeline backwards one frame. Loops to the last frame if gotoPrevFrame moves the playhead before the first frame.
     */
    gotoPrevFrame() {
        var prevFramePlayheadPosition = this.playheadPosition - 1;
        if (prevFramePlayheadPosition <= 0) {
            prevFramePlayheadPosition = this.length;
        }

        this.gotoFrame(prevFramePlayheadPosition);
    }

    /**
     * Moves the playhead to a given frame number or name.
     * @param {string|number} frame - A playhead position or name of a frame to move to.
     */
    gotoFrame(frame) {
        if (typeof frame === 'string') {
            var namedFrame = this.frames.find(seekframe => {
                return seekframe.identifier === frame && !seekframe.onScreen;
            });

            if (namedFrame) {
                this.forceFrame(namedFrame.start);
            }
        } else if (typeof frame === 'number') {
            this.forceFrame(frame);
        } else {
            throw new Error('gotoFrame: Invalid argument: ' + frame);
        }
    }

    /**
     * The method to use to fill gaps in-beteen frames. Options: "blank_frames" or "auto_extend" (see Wick.Layer.resolveGaps)
     * @type {string}
     */
    get fillGapsMethod() {
        return this._fillGapsMethod;
    }

    set fillGapsMethod(fillGapsMethod) {
        if (fillGapsMethod === 'blank_frames' || fillGapsMethod === 'auto_extend') {
            this._fillGapsMethod = fillGapsMethod;
        } else {
            console.warn('Warning: Invalid fillGapsMethod: ' + fillGapsMethod);
            console.warn('Valid fillGapsMethod: "blank_frames", "auto_extend"');
        }
    }

    /**
     * Check if frame gap fixing should be deferred until later. Read only.
     * @type {boolean}
     */
    get waitToFillFrameGaps() {
        return this._waitToFillFrameGaps;
    }

    /**
     * Disables frame gap filling until resolveFrameGaps is called again.
     */
    deferFrameGapResolve() {
        this._waitToFillFrameGaps = true;
    }

    /**
     * Fill in all gaps between frames in all layers in this timeline.
     * @param {Wick.Frame[]} newOrModifiedFrames - The frames that should not be affected by the gap fill by being extended or shrunk.
     */
    resolveFrameGaps(newOrModifiedFrames) {
        if (!newOrModifiedFrames) newOrModifiedFrames = [];

        this._waitToFillFrameGaps = false;
        this.layers.forEach(layer => {
            layer.resolveGaps(newOrModifiedFrames.filter(frame => {
                return frame.parentLayer === layer;
            }));
        });
    }

    /**
     * Prevents frames from overlapping each other by removing pieces of frames that are touching.
     * @param {Wick.Frame[]} newOrModifiedFrames - the frames that should take precedence when determining which frames should get "eaten".
     */
    resolveFrameOverlap(frames) {
        this.layers.forEach(layer => {
            layer.resolveOverlap(frames.filter(frame => {
                return frame.parentLayer === layer;
            }));
        });
    }
}