base/Clip.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/>.
 */


/**
 * A class representing a Wick Clip.
 */
Wick.Clip = class extends Wick.Tickable {
    /**
     * Returns a list of all possible animation types for this object.
     * @type {Object} - An object containing keys that represent the animation type a a key and a human-readable version of the animation type as a value.
     */
    static get animationTypes () {
        return {
            'loop': 'Loop',
            'single': 'Single Frame',
            'playOnce': 'Play Once',
        }
    }

    /**
     * Create a new clip.
     * @param {string} identifier - The identifier of the new clip.
     * @param {Wick.Path|Wick.Clip[]} objects - Optional. A list of objects to add to the clip.
     * @param {Wick.Transformation} transformation - Optional. The initial transformation of the clip.
     */
    constructor(args) {
        if (!args) args = {};
        super(args);

        this.timeline = new Wick.Timeline();
        this.timeline.addLayer(new Wick.Layer());
        this.timeline.activeLayer.addFrame(new Wick.Frame());
        this._animationType = 'loop'; // Can be one of loop, oneFrame, single
        this._singleFrameNumber = 1; // Default to 1, this value is only used if the animation type is single
        this._playedOnce = false;
        this._isSynced = false;

        this._transformation = args.transformation || new Wick.Transformation();

        this.cursor = 'default';

        this._isSymbol = false;
        this._isClone = false;
        this._sourceClipUUID = null;

        this._assetSourceUUID = null;

        /* If objects are passed in, add them to the clip and reposition them */
        if (args.objects) {
            this.addObjects(args.objects);
        }

        this._clones = [];

    }

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

        data.transformation = this.transformation.values;
        data.timeline = this._timeline;
        data.animationType = this._animationType;
        data.singleFrameNumber = this._singleFrameNumber;
        data.assetSourceUUID = this._assetSourceUUID;
        data.isSynced = this._isSynced;

        return data;
    }

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

        this.transformation = new Wick.Transformation(data.transformation);
        this._timeline = data.timeline;
        this._animationType = data.animationType || 'loop';
        this._singleFrameNumber = data.singleFrameNumber || 1;
        this._assetSourceUUID = data.assetSourceUUID;
        this._isSynced = data.isSynced;

        this._playedOnce = false;

        this._clones = [];
    }

    get classname() {
        if (!this._isSymbol) {
            return 'Clip';
        } else {
            return 'Symbol';
        }
    }

    /**
     * Determines whether or not the clip is visible in the project.
     * @type {boolean}
     */
    get onScreen() {
        if (this.isRoot) {
            return true;
        } else if (this.parentFrame) {
            return this.parentFrame.onScreen;
        }
    }

    /**
     * Determines whether or not the clip is the root clip in the project.
     * @type {boolean}
     */
    get isRoot() {
        return this.project && this === this.project.root;
    }

    /**
     * True if the clip should sync to the timeline's position.
     * @type {boolean} 
     */
    get isSynced () {
        let isSingle = this.animationType === 'single';
        return this._isSynced && (!isSingle) && (!this.isRoot);
    }

    set isSynced (bool) {
        if (!(typeof bool === 'boolean')) {
            return;
        }
        this._isSynced = bool;

        if (bool) {
            this.applySyncPosition();
        } else {
            this.timeline.playheadPosition = 1; 
        }
    }

    /**
     * Determines whether or not the clip is the currently focused clip in the project.
     * @type {boolean}
     */
    get isFocus() {
        return this.project && this === this.project.focus;
    }

    /**
     * Check if a Clip is a clone of another object.
     * @type {boolean}
     */
    get isClone() {
        return this._isClone;
    }

    /**
     * The uuid of the clip that this clip was cloned from.
     * @type {string}
     */
    get sourceClipUUID() {
        return this._sourceClipUUID;
    }

    /**
     * Returns the source clip of this clip if this clip is a clone. Null otherwise.
     * 
     */
    get sourceClip () {
        if (!this.sourceClipUUID) return null;

        return this.project.getObjectByUUID(this.sourceClipUUID);
    }

    /**
     * The uuid of the ClipAsset that this clip was created from.
     * @type {string}
     */
    get assetSourceUUID () {
        return this._assetSourceUUID;
    }

    set assetSourceUUID (assetSourceUUID) {
        this._assetSourceUUID = assetSourceUUID;
    }

    /**
     * The timeline of the clip.
     * @type {Wick.Timeline}
     */
    get timeline() {
        return this.getChild('Timeline');
    }

    set timeline(timeline) {
        if (this.timeline) {
            this.removeChild(this.timeline);
        }
        this.addChild(timeline);
    }

    /**
     * The animation type of the clip. Must be of a type represented within animationTypes;
     * @type {string}
     */
    get animationType () {
        return this._animationType;
    }

    set animationType (animationType) {
        // Default to loop if an invalid animation type is passed in.
        if (!Wick.Clip.animationTypes[animationType]) {
            console.error("Animation type:" + animationType + " is invalid for clips! Defaulting to Loop.");
            this._animationType = 'loop';
        } else {
            this._animationType = animationType;

            this.resetTimelinePosition();
        }
    }

    /**
     * The frame to display when animation type is set to singleFrame.
     * @type {number}
     */
    get singleFrameNumber () {
        if (this.animationType !== 'single') {
            return null;
        } else {
            return this._singleFrameNumber;
        }
    }

    set singleFrameNumber (frame) {
        // Constrain to be within the length of the clip.
        if (frame < 1) {
            frame = 1;
        } else if (frame > this.timeline.length) {
            frame = this.timeline.length;
        }

        this._singleFrameNumber = frame;
        this.applySingleFramePosition();
    }

    /**
     * The frame to display when the clip is synced
     * @type {number}
     */
    get syncFrame () {
        let timelineOffset = this.parentClip.timeline.playheadPosition - this.parentFrame.start;

        // Show the last frame if we're past it on a playOnce Clip.
        if (this.animationType === 'playOnce' && (timelineOffset >= this.timeline.length)) {
            return this.timeline.length;
        }

        // Otherwise, show the correct frame.
        return (timelineOffset % this.timeline.length) + 1;
    }

    /**
     * Returns true if the clip has been played through fully once.
     * @type {boolean}
     */
    get playedOnce () {
        return this._playedOnce;
    }

    set playedOnce (bool) {
        return this._playedOnce = bool;
    }

    /**
     * The active layer of the clip's timeline.
     * @type {Wick.Layer}
     */
    get activeLayer() {
        return this.timeline.activeLayer;
    }

    /**
     * The active frame of the clip's timeline.
     * @type {Wick.Frame}
     */
    get activeFrame() {
        return this.activeLayer.activeFrame;
    }

    /**
     * An array containing every clip and frame that is a child of this clip and has an identifier.
     * @type {Wick.Base[]}
     */
    get namedChildren() {
        var namedChildren = [];
        this.timeline.frames.forEach(frame => {
            // Objects that can be accessed by their identifiers:

            // Frames
            if (frame.identifier) {
                namedChildren.push(frame);
            }

            // Clips
            frame.clips.forEach(clip => {
                if (clip.identifier) {
                    namedChildren.push(clip);
                }
            });

            // Dynamic text paths
            frame.dynamicTextPaths.forEach(path => {
                namedChildren.push(path);
            })
        });
        return namedChildren;
    }

    /**
     * An array containing every clip and frame that is a child of this clip and has an identifier, and also is visible on screen.
     * @type {Wick.Base[]}
     */
    get activeNamedChildren() {
        return this.namedChildren.filter(child => {
            return child.onScreen;
        });
    }

    /**
     * Resets the clip's timeline position.
     */
    resetTimelinePosition () {
        if (this.animationType === 'single') {
            this.applySingleFramePosition();
        } else {
            this.timeline.playheadPosition = 1; // Reset timeline position if we are not on single frame.
        }
    }

    /**
     * Updates the frame's single frame positions if necessary. Only works if the clip's animationType is 'single'.
     */
    applySingleFramePosition () {
        if (this.animationType === 'single') { 
            // Ensure that the single frame we've chosen is reflected no matter what.
            this.timeline.playheadPosition = this.singleFrameNumber;
        }
    }

    /**
     * Updates the clip's playhead position if the Clip is in sync mode
     */
    applySyncPosition () {
        if (this.isSynced) {
            this.timeline.playheadPosition = this.syncFrame;
        }
    }

    /**
     * Updates the timeline of the clip based on the animation type of the clip.
     */
    updateTimelineForAnimationType () {
        if (this.animationType === 'single') {
            this.applySingleFramePosition();
        } 
        
        if (this.isSynced) {
            this.applySyncPosition();
        }
    }

    /**
     * Remove a clone from the clones array by uuid.
     * @param {string} uuid 
     */
    removeClone (uuid) {
        if (this.isClone) return;
        this._clones = this.clones.filter(obj => obj.uuid !== uuid);
    }

    /**
     * Remove this clip from its parent frame.
     */
    remove() {
        // Don't attempt to remove if the object has already been removed.
        // (This is caused by calling remove() multiple times on one object inside a script.)
        if (!this.parent || this._willBeRemoved) return;
        this._willBeRemoved = true;

        // Force unload to run now, before object is removed;
        this.runScript('unload'); 

        // Remove from the clones array.
        this.sourceClip && this.sourceClip.removeClone(this.uuid);
        this.parent.removeClip(this);
        this.removed = true;
    }

    /**
     * Remove this clip and add all of its paths and clips to its parent frame.
     * @returns {Wick.Base[]} the objects that were inside the clip.
     */
    breakApart() {
        var leftovers = [];

        this.timeline.activeFrames.forEach(frame => {
            frame.clips.forEach(clip => {
                clip.transformation.x += this.transformation.x;
                clip.transformation.y += this.transformation.y;
                this.parentTimeline.activeFrame.addClip(clip);
                leftovers.push(clip);
            });
            frame.paths.forEach(path => {
                path.x += this.transformation.x;
                path.y += this.transformation.y;
                this.parentTimeline.activeFrame.addPath(path);
                leftovers.push(path);
            });
        });

        this.remove();

        return leftovers;
    }

    /**
     * Add paths and clips to this clip.
     * @param {Wick.Base[]} objects - the paths and clips to add to the clip
     */
    addObjects(objects) {
        // Reposition objects such that their origin point is equal to this Clip's position
        objects.forEach(object => {
            object.x -= this.transformation.x;
            object.y -= this.transformation.y;
        });

        objects.forEach(obj => {
            if (obj instanceof Wick.Clip) {
                this.activeFrame.addClip(obj);
            } else if (obj instanceof Wick.Path) {
                this.activeFrame.addPath(obj);
            }
        }); 
    }

    /**
     * Stops a clip's timeline on that clip's current playhead position.
     */
    stop() {
        this.timeline.stop();
    }

    /**
     * Plays a clip's timeline from that clip's current playhead position.
     */
    play() {
        this.timeline.play();
    }

    /**
     * Moves a clip's playhead to a specific position and stops that clip's timeline on that position.
     * @param {number|string} frame - number or string representing the frame to move the playhead to.
     */
    gotoAndStop(frame) {
        this.timeline.gotoAndStop(frame);
        this.applySingleFramePosition();
    }

    /**
     * Moves a clip's playhead to a specific position and plays that clip's timeline from that position.
     * @param {number|string} frame - number or string representing the frame to move the playhead to.
     */
    gotoAndPlay(frame) {
        this.timeline.gotoAndPlay(frame);
        this.applySingleFramePosition();
    }

    /**
     * Move the playhead of the clips timeline forward one frame. Does nothing if the clip is on its last frame.
     */
    gotoNextFrame() {
        this.timeline.gotoNextFrame();
        this.applySingleFramePosition();
    }

    /**
     * Move the playhead of the clips timeline backwards one frame. Does nothing if the clip is on its first frame.
     */
    gotoPrevFrame() {
        this.timeline.gotoPrevFrame();
        this.applySingleFramePosition();
    }

    /**
     * Returns the name of the frame which is currently active. If multiple frames are active, returns the name of the first active frame.
     * @returns {string} Active Frame name. If the active frame does not have an identifier, returns empty string.
     */
    get currentFrameName() {
        let frames = this.timeline.activeFrames;

        let name = '';
        frames.forEach(frame => {
            if (name) return;

            if (frame.identifier) {
                name = frame.identifier;
            }
        });

        return name;
    }

    /**
     * @deprecated
     * Returns the current playhead position. This is a legacy function, you should use clip.playheadPosition instead.
     * @returns {number} Playhead Position.
     */
    get currentFrameNumber() {
        return this.timeline.playheadPosition;
    }

    /**
     * The current transformation of the clip.
     * @type {Wick.Transformation}
     */
    get transformation() {
        return this._transformation;
    }

    set transformation(transformation) {
        this._transformation = transformation;

        // When the transformation changes, update the current tween, if one exists
        if (this.parentFrame) {
            // This tween must only ever be the tween over the current playhead position.
            // Altering the active tween will overwrite tweens when moving between frames.
            var tween = this.parentFrame.getTweenAtCurrentPlayheadPosition();
            if (tween) {
                tween.transformation = this._transformation.copy();
            }
        }
    }

    /**
     * Perform circular hit test with other clip.
     * @param {Wick.Clip} other - the clip to hit test with
     * @param {object} options - Hit test options
     * @returns {object} Hit information
     */
    circleHits(other, options) {
        let bounds1 = this.absoluteBounds;
        let bounds2 = other.absoluteBounds;
        let c1 = bounds1.center;
        let c2 = bounds2.center;
        let distance = Math.sqrt((c1.x - c2.x)*(c1.x - c2.x) + (c1.y - c2.y)*(c1.y - c2.y));
        let r1 = options.radius ? options.radius : this.radius;
        let r2 = other.radius; //should add option for other radius?
        let overlap = r1 + r2 - distance;
        // TODO: Maybe add a case for overlap === 0?
        if (overlap > 0) {
            let x = c1.x - c2.x;
            let y = c1.y - c2.y;
            let magnitude = Math.sqrt(x*x + y*y);
            x = x / magnitude;
            y = y / magnitude;
            // <x,y> is now a normalized vector from c2 to c1 
            
            let result = {};
            if (options.overlap) {
                result.overlapX = overlap * x;
                result.overlapY = overlap * y;
            }
            if (options.offset) {
                result.offsetX = overlap * x;
                result.offsetY = overlap * y;
            }
            if (options.intersections) {
                if (r2 - distance > r1 || r1 - distance > r2 || distance === 0) {
                    result.intersections = [];
                }
                else {
                    // Using https://mathworld.wolfram.com/Circle-CircleIntersection.html
                    let d = (distance * distance + r1*r1 - r2*r2) / (2 * distance);
                    let h = Math.sqrt(r1 * r1 - d * d);
                    let x0 = c1.x - d*x;
                    let y0 = c1.y - d*y;
                    result.intersections = [{x: x0 + h*y, y: y0 - h*x}, 
                        {x: x0 - h*y, y: y0 + h*x}];
                }
            }
            return result;
        }
        
        return null;
    }

    /**
     * Perform rectangular hit test with other clip.
     * @param {Wick.Clip} other - the clip to hit test with
     * @param {object} options - Hit test options
     * @returns {object} Hit information
     */
    rectangleHits(other, options) {
        let bounds1 = this.absoluteBounds;
        let bounds2 = other.absoluteBounds;

        // TODO: write intersects so we don't rely on paper Rectangle objects
        if (bounds1.intersects(bounds2)) {
            let result = {};

            if (options.overlap) {
                // Find the direction along which we have to travel the least distance to no longer overlap
                let left = bounds2.left - bounds1.right;
                let right = bounds2.right - bounds1.left;
                let up = bounds2.top - bounds1.bottom;
                let down = bounds2.bottom - bounds1.top;
                let overlapX = Math.abs(left) < Math.abs(right) ? left : right;
                let overlapY = Math.abs(up) < Math.abs(down) ? up : down;
                if (Math.abs(overlapX) < Math.abs(overlapY)) {
                    overlapY = 0;
                }
                else {
                    overlapX = 0;
                }

                result.overlapX = overlapX;
                result.overlapY = overlapY;
            }
            if (options.offset) {
                // Find how far along the center to center vector we must travel to no longer overlap
                let vectorX = bounds1.center.x - bounds2.center.x;
                let vectorY = bounds1.center.y - bounds2.center.y;
                let magnitude = Math.sqrt(vectorX*vectorX + vectorY*vectorY);
                vectorX /= magnitude;
                vectorY /= magnitude;

                // Choose p1, p2, based on quadrant of center to center vector
                let p1 = vectorX > 0 ? (vectorY > 0 ? bounds1.topLeft : bounds1.bottomLeft) : (vectorY > 0 ? bounds1.topRight : bounds1.bottomRight);
                let p2 = vectorX > 0 ? (vectorY > 0 ? bounds2.bottomRight : bounds2.topRight) : (vectorY > 0 ? bounds2.bottomLeft : bounds2.topLeft);
                
                if (Math.abs(p2.x - p1.x) < Math.abs((p2.y - p1.y) * vectorX / vectorY)) {
                    result.offsetX = p2.x - p1.x;
                    result.offsetY = result.offsetX * vectorY / vectorX;
                }
                else {
                    result.offsetY = p2.y - p1.y;
                    result.offsetX = result.offsetY * vectorX / vectorY;
                }
            }
            if (options.intersections) {
                result.intersections = [];
                let ps1 = [bounds1.topLeft, bounds1.topRight, bounds1.bottomRight, bounds1.bottomLeft];
                let ps2 = [bounds2.topLeft, bounds2.topRight, bounds2.bottomRight, bounds2.bottomLeft];
                for (let i = 0; i < 4; i++) {
                    for (let j = (i + 1) % 2; j < 4; j += 2) { // iterate over the perpendicular lines
                        let a = ps1[i];
                        let b = ps1[(i + 1) % 4];
                        let c = ps2[j];
                        let d = ps2[(j + 1) % 4];

                        // Perpendicular lines will intersect, we'll use parametric line intersection
                        //<x,y> = a + (b - a)t1
                        //<x,y> = c + (d - c)t2
                        //a + (b - a)t1 = c + (d - c)t2
                        //t1(b - a) = (c + (d - c)t2 - a)
                        //(a - c)/(d - c) = t2
                        let t1, t2;
                        if (a.x === b.x) {
                            t2 = (a.x - c.x) / (d.x - c.x);
                            t1 = (c.y + (d.y - c.y) * t2 - a.y) / (b.y - a.y);
                        }
                        else {
                            //a.y === b.y
                            t2 = (a.y - c.y) / (d.y - c.y);
                            t1 = (c.x + (d.x - c.x) * t2 - a.x) / (b.x - a.x);
                        }
                        if (0 <= t1 && t1 <= 1 && 0 <= t2 && t2 <= 1) {
                            result.intersections.push({x: a.x + (b.x - a.x) * t1, y: a.y + (b.y - a.y) * t1});
                        }
                    }
                }
            }

            return result;
        }
        else {
            return null;
        }
    }

    // Return whether triangle p1 p2 p3 is clockwise (in screen space,
    // means counterclockwise in a normal space with y axis pointed up)
    cw(x1, y1, x2, y2, x3, y3) {           
        const cw = ((y3 - y1) * (x2 - x1)) - ((y2 - y1) * (x3 - x1));
        return cw >= 0; // colinear ?
    }

    /**
     * Perform convex hull hit test with other clip.
     * @param {Wick.Clip} other - the clip to hit test with
     * @param {object} options - Hit test options
     * @returns {object} Hit information
     */
    convexHits(other, options) {
        // Efficient check first
        let bounds1 = this.absoluteBounds;
        let bounds2 = other.absoluteBounds;
        // TODO: write intersects so we don't rely on paper Rectangle objects
        if (!bounds1.intersects(bounds2)) {
            return null;
        }
        let c1 = bounds1.center;
        let c2 = bounds2.center;

        // clockwise arrays of points in format [[x1, y1], [x2, y2], ...]
        let hull1 = this.convexHull;
        let hull2 = other.convexHull;

        let finished1 = false;
        let finished2 = false;

        let i1 = hull1.length - 1;
        let i2 = hull2.length - 1;

        let intersections = [];

        let n = 0;
        // Algorithm from https://www.bowdoin.edu/~ltoma/teaching/cs3250-CompGeom/spring17/Lectures/cg-convexintersection.pdf
        while ((!finished1 || !finished2) && n <= 2 * (hull1.length + hull2.length)) {
            n++;
            // line segments A is ab, B is cd
            let a = hull1[i1],
            b = hull1[((i1 - 1) % hull1.length + hull1.length) % hull1.length],
            c = hull2[i2],
            d = hull2[((i2 - 1) % hull2.length + hull2.length) % hull2.length];

            //Use parametric line intersection
            //<x,y> = a + (b - a)t1
            //<x,y> = c + (d - c)t2
            //a + (b - a)t1 = c + (d - c)t2
            //t1 = (c.x + (d.x - c.x)t2 - a.x) / (b.x - a.x)
            //a.y + (b.y - a.y) * (c.x + (d.x - c.x)t2 - a.x) / (b.x - a.x) = c.y + (d.y - c.y)t2
            //t2((b.y - a.y)(d.x - c.x)/(b.x - a.x) - (d.y - c.y)) = c.y - a.y - (b.y - a.y)*(c.x - a.x)/(b.x - a.x)
            //t2 = (c.y - a.y - (b.y - a.y)*(c.x - a.x)/(b.x - a.x))  /  ((b.y - a.y)(d.x - c.x)/(b.x - a.x) - (d.y - c.y))
            let t2 = (c[1] - a[1] - (b[1] - a[1]) * (c[0] - a[0]) / (b[0] - a[0]))  /  ((b[1] - a[1]) * (d[0] - c[0]) / (b[0] - a[0]) - d[1] + c[1]);
            let t1 = (c[0] + (d[0] - c[0]) * t2 - a[0]) / (b[0] - a[0]);

            if (0 <= t1 && t1 <= 1 && 0 <= t2 && t2 <= 1) {
                intersections.push({x: a[0] + (b[0] - a[0])*t1, y: a[1] + (b[1] - a[1]) * t1});
            }

            let APointingToB = t1 > 1;
            let BPointingToA = t2 > 1;

            if (BPointingToA && !APointingToB) {
                // Advance B
                i2 -= 1;
                if (i2 < 0) {
                    finished2 = true;
                    i2 += hull2.length;
                }
            }
            else if (APointingToB && !BPointingToA) {
                // Advance A
                i1 -= 1;
                if (i1 < 0) {
                    finished1 = true;
                    i1 += hull1.length;
                }
            }
            else {
                // Advance outside
                if (this.cw(a[0], a[1], b[0], b[1], d[0], d[1])) {
                    // Advance B
                    i2 -= 1;
                    if (i2 < 0) {
                        finished2 = true;
                        i2 += hull2.length;
                    }
                }
                else {
                    // Advance A
                    i1 -= 1;
                    if (i1 < 0) {
                        finished1 = true;
                        i1 += hull1.length;
                    }
                }
            }
        }
        // Ok, we have all the intersections now
        let avgIntersection = {x: 0, y: 0};
        if (intersections.length === 0) {
            avgIntersection.x = bounds1.width < bounds2.width ? c1.x : c2.x;
            avgIntersection.y = bounds1.width < bounds2.width ? c1.y : c2.y;
        }
        else {
            for (let i = 0; i < intersections.length; i++) {
                avgIntersection.x += intersections[i].x;
                avgIntersection.y += intersections[i].y;
            }
            avgIntersection.x /= intersections.length;
            avgIntersection.y /= intersections.length;
        }

        let result = {};
        if (options.intersections) {
            result.intersections = intersections;
        }
        if (options.offset) {
            // Calculate offset by taking the center of mass of the intersection, call it P,
            // get the radius from P on this convex hull in the direction
            // from this center to that center,
            // Then, the offset is a vector in the direction from that center to this center
            // with magnitude of that radius

            let targetTheta = Math.atan2(c2.y - c1.y, c2.x - c1.x); //from c1 to c2
            let r = this.radiusAtPointInDirection(hull1, avgIntersection, targetTheta);
            targetTheta = (targetTheta + Math.PI) % (2 * Math.PI);
            r += this.radiusAtPointInDirection(hull2, avgIntersection, targetTheta);

            let directionX = c1.x - c2.x;
            let directionY = c1.y - c2.y;
            let mag = Math.sqrt(directionX*directionX + directionY*directionY);
            directionX *= r / mag;
            directionY *= r / mag;
            result.offsetX = directionX;
            result.offsetY = directionY;
        }
        if (options.overlap) {
            //same as offset except instead of center to center, 
            //we will move perpendicular to the best fit line
            //of the intersection points

            let directionX, directionY;
            if (intersections.length < 2) {
                directionX = c2.x - c1.x;
                directionY = c2.y - c1.y;
            }
            else {
                let max_d = 0;
                for (let i = 1; i < intersections.length; i++) {
                    let d = (intersections[i].y - intersections[0].y) * (intersections[i].y - intersections[0].y) +
                        (intersections[i].x - intersections[0].x) * (intersections[i].x - intersections[0].x);
                    if (d > max_d) {
                        max_d = d;
                        directionX = -(intersections[i].y - intersections[0].y);
                        directionY = intersections[i].x - intersections[0].x;
                        if (directionX * (c1.x - avgIntersection.x) + directionY * (c1.y - avgIntersection.y) > 0) {
                            directionX = -directionX;
                            directionY = -directionY;
                        }
                    }
                }
            }

            let targetTheta = Math.atan2(directionY, directionX); 
            let r = this.radiusAtPointInDirection(hull1, avgIntersection, targetTheta);
            targetTheta = (targetTheta + Math.PI) % (2 * Math.PI);
            r += this.radiusAtPointInDirection(hull2, avgIntersection, targetTheta);

            let r2 = this.radiusAtPointInDirection(hull1, avgIntersection, targetTheta);
            targetTheta = (targetTheta + Math.PI) % (2 * Math.PI);
            r2 += this.radiusAtPointInDirection(hull2, avgIntersection, targetTheta);

            if (r2 < r) {
                r = r2;
                directionX *= -1;
                directionY *= -1;
            }


            let mag = Math.sqrt(directionX*directionX + directionY*directionY);
            directionX *= -r / mag;
            directionY *= -r / mag;
            result.overlapX = directionX;
            result.overlapY = directionY;
        }
        return result;
    }

    /**
     * Casts a ray from p in the direction targetTheta and intersects it with the hull ch,
     * returns the distance from p to the surface of ch.
     * @param {list} ch - the convex hull to intersect a ray with
     * @param {object} p - the point of origin of the ray
     * @param {number} targetTheta - the direction of the ray
     * @returns {number} the distance to the surface of the convex hull from the point in the direction theta
     */
    radiusAtPointInDirection(ch, p, targetTheta) {
        let minThetaDiff = Infinity;
        let index;
        for (let i = 0; i < ch.length; i++) {
            let theta = Math.atan2(ch[i][1] - p.y, ch[i][0] - p.x);
            let thetaDiff = ((targetTheta - theta) % (2 * Math.PI) + 2 * Math.PI) % (2 * Math.PI); //positive mod
            if (thetaDiff < minThetaDiff) {
                minThetaDiff = thetaDiff;
                index = i;
            }
        }
        let a = ch[index];
        let b = ch[(index + 1) % ch.length];
        let c = [p.x, p.y];
        let d = [p.x + 100 * Math.cos(targetTheta), p.y + 100 * Math.sin(targetTheta)];
        //Use parametric line intersection
        //<x,y> = a + (b - a)t1
        //<x,y> = c + (d - c)t2
        //a + (b - a)t1 = c + (d - c)t2
        //t1 = (c.x + (d.x - c.x)t2 - a.x) / (b.x - a.x)
        //a.y + (b.y - a.y) * (c.x + (d.x - c.x)t2 - a.x) / (b.x - a.x) = c.y + (d.y - c.y)t2
        //t2((b.y - a.y)(d.x - c.x)/(b.x - a.x) - (d.y - c.y)) = c.y - a.y - (b.y - a.y)*(c.x - a.x)/(b.x - a.x)
        //t2 = (c.y - a.y - (b.y - a.y)*(c.x - a.x)/(b.x - a.x))  /  ((b.y - a.y)(d.x - c.x)/(b.x - a.x) - (d.y - c.y))
        let t2 = (c[1] - a[1] - (b[1] - a[1]) * (c[0] - a[0]) / (b[0] - a[0]))  /  ((b[1] - a[1]) * (d[0] - c[0]) / (b[0] - a[0]) - d[1] + c[1]);
        let t1 = (c[0] + (d[0] - c[0]) * t2 - a[0]) / (b[0] - a[0]);
        return Math.hypot(a[0] + (b[0] - a[0])*t1 - p.x, a[1] + (b[1] - a[1]) * t1 - p.y);
    }

    /**
     * Perform hit test with other clip.
     * @param {Wick.Clip} other - the clip to hit test with
     * @param {object} options - Hit test options
     * @returns {object} Hit information
     */
    hits(other, options) {
        // Get hit options
        let finalOptions = {...this.project.hitTestOptions};
        if (options) {
            if (options.mode === 'CIRCLE' || options.mode === 'RECTANGLE' || options.mode === 'CONVEX') {
                finalOptions.mode = options.mode;
            }
            if (typeof options.offset === "boolean") {
                finalOptions.offset = options.offset;
            }
            if (typeof options.overlap === "boolean") {
                finalOptions.overlap = options.overlap;
            }
            if (typeof options.intersections === "boolean") {
                finalOptions.intersections = options.intersections;
            }
            if (options.radius) {
                finalOptions.radius = options.radius;
            }
        }

        if (finalOptions.mode === 'CIRCLE') {
            return this.circleHits(other, finalOptions);
        }
        else if (finalOptions.mode === 'CONVEX') {
            return this.convexHits(other, finalOptions);
        }
        else {
            return this.rectangleHits(other, finalOptions);
        }
    }

    /**
     * Returns true if this clip collides with another clip.
     * @param {Wick.Clip} other - The other clip to check collision with.
     * @returns {boolean} True if this clip collides the other clip.
     */
    hitTest(other) {
        // TODO: write intersects so we don't rely on paper Rectangle objects
        return this.absoluteBounds.intersects(other.absoluteBounds);
    }

    /**
     * The bounding box of the clip.
     * @type {object}
     */
    get bounds() {
        // TODO: Refactor so that getting bounds does not rely on the view
        return this.view.bounds;
    }

    get absoluteBounds() {
        // TODO: Refactor so that getting bounds does not rely on the view
        return this.view.absoluteBounds;
    }

    get points () {
        // TODO: Refactor so that does not rely on the view
        return this.view.points;
    }

    get radius () {
        // Use length of half diagonal of bounding box
        let b = this.absoluteBounds;
        return Math.sqrt(b.width*b.width + b.height*b.height)/2/Math.sqrt(2);
        
        // Alternative: use largest distance from center to a point on the object
        /*
        let center = this.absoluteBounds.center;
        let points = this.points;
        let max_r = 0;
        for (let p = 0; p < points.length; p++) {
            let point = points[p];
            let x = point[0] - center.x;
            let y = point[1] - center.y;
            max_r = Math.max(max_r, x*x + y*y);
        }

        return Math.sqrt(max_r);
        */
    }

    // Gives clockwise in screen space, which is ccw in regular axes
    get convexHull () {
        let points = this.points;

        // Infinity gets us the convex hull
        let ch = hull(points, Infinity);

        let removedDuplicates = [];
        let epsilon = 0.01;
        for (let i = 0; i < ch.length; i++) {
            if (removedDuplicates.length > 0) {
                if ((Math.abs(ch[i][0] - removedDuplicates[removedDuplicates.length - 1][0]) > epsilon ||
                    Math.abs(ch[i][1] - removedDuplicates[removedDuplicates.length - 1][1]) > epsilon) && 
                    (Math.abs(ch[i][0] - removedDuplicates[0][0]) > epsilon ||
                    Math.abs(ch[i][1] - removedDuplicates[0][1]) > epsilon)) {
                    removedDuplicates.push(ch[i]);
                }
            }
            else {
                removedDuplicates.push(ch[i]);
            }
        }

        return removedDuplicates;
    }

    /**
     * The X position of the clip.
     * @type {number}
     */
    get x() {
        return this.transformation.x;
    }

    set x(x) {
        this.transformation.x = x;
    }

    /**
     * The Y position of the clip.
     * @type {number}
     */
    get y() {
        return this.transformation.y;
    }

    set y(y) {
        this.transformation.y = y;
    }

    /**
     * The X scale of the clip.
     * @type {number}
     */
    get scaleX() {
        return this.transformation.scaleX;
    }

    set scaleX(scaleX) {
        if (scaleX === 0) scaleX = 0.001; // Protects against NaN issues
        this.transformation.scaleX = scaleX;
    }

    /**
     * The Y scale of the clip.
     * @type {number}
     */
    get scaleY() {
        return this.transformation.scaleY;
    }

    set scaleY(scaleY) {
        if (scaleY === 0) scaleY = 0.001; // Protects against NaN issues
        this.transformation.scaleY = scaleY;
    }

    /**
     * The width of the clip.
     * @type {number}
     */
    get width() {
        return this.isRoot ? this.project.width : this.bounds.width * this.scaleX;
    }

    set width(width) {
        this.scaleX = width / this.width * this.scaleX;
    }

    /**
     * The height of the clip.
     * @type {number}
     */
    get height() {
        return this.isRoot ? this.project.height : this.bounds.height * this.scaleY;
    }

    set height(height) {
        this.scaleY = height / this.height * this.scaleY;
    }

    /**
     * The rotation of the clip.
     * @type {number}
     */
    get rotation() {
        return this.transformation.rotation;
    }

    set rotation(rotation) {
        this.transformation.rotation = rotation;
    }

    /**
     * The opacity of the clip.
     * @type {number}
     */
    get opacity() {
        return this.transformation.opacity;
    }

    set opacity(opacity) {
        opacity = Math.min(1, opacity);
        opacity = Math.max(0, opacity);
        this.transformation.opacity = opacity;
    }

    /**
     * Copy this clip, and add the copy to the same frame as the original clip.
     * @returns {Wick.Clip} the result of the clone.
     */
    clone() {
        var clone = this.copy();
        clone.identifier = null;
        this.parentFrame.addClip(clone);
        this._clones.push(clone);
        clone._isClone = true;
        clone._sourceClipUUID = this.uuid;
        return clone;
    }

    /**
     * An array containing all objects that were created by calling clone() on this Clip.
     * @type {Wick.Clip[]}
     */
    get clones() {
        return this._clones;
    }

    /**
     * This is a stopgap to prevent users from using setText with a Clip.
     */
    setText () {
        throw new Error('setText() can only be used with text objects.');
    }

    /**
     * The list of parents, grandparents, grand-grandparents...etc of the clip.
     * @returns {Wick.Clip[]} Array of all parents
     */
    get lineage() {
        if (this.isRoot) {
            return [this];
        } else {
            return [this].concat(this.parentClip.lineage);
        }
    }

    /**
     * Add a placeholder path to this clip to ensure the Clip is always selectable when rendered.
     */
    ensureActiveFrameIsContentful () {
        // Ensure layer exists
        var firstLayerExists = this.timeline.activeLayer;
        if(!firstLayerExists) {
            this.timeline.addLayer(new Wick.Layer());
        }

        // Ensure active frame exists
        var playheadPosition = this.timeline.playheadPosition;
        var activeFrameExists = this.timeline.getFramesAtPlayheadPosition(playheadPosition).length > 0;
        if(!activeFrameExists) {
            this.timeline.activeLayer.addFrame(new Wick.Frame({start:playheadPosition}));
        }

        // Clear placeholders
        var frame = this.timeline.getFramesAtPlayheadPosition(playheadPosition)[0];
        frame.paths.forEach(path => {
            if(!path.isPlaceholder) return;
            path.remove();
        });

        // Check if active frame is contentful
        var firstFramesAreContentful = false;
        this.timeline.getFramesAtPlayheadPosition(playheadPosition).forEach(frame => {
            if(frame.contentful) {
                firstFramesAreContentful = true;
            }
        });

        // Ensure active frame is contentful
        if(!firstFramesAreContentful) {
            // Clear placeholders
            var frame = this.timeline.getFramesAtPlayheadPosition(playheadPosition)[0];
            frame.paths.forEach(path => {
                path.remove();
            });

            // Generate crosshair
            var size = Wick.View.Clip.PLACEHOLDER_SIZE;
            var line1 = new paper.Path.Line({
                from: [0,-size],
                to: [0,size],
                strokeColor: '#AAA',
            });
            line1.remove();
            frame.addPath(new Wick.Path({path: line1, isPlaceholder: true}));

            var line2 = new paper.Path.Line({
                from: [-size,0],
                to: [size,0],
                strokeColor: '#AAA',
            });
            line2.remove();
            frame.addPath(new Wick.Path({path: line2, isPlaceholder: true}));
        }
    }

    _onInactive () {
        super._onInactive();
        this._tickChildren();
    }

    _onActivated() {
        super._onActivated();
        this._tickChildren();

        if (this.animationType === 'playOnce') {
            this.playedOnce = false;
            this.timeline.playheadPosition = 1;
        }

    }

    _onActive() {
        super._onActive();

        if (this.animationType === 'loop') {
            this.timeline.advance();
        } else if (this.animationType === 'single') {
            this.timeline.playheadPosition = this.singleFrameNumber;
        } else if (this.animationType === 'playOnce') {
            if (!this.playedOnce) {
                if (this.timeline.playheadPosition === this.timeline.length) {
                    this.playedOnce = true;
                } else {
                    this.timeline.advance();
                }
            }
        } 
        
        if (this.isSynced) {
            this.timeline.playheadPosition = this.syncFrame;
        }

        this._tickChildren();
    }

    _onDeactivated() {
        super._onDeactivated();
        this._tickChildren();
    }

    _tickChildren() {
        var childError = null;
        this.timeline.frames.forEach(frame => {
            if (childError) return;
            childError = frame.tick();
        });
        return childError;
    }

    _attachChildClipReferences() {
        this.timeline.activeFrames.forEach(frame => {
            frame.clips.forEach(clip => {
                if (clip.identifier) {
                    this[clip.identifier] = clip;
                    clip._attachChildClipReferences();
                }
            });

            // Dynamic text paths can be accessed by their identifiers.
            frame.dynamicTextPaths.forEach(path => {
                this[path.identifier] = path;
            })
        })
    }
}