UNPKG

@thi.ng/pixel

Version:

Typedarray integer & float pixel buffers w/ customizable formats, blitting, drawing, convolution

435 lines (434 loc) 11.6 kB
var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __decorateClass = (decorators, target, key, kind) => { var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target; for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result; if (kind && result) __defProp(target, key, result); return result; }; import { IGrid2DMixin } from "@thi.ng/api/mixins/igrid"; import { typedArray, uintTypeForBits } from "@thi.ng/api/typedarray"; import { isNumber } from "@thi.ng/checks/is-number"; import { isString } from "@thi.ng/checks/is-string"; import { assert } from "@thi.ng/errors/assert"; import { isPremultipliedInt, postmultiplyInt, premultiplyInt } from "@thi.ng/porter-duff/premultiply"; import { Lane, ROT_IDS } from "./api.js"; import { canvasPixels } from "./canvas.js"; import { ensureAlpha, ensureChannel, ensureImageData, ensureImageDataSize, ensureSize } from "./checks.js"; import { FloatBuffer } from "./float.js"; import { ABGR8888 } from "./format/abgr8888.js"; import { defIntFormat } from "./format/int-format.js"; import { imageCanvas } from "./image.js"; import { __compileGrayFromABGR, __compileGrayToABGR } from "./internal/codegen.js"; import { __blitCanvas, __clampRegion, __prepRegions, __setChannelConvert, __setChannelSame, __setChannelUni, __transformABGR } from "./internal/utils.js"; import { defSampler } from "./sample.js"; function intBuffer(...args) { return args[0] instanceof IntBuffer ? args[0].as(args[1]) : ( // @ts-ignore new IntBuffer(...args) ); } const intBufferFromImage = (img, fmt, width, height = width) => intBufferFromCanvas(imageCanvas(img, width, height).canvas, fmt); const intBufferFromCanvas = (canvas, fmt = ABGR8888) => { const { data } = canvasPixels(canvas); const w = canvas.width; const h = canvas.height; let dest; if (fmt === ABGR8888) { dest = data; } else { dest = typedArray(fmt.type, w * h); const src = data; const from = fmt.fromABGR; for (let i = dest.length; i-- > 0; ) { dest[i] = from(src[i]); } } return new IntBuffer(w, h, fmt, dest); }; let IntBuffer = class { size; stride; format; data; constructor(w, h, fmt = ABGR8888, data) { this.size = [w, h]; this.stride = [1, w]; this.format = fmt.__packed ? fmt : defIntFormat(fmt); this.data = data || typedArray(fmt.type, w * h); } /** @deprecated use `.data` instead */ get pixels() { return this.data; } get width() { return this.size[0]; } get height() { return this.size[1]; } // TODO support custom offsets (via ctor arg) get offset() { return 0; } get dim() { return 2; } *[Symbol.iterator]() { yield* this.data; } as(fmt) { if (!fmt.__float) return this.getRegion( 0, 0, this.width, this.height, fmt ); const dest = new FloatBuffer(this.width, this.height, fmt); const { data: dbuf, format: dfmt, stride: [stride] } = dest; const { data: sbuf, format: sfmt } = this; for (let i = sbuf.length; i-- > 0; ) { dbuf.set(dfmt.fromABGR(sfmt.toABGR(sbuf[i])), i * stride); } return dest; } copy() { const dest = this.empty(); dest.data.set(this.data); return dest; } empty() { return new IntBuffer(this.width, this.height, this.format); } // @ts-ignore mixin order() { } // @ts-ignore mixin includes(x, y) { } // @ts-ignore mixin indexAt(x, y) { } // @ts-ignore mixin indexAtUnsafe(x, y) { } // @ts-ignore mixin getAt(x, y) { } // @ts-ignore mixin getAtUnsafe(x, y) { } // @ts-ignore mixin setAt(x, y, col) { } // @ts-ignore mixin setAtUnsafe(x, y, col) { } getChannelAt(x, y, id, normalized = false) { const chan = ensureChannel(this.format, id); const col = this.getAt(x, y); return normalized ? chan.float(col) : chan.int(col); } setChannelAt(x, y, id, col, normalized = false) { const chan = ensureChannel(this.format, id); const src = this.getAt(x, y); normalized ? chan.setFloat(src, col) : chan.setInt(src, col); return this; } blend(op, dest, opts) { let sw = this.width; let dw = dest.width; const { sx, sy, dx, dy, rw, rh } = __prepRegions(this, dest, opts); if (rw < 1 || rh < 1) return dest; const sbuf = this.data; const dbuf = dest.data; const sf = this.format.toABGR; const df1 = dest.format.toABGR; const df2 = dest.format.fromABGR; for (let si = (sx | 0) + (sy | 0) * sw, di = (dx | 0) + (dy | 0) * dw, yy = 0; yy < rh; yy++, si += sw, di += dw) { for (let xx = 0; xx < rw; xx++) { dbuf[di + xx] = df2(op(sf(sbuf[si + xx]), df1(dbuf[di + xx]))); } } return dest; } blit(dest, opts) { let sw = this.width; let dw = dest.width; const { sx, sy, dx, dy, rw, rh } = __prepRegions(this, dest, opts); if (rw < 1 || rh < 1) return dest; const sbuf = this.data; const dbuf = dest.data; const sf = this.format.toABGR; const df = dest.format.fromABGR; const blitRow = this.format !== dest.format ? (si, di) => { for (let xx = 0; xx < rw; xx++) { dbuf[di + xx] = df(sf(sbuf[si + xx])); } } : (si, di) => dbuf.set(sbuf.subarray(si, si + rw), di); for (let si = (sx | 0) + (sy | 0) * sw, di = (dx | 0) + (dy | 0) * dw, yy = 0; yy < rh; yy++, si += sw, di += dw) { blitRow(si, di); } return dest; } blitCanvas(canvas, opts = {}) { __blitCanvas(this, canvas, opts); } setImageData(idata) { ensureImageDataSize(idata, this.width, this.height); const src = new Uint32Array(idata.data.buffer); const dest = this.data; const fmt = this.format.fromABGR; for (let i = src.length; i-- > 0; ) { dest[i] = fmt(src[i]); } return this; } toImageData(idata) { idata = ensureImageData(idata, this.width, this.height); const dest = new Uint32Array(idata.data.buffer); const src = this.data; const fmt = this.format.toABGR; for (let i = dest.length; i-- > 0; ) { dest[i] = fmt(src[i]); } return idata; } getRegion(x, y, width, height, fmt) { const [sx, sy, w, h] = __clampRegion( x, y, width, height, this.width, this.height ); if (w < 1 || h < 1) return; return this.blit(new IntBuffer(w, h, fmt || this.format), { sx, sy, w, h }); } getChannel(id) { const chan = ensureChannel(this.format, id); const buf = new IntBuffer(this.width, this.height, { type: uintTypeForBits(chan.size), size: chan.size, channels: [{ size: chan.size, lane: Lane.RED }], fromABGR: __compileGrayFromABGR(chan.size), toABGR: __compileGrayToABGR(chan.size) }); const src = this.data; const dest = buf.data; const get = chan.int; for (let i = src.length; i-- > 0; ) { dest[i] = get(src[i]); } return buf; } setChannel(id, src) { const chan = ensureChannel(this.format, id); const dbuf = this.data; const set = chan.setInt; if (isNumber(src)) { __setChannelUni(dbuf, src, set); } else { const sbuf = src.data; const schan = src.format.channels[0]; ensureSize(sbuf, this.width, this.height); if (chan.size === schan.size) { __setChannelSame(dbuf, sbuf, schan.int, set); } else { __setChannelConvert( dbuf, sbuf, this.format.fromABGR, src.format.toABGR, chan.maskA ); } } return this; } invert() { const { data, format } = this; const mask = Math.pow(2, format.size - format.alpha) - 1; for (let i = data.length; i-- > 0; ) { data[i] ^= mask; } return this; } premultiply() { ensureAlpha(this.format); __transformABGR(this.data, this.format, premultiplyInt); return this; } postmultiply() { __transformABGR(this.data, this.format, postmultiplyInt); return this; } isPremultiplied() { const pix = this.data; const to = this.format.toABGR; for (let i = pix.length; i-- > 0; ) { if (!isPremultipliedInt(to(pix[i]))) { return false; } } return true; } forEach(f) { const pix = this.data; for (let i = pix.length; i-- > 0; ) { pix[i] = f(pix[i], i); } return this; } fill(x) { this.data.fill(x); } /** * Flips image horizontally. */ flipX() { const { data, width } = this; for (let i = 0; i < data.length; i += width) { data.subarray(i, i + width).reverse(); } return this; } /** * Flips image vertically. */ flipY() { const { data, width } = this; const tmp = typedArray(this.format.type, width); for (let i = 0, j = data.length - width; i < j; i += width, j -= width) { tmp.set(data.subarray(i, i + width)); data.copyWithin(i, j, j + width); data.set(tmp, j); } return this; } rotateByID(id) { return id > 0 ? this[ROT_IDS[id - 1]]() : this; } rotateCW() { const { width, height } = this; const h1 = height - 1; this._rotate((x, y) => x * height + h1 - y); this.size[0] = height; this.size[1] = width; return this; } rotateCCW() { const { width, height } = this; const w1 = width - 1; this._rotate((x, y) => (w1 - x) * height + y); this.size[0] = height; this.size[1] = width; return this; } rotate180() { const { width, height } = this; const w1 = width - 1; const h1 = height - 1; this._rotate((x, y) => (h1 - y) * width + w1 - x); return this; } /** * Returns scaled version of this buffer using given sampler or filter * (default: `"linear"`) for interpolation. Syntax sugar for * {@link IntBuffer.resize}. * * @param scale - */ scale(scale, sampler = "linear") { assert(scale > 0, `scale must be > 0`); return this.resize(this.width * scale, this.height * scale, sampler); } resize(w, h, sampler = "linear") { w |= 0; h |= 0; assert(w > 0 && h > 0, `target width & height must be > 0`); const dest = intBuffer(w, h, this.format); const dpix = dest.data; const scaleX = w > 0 ? this.width / w : 0; const scaleY = h > 0 ? this.height / h : 0; sampler = isString(sampler) ? defSampler(this, sampler, "repeat") : sampler; for (let y = 0, i = 0; y < h; y++) { const yy = y * scaleY; for (let x = 0; x < w; x++, i++) { dpix[i] = sampler(x * scaleX, yy); } } return dest; } upsize() { const { width, height, data } = this; const dest = new IntBuffer(width * 2, height * 2, this.format); const dpix = dest.data; for (let y = 0, si = 0; y < height; y++) { for (let x = 0, di = y * width * 4; x < width; x++, si++, di += 2) { dpix[di] = data[si]; } } return dest; } _rotate(idxFn) { const { data, format, width, height } = this; const tmp = typedArray(format.type, width * height); for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { tmp[idxFn(x, y)] = data[y * width + x]; } } this.data = tmp; } }; IntBuffer = __decorateClass([ IGrid2DMixin ], IntBuffer); export { IntBuffer, intBuffer, intBufferFromCanvas, intBufferFromImage };