thi-ng/umbrella

View on GitHub
packages/pixel/src/float.ts

Summary

Maintainability
A
1 hr
Test Coverage
File `float.ts` has 567 lines of code (exceeds 250 allowed). Consider refactoring.
import type {
Fn2,
FnN2,
ICopy,
IEmpty,
NumericArray,
Range0_3,
} from "@thi.ng/api";
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 type {
BlendFnFloat,
BlitCanvasOpts,
BlitOpts,
Filter,
FloatFormat,
FloatFormatSpec,
FloatSampler,
IBlend,
IBlit,
IInvert,
IPixelBuffer,
IResizable,
IRotate,
IToImageData,
IntFormat,
} from "./api.js";
import { ensureChannel, ensureImageData, 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";
 
/**
* Syntax sugar for {@link FloatBuffer} ctor.
*
* @param w -
* @param h -
* @param fmt -
* @param data -
*/
export function floatBuffer(
w: number,
h: number,
fmt?: FloatFormat | FloatFormatSpec,
data?: Float32Array
): FloatBuffer;
export function floatBuffer(
src: IntBuffer,
fmt?: FloatFormat | FloatFormatSpec
): FloatBuffer;
export function floatBuffer(...args: any[]) {
return args[0] instanceof IntBuffer
? // @ts-ignore
floatBufferFromInt(...args)
: // @ts-ignore
new FloatBuffer(...args);
}
 
/**
* Creates a new `FloatBuffer` from given {@link IntBuffer} and using
* provided {@link FloatFormat}.
*
* @remarks
* See {@link FloatBuffer.as} for reverse operation.
*
* @param src -
* @param fmt -
*/
export const floatBufferFromInt = (
src: IntBuffer,
fmt?: FloatFormat | FloatFormatSpec
) => {
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;
};
 
export const floatBufferFromImage = (
img: HTMLImageElement,
fmt?: FloatFormat | FloatFormatSpec,
width?: number,
height = width
) => floatBufferFromInt(intBufferFromImage(img, undefined, width, height), fmt);
 
export const floatBufferFromCanvas = (
canvas: HTMLCanvasElement | OffscreenCanvas,
fmt?: FloatFormat
) => floatBufferFromInt(intBufferFromCanvas(canvas), fmt);
 
`FloatBuffer` has 47 functions (exceeds 20 allowed). Consider refactoring.
@IGrid2DMixin
export class FloatBuffer
implements
IPixelBuffer<Float32Array, NumericArray>,
IBlend<FloatBuffer, BlendFnFloat>,
IBlit<FloatBuffer>,
ICopy<FloatBuffer>,
IEmpty<FloatBuffer>,
IInvert<FloatBuffer>,
IResizable<FloatBuffer, FloatSampler>,
IRotate<FloatBuffer>,
IToImageData
{
readonly size: [number, number];
readonly stride: [number, number];
readonly format: FloatFormat;
data: Float32Array;
protected __empty: NumericArray;
 
constructor(
w: number,
h: number,
fmt: FloatFormat | FloatFormatSpec = FLOAT_RGBA,
data?: Float32Array
) {
this.size = [w, h];
this.format = (<any>fmt).__float
? <FloatFormat>fmt
: defFloatFormat(fmt);
// TODO support custom strides (via ctor arg)
const stride = this.format.channels.length;
this.stride = [stride, w * stride];
this.data = data || new Float32Array(w * h * stride);
this.__empty = <NumericArray>(
Object.freeze(new Array<number>(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(): 2 {
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);
}
}
 
Function `as` has a Cognitive Complexity of 7 (exceeds 5 allowed). Consider refactoring.
as(fmt: IntFormat) {
const {
width,
height,
stride: [stride],
data,
format: sfmt,
} = this;
const dest = new IntBuffer(width, height, fmt);
const dpixels = dest.data;
if (sfmt.size === 1 && fmt.channels.length === 1) {
const setFloat = fmt.channels[0].setFloat;
for (let i = 0, j = 0, n = data.length; i < n; i += stride, j++) {
dpixels[j] = setFloat(0, sfmt.getNormalized(data[i]));
}
} else {
for (let i = 0, j = 0, n = data.length; i < n; i += stride, j++) {
dpixels[j] = fmt.fromABGR(
sfmt.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(): number[] {}
 
// @ts-ignore mixin
includes(x: number, y: number): boolean {}
 
// @ts-ignore mixin
indexAt(x: number, y: number): number {}
 
// @ts-ignore mixin
indexAtUnsafe(x: number, y: number): number {}
 
@nomixin
getAt(x: number, y: number) {
return this.includes(x, y) ? this.getAtUnsafe(x, y) : this.__empty;
}
 
@nomixin
getAtUnsafe(x: number, y: number) {
const idx = this.indexAtUnsafe(x, y);
return this.data.subarray(idx, idx + this.stride[0]);
}
 
@nomixin
setAt(x: number, y: number, col: NumericArray) {
return this.includes(x, y)
? (this.data.set(col, this.indexAtUnsafe(x, y)), true)
: false;
}
 
@nomixin
setAtUnsafe(x: number, y: number, col: NumericArray) {
this.data.set(col, this.indexAtUnsafe(x, y));
return true;
}
 
getChannelAt(x: number, y: number, id: number) {
ensureChannel(this.format, id);
return this.includes(x, y)
? this.data[this.indexAtUnsafe(x, y) + id]
: undefined;
}
 
setChannelAt(x: number, y: number, id: number, col: number) {
ensureChannel(this.format, id);
this.includes(x, y) && (this.data[this.indexAtUnsafe(x, y) + id] = col);
return this;
}
 
getChannel(id: number) {
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);
}
 
Function `setChannel` has a Cognitive Complexity of 6 (exceeds 5 allowed). Consider refactoring.
setChannel(id: number, src: FloatBuffer | number) {
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: BlendFnFloat, dest: FloatBuffer, opts?: Partial<BlitOpts>) {
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];
Similar blocks of code found in 2 locations. Consider refactoring.
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: FloatBuffer, opts?: Partial<BlitOpts>) {
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;
Similar blocks of code found in 2 locations. Consider refactoring.
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:
| HTMLCanvasElement
| CanvasRenderingContext2D
| OffscreenCanvas
| OffscreenCanvasRenderingContext2D,
opts: Partial<BlitCanvasOpts> = {}
) {
__blitCanvas(this, canvas, opts);
}
 
toImageData(idata?: ImageData) {
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: number, y: number, width: number, height: number) {
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: Fn2<NumericArray, number, NumericArray>) {
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: NumericArray) {
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);
}
}
 
Similar blocks of code found in 2 locations. Consider refactoring.
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;
}
 
Similar blocks of code found in 2 locations. Consider refactoring.
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: number) {
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++) {
Similar blocks of code found in 2 locations. Consider refactoring.
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);
Similar blocks of code found in 2 locations. Consider refactoring.
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: Range0_3): this {
return id > 0 ? this[ROT_IDS[id - 1]]() : this;
}
 
Similar blocks of code found in 2 locations. Consider refactoring.
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;
}
 
Similar blocks of code found in 2 locations. Consider refactoring.
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: number, sampler?: FloatSampler | Filter) {
assert(scale > 0, `scale must be > 0`);
return this.resize(this.width * scale, this.height * scale, sampler);
}
 
Function `resize` has a Cognitive Complexity of 7 (exceeds 5 allowed). Consider refactoring.
resize(w: number, h: number, sampler: FloatSampler | Filter = "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;
}
 
protected _rotate(idxFn: FnN2) {
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;
}
 
protected ensureFormat(dest: FloatBuffer) {
assert(
dest.format === this.format,
`dest buffer format must be same as src`
);
}
 
protected ensureRGBA() {
assert(this.format === FLOAT_RGBA, "require FLOAT_RGBA format");
}
}