Skip to content

Commit

Permalink
feat(pixel): add dither support for int buffers/formats
Browse files Browse the repository at this point in the history
  • Loading branch information
postspectacular committed May 26, 2020
1 parent d6c490f commit 4475fc1
Show file tree
Hide file tree
Showing 5 changed files with 182 additions and 1 deletion.
18 changes: 18 additions & 0 deletions packages/pixel/src/api.ts
Expand Up @@ -126,6 +126,16 @@ export interface PackedChannel {
* Normalized float accessor
*/
setFloat: ChannelSetter<number>;
/**
* Applies ordered dithering to given channel value.
*/
dither: (
mat: BayerMatrix,
steps: number,
x: number,
y: number,
val: number
) => number;
}

/**
Expand Down Expand Up @@ -324,3 +334,11 @@ export interface BlitOpts {
*/
h: number;
}

export type BayerSize = 1 | 2 | 4 | 8 | 16 | 32 | 64;

export interface BayerMatrix {
mat: number[][];
invSize: number;
mask: number;
}
104 changes: 104 additions & 0 deletions packages/pixel/src/dither.ts
@@ -0,0 +1,104 @@
import type { NumericArray } from "@thi.ng/api";
import { clamp } from "@thi.ng/math";
import type { BayerMatrix, BayerSize } from "./api";

const init = (
x: number,
y: number,
size: number,
val: number,
step: number,
mat: number[][]
) => {
if (size === 1) {
!mat[y] && (mat[y] = []);
mat[y][x] = val;
return mat;
}
size >>= 1;
const step4 = step << 2;
init(x, y, size, val, step4, mat);
init(x + size, y + size, size, val + step, step4, mat);
init(x + size, y, size, val + step * 2, step4, mat);
init(x, y + size, size, val + step * 3, step4, mat);
return mat;
};

/**
* Creates a Bayer matrix of given kernel size (power of 2) for ordered
* dithering and use with {@link ditherPixels}
*
* @remarks
* Reference:
* - https://en.wikipedia.org/wiki/Ordered_dithering
*
* @param size
*/
export const defBayer = (size: BayerSize): BayerMatrix => ({
mat: init(0, 0, size, 0, 1, []),
invSize: 1 / (size * size),
mask: size - 1,
});

/**
* Single-channel/value ordered dither using provided Bayer matrix.
*
* @param mat - matrix
* @param dsteps - number of dest colors
* @param drange - dest color range
* @param srange - src color range
* @param x - x pos
* @param y - y pos
* @param val - src value
*/
export const orderedDither = (
{ mat, mask, invSize }: BayerMatrix,
dsteps: number,
drange: number,
srange: number,
x: number,
y: number,
val: number
) => {
val =
(dsteps * (val / srange) + mat[y & mask][x & mask] * invSize - 0.5) | 0;
dsteps--;
return clamp(val, 0, dsteps) * ((drange - 1) / dsteps);
};

/**
* Applies ordered dither to given single-channel raw pixel array `src`
* and writes results to `dest` (will be created if `null`).
*
* @remarks
* Also see {@link defBayer} for Bayer matrix creation.
*
* @param dest
* @param src
* @param width
* @param height
* @param mat - bayer dither matrix
* @param dsteps - target number of color steps
* @param drange - target color resolution (e.g. 256)
* @param srange - source color resolution
*/
export const ditherPixels = (
dest: NumericArray | null,
src: NumericArray,
width: number,
height: number,
mat: BayerMatrix,
dsteps: number,
drange: number,
srange: number
) => {
!dest && (dest = src.slice());
drange--;
for (let y = 0, i = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
dest[i] = orderedDither(mat, dsteps, drange, srange, x, y, src[i]);
i++;
}
}
return dest;
};
6 changes: 5 additions & 1 deletion packages/pixel/src/format.ts
Expand Up @@ -10,14 +10,16 @@ import {
PackedFormatSpec,
} from "./api";
import { compileFromABGR, compileToABGR } from "./codegen";
import { orderedDither } from "./dither";
import { luminanceABGR } from "./utils";

const defChannel = (
ch: PackedChannelSpec,
idx: number,
shift: number
): PackedChannel => {
const mask0 = (1 << ch.size) - 1;
const num = 1 << ch.size;
const mask0 = num - 1;
const maskA = (mask0 << shift) >>> 0;
const invMask = ~maskA >>> 0;
const lane = ch.lane != null ? ch.lane : idx;
Expand All @@ -35,6 +37,8 @@ const defChannel = (
setInt,
float: (x) => int(x) / mask0,
setFloat: (src, x) => setInt(src, clamp01(x) * mask0),
dither: (mat, steps, x, y, val) =>
orderedDither(mat, steps, num, num, x, y, val),
};
};

Expand Down
1 change: 1 addition & 0 deletions packages/pixel/src/index.ts
@@ -1,6 +1,7 @@
export * from "./api";
export * from "./canvas";
export * from "./codegen";
export * from "./dither";
export * from "./float";
export * from "./format";
export * from "./packed";
Expand Down
54 changes: 54 additions & 0 deletions packages/pixel/src/packed.ts
Expand Up @@ -6,6 +6,8 @@ import {
premultiplyInt,
} from "@thi.ng/porter-duff";
import {
BayerMatrix,
BayerSize,
BlendFnInt,
BlitOpts,
Lane,
Expand All @@ -15,6 +17,7 @@ import {
} from "./api";
import { canvasPixels, imageCanvas } from "./canvas";
import { compileGrayFromABGR, compileGrayToABGR } from "./codegen";
import { defBayer } from "./dither";
import { ABGR8888, defPackedFormat } from "./format";
import {
clampRegion,
Expand Down Expand Up @@ -319,4 +322,55 @@ export class PackedBuffer {
}
return this;
}

/**
* Applies in-place, ordered dithering using provided dither matrix
* (or matrix size) and desired number of dither levels, optionally
* specified individually (per channel). Each channel is be
* processed independently. Channels can be excluded from dithering
* by setting their target size to zero or negative numbers.
*
* @remarks
* A `size` of 1 will result in simple posterization of each
* channel. The `numColors` value(s) MUST be in the `[0 ..
* numColorsInChannel]` interval.
*
* Also see: {@link defBayer}, {@link ditherPixels}.
*
* @param size - dither matrix/size
* @param numColors - num target colors/steps
*/
dither(size: BayerSize | BayerMatrix, numColors: number | number[]) {
const { pixels, format, width } = this;
const steps = isNumber(numColors)
? new Array<number>(format.channels.length).fill(numColors)
: numColors;
const mat = isNumber(size) ? defBayer(size) : size;
for (
let i = 0,
n = pixels.length,
nc = format.channels.length,
x = 0,
y = 0;
i < n;
i++
) {
let col = pixels[i];
for (let j = 0; j < nc; j++) {
const ch = format.channels[j];
const cs = steps[j];
cs > 0 &&
(col = ch.setInt(
col,
ch.dither(mat, cs, x, y, ch.int(col))
));
}
pixels[i] = col;
if (++x === width) {
x = 0;
y++;
}
}
return this;
}
}

0 comments on commit 4475fc1

Please sign in to comment.