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