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; });