diff options
author | Irene Knapp <ireneista@gmail.com> | 2020-05-04 20:32:58 -0700 |
---|---|---|
committer | Irene Knapp <ireneista@gmail.com> | 2020-05-04 20:32:58 -0700 |
commit | f87d61e2d509f7528341eaa722ddc88493e71fc7 (patch) | |
tree | 4e36057cad2f5a060b81e2f16e50a14ba6c25019 /index.html |
Initial. JavaScript that generates SVG. Color math. A CIELAB color wheel.
Diffstat (limited to 'index.html')
-rw-r--r-- | index.html | 283 |
1 files changed, 283 insertions, 0 deletions
diff --git a/index.html b/index.html new file mode 100644 index 0000000..1982c92 --- /dev/null +++ b/index.html @@ -0,0 +1,283 @@ +<!DOCTYPE html> +<html lang="en"> +<head> +<meta http-equiv="content-type" content="text/html; charset=utf-8"> +<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"> +<title>Color, Harmony, and Math</title> +<style type="text/css"> +.color-wheel { + width: 40vw; + height: 40vw; +} +</style> +<script> +// https://en.wikipedia.org/wiki/CIELAB_color_space +// +// CIELAB colors are stored as arrays [ L*, a*, b* ]. +// L* is scaled from 0.0 to 1.0, while a* and b* are scaled from -1.0 to 1.0. + +// https://en.wikipedia.org/wiki/CIE_1931_color_space +// +// CIEXYZ colors are stored as arrays [ X, Y, Z ]. +// X, Y, and Z are scaled such that the Y of illuminant D65 is 1.0. This is convenient +// because the sRGB conversion is defined in terms of that scale. + +// https://en.wikipedia.org/wiki/SRGB + +// CIEXYZ color space +let ILLUMINANT_D65 = [ 0.950489, 1.0, 1.088840 ]; + + +function main() { + let svgState = svgInit(); + + let colorWheelParameters = { + scale: 10000, + nStepsAround: 32, + nStepsOutward: 128, + }; + + let radiusForStepOutward = function(stepOutward) { + return stepOutward / colorWheelParameters.nStepsOutward; + }; + + let thetaForStepAround = function(stepAround) { + return stepAround * 2 * Math.PI / colorWheelParameters.nStepsAround; + }; + + for (var iOutward = 0; colorWheelParameters.nStepsOutward > iOutward; iOutward++) { + for (var iAround = 0; colorWheelParameters.nStepsAround > iAround; iAround++) { + let chunkParameters = { + startTheta: thetaForStepAround(iAround), + endTheta: thetaForStepAround(iAround + 1), + outsideRadius: radiusForStepOutward(iOutward + 1), + insideRadius: radiusForStepOutward(iOutward), + isInnermostWedge: iOutward == 0, + }; + + generateOneColorWheelChunk(svgState, colorWheelParameters, chunkParameters); + } + } +} + + +function generateOneColorWheelChunk(svgState, colorWheelParameters, chunkParameters) { + let scale = colorWheelParameters.scale, + nStepsAround = colorWheelParameters.nStepsAround, + startTheta = chunkParameters.startTheta, + endTheta = chunkParameters.endTheta, + outsideRadius = chunkParameters.outsideRadius, + insideRadius = chunkParameters.insideRadius, + isInnermostWedge = chunkParameters.isInnermostWedge; + + let outsideScale = scale * outsideRadius, + insideScale = scale * insideRadius, + coordinatesFromPolar = function(radius, theta) { + return [ Math.cos(theta) * radius, Math.sin(theta) * radius ]; + }, + startOutsideCoords = coordinatesFromPolar(outsideScale, startTheta), + endOutsideCoords = coordinatesFromPolar(outsideScale, endTheta), + moveParameters = [].concat(startOutsideCoords), + outerArcParameters = [outsideScale, outsideScale, 0, 0, 1] + .concat(endOutsideCoords); + + let pathData = ['M'].concat(moveParameters).concat(['A']) + .concat(outerArcParameters); + + if (isInnermostWedge) { + let centerCoords = [ 0, 0 ], + lineParameters = [].concat(centerCoords); + + pathData = pathData.concat(['L']).concat(lineParameters); + } else { + let startInsideCoords = coordinatesFromPolar(insideScale, startTheta), + endInsideCoords = coordinatesFromPolar(insideScale, endTheta), + innerArcParameters = [insideScale, insideScale, 0, 0, 0] + .concat(startInsideCoords); + + pathData = pathData.concat(['L']).concat(endInsideCoords).concat(['A']) + .concat(innerArcParameters); + } + + pathData = pathData.concat(['z']).join(' '); + + let gradientId = svgGenerateId(svgState, 'gradient'); + + let gradientElement = document.createElementNS(svgState.SVG_NS, 'linearGradient'); + gradientElement.setAttribute('id', gradientId); + gradientElement.setAttribute('gradientUnits', 'userSpaceOnUse'); + gradientElement.setAttribute('x1', startOutsideCoords[0]); + gradientElement.setAttribute('y1', startOutsideCoords[1]); + gradientElement.setAttribute('x2', endOutsideCoords[0]); + gradientElement.setAttribute('y2', endOutsideCoords[1]); + + let stop1Element = document.createElementNS(svgState.SVG_NS, 'stop'); + stop1Element.setAttribute('offset', '0%'); + stop1Element.setAttribute('stop-color', colorFromPolar(outsideRadius, startTheta)); + gradientElement.appendChild(stop1Element); + + let stop2Element = document.createElementNS(svgState.SVG_NS, 'stop'); + stop2Element.setAttribute('offset', '100%'); + stop2Element.setAttribute('stop-color', colorFromPolar(outsideRadius, endTheta)); + gradientElement.appendChild(stop2Element); + + svgState.defsElement.appendChild(gradientElement); + + let pathElement = document.createElementNS(svgState.SVG_NS, 'path'); + pathElement.setAttribute('d', pathData); + pathElement.setAttribute('fill', 'url(#' + gradientId + ')'); + pathElement.setAttribute('shape-rendering', 'crispEdges'); + svgState.svgElement.appendChild(pathElement); +} + + +function svgInit() { + let svgState = { }; + + svgState.SVG_NS = 'http://www.w3.org/2000/svg'; + svgState.divElement = document.getElementById('color-wheel'); + svgState.svgElement = document.createElementNS(svgState.SVG_NS, 'svg'); + svgState.svgElement.setAttribute('class', 'color-wheel'); + svgState.svgElement.setAttribute('viewBox', '-10000 -10000 20000 20000'); + svgState.svgElement.setAttribute('transform', 'scale(1 -1)'); + svgState.divElement.appendChild(svgState.svgElement); + + svgState.defsElement = document.createElementNS(svgState.SVG_NS, 'defs'); + svgState.svgElement.appendChild(svgState.defsElement); + + svgState.idCounter = 0; + + return svgState; +} + + +function svgGenerateId(svgState, prefix) { + let result = prefix + svgState.idCounter; + svgState.idCounter++; + return result +} + + +function colorFromPolar(radius, theta) { + let cielab = [ radius, Math.cos(theta), Math.sin(theta) ], + ciexyz = cielabToCiexyz(cielab, ILLUMINANT_D65), + sRGB = ciexyzToSRGB(ciexyz), + web = sRGBToWeb(sRGB); + + return web; +} + + +function ciexyzToCielab(ciexyz, illuminant) { + let delta = 6.0 / 29.0, + f = function(t) { + if (t > Math.pow(delta, 3.0)) { + return Math.pow(delta, 1.0 / 3.0); + } else { + return (t / (3 * Math.pow(delta, 2.0))) + (4.0 / 29.0); + } + }, + fX = f(ciexyz[0] / illuminant[0]), + fY = f(ciexyz[1] / illuminant[1]), + fZ = f(ciexyz[2] / illuminant[2]), + cielab = [ 1.16 * fX, 5.0 * (fX - fY), 2.0 * (fY - fZ) ]; + + return cielab; +} + + +function cielabToCiexyz(cielab, illuminant) { + let delta = 6.0 / 29.0, + fPrime = function(t) { + if (t > delta) { + return Math.pow(t, 3.0); + } else { + return 3 * Math.pow(t, 2.0) * (t - (4.0 / 29.0)); + } + }, + rescaledL = (cielab[0] + 0.16) / 1.16, + x = illuminant[0] * fPrime(rescaledL + (cielab[1] / 5.0)); + y = illuminant[1] * fPrime(rescaledL); + z = illuminant[2] * fPrime(rescaledL - (cielab[2] / 2.0)); + ciexyz = [ x, y, z ]; + + return ciexyz; +} + + +function ciexyzToSRGB(ciexyz) { + let rCoefficients = [ +3.24096994, -1.53738318, -0.49861076 ], + gCoefficients = [ -0.96924364, +1.87596750, +0.04155506 ], + bCoefficients = [ +0.05563008, -0.20397696, +1.05697151 ], + multiplyCoefficients = function(coefficients) { + return coefficients[0] * ciexyz[0] + + coefficients[1] * ciexyz[1] + + coefficients[2] * ciexyz[2]; + }, + rLinear = multiplyCoefficients(rCoefficients), + gLinear = multiplyCoefficients(gCoefficients), + bLinear = multiplyCoefficients(bCoefficients), + gamma = function(u) { + if (u > 0.0031308 ) { + return (211.0 * Math.pow(u, 5.0 / 12.0) - 11.0) / 200.0; + } else { + return u * (323.0 / 25.0); + } + }, + sRGB = [ gamma(rLinear), gamma(gLinear), gamma(bLinear) ]; + + return sRGB; +} + + +function sRGBToCiexyz(sRGB) { + let gammaPrime = function(u) { + if (u > 0.04045) { + return Math.pow((200.0 * u + 11.0) / 211.0, 12.0 / 5.0); + } else { + return u * (25.0 / 323.0); + } + } + rgbLinear = gammaPrime(sRGB[0]), + gLinear = gammaPrime(sRGB[1]), + bLinear = gammaPrime(sRGB[2]), + xCoefficients = [ +0.41239080, +0.35758434, +0.18048079 ], + yCoefficients = [ +0.21263901, +0.71516868, +0.07219232 ], + zCoefficients = [ +0.01933082, +0.11919478, +0.95053215 ], + multiplyCoefficients = function(coefficients) { + return coefficients[0] * rLinear, + + coefficients[1] * gLinear, + + coefficients[2] * bLinear; + }, + ciexyz = [ multiplyCoefficients(xCoefficients), + multiplyCoefficients(yCoefficients), + multiplyCoefficients(zCoefficients) ]; + + return ciexyz; +} + + +function sRGBToWeb(sRGB) { + let channelToHex = function(channel) { + let capped = Math.max(0.0, Math.min(channel, 1.0)), + scaled = Math.floor(capped * 255.0); + + let result = Number(scaled).toString(16); + while (2 > result.length) { + result = '0' + result; + } + return result; + }; + + return '#' + channelToHex(sRGB[0]) + channelToHex(sRGB[1]) + + channelToHex(sRGB[2]); +} + + +window.addEventListener('load', main); +</script> +</head> +<body> + <div id="color-wheel"></div> +</body> +</html> |