blob: 78616ea7c03b2198d2f7be187b5bf0c54b7b6403 [file] [log] [blame]
<!-- The <crosshair-sk> custom element declaration.
The crosshair-sk element works with the canvas-layers-sk element
and draws a crosshair on one of the given layers. Use the update_on
attribute to control when the crosshair is moved.
Attributes:
x, y - The x,y coords of the crosshair, as measured in the natural
dimensions of the image. I.e. not in the coords of the img
if it has a different size as determined by CSS styling.
target - The id of the canvas-layers-sk element we are drawing
a crosshair on.
name - The name of the layer in the canvas-layer-sk element
we are drawing a crosshair on.
hidden - If true then don't actually draw the crosshair.
update_on - Update the crosshair:
"move" - when the mouse moves.
"click" - when the user clicks.
"none" - (default) do not update the crosshair based on user action.
Events:
crosshair - Produced if the crosshair is moved by clicking in the image.
The coordinates of the click are in e.detail.x and e.detail.y. They
are in offsets from the image origin in the images natural dimensions.
That is, even if the image is sized using CSS the x and y values
will be reported as values in the images original size.
Methods:
coordinatesUpdated - to be called if the x or y attribute was changed by other code.
prefer calling this and setting useObservers=false if you control all the places which modify
x and y.
-->
<link rel="import" href="/res/imp/bower_components/polymer/polymer.html">
<dom-module id="crosshair-sk">
<template>
</template>
<script>
Polymer({
is: 'crosshair-sk',
properties: {
x: {
type: Number,
value: 0,
reflectToAttribute: true,
notify: true,
observer: "_updatedOneCoord",
},
y: {
type: Number,
value: 0,
reflectToAttribute: true,
notify: true,
observer: "_updatedOneCoord",
},
useObservers: {
type: Boolean,
value: true,
reflectToAttribute: true,
},
target: {
type: String,
value: "",
observer: '_layerChanged',
reflectToAttribute: true
},
hidden: {
type: Boolean,
value: false,
observer: '_hiddenChanged',
reflectToAttribute: true
},
name: {
type: String,
value: "",
observer: '_layerChanged',
reflectToAttribute: true
},
update_on: {
// Valid values are "click", "move", and "none".
type: String,
value: "none",
observer: '_registerCallbacks',
reflectToAttribute: true
},
},
ready: function () {
// The current location of the crosshair, in CSS units.
this.thumb = {
x: 0,
y: 0,
};
// If we are tracking mousemove events we'll store the coords here.
this._move = {
x: 0,
y: 0,
};
// If we are tracking mousemove events we'll store the previous coords
// here, so we only redraw the crosshair if it's actually moved.
this._lastMove = {
x: 0,
y: 0,
};
this.willUpdate = null;
this._xyChangedBound = this.coordinatesUpdated.bind(this);
this._moveEventHandlerBound = this._moveEventHandler.bind(this);
this._clickEventHandlerBound = this._clickEventHandler.bind(this);
this._moveToMouseBound = this._moveToMouse.bind(this);
this._animRequest = null;
},
_updatedOneCoord() {
if (!this.useObservers) { return; }
// Wait and see if the other coordinate updates.
if (!this.willUpdate) {
this.willUpdate = setTimeout(() => {
this.coordinatesUpdated();
}, 1);
}
},
coordinatesUpdated() {
this.willUpdate = null;
if (!this._img) { return; }
this._imgSize = this._standardSize(this._img);
this.thumb.x = this._imgSize.visibleWidth * (this.x / this._imgSize.naturalWidth);
this.thumb.y = this._imgSize.visibleHeight * (this.y / this._imgSize.naturalHeight);
this._drawCrosshair();
this._event();
},
// Returns both the visible and natural size of a canvas or image element,
// Natural size - the width and height of it in it's native resolution.
// Visible size - the width and height of it on the screen as controlled by css
_standardSize(element) {
if (element.nodeName === 'IMG') {
return {
'visibleWidth': element.width,
'visibleHeight': element.height,
'naturalWidth': element.naturalWidth,
'naturalHeight': element.naturalHeight,
};
} else {
var strW = window.getComputedStyle(element, null).width;
var strH = window.getComputedStyle(element, null).height;
return {
// Trim 'px' off the end of the style string and convert to a number.
'visibleWidth': parseFloat(strW.substring(0, strW.length-2)),
'visibleHeight': parseFloat(strH.substring(0, strH.length-2)),
'naturalWidth': element.width,
'naturalHeight': element.height,
};
}
},
_layerChanged: function() {
if (!(this.target && this.name)) {
return;
}
$$$("#" + this.target, this._findParent()).addEventListener(
'canvas-layers-updated', this._canvasLayersUpdated.bind(this));
},
_hiddenChanged: function() {
if (this._ctx) {
this._drawCrosshair();
}
},
_findParent: function() {
var p = this.parentNode;
while (p.parentNode != null) {
p = p.parentNode;
}
return p
},
_canvasLayersUpdated: function() {
var layer = $$$("#" + this.target, Polymer.dom(this).parentNode);
if (this._canvas) {
// If we are switching to a new canvas then unregister all of our old callbacks.
this._unregisterAllCallbacks();
}
this._canvas = layer.canvas(this.name);
this._img = layer.subject();
if (this._canvas) {
this._registerCallbacks();
this._ctx = this._canvas.getContext('2d');
}
this._imgSize = this._standardSize(this._img);
},
_registerCallbacks: function() {
if (!this._canvas) {
return;
}
this._canvas.addEventListener('canvas-update', this._xyChangedBound);
if (this.update_on === "click") {
this._canvas.addEventListener('click', this._clickEventHandlerBound);
this._canvas.removeEventListener('mousemove', this._moveEventHandlerBound);
} else if (this.update_on === "move") {
this._canvas.addEventListener('mousemove', this._moveEventHandlerBound);
this._canvas.removeEventListener('click', this._clickEventHandlerBound);
} else {
this._canvas.removeEventListener('mousemove', this._moveEventHandlerBound);
this._canvas.removeEventListener('click', this._clickEventHandlerBound);
}
},
_unregisterAllCallbacks: function() {
this._canvas.removeEventListener('mousemove', this._moveEventHandlerBound);
this._canvas.removeEventListener('click', this._clickEventHandlerBound);
this._canvas.removeEventListener('canvas-update', this._xyChangedBound);
},
_moveEventHandler: function (e) {
this._move.x = e.clientX;
this._move.y = e.clientY;
if (this._lastMove.x != this._move.x || this._lastMove.y != this._move.y) {
// Don't necessarily move it right now. Several more mouse move events may be triggered
// before it's time to draw the next frame. register or replace our callback with the
// window to move the crosshair before the next frame.
this._animRequest = window.requestAnimationFrame(this._moveToMouseBound);
this._lastMove.x = this._move.x;
this._lastMove.y = this._move.y;
}
},
// Moving to the last recorded mouse position. Registered as the callback
// of window.requestAnimationFrame
_moveToMouse(_) {
this._moveCrosshair(this._move.x, this._move.y);
},
// moving to a clicked position.
_clickEventHandler: function (e) {
this._moveCrosshair(e.clientX, e.clientY);
},
_moveCrosshair: function (clientX, clientY) {
if (!this._img.width || !this._img.height) {
return;
}
var p = sk.elePos(this._canvas);
this.thumb.x = clientX - p.x;
this.thumb.y = clientY - p.y;
this._drawCrosshair();
// Cannot set properties atomically in Polymer 1
this.x = Math.floor(this.thumb.x / this._imgSize.visibleWidth * this._imgSize.naturalWidth);
this.y = Math.floor(this.thumb.y / this._imgSize.visibleHeight * this._imgSize.naturalHeight);
this._event();
},
_event: function() {
var detail = {
x: this.x,
y: this.y
};
var evt = new CustomEvent('crosshair', {
detail: detail,
bubbles: true
});
this.dispatchEvent(evt);
},
_drawCrosshair: function () {
this._ctx.clearRect(0, 0, this._ctx.canvas.width, this._ctx.canvas.height);
if (this.hidden) {
return;
}
this._ctx.lineWidth = 1;
this._ctx.strokeStyle = '#F00';
this._ctx.beginPath();
this._ctx.moveTo(0, this.thumb.y + 0.5);
this._ctx.lineTo(this._ctx.canvas.width, this.thumb.y + 0.5);
this._ctx.stroke();
this._ctx.beginPath();
this._ctx.moveTo(this.thumb.x + 0.5, 0);
this._ctx.lineTo(this.thumb.x + 0.5, this._ctx.canvas.height);
this._ctx.stroke();
}
});
</script>
</dom-module>