|
const getPixels = require("get-pixels"); |
|
const pool = require("ndarray-scratch"); |
|
const fill = require("ndarray-fill"); |
|
const zeros = require("zeros"); |
|
|
|
const RISOCOLORS = [ |
|
{ name: "BLACK", color: [0, 0, 0] }, |
|
{ name: "BURGUNDY", color: [145, 78, 114] }, |
|
{ name: "BLUE", color: [0, 120, 191] }, |
|
{ name: "GREEN", color: [0, 169, 92] }, |
|
{ name: "MEDIUMBLUE", color: [50, 85, 164] }, |
|
{ name: "BRIGHTRED", color: [241, 80, 96] }, |
|
{ name: "RISOFEDERALBLUE", color: [61, 85, 136] }, |
|
{ name: "PURPLE", color: [118, 91, 167] }, |
|
{ name: "TEAL", color: [0, 131, 138] }, |
|
{ name: "FLATGOLD", color: [187, 139, 65] }, |
|
{ name: "HUNTERGREEN", color: [64, 112, 96] }, |
|
{ name: "RED", color: [255, 102, 94] }, |
|
{ name: "BROWN", color: [146, 95, 82] }, |
|
{ name: "YELLOW", color: [255, 232, 0] }, |
|
{ name: "MARINERED", color: [210, 81, 94] }, |
|
{ name: "ORANGE", color: [255, 108, 47] }, |
|
{ name: "FLUORESCENTPINK", color: [255, 72, 176] }, |
|
{ name: "LIGHTGRAY", color: [136, 137, 138] }, |
|
{ name: "METALLICGOLD", color: [172, 147, 110] }, |
|
{ name: "CRIMSON", color: [228, 93, 80] }, |
|
{ name: "FLUORESCENTORANGE", color: [255, 116, 119] }, |
|
{ name: "CORNFLOWER", color: [98, 168, 229] }, |
|
{ name: "SKYBLUE", color: [73, 130, 207] }, |
|
{ name: "SEABLUE", color: [0, 116, 162] }, |
|
{ name: "LAKE", color: [35, 91, 168] }, |
|
{ name: "INDIGO", color: [72, 77, 122] }, |
|
{ name: "MIDNIGHT", color: [67, 80, 96] }, |
|
{ name: "MIST", color: [213, 228, 192] }, |
|
{ name: "GRANITE", color: [165, 170, 168] }, |
|
{ name: "CHARCOAL", color: [112, 116, 124] }, |
|
{ name: "SMOKYTEAL", color: [95, 130, 137] }, |
|
{ name: "STEEL", color: [55, 94, 119] }, |
|
{ name: "SLATE", color: [94, 105, 94] }, |
|
{ name: "TURQUOISE", color: [0, 170, 147] }, |
|
{ name: "EMERALD", color: [25, 151, 93] }, |
|
{ name: "GRASS", color: [57, 126, 88] }, |
|
{ name: "FOREST", color: [81, 110, 90] }, |
|
{ name: "SPRUCE", color: [74, 99, 93] }, |
|
{ name: "MOSS", color: [104, 114, 77] }, |
|
{ name: "SEAFOAM", color: [98, 194, 177] }, |
|
{ name: "KELLYGREEN", color: [103, 179, 70] }, |
|
{ name: "LIGHTTEAL", color: [0, 157, 165] }, |
|
{ name: "IVY", color: [22, 155, 98] }, |
|
{ name: "PINE", color: [35, 126, 116] }, |
|
{ name: "LAGOON", color: [47, 97, 101] }, |
|
{ name: "VIOLET", color: [157, 122, 210] }, |
|
{ name: "ORCHID", color: [170, 96, 191] }, |
|
{ name: "PLUM", color: [132, 89, 145] }, |
|
{ name: "RAISIN", color: [119, 93, 122] }, |
|
{ name: "GRAPE", color: [108, 93, 128] }, |
|
{ name: "SCARLET", color: [246, 80, 88] }, |
|
{ name: "TOMATO", color: [210, 81, 94] }, |
|
{ name: "CRANBERRY", color: [209, 81, 122] }, |
|
{ name: "MAROON", color: [158, 76, 110] }, |
|
{ name: "RASPBERRYRED", color: [209, 81, 122] }, |
|
{ name: "BRICK", color: [167, 81, 84] }, |
|
{ name: "LIGHTLIME", color: [227, 237, 85] }, |
|
{ name: "SUNFLOWER", color: [255, 181, 17] }, |
|
{ name: "MELON", color: [255, 174, 59] }, |
|
{ name: "APRICOT", color: [246, 160, 77] }, |
|
{ name: "PAPRIKA", color: [238, 127, 75] }, |
|
{ name: "PUMPKIN", color: [255, 111, 76] }, |
|
{ name: "BRIGHTOLIVEGREEN", color: [180, 159, 41] }, |
|
{ name: "BRIGHTGOLD", color: [186, 128, 50] }, |
|
{ name: "COPPER", color: [189, 100, 57] }, |
|
{ name: "MAHOGANY", color: [142, 89, 90] }, |
|
{ name: "BISQUE", color: [242, 205, 207] }, |
|
{ name: "BUBBLEGUM", color: [249, 132, 202] }, |
|
{ name: "LIGHTMAUVE", color: [230, 181, 201] }, |
|
{ name: "DARKMAUVE", color: [189, 140, 166] }, |
|
{ name: "WINE", color: [145, 78, 114] }, |
|
{ name: "GRAY", color: [146, 141, 136] }, |
|
{ name: "CORAL", color: [255, 142, 145] }, |
|
{ name: "WHITE", color: [255, 255, 255] }, |
|
{ name: "AQUA", color: [94, 200, 229] }, |
|
{ name: "MINT", color: [130, 216, 213] }, |
|
{ name: "CLEARMEDIUM", color: [242, 242, 242] }, |
|
{ name: "FLUORESCENTYELLOW", color: [255, 233, 22] }, |
|
{ name: "FLUORESCENTRED", color: [255, 76, 101] }, |
|
{ name: "FLUORESCENTGREEN", color: [68, 214, 44] } |
|
]; |
|
|
|
function normalize(min, max, v) { |
|
return (v - min) / (max - min); |
|
} |
|
|
|
function random2(u, l) { |
|
return l + (u - l) * Math.random(); |
|
} |
|
|
|
function random2i(u, l) { |
|
return random2(u, l) | 0; |
|
} |
|
|
|
function bin(v) { |
|
if (v <= 0.25) { |
|
return 0.25; |
|
} else if (v <= 0.5) { |
|
return 0.5; |
|
} else if (v <= 0.75) { |
|
return 0.75; |
|
} else if (v <= 1.0) { |
|
return 1.0; |
|
} |
|
} |
|
|
|
function getChannel(pixels, channel) { |
|
let singleChannel; |
|
switch (channel) { |
|
case "R": |
|
singleChannel = pixels.pick(null, null, 0); |
|
break; |
|
case "G": |
|
singleChannel = pixels.pick(null, null, 1); |
|
break; |
|
case "B": |
|
singleChannel = pixels.pick(null, null, 2); |
|
break; |
|
case "A": |
|
singleChannel = pixels.pick(null, null, 3); |
|
break; |
|
} |
|
return singleChannel; |
|
} |
|
|
|
function atkinsonDither(pixels, w, h) { |
|
console.log("ATKINSON"); |
|
for (let i = 0; i < w; i++) { |
|
for (let j = 0; j < h; j++) { |
|
let currentPixel = pixels.get(i, j); |
|
let newPixel = currentPixel < 129 ? 0 : 255; |
|
let err = Math.floor((currentPixel - newPixel) / 8); |
|
|
|
pixels.set(i, j, newPixel); |
|
pixels.set(i + 1, j, pixels.get(i + 1, j) + err); |
|
pixels.set(i + 2, j, pixels.get(i + 2, j) + err); |
|
pixels.set(i - 1, j + 1, pixels.get(i - 1, j + 1) + err); |
|
pixels.set(i, j + 1, pixels.get(i, j + 1) + err); |
|
pixels.set(i + 1, j + 1, pixels.get(i + 1, j + 1) + err); |
|
pixels.set(i, j + 2, pixels.get(i, j + 2) + err); |
|
} |
|
} |
|
return pixels; |
|
} |
|
|
|
function floydSteinbergDither(pixels, w, h) { |
|
console.log("FLOYD"); |
|
for (let i = 0; i < w; i++) { |
|
for (let j = 0; j < h; j++) { |
|
let currentPixel = pixels.get(i, j); |
|
let newPixel = currentPixel < 129 ? 0 : 255; |
|
let err = Math.floor((currentPixel - newPixel) / 16); |
|
|
|
pixels.set(i, j, newPixel); |
|
pixels.set(i + 1, j, pixels.get(i + 1, j) + 7 * err); |
|
pixels.set(i - 1, j + 1, pixels.get(i - 1, j + 1) + 3 * err); |
|
pixels.set(i, j + 1, pixels.get(i, j + 1) + 5 * err); |
|
pixels.set(i + 1, j + 1, pixels.get(i + 1, j + 1) + err); |
|
} |
|
} |
|
return pixels; |
|
} |
|
|
|
function basicDither(pixels, w, h) { |
|
console.log("BASIC"); |
|
for (let i = 0; i < w; i++) { |
|
for (let j = 0; j < h; j++) { |
|
let currentPixel = pixels.get(i, j); |
|
let newPixel = currentPixel < 129 ? 0 : 255; |
|
|
|
pixels.set(i, j, newPixel); |
|
} |
|
} |
|
return pixels; |
|
} |
|
|
|
function bayerDither(pixels, w, h) { |
|
console.log("BAYER"); |
|
const BAYER_4X4_MAP = [ |
|
[15, 135, 45, 165], |
|
[195, 75, 225, 105], |
|
[60, 180, 30, 150], |
|
[240, 120, 210, 90] |
|
]; |
|
|
|
for (let i = 0; i < w; i++) { |
|
for (let j = 0; j < h; j++) { |
|
let currentPixel = pixels.get(i, j); |
|
let mod = Math.floor((currentPixel + BAYER_4X4_MAP[i % 4][j % 4]) / 2); |
|
let newPixel = mod < 129 ? 0 : 255; |
|
|
|
pixels.set(i, j, newPixel); |
|
} |
|
} |
|
return pixels; |
|
} |
|
|
|
function dither(pixels, type) { |
|
let [w, h] = pixels.shape; |
|
switch (type) { |
|
case "FLOYD": |
|
return floydSteinbergDither(pixels, w, h); |
|
case "ATKINSON": |
|
return atkinsonDither(pixels, w, h); |
|
case "BAYER": |
|
return bayerDither(pixels, w, h); |
|
default: |
|
return basicDither(pixels, w, h); |
|
} |
|
} |
|
|
|
function color(base, alpha, invert) { |
|
let [r, g, b] = RISOCOLORS.find(({ name }) => name === base).color; |
|
|
|
let v = invert ? 1 - normalize(0, 255, alpha) : normalize(0, 255, alpha); |
|
return `rgba(${r}, ${g}, ${b}, ${bin(v)})`; |
|
} |
|
|
|
function drawLayer( |
|
ctx, |
|
w, |
|
h, |
|
layerPixels, |
|
c, |
|
invert = false, |
|
[xshift, yshift] = [0, 0] |
|
) { |
|
let cc = c.replace(/\s/g, "").toUpperCase(); |
|
ctx.save(); |
|
ctx.translate(xshift * w, yshift * h); |
|
for (let i = 0; i < w; i++) { |
|
for (let j = 0; j < h; j++) { |
|
ctx.fillStyle = color(cc, layerPixels.get(i, j), invert); |
|
ctx.fillRect(i, j, 1, 1); |
|
} |
|
} |
|
ctx.restore(); |
|
} |
|
|
|
function drawText(ctx, w, h, text) { |
|
let original = ctx.globalCompositeOperation; |
|
ctx.globalCompositeOperation = "destination-out"; |
|
ctx.fillStyle = "black"; |
|
ctx.font = `bold ${0.125 * w}px arial`; |
|
ctx.fillText(text, 0.065 * w, 0.95 * h); |
|
ctx.globalCompositeOperation = original; |
|
} |
|
|
|
function drawImage(ctx, w, h, quadrant = [0, 0], layers = []) { |
|
ctx.save(); |
|
ctx.translate(quadrant[0] * w, quadrant[1] * h); |
|
layers.forEach(l => l()); |
|
ctx.restore(); |
|
} |
|
|
|
function run(img) { |
|
let w = img.shape[0]; |
|
let h = img.shape[1]; |
|
let atkinson = dither(getChannel(pool.clone(img), "R"), "ATKINSON"); |
|
let floyd = dither(getChannel(pool.clone(img), "R"), "FLOYD"); |
|
let bayer = dither(getChannel(pool.clone(img), "R"), "BAYER"); |
|
let basic = dither(getChannel(pool.clone(img), "R"), "BASIC"); |
|
let flat = zeros([img.shape[0], img.shape[1]], img.dtype); |
|
fill(flat, () => 255); |
|
|
|
let canvas = document.createElement("canvas"); |
|
canvas.setAttribute("width", w * 2 * 2); |
|
canvas.setAttribute("height", h * 3 * 2); |
|
let ctx = canvas.getContext("2d"); |
|
document.body.appendChild(canvas); |
|
|
|
let draw = drawImage.bind(null, ctx, w, h); |
|
let layer = drawLayer.bind(null, ctx, w, h); |
|
let text = drawText.bind(null, ctx, w, h); |
|
|
|
ctx.scale(2, 2); |
|
ctx.globalCompositeOperation = "overlay"; |
|
|
|
draw( |
|
[0, 0], |
|
[ |
|
() => layer(bayer, "BLUE", true), |
|
() => text("GRATULERER"), |
|
() => |
|
layer(atkinson, "RED", true, [ |
|
random2(-0.01, 0.01), |
|
random2(-0.01, 0.01) |
|
]) |
|
] |
|
); |
|
|
|
draw( |
|
[1, 0], |
|
[ |
|
() => layer(bayer, "FLUORESCENTGREEN", true), |
|
() => text("GRATULERER"), |
|
() => |
|
layer(atkinson, "BURGUNDY", false, [ |
|
random2(-0.01, 0.01), |
|
random2(-0.01, 0.01) |
|
]) |
|
] |
|
); |
|
|
|
ctx.globalCompositeOperation = "screen"; |
|
draw( |
|
[0, 1], |
|
[ |
|
() => layer(flat, "BLACK", false), |
|
() => layer(atkinson, "FLUORESCENTRED", true), |
|
() => text("GRATULERER"), |
|
() => layer(basic, "FLUORESCENTRED", true) |
|
] |
|
); |
|
|
|
ctx.globalCompositeOperation = "overlay"; |
|
draw( |
|
[1, 1], |
|
[ |
|
() => layer(basic, "Wine", true, [0.025, 0.025]), |
|
() => text("GRATULERER"), |
|
() => layer(bayer, "Bright Olive Green", true) |
|
] |
|
); |
|
|
|
draw( |
|
[1, 2], |
|
[ |
|
() => layer(flat, "mint", false), |
|
() => text("GRATULERER"), |
|
() => layer(basic, "Fluorescent Yellow", true) |
|
] |
|
); |
|
|
|
draw( |
|
[0, 2], |
|
[ |
|
() => layer(bayer, "Slate", true), |
|
() => text("GRATULERER"), |
|
() => layer(atkinson, "Orchid", true, [-0.025, -0.025]) |
|
] |
|
); |
|
} |
|
|
|
getPixels("src-compressed.jpg", function(err, img) { |
|
run(img); |
|
}); |