Skip to content

Commit

Permalink
feat(color): (#106) add PD int ops, clamp existing porterDuff()
Browse files Browse the repository at this point in the history
- add premultiplyInt / postmultiplyInt
  • Loading branch information
postspectacular committed Jul 25, 2019
1 parent eae671e commit 4c975b2
Show file tree
Hide file tree
Showing 2 changed files with 203 additions and 27 deletions.
199 changes: 172 additions & 27 deletions packages/color/src/porter-duff.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,33 @@
import { Fn2, Fn3 } from "@thi.ng/api";
import { setC4, setN4 } from "@thi.ng/vectors";
import { Color, ReadonlyColor } from "./api";
import { postmultiply, premultiply } from "./premultiply";
import {
postmultiply,
postmultiplyInt,
premultiply,
premultiplyInt
} from "./premultiply";

// http://ssp.impulsetrain.com/porterduff.html
// https://en.wikipedia.org/wiki/Alpha_compositing#Alpha_blending

const porterDuffOp = (
f: Fn2<number, number, number>,
y: 0 | 1,
z: 0 | 1,
min: number
) =>
y
? z
? (s: number, d: number, sda: number, sy: number, sz: number) =>
Math.min(min, f(s, d) * sda + s * sy + d * sz)
: (s: number, d: number, sda: number, sy: number) =>
Math.min(min, f(s, d) * sda + s * sy)
: z
? (s: number, d: number, sda: number, _: number, sz: number) =>
Math.min(min, f(s, d) * sda + d * sz)
: (s: number, d: number, sda: number) => f(s, d) * sda;

/**
* General Porter-Duff HOF operator for **pre-multiplied** RGBA. Use
* `porderDiffP` for applying pre & post multiplication of input and
Expand All @@ -30,16 +52,7 @@ export const porterDuff = (
y: 0 | 1,
z: 0 | 1
) => {
const dot = y
? z
? (s: number, d: number, sda: number, sy: number, sz: number) =>
f(s, d) * sda + s * sy + d * sz
: (s: number, d: number, sda: number, sy: number) =>
f(s, d) * sda + s * sy
: z
? (s: number, d: number, sda: number, _: number, sz: number) =>
f(s, d) * sda + d * sz
: (s: number, d: number, sda: number) => f(s, d) * sda;
const op = porterDuffOp(f, y, z, 1);
return (out: Color | null, src: ReadonlyColor, dest: ReadonlyColor) => {
const sa = src[3];
const da = dest[3];
Expand All @@ -48,17 +61,43 @@ export const porterDuff = (
const sz = z * da * (1 - sa);
return setC4(
out || dest,
dot(src[0], dest[0], sda, sy, sz),
dot(src[1], dest[1], sda, sy, sz),
dot(src[2], dest[2], sda, sy, sz),
x * sda + sy + sz
op(src[0], dest[0], sda, sy, sz),
op(src[1], dest[1], sda, sy, sz),
op(src[2], dest[2], sda, sy, sz),
Math.min(1, x * sda + sy + sz)
);
};
};

export const porterDuffInt = (
f: Fn2<number, number, number>,
x: 0 | 1,
y: 0 | 1,
z: 0 | 1
) => {
const op = porterDuffOp(f, y, z, 255);
return (src: number, dest: number) => {
const sa = (src >>> 24) / 255;
const da = (dest >>> 24) / 255;
const sda = sa * da;
const sy = y * sa * (1 - da);
const sz = z * da * (1 - sa);
return (
((Math.min(255, (x * sda + sy + sz) * 255) << 24) |
(op((src >>> 16) & 0xff, (dest >>> 16) & 0xff, sda, sy, sz) <<
16) |
(op((src >>> 8) & 0xff, (dest >>> 8) & 0xff, sda, sy, sz) <<
8) |
op(src & 0xff, dest & 0xff, sda, sy, sz)) >>>
0
);
};
};

/**
* Like `porterDuff`, but pre-multiplies alpha for both input colors and
* then post-multiplies alpha to output.
* Similar to `porterDuff`, but takes existing PD operator,
* pre-multiplies alpha for both input colors and then post-multiplies
* alpha to output.
*
* @param out
* @param src
Expand All @@ -76,6 +115,19 @@ export const porterDuffP = (
mode(null, premultiply([], src), premultiply(out, dest))
);

/**
* Like `porterDuffP`, but for packed integers.
*
* @param src
* @param dest
* @param mode
*/
export const porterDuffPInt = (
src: number,
dest: number,
mode: Fn2<number, number, number>
) => postmultiplyInt(mode(premultiplyInt(src), premultiplyInt(dest)));

/**
* Porter-Duff operator. None of the terms are used. Always results in
* [0, 0, 0, 0].
Expand All @@ -92,83 +144,86 @@ export const composeClear = (
dest: ReadonlyColor
) => setN4(out || dest, 0);

const FS = (s: number) => s;
const FD = (_: number, d: number) => d;

/**
* Porter-Duff operator. Always results in `src` color, `dest` ignored.
*
* @see porterDuff
*/
export const composeSrc = porterDuff((s) => s, 1, 1, 0);
export const composeSrc = porterDuff(FS, 1, 1, 0);

/**
* Porter-Duff operator. Always results in `dest` color, `src` ignored.
*
* @see porterDuff
*/
export const composeDest = porterDuff((_, d) => d, 1, 0, 1);
export const composeDest = porterDuff(FD, 1, 0, 1);

/**
* Porter-Duff operator. The source color is placed over the destination
* color.
*
* @see porterDuff
*/
export const composeSrcOver = porterDuff((s) => s, 1, 1, 1);
export const composeSrcOver = porterDuff(FS, 1, 1, 1);

/**
* Porter-Duff operator. The destination color is placed over the source
* color.
*
* @see porterDuff
*/
export const composeDestOver = porterDuff((_, d) => d, 1, 1, 1);
export const composeDestOver = porterDuff(FD, 1, 1, 1);

/**
* Porter-Duff operator. The source that overlaps the destination,
* replaces the destination.
*
* @see porterDuff
*/
export const composeSrcIn = porterDuff((s) => s, 1, 0, 0);
export const composeSrcIn = porterDuff(FS, 1, 0, 0);

/**
* Porter-Duff operator. The destination that overlaps the source,
* replaces the source.
*
* @see porterDuff
*/
export const composeDestIn = porterDuff((_, d) => d, 1, 0, 0);
export const composeDestIn = porterDuff(FD, 1, 0, 0);

/**
* Porter-Duff operator. The source that does not overlap the
* destination replaces the destination.
*
* @see porterDuff
*/
export const composeSrcOut = porterDuff((s) => s, 0, 1, 0);
export const composeSrcOut = porterDuff(FS, 0, 1, 0);

/**
* Porter-Duff operator. The destination that does not overlap the
* source replaces the source.
*
* @see porterDuff
*/
export const composeDestOut = porterDuff((_, d) => d, 0, 0, 1);
export const composeDestOut = porterDuff(FD, 0, 0, 1);

/**
* Porter-Duff operator. The source that overlaps the destination is
* composited with the destination.
*
* @see porterDuff
*/
export const composeSrcAtop = porterDuff((s) => s, 1, 0, 1);
export const composeSrcAtop = porterDuff(FS, 1, 0, 1);

/**
* Porter-Duff operator. The destination that overlaps the source is
* composited with the source and replaces the destination.
*
* @see porterDuff
*/
export const composeDestAtop = porterDuff((_, d) => d, 1, 1, 0);
export const composeDestAtop = porterDuff(FD, 1, 1, 0);

/**
* Porter-Duff operator. The non-overlapping regions of source and
Expand All @@ -177,3 +232,93 @@ export const composeDestAtop = porterDuff((_, d) => d, 1, 1, 0);
* @see porterDuff
*/
export const composeXor = porterDuff(() => 0, 0, 1, 1);

////////// Packed ARGB / ABGR versions //////////

export const composeClearInt = () => 0;

/**
* Porter-Duff operator for packed ints. Always results in `src` color, `dest` ignored.
*
* @see porterDuff
*/
export const composeSrcInt = porterDuffInt(FS, 1, 1, 0);

/**
* Porter-Duff operator for packed ints. Always results in `dest` color, `src` ignored.
*
* @see porterDuff
*/
export const composeDestInt = porterDuffInt(FD, 1, 0, 1);

/**
* Porter-Duff operator for packed ints. The source color is placed over the destination
* color.
*
* @see porterDuff
*/
export const composeSrcOverInt = porterDuffInt(FS, 1, 1, 1);

/**
* Porter-Duff operator for packed ints. The destination color is placed over the source
* color.
*
* @see porterDuff
*/
export const composeDestOverInt = porterDuffInt(FD, 1, 1, 1);

/**
* Porter-Duff operator for packed ints. The source that overlaps the destination,
* replaces the destination.
*
* @see porterDuff
*/
export const composeSrcInInt = porterDuffInt(FS, 1, 0, 0);

/**
* Porter-Duff operator for packed ints. The destination that overlaps the source,
* replaces the source.
*
* @see porterDuff
*/
export const composeDestInInt = porterDuffInt(FD, 1, 0, 0);

/**
* Porter-Duff operator for packed ints. The source that does not overlap the
* destination replaces the destination.
*
* @see porterDuff
*/
export const composeSrcOutInt = porterDuffInt(FS, 0, 1, 0);

/**
* Porter-Duff operator for packed ints. The destination that does not overlap the
* source replaces the source.
*
* @see porterDuff
*/
export const composeDestOutInt = porterDuffInt(FD, 0, 0, 1);

/**
* Porter-Duff operator for packed ints. The source that overlaps the destination is
* composited with the destination.
*
* @see porterDuff
*/
export const composeSrcAtopInt = porterDuffInt(FS, 1, 0, 1);

/**
* Porter-Duff operator for packed ints. The destination that overlaps the source is
* composited with the source and replaces the destination.
*
* @see porterDuff
*/
export const composeDestAtopInt = porterDuffInt(FD, 1, 1, 0);

/**
* Porter-Duff operator for packed ints. The non-overlapping regions of source and
* destination are combined.
*
* @see porterDuff
*/
export const composeXorInt = porterDuffInt(() => 0, 0, 1, 1);
31 changes: 31 additions & 0 deletions packages/color/src/premultiply.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,21 @@ export const premultiply: ColorOp = (out, src) => {
return setC4(out || src, src[0] * a, src[1] * a, src[2] * a, a);
};

/**
* Multiplies RGB channels of packed int with alpha channel.
*
* @param src
*/
export const premultiplyInt = (src: number) => {
const a = (src >>> 24) / 0xff;
return (
(src & 0xff000000) |
((((src >>> 16) & 0xff) * a) << 16) |
((((src >>> 8) & 0xff) * a) << 8) |
((src & 0xff) * a)
);
};

/**
* RGBA only. Reverse operation of `premultiply`. Divides RGB channels
* by alpha, unless alpha is zero. Does NOT clamp result.
Expand All @@ -28,3 +43,19 @@ export const postmultiply: ColorOp = (out, src) => {
? set(out, src)
: src;
};

/**
* Reverse op of `premultiplyInt`. Divides RGB channels by alpha (unless
* zero) and DOES clamp result to avoid overflows.
*
* @param src
*/
export const postmultiplyInt = (src: number) => {
const a = (src >>> 24) / 0xff;
return a > 0
? (src & 0xff000000) |
(Math.min(0xff, ((src >>> 16) & 0xff) / a) << 16) |
(Math.min(0xff, ((src >>> 8) & 0xff) / a) << 8) |
Math.min(0xff, (src & 0xff) / a)
: src;
};

0 comments on commit 4c975b2

Please sign in to comment.