Archive for July 16th, 2020

I'm not a designer, and I've never been happy with the color scheme for kavanot.name, so I decided to try something new. I use scssphp with scssphp server (and yes, you need both) to generate the CSS, and it allows variables, so I can do:


$hue = 250;
$saturation = '40%';
$textcolor: hsl($hue, $saturation, 10%);
$displaytextcolor: hsl($hue, $saturation, 40%);
$backcolor: hsl($hue, $saturation, 95%);
$bordercolor: hsl($hue, $saturation, 90%);
$deepbackcolor: hsl($hue, $saturation, 85%);

to make all the colors a variation on one single color. And I like that. But what color? Fortunately, scssphp allows for importing variables, so my `styles.php` includes:


setcookie('hue', $_GET['hue'], time()+12*60*60*24*30, '/'); // one year expiration
setcookie('saturation', $_GET['saturation'], time()+12*60*60*24*30, '/'); // one year expiration

$scss = new Compiler();
$scss->setVariables(array(
	'hue' => intval($_GET['hue']),
	'saturation' => intval($_GET['saturation']).'%',
));

instead of the in-file $hue = 250; $saturation = '40%';. Now I can include those values in the URL for the stylesheet itself,
and the cookies mean that I don't have to write it each time.

To get the query variables to the stylesheet, I have to pass them from the main page:


	<link rel="stylesheet" id=mainstylesheet type="text/css" href="/css/style.php/style.scss?hue=<?=$_GET['hue']?>&saturation=<?=$_GET['saturation']?>" />

(I am ignoring the input sanitization you need to avoid XSS. intval is your friend here!)

But retyping http://kavanot.name?hue=250&saturation=40 with minor variations is a pain. Can I make it interactive? Yes, with a Javascript
color picker. <input type=color /> is supported in all the major browsers, so I can do:


var colorPicker = $('input[type=color]');
let baseHSL = getBaseHSL(); // get the body color as [hue, saturation, lightness]
baseHSL[2] = 50; // start with a color that can be easily seen, 50% lightness
colorPicker.val(hsl2hex(...baseHSL)); // set the initial value

const stylesheet = $('#mainstylesheet')[0];

colorPicker.on('input', function(){
	let [h, s] = hex2hsl(this.value);
	// change the stylesheet href and the styles change on the fly!
	stylesheet.href = stylesheet.href.replace(/hue=\d+/, `hue=${h}`).replace(/saturation=\d+/, `saturation=${s}`);
});

function getBaseHSL(){
	// getComputedStyle.color is rgb(r,g,b)
	let [, r, g, b] = /(\d+),\s*(\d+),\s*(\d+)/.exec(window.getComputedStyle(document.body).color);
	return rgb2hsl (r, g, b);
}

Now I need the functions rgb2hsl, hex2hsl and hsl2hex to convert between the various color spaces. Luckily,
Jon Kantner has done the work for me.


function hex2hsl(hex){
	let [, r, g, b] = /#(..)(..)(..)/.exec(hex);
	return rgb2hsl (+('0x'+r), +('0x'+g), +('0x'+b));
}
	
function rgb2hsl (r, g, b){
	// convert to fractions
	r = r/255;
	b = b/255;
	g = g/255;

	// find greatest and smallest channel values
	const cmin = Math.min(r,g,b);
	const cmax = Math.max(r,g,b);
	const delta = cmax - cmin;
	let h;

	// calculate hue
	if (delta == 0){ // no difference
		h = 0;
	}else if (cmax == r){ // red is max
		h = ((g - b) / delta) % 6;
	}else if (cmax == g){
		h = (b - r) / delta + 2; // green is max
	}else{
		h = (r - g) / delta + 4; // blue is max
	}
	h = Math.round(h * 60);
	if (h < 0) h += 360;

	// calculate lightness
	let l = (cmax + cmin) / 2;

	// calculate saturation
	let s = delta == 0 ? 0 : delta / (1 - Math.abs(2 * l - 1));

	// make s and l percents
	return [h, Math.round(s*100), Math.round(l*100)];
}

function hsl2hex(h, s, l){
	let c = (1 - Math.abs(2 * l/100 - 1)) * s/100;
	let x = c * (1 - Math.abs((h / 60) % 2 - 1));
	let m = l/100 - c/2;
	let r, g, b;
	
	if (0 <= h && h < 60) {
		r = c; g = x; b = 0;
	} else if (60 <= h && h < 120) {
		r = x; g = c; b = 0;
	} else if (120 <= h && h < 180) {
		r = 0; g = c; b = x;
	} else if (180 <= h && h < 240) {
		r = 0; g = x; b = c;
	} else if (240 <= h && h < 300) {
		r = x; g = 0; b = c;
	} else if (300 <= h && h < 360) {
		r = c; g = 0; b = x;
	}

	r = Math.round((r + m) * 255);
	g = Math.round((g + m) * 255);
	b = Math.round((b + m) * 255);
	
	return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
}

function toHex(d) {
	d = d.toString(16);
	if (d.length == 1) d = `0${d}`;
	return d.toUpperCase();
}

and now I can play with colors to my heart's content!