<input type="text" class="gradient-input" />
<div class="test-element"></div>

<svg class="gradient-overlay" width="100%" height="100%">
  <defs>
    <marker id="extending-line-end" markerWidth="50" markerHeight="1"
            orient="auto" markerUnits="userSpaceOnUse" refX="0" refY="0">
      <line class="support-line" x1="0" y1="0" x2="50" y2="0" />
    </marker>
    <marker id="extending-line-start" markerWidth="50" markerHeight="1"
            orient="auto" markerUnits="userSpaceOnUse" refX="50" refY="0">
      <line class="support-line" x1="0" y1="0" x2="50" y2="0" />
    </marker>
  </defs>
  <rect class="gradient-box" />
  <line class="gradient-line extend-line" />
  <line class="angle-0-line support-line" />
  <line class="start-edge support-line extend-line" />
  <line class="end-edge support-line extend-line" />
  <circle class="start-point gradient-line-point" r="2" />
  <circle class="center-point gradient-line-point" r="2" />
  <circle class="end-point gradient-line-point" r="2" />
  <path class="angle-arc" />
</svg>
html, body {
  height: 100%;
  margin: 0;
  overflow: hidden;
}

body {
  display: flex;
  justify-content: center;
  align-items: center;
}

.gradient-input {
  position: absolute;
  top: 0;
  left: 5vw;
  width: 90vw;
  font-size: 1em;
  padding: .8em 0;
  color: #777;
  background: transparent;
  border-color: #eee;
  border-style: solid;
  border-width: 0 0 1px 0;
  text-align: center;
  transition: .2s all;
}

.gradient-input:hover,
.gradient-input:focus {
  background: #eee;
  color: rgb(222, 7, 159);
}

