Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
compress hex color string

compress hex color strings

In hex you use three or six digits to define a color. By replacing all six-digit colors with the closest color which can be expressed with three digits, we save 3 bytes each! Hooray!

http://www.w3.org/TR/CSS2/syndata.html#value-def-color

The three-digit RGB notation (#rgb) is converted into six-digit form (#rrggbb) by replicating digits, not by adding zeros.

Examples:

'00ff00' -> '0f0'

'34cf9d' -> '3c9'

function a(b, c) {
return ++c ?
(("0x" + b) / 17 + .5 | 0).toString(16) :
b.replace(/../g, a)
}
function a(b,c){return++c?(("0x"+b)/17+.5|0).toString(16):b.replace(/../g,a)}
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
Version 2, December 2004
Copyright (C) 2012 Alexander Prinzhorn (@Prinzhorn) https://github.com/Prinzhorn
Everyone is permitted to copy and distribute verbatim or modified
copies of this license document, and changing it is allowed as long
as the name is changed.
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. You just DO WHAT THE FUCK YOU WANT TO.
{
"name": "hexGolfer",
"description": "Compress hex color strings by using three instead of six digits.",
"keywords": [
"color",
"hex",
"compress"
]
}
<!DOCTYPE html>
<title>Foo</title>
<div style="background:#34cf9d;">Original value: #<b>34cf9d</b></div>
<div style="background:#3c9;">Expected value: #<b>3c9</b></div>
<div>Actual value: #<b id="ret"></b></div>
<script>
// write a small example that shows off the API for your example
// and tests it in one fell swoop.
var myFunction = function a(b,c){return++c?(("0x"+b)/17+.5|0).toString(16):b.replace(/../g,a)}
var ret = document.getElementById( "ret" );
var color = myFunction('34cf9d');
ret.innerHTML = color;
ret.parentNode.style.background = '#' + color;
</script>
@atk

This comment has been minimized.

Copy link

atk commented May 8, 2012

Much shorter version without string replacement and more funky binary maths:

function(a){return((a='0x'+a)>>20<<8|a%65536>>12<<4|a%256>>4).toString(16)}

Update: saved a few more bytes, down to 70b:

function(a){return((a='0x'+a)>>12&3840|a>>8&240|a>>4&15).toString(16)}

@Prinzhorn

This comment has been minimized.

Copy link
Owner Author

Prinzhorn commented May 8, 2012

Nice, I should really dig into binary operations. Mine was like a straight forward implementation and I knew someone would come up with some crazy sh*. I guess you know https://gist.github.com/983535 and https://gist.github.com/1325937

I will update the gist as soon as I have time to understand yours.

@atk

This comment has been minimized.

Copy link

atk commented May 8, 2012

I'll gladly help you there: >> is binary shift right (bin100 >> 1 = bin010), & is binary "and" (bin011 & bin110 = bin010) and | is binary "or" (bin100 | bin001 = bin101). I use a combination of bit shift and binary masking to achieve the result.

FF      |FF      |FF
11111111|11111111|11111111
11110000|11110000|11110000
1111    |1111    |1111
F       |F       |F
@Prinzhorn

This comment has been minimized.

Copy link
Owner Author

Prinzhorn commented May 8, 2012

Thank your very much. I know what the binary operations do, I just want to sit down and reproduce every single step like on a piece of paper to make sure I really, 100%, positively understood what's happening. Once I really understood things down to the last bit I can apply them to other problems later. Just knowing what the single operation does sometimes just doesn't do it.

@atk

This comment has been minimized.

Copy link

atk commented May 9, 2012

It's just a little exercise of shifting and masking.

Update: by using the original approach for the first number, we can save another 2 bytes:

function(a){return((a='0x'+a)>>20<<8|a>>8&240|a>>4&15).toString(16)}

@williammalo

This comment has been minimized.

Copy link

williammalo commented May 10, 2012

What about this?
function(a){return a[0]+a[2]+a[4]}
It works!

@Prinzhorn

This comment has been minimized.

Copy link
Owner Author

Prinzhorn commented May 11, 2012

@atk test case "000000" returns "0" instead of expected "000"

@williammalo test case "001a1a" returns "011" instead of expected "022"

Play around with tests https://tinker.io/c0970/2

@atk

This comment has been minimized.

Copy link

atk commented May 11, 2012

@williammalo: not in IE<9. I'll have to rethink it a bit. Okay, that will work:

function(a){return((a='0x'+a)>>20<<8|a>>8&240|a>>4&15|65536).toString(16).substr(1,3)}

@williammalo

This comment has been minimized.

Copy link

williammalo commented May 11, 2012

@Prinzhorn
This should work:
function(a){return""+a[0]+a[2]+a[4]}

@Prinzhorn

This comment has been minimized.

Copy link
Owner Author

Prinzhorn commented May 11, 2012

@williammalo
Nope. The point is to round to the nearest color. Your code returns "011" for "001a1a" but "022" would be closer:
1a = 1*16 + 10 = 26

11 = 1_16 + 1 = 17 -> 26 - 17 = 9
22 = 2_16 + 2 = 34 -> 34 - 26 = 8

8 is smaller than 9, thus "22" is closer to "1a" than "11".

@williammalo

This comment has been minimized.

Copy link

williammalo commented May 11, 2012

@Prinzhorn
Oh! ok.
Back to the proverbial drawing board I guess...

@atk

This comment has been minimized.

Copy link

atk commented May 11, 2012

@williammalo: You could try to check if a[1]<8, but I don't know how to do increase a[0] if it's A-F.

@williammalo

This comment has been minimized.

Copy link

williammalo commented May 11, 2012

This is the best thing I came up with...
function(a){return((a="0x"+a)>>20).toString(16)+(a%65536>>12).toString(16)+(a%256>>4).toString(16)}
It's pretty shit, I know. :)

@Prinzhorn

This comment has been minimized.

Copy link
Owner Author

Prinzhorn commented May 11, 2012

It's all about the spirit ;-)

@atk

This comment has been minimized.

Copy link

atk commented May 11, 2012

if you change the order of the shifting and filtering (like I did), you'll have smaller numbers, i.e. instead of a%65536>>12, use a>>12&15.

@williammalo

This comment has been minimized.

Copy link

williammalo commented May 11, 2012

@atk
This is the best I can do without making my head explode...
Would you mind explaining to me what "filtering" means? I think it could be useful for me.

function(a){return((a='0x'+a)>>20<<8|a%65536>>12<<4|a%256>>4).toString(16)}

edit: I just want to learn. :)

@williammalo

This comment has been minimized.

Copy link

williammalo commented May 11, 2012

btw, if you like the regex version of the code, I made it shorter:

function(a){return a.replace(/../g,function(a){return("0x"+a>>4).toString(16)})}
@atk

This comment has been minimized.

Copy link

atk commented May 11, 2012

filtering: x % 16 == x & 15 - if you ever want to take modulo of a potence of 2, remember it.

Nice one with the replace, even shorter by reusing function:

function a(b,c){return++c?('0x'+b>>4).toString(16):b.replace(/../g,a)}

@Prinzhorn

This comment has been minimized.

Copy link
Owner Author

Prinzhorn commented May 11, 2012

But still, "001a1a" returns "011" instead of "022" ;-)

@atk

This comment has been minimized.

Copy link

atk commented May 11, 2012

that's true for both regex versions. However, that could now be remedied easily enough:

function a(b,c){return++c?(+('0x'+b)+7>>4).toString(16):b.replace(/../g,a)}

@atk

This comment has been minimized.

Copy link

atk commented May 11, 2012

Here's a commented version:

function a(
  b, // hex color string input, e.g. '001a22' or next matched 2 characters within the replace
  c  // undefined, numeric counter when replace runs
) {
  // c will be an unused numeric inside replace starting with 0, so ++c will result in 1, 
  // which is trueish inside replace and NaN outside replace, which is false-y.
  return ++c ?
    // inside replace: the preceding + will coerce the '0x'... to number, adding 7 does the 
    // rounding and >>4 divides by 16 while flooring the result - and .toString(16) reformats
    // the resulting number to hexadecimal.
    (+('0x' + b) + 7 >> 4).toString(16) :
    // outside the replace: replace each two characters and use own function as callback
    b.replace(/../g, a)
}
@Prinzhorn

This comment has been minimized.

Copy link
Owner Author

Prinzhorn commented May 11, 2012

