/********************************************************************** * * 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 })