@thi.ng/pixel
Version:
Typedarray integer & float pixel buffers w/ customizable formats, blitting, drawing, convolution
539 lines (538 loc) • 14.6 kB
JavaScript
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
};