diff --git a/README.md b/README.md index 1bcab860..f6c9adf8 100644 --- a/README.md +++ b/README.md @@ -683,6 +683,44 @@ Returns an array of arrays, where the *i*th array contains the *i*th element fro d3.zip([1, 2], [3, 4]); // returns [[1, 3], [2, 4]] ``` +#### Blur + +# d3.blur(*data*, *radius*) · [Source](https://github.com/d3/d3-array/blob/main/src/blur.js), [Examples](https://observablehq.com/@d3/d3-blur) + +Blurs an array of *data* in-place by applying three iterations of a moving average transform, for a fast approximation of a gaussian kernel of the given *radius*, a non-negative number, and returns the array. + +```js +const randomWalk = d3.cumsum({length: 1000}, () => Math.random() - 0.5); +blur(randomWalk, 5); +``` + +Copy the data if you don’t want to smooth it in-place: +```js +const smoothed = blur(randomWalk.slice(), 5); +``` + +# d3.blur2({*data*, *width*[, *height*]}, *rx*[, *ry*]) · [Source](https://github.com/d3/d3-array/blob/main/src/blur.js), [Examples](https://observablehq.com/@d3/d3-blur) + +Blurs a matrix of the given *width* and *height* in-place, by applying an horizontal blur of radius *rx* and a vertical blur or radius *ry* (which defaults to *rx*). The matrix *data* is stored in a flat array, used to determine the *height* if it is not specified. Returns the blurred {data, width, height}. + +```js +data = [ + 1, 0, 0, + 0, 0, 0, + 0, 0, 1 +]; +blur2({data, width: 3}, 1); +``` + +# d3.blurImage(*imageData*, *rx*[, *ry*]) · [Source](https://github.com/d3/d3-array/blob/main/src/blur.js), [Examples](https://observablehq.com/@d3/d3-blurimage) + +Blurs an [ImageData](https://developer.mozilla.org/en-US/docs/Web/API/ImageData) structure in-place, blurring each of the RGBA layers independently by applying an horizontal blur of radius *rx* and a vertical blur or radius *ry* (which defaults to *rx*). Returns the blurred ImageData. + +```js +const imData = context.getImageData(0, 0, width, height); +blurImage(imData, 5); +``` + ### Iterables These are equivalent to built-in array methods, but work with any iterable including Map, Set, and Generator. diff --git a/src/blur.js b/src/blur.js new file mode 100644 index 00000000..7021cbf3 --- /dev/null +++ b/src/blur.js @@ -0,0 +1,115 @@ +export function blur(values, r) { + if (!((r = +r) >= 0)) throw new RangeError("invalid r"); + let length = values.length; + if (!((length = Math.floor(length)) >= 0)) throw new RangeError("invalid length"); + if (!length || !r) return values; + const blur = blurf(r); + const temp = values.slice(); + blur(values, temp, 0, length, 1); + blur(temp, values, 0, length, 1); + blur(values, temp, 0, length, 1); + return values; +} + +export const blur2 = Blur2(blurf); + +export const blurImage = Blur2(blurfImage); + +function Blur2(blur) { + return function(data, rx, ry = rx) { + if (!((rx = +rx) >= 0)) throw new RangeError("invalid rx"); + if (!((ry = +ry) >= 0)) throw new RangeError("invalid ry"); + let {data: values, width, height} = data; + if (!((width = Math.floor(width)) >= 0)) throw new RangeError("invalid width"); + if (!((height = Math.floor(height !== undefined ? height : values.length / width)) >= 0)) throw new RangeError("invalid height"); + if (!width || !height || (!rx && !ry)) return data; + const blurx = rx && blur(rx); + const blury = ry && blur(ry); + const temp = values.slice(); + if (blurx && blury) { + blurh(blurx, temp, values, width, height); + blurh(blurx, values, temp, width, height); + blurh(blurx, temp, values, width, height); + blurv(blury, values, temp, width, height); + blurv(blury, temp, values, width, height); + blurv(blury, values, temp, width, height); + } else if (blurx) { + blurh(blurx, values, temp, width, height); + blurh(blurx, temp, values, width, height); + blurh(blurx, values, temp, width, height); + } else if (blury) { + blurv(blury, values, temp, width, height); + blurv(blury, temp, values, width, height); + blurv(blury, values, temp, width, height); + } + return data; + }; +} + +function blurh(blur, T, S, w, h) { + for (let y = 0, n = w * h; y < n;) { + blur(T, S, y, y += w, 1); + } +} + +function blurv(blur, T, S, w, h) { + for (let x = 0, n = w * h; x < w; ++x) { + blur(T, S, x, x + n, w); + } +} + +function blurfImage(radius) { + const blur = blurf(radius); + return (T, S, start, stop, step) => { + start <<= 2, stop <<= 2, step <<= 2; + blur(T, S, start + 0, stop + 0, step); + blur(T, S, start + 1, stop + 1, step); + blur(T, S, start + 2, stop + 2, step); + blur(T, S, start + 3, stop + 3, step); + }; +} + +// Given a target array T, a source array S, sets each value T[i] to the average +// of {S[i - r], …, S[i], …, S[i + r]}, where r = ⌊radius⌋, start <= i < stop, +// for each i, i + step, i + 2 * step, etc., and where S[j] is clamped between +// S[start] (inclusive) and S[stop] (exclusive). If the given radius is not an +// integer, S[i - r - 1] and S[i + r + 1] are added to the sum, each weighted +// according to r - ⌊radius⌋. +function blurf(radius) { + const radius0 = Math.floor(radius); + if (radius0 === radius) return bluri(radius); + const t = radius - radius0; + const w = 2 * radius + 1; + return (T, S, start, stop, step) => { // stop must be aligned! + if (!((stop -= step) >= start)) return; // inclusive stop + let sum = radius0 * S[start]; + const s0 = step * radius0; + const s1 = s0 + step; + for (let i = start, j = start + s0; i < j; i += step) { + sum += S[Math.min(stop, i)]; + } + for (let i = start, j = stop; i <= j; i += step) { + sum += S[Math.min(stop, i + s0)]; + T[i] = (sum + t * (S[Math.max(start, i - s1)] + S[Math.min(stop, i + s1)])) / w; + sum -= S[Math.max(start, i - s0)]; + } + }; +} + +// Like blurf, but optimized for integer radius. +function bluri(radius) { + const w = 2 * radius + 1; + return (T, S, start, stop, step) => { // stop must be aligned! + if (!((stop -= step) >= start)) return; // inclusive stop + let sum = radius * S[start]; + const s = step * radius; + for (let i = start, j = start + s; i < j; i += step) { + sum += S[Math.min(stop, i)]; + } + for (let i = start, j = stop; i <= j; i += step) { + sum += S[Math.min(stop, i + s)]; + T[i] = sum / w; + sum -= S[Math.max(start, i - s)]; + } + }; +} diff --git a/src/index.js b/src/index.js index 5fb58e8b..29fc3346 100644 --- a/src/index.js +++ b/src/index.js @@ -1,6 +1,7 @@ export {default as bisect, bisectRight, bisectLeft, bisectCenter} from "./bisect.js"; export {default as ascending} from "./ascending.js"; export {default as bisector} from "./bisector.js"; +export {blur, blur2, blurImage} from "./blur.js"; export {default as count} from "./count.js"; export {default as cross} from "./cross.js"; export {default as cumsum} from "./cumsum.js"; diff --git a/test/blur-test.js b/test/blur-test.js new file mode 100644 index 00000000..014757a4 --- /dev/null +++ b/test/blur-test.js @@ -0,0 +1,202 @@ +import assert from "assert"; +import {blur, blur2} from "../src/index.js"; + +it("blur(values, r) returns values", () => { + const V = [0, 0, 0, 0, 0, 0, 27, 0, 0, 0, 0, 0, 0, 0]; + assert.strictEqual(blur(V, 1), V); + assert.deepStrictEqual(V, [0, 0, 0, 1, 3, 6, 7, 6, 3, 1, 0, 0, 0, 0]); +}); + +it("blur(values, r) observes the expected integer radius r", () => { + assert.deepStrictEqual(blur([0, 0, 0, 0, 0, 0, 27, 0, 0, 0, 0, 0, 0, 0], 0.0).map(round), [0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 27.00, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000]); + assert.deepStrictEqual(blur([0, 0, 0, 0, 0, 0, 27, 0, 0, 0, 0, 0, 0, 0], 1.0).map(round), [0.000, 0.000, 0.000, 1.000, 3.000, 6.000, 7.000, 6.000, 3.000, 1.000, 0.000, 0.000, 0.000, 0.000]); + assert.deepStrictEqual(blur([0, 0, 0, 0, 0, 0, 27, 0, 0, 0, 0, 0, 0, 0], 2.0).map(round), [0.216, 0.648, 1.296, 2.160, 3.240, 3.888, 4.104, 3.888, 3.240, 2.160, 1.296, 0.648, 0.216, 0.000]); + assert.deepStrictEqual(blur([0, 0, 0, 0, 0, 0, 27, 0, 0, 0, 0, 0, 0, 0], 3.0).map(round), [1.023, 1.338, 1.732, 2.204, 2.598, 2.834, 2.913, 2.834, 2.598, 2.204, 1.653, 1.181, 0.787, 0.472]); +}); + +it("blur(values, r) observes the expected fractional radius r", () => { + assert.deepStrictEqual(blur([0, 0, 0, 0, 0, 0, 27, 0, 0, 0, 0, 0, 0, 0], 0.5).map(round), [0.000, 0.000, 0.000, 0.422, 2.531, 6.328, 8.438, 6.328, 2.531, 0.422, 0.000, 0.000, 0.000, 0.000]); + assert.deepStrictEqual(blur([0, 0, 0, 0, 0, 0, 27, 0, 0, 0, 0, 0, 0, 0], 1.5).map(round), [0.053, 0.316, 0.949, 2.004, 3.322, 4.430, 4.852, 4.430, 3.322, 2.004, 0.949, 0.316, 0.053, 0.000]); + assert.deepStrictEqual(blur([0, 0, 0, 0, 0, 0, 27, 0, 0, 0, 0, 0, 0, 0], 2.5).map(round), [0.672, 1.078, 1.609, 2.234, 2.813, 3.188, 3.313, 3.188, 2.813, 2.234, 1.594, 1.031, 0.594, 0.281]); + assert.deepStrictEqual(blur([0, 0, 0, 0, 0, 0, 27, 0, 0, 0, 0, 0, 0, 0], 3.5).map(round), [1.266, 1.503, 1.780, 2.057, 2.294, 2.452, 2.505, 2.452, 2.294, 2.030, 1.701, 1.371, 1.081, 0.844]); +}); + +it("blur(values, r) repeats starting values before the window", () => { + assert.deepStrictEqual(blur([27, 0, 0, 0, 0, 0, 0, 0], 0.0).map(round), [27.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000]); + assert.deepStrictEqual(blur([27, 0, 0, 0, 0, 0, 0, 0], 1.0).map(round), [13.000, 9.000, 4.000, 1.000, 0.000, 0.000, 0.000, 0.000]); + assert.deepStrictEqual(blur([27, 0, 0, 0, 0, 0, 0, 0], 2.0).map(round), [11.016, 9.072, 6.696, 4.104, 2.160, 0.864, 0.216, 0.000]); + assert.deepStrictEqual(blur([27, 0, 0, 0, 0, 0, 0, 0], 3.0).map(round), [10.233, 8.974, 7.478, 5.825, 4.093, 2.676, 1.574, 0.787]); +}); + +it("blur(values, r) approximately preserves total value", () => { + assert.strictEqual(blur([0, 0, 0, 0, 0, 0, 27, 0, 0, 0, 0, 0, 0, 0], 0.0).reduce((p, v) => p + v), 27); + assert.strictEqual(blur([0, 0, 0, 0, 0, 0, 27, 0, 0, 0, 0, 0, 0, 0], 0.5).reduce((p, v) => p + v), 27); + assert.strictEqual(blur([0, 0, 0, 0, 0, 0, 27, 0, 0, 0, 0, 0, 0, 0], 1.0).reduce((p, v) => p + v), 27); + assert.strictEqual(blur([0, 0, 0, 0, 0, 0, 27, 0, 0, 0, 0, 0, 0, 0], 1.5).reduce((p, v) => p + v), 27); + assert.strictEqual(blur([0, 0, 0, 0, 0, 0, 27, 0, 0, 0, 0, 0, 0, 0], 2.0).reduce((p, v) => p + v), 27.000000000000004); + assert.strictEqual(blur([0, 0, 0, 0, 0, 0, 27, 0, 0, 0, 0, 0, 0, 0], 2.5).reduce((p, v) => p + v), 26.640625); + assert.strictEqual(blur([0, 0, 0, 0, 0, 0, 27, 0, 0, 0, 0, 0, 0, 0], 3.0).reduce((p, v) => p + v), 26.370262390670547); +}); + +const unit = { + width: 11, + height: 11, + data: [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + ] +}; + +it("blur2(data, r) modifies in-place", () => { + const copy = copy2(unit); + assert.strictEqual(blur2(copy, 1), copy); +}); + +it("data.height is redundant for blur2", () => { + const copy = copy2(unit); + delete copy.height; + assert.deepStrictEqual(blur2(copy, 1).data.map(round), [ + 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, + 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, + 0.000, 0.000, 0.001, 0.004, 0.008, 0.010, 0.008, 0.004, 0.001, 0.000, 0.000, + 0.000, 0.000, 0.004, 0.012, 0.025, 0.029, 0.025, 0.012, 0.004, 0.000, 0.000, + 0.000, 0.000, 0.008, 0.025, 0.049, 0.058, 0.049, 0.025, 0.008, 0.000, 0.000, + 0.000, 0.000, 0.010, 0.029, 0.058, 0.067, 0.058, 0.029, 0.010, 0.000, 0.000, + 0.000, 0.000, 0.008, 0.025, 0.049, 0.058, 0.049, 0.025, 0.008, 0.000, 0.000, + 0.000, 0.000, 0.004, 0.012, 0.025, 0.029, 0.025, 0.012, 0.004, 0.000, 0.000, + 0.000, 0.000, 0.001, 0.004, 0.008, 0.010, 0.008, 0.004, 0.001, 0.000, 0.000, + 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, + 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000 + ]); +}); + +it("blur2(data, r) observes the expected integer radius r", () => { + assert.deepStrictEqual(blur2(copy2(unit), 0), unit); + assert.deepStrictEqual(blur2(copy2(unit), 1).data.map(round), [ + 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, + 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, + 0.000, 0.000, 0.001, 0.004, 0.008, 0.010, 0.008, 0.004, 0.001, 0.000, 0.000, + 0.000, 0.000, 0.004, 0.012, 0.025, 0.029, 0.025, 0.012, 0.004, 0.000, 0.000, + 0.000, 0.000, 0.008, 0.025, 0.049, 0.058, 0.049, 0.025, 0.008, 0.000, 0.000, + 0.000, 0.000, 0.010, 0.029, 0.058, 0.067, 0.058, 0.029, 0.010, 0.000, 0.000, + 0.000, 0.000, 0.008, 0.025, 0.049, 0.058, 0.049, 0.025, 0.008, 0.000, 0.000, + 0.000, 0.000, 0.004, 0.012, 0.025, 0.029, 0.025, 0.012, 0.004, 0.000, 0.000, + 0.000, 0.000, 0.001, 0.004, 0.008, 0.010, 0.008, 0.004, 0.001, 0.000, 0.000, + 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, + 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000 + ]); + assert.deepStrictEqual(blur2(copy2(unit), 2).data.map(round), [ + 0.001, 0.001, 0.002, 0.003, 0.003, 0.004, 0.003, 0.003, 0.002, 0.001, 0.001, + 0.001, 0.002, 0.004, 0.006, 0.007, 0.007, 0.007, 0.006, 0.004, 0.002, 0.001, + 0.002, 0.004, 0.006, 0.010, 0.012, 0.012, 0.012, 0.010, 0.006, 0.004, 0.002, + 0.003, 0.006, 0.010, 0.014, 0.017, 0.018, 0.017, 0.014, 0.010, 0.006, 0.003, + 0.003, 0.007, 0.012, 0.017, 0.021, 0.022, 0.021, 0.017, 0.012, 0.007, 0.003, + 0.004, 0.007, 0.012, 0.018, 0.022, 0.023, 0.022, 0.018, 0.012, 0.007, 0.004, + 0.003, 0.007, 0.012, 0.017, 0.021, 0.022, 0.021, 0.017, 0.012, 0.007, 0.003, + 0.003, 0.006, 0.010, 0.014, 0.017, 0.018, 0.017, 0.014, 0.010, 0.006, 0.003, + 0.002, 0.004, 0.006, 0.010, 0.012, 0.012, 0.012, 0.010, 0.006, 0.004, 0.002, + 0.001, 0.002, 0.004, 0.006, 0.007, 0.007, 0.007, 0.006, 0.004, 0.002, 0.001, + 0.001, 0.001, 0.002, 0.003, 0.003, 0.004, 0.003, 0.003, 0.002, 0.001, 0.001 + ]); +}); + +it("blur2(data, rx, 0) does horizontal blurring", () => { + assert.deepStrictEqual(blur2(copy2(unit), 0, 0).data, [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + ]); + assert.deepStrictEqual(blur2(copy2(unit), 1, 0).data.map(round), [ + 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, + 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, + 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, + 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, + 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, + 0.000, 0.000, 0.037, 0.111, 0.222, 0.259, 0.222, 0.111, 0.037, 0.000, 0.000, + 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, + 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, + 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, + 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, + 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000 + ]); + assert.deepStrictEqual(blur2(copy2(unit), 2, 0).data.map(round), [ + 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, + 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, + 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, + 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, + 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, + 0.024, 0.048, 0.080, 0.120, 0.144, 0.152, 0.144, 0.120, 0.080, 0.048, 0.024, + 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, + 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, + 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, + 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, + 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000 + ]); +}); + +it("blur2(data, 0, ry) does vertical blurring", () => { + assert.deepStrictEqual(blur2(copy2(unit), 0, 0).data, [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + ]); + assert.deepStrictEqual(blur2(copy2(unit), 0, 1).data.map(round), [ + 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, + 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, + 0.000, 0.000, 0.000, 0.000, 0.000, 0.037, 0.000, 0.000, 0.000, 0.000, 0.000, + 0.000, 0.000, 0.000, 0.000, 0.000, 0.111, 0.000, 0.000, 0.000, 0.000, 0.000, + 0.000, 0.000, 0.000, 0.000, 0.000, 0.222, 0.000, 0.000, 0.000, 0.000, 0.000, + 0.000, 0.000, 0.000, 0.000, 0.000, 0.259, 0.000, 0.000, 0.000, 0.000, 0.000, + 0.000, 0.000, 0.000, 0.000, 0.000, 0.222, 0.000, 0.000, 0.000, 0.000, 0.000, + 0.000, 0.000, 0.000, 0.000, 0.000, 0.111, 0.000, 0.000, 0.000, 0.000, 0.000, + 0.000, 0.000, 0.000, 0.000, 0.000, 0.037, 0.000, 0.000, 0.000, 0.000, 0.000, + 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, + 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000 + ]); + assert.deepStrictEqual(blur2(copy2(unit), 0, 2).data.map(round), [ + 0.000, 0.000, 0.000, 0.000, 0.000, 0.024, 0.000, 0.000, 0.000, 0.000, 0.000, + 0.000, 0.000, 0.000, 0.000, 0.000, 0.048, 0.000, 0.000, 0.000, 0.000, 0.000, + 0.000, 0.000, 0.000, 0.000, 0.000, 0.080, 0.000, 0.000, 0.000, 0.000, 0.000, + 0.000, 0.000, 0.000, 0.000, 0.000, 0.120, 0.000, 0.000, 0.000, 0.000, 0.000, + 0.000, 0.000, 0.000, 0.000, 0.000, 0.144, 0.000, 0.000, 0.000, 0.000, 0.000, + 0.000, 0.000, 0.000, 0.000, 0.000, 0.152, 0.000, 0.000, 0.000, 0.000, 0.000, + 0.000, 0.000, 0.000, 0.000, 0.000, 0.144, 0.000, 0.000, 0.000, 0.000, 0.000, + 0.000, 0.000, 0.000, 0.000, 0.000, 0.120, 0.000, 0.000, 0.000, 0.000, 0.000, + 0.000, 0.000, 0.000, 0.000, 0.000, 0.080, 0.000, 0.000, 0.000, 0.000, 0.000, + 0.000, 0.000, 0.000, 0.000, 0.000, 0.048, 0.000, 0.000, 0.000, 0.000, 0.000, + 0.000, 0.000, 0.000, 0.000, 0.000, 0.024, 0.000, 0.000, 0.000, 0.000, 0.000 + ]); +}); + +function copy2({data, width, height}) { + return {data: data.slice(), width, height}; +} + +function round(x) { + return Math.round(x * 1000) / 1000 || 0; +}