Public
Edited
Nov 2, 2024
2 forks
Importers
54 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
// parameters are passed as an object, so that we can turn boxblur into a worker
function boxblur({source, width, height, radius}) {
if (!(radius > 0)) return source.slice();

let target = new Uint8ClampedArray(source.length);
/*
* total number of summed pixels is two times the radius plus
* the central pixel, squared. See for example this neighbourhood
* of radius 3 around a pixel:
*
* . . . . . . .
* . . . . . . .
* . . . . . . .
* . . . # . . .
* . . . . . . .
* . . . . . . .
* . . . . . . .
*
* At the end we have to divide by the total area, but it's quicker
* to multiply by the inverse instead.
*/
const factor = 1 / ((2*radius + 1) * (2*radius + 1));

const {min, max} = Math;
/*
* Tangent: this is a really slow way to implement a blur function
* We loop over the entire area for every pixel, which makes it
* effectively O(n * r²). That gets slow really quickly for larger
* radii (not to mention memory access patterns and such). There
* are faster ways methods - we explore a two-pass version below
* that is effectively 2 * O(n*r) near the end of this notebook.
* For educational purposes we stick to these slow versions first.
*/
for(let y = 0; y < height; y++) {
const line = y * width * 4;
for (let x = 0; x < width; x++) {
let r = 0;
let g = 0;
let b = 0;
for (let dy = -radius; dy <= radius; dy++) {
const line_dy = min(max(0, y + dy), height-1) * width;
for (let dx = -radius; dx <= radius; dx++) {
const idx = (min(max(0, x + dx), width-1) + line_dy) * 4;
r += source[idx];
g += source[idx + 1];
b += source[idx + 2];
}
}
const cIdx = x*4 + line;
target[cIdx] = r * factor;
target[cIdx+1] = g * factor;
target[cIdx+2] = b * factor;
target[cIdx+3] = 255;
}
}

return target;
}
Insert cell
Insert cell
Insert cell
Insert cell
// This is not very efficient, so don't use this in filters with big
// radii. On the other hand, it's easier to follow code-wise
function selectNearest(cx, cy, dx, dy, source, width, height) {
// assuming that source is an RGBA array, the central index should be at:
const cIdx = (cx + cy * width) * 4;


// Side-note: interesting dilemma: do we pick "per color" or "per color channel"?
// I suspect the latter will perform better. However, we first try this with a
// closest-color formula using Kotsarenko/Ramos.

// 35215 is the maximum possible value for the Kotsarenko/Ramos YIQ difference metric
let minDiff = 35215 ;
let pick = cIdx;

// we only want to test pixels that are inside the boundaries
if (cx + dx < width && cy + dy < height) {
const pos = cIdx + (dx + dy * width) * 4;
const diff = colorDelta(source, cIdx, pos);
if (diff < minDiff) {
pick = pos;
minDiff = diff;
}
}
if (cx - dx >= 0 && cy - dy >= 0) {
const pos = cIdx - (dx + dy * width) * 4;
const diff = colorDelta(source, cIdx, pos);
if (diff < minDiff) {
pick = pos;
minDiff = diff;
}
}
if (cx + dy < width && cy - dx >= 0) {
const pos = cIdx + (dy - dy * width) * 4;
const diff = colorDelta(source, cIdx, pos);
if (diff < minDiff) {
pick = pos;
minDiff = diff;
}
}
if (cx - dy >= 0 && cy + dx < height) {
const pos = cIdx - (dy - dy * width) * 4;
const diff = colorDelta(source, cIdx, pos);
if (diff < minDiff) {
pick = pos;
minDiff = diff;
}
}
return pick;
}
Insert cell
// Adapted from code in https://github.com/mapbox/pixelmatch

/* Inlined for better performance
// function rgb2y(r, g, b) { return r * 0.29889531 + g * 0.58662247 + b * 0.11448223; }
// function rgb2i(r, g, b) { return r * 0.59597799 - g * 0.27417610 - b * 0.32180189; }
// function rgb2q(r, g, b) { return r * 0.21147017 - g * 0.52261711 + b * 0.31114694; }

// // blend semi-transparent color with white
// function blend(c, a) {
// return 255 + (c - 255) * a;
// }
*/

