mirror of
				https://github.com/flynx/ImageGrid.git
				synced 2025-10-30 10:50:08 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			473 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			473 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| //---------------------------------------------------------------------
 | |
| //
 | |
| //
 | |
| // XXX still thinking on how to package this correctly...
 | |
| //
 | |
| //---------------------------------------------------------------------
 | |
| // image manipulation basics...
 | |
| 
 | |
| var Filters = {
 | |
| 	makeCanvas: function(w, h){
 | |
| 		var c = document.createElement('canvas')
 | |
| 		c.width = w
 | |
| 		c.height = h
 | |
| 		return c
 | |
| 	},
 | |
| 
 | |
| 	// as input takes an HTML Image object...
 | |
| 	getPixels: function(img, w, h){
 | |
| 		var w = w || img.width
 | |
| 		var h = h || img.height
 | |
| 		var c = this.makeCanvas(w, h)
 | |
| 		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){
 | |
| 		c.width = data.width
 | |
| 		c.height = data.height
 | |
| 		var context = c.getContext('2d')
 | |
| 		context.putImageData(data, 0, 0)
 | |
| 	},
 | |
| 
 | |
| 	filterImage: function(filter, image, var_args){
 | |
| 		var args = [this.getPixels(image)]
 | |
| 		for(var i=2; i<arguments.length; i++){
 | |
| 			args.push(arguments[i])
 | |
| 		}
 | |
| 		return filter.apply(null, args)
 | |
| 	},
 | |
| 
 | |
| 	grayscale: function(pixels, args){
 | |
| 		var d = pixels.data
 | |
| 		for(var i=0; i<d.length; i+=4){
 | |
| 			var r = d[i]
 | |
| 			var g = d[i+1]
 | |
| 			var b = d[i+2]
 | |
| 			// CIE luminance for the RGB
 | |
| 			// The human eye is bad at seeing red and blue, so we de-emphasize them.
 | |
| 			var v = 0.2126*r + 0.7152*g + 0.0722*b
 | |
| 			d[i] = d[i+1] = d[i+2] = v
 | |
| 		}
 | |
| 		return pixels
 | |
| 	},
 | |
| 	// XXX need to resize this...
 | |
| 	histogram: function(pixels, mode, color){
 | |
| 		color = color || 'fill'
 | |
| 		mode = mode || 'luminance'
 | |
| 
 | |
| 		var w = 255 
 | |
| 		var h = 255 
 | |
| 
 | |
| 		// output buffer...
 | |
| 		var out = this.getPixels(null, w, h)
 | |
| 
 | |
| 		// pixel hit buffer...
 | |
| 		var count = []
 | |
| 
 | |
| 		var od = out.data
 | |
| 		var d = pixels.data
 | |
| 
 | |
| 		// get the stats...
 | |
| 		for(var i=0; i<d.length; i+=4){
 | |
| 			var r = d[i]
 | |
| 			var g = d[i+1]
 | |
| 			var b = d[i+2]
 | |
| 
 | |
| 			if(mode == 'luminance'){
 | |
| 				var v = Math.round(0.2126*r + 0.7152*g + 0.0722*b) * 4
 | |
| 				count[v] = count[v+1] = count[v+2] = (count[v] || 0) + 1
 | |
| 
 | |
| 			} else {
 | |
| 				if(mode == 'color' || mode == 'R'){
 | |
| 					count[r*4] = (count[r*4] || 0) + 1 }
 | |
| 				if(mode == 'color' || mode == 'G'){
 | |
| 					count[g*4+1] = (count[g*4+1] || 0) + 1 }
 | |
| 				if(mode == 'color' || mode == 'B'){
 | |
| 					count[b*4+2] = (count[b*4+2] || 0) + 1 } }
 | |
| 		}
 | |
| 
 | |
| 		var m = 255 / Math.max(...count.filter(function(){ return true }))
 | |
| 
 | |
| 		var pos = function(i, value){
 | |
| 			return (
 | |
| 				// horizontal position...
 | |
| 				i*4 
 | |
| 				// value vertical offset...
 | |
| 				+ (255-Math.round(value*m))*w*4) }
 | |
| 
 | |
| 		// XXX would be nice to have an option to draw full columns...
 | |
| 		count.forEach(function(v, i){
 | |
| 			var j = pos(i/4, v)
 | |
| 			while(j < od.length){
 | |
| 				j += w*4
 | |
| 				od[j] = 255
 | |
| 				if(color == 'point'){
 | |
| 					// correct for blue visibility...
 | |
| 					mode != 'luminance' 
 | |
| 						&& (i-2)%4 == 0
 | |
| 						&& (od[j-1] = od[j-2] = 180) 
 | |
| 					break } } })
 | |
| 
 | |
| 		return out
 | |
| 	},
 | |
| 	waveform: function(pixels, mode, color){
 | |
| 		mode = mode || 'luminance'
 | |
| 		color = color || 'normalized'
 | |
| 
 | |
| 		var w = pixels.width
 | |
| 
 | |
| 		// normalize pixel ratio...
 | |
| 		var m = (1/pixels.height)*255
 | |
| 
 | |
| 		var offsetTop = 0
 | |
| 		var offsetBottom = 0
 | |
| 
 | |
| 		// output buffer...
 | |
| 		var out = this.getPixels(null, 
 | |
| 			w, 
 | |
| 			offsetTop + 255 + offsetBottom)
 | |
| 
 | |
| 		// pixel hit buffer...
 | |
| 		var count = []
 | |
| 
 | |
| 		var od = out.data
 | |
| 		var d = pixels.data
 | |
| 
 | |
| 		var pos = function(i, value){
 | |
| 			return (
 | |
| 				// top margin...
 | |
| 				offsetTop*w*4 
 | |
| 				// horixontal position...
 | |
| 				+ i%(w*4)
 | |
| 				// value vertical offset...
 | |
| 				+ (255-Math.round(value))*w*4) }
 | |
| 
 | |
| 		var gain = 100
 | |
| 
 | |
| 		for(var i=0; i<d.length; i+=4){
 | |
| 
 | |
| 			var r = d[i]
 | |
| 			var g = d[i+1]
 | |
| 			var b = d[i+2]
 | |
| 			var c, j, f, x, y
 | |
| 
 | |
| 
 | |
| 			if(mode == 'luminance'){
 | |
| 				// CIE luminance for RGB
 | |
| 				var v = 0.2126*r + 0.7152*g + 0.0722*b
 | |
| 				c = count[j = pos(i, v)] = (count[j] || 0) + m
 | |
| 				od[j] = od[j+1] = od[j+2] = c * gain
 | |
| 
 | |
| 			} else {
 | |
| 
 | |
| 				if(mode == 'color' || mode == 'R'){
 | |
| 					f = 0.2126
 | |
| 					x = 1
 | |
| 					y = 2
 | |
| 					j = pos(i, r)
 | |
| 					c = count[j] = (count[j] || 0) + m
 | |
| 					od[j] = c * gain
 | |
| 				}
 | |
| 
 | |
| 				if(mode == 'color' || mode == 'G'){
 | |
| 					f = 0.7152
 | |
| 					x = -1
 | |
| 					y = 1
 | |
| 					j = pos(i, g) + 1
 | |
| 					c = count[j] = (count[j] || 0) + m
 | |
| 					od[j] = c * gain
 | |
| 				}
 | |
| 
 | |
| 				if(mode == 'color' || mode == 'B'){
 | |
| 					f = 0.0722
 | |
| 					x = -2
 | |
| 					y = -1
 | |
| 					j = pos(i, b) + 2
 | |
| 					c = count[j] = (count[j] || 0) + m
 | |
| 					od[j] = c * gain
 | |
| 				}
 | |
| 
 | |
| 				// normalize...
 | |
| 				mode != 'color'
 | |
| 					&& (color == 'white' ?
 | |
| 							(od[j+x] = od[j+y] = c * gain)
 | |
| 						: color == 'normalized' ?
 | |
| 							(od[j+x] = od[j+y] = c * gain/2 * (1-f))
 | |
| 						: null)
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		return out
 | |
| 	},
 | |
| }
 | |
| 
 | |
| 
 | |
| 
 | |
| //---------------------------------------------------------------------
 | |
| // helpers...
 | |
| 
 | |
| var WAVEFORM_SIZE = 1000
 | |
| var waveform = function(img, canvas, mode, color){
 | |
| 	var d = Filters.getPixels(img, WAVEFORM_SIZE)
 | |
| 	var w = Filters.waveform(d, mode, color)
 | |
| 	Filters.setPixels(canvas, w) }
 | |
| 
 | |
| var HISTOGRAM_SIZE = 1000
 | |
| var histogram = function(img, canvas, mode, color){
 | |
| 	var d = Filters.getPixels(img)
 | |
| 	var w = Filters.histogram(d, mode, color)
 | |
| 	Filters.setPixels(canvas, w) }
 | |
| 
 | |
| 
 | |
| 
 | |
| //---------------------------------------------------------------------
 | |
| // Custom element...
 | |
| 
 | |
| igImageGraph_template = `
 | |
| <style>
 | |
| 	:host {
 | |
| 		position: relative;
 | |
| 		display: inline-block;
 | |
| 
 | |
| 		background: black;
 | |
| 
 | |
| 		width: attr(image-width);
 | |
| 		height: attr(graph-height);
 | |
| 	}
 | |
| 	:host canvas {
 | |
| 		box-sizing: border-box;
 | |
| 		width: 100%;
 | |
| 		height: 100%;
 | |
| 
 | |
| 		border: 2px solid gray;
 | |
| 	}
 | |
| 	: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;
 | |
| 	}
 | |
| 	:host .controls button.current {
 | |
| 		text-decoration: underline;
 | |
| 		opacity: 0.9;
 | |
| 	}
 | |
| 	:host .controls button:hover {
 | |
| 		opacity: 1;
 | |
| 	}
 | |
| </style>
 | |
| <canvas class="graph"></canvas>
 | |
| <div class="controls"></div>
 | |
| `
 | |
| 
 | |
| class igImageGraph extends HTMLElement {
 | |
| 	template = 'ig-image-graph'
 | |
| 	graphs = {
 | |
| 		waveform,
 | |
| 		histogram,
 | |
| 	}
 | |
| 	modes = ['luminance', 'color', 'R', 'G', 'B']
 | |
| 	color_modes = ['normalized', 'white', 'point']
 | |
| 
 | |
| 	constructor(src){
 | |
| 		super()
 | |
| 		// 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(){
 | |
| 		this.update_controls()
 | |
| 		this.update() }
 | |
| 
 | |
| 	// attributes...
 | |
| 	get observedAttributes(){
 | |
| 		return [
 | |
| 			'src', 
 | |
| 			'mode', 
 | |
| 			'color',
 | |
| 			'nocontrols',
 | |
| 			'graph',
 | |
| 		]}
 | |
| 	attributeChangedCallback(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') }
 | |
| 	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'))
 | |
| 				: 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 nocontrols(){
 | |
| 		return this.getAttribute('nocontrols') != null }
 | |
| 	set nocontrols(value){
 | |
| 		value ?
 | |
| 			this.setAttribute('nocontrols', '')
 | |
|    			: this.removeAttribute('nocontrols') 
 | |
| 		this.update_controls()
 | |
| 		this.update() }
 | |
| 
 | |
| 	// API...
 | |
| 	update_controls(){
 | |
| 		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(){ 
 | |
| 						that.graph = that.graph == 'waveform' ?
 | |
| 							'histogram'
 | |
| 							: 'waveform'
 | |
| 						that.update() }
 | |
| 					return button }(),
 | |
| 				// modes...
 | |
| 				(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 }),
 | |
| 				/* 
 | |
| 				// color mode switch...
 | |
| 				function(){
 | |
| 					var button = document.createElement('button')
 | |
| 					button.innerText = '('+ that.color[0] +')'
 | |
| 					button.onclick = function(){ 
 | |
| 						that.color = that.color_modes[
 | |
| 							(that.color_modes.indexOf(that.color) + 1) 
 | |
| 								% that.color_modes.length]
 | |
| 						this.innerText = '('+ that.color[0] +')' }
 | |
| 					return button }(),
 | |
| 					//*/
 | |
| 				// reload...
 | |
| 				function(){
 | |
| 					var button = document.createElement('button')
 | |
| 					button.classList.add('update')
 | |
| 					button.innerHTML = '⟳'
 | |
| 					button.onclick = function(){ that.update() }
 | |
| 					return button }(),
 | |
| 			]
 | |
| 			.flat()
 | |
| 			.reverse()
 | |
| 			.forEach(function(button){
 | |
| 				controls.appendChild(button) })
 | |
| 		return this
 | |
| 	}
 | |
| 	update(){
 | |
| 		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')
 | |
| 		}
 | |
| 
 | |
| 		// XXX configurable...
 | |
| 		var type = this.graph
 | |
| 		var graph = this.graphs[type]
 | |
| 
 | |
| 		var canvas = this.__shadow.querySelector('canvas')
 | |
| 
 | |
| 		if(this.image){
 | |
| 			graph(this.image, canvas, this.mode, this.color)
 | |
| 
 | |
| 		} else if(this.src){
 | |
| 			this.src = this.src
 | |
| 		}
 | |
| 
 | |
| 		return this
 | |
| 	}
 | |
| }
 | |
| window.customElements.define('ig-image-graph', igImageGraph)
 | |
| 
 | |
| var makeImageGraph = function(img, options){
 | |
| 	var g = document.createElement('ig-image-graph')
 | |
| 	Object.assign(g, 
 | |
| 		options || {})
 | |
| 	g.src = img
 | |
| 	return g }
 | |
| 
 | |
| 
 | |
| 
 | |
| //---------------------------------------------------------------------
 | |
| //                                                 vim:set ts=4 sw=4 :
 |