| <!-- The <zoom-sk> custom element declaration. |
| |
| The zoom-sk element presents a zoomed in view of a source img element. |
| |
| Attributes: |
| 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. |
| |
| Events: |
| 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 - || |
| |
| Methods: |
| |
| 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; |
| } |
| |
| </style> |
| <template> |
| <canvas id=zoom width=10 height=10></canvas> |
| <canvas id=copy width=10 height=10></canvas> |
| </template> |
| <script> |
| Polymer({ |
| is: 'zoom-sk', |
| |
| behaviors: [ |
| // Listen for resize events and change the size of the #zoom canvas accordingly. |
| Polymer.IronResizableBehavior |
| ], |
| |
| 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 |
| }); |
| this.dispatchEvent(evt); |
| }); |
| }, |
| |
| changeSource(element) { |
| this._source = element; |
| if (this._source.nodeName === 'IMG') { |
| this._source.addEventListener('load', function() { |
| this.allow_draw = true; |
| this._cloneImage(); |
| this.dispatchEvent(new CustomEvent('zoom-loaded', { bubbles: true })); |
| }.bind(this)); |
| this._cloneImage(); |
| } |
| }, |
| |
| // 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; |
| this.updateZoom(); |
| }, |
| |
| _cloneImage: function() { |
| if (!this._source) { return; } |
| this.$.copy.width = this._source.naturalWidth; |
| this.$.copy.height = this._source.naturalHeight; |
| this.$.copy.getContext('2d').drawImage( |
| this._source, 0, 0, this._source.naturalWidth, this._source.naturalHeight); |
| this.updateZoom(); |
| }, |
| |
| 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 |
| sourceCtx.readPixels( |
| 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 |
| }); |
| this.dispatchEvent(evt); |
| }, |
| |
| }); |
| </script> |
| </dom-module> |