Public
Edited
Nov 2
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

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more