Source: ARCore/snapshot.js

define(["lib/jquery",                "lib/underscore",        "lib/pixastic",
        "common/network/dispatcher", "common/util/log",       "./coordinate",
        "common/util/id_generator",  "common/network/tracer", "common/util/cap_utils",
        "common/util/check"],
function($,                           _,                       Pixastic,
         Dispatcher,                  Log,                     Coordinate,
         IDGenerator,                 Tracer,                  CapUtils,
         check) {

    "use strict";

    var logger = Log("Snapshot");

    //given an image uri, returns the base64
    var toBase64 = function(uri) {
        return uri.split(",")[1];
    };

    // given base64 data, constructs an image url
    var toImageURL = function(base64) {
        return "data:image/jpeg;base64," + base64;
    };

    // given base64 data, renders it to a canvas asynchronously
    var toCanvas = function(base64, callback) {
        var img = new Image();
        var canvas = document.createElement("canvas");
        var ctx = canvas.getContext("2d");

        img.onload = function() {
            canvas.width = img.width;
            canvas.height = img.height;
            ctx.drawImage(img, 0, 0);
            callback(canvas);
        };
        img.src = toImageURL(base64);
    };

    // returns a clone of a canvas
    var cloneCanvas = function(oldCanvas) {
        var newCanvas = document.createElement("canvas");

        var ctx = newCanvas.getContext("2d");
        ctx.drawImage(oldCanvas, 0, 0);

        newCanvas.width  = oldCanvas.width;
        newCanvas.height = oldCanvas.height;

        return newCanvas;
    };

    // Crops a snapshot to a given element.
    var cropToElement = function(snapshot, element, callback) {
        $.when.apply($, Coordinate.forElement(element).map(function(coordinate) {
            return coordinate.transform(Coordinate.PROJECTOR);
        })).then(function(topLeft, bottomRight) {
            // calculate crop parameters
            var cropParams = {};
            cropParams.left   = topLeft.x - $(window).scrollLeft();
            cropParams.top    = topLeft.y - $(window).scrollTop();
            cropParams.width  = bottomRight.x - topLeft.x;
            cropParams.height = bottomRight.y - topLeft.y;

            // modify the Snapshot
            callback(snapshot.crop(cropParams));
        });
    };

    // enable dev mode: respond to snapshot requests with an html2canvas
    // screenshot
    Lens._onDev(function(){
        Dispatcher._bridge.registerAsyncRPC("snapshot", function(data, callback) {
            require(["lib/html2canvas"], function(html2canvas) {
                html2canvas([document.body], {
                    onrendered: function(canvas) {
                        // set the background to white -- we get it as transparent,
                        // which becomes black when we convert to jpeg
                        var ctx = canvas.getContext("2d");
                        ctx.globalCompositeOperation = "destination-over";
                        ctx.fillStyle = "rgba(255, 255, 255, 1)";
                        ctx.fillRect(0, 0, canvas.width, canvas.height);

                        // send the base64-encoded data to the callback
                        callback({
                            picture: toBase64(canvas.toDataURL("image/jpeg"))
                        });
                    }
                });
            });
        });
    });

    /**
     * @privconstructor
     * @class
     * Snapshots from the LuminAR camera. Capturing snapshots is asynchronous,
     * so use {@link Lens.Snapshot.capture} to get a `Snapshot` instance.
     *
     * @memberOf Lens
     * @privConstructor
     */
    var Snapshot = function(canvas) {
        /** @alias Lens.Snapshot.prototype */
        var self = {};

        /**
         * Converts the snapshot to an image.
         * @return {Image} HTML image element.
         */
        self.toImage = function() {
            var img = new Image();
            img.src = toImageURL(self.toBase64());
            return img;
        };

        /**
         * Returns a canvas element that contains the snapshot.
         * @return {Canvas}
         */
        self.toCanvas = function() {
            return cloneCanvas(canvas);
        };

        /**
         * Returns the base64-encoded JPEG data of this snapshot.
         * @return {String}
         */
        self.toBase64 = function() {
            return toBase64(canvas.toDataURL("image/jpeg"));
        };

        /**
         * Returns a new Snapshot that's a cropped version of this one.
         *
         * @param {Object} opts           Cropping options
         * @param {Number} [opts.left]    Number of pixels to remove from the left.
         *                                Defaults to 0.
         * @param {Number} [opts.top]     Number of pixels to remove from the top.
         *                                Defaults to 0.
         * @param {Number} [opts.width]   Width of the cropped image. Defaults to
         *                                the width of the image, minus opts.left.
         * @param {Number} [opts.height]  Height of the cropped image. Defaults to
         *                                the height of the image, minus opts.top.
         *
         * @return {Snapshot} The cropped Snapshot.
         */
        self.crop = function(opts) {
            check(opts, {
                left: check.Match.Optional(Number),
                top: check.Match.Optional(Number),
                width: check.Match.Optional(Number),
                height: check.Match.Optional(Number)
            });

            var left   = opts.left   || 0;
            var top    = opts.top    || 0;
            var width  = opts.width  || (canvas.width -  left);
            var height = opts.height || (canvas.height - top);

            var newCanvas = Pixastic.process(canvas, "crop", {
                left: left,
                top: top,
                width: width,
                height: height
            });

            return Snapshot(newCanvas);
        };

        /**
         * Returns a new Snapshot that's a scaled version of this one.
         *
         * @params {Object} opts   Scaling options. At least one required. If
         *                         only one is given, the other will be set to
         *                         preserve the aspect ratio.
         * @param  {Number} [opts.width]  The width of the new Snapshot
         * @param  {Number} [opts.height] The height of the new Snapshot
         *
         * @return {Snapshot} The resized new Snapshot.
         */
        self.scale = function(opts) {
            check(opts, {
                width: check.Match.Optional(Number),
                height: check.Match.Optional(Number)
            });

            var width, height;

            if(!opts.width && !opts.height) {
                throw new TypeError("Neither width nor height give to Snapshot#scale");
            }
            else if(!opts.height) {
                width  = opts.width;
                height = width * (canvas.height / canvas.width);
            }
            else if(!opts.width) {
                height = opts.height;
                width  = height * (canvas.width / canvas.height);
            }
            else {
                width  = opts.width;
                height = opts.height;
            }

            var newCanvas = Pixastic.process(canvas, "resize", {
                width: width,
                height: height
            });

            return Snapshot(newCanvas);
        };

        Object.freeze(self);
        return self;
    };

    _.extend(Snapshot, /** @lends Lens.Snapshot */ {
        /**
         * Captures a snapshot from the Lens HD camera.
         *
         * @param {string | Element | jQuery} [element]
         * If given, this snapshot will be of just the contents of this div.
         * Otherwise, it will be a full-size, full-resolution snapshot from the
         * HD LuminAR camera. This can be a CSS selector string, DOM element,
         * or jQuery object. This element must be rendered to the screen; it
         * can't have been hidden with `display: none` in CSS or with jQuery's
         * `.hide()`.
         *
         * @param {Function} callback
         * When the snapshot has been taken, this is called with the Snapshot as
         * an argument.
         *
         * @param {Function} errorCallback
         * If an error occurs when a snapshot is taken, this callback is called
         * with the error message as an argument.
         */
        capture: function() {
            // extract the arguments
            var callback, element, errorCallback;

            if(arguments.length === 1) {
                check(arguments[0], Function);
                callback = arguments[0];
                element = null;
            }
            else if(arguments.length >= 2) {
                check(arguments[0], check.Match.Elementish);
                check(arguments[1], Function);
                check(arguments[2], check.Match.Optional(Function));

                callback = arguments[1];
                var elements = $(arguments[0]);

                if(elements.length !== 1) {
                    throw "Argument error: argument to Lens.Snapshot.capture was not a single element.";
                }

                element = elements[0];

                errorCallback = arguments[2] || function(message) {
                    logger.warn("Snapshot failed.", message);
                };
            }
            else {
                throw new TypeError("Snapshot.capture requires 1 or 2 arguments.");
            }

            check(callback, Function);

            var traceId = IDGenerator.traceId();
            Tracer.post("requesting snapshot", traceId, "snapshot_request", "Lens Outgoing");


            // call the snapshot RPC after ensuring snapshots are on
            CapUtils.ensure("SnapshotModule", logger, function() {
                Dispatcher._bridge.callRPC("snapshot", {camera: "hd", transform: !!element}, function(data, traceId) {
                    Tracer.post("got snapshot", traceId);

                    if (data.error) {
                        if (errorCallback) {
                            errorCallback(data.error, traceId);
                        }
                        return;
                    }

                    // conver the base64 data to a snapshot
                    toCanvas(data.picture, function(canvas) {
                        Tracer.post("rendered to canvas", traceId);
                        var snapshot = new Snapshot(canvas);
                        Tracer.post("created Snapshot object", traceId);
                        if(element) {
                            cropToElement(snapshot, element, function(cropped) {
                                Tracer.post("cropped snapshot", traceId);
                                callback(cropped, traceId);
                            });
                        }
                        else {
                            // no element given; just return the snapshot
                            callback(snapshot, traceId);
                        }
                    });
                }, traceId);
            });
        }
    });

    Lens._addMember(Snapshot, "Snapshot");
    return Snapshot;
});