<commit_before>/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

import {CameraState} from "../model/CameraState";

// Constant: the Sozi namespace
var SVG_NS = "http://www.w3.org/2000/svg";

export var Camera = Object.create(CameraState);

Camera.init = function (viewport, layer) {
    CameraState.init.call(this, viewport.svgRoot);

    this.viewport = viewport;
    this.layer = layer;
    this.selected = true;

    // The clipping rectangle of this camera
    this.svgClipRect = document.createElementNS(SVG_NS, "rect");

    if (viewport.editMode) {
        // In edit mode, we set up a mask outside the clipping rectangle.
        // This value allows to set the opacity of the mask.
        this.maskValue = 0;

        var svgMask = document.createElementNS(SVG_NS, "mask");
        var svgMaskId = viewport.makeUniqueId("sozi-mask-");
        svgMask.setAttribute("id", svgMaskId);
        viewport.svgRoot.appendChild(svgMask);

        this.svgMaskRect = document.createElementNS(SVG_NS, "rect");
        svgMask.appendChild(this.svgMaskRect);

        this.svgClipRect.setAttribute("fill", "white");
        svgMask.appendChild(this.svgClipRect);

        // We also define two rectangles that will show the outline
        // of the clipped region in alternating white and black dashes.
        this.svgClipOutlineRect1 = document.createElementNS(SVG_NS, "rect");
        this.svgClipOutlineRect1.setAttribute("stroke", "black");
        this.svgClipOutlineRect1.setAttribute("fill", "none");
        viewport.svgRoot.appendChild(this.svgClipOutlineRect1);

        this.svgClipOutlineRect2 = document.createElementNS(SVG_NS, "rect");
        this.svgClipOutlineRect2.setAttribute("stroke", "white");
        this.svgClipOutlineRect2.setAttribute("fill", "none");
        this.svgClipOutlineRect2.setAttribute("stroke-dasharray", "2,2");
        viewport.svgRoot.appendChild(this.svgClipOutlineRect2);

        this.concealClipping();
    }
    else {
        // When playing the presentation, we use the default SVG
        // clipping technique.
        var svgClipPath = document.createElementNS(SVG_NS, "clipPath");
        var svgClipPathId = viewport.makeUniqueId("sozi-clip-path-");
        svgClipPath.setAttribute("id", svgClipPathId);
        svgClipPath.appendChild(this.svgClipRect);
        viewport.svgRoot.appendChild(svgClipPath);
    }

    // The groups that will support transformations
    this.svgTransformGroups = layer.svgNodes.map(svgNode => {
        // The group that will support the clipping operation
        var svgClippedGroup = document.createElementNS(SVG_NS, "g");
        viewport.svgRoot.insertBefore(svgClippedGroup, svgNode);

        if (viewport.editMode) {
            svgClippedGroup.setAttribute("mask", "url(#" + svgMaskId + ")");
        }
        else {
            svgClippedGroup.setAttribute("clip-path", "url(#" + svgClipPathId + ")");
        }

        var svgGroup = document.createElementNS(SVG_NS, "g");
        svgGroup.appendChild(svgNode);
        svgClippedGroup.appendChild(svgGroup);
        return svgGroup;
    });

    return this;
};

Camera.revealClipping = function () {
    this.maskValue = 64;
    this.svgClipOutlineRect1.style.display = "inline";
    this.svgClipOutlineRect2.style.display = "inline";
};

Camera.concealClipping = function () {
    this.maskValue = 0;
    this.svgClipOutlineRect1.style.display = "none";
    this.svgClipOutlineRect2.style.display = "none";
};

Object.defineProperty(Camera, "scale", {
    get() {
        return Math.min(this.viewport.width / this.width, this.viewport.height / this.height);
    }
});

Camera.rotate = function (angle) {
    this.restoreAspectRatio();
    return this.setAngle(this.angle + angle).update();
};

Camera.zoom = function (factor, x, y) {
    this.width /= factor;
    this.height /= factor;
    this.restoreAspectRatio();
    return this.translate(
        (1 - factor) * (x - this.viewport.width  / 2),
        (1 - factor) * (y - this.viewport.height / 2)
    );
};

Camera.translate = function (deltaX, deltaY) {
    var scale = this.scale;
    var angleRad = this.angle * Math.PI / 180;
    var si = Math.sin(angleRad);
    var co = Math.cos(angleRad);
    this.cx -= (deltaX * co - deltaY * si) / scale;
    this.cy -= (deltaX * si + deltaY * co) / scale;
    this.restoreAspectRatio();
    return this.update();
};

Camera.clip = function (x0, y0, x1, y1) {
    this.clipped = true;
    var scale = this.scale;
    var clipWidth = Math.abs(x1 - x0) + 1;
    var clipHeight = Math.abs(y1 - y0) + 1;
    this.clipXOffset = (Math.min(x0, x1) - (this.viewport.width  - clipWidth)  / 2) * this.width  / clipWidth;
    this.clipYOffset = (Math.min(y0, y1) - (this.viewport.height - clipHeight) / 2) * this.height / clipHeight;
    this.clipWidthFactor  = clipWidth  / this.width  / scale;
    this.clipHeightFactor = clipHeight / this.height / scale;
    return this.update();
};

Camera.restoreAspectRatio = function () {
    var viewportRatio = this.viewport.width / this.viewport.height;
    var camRatio = this.width / this.height;
    var ratio = viewportRatio / camRatio;
    if (ratio > 1) {
        this.width *= ratio;
        if (this.clipped) {
            this.clipWidthFactor /= ratio;
        }
    }
    else {
        this.height /= ratio;
        if (this.clipped) {
            this.clipHeightFactor *= ratio;
        }
    }
};

Camera.getCandidateReferenceElement = function () {
    // getIntersectionList is not supported in Gecko
    if (!this.svgRoot.getIntersectionList || !this.layer.svgNodes.length) {
        return this.svgRoot;
    }

    // Get all elements that intersect with the viewport.
    var viewportRect = this.svgRoot.createSVGRect();
    viewportRect.x = 0;
    viewportRect.y = 0;
    viewportRect.width = this.viewport.width;
    viewportRect.height = this.viewport.height;
    var viewportArea = this.viewport.width * this.viewport.height;

    var intersectionList = this.svgRoot.getIntersectionList(viewportRect, this.layer.svgNodes[0]);

    // Find the element which bounding box best fits in the viewport.
    var bestScore = -1;
    var result;

    for (var i = 0; i < intersectionList.length; i ++) {
        var elt = intersectionList[i];
        if (elt.hasAttribute("id")) {
            // TODO getBoundingClientRect returns bounding box of bounding box
            var eltRect = elt.getBoundingClientRect();
            var eltArea = eltRect.width * eltRect.height;

            // Compute the intersection of the element'b bounding
            // box with the current viewport.
            var l = Math.max(eltRect.left, this.viewport.x);
            var t = Math.max(eltRect.top, this.viewport.y);
            var r = Math.min(eltRect.right, this.viewport.x + this.viewport.width);
            var b = Math.min(eltRect.bottom, this.viewport.y + this.viewport.height);

            var intersectArea = (r - l) * (b - t);

            // An element is selected if it has the biggest intersect area
            // and the smallest area outside the intersection.
            var eltScore = viewportArea + eltArea - 2 * intersectArea;
            if (bestScore < 0 || eltScore < bestScore) {
                bestScore = eltScore;
                result = elt;
            }
        }
    }

    return result;
};

Object.defineProperty(Camera, "clipRect", {
    get: function () {
        var width, height, x, y;
        if (this.clipped) {
            var scale = this.scale;
            width = Math.round(this.width  * this.clipWidthFactor  * scale);
            height = Math.round(this.height * this.clipHeightFactor * scale);
            x = Math.round((this.viewport.width  - width)  / 2 + this.clipXOffset * this.clipWidthFactor  * scale);
            y = Math.round((this.viewport.height - height) / 2 + this.clipYOffset * this.clipHeightFactor * scale);
        }
        else {
            width = this.viewport.width;
            height = this.viewport.height;
            x = 0;
            y = 0;
        }
        return {width, height, x, y};
    }
});

Camera.update = function () {
    // Adjust the location and size of the clipping rectangle
    var rect = this.clipRect;
    this.svgClipRect.setAttribute("x", rect.x);
    this.svgClipRect.setAttribute("y", rect.y);
    this.svgClipRect.setAttribute("width",  rect.width);
    this.svgClipRect.setAttribute("height", rect.height);

    if (this.viewport.editMode) {
        this.svgMaskRect.setAttribute("fill", "rgb(" + this.maskValue + "," + this.maskValue + "," + this.maskValue + ")");
        this.svgMaskRect.setAttribute("x", 0);
        this.svgMaskRect.setAttribute("y", 0);
        this.svgMaskRect.setAttribute("width",  this.viewport.width);
        this.svgMaskRect.setAttribute("height", this.viewport.height);

        this.svgClipOutlineRect1.setAttribute("x", rect.x);
        this.svgClipOutlineRect1.setAttribute("y", rect.y);
        this.svgClipOutlineRect1.setAttribute("width",  rect.width);
        this.svgClipOutlineRect1.setAttribute("height", rect.height);

        this.svgClipOutlineRect2.setAttribute("x", rect.x);
        this.svgClipOutlineRect2.setAttribute("y", rect.y);
        this.svgClipOutlineRect2.setAttribute("width",  rect.width);
        this.svgClipOutlineRect2.setAttribute("height", rect.height);
    }

    // Compute and apply the geometrical transformation to the layer group
    var scale = this.scale;
    var translateX = this.viewport.width  / scale / 2 - this.cx;
    var translateY = this.viewport.height / scale / 2 - this.cy;

    this.svgTransformGroups.forEach(svgGroup => {
        svgGroup.setAttribute("transform",
            "scale(" + scale + ")" +
            "translate(" + translateX + "," + translateY + ")" +
            "rotate(" + (-this.angle) + ',' + this.cx + "," + this.cy + ")"
        );
        svgGroup.setAttribute("opacity", this.opacity);
    });

    return this;
};

Camera.interpolate = function (initialState, finalState, progress, timingFunction, relativeZoom, svgPath, reversePath) {
    var tfProgress = timingFunction(progress);
    var tfRemaining = 1 - tfProgress;

    function linear(initial, final) {
        return final * tfProgress + initial * tfRemaining;
    }

    function quadratic(u0, u1) {
        var um = (relativeZoom > 0 ? Math.max(u0, u1) : Math.min(u0, u1)) * (1 + relativeZoom);
        var du0 = u0 - um;
        var du1 = u1 - um;
        var r = Math.sqrt(du0 / du1);
        var tm = r / (1 + r);
        var k = du0 / tm / tm;
        var dt = progress - tm;
        return k * dt * dt + um;
    }

    // Interpolate camera width and height
    if (relativeZoom) {
        this.width  = quadratic(initialState.width,  finalState.width);
        this.height = quadratic(initialState.height, finalState.height);
    }
    else {
        this.width  = linear(initialState.width,  finalState.width);
        this.height = linear(initialState.height, finalState.height);
    }

    // Interpolate camera location
    if (svgPath) {
        var pathLength   = svgPath.getTotalLength();
        var startPoint   = svgPath.getPointAtLength(reversePath ? pathLength : 0);
        var endPoint     = svgPath.getPointAtLength(reversePath ? 0 : pathLength);
        var currentPoint = svgPath.getPointAtLength(pathLength * (reversePath ? tfRemaining : tfProgress));

        this.cx = currentPoint.x + linear(initialState.cx - startPoint.x, finalState.cx - endPoint.x);
        this.cy = currentPoint.y + linear(initialState.cy - startPoint.y, finalState.cy - endPoint.y);
    }
    else {
        this.cx = linear(initialState.cx, finalState.cx);
        this.cy = linear(initialState.cy, finalState.cy);
    }

    // Interpolate opacity
    this.opacity = linear(initialState.opacity, finalState.opacity);

    // Interpolate camera angle
    // Keep the smallest angle between the initial and final states
    if (finalState.angle - initialState.angle > 180) {
        this.angle = linear(initialState.angle, finalState.angle - 360);
    }
    else if (finalState.angle - initialState.angle < -180) {
        this.angle = linear(initialState.angle - 360, finalState.angle);
    }
    else {
        this.angle = linear(initialState.angle, finalState.angle);
    }

    // Interpolate clip rectangle
    this.clipped = true;
    var scale = this.scale;
    var clipDefaults = {
        clipXOffset: 0,
        clipYOffset: 0,
        clipWidthFactor:  this.viewport.width  / this.width  / scale,
        clipHeightFactor: this.viewport.height / this.height / scale
    };
    var initialClipping = initialState.clipped ? initialState : clipDefaults;
    var finalClipping   = finalState.clipped   ? finalState   : clipDefaults;
    for (var clipProp in clipDefaults) {
        this[clipProp] = linear(initialClipping[clipProp], finalClipping[clipProp]);
    }
};
<commit_msg>Invert/revert relative zoom (Fix #334)<commit_after>/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

import {CameraState} from "../model/CameraState";

// Constant: the Sozi namespace
var SVG_NS = "http://www.w3.org/2000/svg";

export var Camera = Object.create(CameraState);

Camera.init = function (viewport, layer) {
    CameraState.init.call(this, viewport.svgRoot);

    this.viewport = viewport;
    this.layer = layer;
    this.selected = true;

    // The clipping rectangle of this camera
    this.svgClipRect = document.createElementNS(SVG_NS, "rect");

    if (viewport.editMode) {
        // In edit mode, we set up a mask outside the clipping rectangle.
        // This value allows to set the opacity of the mask.
        this.maskValue = 0;

        var svgMask = document.createElementNS(SVG_NS, "mask");
        var svgMaskId = viewport.makeUniqueId("sozi-mask-");
        svgMask.setAttribute("id", svgMaskId);
        viewport.svgRoot.appendChild(svgMask);

        this.svgMaskRect = document.createElementNS(SVG_NS, "rect");
        svgMask.appendChild(this.svgMaskRect);

        this.svgClipRect.setAttribute("fill", "white");
        svgMask.appendChild(this.svgClipRect);

        // We also define two rectangles that will show the outline
        // of the clipped region in alternating white and black dashes.
        this.svgClipOutlineRect1 = document.createElementNS(SVG_NS, "rect");
        this.svgClipOutlineRect1.setAttribute("stroke", "black");
        this.svgClipOutlineRect1.setAttribute("fill", "none");
        viewport.svgRoot.appendChild(this.svgClipOutlineRect1);

        this.svgClipOutlineRect2 = document.createElementNS(SVG_NS, "rect");
        this.svgClipOutlineRect2.setAttribute("stroke", "white");
        this.svgClipOutlineRect2.setAttribute("fill", "none");
        this.svgClipOutlineRect2.setAttribute("stroke-dasharray", "2,2");
        viewport.svgRoot.appendChild(this.svgClipOutlineRect2);

        this.concealClipping();
    }
    else {
        // When playing the presentation, we use the default SVG
        // clipping technique.
        var svgClipPath = document.createElementNS(SVG_NS, "clipPath");
        var svgClipPathId = viewport.makeUniqueId("sozi-clip-path-");
        svgClipPath.setAttribute("id", svgClipPathId);
        svgClipPath.appendChild(this.svgClipRect);
        viewport.svgRoot.appendChild(svgClipPath);
    }

    // The groups that will support transformations
    this.svgTransformGroups = layer.svgNodes.map(svgNode => {
        // The group that will support the clipping operation
        var svgClippedGroup = document.createElementNS(SVG_NS, "g");
        viewport.svgRoot.insertBefore(svgClippedGroup, svgNode);

        if (viewport.editMode) {
            svgClippedGroup.setAttribute("mask", "url(#" + svgMaskId + ")");
        }
        else {
            svgClippedGroup.setAttribute("clip-path", "url(#" + svgClipPathId + ")");
        }

        var svgGroup = document.createElementNS(SVG_NS, "g");
        svgGroup.appendChild(svgNode);
        svgClippedGroup.appendChild(svgGroup);
        return svgGroup;
    });

    return this;
};

Camera.revealClipping = function () {
    this.maskValue = 64;
    this.svgClipOutlineRect1.style.display = "inline";
    this.svgClipOutlineRect2.style.display = "inline";
};

Camera.concealClipping = function () {
    this.maskValue = 0;
    this.svgClipOutlineRect1.style.display = "none";
    this.svgClipOutlineRect2.style.display = "none";
};

Object.defineProperty(Camera, "scale", {
    get() {
        return Math.min(this.viewport.width / this.width, this.viewport.height / this.height);
    }
});

Camera.rotate = function (angle) {
    this.restoreAspectRatio();
    return this.setAngle(this.angle + angle).update();
};

Camera.zoom = function (factor, x, y) {
    this.width /= factor;
    this.height /= factor;
    this.restoreAspectRatio();
    return this.translate(
        (1 - factor) * (x - this.viewport.width  / 2),
        (1 - factor) * (y - this.viewport.height / 2)
    );
};

Camera.translate = function (deltaX, deltaY) {
    var scale = this.scale;
    var angleRad = this.angle * Math.PI / 180;
    var si = Math.sin(angleRad);
    var co = Math.cos(angleRad);
    this.cx -= (deltaX * co - deltaY * si) / scale;
    this.cy -= (deltaX * si + deltaY * co) / scale;
    this.restoreAspectRatio();
    return this.update();
};

Camera.clip = function (x0, y0, x1, y1) {
    this.clipped = true;
    var scale = this.scale;
    var clipWidth = Math.abs(x1 - x0) + 1;
    var clipHeight = Math.abs(y1 - y0) + 1;
    this.clipXOffset = (Math.min(x0, x1) - (this.viewport.width  - clipWidth)  / 2) * this.width  / clipWidth;
    this.clipYOffset = (Math.min(y0, y1) - (this.viewport.height - clipHeight) / 2) * this.height / clipHeight;
    this.clipWidthFactor  = clipWidth  / this.width  / scale;
    this.clipHeightFactor = clipHeight / this.height / scale;
    return this.update();
};

Camera.restoreAspectRatio = function () {
    var viewportRatio = this.viewport.width / this.viewport.height;
    var camRatio = this.width / this.height;
    var ratio = viewportRatio / camRatio;
    if (ratio > 1) {
        this.width *= ratio;
        if (this.clipped) {
            this.clipWidthFactor /= ratio;
        }
    }
    else {
        this.height /= ratio;
        if (this.clipped) {
            this.clipHeightFactor *= ratio;
        }
    }
};

Camera.getCandidateReferenceElement = function () {
    // getIntersectionList is not supported in Gecko
    if (!this.svgRoot.getIntersectionList || !this.layer.svgNodes.length) {
        return this.svgRoot;
    }

    // Get all elements that intersect with the viewport.
    var viewportRect = this.svgRoot.createSVGRect();
    viewportRect.x = 0;
    viewportRect.y = 0;
    viewportRect.width = this.viewport.width;
    viewportRect.height = this.viewport.height;
    var viewportArea = this.viewport.width * this.viewport.height;

    var intersectionList = this.svgRoot.getIntersectionList(viewportRect, this.layer.svgNodes[0]);

    // Find the element which bounding box best fits in the viewport.
    var bestScore = -1;
    var result;

    for (var i = 0; i < intersectionList.length; i ++) {
        var elt = intersectionList[i];
        if (elt.hasAttribute("id")) {
            // TODO getBoundingClientRect returns bounding box of bounding box
            var eltRect = elt.getBoundingClientRect();
            var eltArea = eltRect.width * eltRect.height;

            // Compute the intersection of the element'b bounding
            // box with the current viewport.
            var l = Math.max(eltRect.left, this.viewport.x);
            var t = Math.max(eltRect.top, this.viewport.y);
            var r = Math.min(eltRect.right, this.viewport.x + this.viewport.width);
            var b = Math.min(eltRect.bottom, this.viewport.y + this.viewport.height);

            var intersectArea = (r - l) * (b - t);

            // An element is selected if it has the biggest intersect area
            // and the smallest area outside the intersection.
            var eltScore = viewportArea + eltArea - 2 * intersectArea;
            if (bestScore < 0 || eltScore < bestScore) {
                bestScore = eltScore;
                result = elt;
            }
        }
    }

    return result;
};

Object.defineProperty(Camera, "clipRect", {
    get: function () {
        var width, height, x, y;
        if (this.clipped) {
            var scale = this.scale;
            width = Math.round(this.width  * this.clipWidthFactor  * scale);
            height = Math.round(this.height * this.clipHeightFactor * scale);
            x = Math.round((this.viewport.width  - width)  / 2 + this.clipXOffset * this.clipWidthFactor  * scale);
            y = Math.round((this.viewport.height - height) / 2 + this.clipYOffset * this.clipHeightFactor * scale);
        }
        else {
            width = this.viewport.width;
            height = this.viewport.height;
            x = 0;
            y = 0;
        }
        return {width, height, x, y};
    }
});

Camera.update = function () {
    // Adjust the location and size of the clipping rectangle
    var rect = this.clipRect;
    this.svgClipRect.setAttribute("x", rect.x);
    this.svgClipRect.setAttribute("y", rect.y);
    this.svgClipRect.setAttribute("width",  rect.width);
    this.svgClipRect.setAttribute("height", rect.height);

    if (this.viewport.editMode) {
        this.svgMaskRect.setAttribute("fill", "rgb(" + this.maskValue + "," + this.maskValue + "," + this.maskValue + ")");
        this.svgMaskRect.setAttribute("x", 0);
        this.svgMaskRect.setAttribute("y", 0);
        this.svgMaskRect.setAttribute("width",  this.viewport.width);
        this.svgMaskRect.setAttribute("height", this.viewport.height);

        this.svgClipOutlineRect1.setAttribute("x", rect.x);
        this.svgClipOutlineRect1.setAttribute("y", rect.y);
        this.svgClipOutlineRect1.setAttribute("width",  rect.width);
        this.svgClipOutlineRect1.setAttribute("height", rect.height);

        this.svgClipOutlineRect2.setAttribute("x", rect.x);
        this.svgClipOutlineRect2.setAttribute("y", rect.y);
        this.svgClipOutlineRect2.setAttribute("width",  rect.width);
        this.svgClipOutlineRect2.setAttribute("height", rect.height);
    }

    // Compute and apply the geometrical transformation to the layer group
    var scale = this.scale;
    var translateX = this.viewport.width  / scale / 2 - this.cx;
    var translateY = this.viewport.height / scale / 2 - this.cy;

    this.svgTransformGroups.forEach(svgGroup => {
        svgGroup.setAttribute("transform",
            "scale(" + scale + ")" +
            "translate(" + translateX + "," + translateY + ")" +
            "rotate(" + (-this.angle) + ',' + this.cx + "," + this.cy + ")"
        );
        svgGroup.setAttribute("opacity", this.opacity);
    });

    return this;
};

Camera.interpolate = function (initialState, finalState, progress, timingFunction, relativeZoom, svgPath, reversePath) {
    var tfProgress = timingFunction(progress);
    var tfRemaining = 1 - tfProgress;

    function linear(initial, final) {
        return final * tfProgress + initial * tfRemaining;
    }

    function quadratic(u0, u1) {
        var um = (relativeZoom > 0 ? Math.max(u0, u1) : Math.min(u0, u1)) * (1 - relativeZoom);
        var du0 = u0 - um;
        var du1 = u1 - um;
        var r = Math.sqrt(du0 / du1);
        var tm = r / (1 + r);
        var k = du0 / tm / tm;
        var dt = progress - tm;
        return k * dt * dt + um;
    }

    // Interpolate camera width and height
    if (relativeZoom) {
        this.width  = quadratic(initialState.width,  finalState.width);
        this.height = quadratic(initialState.height, finalState.height);
    }
    else {
        this.width  = linear(initialState.width,  finalState.width);
        this.height = linear(initialState.height, finalState.height);
    }

    // Interpolate camera location
    if (svgPath) {
        var pathLength   = svgPath.getTotalLength();
        var startPoint   = svgPath.getPointAtLength(reversePath ? pathLength : 0);
        var endPoint     = svgPath.getPointAtLength(reversePath ? 0 : pathLength);
        var currentPoint = svgPath.getPointAtLength(pathLength * (reversePath ? tfRemaining : tfProgress));

        this.cx = currentPoint.x + linear(initialState.cx - startPoint.x, finalState.cx - endPoint.x);
        this.cy = currentPoint.y + linear(initialState.cy - startPoint.y, finalState.cy - endPoint.y);
    }
    else {
        this.cx = linear(initialState.cx, finalState.cx);
        this.cy = linear(initialState.cy, finalState.cy);
    }

    // Interpolate opacity
    this.opacity = linear(initialState.opacity, finalState.opacity);

    // Interpolate camera angle
    // Keep the smallest angle between the initial and final states
    if (finalState.angle - initialState.angle > 180) {
        this.angle = linear(initialState.angle, finalState.angle - 360);
    }
    else if (finalState.angle - initialState.angle < -180) {
        this.angle = linear(initialState.angle - 360, finalState.angle);
    }
    else {
        this.angle = linear(initialState.angle, finalState.angle);
    }

    // Interpolate clip rectangle
    this.clipped = true;
    var scale = this.scale;
    var clipDefaults = {
        clipXOffset: 0,
        clipYOffset: 0,
        clipWidthFactor:  this.viewport.width  / this.width  / scale,
        clipHeightFactor: this.viewport.height / this.height / scale
    };
    var initialClipping = initialState.clipped ? initialState : clipDefaults;
    var finalClipping   = finalState.clipped   ? finalState   : clipDefaults;
    for (var clipProp in clipDefaults) {
        this[clipProp] = linear(initialClipping[clipProp], finalClipping[clipProp]);
    }
};
<|endoftext|>


<commit_before>import ansiHTML from 'ansi-html'
import serialize from 'serialize-javascript'
import generateETag from 'etag'
import fresh from 'fresh'
import Tapable from 'tappable'
import pify from 'pify'
import serveStatic from 'serve-static'
import compression from 'compression'
import _ from 'lodash'
import { join, resolve } from 'path'
import fs from 'fs-extra'
import { createBundleRenderer } from 'vue-server-renderer'
import { encodeHtml, getContext, setAnsiColors, isUrl } from 'utils'
import Debug from 'debug'
import connect from 'connect'

const debug = Debug('nuxt:render')
debug.color = 4 // Force blue color

setAnsiColors(ansiHTML)

let jsdom = null

const parseTemplate = templateStr => _.template(templateStr, {
  interpolate: /{{([\s\S]+?)}}/g
})

const resourceMap = [
  {
    key: 'clientManifest',
    fileName: 'vue-ssr-client-manifest.json',
    transform: JSON.parse
  },
  {
    key: 'serverBundle',
    fileName: 'server-bundle.json',
    transform: JSON.parse
  },
  {
    key: 'appTemplate',
    fileName: 'index.html',
    transform: parseTemplate
  }
]

// Protector utility against request to SSR bundle files
const ssrResourceRegex = new RegExp(resourceMap.map(resource => resource.fileName).join('|'), 'i')

export default class Renderer extends Tapable {
  constructor (nuxt) {
    super()
    this.nuxt = nuxt
    this.options = nuxt.options

    // Will be set by createRenderer
    this.bundleRenderer = null

    // Will be available on dev
    this.webpackDevMiddleware = null
    this.webpackHotMiddleware = null

    // Create new connect instance
    this.app = connect()

    // Renderer runtime resources
    this.resources = {
      clientManifest: null,
      serverBundle: null,
      appTemplate: null,
      errorTemplate: parseTemplate('
{{ stack }}
') // Will be loaded on ready
    }

    // Initialize
    /* istanbul ignore if */
    if (nuxt.initialized) {
      // If nuxt already initialized
      this._ready = this.ready().catch(this.nuxt.errorHandler)
    } else {
      // Wait for hook
      this.nuxt.plugin('afterInit', () => {
        this._ready = this.ready()
        return this._ready
      })
    }
  }

  async ready () {
    /* istanbul ignore if */
    if (this._ready) {
      return this._ready
    }

    // Setup all middleWare
    this.setupMiddleware()

    // Load error template
    const errorTemplatePath = resolve(this.options.buildDir, 'views/error.html')
    if (fs.existsSync(errorTemplatePath)) {
      this.resources.errorTemplate = parseTemplate(fs.readFileSync(errorTemplatePath, 'utf8'))
    }

    // Load SSR resources from fs
    if (!this.options.dev) {
      await this.loadResources()
    }

    return this
  }

  async loadResources (_fs) {
    let distPath = resolve(this.options.buildDir, 'dist')

    let updated = []

    resourceMap.forEach(({ key, fileName, transform }) => {
      let rawKey = '$$' + key
      const path = join(distPath, fileName)

      let rawData, data
      if (!_fs.existsSync(path)) {
        return // Resource not exists
      }
      rawData = _fs.readFileSync(path, 'utf8')
      if (!rawData || rawData === this.resources[rawKey]) {
        return // No changes
      }
      this.resources[rawKey] = rawData
      data = transform(rawData)
      /* istanbul ignore if */
      if (!data) {
        return // Invalid data ?
      }
      this.resources[key] = data
      updated.push(key)
    })

    if (updated.length > 0) {
      // debug('Updated', updated.join(', '), isServer)
      this.createRenderer()
    }
  }

  createRenderer () {
    // If resources are not yet provided
    if (!this.resources.serverBundle || !this.resources.clientManifest) {
      return
    }

    // Create bundle renderer for SSR
    this.bundleRenderer = createBundleRenderer(this.resources.serverBundle, Object.assign({
      clientManifest: this.resources.clientManifest,
      runInNewContext: false,
      basedir: this.options.rootDir
    }, this.options.render.ssr, this.options.build.ssr))

    if (this.options.build.ssr) {
      console.warn('[nuxt] `build.ssr` is deprecated and will be removed in 1.0 release, please use `renderer.ssr` instead!')
    }

    // Promisify renderToString
    this.bundleRenderer.renderToString = pify(this.bundleRenderer.renderToString)
  }

  useMiddleware (m) {
    // Resolve
    if (typeof m === 'string') {
      let src = m
      // Using ~ or ./ shorthand to resolve from project srcDir
      if (src.indexOf('~') === 0 || src.indexOf('./') === 0) {
        src = join(this.nuxt.options.srcDir, src.substr(1))
      }
      m = require(src)
    }
    // Use middleware
    if (m instanceof Function) {
      this.app.use(m)
    } else if (m && m.path && m.handler) {
      this.app.use(m.path, m.handler)
    }
  }

  setupMiddleware () {
    // Gzip middleware for production
    if (!this.options.dev && this.options.render.gzip) {
      this.useMiddleware(compression(this.options.render.gzip))
    }

    // Add User provided middleware
    this.options.serverMiddleware.forEach(m => {
      this.useMiddleware(m)
    })

    // Common URL checks
    this.useMiddleware((req, res, next) => {
      // If base in req.url, remove it for the middleware and vue-router
      if (this.options.router.base !== '/' && req.url.indexOf(this.options.router.base) === 0) {
        req.url = req.url.replace(this.options.router.base, '/')
      }
      // Prevent access to SSR resources
      if (ssrResourceRegex.test(req.url)) {
        res.statusCode = 404
        return res.end()
      }
      next()
    })

    // Remove publicPath from requests when it is pointing to CDN
    if (isUrl(this.options.build.publicPath)) {
      this.useMiddleware((req, res, next) => {
        if (req.url.indexOf(this.options.build.publicPath) === 0) {
          req.url = req.url.replace(this.options.build.publicPath, '/')
        }
        next()
      })
    }

    // Add webpack middleware only for development
    if (this.options.dev) {
      this.useMiddleware(async (req, res, next) => {
        if (req.url.includes('.hot-update.json')) {
          res.statusCode = 404
          return res.end()
        }
        if (this.webpackDevMiddleware) {
          await this.webpackDevMiddleware(req, res)
        }
        if (this.webpackHotMiddleware) {
          await this.webpackHotMiddleware(req, res)
        }
        next()
      })
    }

    // For serving static/ files to /
    this.useMiddleware(serveStatic(resolve(this.options.srcDir, 'static'), this.options.render.static))

    // Serve .nuxt/dist/ files only for production
    // For dev they will be served with devMiddleware
    if (!this.options.dev) {
      this.useMiddleware(serveStatic(resolve(this.options.buildDir, 'dist'), {
        maxAge: (this.options.dev ? 0 : '1y') // 1 year in production
      }))
    }

    // Finally use nuxtMiddleware
    this.useMiddleware(this.nuxtMiddleware.bind(this))
  }

  async nuxtMiddleware (req, res) {
    // Get context
    const context = getContext(req, res)
    res.statusCode = 200
    try {
      const { html, error, redirected, resourceHints } = await this.renderRoute(req.url, context)
      if (redirected) {
        return html
      }
      if (error) {
        res.statusCode = context.nuxt.error.statusCode || 500
      }

      // Add ETag header
      if (!error && this.options.render.etag) {
        const etag = generateETag(html, this.options.render.etag)
        if (fresh(req.headers, { etag })) {
          res.statusCode = 304
          res.end()
          return
        }
        res.setHeader('ETag', etag)
      }

      // HTTP2 push headers
      if (!error && this.options.render.http2.push) {
        // Parse resourceHints to extract HTTP.2 prefetch/push headers
        // https://w3c.github.io/preload/#server-push-http-2
        const regex = /link rel="([^"]*)" href="([^"]*)" as="([^"]*)"/g
        const pushAssets = []
        let m
        while (m = regex.exec(resourceHints)) { // eslint-disable-line no-cond-assign
          const [_, rel, href, as] = m // eslint-disable-line no-unused-vars
          if (rel === 'preload') {
            pushAssets.push(`<${href}>; rel=${rel}; as=${as}`)
          }
        }
        // Pass with single Link header
        // https://blog.cloudflare.com/http-2-server-push-with-multiple-assets-per-link-header
        res.setHeader('Link', pushAssets.join(','))
      }

      // Send response
      res.setHeader('Content-Type', 'text/html; charset=utf-8')
      res.setHeader('Content-Length', Buffer.byteLength(html))
      res.end(html, 'utf8')
      return html
    } catch (err) {
      /* istanbul ignore if */
      if (context.redirected) {
        console.error(err) // eslint-disable-line no-console
        return err
      }
      // Render error template
      const html = this.resources.errorTemplate({
        error: err,
        stack: ansiHTML(encodeHtml(err.stack))
      })
      // Send response
      res.statusCode = 500
      res.setHeader('Content-Type', 'text/html; charset=utf-8')
      res.setHeader('Content-Length', Buffer.byteLength(html))
      res.end(html, 'utf8')
      return err
    }
  }

  async renderRoute (url, context = {}) {
    /* istanbul ignore if */
    if (!this.bundleRenderer || !this.resources.appTemplate) {
      return new Promise(resolve => {
        setTimeout(() => resolve(this.renderRoute(url, context)), 1000)
      })
    }

    // Log rendered url
    debug(`Rendering url ${url}`)

    // Add url and isSever to the context
    context.url = url
    context.isServer = true

    // Call renderToString from the bundleRenderer and generate the HTML (will update the context as well)
    let APP = await this.bundleRenderer.renderToString(context)

    if (!context.nuxt.serverRendered) {
      APP = '

'
    }
    const m = context.meta.inject()
    let HEAD = m.meta.text() + m.title.text() + m.link.text() + m.style.text() + m.script.text() + m.noscript.text()
    if (this.options._routerBaseSpecified) {
      HEAD += `<base href="${this.options.router.base}">`
    }
    const resourceHints = context.renderResourceHints()
    HEAD += resourceHints + context.renderStyles()
    APP += `<script type="text/javascript">window.__NUXT__=${serialize(context.nuxt, { isJSON: true })}</script>`
    APP += context.renderScripts()

    const html = this.resources.appTemplate({
      HTML_ATTRS: 'data-n-head-ssr ' + m.htmlAttrs.text(),
      BODY_ATTRS: m.bodyAttrs.text(),
      HEAD,
      APP
    })

    return {
      html,
      resourceHints,
      error: context.nuxt.error,
      redirected: context.redirected
    }
  }

  async renderAndGetWindow (url, opts = {}) {
    /* istanbul ignore if */
    if (!jsdom) {
      try {
        jsdom = require('jsdom')
      } catch (e) /* istanbul ignore next */ {
        console.error('Fail when calling nuxt.renderAndGetWindow(url)') // eslint-disable-line no-console
        console.error('jsdom module is not installed') // eslint-disable-line no-console
        console.error('Please install jsdom with: npm install --save-dev jsdom') // eslint-disable-line no-console
        throw e
      }
    }
    let options = {
      resources: 'usable', // load subresources (https://github.com/tmpvar/jsdom#loading-subresources)
      runScripts: 'dangerously',
      beforeParse (window) {
        // Mock window.scrollTo
        window.scrollTo = () => {
        }
      }
    }
    if (opts.virtualConsole !== false) {
      options.virtualConsole = new jsdom.VirtualConsole().sendTo(console)
    }
    url = url || 'http://localhost:3000'
    const { window } = await jsdom.JSDOM.fromURL(url, options)
    // If Nuxt could not be loaded (error from the server-side)
    const nuxtExists = window.document.body.innerHTML.includes('window.__NUXT__')
    /* istanbul ignore if */
    if (!nuxtExists) {
      let error = new Error('Could not load the nuxt app')
      error.body = window.document.body.innerHTML
      throw error
    }
    // Used by nuxt.js to say when the components are loaded and the app ready
    await new Promise((resolve) => {
      window._onNuxtLoaded = () => resolve(window)
    })
    // Send back window object
    return window
  }
}
<commit_msg>eslint<commit_after>import ansiHTML from 'ansi-html'
import serialize from 'serialize-javascript'
import generateETag from 'etag'
import fresh from 'fresh'
import Tapable from 'tappable'
import pify from 'pify'
import serveStatic from 'serve-static'
import compression from 'compression'
import _ from 'lodash'
import { join, resolve } from 'path'
import fs from 'fs-extra'
import { createBundleRenderer } from 'vue-server-renderer'
import { encodeHtml, getContext, setAnsiColors, isUrl } from 'utils'
import Debug from 'debug'
import connect from 'connect'