.test-element {
  width: 50vw;
  height: 50vh;
  background-image: linear-gradient(75deg, #3800fd -5%, #ff0092 40%, #ff6fb1 65%, #ff4740 90%);
}

.gradient-overlay {
  position: absolute;
  top: 0;
  left: 0;
  pointer-events: none;
  color: #555;
}

.gradient-box {
  stroke: currentColor;
  fill: none;
  shape-rendering: crispEdges;
}

.gradient-line {
  stroke-width: 2;
  stroke: currentColor;
}

.angle-arc {
  stroke-width: 1;
  stroke: currentColor;
  fill: transparent;
}

.support-line {
  stroke-width: 1;
  stroke: currentColor;
  stroke-dasharray: 3 2;
}

.extend-line {
  marker-start: url(#extending-line-start);
  marker-end: url(#extending-line-end);
}

.gradient-line-point,
.color-stop {
  fill: currentColor;
}

.color-stop-label {
  position: absolute;
  width: 20px;
  height: 20px;
  border-radius: 5px;
  border: 2px solid white;
  box-sizing: border-box;
  box-shadow: 0 1px 5px rgba(0,0,0,.3);
  pointer-events: auto;
  transform-origin: 10px -10px;
}

.color-stop-label::before {
  content: "";
  position: absolute;
  width: 0;
  height: 0;
  border-style: solid;
  border-color: transparent transparent white transparent;
  border-width: 0px 6px 6px;
  top: -6px;
  left: 2px;
}
// TODO
// - the gradient-box depends on background-size and background position
// - support multiple gradients!
// - other angle units don't work (rad, grad, turn)
// - bug with (red 30%, orange, yellow, black 10%) -> all stops should be at 30%
// - the computed-style isn't formatted the same on all browsers, so changing the value in the input only works in firefox for now.

/*
      B
    +-----+
    |    /
    |   /
  A |  /  C
    | /
    |/

     C2 = A2 + B2
     A = cos(AC) * C
     AC = acos(A/C)
*/

const SVG_NS = "http://www.w3.org/2000/svg";
const ANGLE_KEYWORDS = {
  "to top": () => 0,
  "to top right": ({width, height}) => {
    return Math.acos((width/2) / (Math.sqrt(Math.pow(width, 2) + Math.pow(height, 2)) / 2));
  },
  "to right": () => Math.PI/2,
  "to bottom right": bounds => Math.PI - ANGLE_KEYWORDS["to top right"](bounds),
  "to bottom": () => Math.PI,
  "to bottom left": bounds => Math.PI + ANGLE_KEYWORDS["to top right"](bounds),
  "to left": () => 3*Math.PI/2,
  "to top left": bounds => (2*Math.PI) - ANGLE_KEYWORDS["to top right"](bounds)
};
const ANGLE_REGEX = /^-?[0-9.]+deg$/;
const COLOR_REGEX = /^\([0-9., ]+\)/;
const ARC_RADIUS = 50;

let testElement = document.querySelector(".test-element");

let svg = document.querySelector(".gradient-overlay");
let box = document.querySelector(".gradient-box");
let line = document.querySelector(".gradient-line");
let angleArc = document.querySelector(".angle-arc");
let centerLine = document.querySelector(".angle-0-line");
let startEdge = document.querySelector(".start-edge");
let endEdge = document.querySelector(".end-edge");
let startPoint = document.querySelector(".start-point");
let centerPoint = document.querySelector(".center-point");
let endPoint = document.querySelector(".end-point");

let input = document.querySelector(".gradient-input");
input.value = document.styleSheets[0].cssRules[4]
              .style.getPropertyValue("background-image");

function deleteStops() {
  let stops = document.querySelectorAll(".color-stop, .color-stop-label");
  [].slice.apply(stops).forEach(stop => {
    stop.remove();
  });
}

function renderStop(color, position, angle, index) {
  let stop = document.createElementNS(SVG_NS, "circle");
  stop.classList.add("color-stop");
  stop.setAttribute("r", 4);
  stop.setAttribute("cx", position.x);
  stop.setAttribute("cy", position.y);
  svg.appendChild(stop);

  let label = document.createElement("div");
  label.classList.add("color-stop-label");
  label.style.background = color;
  label.style.top = position.y + "px";
  label.style.left = position.x + "px";
  document.body.appendChild(label);

  angle = angle * 180 / Math.PI;
  if (index % 2 === 0) {
    angle -= 90;
  } else {
    angle += 90;
  }
  label.style.webkitTransform = "translateX(-10px) translateY(10px) rotate(" + angle + "deg)";
  label.style.transform = "translateX(-10px) translateY(10px) rotate(" + angle + "deg)";
}

function getColorStopPosition(angle, gradientLine, percentagePosition) {
  let yDiff = Math.sin(angle-Math.PI/2) *
              (gradientLine.length * percentagePosition/100);
  let xDiff = Math.cos(angle-Math.PI/2) *
              (gradientLine.length * percentagePosition/100);
  return {
    x: gradientLine.start.x + xDiff,
    y: gradientLine.start.y + yDiff
  };
}

function renderGradientLine(quad, {angle, stops, gradientLine}) {
  deleteStops();

  // Display the gradient box
  box.setAttribute("x", quad.p1.x);
  box.setAttribute("y", quad.p1.y);
  box.setAttribute("width", quad.bounds.width);
  box.setAttribute("height", quad.bounds.height);

  // Display the gradient line
  line.setAttribute("x1", gradientLine.start.x);
  line.setAttribute("y1", gradientLine.start.y);
  line.setAttribute("x2", gradientLine.end.x);
  line.setAttribute("y2", gradientLine.end.y);

  // Display the start point
  startPoint.setAttribute("cx", gradientLine.start.x);
  startPoint.setAttribute("cy", gradientLine.start.y);

  // Display the center point
  centerPoint.setAttribute("cx", gradientLine.center.x);
  centerPoint.setAttribute("cy", gradientLine.center.y);

  // Display the end point
  endPoint.setAttribute("cx", gradientLine.end.x);
  endPoint.setAttribute("cy", gradientLine.end.y);

  // Display the center line
  centerLine.setAttribute("x1", gradientLine.center.x);
  centerLine.setAttribute("y1", gradientLine.center.y);
  centerLine.setAttribute("x2", gradientLine.center.x);
  centerLine.setAttribute("y2", gradientLine.center.y - ARC_RADIUS - 10);

  // Display the angle arc
  angle = angle % (Math.PI*2);
  if (angle < 0) {
    angle = (Math.PI*2) + angle;
  }

  let arcX = gradientLine.center.x + Math.cos(angle-Math.PI/2) * ARC_RADIUS;
  let arcY = gradientLine.center.y + Math.sin(angle-Math.PI/2) * ARC_RADIUS;
  let largeArc = angle > Math.PI ? 1 : 0;
  let path = ["M", gradientLine.center.x, ",", gradientLine.center.y, " ",
              "m", "0,-", ARC_RADIUS, " ",
              "A", ARC_RADIUS, ",", ARC_RADIUS, " 0 ", largeArc, " 1 ", arcX, ",", arcY];
  angleArc.setAttribute("d", path.join(""));

  // Display the two perpendicular lines
  let point1, point2;
  if (angle <= Math.PI/2) {
    point1 = quad.p4;
    point2 = quad.p2;
  } else if (angle <= Math.PI) {
    point1 = quad.p1;
    point2 = quad.p3;
  } else if (angle <= 3*Math.PI/2) {
    point1 = quad.p2;
    point2 = quad.p4;
  } else {
    point1 = quad.p3;
    point2 = quad.p1;
  }

  startEdge.setAttribute("x1", gradientLine.start.x);
  startEdge.setAttribute("y1", gradientLine.start.y);
  startEdge.setAttribute("x2", point1.x);
  startEdge.setAttribute("y2", point1.y);
  endEdge.setAttribute("x1", gradientLine.end.x);
  endEdge.setAttribute("y1", gradientLine.end.y);
  endEdge.setAttribute("x2", point2.x);
  endEdge.setAttribute("y2", point2.y);

  for (let i = 0; i < stops.length; i ++) {
    let {color, position} = stops[i];
    renderStop(color, getColorStopPosition(angle, gradientLine, position), angle, i);
  }
}

function parseGradientPart(part, gradientBoxBounds) {
  // All parts have a useless trailing character, either ) or ,
  part = part.trim();
  part = part.substring(0, part.length - 1);

  // Check if the part is an angle, a signed float with 'deg' suffix or any of
  // the accepted keywords.
  if (Object.keys(ANGLE_KEYWORDS).indexOf(part) !== -1){
    return {
      type: "angle",
      angle: ANGLE_KEYWORDS[part](gradientBoxBounds)
    };
  } else if (part.match(ANGLE_REGEX)) {
    return {
      type: "angle",
      angle: parseFloat(part) * Math.PI / 180
    };
  } else {
    // Otherwise, it's a color stop. Color stops may or may not have a position.
    // Positions may be in px or %. Colors are a list of 3 or 4 numbers between
    // parens, and are either rgb or rgba.
    let color = part.match(COLOR_REGEX)[0];
    let rgbPrefix = color.split(",").length === 3 ? "rgb" : "rgba";
    let position = part.substring(color.length).trim();

    return {
      type: "stop",
      position,
      color: rgbPrefix + color
    };
  }

  return part;
}

function getGradientLine(angle, gradientBoxBounds) {
  let gradientLineLength = Math.abs(gradientBoxBounds.width * Math.sin(angle)) +
                           Math.abs(gradientBoxBounds.height * Math.cos(angle));
  let center = {
    x: gradientBoxBounds.x + gradientBoxBounds.width/2,
    y: gradientBoxBounds.y + gradientBoxBounds.height/2
  };

  let yDiff = Math.sin(angle-Math.PI/2) * gradientLineLength/2;
  let xDiff = Math.cos(angle-Math.PI/2) * gradientLineLength/2;

  return {
    length: gradientLineLength,
    center: center,
    start: {
      x: center.x - xDiff,
      y: center.y - yDiff
    },
    end: {
      x: center.x + xDiff,
      y: center.y + yDiff
    }
  };
}

function resolvePosition(positionString, gradientLine) {
  positionString = positionString + "";
  let isPx = positionString.endsWith("px");
  let value = parseFloat(positionString);

  return isPx ? value * 100 / gradientLine.length : value;
}

function parseGradient(parsedBackgroundImage, gradientBoxBounds) {
  // The computed-style gives us some facility to parse the gradient, but not
  // much. Color stops that don't have a defined position still don't have a
  // position in the computed-style. Angle keywords aren't replaced with degrees.
  // Position units can be mixed (% and px). So we need to parse the angle and
  // stops ourselves.
  let gradient = parsedBackgroundImage.value;

  // TODO: use the tokens in parsedBackgroundImage to only care about the actual
  // value between ( and ), and then add a new tokenizer function that will
  // tokenize this value correctly (right now, it's failing with transparent)

  let index = gradient.indexOf("linear-gradient");
  if (index === -1) {
    return;
  }

  // Removing the linear-gradient( part. Remaining is the angle and color stops.
  gradient = gradient.substring(index + 16);

  // Computed colors are always rgb or rgba, and positions, if present, are
  // found after colors, so splitting by this will give us a nice array with
  // the angle first (if present) and stops.
  let parts = [];
  while (true) {
    let rgbIndex = gradient.indexOf("rgb(");
    let rgbaIndex = gradient.indexOf("rgba(");
    let transparentIndex = gradient.indexOf("transparent");
    if (rgbIndex === -1 && rgbaIndex === -1 && transparentIndex === -1) {
      parts.push(parseGradientPart(gradient, gradientBoxBounds));
      break;
    }

    let index = Math.min.apply(null,
      [rgbIndex, rgbaIndex, transparentIndex].filter(i => i >= 0));
    let colorLength = rgbIndex === index ? 3 : rgbaIndex === index ? 4 : 11;

    let part = gradient.substring(0, index);
    if (part.trim()) {
      parts.push(parseGradientPart(gradient.substring(0, index), gradientBoxBounds));
    }
    gradient = gradient.substring(index + (rgbIndex === index ? 3 : 4));
  }

  // An angle is not mandatory. If missing, defaults ot "to bottom".
  let angle = ANGLE_KEYWORDS["to bottom"]();
  if (parts[0].type === "angle") {
    angle = parts[0].angle;
    parts.splice(0, 1);
  }

  // Get the gradient line data.
  let gradientLine = getGradientLine(angle, gradientBoxBounds);

  // Post-process the color stops to calculate the missing positions.
  // When a position is missing, it's typically between the previous and next.
  // But there are edge cases. The gradient line extends infinitely in either
  // directions, and so if the first position is missing but the second is -50
  // then first is also -50.

  // Given a stop index, get the position of the next stop. If there is no next
  // stop, return either 100 (for 100%) or the provided last position if it is
  // more than 100.
  let nextPos = (i, lastPos) => {
    if (parts[i+1]) {
      // If there's a next stop, get its position, or go to the next one if it
      // doesn't have one.
      let pos = resolvePosition(parts[i+1].position, gradientLine);
      return pos ? {nextIndex: i+1, next: pos} : nextPos(i+1, lastPos);
    } else {
      // Otherwise, this is the last stop.
      return {nextIndex: i, next: Math.max(lastPos||0, 100)};
    }
  }

  // The last defined position we found.
  let lastPos;

  let stops = [];
  for (let i = 0; i < parts.length; i++) {
    let stop = parts[i];
    let position;

    // If a position is already defined, use this one, except if there was
    // previous, greater, position.
    if (stop.position !== "") {
      position = resolvePosition(stop.position, gradientLine);
      if (typeof lastPos !== "undefined" && lastPos >= position) {
        position = lastPos;
      }
    }

    // Otherwise, distribute the value according to the number of stops until
    // the next defined value.
    else {
      let {nextIndex, next} = nextPos(i, lastPos);

      // If this is the first stop and the next is greater than 0, then this is
      // 0.
      if (i === 0 && next > 0) {
        position = 0;
      }

      // If this is the first stop and the next is lower than 0, then clamp it
      // to the next value.
      if (i === 0 && next <= 0) {
        position = next;
      }

      // If this is the last stop and the previous is lower than 100, then this
      // is 100.
      else if (i === parts.length - 1 && lastPos < 100) {
        position = 100;
      }

      // If this is the last stop and the previous was greater than 100, then
      // clamp it to the previous value.
      else if (i === parts.length - 1 && lastPos >= 100) {
        position = lastPos;
      }

      else {
        if (typeof lastPos === "undefined") {
          position = Math.min(0, next);
        } else {
          position = lastPos + ((next - lastPos) / (nextIndex - i + 1));
        }
      }
    }

    stops.push({
      color: stop.color,
      position: position
    });

    lastPos = position;
  }

  return {angle, stops, gradientLine};
}

/**
 * Parses the value of a background-image CSS property.
 * Note that this only works with computed properties! Do not attempt to use with
 * authored styles as it doesn't handle malformed syntax at all (may fail
 * unexpectedly or create an infinite loop).
 * @param {String} backgroundImage The computed background-image value.
 * @return {Array} An array of background image objects: {type, value, tokens}
 */
function parseBackgroundImage(backgroundImage) {
  let images = [];
  let tokens = tokenizeBackgroundImage(backgroundImage);
  for (let i = 0; i < tokens.length; i += 5) {
    images.push({
      type: tokens[i].value,
      value: tokens[i].value + tokens[i+1].value + tokens[i+2].value + tokens[i+3].value,
      tokens: tokens.slice(i, i+4)
    });
  }
  return images;
}

/**
 * Tokenizes the value of a background-image CSS property.
 * Note that this only works with computed properties! Do not attempt to use with
 * authored styles as it doesn't handle malformed syntax at all (may fail
 * unexpectedly or create an infinite loop).
 * @param {String} backgroundImage The computed background-image value.
 * @return {Array} An array of tokens: {type, value, startIndex, endIndex}
 */
function tokenizeBackgroundImage(backgroundImage) {
  // A CSS <image> may be a <uri>, a <gradient>, or a part of the page, defined by
  // the element() function.
  // "linear-gradient(50deg, rgba(0, 0, 0, 0.6), transparent), repeating-linear-gradient(to right, transparent 0px, transparent 100px, rgb(0, 0, 0) 100px, rgb(0, 0, 0) 200px, transparent 200px), -moz-element(#angle-range), url("https://dl.dropboxusercontent.com/u/714210/grid.png")"

  let imageTypes = [
    "repeating-linear-gradient", "linear-gradient",
    "repeating-radial-gradient", "radial-gradient",
    "url", "-moz-element"
  ];

  // Expect one of the image types at startIndex, return the type and endIndex.
  let eatType = startIndex => {
    for (let type of imageTypes) {
      if (backgroundImage.substring(startIndex).startsWith(type)) {
        return {
          endIndex: startIndex + type.length,
          value: type
        };
      }
    }
  };

  // Expect an <image> value at startIndex, looks for the next ) character.
  let eatValue = startIndex => {
    let nbOpen = 0;
    let i = startIndex;
    while (true) {
      if (backgroundImage[i] === "(") {
        nbOpen ++;
      } else if (backgroundImage[i] === ")") {
        nbOpen --;
        if (nbOpen === -1) {
          return {
            endIndex: i,
            value: backgroundImage.substring(startIndex, i)
          }
        }
      }
      i ++;
    }
  };

  let tokens = [];
  let lastToken = null;
  let i = 0;

  while (i < backgroundImage.length) {
    let char = backgroundImage[i];

    if (!lastToken) {
      // Progress to end of function name.
      let {endIndex, value} = eatType(i);
      tokens.push({
        type: "function",
        value,
        startIndex: i,
        endIndex
      });
      i = endIndex;
      lastToken = "function";
    } else if (lastToken === "function" && char === "(") {
      // Just saw a function, eat the (.
      lastToken = "(";
      tokens.push({
        type: "(",
        value: "(",
        startIndex: i,
        endIndex: i+1
      });
      i ++;
    } else if (lastToken === "(") {
      // Just opened a ( after a function, progress to end of value.
      let {endIndex, value} = eatValue(i);
      tokens.push({
        type: "value",
        value,
        startIndex: i,
        endIndex
      });
      i = endIndex;
      lastToken = "value";
    } else if (lastToken === "value" && char === ")") {
      // Just saw a closing ). Eat it.
      lastToken = ")";
      tokens.push({
        type: ")",
        value: ")",
        startIndex: i,
        endIndex: i+1
      });
      i ++;
    } else if (lastToken === ")" && char === ",") {
      // Multiple background separator, go back to start.
      lastToken = null;
      tokens.push({
        type: ",",
        value: ",",
        startIndex: i,
        endIndex: i+1
      });
      i += 2;
    } else {
      i ++;
    }
  }

  return tokens;
}

function getBoxQuad(element) {
  let quad;
  if (element.getBoxQuads) {
    return element.getBoxQuads()[0];
  } else {
    return {
      p1: {
        x: element.offsetLeft,
        y: element.offsetTop
      },
      p2: {
        x: element.offsetLeft + element.offsetWidth,
        y: element.offsetTop
      },
      p3: {
        x: element.offsetLeft + element.offsetWidth,
        y: element.offsetTop + element.offsetHeight
      },
      p4: {
        x: element.offsetLeft,
        y: element.offsetTop + element.offsetHeight
      },
      bounds: {
        x: element.offsetLeft,
        y: element.offsetTop,
        width: element.offsetWidth,
        height: element.offsetHeight
      }
    };
  }
}

function previewGradient(element, index) {
  let backgroundImages = getComputedStyle(element).backgroundImage;
  let parsedImages = parseBackgroundImage(backgroundImages);
  let gradient = parsedImages[index];

  let quad = getBoxQuad(element);

  renderGradientLine(quad, parseGradient(gradient, quad.bounds));
}

function isNumber(string) {
  return parseInt(string) + "" === string || string === "-" || string === ".";
}

function isCloseToNumber(string, index) {
  return (string[index - 1] && isNumber(string[index - 1])) ||
    (string[index] && isNumber(string[index]));
}

function getNumberRange(string, index) {
  if (isCloseToNumber(string, index)) {
    let start, end;
    index --;
    while (string[index]) {
      if (isNumber(string[index])) {
        index --;
      } else {
        start = index + 1;
        break;
      }
    }
    index ++;
    while (string[index]) {
      if (isNumber(string[index])) {
        index ++;
      } else {
        end = index;
        break;
      }
    }
    return {start: start, end: end};
  } else {
    return false;
  }
}

function increaseValue(input, isUp, isShift, isCtrl) {
  let selectionStartBefore = input.selectionStart;
  let nbRange = getNumberRange(input.value, selectionStartBefore);
  if (nbRange) {
    let delta = isUp === false ? -1 : 1;
    if (isShift) {
      delta *= 10;
    } else if (isCtrl) {
      delta /= 10;
    }
    let value = parseFloat(input.value.substring(nbRange.start, nbRange.end));
    value += delta;
    value = Math.round(value * 10) / 10;
    input.value = input.value.slice(0, nbRange.start) + value + input.value.slice(nbRange.end);
    setTimeout(function () {
      input.setSelectionRange(nbRange.start, nbRange.start);
    }, 0);
  }
}

input.addEventListener("keydown", e => {
  if (e.keyCode !== 38 && e.keyCode !== 40) {
    return;
  }

  increaseValue(input, e.keyCode === 38, e.shiftKey, e.ctrlKey);
  e.preventDefault();

  testElement.style.backgroundImage = input.value;
  previewGradient(testElement, 0);
});

input.addEventListener("keyup", e => {
  let value = input.value;
  testElement.style.backgroundImage = value;
  previewGradient(testElement, 0);
});

addEventListener("resize", e => {
  previewGradient(testElement, 0);
});

previewGradient(testElement, 0);

// TESTS

const TEST_DATA = [
  {input: [undefined, undefined],
   output: [0, 100]},
  {input: [undefined, undefined, undefined],
   output: [0, 50, 100]},
  {input: [undefined, undefined, undefined, undefined, undefined],
   output: [0, 25, 50, 75, 100]},
  {input: [-10, 100],
   output: [-10, 100]},
  {input: [-10, -20],
   output: [-10, -10]},
  {input: [110, 100],
   output: [110, 110]},
  {input: [50, undefined, 100],
   output: [50, 75, 100]},
  {input: [100, 50, 75, undefined],
   output: [100, 100, 100, 100]},
  {input: [undefined, undefined, undefined, -20],
   output: [-20, -20, -20, -20]},
  {input: [10, 20, 30, -20],
   output: [10, 20, 30, 30]},
  {input: [10, undefined, 20],
   output: [10, 15, 20]},
  {input: [75, 20, 30],
   output: [75, 75, 75]},
  {input: [75, 20, undefined],
   output: [75, 75, 100]},
  {input: [60, 20, undefined, undefined],
   output: [60, 60, 80, 100]},
];

for (let {input, output} of TEST_DATA) {
  let inputStr = "linear-gradient(rgb(0,0,0)";
  for (let i = 0; i < input.length; i ++) {
    let stop = input[i];
    if (stop) {
      inputStr += " " + stop + "%";
    }
    if (i < input.length - 1) {
      inputStr += ", rgb(0,0,0)";
    }
  }
  inputStr += ")";

  console.log("Testing " + inputStr);

  let {stops} = parseGradient({value: inputStr}, {
    x: 0,
    y: 0,
    width: 200,
    height: 200
  });

  if (stops.length !== output.length) {
    console.error("Unexpected number of stops. Expected " + output.length + ". Got " + stops.length);
  }

  for (let i = 0; i < stops.length; i ++) {
    let stop = stops[i];
    let expected = output[i];
    if (stop.position !== expected) {
      console.error("Unexpected stop " + i + " position. Expected " + expected + ". Got " + stop.position);
    }
  }
}
View Compiled

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.