Skip to content

Instantly share code, notes, and snippets.

@rsms
Created August 19, 2019 19:55
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rsms/0c2a71ebfc5edcc1d8c475bb820d7ffa to your computer and use it in GitHub Desktop.
Save rsms/0c2a71ebfc5edcc1d8c475bb820d7ffa to your computer and use it in GitHub Desktop.
imutil
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
)
)
}
}
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