const debug = Debug('nuxt:render')
debug.color = 4 // Force blue color

setAnsiColors(ansiHTML)

let jsdom = null

const parseTemplate = templateStr => _.template(templateStr, {
  interpolate: /{{([\s\S]+?)}}/g
})

const resourceMap = [
  {
    key: 'clientManifest',
    fileName: 'vue-ssr-client-manifest.json',
    transform: JSON.parse
  },
  {
    key: 'serverBundle',
    fileName: 'server-bundle.json',
    transform: JSON.parse
  },
  {
    key: 'appTemplate',
    fileName: 'index.html',
    transform: parseTemplate
  }
]

// Protector utility against request to SSR bundle files
const ssrResourceRegex = new RegExp(resourceMap.map(resource => resource.fileName).join('|'), 'i')

export default class Renderer extends Tapable {
  constructor (nuxt) {
    super()
    this.nuxt = nuxt
    this.options = nuxt.options

    // Will be set by createRenderer
    this.bundleRenderer = null

    // Will be available on dev
    this.webpackDevMiddleware = null
    this.webpackHotMiddleware = null

    // Create new connect instance
    this.app = connect()

    // Renderer runtime resources
    this.resources = {
      clientManifest: null,
      serverBundle: null,
      appTemplate: null,
      errorTemplate: parseTemplate('
{{ stack }}
') // Will be loaded on ready
    }

    // Initialize
    /* istanbul ignore if */
    if (nuxt.initialized) {
      // If nuxt already initialized
      this._ready = this.ready().catch(this.nuxt.errorHandler)
    } else {
      // Wait for hook
      this.nuxt.plugin('afterInit', () => {
        this._ready = this.ready()
        return this._ready
      })
    }
  }

  async ready () {
    /* istanbul ignore if */
    if (this._ready) {
      return this._ready
    }

    // Setup all middleWare
    this.setupMiddleware()

    // Load error template
    const errorTemplatePath = resolve(this.options.buildDir, 'views/error.html')
    if (fs.existsSync(errorTemplatePath)) {
      this.resources.errorTemplate = parseTemplate(fs.readFileSync(errorTemplatePath, 'utf8'))
    }

    // Load SSR resources from fs
    if (!this.options.dev) {
      await this.loadResources()
    }

    return this
  }

  async loadResources (_fs) {
    let distPath = resolve(this.options.buildDir, 'dist')

    let updated = []

    resourceMap.forEach(({ key, fileName, transform }) => {
      let rawKey = '$$' + key
      const path = join(distPath, fileName)

      let rawData, data
      if (!_fs.existsSync(path)) {
        return // Resource not exists
      }
      rawData = _fs.readFileSync(path, 'utf8')
      if (!rawData || rawData === this.resources[rawKey]) {
        return // No changes
      }
      this.resources[rawKey] = rawData
      data = transform(rawData)
      /* istanbul ignore if */
      if (!data) {
        return // Invalid data ?
      }
      this.resources[key] = data
      updated.push(key)
    })

    if (updated.length > 0) {
      // debug('Updated', updated.join(', '), isServer)
      this.createRenderer()
    }
  }

  createRenderer () {
    // If resources are not yet provided
    if (!this.resources.serverBundle || !this.resources.clientManifest) {
      return
    }

    // Create bundle renderer for SSR
    this.bundleRenderer = createBundleRenderer(this.resources.serverBundle, Object.assign({
      clientManifest: this.resources.clientManifest,
      runInNewContext: false,
      basedir: this.options.rootDir
    }, this.options.render.ssr, this.options.build.ssr))

    if (this.options.build.ssr) {
      // eslint-disable-next-line no-console
      console.warn('[nuxt] `build.ssr` is deprecated and will be removed in 1.0 release, please use `renderer.ssr` instead!')
    }

    // Promisify renderToString
    this.bundleRenderer.renderToString = pify(this.bundleRenderer.renderToString)
  }

  useMiddleware (m) {
    // Resolve
    if (typeof m === 'string') {
      let src = m
      // Using ~ or ./ shorthand to resolve from project srcDir
      if (src.indexOf('~') === 0 || src.indexOf('./') === 0) {
        src = join(this.nuxt.options.srcDir, src.substr(1))
      }
      m = require(src)
    }
    // Use middleware
    if (m instanceof Function) {
      this.app.use(m)
    } else if (m && m.path && m.handler) {
      this.app.use(m.path, m.handler)
    }
  }

  setupMiddleware () {
    // Gzip middleware for production
    if (!this.options.dev && this.options.render.gzip) {
      this.useMiddleware(compression(this.options.render.gzip))
    }

    // Add User provided middleware
    this.options.serverMiddleware.forEach(m => {
      this.useMiddleware(m)
    })

    // Common URL checks
    this.useMiddleware((req, res, next) => {
      // If base in req.url, remove it for the middleware and vue-router
      if (this.options.router.base !== '/' && req.url.indexOf(this.options.router.base) === 0) {
        req.url = req.url.replace(this.options.router.base, '/')
      }
      // Prevent access to SSR resources
      if (ssrResourceRegex.test(req.url)) {
        res.statusCode = 404
        return res.end()
      }
      next()
    })

    // Remove publicPath from requests when it is pointing to CDN
    if (isUrl(this.options.build.publicPath)) {
      this.useMiddleware((req, res, next) => {
        if (req.url.indexOf(this.options.build.publicPath) === 0) {
          req.url = req.url.replace(this.options.build.publicPath, '/')
        }
        next()
      })
    }

    // Add webpack middleware only for development
    if (this.options.dev) {
      this.useMiddleware(async (req, res, next) => {
        if (req.url.includes('.hot-update.json')) {
          res.statusCode = 404
          return res.end()
        }
        if (this.webpackDevMiddleware) {
          await this.webpackDevMiddleware(req, res)
        }
        if (this.webpackHotMiddleware) {
          await this.webpackHotMiddleware(req, res)
        }
        next()
      })
    }

    // For serving static/ files to /
    this.useMiddleware(serveStatic(resolve(this.options.srcDir, 'static'), this.options.render.static))

    // Serve .nuxt/dist/ files only for production
    // For dev they will be served with devMiddleware
    if (!this.options.dev) {
      this.useMiddleware(serveStatic(resolve(this.options.buildDir, 'dist'), {
        maxAge: (this.options.dev ? 0 : '1y') // 1 year in production
      }))
    }

    // Finally use nuxtMiddleware
    this.useMiddleware(this.nuxtMiddleware.bind(this))
  }

  async nuxtMiddleware (req, res) {
    // Get context
    const context = getContext(req, res)
    res.statusCode = 200
    try {
      const { html, error, redirected, resourceHints } = await this.renderRoute(req.url, context)
      if (redirected) {
        return html
      }
      if (error) {
        res.statusCode = context.nuxt.error.statusCode || 500
      }

      // Add ETag header
      if (!error && this.options.render.etag) {
        const etag = generateETag(html, this.options.render.etag)
        if (fresh(req.headers, { etag })) {
          res.statusCode = 304
          res.end()
          return
        }
        res.setHeader('ETag', etag)
      }

      // HTTP2 push headers
      if (!error && this.options.render.http2.push) {
        // Parse resourceHints to extract HTTP.2 prefetch/push headers
        // https://w3c.github.io/preload/#server-push-http-2
        const regex = /link rel="([^"]*)" href="([^"]*)" as="([^"]*)"/g
        const pushAssets = []
        let m
        while (m = regex.exec(resourceHints)) { // eslint-disable-line no-cond-assign
          const [_, rel, href, as] = m // eslint-disable-line no-unused-vars
          if (rel === 'preload') {
            pushAssets.push(`<${href}>; rel=${rel}; as=${as}`)
          }
        }
        // Pass with single Link header
        // https://blog.cloudflare.com/http-2-server-push-with-multiple-assets-per-link-header
        res.setHeader('Link', pushAssets.join(','))
      }

      // Send response
      res.setHeader('Content-Type', 'text/html; charset=utf-8')
      res.setHeader('Content-Length', Buffer.byteLength(html))
      res.end(html, 'utf8')
      return html
    } catch (err) {
      /* istanbul ignore if */
      if (context.redirected) {
        console.error(err) // eslint-disable-line no-console
        return err
      }
      // Render error template
      const html = this.resources.errorTemplate({
        error: err,
        stack: ansiHTML(encodeHtml(err.stack))
      })
      // Send response
      res.statusCode = 500
      res.setHeader('Content-Type', 'text/html; charset=utf-8')
      res.setHeader('Content-Length', Buffer.byteLength(html))
      res.end(html, 'utf8')
      return err
    }
  }

  async renderRoute (url, context = {}) {
    /* istanbul ignore if */
    if (!this.bundleRenderer || !this.resources.appTemplate) {
      return new Promise(resolve => {
        setTimeout(() => resolve(this.renderRoute(url, context)), 1000)
      })
    }

    // Log rendered url
    debug(`Rendering url ${url}`)

    // Add url and isSever to the context
    context.url = url
    context.isServer = true

    // Call renderToString from the bundleRenderer and generate the HTML (will update the context as well)
    let APP = await this.bundleRenderer.renderToString(context)

    if (!context.nuxt.serverRendered) {
      APP = '

'
    }
    const m = context.meta.inject()
    let HEAD = m.meta.text() + m.title.text() + m.link.text() + m.style.text() + m.script.text() + m.noscript.text()
    if (this.options._routerBaseSpecified) {
      HEAD += `<base href="${this.options.router.base}">`
    }
    const resourceHints = context.renderResourceHints()
    HEAD += resourceHints + context.renderStyles()
    APP += `<script type="text/javascript">window.__NUXT__=${serialize(context.nuxt, { isJSON: true })}</script>`
    APP += context.renderScripts()

    const html = this.resources.appTemplate({
      HTML_ATTRS: 'data-n-head-ssr ' + m.htmlAttrs.text(),
      BODY_ATTRS: m.bodyAttrs.text(),
      HEAD,
      APP
    })

    return {
      html,
      resourceHints,
      error: context.nuxt.error,
      redirected: context.redirected
    }
  }

  async renderAndGetWindow (url, opts = {}) {
    /* istanbul ignore if */
    if (!jsdom) {
      try {
        jsdom = require('jsdom')
      } catch (e) /* istanbul ignore next */ {
        console.error('Fail when calling nuxt.renderAndGetWindow(url)') // eslint-disable-line no-console
        console.error('jsdom module is not installed') // eslint-disable-line no-console
        console.error('Please install jsdom with: npm install --save-dev jsdom') // eslint-disable-line no-console
        throw e
      }
    }
    let options = {
      resources: 'usable', // load subresources (https://github.com/tmpvar/jsdom#loading-subresources)
      runScripts: 'dangerously',
      beforeParse (window) {
        // Mock window.scrollTo
        window.scrollTo = () => {
        }
      }
    }
    if (opts.virtualConsole !== false) {
      options.virtualConsole = new jsdom.VirtualConsole().sendTo(console)
    }
    url = url || 'http://localhost:3000'
    const { window } = await jsdom.JSDOM.fromURL(url, options)
    // If Nuxt could not be loaded (error from the server-side)
    const nuxtExists = window.document.body.innerHTML.includes('window.__NUXT__')
    /* istanbul ignore if */
    if (!nuxtExists) {
      let error = new Error('Could not load the nuxt app')
      error.body = window.document.body.innerHTML
      throw error
    }
    // Used by nuxt.js to say when the components are loaded and the app ready
    await new Promise((resolve) => {
      window._onNuxtLoaded = () => resolve(window)
    })
    // Send back window object
    return window
  }
}
<|endoftext|>


<commit_before>BR.Search = L.Control.Geocoder.extend({
    options: {
        geocoder: new L.Control.Geocoder.Nominatim({
            serviceUrl: 'https://nominatim.openstreetmap.org/'
        }),
        position: 'topleft'
    },

        onAdd: function (map) {
        map.attributionControl.addAttribution(
            'search by Nominatim');

        return L.Control.Geocoder.prototype.onAdd.call(this, map);
    },

    markGeocode: function(result) {
        this._map.fitBounds(result.geocode.bbox, {
            maxZoom: 17
        });

        this.clear();
        this._geocodeMarker = new L.CircleMarker(result.geocode.center, {
            clickable: false,
            color: 'red',
            opacity: 1,
            weight: 3
        }).addTo(this._map);

          return this;
    },

    clear: function() {
        if (this._geocodeMarker) {
          this._map.removeLayer(this._geocodeMarker);
        }
    }
});
<commit_msg>Leaflet 1.0: "clickable" option renamed to "interactive"<commit_after>BR.Search = L.Control.Geocoder.extend({
    options: {
        geocoder: new L.Control.Geocoder.Nominatim({
            serviceUrl: 'https://nominatim.openstreetmap.org/'
        }),
        position: 'topleft'
    },

    onAdd: function (map) {
        map.attributionControl.addAttribution(
            'search by Nominatim');

        return L.Control.Geocoder.prototype.onAdd.call(this, map);
    },

    markGeocode: function(result) {
        this._map.fitBounds(result.geocode.bbox, {
            maxZoom: 17
        });

        this.clear();
        this._geocodeMarker = new L.CircleMarker(result.geocode.center, {
            interactive: false,
            color: 'red',
            opacity: 1,
            weight: 3
        }).addTo(this._map);

        return this;
    },

    clear: function() {
        if (this._geocodeMarker) {
            this._map.removeLayer(this._geocodeMarker);
        }
    }
});
<|endoftext|>


<commit_before>"use babel";

import Grim from "grim";
import glob from "glob";
import path from "path";
import fs from "fs";
import util from "util";
import { remote } from "electron";
import _ from "underscore-plus";
import Jasmine from "jasmine";
import TimeReporter from "./time-reporter/time-reporter";
import AtomReporter from "./atom-reporter/atom-reporter";

function setSpecProperties(suite, specDirectory) {
	for (let child of suite.children) {
		if (child instanceof jasmine.Spec) {
			if (!child.specType) {
				child.suite = suite;
				child.specDirectory = specDirectory;
				child.specType = "user";
			}
		} else {
			setSpecProperties(child, specDirectory);
		}
	}
}

function isFocused(suite) {
	for (let child of suite.children) {
		if (child.priority > 0 || (child instanceof jasmine.Suite && isFocused(child))) {
			return true;
		}
	}
	return false;
}

const defaults = {
	reporter: null,
	timeReporter: false,
	specHelper: false,
	showColors: true,
	htmlTitle: "",
	suffix: "-spec",
	legacySuffix: "-spec-v1",
	showEditor: false,
};

export default function createRunner(options = {}, configFunc) {
	if (typeof options === "function") {
		configFunc = options;
		options = defaults;
	} else {
		options = Object.assign({}, defaults, options);
		if (typeof configFunc !== "function") {
			configFunc = function () {};
		}
	}
	if (/\.(js|coffee)$/.test(options.suffix)) {
		options.fileGlob = "**/*" + options.suffix;
		options.fileRegexp = new RegExp(options.suffix);
	} else {
		options.fileGlob = "**/*" + options.suffix + "\.+(js|coffee)";
		options.fileRegexp = new RegExp(options.suffix + "\.(js|coffee)");
	}
	if (/\.(js|coffee)$/.test(options.legacySuffix)) {
		options.fileLegacyGlob = "**/*" + options.legacySuffix;
		options.fileLegacyRegexp = new RegExp(options.legacySuffix);
	} else {
		options.fileLegacyGlob = "**/*" + options.legacySuffix + "\.+(js|coffee)";
		options.fileLegacyRegexp = new RegExp(options.legacySuffix + "\.(js|coffee)");
	}

	return async ({ testPaths, buildAtomEnvironment, buildDefaultApplicationDelegate, logFile, headless, legacyTestRunner }) => {
		let resolveWithResult;
		const promise = new Promise(resolve => { resolveWithResult = resolve; });

		try {
			const jasmine = new Jasmine();

			for (let key in jasmine) {
				window[key] = jasmine[key];
			}

			window.headless = headless;

			const applicationDelegate = buildDefaultApplicationDelegate();
			window.atom = buildAtomEnvironment({
				applicationDelegate,
				window,
				document,
				enablePersistence: false,
				configDirPath: process.env.ATOM_HOME
			});

			if (headless) {
				console.debug = console.log = function (...args) {
					const formatted = util.format(...args);
					process.stdout.write(formatted + "\n");
				};

				Object.defineProperties(process, {
					stdout: { value: remote.process.stdout },
					stderr: { value: remote.process.stderr }
				});
			}

			await require("./spec-helper/spec-helper")(options.specHelper);
			require("./spec-helper/deprecations");
			require("./spec-helper/fixtures");
			require("./spec-helper/yield");

			await configFunc();

			let legacyTestPaths = [];
			testPaths.forEach(testPath => {
				if (fs.statSync(testPath).isDirectory()) {
					jasmine.addSpecFiles(glob.sync(path.join(testPath, options.fileGlob)));
					jasmine.loadSpecs();
					setSpecProperties(jasmine.env.topSuite(), path.resolve(testPath));
					if (options.legacySuffix) {
						legacyTestPaths = legacyTestPaths.concat(glob.sync(path.join(testPath, options.fileLegacyGlob)));
					}
				} else {
					if (options.fileRegexp.test(testPath)) {
						jasmine.addSpecFile(testPath);
						jasmine.loadSpecs();
						setSpecProperties(jasmine.env.topSuite(), path.resolve(path.dirname(testPath)));
					} else if (options.legacySuffix && options.fileLegacyRegexp.test(testPath)) {
						legacyTestPaths.push();
					}
				}
			});

			const focused = isFocused(jasmine.env.topSuite());

			jasmine.showColors(options.showColors);

			jasmine.onComplete(function (passed) {
				if (!focused && legacyTestPaths.length > 0) {
					Grim.clearDeprecations();
					if (headless) {
						console.log("\n\nStarting Legacy Tests");
					}
					legacyTestRunner({ testPaths: legacyTestPaths, buildAtomEnvironment, buildDefaultApplicationDelegate, logFile, headless }).then(errorCode => {
						resolveWithResult(passed && !errorCode ? 0 : 1);
					});
				} else {
					resolveWithResult(passed ? 0 : 1);
				}
			});

			if (headless) {
				if (options.reporter) {
					jasmine.env.clearReporters();
					jasmine.addReporter(options.reporter);
				}
			} else {
				process.on("uncaughtException", console.error.bind(console));
				jasmine.env.clearReporters();
				if (options.timeReporter) {
					jasmine.addReporter(new TimeReporter());
				}
				jasmine.addReporter(new AtomReporter({
					legacyTestsAvailable: !focused && legacyTestPaths.length > 0,
					title: options.htmlTitle || _.undasherize(_.uncamelcase(path.basename(path.dirname(testPaths[0])))),
					showEditor: options.showEditor
				}));
			}

			Grim.clearDeprecations();

			jasmine.execute();

		} catch (ex) {
			console.error(ex);
			resolveWithResult(1);
		}

		return promise;
	};
}
<commit_msg>add testPaths option<commit_after>"use babel";