Now the code returns "099" for "008888" ;-)

@atk

This comment has been minimized.

Copy link

atk commented May 11, 2012

fixed, we need to add 7, not 8.

@Prinzhorn

This comment has been minimized.

Copy link
Owner Author

Prinzhorn commented May 11, 2012

Now the code returns "0aa" for "009999".

I can do this all day :-D. But we're getting closer (the test iterates from 000 to fff)

@atk

This comment has been minimized.

Copy link

atk commented May 11, 2012

...which is quite correct. the closest color to "009999" is "0aa".

@Prinzhorn

This comment has been minimized.

Copy link
Owner Author

Prinzhorn commented May 11, 2012

You can't get closer to "009999" than "099". Don't confuse this with actual hex.

http://www.w3.org/TR/CSS2/syndata.html#value-def-color

The three-digit RGB notation (#rgb) is converted into six-digit form (#rrggbb) by replicating digits, not by adding zeros.
@atk

This comment has been minimized.

Copy link

atk commented May 11, 2012

Ah, I hadn't noticed this. So instead of requiring to round up, we need to rely on your former ("0x"+a)/17+.5|0 - or a smaller bit-math representation of that one, that I'll still have to think up.

@atk

This comment has been minimized.

Copy link

atk commented May 14, 2012

I haven't yet found a shorter version, so currently it is

function a(b,c){return++c?(("0x"+b)/17+.5|0).toString(16):b.replace(/../g,a)}

@maettig

This comment has been minimized.

Copy link

maettig commented May 18, 2012

Nice one. Reminds me of my CSS Color Converter. I tried to start from scratch but came up with the same solution in the end. Good work. What about spending a few bytes to make this work with values like "#34cf9d" and "#3c9"?

function a(b,c){return++c?(("0x"+b)/17+.5|0).toString(16):b[5]?b.replace(/\w\w/g,a):b}
@Prinzhorn

This comment has been minimized.

Copy link
Owner Author

Prinzhorn commented May 18, 2012

The idea was to have a meta function which finds all /#([0-9a-f]{6})/i inside the CSS file and passes it to this function. Could then be used in stuff like CSS minifiers as an option (because it's destructive).

This could of course all fit in 140 bytes combined ;-)

@maettig

This comment has been minimized.

Copy link

maettig commented May 18, 2012

Non-destructive version:

function(a){return a.replace(/(\w)\1(.)\2(.)\3/i,'$1$2$3')}
@subzey

This comment has been minimized.

Copy link

subzey commented Jun 9, 2012

This version is slightly shorter, also non-destructive

function(a,b){return(b=a.split(/(.)\1/i))[5]?b.join(''):a}
@Prinzhorn

This comment has been minimized.

Copy link
Owner Author

Prinzhorn commented Jun 10, 2012

@subzey the point is to be destructive. I already gave '34cf9d' -> '3c9' as an example.

The non-destructive version is a different problem. Maybe this should be separated.

@subzey

This comment has been minimized.

Copy link

subzey commented Jun 11, 2012

Oh, sorry. I missed that.

Maybe then something like this?

function(a){return a.split(/(\w)./).join('')}
@maettig

This comment has been minimized.

Copy link

maettig commented Jun 11, 2012

No, this will turn F0F0F0 into FFF which is wrong. See similar suggestions in the comments above.

@atk

This comment has been minimized.

Copy link

atk commented Jun 11, 2012

If we want this to be completely non-destructive, we can only change #000000 to #000, #000011 to #001, ... #111111 to #111 ... #FFFFFF to #FFF, almost as in Subzey's first approach (which would go wrong on colors like #099330:

function(a){return a.replace(/(\w)\1(\w)\2(\w)\3/,"$1$2$3")}

But as we wanted to get the closest color (as is specified in the readme), the original approach (with function body reusal) works best.

@maettig

This comment has been minimized.

Copy link

maettig commented Jun 11, 2012

That's what I suggested a few comments above. Mine is even shorter. ;-) Edit: And I added /i to fetch stuff like FffFFf.

@atk

This comment has been minimized.

Copy link

atk commented Jun 12, 2012

Ah, I've seen it; you've substituted the 2 last \w with dots.

@yckart

This comment has been minimized.

Copy link

yckart commented Aug 25, 2013

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.