define(["lib/jquery", "lib/underscore", "common/network/dispatcher",
"common/util/log", "./snapshot", "common/util/id_generator",
"common/util/transformations", "common/util/cap_utils", "common/util/binding_list",
"common/util/check", "ARCore/coordinate"],
function($, _, Dispatcher,
Log, Snapshot, IDGenerator,
Transformations, CapUtils, BindingList,
check, Coordinate) {
"use strict";
var logger = Log("Marker");
// map of UUID -> Marker
var markers = {};
// given a loaded image, returns the base64-encoded PNG data for the image.
var imageToBase64 = function(image) {
var canvas = document.createElement("canvas");
var ctx = canvas.getContext("2d");
canvas.width = image.width;
canvas.height = image.height;
ctx.drawImage(image, 0, 0);
return canvas.toDataURL("image/png").split(",")[1];
};
// given image data, mask data, and an id, registers a Marker.
var registerMarker = function(id, imageData, maskData) {
var data = {id: id, image: imageData};
// parse arguments
if(maskData) {
data.mask = maskData;
}
// send registration message after ensuring that markers are on
CapUtils.ensure("MarkersModule", logger, function() {
Dispatcher._bridge.fireEvent(
"marker_register",
data
);
});
};
// given image data, renders it to an image
var renderImage = function(imageData, callback) {
var img = new Image();
img.onload = function() {
callback(img);
};
img.src = "data:image;base64," + imageData;
};
// returns the given property of the given object, or null if the object
// is null.
var getPropOrNull = function(obj, prop) {
if(obj === null) {
return null;
}
else {
return obj[prop];
}
};
// array of marker ids seen in the last update
var lastMarkers = [];
// subscribe to incoming updates
Dispatcher._bridge.subscribe("marker_updates", function(updates) {
var currentMarkers = _.keys(updates);
var appeared = _.difference(currentMarkers, lastMarkers);
var disappeared = _.difference(lastMarkers, currentMarkers);
lastMarkers = currentMarkers;
if(appeared.length > 0) {
logger.info("Markers appeared: ", appeared);
}
if(disappeared.length > 0) {
logger.info("Markers disappeared: ", disappeared);
}
// inform appearance subscribers
_.each(appeared, function(id) {
markers[id].appearCallback();
});
// inform disappearance subscribers
_.each(disappeared, function(id) {
markers[id].disappearCallback();
});
// iterate over each update. apply this app's offset to the translation
// terms of the transforms
Coordinate.getWindowOffset(function(top, left) {
_.each(updates, function(value, key) {
value[2] = value[2] - left;
value[6] = value[6] - top;
// update the marker's transform and mark it dirty
markers[key].moveCallback(value);
});
});
});
// insert a container for pinned markers or return an existing container
var markerContainer = function() {
if($("body #lens-marker-container").length === 0) {
$("<div />").attr("id", "lens-marker-container")
.css( "position", "fixed")
.css( "top", "0")
.css( "left", "0")
.css( "width", $(window).width())
.css( "height", $(window).height())
.css( "pointer-events", "none")
.css( "transform-style", "preserve-3d")
.css( "perspective", "Infinity")
.css( "perspective-origin", "top left")
.appendTo("body");
}
return $("body #lens-marker-container");
};
/**
* Creates a new Marker, given a marker id.
*
* @param {String} id Marker id.
* @param {Number} origWidth Original width of the tracked image.
* @param {Number} origHeight Original height of the tracked image.
*
* @privconstructor
* @class
* Markers are images that LuminAR watches for and tracks. They can be used
* to track physical objects within LuminAR's field of view. They can track
* objects even if they are rotated or scaled.
*
* @property {String} id Unique ID for the Marker.
*
* @property {Number} origWidth
* Original width of the tracked image. May be null for a short time after
* marker creation.
*
* @property {Number} origHeight
* Original height of the tracked image. May be null for a short time after
* marker creation.
*
* @memberOf Lens
*/
var Marker = function(id, origWidth, origHeight) {
/** @alias Lens.Marker.prototype */
var self = {};
// The CSS transform representing the marker's position.
var transform = null;
// The decomposed CSS transform representing the marker's position.
var decomposed = null;
// Whether the decomposed CSS trasform is out of date and needs to
// be regenerated
var transformDirty = false;
// List of movement subscribers
var movementSubscribers = BindingList();
/// List of appearance subscribers
var appearanceSubscribers = BindingList();
// List of disappearance subscribers
var disappearanceSubscribers = BindingList();
// updates the decomposed trasform of this Marker if needed, and returns
// the updated decomposition.
var decompose = function() {
if(transformDirty) {
if(transform) {
decomposed = Transformations.decompose(self.getTransform());
}
else {
decomposed = null;
}
transformDirty = false;
}
return decomposed;
};
// gets the given properties from the decomposed transform of the marker,
// or null if the marker is not visible.
var getDecomposedValue = function(prop) {
return getPropOrNull(decompose(), prop);
};
var appearCallback = function() {
appearanceSubscribers.callAll(self);
};
var disappearCallback = function() {
disappearanceSubscribers.callAll(self);
};
var moveCallback = function(newTransform) {
transform = newTransform;
transformDirty = true;
movementSubscribers.callAll(self);
};
/**
* Sets the original width and height of this marker. Used so factories
* can calculate these dimensions asynchronously.
* @param {Number} x
* @param {Number} y
* @private
*/
self._setOrigDims = function(width, height) {
origWidth = width;
origHeight = height;
};
/**
* Subscribes a function to be called any time this marker is moved.
* @param {Function} fn The function to subscribe. Gets this marker
* as an argument when called.
* @return {Binding} A binding that allows this handler to be
* cleared.
*/
self.moved = function(fn) {
check(fn, Function);
return movementSubscribers.add(fn);
};
/**
* Subscribes a function to be called any time this marker appears.
* @param {Function} fn The function to subscribe. Gets this marker
* as an argument when called.
* @return {Binding} A binding that allows this handler to be
* cleared.
*/
self.appeared = function(fn) {
check(fn, Function);
return appearanceSubscribers.add(fn);
};
/**
* Subscribes a function to be called any time this marker disappears.
* @param {Function} fn The function to subscribe. Gets this marker
* as an argument when called.
* @return {Binding} A binding that allows this handler to be
* cleared.
*/
self.disappeared = function(fn) {
check(fn, Function);
return disappearanceSubscribers.add(fn);
};
/**
* Stops tracking of this marker.
*/
self.deregister = function() {
Dispatcher._bridge.fireEvent("marker_deregister", { id: id });
};
/**
* Pins an element to this marker. The element's CSS will be modified to
* move/resize/rotate/transform it to match the tracked object.
*
* @param {string | Element | jQuery} el
* The element to track with. This can be a CSS selector string, DOM
* element, or jQuery object.
*
* @param {Boolean} [clone]
* If true, a clone of the element will be used to track the object.
* The clone will be destroyed when the object is no longer visible.
* Defaults to false.
*
* @param {Function} [onPinStart]
* Optional. If given, this function will be invoked when we start
* pinning the element. It will get the pinned element as an argument.
*/
self.pin = function(el, clone, onPinStart) {
check(el, check.Match.Elementish);
check(clone, check.Match.Optional(Boolean));
check(onPinStart, check.Match.Optional(Function));
var pinned;
if(clone) {
pinned = $(el).clone();
}
else {
pinned = $(el);
}
if((pinned.length !== 1) || !(pinned[0] instanceof Element)) {
throw "Argument error: invalid element passed to pin()";
}
self.appeared(function(marker) {
pinned.remove().appendTo(markerContainer());
pinned.css("position", "absolute");
pinned.css("top", 0);
pinned.css("left", 0);
pinned.css("transform-origin", "top left");
pinned.css("perspective-origin", "top left");
pinned.css("width", marker.origWidth);
pinned.css("height", marker.origHeight);
});
self.moved(function(marker) {
pinned.css("transform", marker.getCSSTransform());
});
};
/**
* Return true if this marker is on screen, false otherwise.
* @return {Boolean}
*/
self.visible = function() {
return !!transform;
};
/**
* Returns a 4x4 transformation matrix that represents the
* transformation of a rectangle the side of the tracked image anchored
* at the top-left corner of the screen to the current positon of the
* marker. Returns null if the marker is not visible.
*
* @return {Array} 16-element array of Numbers representing the 3-D
* transformation matrix in row-major order.
*/
self.getTransform = function() {
var t;
if(transform === null) {
t = [1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1];
}
else {
t = transform;
}
return [
t[0], t[4], 0, t[8],
t[1], t[5], 0, t[9],
0, 0, 1, 0,
t[2], t[6], 0, t[10]
];
};
/**
* Returns a CSS transform that represents the transformation of a
* rectangle the side of the tracked image anchored at the top-left
* corner of the screen to the current positon of the marker. Returns
* a transform with the identity matrix if the marker is not on screen.
*
* @return {String} The CSS transform, suitable for use as the value
* of the "transform" CSS property.
*/
self.getCSSTransform = function() {
var t = self.getTransform();
return "matrix3d(" +
t[0] + "," + t[1] + "," + t[2] + "," + t[3] + "," +
t[4] + "," + t[5] + "," + t[6] + "," + t[7] + "," +
t[8] + "," + t[9] + "," + t[10] + "," + t[11] + "," +
t[12] + "," + t[13] + "," + t[14] + "," + t[15] + ") ";
};
/**
* Gets the X coordinate of the left side of the marker. Return null if
* the marker is not visible.
* @return{Number} X coordinate, in pixels.
*/
self.getX = function() {
return getPropOrNull(getDecomposedValue("translate"), "x");
};
/**
* Gets the Y coordinate of the top side of the marker. Return null if
* the marker is not visible.
* @return {Number} Y coordinate, in pixels.
*/
self.getY = function() {
return getPropOrNull(getDecomposedValue("translate"), "y");
};
/**
* Gets the angle of rotation of this marker. Return null if the
* marker is not visible.
* @return {Number} Rotation, in radians, clockwise.
*/
self.getRotation = function() {
return getPropOrNull(getDecomposedValue("rotate"), "z");
};
/**
* Gets the scale of the object, e.g. how much larger or smaller it is
* than the tracked image. Return null if the marker is not visible.
* @return {Number} The scale, as a multiple of the original size (e.g.
* a value of 1 indicates no scale).
*/
self.getScale = function() {
var scale = getDecomposedValue("scale");
if(scale === null) {
return null;
}
return (scale.x + scale.y) / 2;
};
/**
* Gets the skew of the object, e.g. how much is is deformed into a
* trapezoid, or null if the marker is not visible.
* @return {Number} The skew of the object.
*/
self.getSkew = function() {
return getPropOrNull(getDecomposedValue("skew"), "x");
};
self.id = id;
Object.defineProperty(self, "origWidth", { get: function() { return origWidth; } });
Object.defineProperty(self, "origHeight", { get: function() { return origHeight; } });
markers[id] = {
marker: self,
appearCallback: appearCallback,
disappearCallback: disappearCallback,
moveCallback: moveCallback
};
Object.freeze(self);
return self;
};
_.extend(Marker, /** @lends Lens.Marker */ {
/**
* Creates a marker from base64-encoded PNG data.
*
* @param {String} image
* Base64-encoded data.
* @param {String} [mask]
* Optional base64-encoded mask. The mask should be an image of the
* same size as the image with only black and white pixels. Any areas
* of black will be ignored when processing the image.
*
* @return {Lens.Marker} The newly-created Marker.
*/
fromImageData: function(image, mask) {
check(image, String);
check(image, check.Match.Optional(String));
var id = IDGenerator.uuid();
registerMarker(id, image, mask);
var marker = Marker(id, null, null);
// asynchronously render image, update original width and height
// when done.
renderImage(image, function(rendered) {
marker._setOrigDims(rendered.width, rendered.height);
});
// return a marker with unkown original width and height
return marker;
},
/**
* Creates a marker from the URL of a PNG image.
*
* @param {String} imageURL
* URL of an image. The image must be on the same host as the Lens
* app to comply with the same-origin policy.
* @param {String} [maskURL]
* Optional URL of a mask image. The image must be on the same host
* as the Lens app to comply with the same-origin policy. The mask
* should be an image of the same size as the image with only black
* and white pixels. Any areas of black will be ignored when
* processing the image.
* @return {Lens.Marker} The newly-created Marker.
*/
fromImageURL: function(imageURL, maskURL) {
check(imageURL, String);
check(maskURL, check.Match.Optional(String));
var image, mask;
var id = IDGenerator.uuid();
var marker = Marker(id, null, null);
// callback for when image and mask load
var onLoad = function() {
if(image.complete && (!mask || mask.complete)) {
// both loaded, convert to base64 and register the marker
var imageData = imageToBase64(image);
var maskData = null;
if(mask) {
maskData = imageToBase64(mask);
}
registerMarker(id, imageData, maskData);
// set the original width and height
marker._setOrigDims(image.width, image.height);
}
};
// start loading mask and image
if(maskURL) {
mask = new Image();
mask.onload = onLoad;
mask.src = maskURL;
}
image = new Image();
image.onload = onLoad;
image.src = imageURL;
// return the marker with unknown orig width and height
return marker;
},
/**
* Creates a marker by taking a snapshot of a page element with
* the LuminAR camera and tracking that image.
*
* @param {string | Element | jQuery} el
* The snapshot taken by the LuminAR HD camera will be cropped to this
* element. 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()`.
*
* @return {Lens.Marker} The newly-created Marker.
*/
fromSnapshot: function(el) {
check(el, check.Match.Elementish);
var id = IDGenerator.uuid();
var marker = Marker(id, null, null);
Snapshot.capture(el, function(snapshot) {
// register the marker
registerMarker(id, snapshot.base64);
// get the width and height of the snapshot
var img = snapshot.toImage();
marker._setOrigDims(img.width, img.height);
});
return marker;
},
/**
* Stops tracking of all markers.
*/
clear: function() {
Dispatcher._bridge.fireEvent("marker_clear");
}
});
$(window).unload(function() {
Marker.clear();
});
Lens._addMember(Marker, "Marker");
return Marker;
});