/**********************************************************************
*
* Web Component defining an image waveform/histogram view widget.
*
*
* Example:
*
* ...
*
*
*
* XXX add docs and examples -- canvas-waveform.html is outdated...
* XXX might be a good idea to add interactive feedback -- show on image
* the denseties under the ursor for histogram or the slice/density
* for the waveform...
* XXX add worker support...
*
**********************************************************************/
((typeof define)[0]=='u'?function(f){module.exports=f(require)}:define)
(function(require){ var module={} // make module AMD/node compatible...
/*********************************************************************/
var object = require('lib/object')
//---------------------------------------------------------------------
// image manipulation basics...
var Filters =
module.Filters = {
makeCanvas: function(w, h, canvas){
var c = canvas || document.createElement('canvas')
c.width = w
c.height = h
return c },
// as input takes an HTML Image object...
getPixels: function(img, tmp_canvas, w, h){
var w = w || img.naturalWidth
var h = h || img.naturalHeight
var c = this.makeCanvas(w, h, tmp_canvas)
var context = c.getContext('2d')
if(img == null){
context.rect(0, 0, w, h)
context.fillStyle = "black"
context.fill()
} else {
context.drawImage(img, 0, 0, w, h) }
return context.getImageData(0, 0, c.width, c.height) },
setPixels: function(c, data, w, h){
w = c.width = w || data.width
h = c.height = h || data.height
var context = c.getContext('2d')
context.putImageData(data, 0, 0) },
// get image pixels normalized to a square of size s, rotated and flipped...
//
// NOTE: flip is applied to the image before it is rotated... (XXX ???)
// XXX BUG: this is still wrong for images with exif orientation...
// to reproduce:
// loadImages: "L:/tmp/test/export-test/index-with-exif-rotation"
// focusImage: 2
// showMetadata
getNormalizedPixels: function(img, tmp_canvas, s, rotate, flip){
s = s || Math.max(img.naturalWidth, img.naturalHeight)
rotate = rotate || 0
;(rotate == 90 || rotate == 270)
&& (flip = flip == 'horizontal' ?
'vertical'
: flip == 'vertical' ?
'horizontal'
: flip)
var [h, v] = flip == 'both' ?
[-1, -1]
: flip == 'horizontal' ?
[-1, 1]
: flip == 'vertical' ?
[1, -1]
: [1, 1]
var c = this.makeCanvas(s, s, tmp_canvas)
var context = c.getContext('2d')
context.rect(0, 0, s, s)
context.fillStyle = 'black'
context.fill()
if(img){
context.setTransform(h*1, 0, 0, v*1, s/2, s/2)
context.rotate(rotate * Math.PI/180)
context.drawImage(img, -s/2, -s/2, s, s) }
return context.getImageData(0, 0, s, s) },
filterImage: function(filter, image, var_args){
var args = [this.getPixels(image)]
for(var i=2; i
:host {
position: relative;
display: inline-block;
background: black;
width: attr(image-width);
height: attr(graph-height);
padding-top: 16px;
padding-bottom: 10px;
}
:host canvas {
box-sizing: border-box;
width: 100%;
height: 100%;
border-top: 1px dashed rgba(255, 255, 255, 0.2);
border-bottom: 1px dashed rgba(255, 255, 255, 0.2);
}
:host .controls {
display: inline-block;
position: absolute;
top: 2px;
right: 2px;
left: 2px;
}
:host .controls button {
background: transparent;
border: none;
color: white;
opacity: 0.7;
float: right;
font-size: 12px;
}
:host .controls button[disabled] {
opacity: 0.3;
user-select: none;
}
:host .controls button.current {
text-decoration: underline;
opacity: 0.9;
}
:host .controls button:hover:not([disabled]) {
opacity: 1;
}
:host .hidden {
position: absolute;
width: 0;
height: 0;
opacity: 0;
image-orientation: none;
}
`
var igImageGraph =
module.igImageGraph =
object.Constructor('igImageGraph', HTMLElement, {
template: 'ig-image-graph',
graphs: {
waveform,
histogram,
},
modes: ['luminance', 'color', 'R', 'G', 'B'],
color_modes: ['normalized', 'white', 'point'],
__init__: function(src){
// shadow DOM
var shadow = this.__shadow =
this.attachShadow({mode: 'open'})
// get/create template...
var tpl = document.getElementById(this.template)
if(!tpl){
var tpl = document.createElement('template')
tpl.setAttribute('id', this.template)
tpl.innerHTML = igImageGraph_template
document.head.appendChild(tpl) }
shadow.appendChild(tpl.content.cloneNode(true)) },
connectedCallback: function(){
this.update_controls()
this.image
|| this.update() },
// attributes...
get observedAttributes(){
return [
'src',
'mode',
'color',
'graph',
'orientation',
'flipped',
'nocontrols',
]},
attributeChangedCallback: function(name, from, to){
name == 'nocontrols'
&& this.update_controls()
this.update() },
get graph(){
return this.getAttribute('graph') || 'waveform' },
set graph(value){
value in this.graphs
&& this.setAttribute('graph', value)
value == ''
&& this.removeAttribute('graph')
this.update() },
get src(){
return this.getAttribute('src') },
// XXX make this async...
set src(value){
var that = this
this.__update_handler = this.__update_handler
|| this.update.bind(this)
var url = typeof(value) == typeof('str')
// get/create image...
var img = this.image =
url ?
//(this.image || document.createElement('img'))
// XXX HACK: image-orientation only works if element is attached to DOM...
(this.image || this.__shadow.querySelector('img'))
: value
img.removeEventListener('load', this.__update_handler)
img.addEventListener('load', this.__update_handler)
// set .src and img.src...
this.setAttribute('src',
url ?
(img.src = value)
: img.src) },
get mode(){
return this.getAttribute('mode') || 'color' },
set mode(value){
this.modes.includes(value)
&& this.setAttribute('mode', value)
value === undefined
&& this.removeAttribute('color')
this.update_controls()
this.update() },
get color(){
return this.getAttribute('color') || 'normalized' },
set color(value){
this.color_modes.includes(value)
&& this.setAttribute('color', value)
value === undefined
&& this.removeAttribute('color')
this.update() },
get orientation(){
return this.getAttribute('orientation') || 0 },
set orientation(value){
;(['top', 'left', 'bottom', 'right'].includes(value)
|| typeof(value) == typeof(123))
&& this.setAttribute('orientation', value)
value == null
&& this.removeAttribute('orientation')
this.update() },
get flipped(){
return this.getAttribute('flipped') },
set flipped(value){
;(['vertical', 'horizontal', 'both'].includes(value)
|| typeof(value) == typeof(123))
&& this.setAttribute('flipped', value)
value == null
&& this.removeAttribute('flipped')
this.update() },
get nocontrols(){
return this.getAttribute('nocontrols') != null },
set nocontrols(value){
value ?
this.setAttribute('nocontrols', '')
: this.removeAttribute('nocontrols')
this.update_controls()
this.update() },
// API...
update_controls: function(){
var that = this
var mode = this.mode
var controls = this.__shadow.querySelector('.controls')
controls.innerHTML = ''
// modes...
var buttons = [
// graph...
function(){
var button = document.createElement('button')
button.classList.add('update')
//button.innerHTML = '◑'
button.innerHTML = '◪'
button.onclick = function(){
var g = that.graph = that.graph == 'waveform' ?
'histogram'
: 'waveform'
var b = button.parentElement.querySelector('#orientation-button') || {}
b.disabled = that.graph != 'waveform' }
return button }(),
// orientation...
//
// switch from vertical to horizontal and back, keeping
// only two orientations with top-to-top (default) and
// top-to-right (alternative) modes.
//
// the button arrow:
// - indicates orientation
// - points to top of image relative to waveform
//
function(){
var button = document.createElement('button')
button.setAttribute('id', 'orientation-button')
button.classList.add('update')
button.innerHTML = '🡑'
// load button state...
var _update = function(){
Object.assign(button.style,
that.__rotated == null ?
// top...
{
transform: '',
marginTop: '-2px',
}
// right...
: {
transform: 'rotate(90deg)',
marginTop: '-1px',
}) }
_update()
button.disabled = that.graph != 'waveform'
// click -> do the rotation...
button.onclick = function(){
var o = that.__rotated
var c = that.orientation*1
that.orientation = o == null ?
// rotate cw...
(c + 90) % 360
// restore...
: o
that.__rotated = o == null ?
c
: null
_update() }
return button }(),
// modes...
// ...generate mode toggles...
...(this.nocontrols ?
[]
: this.modes)
// mode buttons...
.map(function(m){
var button = document.createElement('button')
button.innerText = m
button.classList.add(m, ...(m == mode ? ['current'] : []))
button.onclick = function(){
that.mode = m }
return button }),
// reload...
/*/ XXX do we actually need this???
function(){
var button = document.createElement('button')
button.classList.add('update')
button.innerHTML = '⟳'
button.onclick = function(){ that.update() }
return button }(),
//*/
]
.reverse()
.forEach(function(button){
controls.appendChild(button) })
return this },
// XXX add option to update graph in a worker...
// XXX show a spinner while updating...
update: function(){
var that = this
var mode = this.mode
// controls...
// remove...
if(!this.nocontrols){
var controls = this.__shadow.querySelector('.controls')
// current button state...
var button = controls.querySelector('button.'+this.mode)
button
&& button.classList.add('current') }
if(this.image){
var orientation = this.orientation
orientation = parseFloat(
{top: 180, left: 90, bottom: 0, right: 270}[orientation]
|| orientation)
var canvas = this.__shadow.querySelector('canvas.graph')
var tmp_canvas = this.__shadow.querySelector('canvas.hidden')
// XXX configurable...
this.graphs[this.graph](this.image,
canvas, tmp_canvas,
this.mode,
this.color,
Math.round(orientation),
this.flipped)
} else if(this.src){
this.src = this.src }
return this },
})
window.customElements.define('ig-image-graph', igImageGraph)
/**********************************************************************
* vim:set ts=4 sw=4 : */ return module })