summary refs log tree commit diff
diff options
context:
space:
mode:
authorIrene Knapp <ireneista@gmail.com>2020-05-04 20:32:58 -0700
committerIrene Knapp <ireneista@gmail.com>2020-05-04 20:32:58 -0700
commitf87d61e2d509f7528341eaa722ddc88493e71fc7 (patch)
tree4e36057cad2f5a060b81e2f16e50a14ba6c25019
Initial. JavaScript that generates SVG. Color math. A CIELAB color wheel.
-rw-r--r--index.html283
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>