| <!-- The <multi-zoom-sk> custom element declaration. |
| |
| This element is a view that allows zooming into images. |
| It displays the three images, left, top and diff, and then allows zooming and |
| panning over various combinations of the images. There is a single pixel in |
| the center of the zoomed view which is highlighted, and its information is |
| displayed on the zoom dialog. |
| |
| There are three images that it deals with, called left, top, and diff. |
| |
| Attributes: |
| None |
| |
| Events: |
| None |
| |
| Methods: |
| setDetails(details) - Sets the information the zoom dialog needs, which |
| is an object of the following form, where each member is a |
| URL of the full size image. |
| |
| { |
| leftImgUrl: "...", |
| middleImgUrl: "...", |
| rightImgUrl: "...", |
| llabel: "Left Label", |
| rlabel: "Right Label" |
| } |
| |
| Mailboxes: |
| None |
| |
| --> |
| <link rel="import" href="bower_components/iron-flex-layout/iron-flex-layout-classes.html"> |
| <link rel="import" href="bower_components/polymer/polymer.html"> |
| <link rel="import" href="bower_components/paper-dialog/paper-dialog.html"> |
| <link rel="import" href="bower_components/paper-button/paper-button.html"> |
| <link rel="import" href="bower_components/paper-dialog-scrollable/paper-dialog-scrollable.html"> |
| <link rel="import" href="bower_components/paper-toggle-button/paper-toggle-button.html"> |
| |
| <link rel=import href="../common/imp/canvas-layers.html"> |
| <link rel=import href="../common/imp/crosshair.html"> |
| <link rel=import href="../common/imp/zoom.html"> |
| |
| <link rel="import" href="shared-styles.html"> |
| |
| <dom-module id="multi-zoom-sk"> |
| <style include="iron-flex iron-flex-alignment shared-styles"> |
| img, .imgContainer { |
| width: 128px; |
| } |
| |
| .imgContainer, .zoomContainer { |
| padding: 4px; |
| border: solid lightgray 1px; |
| margin: 4px; |
| } |
| |
| .zoomContainer { |
| padding: 0; |
| margin: 0; |
| } |
| |
| .imgColumn { |
| width: 12em; |
| } |
| |
| .selectorWrapper { |
| font-size: 16px; |
| } |
| |
| .imagesWrap { |
| margin-bottom: 2em; |
| } |
| |
| .showBold { |
| font-weight: bold; |
| } |
| |
| .multiZoomWrapper { |
| min-width: 20em; |
| min-height: 20em; |
| padding: 2em; |
| } |
| |
| .legendContainer { |
| margin: 2em; |
| font-size: 90%; |
| } |
| |
| </style> |
| |
| <template> |
| <div class="multiZoomWrapper"> |
| <div class="horizontal layout"> |
| <div class="vertical layout" hidden="{{_noImage(details)}}"> |
| <div class="horizontal layout wrap imagesWrap"> |
| <div class="vertical layout imgColumn"> |
| <div class="imgContainer"> |
| <canvas-layers-sk layers='["crosshairLeft"]' id="layersLeft"> |
| <img id="left_img" src$="{{details.leftImgUrl}}"/> |
| </canvas-layers-sk> |
| <crosshair-sk x="{{_x}}" |
| y="{{_y}}" |
| target="layersLeft" |
| name="crosshairLeft" |
| update_on="click"> |
| </crosshair-sk> |
| </div> |
| <div class="selectorWrapper"> |
| <paper-toggle-button checked={{_leftActive}}> |
| <span class$="[[_boldClass(_currSeq, 'left')]]">[[details.llabel]]</span> |
| </paper-toggle-button> |
| </div> |
| </div> |
| |
| <div class="layout vertical imgColumn" hidden="{{_hideImg(details.middleImgUrl)}}"> |
| <div class="imgContainer"> |
| <canvas-layers-sk layers='["crosshairMiddle"]' id="layersMiddle"> |
| <img id="middle_img" src$="[[details.middleImgUrl]]"/> |
| </canvas-layers-sk> |
| <crosshair-sk x="{{_x}}" |
| y="{{_y}}" |
| target="layersMiddle" |
| name="crosshairMiddle" |
| update_on="click"> |
| </crosshair-sk> |
| </div> |
| <div class="selectorWrapper"> |
| <paper-toggle-button checked={{_middleActive}}> |
| <span class$="{{_boldClass(_currSeq, 'middle')}}">Diff</span> |
| </paper-toggle-button> |
| </div> |
| |
| </div> |
| |
| <div class="layout vertical imgColumn" hidden="{{_hideImg(details.rightImgUrl)}}"> |
| <div class="imgContainer"> |
| <canvas-layers-sk layers='["crosshairRight"]' id="layersRight"> |
| <img id="right_img" src$="{{details.rightImgUrl}}"/> |
| </canvas-layers-sk> |
| <crosshair-sk x="{{_x}}" |
| y="{{_y}}" |
| target="layersRight" |
| name="crosshairRight" |
| update_on="click"> |
| </crosshair-sk> |
| </div> |
| |
| <div class="selectorWrapper"> |
| <paper-toggle-button checked={{_rightActive}}> |
| <span class$="{{_boldClass(_currSeq, 'right')}}">[[details.rlabel]]</span> |
| </paper-toggle-button> |
| </div> |
| </div> |
| </div> |
| |
| <div class="layout horizontal wrap"> |
| <div class="layout vertical zoomContainer"> |
| <zoom-sk hidden$="[[_hideMe(_currSeq, 'left')]]" |
| source="left_img" |
| pixels="{{_pixels}}" |
| pixel_size="{{_pixel_size}}" |
| hide_grid="[[_hide_grid]]" |
| id="zoomLeft" |
| x="{{_x}}" |
| y="{{_y}}"> |
| </zoom-sk> |
| |
| <zoom-sk hidden$="[[_hideMe(_currSeq, 'middle')]]" |
| source="middle_img" |
| pixels="{{_pixels}}" |
| pixel_size="{{_pixel_size}}" |
| id="zoomMiddle" |
| hide_grid="[[_hide_grid]]" |
| x="{{_x}}" |
| y="{{_y}}"> |
| </zoom-sk> |
| |
| <zoom-sk hidden$="[[_hideMe(_currSeq, 'right')]]" |
| source="right_img" |
| pixels="{{_pixels}}" |
| pixel_size="{{_pixel_size}}" |
| id="zoomRight" |
| hide_grid="[[_hide_grid]]" |
| x="{{_x}}" |
| y="{{_y}}"> |
| </zoom-sk> |
| </div> |
| </div> |
| </div> |
| <div class="vertical layout"> |
| <div class="legendContainer"> |
| <table border="0" cellspacing="5" cellpadding="5"> |
| <tr><th>Coord</th><td><pre>([[_x]], [[_y]])</pre><td></tr> |
| <tr><th>[[details.llabel]]</th><td><pre>[[_centerPixel.zoomLeft]]</pre><td></tr> |
| <tr hidden="[[_hideImg(details.middleImgUrl)]]"><th>Diff</th><td><pre>[[_centerPixel.zoomMiddle]]</pre><td></tr> |
| <tr hidden="[[_hideImg(details.rightImgUrl)]]"><th>[[details.rlabel]]</th><td><pre>[[_centerPixel.zoomRight]]</pre><td></tr> |
| <tr><td colspan=2>[[_nthPixelDiff(_x, _y)]]</td></tr> |
| </table> |
| <div class="horizontal layout"> |
| <table border="0" cellspacing="5" cellpadding="5"> |
| <tr><th>Color Distance</th><th>RGBA</th><th>Alpha</th></tr> |
| <tr><td>1-1 </td><td><div style="background :#fdd0a2"> </div> </td><td><div style="background :#c6dbef"> </div></td></tr> |
| <tr><td>2-5 </td><td><div style="background :#fdae6b"> </div> </td><td><div style="background :#9ecae1"> </div></td></tr> |
| <tr><td>6-15 </td><td><div style="background :#fd8d3c"> </div> </td><td><div style="background :#6baed6"> </div></td></tr> |
| <tr><td>16-46 </td><td><div style="background :#f16913"> </div> </td><td><div style="background :#4292c6"> </div></td></tr> |
| <tr><td>47-140 </td><td><div style="background :#d94801"> </div> </td><td><div style="background :#2171b5"> </div></td></tr> |
| <tr><td>141-420 </td><td><div style="background :#a63603"> </div> </td><td><div style="background :#08519c"> </div></td></tr> |
| <tr><td>421-1024 </td><td><div style="background :#7f2704"> </div> </td><td></td></tr> |
| </table> |
| <table border="0" cellspacing="5" cellpadding="5" id=nav> |
| <tr><th colspan=2>Naviation</th></tr> |
| <tr><th>H</th><td>Left</td></tr> |
| <tr><th>J</th><td>Down</td></tr> |
| <tr><th>K</th><td>Up</td></tr> |
| <tr><th>L</th><td>Right</td></tr> |
| <tr><th>A</th><td>Zoom Out</td></tr> |
| <tr><th>Z</th><td>Zoom In</td></tr> |
| <tr><th>U</th><td>Jump To Next Largest Diff</td></tr> |
| <tr><th>Y</th><td>Jump To Prev. Largest Diff</td></tr> |
| <tr><th>M</th><td>Manual Toggle</td></tr> |
| <tr><th>G</th><td>Hide/Show Grid</td></tr> |
| </table> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
| </template> |
| |
| <script> |
| Polymer({ |
| is: "multi-zoom-sk", |
| |
| properties:{ |
| details: { |
| type: Object, |
| value: function() { return {}; } |
| }, |
| |
| _currSeq: '', |
| |
| _leftActive: { |
| type: Boolean, |
| value: true, |
| observer: "_updateTiming" |
| }, |
| |
| _middleActive: { |
| type: Boolean, |
| value: false, |
| observer: "_updateTiming" |
| }, |
| |
| _rightActive: { |
| type: Boolean, |
| value: true, |
| observer: "_updateTiming" |
| }, |
| |
| _x : { |
| type: Number, |
| value: 0, |
| notify: true |
| }, |
| |
| _y : { |
| type: Number, |
| value: 0, |
| notify: true |
| }, |
| |
| _pixels : { |
| type: Number, |
| value: 128, |
| notify: true |
| }, |
| |
| _pixel_size : { |
| type: Number, |
| value: 4, |
| notify: true |
| }, |
| |
| _centerPixel: { |
| type: Object, |
| value: function() { return {}; } |
| }, |
| |
| _hide_grid: { |
| type: Boolean, |
| value: false, |
| } |
| }, |
| |
| ready: function(){ |
| this._seq = []; |
| this.listen(this.$.zoomLeft, 'zoom-point', '_handleZoomPixels'); |
| this.listen(this.$.zoomMiddle, 'zoom-point', '_handleZoomPixels'); |
| this.listen(this.$.zoomRight, 'zoom-point', '_handleZoomPixels'); |
| |
| this.addEventListener('crosshair', () => { |
| this.$.zoomLeft.updateZoom(); |
| this.$.zoomMiddle.updateZoom(); |
| this.$.zoomRight.updateZoom(); |
| }); |
| |
| this.listen(document, 'keydown', '_handleKeyDown'); |
| }, |
| |
| setDetails: function(details) { |
| details = sk.object.shallowCopy(details); |
| details.llabel = details.llabel || "Left"; |
| details.rlabel = details.rlabel || "Right"; |
| |
| this.set('_leftActive', true); |
| this.set('_middleActive', false); |
| this.set('_rightActive', true); |
| this.set('_x', 0); |
| this.set('_y', 0); |
| this.set('details', details); |
| this._cachedDiffs = null; |
| this._cachedDiffIdx = -1; |
| this._updateTiming(); |
| this._showNext(true); |
| this._active = true; |
| this.$.zoomLeft.notifyResize(); |
| this.$.zoomMiddle.notifyResize(); |
| this.$.zoomRight.notifyResize(); |
| }, |
| |
| clear: function(details) { |
| this.set('details', {}); |
| this._cancelAsync(); |
| this._cachedDiffs = null; |
| this._cachedDiffIdx = -1; |
| this._active = false; |
| }, |
| |
| _cancelAsync: function() { |
| if (this._asyncHandle) { |
| this.cancelAsync(this._asyncHandle); |
| this._asyncHandle = null; |
| } |
| }, |
| |
| _showNext: function(triggerAsync) { |
| var currSeq = ''; |
| if (this._seq.length > 0) { |
| var idx = this._seq.indexOf(this._currSeq); |
| if (idx < 0) { |
| idx = 0; |
| } else { |
| idx = (idx+1) % this._seq.length; |
| } |
| currSeq = this._seq[idx]; |
| } |
| this.set('_currSeq', currSeq); |
| if (triggerAsync) { |
| this._asyncHandle = this.async(function() { |
| this._showNext(true); |
| }.bind(this), 500); |
| } |
| }, |
| |
| _updateTiming: function() { |
| var seq = []; |
| if (this._leftActive) { |
| seq.push("left"); |
| } |
| |
| if (this._middleActive && this.details.middleImgUrl && (this.details.middleImgUrl != '')) { |
| seq.push('middle'); |
| } |
| |
| if (this._rightActive && this.details.rightImgUrl && (this.details.rigthImgUrl != '')) { |
| seq.push('right'); |
| } |
| |
| this._seq = seq; |
| }, |
| |
| _handleZoomPixels: function(ev) { |
| ev.stopPropagation(); |
| this.set('_centerPixel.' + ev.target.id, ev.detail.rgb + ' ' + ev.detail.hex); |
| |
| // Always calculate the middle value. |
| var lc = this.$.zoomLeft.getPixelColor(ev.detail.x, ev.detail.y); |
| var rc = this.$.zoomRight.getPixelColor(ev.detail.x, ev.detail.y); |
| var color = [ |
| Math.abs(lc[0]-rc[0]), |
| Math.abs(lc[1]-rc[1]), |
| Math.abs(lc[2]-rc[2]), |
| Math.abs(lc[3]-rc[3]) |
| ]; |
| var diffStr = sk.colorRGB(color, 0, true) + ' ' + sk.colorHex(color, 0); |
| this.set("_centerPixel.zoomMiddle", diffStr); |
| }, |
| |
| _handleKeyDown: function(e) { |
| if (!this._active) { |
| return; |
| } |
| |
| var c = String.fromCharCode(e.keyCode); |
| switch (c) { |
| case "G": |
| this.set('_hide_grid', !this._hide_grid); |
| break; |
| case "J": |
| this.set('_y', this._y+1); |
| break; |
| case "K": |
| this.set('_y', this._y-1); |
| break; |
| case "H": |
| this.set('_x', this._x-1); |
| break; |
| case "L": |
| this.set('_x', this._x+1); |
| break; |
| case "A": |
| this.set('_pixels', Math.min(this._pixels*2, 256)); |
| this.set('_pixel_size', Math.max(this._pixel_size/2, 2)); |
| break; |
| case "Z": |
| this.set('_pixels', Math.max(this._pixels/2, 2)); |
| this.set('_pixel_size', Math.min(this._pixel_size*2, 256)); |
| break; |
| case "U": |
| this._moveToNextLargestDiff(false); |
| break; |
| case "Y": |
| this._moveToNextLargestDiff(true); |
| break; |
| case "M": |
| this._manualToggle(); |
| break; |
| } |
| |
| if ("JKHLAZUM".indexOf(c) != -1 ) { |
| e.stopPropagation(); |
| } |
| this.$.zoomLeft.updateZoom(); |
| this.$.zoomMiddle.updateZoom(); |
| this.$.zoomRight.updateZoom(); |
| }, |
| |
| _moveToNextLargestDiff: function(backwards) { |
| if (!this.details || !this.details.middleImgUrl) { |
| return; |
| } |
| if (!this._cachedDiffs) { |
| // find all the diffs and sort them biggest diff to |
| // smallest diff. |
| var leftColors = this.$.zoomLeft.getImageData(); |
| var rightColors = this.$.zoomRight.getImageData(); |
| var width = leftColors.width; |
| var height = leftColors.height; |
| var lc = leftColors.data; |
| var rc = rightColors.data; |
| |
| if (!lc || !rc) { |
| // not loaded yet |
| return; |
| } |
| |
| this._cachedDiffs = []; |
| for (var x=0; x<width; x++) { |
| for (var y=0; y<height; y++) { |
| var offset = (y*width+x)*4; // Offset into the colors array. |
| var dist = this._colorDist( |
| rc[offset+0]-lc[offset+0], |
| rc[offset+1]-lc[offset+1], |
| rc[offset+2]-lc[offset+2], |
| rc[offset+3]-lc[offset+3] |
| ); |
| if (!dist) { |
| // No difference in pixels - no need to add |
| // it to our list of "different pixels" |
| continue; |
| } |
| this._cachedDiffs.push({ |
| x: x, |
| y: y, |
| diff: dist, |
| }); |
| } |
| } |
| this._cachedDiffs.sort(function(a, b) { |
| // First sort diffs high to low, so biggest ones are first |
| var d = b.diff - a.diff; |
| if (d) { |
| return d; |
| } |
| // prioritize up and to the left for tie breaks. |
| if (b.x !== a.x) { |
| return a.x - b.x; |
| } |
| return a.y - b.y; |
| }); |
| } |
| |
| if (backwards && this._cachedDiffIdx > 0) { |
| this._cachedDiffIdx--; |
| } else if (!backwards && this._cachedDiffIdx < this._cachedDiffs.length-1) { |
| this._cachedDiffIdx++; |
| } |
| |
| var diff = this._cachedDiffs[this._cachedDiffIdx]; |
| this._x = diff.x; |
| this._y = diff.y; |
| }, |
| |
| // colorDist returns the distance of a color from (0, 0, 0, 0) using a |
| // crude square distance per channel. |
| _colorDist: function(r, g, b, a) { |
| return r*r + g*g + b*b + a*a; |
| }, |
| |
| _manualToggle: function() { |
| if (!this.details || !this.details.middleImgUrl) { |
| return; |
| } |
| |
| this._cancelAsync(); |
| this._showNext(false); |
| }, |
| |
| _hideMe: function(currSeq, pos) { |
| return currSeq != pos; |
| }, |
| |
| _boldClass: function(currSeq, pos) { |
| return (currSeq==pos) ? 'showBold' : ''; |
| }, |
| |
| _hideImg: function(url) { |
| return !url; |
| }, |
| |
| _noImage: function(details) { |
| return !(details && details.leftImgUrl); |
| }, |
| |
| _nthPixelDiff: function(x, y) { |
| if (!this._cachedDiffs) { |
| return ''; |
| } |
| var endings = ['st', 'nd', 'rd']; // for 1st, 2nd, 3rd |
| var total = this._cachedDiffs.length; |
| for (var i = 0; i < total; i++) { |
| var d = this._cachedDiffs[i] |
| if (d.x === x && d.y === y) { |
| // Update our current diff index so that if the user navigates (using |
| // J/H/K/L) from the 3rd to the 12th biggest pixel and hits U, they |
| // go to the 13th biggest diff, not the 4th. |
| this._cachedDiffIdx = i; |
| var e = endings[i] || 'th'; |
| return `${i+1}${e} biggest pixel diff (out of ${total})`; |
| } |
| } |
| return 'No difference on this pixel'; |
| }, |
| |
| |
| }); |
| </script> |
| </dom-module> |