<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<link rel="icon" src="/images/favicon.png" />
<title>color thing</title>
<style type="text/css">
html { font-size: 2em; }
html, input, select { color: #fff; background: #000; }
* {
line-height: 1.25em; padding: 0; margin: 0; border: 0;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
-ms-box-sizing: border-box;
-o-box-sizing: border-box;
box-sizing: border-box;
-webkit-tab-size: 4;
-moz-tab-size: 4;
-ms-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
text-align: center;
font-family: sans-serif;
}
#output, #output div, label { display: block; margin: auto; }
#output, #output div { width: 100%; }
input, select { font-size: 1em; padding: 0px 2px; }
input { height: 1em; width: 2em; vertical-align: text-top; }
</style>
<script type="text/javascript">
var input, div, method, normalization;
// hue interpolation wants six different "phases"
// for each of the three color channels.
//
// down, zero, and up
//
// phase => red, green, blue
// 0/6 => x, 0, 0 - red is at its peak
// 1/6 => x, x, 0 - red is falling, green is rising
// 2/6 => 0, x, 0 - green is at its peak
// 3/6 => 0, x, x - green is falling, blue is rising
// 4/6 => 0, 0, x - blue is at its peak
// 5/6 => x, 0, x - blue is falling, red is rising
// and we wrap around from here
//
// here's a shitty ascii art representation of the overlapping phases
// top of the line is max, bottom of the line is minimum
// note that there is time spent on the bottom, but no "hang time" -
// no time spent on the top
//
// red: \_/
// grn: /\_
// blu: _/\
var channel = deg => {
deg = (deg + 5/6) % 1;
switch (method.value) {
case "0":
// cosine interpolation is very gentle and actually follows
// the hue knob like converting from HSL colorspace
var cos = Math.cos(Math.PI * deg * 3) / 2 + .5;
return ((deg < 1/3) * cos + (deg >= 2/3) * (1 - cos));
case "1":
// linear interpolation is dead simple, but produces harsher peaks
return ((deg < 1/3) * (1 - deg * 3) + 0
+ (deg >= 2/3) * (deg * 3 - 2));
case "2":
// tangent produces harsher peaks than linear
return (deg < 1/3) * (1 - Math.tan(Math.PI * (6 * deg - 1) / 4)) / 2
+ (deg > 2/3) * (1 - Math.tan(Math.PI * (1 - 6 * deg) / 4)) / 2;
case "3":
// arc cosine interpolation is complicated and produces the harshest peaks
// this causes movement from one color to the next to be very
// clear, even near the maximum values of any given channel
// unfortunately, i can't see how to do this without branching,
// due to the NaNs produced by acos() outside the proper range.
// thankfully, speed is not likely to be terribly important here.
return (((deg < 1/3) ? Math.acos(deg * 6 - 1) : 0)
+ ((deg > 2/3) ? Math.acos(5 - deg * 6) : 0)) / Math.PI;
}
};
var hue = hue => {
var red = channel(hue);
var grn = channel(hue + 2 / 3);
var blu = channel(hue + 1 / 3);
var hyp = 1;
switch (normalization.value) {
case "0": // none
hyp = 1;
break;
case "1": // straight normalization
hyp = Math.sqrt(red * red + grn * grn + blu * blu);
break;
case "2": // perceptual brightness normalization
// these values aren't universally agreed upon
// 0.2989, 0.5870, 0.1140
// 0.2126, 0.7152, 0.0722
var rn = 0.2126 / 0.0722;
var gn = 0.5870 / 0.1140;
hyp = Math.sqrt(red * red * rn + grn * grn * gn + blu * blu);
break;
}
red = Math.round(255 * red / hyp);
grn = Math.round(255 * grn / hyp);
blu = Math.round(255 * blu / hyp);
return "rgb(" + red + ", " + grn + ", " + blu + ")";
};
var update = _ => {
var height = 24 / count.value + "em";
output.innerHTML = "";
for (var i = 0; i < count.value; i++) {
var color = document.createElement("div");
color.style.background = hue(i / count.value);
color.style.height = height;
output.appendChild(color);
}
};
onload = _ => {
count = document.querySelector("#count");
method = document.querySelector("#method");
output = document.querySelector("#output");
normalization = document.querySelector("#normalization");
count.oninput = e => {
count.value = count.value.replace(/[^0-9]/g, "");
};
count.onkeydown = e => {
if (13 == e.keyCode) update();
};
count.focus();
method.onchange = update;
normalization.onchange = update;
update();
};
</script>
</head>
<body>
<div>
<label for="count">Colors:
<input id="count" value="48" />
</label>
<label for="method">Interpolator:
<select id="method">
<option value="0">cosine</option>
<option value="1" selected>linear</option>
<option value="2">tangent</option>
<option value="3">arccosine</option>
</select>
</label>
<label for="normalization">Normalization:
<select id="normalization">
<option value="0">none</option>
<option value="1">brightness</option>
<option value="2">luminance/perceived/weighted</option>
</select>
</label>
</div>
<div id="output"></div>
</body>
</html>