import Grim from "grim";
import glob from "glob";
import path from "path";
import fs from "fs";
import util from "util";
import { remote } from "electron";
import _ from "underscore-plus";
import Jasmine from "jasmine";
import TimeReporter from "./time-reporter/time-reporter";
import AtomReporter from "./atom-reporter/atom-reporter";
import SetupView from "./setup-view";

function setSpecProperties(suite, specDirectory) {
	for (let child of suite.children) {
		if (child instanceof jasmine.Spec) {
			if (!child.specType) {
				child.suite = suite;
				child.specDirectory = specDirectory;
				child.specType = "user";
			}
		} else {
			setSpecProperties(child, specDirectory);
		}
	}
}

function isFocused(suite) {
	for (let child of suite.children) {
		if (child.priority > 0 || (child instanceof jasmine.Suite && isFocused(child))) {
			return true;
		}
	}
	return false;
}

const defaults = {
	reporter: null,
	timeReporter: false,
	specHelper: false,
	showColors: true,
	htmlTitle: "",
	suffix: "-spec",
	legacySuffix: "-spec-v1",
	showEditor: false,
	testPaths: null,
};

export default function createRunner(options = {}, configFunc) {
	if (typeof options === "function") {
		configFunc = options;
		options = defaults;
	} else {
		options = Object.assign({}, defaults, options);
		if (typeof configFunc !== "function") {
			configFunc = function () {};
		}
	}
	if (/\.(js|coffee)$/.test(options.suffix)) {
		options.fileGlob = "**/*" + options.suffix;
		options.fileRegexp = new RegExp(options.suffix);
	} else {
		options.fileGlob = "**/*" + options.suffix + "\.+(js|coffee)";
		options.fileRegexp = new RegExp(options.suffix + "\.(js|coffee)");
	}
	if (/\.(js|coffee)$/.test(options.legacySuffix)) {
		options.fileLegacyGlob = "**/*" + options.legacySuffix;
		options.fileLegacyRegexp = new RegExp(options.legacySuffix);
	} else {
		options.fileLegacyGlob = "**/*" + options.legacySuffix + "\.+(js|coffee)";
		options.fileLegacyRegexp = new RegExp(options.legacySuffix + "\.(js|coffee)");
	}

	return async ({ testPaths, buildAtomEnvironment, buildDefaultApplicationDelegate, logFile, headless, legacyTestRunner }) => {
		let resolveWithResult;
		const promise = new Promise(resolve => { resolveWithResult = resolve; });

		try {
			const jasmine = new Jasmine();

			for (let key in jasmine) {
				window[key] = jasmine[key];
			}

			window.headless = headless;

			const applicationDelegate = buildDefaultApplicationDelegate();
			window.atom = buildAtomEnvironment({
				applicationDelegate,
				window,
				document,
				enablePersistence: false,
				configDirPath: process.env.ATOM_HOME
			});

			if (headless) {
				console.debug = console.log = function (...args) {
					const formatted = util.format(...args);
					process.stdout.write(formatted + "\n");
				};

				Object.defineProperties(process, {
					stdout: { value: remote.process.stdout },
					stderr: { value: remote.process.stderr }
				});
			}
			const setupView = new SetupView({headless});
			await require("./spec-helper/spec-helper")(options.specHelper, setupView);
			require("./spec-helper/deprecations");
			require("./spec-helper/fixtures");
			require("./spec-helper/yield");

			await configFunc();

			let legacyTestPaths = [];
			let paths = [];
			const root = path.resolve(path.dirname(testPaths[0]));
			if (options.testPaths !== null) {
				if (!Array.isArray(options.testPaths)) {
					options.testPaths = [options.testPaths];
				}
				paths = options.testPaths.map(p => {
					const fullPath = path.resolve(root, p);
					try {
						fs.statSync(fullPath);
					} catch (err) {
						if (err.code === "ENOENT"){
							err.message = `ENOENT: no such file or directory '${fullPath}' from testPaths`;
							setupView.error(err.toString(), "in testPaths");
						}
						throw err;
					}
					return fullPath;
				});
			} else {
				paths = testPaths;
			}
			paths.forEach(testPath => {
				if (fs.statSync(testPath).isDirectory()) {
					const files = glob.sync(path.join(testPath, options.fileGlob)).filter(f => !f.includes("node_modules"));
					jasmine.addSpecFiles(files);
					jasmine.loadSpecs();
					setSpecProperties(jasmine.env.topSuite(), path.resolve(testPath));
					if (options.legacySuffix) {
						legacyTestPaths = legacyTestPaths.concat(glob.sync(path.join(testPath, options.fileLegacyGlob)));
					}
				} else {
					if (options.fileRegexp.test(testPath)) {
						jasmine.addSpecFile(testPath);
						jasmine.loadSpecs();
						setSpecProperties(jasmine.env.topSuite(), path.resolve(path.dirname(testPath)));
					} else if (options.legacySuffix && options.fileLegacyRegexp.test(testPath)) {
						legacyTestPaths.push(testPath);
					}
				}
			});

			const focused = isFocused(jasmine.env.topSuite());

			jasmine.showColors(options.showColors);

			jasmine.onComplete(function (passed) {
				if (!focused && legacyTestPaths.length > 0) {
					Grim.clearDeprecations();
					if (headless) {
						console.log("\n\nStarting Legacy Tests");
					}
					legacyTestRunner({ testPaths: legacyTestPaths, buildAtomEnvironment, buildDefaultApplicationDelegate, logFile, headless }).then(errorCode => {
						resolveWithResult(passed && !errorCode ? 0 : 1);
					});
				} else {
					resolveWithResult(passed ? 0 : 1);
				}
			});

			setupView.destroy();

			if (headless) {
				if (options.reporter) {
					jasmine.env.clearReporters();
					jasmine.addReporter(options.reporter);
				}
			} else {
				process.on("uncaughtException", console.error.bind(console));
				jasmine.env.clearReporters();
				if (options.timeReporter) {
					jasmine.addReporter(new TimeReporter());
				}
				jasmine.addReporter(new AtomReporter({
					legacyTestsAvailable: !focused && legacyTestPaths.length > 0,
					title: options.htmlTitle || _.undasherize(_.uncamelcase(path.basename(path.dirname(paths[0])))),
					showEditor: options.showEditor
				}));
			}

			Grim.clearDeprecations();

			jasmine.execute();

		} catch (ex) {
			console.error(ex);
			resolveWithResult(1);
		}

		return promise;
	};
}
<|endoftext|>


<commit_before>(function ($, tinycolor) {
    "use strict";

    var Utils = {
        "popRandom": function (arr) {
            var item, index = Math.floor(Math.random() * arr.length);
            item = arr[index];
            arr.splice(index, 1);
            return item;
        },
    };

    /* RedditApi */

    var RedditApi = function () {
        this.currSubreddit = undefined;

        this.hot = [];
        this.lastHot = undefined;

        this.new = [];
        this.lastNew = undefined;

        this.seen = {};
    };

    RedditApi.prototype._reset = function () {
        this.hot = [];
        this.new = [];
        this.seen = {};
        this.lastHot = undefined;
        this.lastNew = undefined;
    };

    RedditApi.prototype.switchSubreddit = function (subreddit) {
        this.currSubreddit = subreddit;
        this._reset();
    };

    RedditApi.prototype.switchMode = function (mode) {
        this.mode = mode;
        this._reset();
    };

    RedditApi.prototype.searchSubreddits = function (query, callback) {
        var LIMIT = 5;
        var path = "subreddits/search.json?q=" + query + "&limit=" + LIMIT;
        this.apiCall(path, function (response) {
            var results = [];
            response.children.forEach(function (sub) {
                results.push(sub.data.display_name);
            });
            callback(results);
        });
    };

    RedditApi.prototype.apiCall = function (path, callback) {
        var apiEndpoint = "http://www.reddit.com/" + path;

        $.ajax({
            "url": apiEndpoint,
            "dataType": "jsonp",
            "jsonp": "jsonp",
            "success": function (response) {
                callback(response.data);
            },
        });
    };

    RedditApi.prototype.refreshHot = function (callback) {
        var path = "", that = this;

        if (this.currSubreddit) {
            path += "r/" + this.currSubreddit;
        }
        path += "/hot.json";
        if (this.lastHot) {
            path += "?after=" + this.lastHot;
        }

        this.apiCall(path, function (data) {
            var posts = data.children;
            posts.forEach(function (post) {
                post = post.data;
                if (!that.seen.hasOwnProperty(post.name)) {
                    that.hot.push(post);
                    that.seen[post.name] = true;
                }
            });
            if (posts) {
                that.lastHot = posts.pop().data.name;
            }
            if (callback) {
                callback();
            }
        });
    };

    RedditApi.prototype.refreshNew = function (callback) {
        var path = "", that = this;

        if (this.currSubreddit) {
            path += "r/" + this.currSubreddit;
        }
        path += "/new.json";
        if (this.lastNew) {
            path += "?after=" + this.lastNew;
        }

        this.apiCall(path, function (data) {
            var posts = data.children;
            posts.forEach(function (post) {
                post = post.data;
                if (!that.seen.hasOwnProperty(post.name)) {
                    that.new.push(post);
                    that.seen[post.name] = true;
                }
            });
            if (posts) {
                that.lastNew = posts.pop().data.name;
            }
            if (callback) {
                callback();
            }
        });
    };

    RedditApi.prototype.hotItem = function () {
        if (this.hot.length < 5) {
            this.refreshHot();
        }

        if (this.hot.length > 0) {
            return Utils.popRandom(this.hot);
        }
    };

    RedditApi.prototype.newItem = function () {
        if (this.new.length < 5) {
            this.refreshNew();
        }

        if (this.new.length > 0) {
            return Utils.popRandom(this.new);
        }
    };

    /* Reddit BigSpy view */

    var RedditBigSpyView = function (app) {
        this.app = app;

        this.$navbarTitle = $("#navbar-title");
        this.$feed = $("#feed");
        this.$settingsDialog = $("#settings-dialog");
        this.$settingsButton = $("#settings-button");
        this.$percentNewSlider = $("#percent-new-slider");
        this.$percentNew = $("#percent-new");
        this.$percentPopular = $("#percent-popular");
        this.$subredditField = $("#subreddit-field");

        this.setupSettings();
    };

    RedditBigSpyView.prototype.setTitle = function (title) {
        this.$navbarTitle.text(title);
    };

    RedditBigSpyView.prototype.clear = function () {
        var that = this;
        this.$feed.hide("drop", function () {
            that.$feed.empty();
            that.$feed.show("drop");
        });
    };

    RedditBigSpyView.prototype.setupSettings = function () {
        var that = this, updatedPercentNew;

        // settings button/dialog

        this.$settingsDialog.dialog({
            "autoOpen": false,
            "dialogClass": "no-close",
            "buttons": {
                "Cancel": function () {
                    that.updatePercentNewView(that.app.percentNew);
                    that.$subredditField.val(that.app.api.currSubreddit);
                    $(this).dialog("close");
                },
                "Save": function () {
                    var newSub = $.trim(that.$subredditField.val());
                    var oldSub = that.app.api.currSubreddit || "";
                    var newTitle = "Posts from ";

                    that.app.percentNew = updatedPercentNew;

                    if (newSub !== oldSub) {
                        if (newSub) {
                            newTitle += "/r/" + newSub;
                        } else {
                            newTitle += "all subreddits";
                        }
                        that.setTitle(newTitle);
                        that.app.api.switchSubreddit(newSub);
                        that.clear();
                    }

                    $(this).dialog("close");
                },
            },
        });

        this.$settingsButton.click(function () {
            that.$settingsDialog.dialog("open");
        });

        // percent new/popular sliders/text

        this.updatePercentNewView(this.app.percentNew);

        this.$percentNewSlider.on("slide", function (evt, ui) {
            that.$percentNew.text(ui.value);
            that.$percentPopular.text(100 - ui.value);
            updatedPercentNew = ui.value;
        });

        // autocomplete

        this.$subredditField.autocomplete({
            "source": function (req, res) {
                that.app.api.searchSubreddits(req.term, function (subs) {
                    res(subs);
                });
            },
        });
    };

    RedditBigSpyView.prototype.updatePercentNewView = function (percentNew) {
        this.$percentNewSlider.slider({"value": percentNew});
        this.$percentNew.text(percentNew);
        this.$percentPopular.text(100 - percentNew);
    };

    RedditBigSpyView.prototype.getPostColor = function (score) {
        var green = tinycolor("#ffff4a"), desatAmount,
            upperBound = 3000.0;
        score = +score;
        desatAmount = score > upperBound ? 0: 100 - (100 * score / upperBound);
        return tinycolor.desaturate(green, desatAmount).toHexString();
    };

    RedditBigSpyView.prototype.showPost = function (post) {
        if (!post) {
            console.log("post was empty");
            return;
        }

        var that = this;
        var $li, $row, $score, $postTitle, $subredditLink,
            $subredditLabel, $commentsCell, $commentsLink;

        $li = $("
");
        $row = $("
");

        // score cell
        $score = $("").text(post.score);
        $score.css("background-color", this.getPostColor(post.score));
        $("
").append($score).appendTo($row);

        // title cell
        $subredditLink = $("")
            .attr("href", "http://reddit.com/r/" + post.subreddit)
            .text(post.subreddit);
        $subredditLabel = $("")
            .append($subredditLink);
        $postTitle = $("")
            .attr("href", post.url)
            .text(post.title);
        $("
")
            .append($subredditLabel)
            .append($postTitle)
            .appendTo($row);

        // comments cell
        $commentsLink = $("")
            .data("post-id", post.id)
            .attr("href", "#")
            .text("comments");
        $commentsLink.click(function () {
            that.clear();
        });
        $commentsCell = $("
")
            .appendTo($row)
            .append($commentsLink);

        $li.wrapInner($row).hide();
        var MAX_ITEMS = 30;
        this.$feed.prepend($li);
        $li.show("drop");

        if (this.$feed.children().length > MAX_ITEMS) {
            this.$feed.children().last().remove();
        }
    };

    /* Reddit BigSpy */

    var RedditBigSpy = function () {
        this.api = new RedditApi();
        this.percentNew = 20;
        this.postInterval = 2000;

        this.view = new RedditBigSpyView(this);
    };

    RedditBigSpy.prototype.start = function () {
        var that = this;
        this.api.refreshHot(function () {
            var firstPost = that.api.hotItem();
            window.setInterval(function () {
                var post = that.getPost();
                that.view.showPost(post);
            }, that.postInterval);
            that.view.showPost(firstPost);
        });
        this.api.refreshNew();
    };

    RedditBigSpy.prototype.getPost = function () {
        if (Math.random() <= this.percentNew / 100.0) {
            return this.api.newItem();
        }
        return this.api.hotItem();
    };

    /* MAIN */

    $(function () {
        var app = new RedditBigSpy();
        app.start();
    });
}(window.jQuery, window.tinycolor));
<commit_msg>Beginning of comments logic<commit_after>(function ($, tinycolor) {
    "use strict";

    var Utils = {
        "popRandom": function (arr) {
            var item, index = Math.floor(Math.random() * arr.length);
            item = arr[index];
            arr.splice(index, 1);
            return item;
        },
    };

    /* RedditApi */

    var RedditApi = function () {
        this.currSubreddit = undefined;
        this.currPost = undefined;

        this.hot = [];
        this.lastHot = undefined;

        this.new = [];
        this.lastNew = undefined;

        this.seen = {};

        this.mode = "posts";
    };

    RedditApi.prototype._reset = function () {
        this.hot = [];
        this.new = [];
        this.seen = {};
        this.lastHot = undefined;
        this.lastNew = undefined;
    };

    RedditApi.prototype.switchSubreddit = function (subreddit) {
        this.currSubreddit = subreddit;
        this._reset();
    };

    RedditApi.prototype.switchMode = function (mode) {
        this.mode = mode;
        this._reset();
    };

    RedditApi.prototype.searchSubreddits = function (query, callback) {
        var LIMIT = 5;
        var path = "subreddits/search.json?q=" + query + "&limit=" + LIMIT;
        this.apiCall(path, function (response) {
            var results = [];
            response.children.forEach(function (sub) {
                results.push(sub.data.display_name);
            });
            callback(results);
        });
    };

    RedditApi.prototype.apiCall = function (path, callback) {
        var apiEndpoint = "http://www.reddit.com/" + path;

        $.ajax({
            "url": apiEndpoint,
            "dataType": "jsonp",
            "jsonp": "jsonp",
            "success": function (response) {
                callback(response);
            },
        });
    };

    RedditApi.prototype.refreshHot = function (callback) {
        var path = "", that = this;

        if (this.mode === "posts") {
            if (this.currSubreddit) {
                path += "r/" + this.currSubreddit;
            }

            path += "/hot.json";
            if (this.lastHot) {
                path += "?after=" + this.lastHot;
            }

            this.apiCall(path, function (data) {
                var posts = data.data.children;
                posts.forEach(function (post) {
                    post = post.data;
                    if (!that.seen.hasOwnProperty(post.name)) {
                        that.hot.push(post);
                        that.seen[post.name] = true;
                    }
                });
                if (posts) {
                    that.lastHot = posts.pop().data.name;
                }
                if (callback) {
                    callback();
                }
            });
        } else if (this.mode === "comments") {
            path += this.currPost;

            path += "/hot.json";
            if (this.lastHot) {
                path += "?after=" + this.lastHot;
            }

            this.apiCall(path, function (data) {
                data[1].data.children.forEach(function (comment) {
                    that.hot.push(comment);
                    that.seen[comment.name] = true;
                });
                console.log(that.hot);
            });
        }
    };

    RedditApi.prototype.refreshNew = function (callback) {
        var path = "", that = this;

        if (this.mode === "posts") {
            if (this.currSubreddit) {
                path += "r/" + this.currSubreddit;
            }

            path += "/new.json";
            if (this.lastNew) {
                path += "?after=" + this.lastNew;
            }

            this.apiCall(path, function (data) {
                var posts = data.data.children;
                posts.forEach(function (post) {
                    post = post.data;
                    if (!that.seen.hasOwnProperty(post.name)) {
                        that.new.push(post);
                        that.seen[post.name] = true;
                    }
                });
                if (posts) {
                    that.lastNew = posts.pop().data.name;
                }
                if (callback) {
                    callback();
                }
            });
        } else if (this.mode === "comments") {
            path += this.currPost;

            path += "/new.json";
            if (this.lastNew) {
                path += "?after=" + this.lastNew;
            }

            this.apiCall(path, function (data) {
                data[1].data.children.forEach(function (comment) {
                    that.hot.push(comment);
                    that.seen[comment.name] = true;
                });
                console.log(that.hot);
            });
        }
    };

    RedditApi.prototype.hotItem = function () {
        if (this.hot.length < 5) {
            this.refreshHot();
        }

        if (this.hot.length > 0) {
            return Utils.popRandom(this.hot);
        }
    };

    RedditApi.prototype.newItem = function () {
        if (this.new.length < 5) {
            this.refreshNew();
        }

        if (this.new.length > 0) {
            return Utils.popRandom(this.new);
        }
    };

    /* Reddit BigSpy view */

    var RedditBigSpyView = function (app) {
        this.app = app;

        this.$navbarTitle = $("#navbar-title");
        this.$feed = $("#feed");
        this.$settingsDialog = $("#settings-dialog");
        this.$settingsButton = $("#settings-button");
        this.$percentNewSlider = $("#percent-new-slider");
        this.$percentNew = $("#percent-new");
        this.$percentPopular = $("#percent-popular");
        this.$subredditField = $("#subreddit-field");

        this.setupSettings();
    };

    RedditBigSpyView.prototype.setTitle = function (title) {
        this.$navbarTitle.text(title);
    };

    RedditBigSpyView.prototype.clear = function () {
        var that = this;
        this.$feed.hide("drop", function () {
            that.$feed.empty();
            that.$feed.show("drop");
        });
    };

    RedditBigSpyView.prototype.setupSettings = function () {
        var that = this, updatedPercentNew;

        // settings button/dialog

        this.$settingsDialog.dialog({
            "autoOpen": false,
            "dialogClass": "no-close",
            "buttons": {
                "Cancel": function () {
                    that.updatePercentNewView(that.app.percentNew);
                    that.$subredditField.val(that.app.api.currSubreddit);
                    $(this).dialog("close");
                },
                "Save": function () {
                    var newSub = $.trim(that.$subredditField.val());
                    var oldSub = that.app.api.currSubreddit || "";
                    var newTitle = "Posts from ";

                    that.app.percentNew = updatedPercentNew;

                    if (newSub !== oldSub) {
                        if (newSub) {
                            newTitle += "/r/" + newSub;
                        } else {
                            newTitle += "all subreddits";
                        }
                        that.setTitle(newTitle);
                        that.app.api.switchSubreddit(newSub);
                        that.clear();
                    }

                    $(this).dialog("close");
                },
            },
        });

        this.$settingsButton.click(function () {
            that.$settingsDialog.dialog("open");
        });

        // percent new/popular sliders/text

        this.updatePercentNewView(this.app.percentNew);

        this.$percentNewSlider.on("slide", function (evt, ui) {
            that.$percentNew.text(ui.value);
            that.$percentPopular.text(100 - ui.value);
            updatedPercentNew = ui.value;
        });

        // autocomplete

        this.$subredditField.autocomplete({
            "source": function (req, res) {
                that.app.api.searchSubreddits(req.term, function (subs) {
                    res(subs);
                });
            },
        });
    };

    RedditBigSpyView.prototype.updatePercentNewView = function (percentNew) {
        this.$percentNewSlider.slider({"value": percentNew});
        this.$percentNew.text(percentNew);
        this.$percentPopular.text(100 - percentNew);
    };

    RedditBigSpyView.prototype.getPostColor = function (score) {
        var green = tinycolor("#ffff4a"), desatAmount,
            upperBound = 3000.0;
        score = +score;
        desatAmount = score > upperBound ? 0: 100 - (100 * score / upperBound);
        return tinycolor.desaturate(green, desatAmount).toHexString();
    };

    RedditBigSpyView.prototype.showPost = function (post) {
        if (!post) {
            console.log("post was empty");
            return;
        }

        var that = this;
        var $li, $row, $score, $postTitle, $subredditLink,
            $subredditLabel, $commentsCell, $commentsLink;

        $li = $("
");
        $row = $("
");

        // score cell
        $score = $("").text(post.score);
        $score.css("background-color", this.getPostColor(post.score));
        $("
").append($score).appendTo($row);

        // title cell
        $subredditLink = $("")
            .attr("href", "http://reddit.com/r/" + post.subreddit)
            .text(post.subreddit);
        $subredditLabel = $("")
            .append($subredditLink);
        $postTitle = $("")
            .attr("href", post.url)
            .text(post.title);
        $("
")
            .append($subredditLabel)
            .append($postTitle)
            .appendTo($row);

        // comments cell
        $commentsLink = $("")
            .data("post-id", post.id)
            .attr("href", "#")
            .text("comments");
        $commentsLink.click(function () {
            that.clear();
            that.app.api.switchMode("comments");
            that.app.api.currPost = "/r/" + post.subreddit + "/comments/" +
                post.id;
        });
        $commentsCell = $("
")
            .appendTo($row)
            .append($commentsLink);

        $li.wrapInner($row).hide();
        var MAX_ITEMS = 30;
        this.$feed.prepend($li);
        $li.show("drop");

        if (this.$feed.children().length > MAX_ITEMS) {
            this.$feed.children().last().remove();
        }
    };

    /* Reddit BigSpy */

    var RedditBigSpy = function () {
        this.api = new RedditApi();
        this.percentNew = 20;
        this.postInterval = 2000;

        this.view = new RedditBigSpyView(this);
    };

    RedditBigSpy.prototype.start = function () {
        var that = this;
        this.api.refreshHot(function () {
            var firstPost = that.api.hotItem();
            window.setInterval(function () {
                var post = that.getPost();
                that.view.showPost(post);
            }, that.postInterval);
            that.view.showPost(firstPost);
        });
        this.api.refreshNew();
    };

    RedditBigSpy.prototype.getPost = function () {
        if (Math.random() <= this.percentNew / 100.0) {
            return this.api.newItem();
        }
        return this.api.hotItem();
    };

    /* MAIN */

    $(function () {
        var app = new RedditBigSpy();
        app.start();
    });
}(window.jQuery, window.tinycolor));
<|endoftext|>


<commit_before>game.TitleScreen = me.ScreenObject.extend({
	/**
	 *  action to perform on state change
	 */
	onResetEvent: function() {
		//inserts the title-screen image into the map
		me.game.world.addChild(new me.Sprite(0, 0, me.loader.getImage('title-screen')), -10); // TODO
	},

	/**
	 *  action to perform when leaving this screen (state change)
	 */
	onDestroyEvent: function() {
		; // TODO
	}
});<commit_msg>video 27 code and comment<commit_after>game.TitleScreen = me.ScreenObject.extend({
	/**
	 *  action to perform on state change
	 */
	onResetEvent: function() {
		//inserts the title-screen image into the map
		me.game.world.addChild(new me.Sprite(0, 0, me.loader.getImage('title-screen')), -10); // TODO
		//makes the enter key into an object
		me.input.bindKey(me.input.KEY.ENTER, "start");

		me.game.world.addChild(new (me.Renderable.extend({
			init: function(){
				//calls super class
				this._super(me.Renderable, 'init', [510, 30, me.game.viewport.width, me.game.viewport.height]);
				//sets the font to arial, the size 46, and colors it white
				this.font = new me.Font("Arial", 46, "white");
			},
			//function that sets up the writing
			draw: function(renderer){
				//inserts the message "Awesomenauts!" and sets where writing starts
				this.font.draw(renderer.getContext(), "Awesomenauts!", 450, 130);
				//inserts the message "Press ENTER to play!" and sets where writing starts
				this.font.draw(renderer.getContext(), "Press ENTER to play!", 250, 530);
			}
		})));
		//listens to see if enter button is pressed
		this.handler = me.event.subscribe(me.event.KEYDOWN, function (action, keyCode, edge){
			//runs if enter button is pressed
			if (action === "start") {
				//goes to play screen
				me.state.change(me.state.PLAY);
			}
		});
	},

	/**
	 *  action to perform when leaving this screen (state change)
	 */
	onDestroyEvent: function() {
		//removes the enter key's status as an object
		me.input.unbindKey(me.input.KEY.ENTER);
		//inform's program not to listen for pressed enter key
		me.event.unsubscribe(this.handler);
	}
});<|endoftext|>