From c416c24a9ca2f05f4befabae48f5aea13515eda1 Mon Sep 17 00:00:00 2001 From: Alek Ratzloff Date: Mon, 3 Jun 2024 11:16:07 -0700 Subject: [PATCH] Add Javascript implementation Add a simple JS port of colorhash for easier demonstration purposes. You don't need to open a command line to create a colorhash, you can just use the webpage. Signed-off-by: Alek Ratzloff --- examples/commithash.svg | 80 ++++---- js/colorhash.js | 420 ++++++++++++++++++++++++++++++++++++++++ js/index.html | 105 ++++++++++ 3 files changed, 565 insertions(+), 40 deletions(-) create mode 100644 js/colorhash.js create mode 100644 js/index.html diff --git a/examples/commithash.svg b/examples/commithash.svg index d5150f5..4532a2f 100644 --- a/examples/commithash.svg +++ b/examples/commithash.svg @@ -1,42 +1,42 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/js/colorhash.js b/js/colorhash.js new file mode 100644 index 0000000..95dbdf4 --- /dev/null +++ b/js/colorhash.js @@ -0,0 +1,420 @@ +/** + * Simplified colorhash web frontend. + */ + +// TODO - need a way to share palettes between Python and JS + +//////////////////////////////////////////////////////////////////////////////// +// Colors +//////////////////////////////////////////////////////////////////////////////// + +class Color { + constructor() { + if(new.target === Color) { + throw new Error("Cannot instantiate abstract class Color"); + } + } + + toString() { return self.toHTMLColor(); } + + toHTMLColor() { throw new Error("Cannot call abstract method toHTMLColor"); } + + toRGB() { throw new Error("Cannot call abstract method toRGB"); } + + toHSL() { throw new Error("Cannot call abstract method toHSL"); } +} + +class RGBColor extends Color { + constructor(r, g, b) { + super(); + this.r = r; + this.g = g; + this.b = b; + } + + toHTMLColor() { + return `rgb(${this.r}, ${this.g}, ${this.b})`; + } + + toRGB() { + return this; + } + + toHSL() { + // NOTE - this isn't perfect, but it should be OK for our uses. + // We don't really use the toHSL at all anyway, this is really just here for completeness. + // example of floating point biting us in the ass: + // new HSLColor(120, 50, 25).toRGB().toHSL() + // { h: 120, s: 50, l: 25.098039215686274 } + let r = this.r / 255; + let g = this.g / 255; + let b = this.b / 255; + + let max = Math.max(r, g, b); + let min = Math.min(r, g, b); + + let h, s; +let l = (max + min) / 2; + + if (max === min) { + h = s = 0; // achromatic + } else { + let d = max - min; + s = l > 0.5 ? d / (2 - max - min) : d / (max + min); + switch (max) { + // not sure if braces are necessary here, this is mostly just a + // habit from C/C++ where the `switch` scope is shared + case r: { + h = (g - b) / d + (g < b ? 6 : 0); + }; break; + case g: { + h = (b - r) / d + 2; + }; break; + case b: { + h = (r - g) / d + 4; + }; break; + } + h /= 6; + } + + return new HSLColor(h * 360, s * 100, l * 100); + } +} + +class HSLColor extends Color { + constructor(h, s, l) { + super(); + this.h = h; + this.s = s; + this.l = l; + } + + toHTMLColor() { + return `hsl(${this.h}, ${this.s}%, ${this.l}%)`; + } + + toRGB() { + // nice little hack we can use to convert to RGB + const div = document.createElement("div"); + div.style.backgroundColor = this.toHTMLColor(); + const [_, r, g, b] = div + .style + .backgroundColor + .match(/rgb\((\d+), (\d+), (\d+)\)/) + .map(Number); + return new RGBColor(r, g, b) + } + + toHSL() { + return this; + }; +} + +//////////////////////////////////////////////////////////////////////////////// +// Utilities +//////////////////////////////////////////////////////////////////////////////// + +// hahahahahahahaha javascript doesn't even have a zip() function +// HAHAHAHAHAHAHAHAHAHAHAHAHAHAHAHAHA "JUST INSTALL A DEPENDENCY" +// HAHAHAHAHAHAHAHAHAHAHAHAHAHAHAHAHAHAHAHAHAHAHAHAHAHAHAHAHAHAHAHAHAHAHAHAHA +const zip = (a, b) => a.map((k, i) => [k, b[i]]); + +function quantize(min, max, steps) { + const dist = max - min; + return Array.from( + {length: steps}, + (_, i) => min + (i * dist / (steps - 1)) + ); +} + +/** + * Converts a hash string to Uint8Array. + */ +function hash2array(hash) { + return Array.from( + new Uint8Array( + hash.match(/../g) + .map(e => parseInt(e, 16)) + ) + ) +} + +/** + * Attempts to detect the type of hash or algorithm that has been supplied. + * Examples: + * + * 'sha1' => 'sha1' + * 'ae288b06df4a460c37c836e270f9edaabffb7d6d' => 'sha1' + * 'e575e0af2fbe9d8cc3a2c13542aa1375d2f75875e55529d0889f9b46' => 'sha224' + * 'md5' => 'md5' + * 'crc32' => error, not supported + * + * etc. + */ +function detectAlgorithm(hashOrAlgo) { + const dimensions = { + md5: 32, + 32: "md5", + sha1: 40, + 40: "sha1", + sha224: 56, + 56: "sha224", + sha256: 64, + 64: "sha256", + sha384: 96, + 96: "sha384", + sha512: 128, + 128: "sha512", + }; + + hashOrAlgo = hashOrAlgo.toLowerCase(); + + if(hashOrAlgo.match(/^([0-9a-fA-F]{2})+$/)) { + if(dimensions.hasOwnProperty(hashOrAlgo.length)) { + return dimensions[hashOrAlgo.length]; + } else { + return null; + } + } else if(dimensions.hasOwnProperty(hashOrAlgo)) { + return dimensions[hashOrAlgo]; + } else { + return null; + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Palettes +//////////////////////////////////////////////////////////////////////////////// + +/** + * Create a list of HSL + */ +function hslPalette(start, end) { + if(!(start instanceof HSLColor)) { + throw new TypeError("expected param 'start' to be instance of HSLColor"); + } + if(!(end instanceof HSLColor)) { + throw new TypeError("expected param 'end' to be instance of HSLColor"); + } + const hues = quantize(start.h, end.h, 16); + const sats = quantize(start.s, end.s, 16); + const lights = quantize(start.l, end.l, 16); + + return zip(zip(hues, sats), lights).map( + ([[h, s], l]) => new HSLColor(h, s, l) + ); +} + +const GRADIENT_PALETTES = [ + ["red-light", hslPalette(new HSLColor(0, 100, 50), new HSLColor(0, 100, 100))], + ["red-dark", hslPalette(new HSLColor(0, 100, 0), new HSLColor(0, 100, 50))], + // + ["orange-light", hslPalette(new HSLColor(30, 100, 50), new HSLColor(30, 100, 100))], + ["orange-dark", hslPalette(new HSLColor(30, 100, 0), new HSLColor(30, 100, 50))], + // + //["yellow-light", hslPalette(new HSLColor(60, 100, 50), new HSLColor(60, 100, 100))], + ["yellow-dark", hslPalette(new HSLColor(60, 100, 0), new HSLColor(60, 100, 50))], + // + //["lime-light", hslPalette(new HSLColor(90, 100, 50), new HSLColor(90, 100, 100))], + //["lime-dark", hslPalette(new HSLColor(90, 100, 0), new HSLColor(90, 100, 50))], + // + ["green-light", hslPalette(new HSLColor(120, 100, 50), new HSLColor(120, 100, 100))], + ["green-dark", hslPalette(new HSLColor(120, 100, 0), new HSLColor(120, 100, 50))], + // + //["seafoam-light", hslPalette(new HSLColor(150, 100, 50), new HSLColor(150, 100, 100))], + //["seafoam-dark", hslPalette(new HSLColor(150, 100, 0), new HSLColor(150, 100, 50))], + // + ["cyan-light", hslPalette(new HSLColor(180, 100, 50), new HSLColor(180, 100, 100))], + ["cyan-dark", hslPalette(new HSLColor(180, 100, 0), new HSLColor(180, 100, 50))], + // + //["teal-light", hslPalette(new HSLColor(210, 100, 50), new HSLColor(210, 100, 100))], + //["teal-dark", hslPalette(new HSLColor(210, 100, 0), new HSLColor(210, 100, 50))], + // + ["blue-light", hslPalette(new HSLColor(240, 100, 50), new HSLColor(240, 100, 100))], + ["blue-dark", hslPalette(new HSLColor(240, 100, 0), new HSLColor(240, 100, 50))], + // + ["purple-light", hslPalette(new HSLColor(270, 100, 50), new HSLColor(270, 100, 100))], + ["purple-dark", hslPalette(new HSLColor(270, 100, 0), new HSLColor(270, 100, 50))], + // + ["magenta-light", hslPalette(new HSLColor(300, 100, 50), new HSLColor(300, 100, 100))], + ["magenta-dark", hslPalette(new HSLColor(300, 100, 0), new HSLColor(300, 100, 50))], + // + ["pink-light", hslPalette(new HSLColor(330, 100, 50), new HSLColor(330, 100, 100))], + ["pink-dark", hslPalette(new HSLColor(330, 100, 0), new HSLColor(330, 100, 50))], + // + ["gray-light", hslPalette(new HSLColor(0, 0, 50), new HSLColor(0, 0, 100))], + ["gray-dark", hslPalette(new HSLColor(0, 0, 0), new HSLColor(0, 0, 50))], +]; + +const MULTICOLOR_PALETTES = [ + ["rainbow", hslPalette(new HSLColor(0, 100, 50), new HSLColor(360, 0, 50))], + ["rainbow-reverse", hslPalette(new HSLColor(360, 100, 50), new HSLColor(0, 0, 50))], +]; + +DEFAULT_PALETTES = [].concat( + GRADIENT_PALETTES, + MULTICOLOR_PALETTES, +); + +//////////////////////////////////////////////////////////////////////////////// +// Matricizer +//////////////////////////////////////////////////////////////////////////////// + +class Matricizer { + constructor() { + if(new.target === Matricizer) { + throw new Error("Cannot instantiate abstract class Matricizer"); + } + } + + matricize(hash) { + throw new Error("Cannot call abstract method matricize"); + } + + choosePalette(hash, palettes = null) { + if(!palettes) { + palettes = DEFAULT_PALETTES; + } + const total = hash2array(hash).reduce((acc, curr) => acc + curr, 0); + + return palettes[total % palettes.length][1] + } + + static chooseDimensions(hashOrAlgo) { + throw new Error("Cannot call abstract method chooseDimensions"); + } +} + +class NibbleMatricizer extends Matricizer { + constructor() { + super(); + } + + matricize(hash) { + const dimensions = { + md5: [8, 4], + sha1: [8, 5], + sha224: [8, 7], + sha256: [8, 8], + sha384: [12, 8], + sha512: [16, 8], + }; + + const hashAlgo = detectAlgorithm(hash); + if(hashAlgo === null) { + throw new Error(`unable to determine hash algorithm`); + } + + const [w, h] = dimensions[hashAlgo]; + + const nibbles = hash2array(hash) + .map(b => [(b & 0xf0) >> 4, b & 0x0f]) + .flat(); + + if(nibbles.length != w * h) { + throw new Error(`invalid hash length: ${hash.length} (${nibbles.length} nibbles)`); + } + + const rows = [] + let cols = [] + for(const b of nibbles) { + cols.push(b); + if(cols.length === w) { + rows.push(cols); + cols = [] + } + } + return rows; + } + + choosePalette(data, palettes = null) { + return super.choosePalette(data, palettes || GRADIENT_PALETTES); + } +} + +class RandomartMatricizer extends Matricizer { + constructor() { + super(); + } + + matricize(hash) { + const dimensions = { + md5: [7, 6], + sha1: [7, 6], + sha224: [8, 7], + sha256: [8, 7], + sha384: [11, 10], + sha512: [11, 10], + } + + const hashAlgo = detectAlgorithm(hash); + if(hashAlgo === null) { + throw new Error(`unable to determine hash algorithm`); + } + + const [w, h] = dimensions[hashAlgo]; + const bytes = hash2array(hash); + + // create an empty 2d array + const rows = Array.from( + { length: h }, + () => Array.from({ length: w }, () => 0) + ); + + let c = Math.floor(w / 2); + let r = Math.floor(h / 2); + + // do the randomart algorithm + for(let value of bytes) { + for(let i = 0; i < 4; i++) { + if(value & 1) { + c += 1; + } else { + c -= 1; + } + if(value & 2) { + r += 1; + } else { + r -= 1; + } + c = Math.min(Math.max(c, 0), w - 1); + r = Math.min(Math.max(r, 0), h - 1); + if(rows[r][c] < 0xf) { + rows[r][c] += 1; + } + value >>= 2; + } + } + + return rows; + } + + choosePalette(data, palettes = null) { + return super.choosePalette(data, palettes || MULTICOLOR_PALETTES); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// SVG output +//////////////////////////////////////////////////////////////////////////////// + +function genSVG(matrix, palette, squareSize = 32) { + const colors = matrix.map( + row => row.map(v => palette[v]) + ); + + const w = colors[0].length; + const h = colors.length; + let svg = `\n`; + + for(let r = 0; r < h; r++) { + for(let c = 0; c < w; c++) { + const x = c * squareSize; + const y = r * squareSize; + const color = colors[r][c]; + svg += `\n`; + } + } + svg += ""; + return svg; +} diff --git a/js/index.html b/js/index.html new file mode 100644 index 0000000..92458ea --- /dev/null +++ b/js/index.html @@ -0,0 +1,105 @@ + + + + Colorhash + + + +
+

+ Create a colorhash image by typing in a hash here +

+
+
+

+ + + + + + + + + +
+
+
+
+ + + + + + +
+

+
+
+
+

+

+
+

+ + + + +