thi-ng/umbrella

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

Summary

Maintainability
A
0 mins
Test Coverage
import type { Fn2, FnN2, ICopy, IEmpty, Maybe, Range0_3 } from "@thi.ng/api";
import { IGrid2DMixin } from "@thi.ng/api/mixins/igrid";
import {
    typedArray,
    uintTypeForBits,
    type UIntArray,
} 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,
    type BlendFnInt,
    type BlitCanvasOpts,
    type BlitOpts,
    type Filter,
    type IBlend,
    type IBlit,
    type IInvert,
    type IPixelBuffer,
    type IResizable,
    type IRotate,
    type IToImageData,
    type IntChannel,
    type IntFormat,
    type IntFormatSpec,
    type IntSampler,
} from "./api.js";
import { canvasPixels } from "./canvas.js";
import {
    ensureAlpha,
    ensureChannel,
    ensureImageData,
    ensureSize,
} from "./checks.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 {
    __clampRegion,
    __prepRegions,
    __setChannelConvert,
    __setChannelSame,
    __setChannelUni,
    __transformABGR,
} from "./internal/utils.js";
import { defSampler } from "./sample.js";

/**
 * Syntax sugar for {@link IntBuffer} ctor.
 *
 * @param w -
 * @param h -
 * @param fmt -
 * @param data -
 */
export function intBuffer(
    w: number,
    h: number,
    fmt?: IntFormat | IntFormatSpec,
    data?: UIntArray
): IntBuffer;
export function intBuffer(
    src: IntBuffer,
    fmt?: IntFormat | IntFormatSpec
): IntBuffer;
export function intBuffer(...args: any[]) {
    return args[0] instanceof IntBuffer
        ? args[0].as(args[1])
        : // @ts-ignore
          new IntBuffer(...args);
}

/**
 * Creates a new pixel buffer from given HTML image element with optional
 * support for format conversion (default: {@link ABGR8888} & resizing.
 *
 * @param img -
 * @param fmt -
 * @param width -
 * @param height -
 */
export const intBufferFromImage = (
    img: HTMLImageElement,
    fmt?: IntFormat,
    width?: number,
    height = width
) => intBufferFromCanvas(imageCanvas(img, width, height).canvas, fmt);

/**
 * Creates a new pixel buffer from given HTML canvas element with optional
 * support for format conversion (default: {@link ABGR8888}.
 *
 * @param canvas -
 * @param fmt -
 */
export const intBufferFromCanvas = (
    canvas: HTMLCanvasElement,
    fmt: IntFormat = ABGR8888
) => {
    const ctx = canvasPixels(canvas);
    const w = canvas.width;
    const h = canvas.height;
    let dest: Maybe<UIntArray>;
    if (fmt === ABGR8888) {
        dest = ctx.data;
    } else {
        dest = typedArray(fmt.type, w * h);
        const src = ctx.data;
        const from = fmt.fromABGR;
        for (let i = dest.length; i-- > 0; ) {
            dest[i] = from(src[i]);
        }
    }
    return new IntBuffer(w, h, fmt, dest);
};

@IGrid2DMixin
export class IntBuffer
    implements
        IPixelBuffer<UIntArray, number>,
        IBlend<IntBuffer, BlendFnInt>,
        IBlit<IntBuffer>,
        ICopy<IntBuffer>,
        IEmpty<IntBuffer>,
        IInvert<IntBuffer>,
        IResizable<IntBuffer, IntSampler>,
        IRotate<IntBuffer>,
        IToImageData
{
    readonly size: [number, number];
    readonly stride: [number, number];
    readonly format: IntFormat;
    data: UIntArray;

    constructor(
        w: number,
        h: number,
        fmt: IntFormat | IntFormatSpec = ABGR8888,
        data?: UIntArray
    ) {
        this.size = [w, h];
        // TODO support custom strides (via ctor arg)
        this.stride = [1, w];
        this.format = (<any>fmt).__packed ? <IntFormat>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(): 2 {
        return 2;
    }

    as(fmt: IntFormat): IntBuffer {
        return this.getRegion(0, 0, this.width, this.height, fmt)!;
    }

    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(): 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 {}

    // @ts-ignore mixin
    getAt(x: number, y: number): number {}

    // @ts-ignore mixin
    getAtUnsafe(x: number, y: number): number {}

    // @ts-ignore mixin
    setAt(x: number, y: number, col: number): boolean {}

    // @ts-ignore mixin
    setAtUnsafe(x: number, y: number, col: number): boolean {}

    getChannelAt(x: number, y: number, id: number, normalized = false) {
        const chan = <IntChannel>ensureChannel(this.format, id);
        const col = this.getAt(x, y);
        return normalized ? chan.float(col) : chan.int(col);
    }

    setChannelAt(
        x: number,
        y: number,
        id: number,
        col: number,
        normalized = false
    ) {
        const chan = <IntChannel>ensureChannel(this.format, id);
        const src = this.getAt(x, y);
        normalized ? chan.setFloat(src, col) : chan.setInt(src, col);
        return this;
    }

    blend(op: BlendFnInt, dest: IntBuffer, opts?: Partial<BlitOpts>) {
        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: IntBuffer, opts?: Partial<BlitOpts>) {
        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: number, di: number) => {
                        for (let xx = 0; xx < rw; xx++) {
                            dbuf[di + xx] = df(sf(sbuf[si + xx]));
                        }
                  }
                : (si: number, di: number) =>
                        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: HTMLCanvasElement | CanvasRenderingContext2D,
        opts: Partial<BlitCanvasOpts> = {}
    ) {
        const ctx =
            canvas instanceof HTMLCanvasElement
                ? canvas.getContext("2d")!
                : canvas;
        ctx.putImageData(this.toImageData(opts.data), opts.x || 0, opts.y || 0);
    }

    toImageData(idata?: ImageData) {
        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: number,
        y: number,
        width: number,
        height: number,
        fmt?: IntFormat
    ) {
        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: number) {
        const chan = <IntChannel>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: number, src: IntBuffer | number) {
        const chan = <IntChannel>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: Fn2<number, number, number>) {
        const pix = this.data;
        for (let i = pix.length; i-- > 0; ) {
            pix[i] = f(pix[i], i);
        }
        return this;
    }

    fill(x: number) {
        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: Range0_3): this {
        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: number, sampler: IntSampler | Filter = "linear") {
        assert(scale > 0, `scale must be > 0`);
        return this.resize(this.width * scale, this.height * scale, sampler);
    }

    resize(w: number, h: number, sampler: IntSampler | Filter = "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;
    }

    protected _rotate(idxFn: FnN2) {
        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;
    }
}