Skip to content

Instantly share code, notes, and snippets.

@mattdesl
Last active December 14, 2023 21:49
Show Gist options
  • Save mattdesl/58b806478f4d33e8b91ed9c51c39014d to your computer and use it in GitHub Desktop.
Save mattdesl/58b806478f4d33e8b91ed9c51c39014d to your computer and use it in GitHub Desktop.
palette compression

code-golfing color palettes in JavaScript

If you need to code-golf a set of predefined RGB color palettes, how would you do it?

Problem: Each palette has a variable amount of RGB colors. The program output should closely match the input palettes, in string hex code format (with or without # prefix) so that it can be fed into Canvas2D APIs. Solution must be valid JavaScript code.

Proposed Solution: Turn each palette into a base64 encoded string. During decoding, use atob to convert the Base64 palette to a set of hex codes.

Other Solutions? Maybe with RGB565 or RGB444? Please comment if you have any other solutions. :)

// Polyfill so we can run this in Node.js as well
if (typeof atob !== 'function') {
var atob = a => Buffer.from(a, 'base64').toString('binary')
var btoa = b => Buffer.from(b).toString('base64');
}
// 511 bytes after minify
var a=[
["#1b6f3f", "#10c5b4", "#ade4cd", "#29ec19"],
["#96bde8", "#246a85", "#3483e4", "#b168f6"],
["#b623b1", "#6e18f9", "#49cbc1", "#5ad67d"],
["#73267b", "#c29c5b", "#6086de"],
["#e79f9b", "#772140", "#cb8c96"],
["#a45cd7", "#3d0709", "#9778c4", "#5893b6", "#40a399"],
["#6df6d1", "#5ea534", "#88bc86"],
["#6f4895", "#d5a6fc", "#3951c3", "#4e2816", "#7fd50c"],
["#58498e", "#596a5c", "#9d53ee", "#c30935", "#3a0480"],
["#67aef5", "#2eae76", "#6e2376", "#e05d7d"],
["#ca15c8", "#a98acd", "#0293c3", "#856e0d"],
["#b65cd2", "#fc2bba", "#59a669", "#2f2c99"],
];
// turn into base64 encoded strings
var txt = a.map(palette => {
const uint8 = new Uint8Array(palette.map(n => hexToRGB(n)).flat())
return btoa(uint8)
}).join(`:`)
// decompress without # prefix (313 bytes)
var k=`G28/EMW0reTNKewZ:lr3oJGqFNIPksWj2:tiOxbhj5ScvBWtZ9:cyZ7wpxbYIbe:55+bdyFAy4yW:pFzXPQcJl3jEWJO2QKOZ:bfbRXqU0iLyG:b0iV1ab8OVHDTigWf9UM:WEmOWWpcnVPuwwk1OgSA:Z671Lq52biN24F19:yhXIqYrNApPDhW4N:tlzS/Cu6WaZpLyyZ`.split`:`.map(b=>[...atob(b)].map(c=>c.charCodeAt().toString(16).padStart(2,0)).join``.match(/.{6}/g))
console.log(k);
function hexToRGB(str) {
var hex = str.replace(/^#/, "");
if (hex.length === 3) hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
var num = parseInt(hex, 16);
var red = num >> 16;
var green = (num >> 8) & 255;
var blue = num & 255;
return [red, green, blue];
}
// 511 bytes after minify
var a=[
["#1b6f3f", "#10c5b4", "#ade4cd", "#29ec19"],
["#96bde8", "#246a85", "#3483e4", "#b168f6"],
["#b623b1", "#6e18f9", "#49cbc1", "#5ad67d"],
["#73267b", "#c29c5b", "#6086de"],
["#e79f9b", "#772140", "#cb8c96"],
["#a45cd7", "#3d0709", "#9778c4", "#5893b6", "#40a399"],
["#6df6d1", "#5ea534", "#88bc86"],
["#6f4895", "#d5a6fc", "#3951c3", "#4e2816", "#7fd50c"],
["#58498e", "#596a5c", "#9d53ee", "#c30935", "#3a0480"],
["#67aef5", "#2eae76", "#6e2376", "#e05d7d"],
["#ca15c8", "#a98acd", "#0293c3", "#856e0d"],
["#b65cd2", "#fc2bba", "#59a669", "#2f2c99"],
];
// compress
const txt = a.map((n) => n.join("").replace(/\#/g, "")).join("Z");
console.log(txt);
// decompress without # symbol (342 bytes)
var p="1b6f3f10c5b4ade4cd29ec19Z96bde8246a853483e4b168f6Zb623b16e18f949cbc15ad67dZ73267bc29c5b6086deZe79f9b772140cb8c96Za45cd73d07099778c45893b640a399Z6df6d15ea53488bc86Z6f4895d5a6fc3951c34e28167fd50cZ58498e596a5c9d53eec309353a0480Z67aef52eae766e2376e05d7dZca15c8a98acd0293c3856e0dZb65cd2fc2bba59a6692f2c99".split`Z`.map(c=>c.match(/.{6}/g));
// decompress with # prefix (356 bytes)
var h="1b6f3f10c5b4ade4cd29ec19Z96bde8246a853483e4b168f6Zb623b16e18f949cbc15ad67dZ73267bc29c5b6086deZe79f9b772140cb8c96Za45cd73d07099778c45893b640a399Z6df6d15ea53488bc86Z6f4895d5a6fc3951c34e28167fd50cZ58498e596a5c9d53eec309353a0480Z67aef52eae766e2376e05d7dZca15c8a98acd0293c3856e0dZb65cd2fc2bba59a6692f2c99".split`Z`.map(c=>c.match(/.{6}/g).map(s=>'#'+s));
@p01
Copy link

p01 commented Apr 11, 2021

As you are fine with RGB444 values, I thought about encoding the array of #RGB888 colors down to #RGB444 and into a string of UTF8 characters to use one character per color which would take 1-3 byte per color.

This brings the data + decoder for the list of colors you provided down to 185bytes

Hope this helps,

// an array of 48 #RGB888 colors
const RGB888Colors = [
  "#1b6f3f","#10c5b4","#ade4cd","#29ec19","#96bde8","#246a85",
  "#3483e4","#b168f6","#b623b1","#6e18f9","#49cbc1","#5ad67d",
  "#73267b","#c29c5b","#6086de","#e79f9b","#772140","#cb8c96",
  "#a45cd7","#3d0709","#9778c4","#5893b6","#40a399","#6df6d1",
  "#5ea534","#88bc86","#6f4895","#d5a6fc","#3951c3","#4e2816",
  "#7fd50c","#58498e","#596a5c","#9d53ee","#c30935","#3a0480",
  "#67aef5","#2eae76","#6e2376","#e05d7d","#ca15c8","#a98acd",
  "#0293c3","#856e0d","#b65cd2","#fc2bba","#59a669","#2f2c99"];

// Encode an array of #RGB888 colors down to RGB444 and into a string of UTF8 characters to use one character per color
String.fromCharCode(...RGB888Colors.map(n=>(parseInt(n[1]+n[3]+n[5],16))))

// This encodes our array of RGB888Colors to the following string which take 111 bytes
// "ţNj૬ˡাɨΎ୯ଫ؟ӌחܧಕڍນܤಉ੝̀ॼ֛ҩ۽֣ࢸىද͜СߐՈեफ़ః̈گʧا๗జઌ�ࡠଢ଼༫֦ȩ"

// The encoded string and decoder take 185 bytes 
"ţNj૬ˡাɨΎ୯ଫ؟ӌחܧಕڍນܤಉ੝̀ॼ֛ҩ۽֣ࢸىද͜СߐՈեफ़ః̈گʧا๗జઌ�ࡠଢ଼༫֦ȩ".split("").map(k=>'#'+`00${k.charCodeAt(0).toString(16)}`.slice(-3))

// Which returns an array of #RGB444 values
//  ["#163", "#1cb", "#aec", "#2e1", "#9be", "#268", "#38e", "#b6f", "#b2b", "#61f", "#4cc", "#5d7", "#727", "#c95", "#68d", "#e99", "#724", "#c89", "#a5d", "#300", "#97c", "#59b", "#4a9", "#6fd", "#5a3", "#8b8", "#649", "#daf", "#35c", "#421", "#7d0", "#548", "#565", "#95e", "#c03", "#308", "#6af", "#2a7", "#627", "#e57", "#c1c", "#a8c", "#09c", "#860", "#b5d", "#f2b", "#5a6", "#229"]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment