Created
August 19, 2019 19:55
-
-
Save rsms/0c2a71ebfc5edcc1d8c475bb820d7ffa to your computer and use it in GitHub Desktop.
imutil
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Vec, RGBA } from './util' | |
interface OffscreenCanvas extends EventTarget { | |
readonly width: number | |
readonly height: number | |
getContext( | |
contextId: "2d", | |
contextAttributes?: CanvasRenderingContext2DSettings | |
) :CanvasRenderingContext2D | null | |
getContext( | |
contextId: "webgl" | "experimental-webgl", | |
contextAttributes?: WebGLContextAttributes | |
) :WebGLRenderingContext | null | |
getContext( | |
contextId: string, | |
contextAttributes?: {} | |
): CanvasRenderingContext2D | WebGLRenderingContext | null | |
transferToImageBitmap(): ImageBitmap | |
} | |
declare var OffscreenCanvas: { | |
prototype: OffscreenCanvas | |
new(width :number, height :number): OffscreenCanvas | |
} | |
// ImUtil provides 2D imaging functions, backed by HTML canvas. | |
// | |
export class ImUtil { | |
private _canvas :OffscreenCanvas|HTMLCanvasElement|null = null | |
private _context2D :CanvasRenderingContext2D|null = null | |
// set to true before calling any functions on this object to cause | |
// the underlying canvas to be displayed in the current HTML document. | |
debugShowCanvas :bool = false | |
// #B54ABC43C23D | |
context2D() :CanvasRenderingContext2D|null { | |
if (!this._canvas) { | |
let c | |
if (!this.debugShowCanvas && typeof OffscreenCanvas != 'undefined') { | |
c = new OffscreenCanvas(1, 1) | |
} else { | |
c = document.createElement('canvas') | |
if (this.debugShowCanvas) { | |
c.style.position = 'fixed' | |
if (window.devicePixelRatio != 0) { | |
c.style.zoom = String(1 / window.devicePixelRatio) | |
} | |
document.body.appendChild(c) // XXX | |
} | |
} | |
this._canvas = c | |
this._context2D = c.getContext && c.getContext('2d') | |
} | |
return this._context2D | |
} | |
async loadBitmapData(imageData :ArrayBuffer) :Promise<ImageBitmap> { | |
return await createImageBitmap(new Blob([imageData])) | |
} | |
// drawImageData draws image encoded by imageData to backing canvas | |
async drawImageData( | |
imageData :ArrayBuffer, | |
origin :Vec=Vec.ZERO, | |
) :Promise<ImageBitmap|null> { | |
let g = this.context2D() | |
if (!g) { return null } // no canvas2d | |
let bm = await this.loadBitmapData(imageData) | |
// resize canvas to match bitmap size | |
g.canvas.width = bm.width | |
g.canvas.height = bm.height | |
// draw bitmap | |
g.drawImage(bm, origin.x, origin.y) | |
return bm | |
} | |
// pixels retrieves pixel values as a Uint8ClampedArray, usually in RGBA | |
// format (4 bytes per pixel) | |
pixels() :Uint8ClampedArray { | |
let g = this.context2D() | |
return ( | |
g ? g.getImageData(0, 0, g.canvas.width, g.canvas.height).data : | |
new Uint8ClampedArray() | |
) | |
} | |
// dominantColor returns a best-guess dominant color of the canvas. | |
// scale optionally lowers precision and speeds up computation. | |
dominantColor(scale :number = 1) :RGBA { | |
let g = this.context2D() | |
if (!g) { return RGBA.White } | |
// retrieve pixel values as a Uint8ClampedArray | |
let pixels = g.getImageData(0, 0, g.canvas.width, g.canvas.height).data | |
// calculate bytes per pixel and number of pixels | |
let pxcount = g.canvas.width * g.canvas.height | |
let stride = (pixels.length / pxcount) >>> 0 // bytes per pixel | |
if (stride != 4) { | |
print('[imutil] unsupported pixel format stride=' + stride) | |
return RGBA.White | |
} | |
stride *= Math.max(1, scale) | |
return this.dominantColorRGBA(pixels, pxcount, stride) | |
} | |
// averageColor returns the average color of the canvas. | |
// scale optionally lowers precision and speeds up computation. | |
averageColor(scale :number = 1) :RGBA { | |
let g = this.context2D() | |
if (!g) { return RGBA.White } | |
let pixels = g.getImageData(0, 0, g.canvas.width, g.canvas.height).data | |
let pxcount = g.canvas.width * g.canvas.height | |
let stride = (pixels.length / pxcount) >>> 0 // bytes per pixel | |
if (stride != 4) { | |
print('[imutil] unsupported pixel format stride=' + stride) | |
return RGBA.White | |
} | |
stride *= Math.max(1, scale) | |
return this.averageColorRGBA(pixels, pxcount, stride) | |
} | |
averageColorRGBA(pxv :ArrayLike<int>, pxcount :int, stride :int) { | |
let r = 0, g = 0, b = 0, a = 0 | |
for (let i = 0; i < pxcount; i += stride) { | |
let a1 = pxv[i + 3] | |
r += pxv[i] * a1; | |
g += pxv[i + 1] * a1; | |
b += pxv[i + 2] * a1; | |
a += a1; | |
} | |
return ( a == 0 ? | |
new RGBA(0, 0, 0, 0) : | |
new RGBA( | |
(r / a) / 255, | |
(g / a) / 255, | |
(b / a) / 255, | |
(a / pxcount) / 255, | |
) | |
) | |
} | |
dominantColorRGBA(pxv :ArrayLike<int>, pxcount :int, stride :int) :RGBA { | |
let cm = new Map<int,int[]>() | |
// average and count RGB values | |
for (let i = 0; i < pxcount; i += stride) { | |
let r = pxv[i], g = pxv[i + 1], b = pxv[i + 2], a = pxv[i + 3] | |
// key = (a << 24) + (r << 16) + (g << 8) + b | |
let key = (r << 16) + (g << 8) + b | |
// update or store entry in temporary map. | |
// note that we premultiply alpha. | |
let e = cm.get(key) | |
if (e) { | |
// update existing entry | |
e[0] += r * a | |
e[1] += g * a | |
e[2] += b * a | |
e[3] += a | |
e[4]++ // increment count | |
} else { | |
// new RGB value | |
cm.set(key, [r * a, g * a, b * a, a, 1]); | |
} | |
} | |
// pick the RGB value with highest frequency | |
let [r, g, b, a, count] = Array.from(cm.values()).sort((a, b) => { | |
let acount = a[4], bcount = b[4] | |
return ( acount < bcount ? 1 : | |
bcount < acount ? -1 : | |
0 ) | |
})[0] | |
return ( a == 0 ? | |
new RGBA(0, 0, 0, 0) : | |
new RGBA( | |
(r / a) / 255, | |
(g / a) / 255, | |
(b / a) / 255, | |
(a / count) / 255 | |
) | |
) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
type VecOperand = {x:number,y:number} | number | |
function vop(v :VecOperand) : {x:number,y:number} { | |
return typeof v == 'object' ? v : { x: v, y: v } | |
} | |
// DOM helpers | |
export function $(s :string, e? :HTMLElement) { | |
return (e || document).querySelector(s) | |
} | |
export function $$(s :string, e? :HTMLElement) { | |
return Array.prototype.slice.call((e || document).querySelectorAll(s)) | |
} | |
// RGBA represents a RGB color with Alpha | |
export class RGBA { | |
static readonly White = Object.freeze(new RGBA(1, 1, 1, 1)) | |
static readonly Black = Object.freeze(new RGBA(0, 0, 0, 1)) | |
r :float // [0-1] | |
g :float // [0-1] | |
b :float // [0-1] | |
a :float // [0-1] | |
constructor(r :float, g :float, b :float, a :float) { | |
this.r = r | |
this.g = g | |
this.b = b | |
this.a = a | |
} | |
toString() { | |
if (this.a < 1.0) { | |
let a = parseFloat(this.a.toFixed(3)) | |
return `rgba(${this.r*255}, ${this.g*255}, ${this.b*255}, ${a})` | |
} | |
return `rgb(${this.r*255}, ${this.g*255}, ${this.b*255})` | |
} | |
} | |
// 2D vector | |
export class Vec { | |
static readonly ZERO = new Vec(0, 0) | |
x :number | |
y :number | |
constructor(x :number, y :number) { | |
this.x = x | |
this.y = y | |
} | |
toUInt() { return new Vec(this.x >>> 0, this.y >>> 0) } | |
toInt() { return new Vec(this.x | 0, this.y | 0) } | |
round() { return new Vec(Math.round(this.x), Math.round(this.y)) } | |
add(v :VecOperand) { v = vop(v); return new Vec(this.x + v.x, this.y + v.y) } | |
sub(v :VecOperand) { v = vop(v); return new Vec(this.x - v.x, this.y - v.y) } | |
mul(v :VecOperand) { v = vop(v); return new Vec(this.x * v.x, this.y * v.y) } | |
div(v :VecOperand) { v = vop(v); return new Vec(this.x / v.x, this.y / v.y) } | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment