blob: fa96d8aa095ceb1ccd5e92da97ba04f30c10a5b2 [file] [log] [blame]
<!-- 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">&nbsp;</div> </td><td><div style="background :#c6dbef">&nbsp;</div></td></tr>
<tr><td>2-5 </td><td><div style="background :#fdae6b">&nbsp;</div> </td><td><div style="background :#9ecae1">&nbsp;</div></td></tr>
<tr><td>6-15 </td><td><div style="background :#fd8d3c">&nbsp;</div> </td><td><div style="background :#6baed6">&nbsp;</div></td></tr>
<tr><td>16-46 </td><td><div style="background :#f16913">&nbsp;</div> </td><td><div style="background :#4292c6">&nbsp;</div></td></tr>
<tr><td>47-140 </td><td><div style="background :#d94801">&nbsp;</div> </td><td><div style="background :#2171b5">&nbsp;</div></td></tr>
<tr><td>141-420 </td><td><div style="background :#a63603">&nbsp;</div> </td><td><div style="background :#08519c">&nbsp;</div></td></tr>
<tr><td>421-1024 </td><td><div style="background :#7f2704">&nbsp;</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>