blob: 0da3aec012c9e09dd2d3fe095d61c74026f70cfe [file] [log] [blame]
<!-- The <zoom-sk> custom element declaration.
The zoom-sk element presents a zoomed in view of a source img element.
source - The id of an img element to present a zoom for.
Affects only which element is used as source at creation time. to modify it
at a later time, call changeSource(element).
x, y - The point to center the zoom on. The units are in the natural size
of the source image.
pixels - The number of pixels to display in the horizontal direction.
allow_draw - used to block mouse move zoom function before data is available.
zoom-point - This event is generated whenever the zoom moves. The detail
of the event contains the values for the color:
x: 100,
y: 100,
r: 0xff,
g: 0xef,
b: 0xd5,
a: 0xff,
rgb: "rbg(255, 239, 213, 1.0)",
hex: "#ffefd5ff",
zoom-loaded - This event is generated whenever the zoom has finished
updating the zoomed view after an image load. It is never produced when
zoom source is a canvas since you are expected to have synchronously
called updateZoom in that case.
click-to-move - This event is generated if the user clicks on the zoom element.
x - the position of the click in the coordinate space of the source image/canvas
in it's native resolution.
y - ||
updateZoom() - To be called if the source img/canvas in the zoomed region has changed,
or the x,y location has changed. Call this directly, as it is not registered
as an observer on x or y to avoid it being called twice and slowing you down.
getImageData() - Returns the image data of the image wrapped by this zoom-sk element.
getPixelColor(x,y) - Returns the color of the pixel as an array in RGBA format.
changeSource(element) - Sets the img or canvas element acting as the source of zoom data.
<link rel="import" href="/res/imp/bower_components/iron-resizable-behavior/iron-resizable-behavior.html">
<dom-module id="zoom-sk">
<style type="text/css" media="screen">
#copy {
display: none;
#zoom {
margin: 0;
padding: 0;
:host {
padding: 0;
<canvas id=zoom width=10 height=10></canvas>
<canvas id=copy width=10 height=10></canvas>
is: 'zoom-sk',
behaviors: [
// Listen for resize events and change the size of the #zoom canvas accordingly.
listeners: {
'iron-resize': '_onIronResize'
attached: function() {
this.async(this.notifyResize, 1);
properties: {
source: {
type: String,
value: "",
x: {
type: Number,
value: 0,
reflectToAttribute: true,
y: {
type: Number,
value: 0,
reflectToAttribute: true,
pixels: {
type: Number,
value: 10, // how many pixels to draw
reflectToAttribute: true,
observer: '_onIronResize',
pixel_size: {
type: Number,
value: 13, // # how large to draw each pixel
reflectToAttribute: true,
observer: '_onIronResize',
allow_draw: {
type: Boolean,
value: false,
hide_grid: {
type: Boolean,
value: false,
observer: 'updateZoom',
ready: function () {
// Grab the img or canvas we are zooming from.
this.changeSource($$$('#' + this.source, this._findParent()));
// The canvas context we are drawing the zoomed pixels on.
this.ctx = this.$.zoom.getContext('2d');
// A click on the zoom view can be used to move it.
this.$.zoom.addEventListener('click', (e) => {
let halfsize = Math.floor(this.pixels / 2);
let gridX = Math.floor(e.offsetX / this.pixel_size) - halfsize;
let gridY = Math.floor(e.offsetY / this.pixel_size) - halfsize;
// Dispatch an event indicating the user would like to move the selection.
let detail = {
x: this.x + gridX,
y: this.y + gridY,
let evt = new CustomEvent('click-to-move', {
detail: detail,
bubbles: true
changeSource(element) {
this._source = element;
if (this._source.nodeName === 'IMG') {
this._source.addEventListener('load', function() {
this.allow_draw = true;
this.dispatchEvent(new CustomEvent('zoom-loaded', { bubbles: true }));
// Only works when source is an image.
getImageData: function() {
var c = this.$.copy;
return c.getContext('2d').getImageData(0, 0, c.width, c.height);
// Only works when source is an image.
getPixelColor: function(x,y) {
return this.$.copy.getContext('2d').getImageData(x, y, 1, 1).data;
_findParent: function() {
var p = this.parentNode;
while (p.parentNode != null) {
p = p.parentNode;
return p
_onIronResize: function() {
var w = this.pixels * this.pixel_size + 1;
this.$.zoom.width = w;
this.$.zoom.height = w;
this.$.zoom.parentElement.width = w;
this.$.zoom.parentElement.height = w;
_cloneImage: function() {
if (!this._source) { return; }
this.$.copy.width = this._source.naturalWidth;
this.$.copy.height = this._source.naturalHeight;
this._source, 0, 0, this._source.naturalWidth, this._source.naturalHeight);
updateZoom: function() {
if (!this.allow_draw) { return; }
if (!this.ctx) { return; }
// Clears to transparent black.
this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
this.ctx.lineWidth = 1;
this.ctx.strokeStyle = '#000';
// Draw out each pixel as a rect on the target canvas, as this works around FireFox doing a
// blur as it copies from one canvas to another.
var size = this.pixels; // number of zoomed pixels to show (width and height)
var halfsize = Math.floor(size / 2);
var ps = this.pixel_size;
var colors;
var flip = false;
// Copy out the color data from the region we are zooming into.
// Do it differently based on what kind of element is being copied out of.
if (this._source.nodeName === "IMG") {
// If it is an image, assume it has been cloned into the canvas called copy, which has a
// '2d' context.
colors = this.$.copy.getContext('2d').getImageData(
this.x-halfsize, this.y-halfsize, size, size).data;
} else if (sourceCtx = this._source.getContext('2d')) {
// Note that if it is a canvas, we can assume getContext has been called before, and the
// element is locked into a certain kind of context (2d, webgl, or webgl2). If we ask for
// the one it is locked into, it will be returned, otherwise getContext will return null.
// We use this both to fetch the context and determine what type it is.
// If zooming from a canvas with a 2d context, call getImageData directly.
// (no need for intermediate copy)
colors = sourceCtx.getImageData(this.x-halfsize, this.y-halfsize, size, size).data;
} else if ((sourceCtx = this._source.getContext('webgl2')) ||
(sourceCtx = this._source.getContext('webgl'))) {
// If zooming from a canvas with a webgl context, getImageData will not work.
// We can use the similar readPixels function, assuming the context was created with
// preserveDrawingBuffer=1
colors = new Uint8Array(size*size*4); // 4 bytes for 8888 RGBA
this.x-halfsize, (sourceCtx.drawingBufferHeight-this.y)-halfsize, size, size,
sourceCtx.RGBA, sourceCtx.UNSIGNED_BYTE, colors);
// Indicate to the code below that this buffer is vertically flipped. readPixels origin
// is at the bottom left. HTML canvas origin is at the top left.
flip = true;
var selectedPixelOffset = 0;
var selectedColorRGB;
// Now draw each zoomed pixel as a rect.
for (var x = 0; x < size; x++) {
for (var y = 0; y < size; y++) {
// Calulate the offset of this pixel into the buffer from x and y.
var offset = (y * size + x) * 4;
// If the buffer is vertically flipped, look for row size-1-y instead.
if (flip) {
offset = ((size - 1 - y) * size + x) * 4;
if (x === halfsize && y === halfsize) {
selectedPixelOffset = offset;
selectedColorRGB = sk.colorRGB(colors, offset, true);
var colorRGBStyle = sk.colorRGB(colors, offset, false);
this.set('ctx.fillStyle', colorRGBStyle);
if (this.hide_grid) {
// Draw the pixel full size
this.ctx.fillRect(x*ps, y*ps, ps, ps);
} else {
// inset the rect by 1 pixel to give an implicit grid
this.ctx.fillRect(x*ps+1, y*ps+1, ps-1, ps-1);
// Box one selected pixel with its rgba values.
// This selected pixel is in the exact middle of the canvas.
this.ctx.strokeRect(halfsize*ps+0.5, halfsize*ps+0.5, ps, ps);
// When the selected pixel is drawn, publish some info about it in an event.
var detail = {
x: this.x,
y: this.y,
r: colors[selectedPixelOffset+0],
g: colors[selectedPixelOffset+1],
b: colors[selectedPixelOffset+2],
a: colors[selectedPixelOffset+3],
rgb: selectedColorRGB,
hex: sk.colorHex(colors, selectedPixelOffset),
var evt = new CustomEvent('zoom-point', {
detail: detail,
bubbles: true