UNPKG

@thi.ng/pixel

Version:

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

539 lines (538 loc) 14.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 { nomixin } from "@thi.ng/api/decorators/nomixin"; import { IGrid2DMixin } from "@thi.ng/api/mixins/igrid"; import { isNumber } from "@thi.ng/checks/is-number"; import { isString } from "@thi.ng/checks/is-string"; import { assert } from "@thi.ng/errors/assert"; import { clamp } from "@thi.ng/math/interval"; import { isPremultiplied, postmultiply, premultiply } from "@thi.ng/porter-duff/premultiply"; import { ensureChannel, ensureImageData, ensureImageDataSize, ensureSize } from "./checks.js"; import { defFloatFormat } from "./format/float-format.js"; import { FLOAT_GRAY } from "./format/float-gray.js"; import { FLOAT_RGBA, ROT_IDS } from "./index.js"; import { IntBuffer, intBufferFromCanvas, intBufferFromImage } from "./int.js"; import { __blitCanvas, __clampRegion, __prepRegions } from "./internal/utils.js"; import { defSampler } from "./sample.js"; function floatBuffer(...args) { return args[0] instanceof IntBuffer ? ( // @ts-ignore floatBufferFromInt(...args) ) : ( // @ts-ignore new FloatBuffer(...args) ); } const floatBufferFromInt = (src, fmt) => { const dest = new FloatBuffer(src.width, src.height, fmt); const { data: dbuf, format: dfmt, stride: [stride] } = dest; const { data: sbuf, format: sfmt } = src; for (let i = sbuf.length; i-- > 0; ) { dbuf.set(dfmt.fromABGR(sfmt.toABGR(sbuf[i])), i * stride); } return dest; }; const floatBufferFromImage = (img, fmt = FLOAT_RGBA, width, height = width) => intBufferFromImage(img, void 0, width, height).as(fmt); const floatBufferFromCanvas = (canvas, fmt = FLOAT_RGBA) => intBufferFromCanvas(canvas).as(fmt); let FloatBuffer = class { size; stride; format; data; __empty; constructor(w, h, fmt = FLOAT_RGBA, data) { this.size = [w, h]; this.format = fmt.__float ? fmt : defFloatFormat(fmt); const stride = this.format.channels.length; this.stride = [stride, w * stride]; this.data = data || new Float32Array(w * h * stride); this.__empty = Object.freeze(new Array(stride).fill(0)); } /** @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]() { const { data, stride: [stride] } = this; for (let i = 0, n = data.length; i < n; i += stride) { yield data.subarray(i, i + stride); } } as(fmt) { const { width, height, stride: [stride], data, format: { size, normalized: getNormalized, toABGR } } = this; let i = 0, j = 0, n = data.length; if (fmt.__float) { const $fmt = fmt; const dest = new FloatBuffer(width, height, $fmt); const { data: dpixels, stride: [dstride] } = dest; if (size === 1 && $fmt.channels.length === 1) { const setNormalized = $fmt.fromNormalized; for (; i < n; i += stride, j += dstride) { dpixels[j] = setNormalized(getNormalized(data[i])); } } else { for (; i < n; i += stride, j += dstride) { dpixels.set( $fmt.fromABGR(toABGR(data.subarray(i, i + stride))), j ); } } return dest; } else { const $fmt = fmt; const dest = new IntBuffer(width, height, $fmt); const dpixels = dest.data; if (size === 1 && $fmt.channels.length === 1) { const setFloat = $fmt.channels[0].setFloat; for (; i < n; i += stride, j++) { dpixels[j] = setFloat(0, getNormalized(data[i])); } } else { for (; i < n; i += stride, j++) { dpixels[j] = $fmt.fromABGR( toABGR(data.subarray(i, i + stride)) ); } } return dest; } } copy() { const dest = this.empty(); dest.data.set(this.data); return dest; } empty() { return new FloatBuffer(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) { } getAt(x, y) { return this.includes(x, y) ? this.getAtUnsafe(x, y) : this.__empty; } getAtUnsafe(x, y) { const idx = this.indexAtUnsafe(x, y); return this.data.subarray(idx, idx + this.stride[0]); } setAt(x, y, col) { return this.includes(x, y) ? (this.data.set(col, this.indexAtUnsafe(x, y)), true) : false; } setAtUnsafe(x, y, col) { this.data.set(col, this.indexAtUnsafe(x, y)); return true; } getChannelAt(x, y, id) { ensureChannel(this.format, id); return this.includes(x, y) ? this.data[this.indexAtUnsafe(x, y) + id] : void 0; } setChannelAt(x, y, id, col) { ensureChannel(this.format, id); this.includes(x, y) && (this.data[this.indexAtUnsafe(x, y) + id] = col); return this; } getChannel(id) { ensureChannel(this.format, id); const { data, stride: [stride] } = this; const [min, max] = this.format.range; const dest = new Float32Array(this.width * this.height); for (let i = id, j = 0, n = data.length; i < n; i += stride, j++) { dest[j] = clamp(data[i], min, max); } return new FloatBuffer(this.width, this.height, FLOAT_GRAY, dest); } setChannel(id, src) { ensureChannel(this.format, id); const { data: dest, stride: [stride] } = this; if (isNumber(src)) { for (let i = id, n = dest.length; i < n; i += stride) { dest[i] = src; } } else { const { data: sbuf, stride: [sstride] } = src; ensureSize(sbuf, this.width, this.height, sstride); for (let i = id, j = 0, n = dest.length; i < n; i += stride, j += sstride) { dest[i] = sbuf[j]; } } return this; } blend(op, dest, opts) { this.ensureFormat(dest); 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 [stride, sw] = this.stride; const dw = dest.stride[1]; for (let si = (sx | 0) * stride + (sy | 0) * sw, di = (dx | 0) * stride + (dy | 0) * dw, yy = 0; yy < rh; yy++, si += sw, di += dw) { for (let xx = rw, sii = si, dii = di; xx-- > 0; sii += stride, dii += stride) { const out = dbuf.subarray(dii, dii + stride); op(out, sbuf.subarray(sii, sii + stride), out); } } return dest; } blit(dest, opts) { this.ensureFormat(dest); 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 [stride, sw] = this.stride; const dw = dest.stride[1]; const rww = rw * stride; for (let si = (sx | 0) * stride + (sy | 0) * sw, di = (dx | 0) * stride + (dy | 0) * dw, yy = 0; yy < rh; yy++, si += sw, di += dw) { dbuf.set(sbuf.subarray(si, si + rww), 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 { data: dest, format: { fromABGR }, stride: [stride] } = this; const tmp = []; for (let i = src.length; i-- > 0; ) { dest.set(fromABGR(src[i], tmp), i * stride); } return this; } toImageData(idata) { idata = ensureImageData(idata, this.width, this.height); const dest = new Uint32Array(idata.data.buffer); const { stride: [stride], data, format } = this; for (let i = 0, j = 0, n = data.length; i < n; i += stride, j++) { dest[j] = format.toABGR(data.subarray(i, i + stride)); } return idata; } getRegion(x, y, width, height) { const [sx, sy, w, h] = __clampRegion( x, y, width, height, this.width, this.height ); if (w < 1 || h < 1) return; return this.blit(new FloatBuffer(w, h, this.format), { sx, sy, w, h }); } forEach(f) { const { data, stride: [stride] } = this; for (let i = 0, j = 0, n = data.length; i < n; i += stride, j++) { data.set(f(data.subarray(i, i + stride), j), i); } return this; } fill(x) { assert( x.length <= this.format.channels.length, `fill value has too many channels` ); const { data, stride: [stride] } = this; for (let i = 0, n = data.length; i < n; i += stride) { data.set(x, i); } } premultiply() { this.ensureRGBA(); const { data, stride: [stride] } = this; for (let i = 0, n = data.length; i < n; i += stride) { premultiply(null, data.subarray(i, i + stride)); } return this; } postmultiply() { this.ensureRGBA(); const { data, stride: [stride] } = this; for (let i = 0, n = data.length; i < n; i += stride) { postmultiply(null, data.subarray(i, i + stride)); } return this; } isPremultiplied() { this.ensureRGBA(); const { data, stride: [stride] } = this; for (let i = 0, n = data.length; i < n; i += stride) { if (!isPremultiplied(data.subarray(i, i + stride))) { return false; } } return true; } clamp() { const data = this.data; const [min, max] = this.format.range; for (let i = data.length; i-- > 0; ) { data[i] = clamp(data[i], min, max); } return this; } clampChannel(id) { ensureChannel(this.format, id); const { data, stride: [stride] } = this; const [min, max] = this.format.range; for (let i = id, n = data.length; i < n; i += stride) { data[i] = clamp(data[i], min, max); } } flipX() { const { data, width, height, stride: [sx, sy] } = this; const tmp = new Float32Array(sx); const w1 = width - 1; const w2 = width >>> 1; for (let y = 0; y < height; y++) { for (let x = 0, i = y * sy, j = i + w1 * sx; x < w2; x++, i += sx, j -= sx) { tmp.set(data.subarray(i, i + sx)); data.copyWithin(i, j, j + sx); data.set(tmp, j); } } return this; } /** * Flips image vertically. */ flipY() { const data = this.data; const rowStride = this.stride[1]; const tmp = new Float32Array(rowStride); for (let i = 0, j = data.length - rowStride; i < j; i += rowStride, j -= rowStride) { tmp.set(data.subarray(i, i + rowStride)); data.copyWithin(i, j, j + rowStride); 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; } invert() { const { data, format, stride: [stride] } = this; for (let i = 0, n = data.length, m = format.alpha ? stride - 1 : stride; i < n; i += stride) { for (let j = 0; j < m; j++) data[i + j] = 1 - data[i + j]; } return this; } scale(scale, sampler) { 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 = floatBuffer(w, h, this.format); const dpix = dest.data; const scaleX = w > 0 ? this.width / w : 0; const scaleY = h > 0 ? this.height / h : 0; const stride = this.stride[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 += stride) { dpix.set(sampler(x * scaleX, yy), i); } } return dest; } upsize() { const { width, height, data, stride: [stride, rowStride] } = this; const stride2x = stride * 2; const dest = floatBuffer(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 * rowStride * 4; x < width; x++, si += stride, di += stride2x) { dpix.set(data.subarray(si, si + stride), di); } } return dest; } _rotate(idxFn) { const { data, width, height, stride: [stride] } = this; const tmp = new Float32Array(width * height * stride); for (let y = 0, i = 0; y < height; y++) { for (let x = 0; x < width; x++, i += stride) { tmp.set(data.subarray(i, i + stride), idxFn(x, y) * stride); } } this.data = tmp; } ensureFormat(dest) { assert( dest.format === this.format, `dest buffer format must be same as src` ); } ensureRGBA() { assert(this.format === FLOAT_RGBA, "require FLOAT_RGBA format"); } }; __decorateClass([ nomixin ], FloatBuffer.prototype, "getAt", 1); __decorateClass([ nomixin ], FloatBuffer.prototype, "getAtUnsafe", 1); __decorateClass([ nomixin ], FloatBuffer.prototype, "setAt", 1); __decorateClass([ nomixin ], FloatBuffer.prototype, "setAtUnsafe", 1); FloatBuffer = __decorateClass([ IGrid2DMixin ], FloatBuffer); export { FloatBuffer, floatBuffer, floatBufferFromCanvas, floatBufferFromImage, floatBufferFromInt };