// calculate color difference according to the paper "Measuring perceived color difference
// using YIQ NTSC transmission color space in mobile applications" by Y. Kotsarenko and F. Ramos
function colorDelta(img, pos1, pos2) {
let r1 = img[pos1 + 0];
let g1 = img[pos1 + 1];
let b1 = img[pos1 + 2];
// let a1 = img[pos1 + 3]; // we ignore alpha in this demo, for perf reasons

let r2 = img[pos2 + 0];
let g2 = img[pos2 + 1];
let b2 = img[pos2 + 2];
// let a2 = img[pos2 + 3];

if (r1 === r2 && g1 === g2 && b1 === b2) return 0;

// if (a1 < 255) {
// a1 /= 255;
// r1 = 255 + (r1 - 255) * a1; // blend(r1, a1);
// g1 = 255 + (g1 - 255) * a1; // blend(g1, a1);
// b1 = 255 + (b1 - 255) * a1; // blend(b1, a1);
// }

// if (a2 < 255) {
// a2 /= 255;
// r2 = 255 + (r2 - 255) * a2; // blend(r2, a2);
// g2 = 255 + (g2 - 255) * a2; // blend(g2, a2);
// b2 = 255 + (b2 - 255) * a2; // blend(b2, a2);
// }

// const y = rgb2y(r1, g1, b1) - rgb2y(r2, g2, b2);
// const i = rgb2i(r1, g1, b1) - rgb2i(r2, g2, b2);
// const q = rgb2q(r1, g1, b1) - rgb2q(r2, g2, b2);
const r = r1 - r2;
const g = g1 - g2;
const b = b1 - b2;

// Without strength-reduction
// const y = r * 0.29889531 + g * 0.58662247 + b * 0.11448223
// const i = r * 0.59597799 - g * 0.27417610 - b * 0.32180189
// const q = r * 0.21147017 - g * 0.52261711 + b * 0.31114694
// return 0.5053 * y * y + 0.299 * i * i + 0.1957 * q * q;
// With strength-reduced constants
const y = r * 0.2124681075446384 + g * 0.4169973963260294 + b * 0.08137907133969426;
const i = r * 0.3258860837850668 - g * 0.14992193838645426 - b * 0.17596414539861255;
const q = r * 0.0935501584120867 - g * 0.23119531908149002 + b * 0.13764516066940333;
return y*y + i*i + q*q
}
Insert cell
Insert cell
function boxblurSNN({source, width, height, radius}) {
if (!(radius > 0)) return source.slice();

let target = new Uint8ClampedArray(source.length);
/*
* total number of summed pixels is the central pixel,
* plus one quadrant, which turns out to be radius * (radius + 1)
* See for example this neighbourhood of radius 3 around a pixel:
*
* | | | | - - -
* | | | | - - -
* | | | | - - -
* - - - # - - -
* - - - | | | |
* - - - | | | |
* - - - | | | |
* At the end we have to divide by that number, but it's quicker
* to multiply by the inverse instead.
*/
const factor = 1 / (radius * (radius + 1) + 1);

for(let y = 0; y < height; y++) {
const line = y * width * 4;
for (let x = 0; x < width; x++) {
const cIdx = x*4 + line;
let r = source[cIdx];
let g = source[cIdx + 1];
let b = source[cIdx + 2];
for (let dx = 1; dx <= radius; dx++) {
for (let dy = 0; dy <= radius; dy++) {
const pick = selectNearest(x, y, dx, dy, source, width, height);
r += source[pick];
g += source[pick + 1];
b += source[pick + 2];
}
}
target[cIdx] = r * factor | 0;
target[cIdx+1] = g * factor | 0;
target[cIdx+2] = b * factor | 0;
target[cIdx+3] = 255;
}
}
return target;
}
Insert cell
Insert cell
Insert cell
Insert cell
function bellcurvish2D(radius) {
const p = new Uint32Array(radius+1);
for (let i = 0; i <= (radius+1)/2; i++){
for (let j = 0; j <= radius/2; j++) {
p[i+j]++;
}
}
const q = new Uint32Array(p.length*2 - 1);
for (let i = 0; i < p.length; i++){
for (let j = 0; j < p.length; j++) {
q[i+j] += p[j];
}
}
let total = 0;
const quadratic2D = [];
for (let i = 0; i < q.length; i++){
const c = q[i];
const q2 = [];
for(let j = 0; j < q.length; j++){
const val = c * q[j];
total += val;
q2.push(val);
}
quadratic2D.push(q2);
}
const norm = 1/total;
for (let i = 0; i < q.length; i++){
for(let j = 0; j < q.length; j++){
quadratic2D[i][j] *= norm;
}
}
return quadratic2D;
}
Insert cell
Insert cell
function gaussianBlur({source, width, height, radius}) {
if (!(radius > 0)) return source.slice();
let target = new Uint8ClampedArray(source.length);
const kernel2 = bellcurvish2D(radius);
const {min, max} = Math;
for(let y = 0; y < height; y++) {
const line = y * width * 4;
for (let x = 0; x < width; x++) {
let r = 0;
let g = 0;
let b = 0;
for (let dy = -radius; dy <= radius; dy++) {
const line_dy = min(max(0, y + dy), height-1) * width;
for (let dx = -radius; dx <= radius; dx++) {
const idx = (min(max(0, x + dx), width-1) + line_dy) * 4;
const weight = kernel2[radius + dx][radius + dy];
r += source[idx] * weight;
g += source[idx + 1] * weight;
b += source[idx + 2] * weight;
}
}
const cIdx = x*4 + line;
target[cIdx] = r;
target[cIdx+1] = g;
target[cIdx+2] = b;
target[cIdx+3] = 255;
}
}

return target;
}
Insert cell
Insert cell
Insert cell
Insert cell
function gaussianBlurSNN({source, width, height, radius}) {
if (!(radius > 0)) return source.slice();
let target = new Uint8ClampedArray(source.length);
let totalWeight = 0;
const kernel = bellcurvish2D(radius);

// because we're dealing with quadrants again, we only want
// 1/4 of the kernel weights plus the central pixel.
for(let x = radius ; x < kernel.length; x++) {
const row = kernel[x];
for (let y = radius + 1; y < row.length; y++) {
totalWeight += row[y];
}
}
const centerWeight = kernel[radius][radius];
totalWeight += centerWeight;

const norm = 1 / totalWeight
const {min, max} = Math;
for(let y = 0; y < height; y++) {
const line = y * width * 4;
for (let x = 0; x < width; x++) {
const cIdx = x*4 + line;
let r = source[cIdx] * centerWeight;
let g = source[cIdx + 1] * centerWeight;
let b = source[cIdx + 2] * centerWeight;
for (let dx = 1; dx <= radius; dx++) {
const row = kernel[radius + dx];
for (let dy = 0; dy <= radius; dy++) {
const pick = selectNearest(x, y, dx, dy, source, width, height);
const weight = row[radius + dy];
r += source[pick] * weight;
g += source[pick + 1] * weight;
b += source[pick + 2] * weight;
}
}
target[cIdx] = r * norm | 0;
target[cIdx+1] = g * norm | 0;
target[cIdx+2] = b * norm | 0;
target[cIdx+3] = 255;
}
}

return target;
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function selectDipole(cx, cy, dx, dy, source, width, height) {
// assuming that source is an RGBA array, the central index should be at:
const cIdx = (cx + cy * width) * 4;
// 35215 is the maximum possible value for the Kotsarenko/Ramos YIQ difference metric
let minDiff = 35215, maxDiff = 0;
let minPick = cIdx, maxPick = cIdx;

// we only want to test pixels that are within the boundaries
if (cx + dx < width && cy + dy < height) {
const pos = cIdx + (dx + dy * width) * 4;
const diff = colorDelta(source, cIdx, pos);
if (diff < minDiff) {
minPick = pos;
minDiff = diff;
}
if (diff > maxDiff) {
maxPick = pos;
maxDiff = diff;
}
}
if (cx - dx >= 0 && cy - dy >= 0) {
const pos = cIdx - (dx + dy * width) * 4;
const diff = colorDelta(source, cIdx, pos);
if (diff < minDiff) {
minPick = pos;
minDiff = diff;
}
if (diff > maxDiff) {
maxPick = pos;
maxDiff = diff;
}
}
if (cx + dy < width && cy - dx >= 0) {
const pos = cIdx + (dy - dy * width) * 4;
const diff = colorDelta(source, cIdx, pos);
if (diff < minDiff) {
minPick = pos;
minDiff = diff;
}
if (diff > maxDiff) {
maxPick = pos;
maxDiff = diff;
}
}
if (cx - dy >= 0 && cy + dx < height) {
const pos = cIdx - (dy - dy * width) * 4;
const diff = colorDelta(source, cIdx, pos);
if (diff < minDiff) {
minPick = pos;
minDiff = diff;
}
if (diff > maxDiff) {
maxPick = pos;
maxDiff = diff;
}
}
return {minPick, minDiff, maxPick, maxDiff};
}
Insert cell
function symmetricalNeighbourEdgeDetection({source, width, height, radius}){
if (!(radius > 0)) return source.slice();
let target = new Uint8ClampedArray(source.length);
let totalWeight = 0;
const kernel = bellcurvish2D(radius);

// because we're dealing with quadrants again, we only want
// 1/4 of the kernel weights plus the central pixel.
for(let x = radius ; x < kernel.length; x++) {
const row = kernel[x];
for (let y = radius + 1; y < row.length; y++) {
totalWeight += row[y];
}
}
const centerWeight = kernel[radius][radius];
totalWeight += centerWeight;

const norm = 1 / totalWeight

const {min, max, abs, round} = Math;
for(let y = 0; y < height; y++) {
const line = y * width * 4;
for (let x = 0; x < width; x++) {
const cIdx = x*4 + line;
let rf = source[cIdx] * centerWeight;
let gf = source[cIdx + 1] * centerWeight;
let bf = source[cIdx + 2] * centerWeight;
let rn = rf;
let gn = gf;
let bn = bf;
for (let dx = 1; dx <= radius; dx++) {
const row = kernel[radius + dx];
for (let dy = 0; dy <= radius; dy++) {
const {minPick, maxPick} = selectDipole(x, y, dx, dy, source, width, height);
const weight = row[radius + dy];
rf += source[maxPick] * weight;
gf += source[maxPick + 1] * weight;
bf += source[maxPick + 2] * weight;
rn += source[minPick] * weight;
gn += source[minPick + 1] * weight;
bn += source[minPick + 2] * weight;
}
}
// In order to show whether the edge is brighter or darker,
// we subtract the difference from 127.5, or neutral grey
target[cIdx] = round(127.5 - (rn - rf) * norm);
target[cIdx+1] = round(127.5 - (gn - gf) * norm);
target[cIdx+2] = round(127.5 - (bn - bf) * norm);
target[cIdx+3] = 255;
}
}

return target;
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function unsharpMask({source, width, height, radius}) {
if (!(radius > 0)) return source.slice();
let target = new Uint8ClampedArray(source.length);
let totalWeight = 0;
const kernel = bellcurvish2D(radius);

// we need a negative weight to get an unsharp-mask effect
for(let i = 0; i < kernel.length; i++) {
const row = kernel[i];
for(let j = 0; j < kernel.length; j++) {
row[j] = -row[j];
}
}
// the central kernel has to be offset by the negative weight
const centerWeight = 2 + kernel[radius][radius];
kernel[radius][radius] = centerWeight;

const {min, max} = Math;
for(let y = 0; y < height; y++) {
const line = y * width * 4;
for (let x = 0; x < width; x++) {
let r = 0;
let g = 0;
let b = 0;
for (let dy = -radius; dy <= radius; dy++) {
const line_dy = min(max(0, y + dy), height-1) * width;
// funny enough, because our kernel is symmetrical we can
// interchangably do kernel[x][y] or kernel[y][x]
const column = kernel[radius + dy];
for (let dx = -radius; dx <= radius; dx++) {
const idx = (min(max(0, x + dx), width-1) + line_dy) * 4;
const weight = column[radius + dx];
r += source[idx] * weight;
g += source[idx + 1] * weight
b += source[idx + 2] * weight;
}
}
const cIdx = x*4 + line;
target[cIdx] = r;
target[cIdx+1] = g;
target[cIdx+2] = b;
target[cIdx+3] = 255;
}
}

return target;
}
Insert cell
Insert cell
Insert cell
Insert cell
function symmetricalNeighbourSharpen({source, width, height, radius}){
if (!(radius > 0)) return source.slice();
let target = new Uint8ClampedArray(source.length);
let totalWeight = 0;
const kernel = bellcurvish2D(radius);

// because we're dealing with quadrants again, we only want
// 1/4 of the kernel weights plus the central pixel.
for(let x = radius ; x < kernel.length; x++) {
const row = kernel[x];
for (let y = radius + 1; y < row.length; y++) {
totalWeight += row[y];
}
}
const centerWeight = kernel[radius][radius];
totalWeight += centerWeight;

const norm = 1 / totalWeight

const {min, max, abs, round} = Math;
for(let y = 0; y < height; y++) {
const line = y * width * 4;
for (let x = 0; x < width; x++) {
const cIdx = x*4 + line;
const r0 = source[cIdx];
const g0 = source[cIdx + 1];
const b0 = source[cIdx + 2];
let rf = r0 * centerWeight;
let gf = g0 * centerWeight;
let bf = b0 * centerWeight;
let rn = rf;
let gn = gf;
let bn = bf;
for (let dx = 1; dx <= radius; dx++) {
const row = kernel[radius + dx];
for (let dy = 0; dy <= radius; dy++) {
const {minPick, maxPick} = selectDipole(x, y, dx, dy, source, width, height);
const weight = row[radius + dy];
rf += source[maxPick] * weight;
gf += source[maxPick + 1] * weight;
bf += source[maxPick + 2] * weight;
rn += source[minPick] * weight;
gn += source[minPick + 1] * weight;
bn += source[minPick + 2] * weight;
}
}
// instead of neutral grey, we now apply
// the difference to the original pixel
target[cIdx] = round(r0 + (rn - rf) * norm);
target[cIdx+1] = round(g0 + (gn - gf) * norm);
target[cIdx+2] = round(b0 + (bn - bf) * norm);
target[cIdx+3] = 255;
}
}

return target;
}
Insert cell
Insert cell
Insert cell
Insert cell
function symmetricalNeighbourSharpenNoHalo({source, width, height, radius}){
if (!(radius > 0)) return source.slice();
let target = new Uint8ClampedArray(source.length);
let totalWeight = 0;
const kernel = bellcurvish2D(radius);

// because we're dealing with quadrants again, we only want
// 1/4 of the kernel weights plus the central pixel.
for(let x = radius ; x < kernel.length; x++) {
const row = kernel[x];
for (let y = radius + 1; y < row.length; y++) {
totalWeight += row[y];
}
}
const centerWeight = kernel[radius][radius];
totalWeight += centerWeight;

const norm = 1 / totalWeight

const {min, max, abs, round} = Math;
for(let y = 0; y < height; y++) {
const line = y * width * 4;
for (let x = 0; x < width; x++) {
const cIdx = x*4 + line;
const r0 = source[cIdx];
const g0 = source[cIdx + 1];
const b0 = source[cIdx + 2];
let rf = r0 * centerWeight;
let gf = g0 * centerWeight;
let bf = b0 * centerWeight;
let rn = rf;
let gn = gf;
let bn = bf;
let diffF = 0;
let diffN = 0;
for (let dx = 1; dx <= radius; dx++) {
const row = kernel[radius + dx];
for (let dy = 0; dy <= radius; dy++) {
const {minPick, minDiff, maxPick, maxDiff} = selectDipole(x, y, dx, dy, source, width, height);
diffF += maxDiff;
diffN += minDiff;
const weight = row[radius + dy];
rf += source[maxPick] * weight;
gf += source[maxPick + 1] * weight;
bf += source[maxPick + 2] * weight;
rn += source[minPick] * weight;
gn += source[minPick + 1] * weight;
bn += source[minPick + 2] * weight;
}
}
const diffscale = (diffN > 0 && diffF > 0) ? diffN / diffF : 1;
rf = (rf * norm - r0) * diffscale;
gf = (gf * norm - g0) * diffscale;
bf = (bf * norm - b0) * diffscale;
rn = rn * norm - r0;
gn = gn * norm - g0;
bn = bn * norm - b0;
// instead of neutral grey, we now apply
// the difference to the original pixel
target[cIdx] = round(r0 + (rn - rf));
target[cIdx+1] = round(g0 + (gn - gf));
target[cIdx+2] = round(b0 + (bn - bf));
target[cIdx+3] = 255;
}
}

return target;
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function bellcurvish1D(radius) {
const p = new Uint32Array(radius+1);
for (let i = 0; i <= (radius+1)/2; i++){
for (let j = 0; j <= radius/2; j++) {
p[i+j]++;
}
}
const q = new Uint32Array(p.length*2 - 1);
for (let i = 0; i < p.length; i++){
for (let j = 0; j < p.length; j++) {
q[i+j] += p[j];
}
}

const quadratic = [];
for (let v of q){
quadratic.push(v*v);
}
return quadratic;
}
Insert cell
function gaussianBlurSNNTwoPass({source, width, height, radius}) {
let target = new Uint8ClampedArray(source.length),
buffer = new Float64Array(source.length);
const kernel = bellcurvish1D(radius);
let totalWeight = 0;
for(let i = radius; i < kernel.length; i++) totalWeight += kernel[i];
const norm = 1 / totalWeight;
const {min, max} = Math;
// Horizontal pass
const centerWeight = kernel[radius];
for(let y = 0; y < height; y++) {
const line = y * width * 4;
for (let x = 0; x < width; x++) {
const cIdx = x*4 + line;
let r = source[cIdx] * centerWeight;
let g = source[cIdx+1] * centerWeight;
let b = source[cIdx+2] * centerWeight;
for (let dx = 1; dx <= radius; dx++) {
const pick = selectNearest(x, y, dx, 0, source, width, height);
const weight = kernel[radius + dx]
r += source[pick] * weight;
g += source[pick + 1] * weight;
b += source[pick + 2] * weight;
}
buffer[cIdx] = r * norm;
buffer[cIdx+1] = g * norm;
buffer[cIdx+2] = b * norm;
}
}
// Vertical pass
for(let y = 0; y < height; y++) {
const line = y * width * 4;
for (let x = 0; x < width; x++) {
const cIdx = x*4 + line;
let r = buffer[cIdx] * centerWeight;
let g = buffer[cIdx+1] * centerWeight;
let b = buffer[cIdx+2] * centerWeight;
for (let dy = 1; dy <= radius; dy++) {
const pick = selectNearest(x, y, 0, dy, buffer, width, height);
const weight = kernel[radius + dy];
r += buffer[pick] * weight;
g += buffer[pick + 1] * weight;
b += buffer[pick + 2] * weight;
}
target[cIdx] = r * norm;
target[cIdx+1] = g * norm;
target[cIdx+2] = b * norm;
target[cIdx+3] = 255;
}
}

return target;
}
Insert cell
Insert cell
Insert cell
Insert cell
// This is becoming quite the mouthful
function symmetricalNeighbourSharpenNoHaloTwoPass({source, width, height, radius}){
if (!(radius > 0)) return source.slice();
let target = new Uint8ClampedArray(source.length),
buffer = new Float64Array(source.length);
const kernel = bellcurvish1D(radius);
let totalWeight = 0;
const centerWeight = kernel[radius];
totalWeight += centerWeight;
for(let i = radius+1; i < kernel.length; i++) {
// we apply the unsharp mask in two passes, so we should
// reduce the weights a bit in order to avoid double-sharpening
const v = kernel[i] * 0.5;
totalWeight += v;
kernel[i] = v;
}
const norm = 1 / totalWeight;

const {min, max, abs, round} = Math;
// Horizontal pass
for(let y = 0; y < height; y++) {
const line = y * width * 4;
for (let x = 0; x < width; x++) {
const cIdx = x*4 + line;
const r0 = source[cIdx];
const g0 = source[cIdx + 1];
const b0 = source[cIdx + 2];
let rf = r0 * centerWeight;
let gf = g0 * centerWeight;
let bf = b0 * centerWeight;
let rn = rf;
let gn = gf;
let bn = bf;
let diffF = 0;
let diffN = 0;
for (let dx = 1; dx <= radius; dx++) {
const {minPick, minDiff, maxPick, maxDiff} = selectDipole(x, y, dx, 0, source, width, height);
diffF += maxDiff;
diffN += minDiff;
const weight = kernel[radius + dx];
rf += source[maxPick] * weight;
gf += source[maxPick + 1] * weight;
bf += source[maxPick + 2] * weight;
rn += source[minPick] * weight;
gn += source[minPick + 1] * weight;
bn += source[minPick + 2] * weight;
}
const diffscale = (diffN > 0 && diffF > 0) ? diffN / diffF : 1;
rf = (rf * norm - r0) * diffscale;
gf = (gf * norm - g0) * diffscale;
bf = (bf * norm - b0) * diffscale;
rn = rn * norm - r0;
gn = gn * norm - g0;
bn = bn * norm - b0;
buffer[cIdx] = rn - rf;
buffer[cIdx+1] = gn - gf;
buffer[cIdx+2] = bn - bf;
}
}

// Vertical pass
for(let y = 0; y < height; y++) {
const line = y * width * 4;
for (let x = 0; x < width; x++) {
const cIdx = x*4 + line;
const r0 = source[cIdx];
const g0 = source[cIdx + 1];
const b0 = source[cIdx + 2];
let rf = buffer[cIdx] * centerWeight;
let gf = buffer[cIdx + 1] * centerWeight;
let bf = buffer[cIdx + 2] * centerWeight;
let rn = rf;
let gn = gf;
let bn = bf;
let diffF = 0;
let diffN = 0;
for (let dy = 1; dy <= radius; dy++) {
const {minPick, minDiff, maxPick, maxDiff} = selectDipole(x, y, dy, 0, source, width, height);
diffF += maxDiff;
diffN += minDiff;
const weight = kernel[radius + dy];
rf += buffer[maxPick] * weight;
gf += buffer[maxPick + 1] * weight;
bf += buffer[maxPick + 2] * weight;
rn += buffer[minPick] * weight;
gn += buffer[minPick + 1] * weight;
bn += buffer[minPick + 2] * weight;
}
const diffscale = (diffN > 0 && diffF > 0) ? diffN / diffF : 1;
target[cIdx] = round(r0 + (rn - rf * diffscale) * norm);
target[cIdx+1] = round(g0 + (gn - gf * diffscale) * norm);
target[cIdx+2] = round(b0 + (bn - bf * diffscale) * norm);
target[cIdx+3] = 255;
}
}
return target;
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
tulipData = {
width; // trigger resize when going to fullscreen, or landscape on mobile
const ctx = DOM.context2d(tulip.width, tulip.height, 1);
ctx.drawImage(tulip, 0, 0, ctx.canvas.width, ctx.canvas.height);
const imageData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
return imageData;
}
Insert cell
kissData = {
width;
const ctx = DOM.context2d(kiss.width, kiss.height, 1);
ctx.drawImage(kiss, 0, 0, ctx.canvas.width, ctx.canvas.height);
const imageData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
return imageData;
}
Insert cell
function makeFilterParams(imageData, radius) {
return {
source: imageData.data,
width: imageData.width,
height: imageData.height,
radius,
};
}
Insert cell
// Assumes the filter will return a typed array
function makeFilterWorker(filter, params) {
const script = makeFilterWorkerScript(filter);
const context = DOM.context2d(params.width, params.height, 1);
const imageData = context.getImageData(0, 0, params.width, params.height);
// use original image to start with
imageData.data.set(params.source);
context.putImageData(imageData, 0, 0);
// emphasize that this is the unrendered image
context.font = '40px serif';
context.fillStyle = '#00000088';
context.fillRect(0, 0, params.width, params.height);
context.fillText('Rendering...', 24, 64);
context.fillStyle = 'white';
context.fillText('Rendering...', 20, 60);
context.strokeStyle = 'black';
context.strokeText('Rendering...', 20, 60);
const worker = new Worker(script);
const messaged = ({data: {rgba}}) => {
imageData.data.set(rgba);
context.putImageData(imageData, 0, 0);
};
invalidation.then(() => worker.terminate());
worker.onmessage = messaged;
worker.postMessage({params});
context.canvas.style = "image-rendering: crisp-edges; image-rendering: pixelated"
return context.canvas;
}
Insert cell
function identityFilter({source}){
return source;
}
Insert cell
// Assumes the filter will return a typed array
function makeFilterWorkerScript(filter) {
const filterScript = filter.toString();
const script = `
// Inline all the generic functions used by the filter
${colorDelta.toString()}
${filterScript.indexOf("selectNearest") !== -1 ? selectNearest.toString() : ""}
${filterScript.indexOf("selectFurthest") !== -1 ? selectFurthest.toString() : ""}
${filterScript.indexOf("selectDipole") !== -1 ? selectDipole.toString() : ""}
${filterScript.indexOf("bellcurvish2D") !== -1 ? bellcurvish2D.toString() : ""}
${filterScript.indexOf("bellcurvish1D") !== -1 ? bellcurvish1D.toString() : ""}
// Inline the filter
${filterScript}
onmessage = event => {
const {params} = event.data;
const rgba = ${filter.name}(params);
postMessage({rgba}, [rgba.buffer]);
close();
}
`;
const url = URL.createObjectURL(new Blob([script], {type: 'text/javascript'}))
invalidation.then(() => URL.revokeObjectURL(url));
return url;
}
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more