Skip to content

Instantly share code, notes, and snippets.

@mgd020
Created October 29, 2019 02:05
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mgd020/a6ab84904c365b31c35df5262dc57dfb to your computer and use it in GitHub Desktop.
Save mgd020/a6ab84904c365b31c35df5262dc57dfb to your computer and use it in GitHub Desktop.
var image_nes_filter = (function() {
var PIXEL_SIZE = 4;
dither = (function() {
// Floyd–Steinberg dither
var ERROR_DIFFUSION_1_16 = 1 / 16,
ERROR_DIFFUSION_3_16 = 3 / 16,
ERROR_DIFFUSION_5_16 = 5 / 16,
ERROR_DIFFUSION_7_16 = 7 / 16,
RED = 0,
GREEN = 1,
BLUE = 2,
ALPHA = 3,
CHANNELS = 4;
return function (imageData, clamp_color) {
var W_CHANNELS = imageData.width * CHANNELS,
data = imageData.data,
error = new Int8Array(data.length);
function clamp_error(color) {
var new_color = clamp_color(color),
error = [
color[RED] - new_color[RED],
color[GREEN] - new_color[GREEN],
color[BLUE] - new_color[BLUE],
color[ALPHA] - new_color[ALPHA]
];
return [new_color, error];
}
for (var i = 0; i < data.length; i += CHANNELS) {
var r = i + RED,
g = i + GREEN,
b = i + BLUE,
a = i + ALPHA,
rgba_color_error = clamp_error([
data[r] + error[r],
data[g] + error[g],
data[b] + error[b],
data[a] + error[a]
]),
i_right = i + CHANNELS,
i_down = i + W_CHANNELS,
i_down_left = i_down - CHANNELS,
i_down_right = i_down + CHANNELS,
rgba = rgba_color_error[0],
rgba_error = rgba_color_error[1],
red_error = rgba_error[RED],
green_error = rgba_error[GREEN],
blue_error = rgba_error[BLUE],
alpha_error = rgba_error[ALPHA];
// update color channels
data[r] = rgba[RED];
data[g] = rgba[GREEN];
data[b] = rgba[BLUE];
data[a] = rgba[ALPHA];
// distribute errors
if (i_down > data.length) {
// last row (no down)
i_down_right = i_down_left = i_down = null;
} else if (i % W_CHANNELS === 0) {
// first column (no left)
i_down_left = null;
} else if (i_down_right % W_CHANNELS === 0) {
// last column (no right)
i_down_right = i_right = null;
}
if (i_right) {
error[i_right + RED] += red_error * ERROR_DIFFUSION_7_16;
error[i_right + GREEN] += green_error * ERROR_DIFFUSION_7_16;
error[i_right + BLUE] += blue_error * ERROR_DIFFUSION_7_16;
error[i_right + ALPHA] += alpha_error * ERROR_DIFFUSION_7_16;
}
if (i_down_left) {
error[i_down_left + RED] += red_error * ERROR_DIFFUSION_3_16;
error[i_down_left + GREEN] += green_error * ERROR_DIFFUSION_3_16;
error[i_down_left + BLUE] += blue_error * ERROR_DIFFUSION_3_16;
error[i_down_left + ALPHA] += alpha_error * ERROR_DIFFUSION_3_16;
}
if (i_down) {
error[i_down + RED] += red_error * ERROR_DIFFUSION_5_16;
error[i_down + GREEN] += green_error * ERROR_DIFFUSION_5_16;
error[i_down + BLUE] += blue_error * ERROR_DIFFUSION_5_16;
error[i_down + ALPHA] += alpha_error * ERROR_DIFFUSION_5_16;
}
if (i_down_right) {
error[i_down_right + RED] += red_error * ERROR_DIFFUSION_1_16;
error[i_down_right + GREEN] += green_error * ERROR_DIFFUSION_1_16;
error[i_down_right + BLUE] += blue_error * ERROR_DIFFUSION_1_16;
error[i_down_right + ALPHA] += alpha_error * ERROR_DIFFUSION_1_16;
}
}
}
})();
var get_closest_nes_color = (function() {
// https://wiki.nesdev.com/w/index.php/PPU_palettes#2C02
var nes_colors = [
[0, 0, 0],
[0, 102, 120],
[0, 118, 40],
[0, 30, 116],
[0, 50, 60],
[0, 60, 0],
[0, 64, 0],
[116, 196, 0],
[120, 124, 236],
[120, 60, 0],
[136, 20, 176],
[152, 150, 152],
[152, 226, 180],
[152, 34, 32],
[160, 162, 160],
[160, 170, 0],
[160, 20, 100],
[160, 214, 228],
[168, 204, 236],
[168, 226, 144],
[176, 98, 236],
[180, 222, 120],
[188, 188, 236],
[204, 210, 120],
[212, 136, 32],
[212, 178, 236],
[228, 196, 144],
[228, 84, 236],
[236, 106, 100],
[236, 174, 212],
[236, 174, 236],
[236, 180, 176],
[236, 238, 236],
[236, 88, 180],
[32, 42, 0],
[40, 114, 0],
[48, 0, 136],
[48, 50, 236],
[56, 180, 204],
[56, 204, 108],
[60, 24, 0],
[60, 60, 60],
[68, 0, 100],
[76, 154, 236],
[76, 208, 32],
[8, 124, 0],
[8, 16, 144],
[8, 58, 0],
[8, 76, 196],
[84, 4, 0],
[84, 84, 84],
[84, 90, 0],
[92, 0, 48],
[92, 30, 228]
];
var cache = {};
function distance(p1, p2) {
// euclidean distance (squared)
// in order for this to be better, we should convert colors to CIE LAB
var p_0=p2[0] - p1[0], p_1=p2[1] - p1[1], p_2=p2[2] - p1[2];
return p_0*p_0 + p_1*p_1 + p_2*p_2;
}
function find(rgb) {
var min_distance = null;
var min_value = null;
for (var i = 0; i < nes_colors.length; ++i) {
var nes = nes_colors[i];
var d = distance(rgb, nes);
if (min_distance === null || d < min_distance) {
min_distance = d;
min_value = nes;
}
}
return min_value;
}
return function(rgb) {
var val = cache[rgb] || find(rgb);
cache[rgb] = val;
return val;
}
})();
function nes_color(color) {
var rgb = get_closest_nes_color([color[0], color[1], color[2]]);
return [rgb[0], rgb[1], rgb[2], (color[3] >> 6) * 85];
}
function clamp(color) {
return [
(color[0] >> 6) * 85,
(color[1] >> 6) * 85,
(color[2] >> 6) * 85,
(color[3] >> 6) * 85,
];
}
return function (img) {
var w = img.offsetWidth / PIXEL_SIZE | 0,
h = img.offsetHeight / PIXEL_SIZE | 0,
canvas = document.createElement('canvas');
// set canvas size
canvas.width = w * PIXEL_SIZE;
canvas.height = h * PIXEL_SIZE;
// get context
var context = canvas.getContext('2d');
// draw scaled down
context.drawImage(img, 0, 0, w, h);
// dither and clamp colors
var imageData = context.getImageData(0, 0, w, h);
dither(imageData, nes_color);
context.putImageData(imageData, 0, 0);
// draw scaled up (no antialiasing)
context.msImageSmoothingEnabled = false;
context.mozImageSmoothingEnabled = false;
context.webkitImageSmoothingEnabled = false;
context.imageSmoothingEnabled = false;
context.drawImage(canvas, 0, 0, w, h, 0, 0, canvas.width, canvas.height);
// update element
img.src = canvas.toDataURL("image/png");
};
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment