Shapes

/* Basic Shapes */

function Rectangle(width, height, borderRadius, borderRadiusUnits) {
  const shape = new Shape();
  shape.width = width;
  shape.height = height;
  shape.borderRadius = borderRadius;
  shape.borderRadiusUnits = borderRadiusUnits;
  return shape;
}

function Square(width, borderRadius, borderRadiusUnits) {
  return Rectangle(width, width, borderRadius, borderRadiusUnits);
}

function Oval(width, height) {
  return Rectangle(width, height, 50, "%");
}

function Circle(diameter) {
  return Oval(diameter, diameter);
}

function Triangle(width, height, color) {
  const e = document.createElement("div");
  e.style.width = 0;
  e.style.height = 0;
  e.style.position = "absolute";
  e.style.borderLeft = width + "px solid transparent";
  e.style.borderRight = width + "px solid transparent";
  e.style.borderBottom = height + "px solid " + color;
  const shape = new Shape(e);
  return shape;  
}

/** Draws a shape.
 *  Options: container, id, color, left, top, units.
 *  Example: draw(Circle(20), {left: 10, units: "mm", color: "red"});
 */
function draw(shape, options) {

  if (options === undefined)
    options = {};
  if (options.container === undefined)
    options.container = document.body;
  if (options.id !== undefined)
    shape.id = options.id;
  if (options.color !== undefined)
    shape.backgroundColor = options.color;
  if (options.left !== undefined)
    shape.left = options.left;
  if (options.top !== undefined)
    shape.top = options.top;
  if (options.units !== undefined)
    shape.positionUnits = options.units;

  return shape.appendToElement(options.container);
}

/** An ordered pair of values. */
function OrderedPair(value1, value2) {

  /* Argument Validation */
  value1 = this.vetInitial(value1, this.defaultValue);
  value2 = this.vetInitial(value2, this.defaultValue);

  /* Public Properties */
  Object.defineProperty(this, "value1", {get: getValue1, set: setValue1});
  Object.defineProperty(this, "value2", {get: getValue2, set: setValue2});

  /* Public Methods */
  this.equals = function(other) {
    return other instanceof OrderedPair &&
      other.value1 === value1 && other.value2 === value2;
  };
  this.toString = function() {
    return "(" + value1 + "," + value2 + ")";
  };

  /* Private Functions */
  function getValue1() { return value1; }
  function getValue2() { return value2; }
  function setValue1(value) { value1 = this.vetUpdated(value, value1); }
  function setValue2(value) { value2 = this.vetUpdated(value, value2); }
}
OrderedPair.prototype = {
  vetInitial: (val, alt) => val !== undefined ? val : alt,
  vetUpdated: (val, alt) => val,
  defaultValue: null
};

/** A readonly ordered pair of values. */
function ReadonlyOrderedPair(value1, value2) {
  OrderedPair.call(this, value1, value2);
}
ReadonlyOrderedPair.prototype = Object.create(OrderedPair.prototype);
ReadonlyOrderedPair.prototype.vetUpdated = (newValue, oldValue) => oldValue;

/** An ordered pair of finite numbers. */
function CoordPair(value1, value2) {
  OrderedPair.call(this, value1, value2);
}
CoordPair.vetValue = function(value, alternate) {
  if (Number.isFinite(value)) return value;
  else if (Number.isFinite(alternate)) {
    return alternate;
  }
  throw Error("Not finite: " + value + " or " + alternate);
}
CoordPair.prototype = Object.create(OrderedPair.prototype);
CoordPair.prototype.vetInitial = CoordPair.vetValue;
CoordPair.prototype.vetUpdated = CoordPair.vetValue;
CoordPair.prototype.defaultValue = 0;

/** A pair of Cartesian coordinates. */
function XYCoords(x, y) {

  CoordPair.call(this, x, y);

  /* Public Properties */
  Object.defineProperty(this, "x", {
    get: () => this.value1,
    set: (value) => this.value1 = value
  });
  Object.defineProperty(this, "y", {
    get: () => this.value2,
    set: (value) => this.value2 = value
  });

  /* Public Methods */
  this.dist = (other) => XYCoords.dist(this, other);
}
XYCoords.dist = function(p1, p2) {
  const dx = p2.x - p1.x;
  const dy = p2.y - p1.y;
  return Math.sqrt(dx*dx + dy*dy);
};
XYCoords.prototype = Object.create(CoordPair.prototype);

/** HTML units of measure. */
const WebUnits = {
  defaultUnits: "px",
  isValid: (value) => WebUnits.toArray().indexOf(value) >= 0,
  toArray: () => [
    "cm",   // centimeters
    "mm",   // millimeters
    "in",   // inches
    "px",   // pixels (1 pixel = 1/96th of 1 inch)
    "pt",   // points (1 point = 1/72nd of 1 inch)
    "pc",   // picas (1 pica = 12 points)
    "em",   // relative to font size of element
    "ex",   // relative to x-height of font
    "ch",   // relative to width of "0"
    "rem",  // relative to font size of root element
    "vw",   // relative to 1% of width of viewport
    "vh",   // relative to 1% of height of viewport
    "vmin", // relative to 1% of smaller dimension of viewport
    "vmax", // relative to 1% of larger dimension of viewport
    "%"     // relative to parent element
  ],
  toString: () => WebUnits.toArray().toString(),
  vet: (value, alternate) => WebUnits.isValid(value) ? value : alternate
}

/** HTML color names. */
const WebColors = {
  defaultColor: "black",
  isValid: (value) => WebColors.toArray().indexOf(value) >= 0,
  random: function() {
    const a = WebColors.toArray();
    const n = a.length;
    const i = Math.floor(Math.random() * n);
    return a[i];
  },
  toArray: () => ["white", "silver", "gray", "black", "red", "maroon",
    "yellow", "olive", "lime", "green", "aqua", "teal", "blue", "navy",
    "fuchsia", "purple", "orange", "pink", "brown", "tan"
  ],
  toString: () => WebUnits.toArray().toString(),
  vet: (value, alternate) => WebColors.isValid(value) ? value : alternate
}

/** A pair of coordinates with units. */
function WebpageCoords(x, y, units) {
  const coords = new XYCoords(x, y);
  units = WebUnits.vet(units, WebUnits.defaultUnits);
  Object.defineProperty(this, "x", {get: getX, set: setX});
  Object.defineProperty(this, "y", {get: getY, set: setY});
  Object.defineProperty(this, "xPlusUnits", {get: xPlusUnits});
  Object.defineProperty(this, "yPlusUnits", {get: yPlusUnits});
  Object.defineProperty(this, "units", {get: getUnits, set: setUnits});
  function getX() { return coords.x; }
  function getY() { return coords.y; }
  function setX(value) { coords.x = value; }
  function setY(value) { coords.y = value; }
  function xPlusUnits() { return coords.x.toString() + units; }
  function yPlusUnits() { return coords.y.toString() + units; }
  function getUnits() { return units; }
  function setUnits(value) { units = WebUnits.vet(value, units); }
  this.equals = (other) => this.units === other.units &&
    this.x === other.x && this.y === other.y; 
  this.toString = () => "(" + xPlusUnits() + "," + yPlusUnits() + ")";
}

/** A shape that can be drawn on a webpage. */
function Shape(e) {

  /* Private Values */
  const position = new WebpageCoords(0,0);
  let element = createPositionableElement(),
      sizeUnits = position.units,
      borderRadiusUnits = "%",
      width = 1, height = 1,
      borderRadius = 0,
      backgroundColor = "black";

  /* Private Functions */
  function createPositionableElement() {
    const e = document.createElement("div");
    e.style.position = "absolute";
    return e;
  }
  function updateBorder() {
    element.style.borderRadius = borderRadius.toString() + borderRadiusUnits;
  }
  function updateSize() {
    element.style.width = width.toString() + sizeUnits;
    element.style.height = height.toString() + sizeUnits;
  }
  function updatePosition() {
    element.style.top = position.yPlusUnits;
    element.style.left = position.xPlusUnits;
  }
  function updateColoring() {
    element.style.backgroundColor = backgroundColor;
  }

  /* Public Properties*/
  Object.defineProperty(this, "id", {
    get: function() { return element.id; },
    set: function(value) { element.id = value; }
  });
  Object.defineProperty(this, "left", {
    get: function() { return position.x; },
    set: function(value) {
      position.x = value;
      updatePosition();
    }
  });
  Object.defineProperty(this, "top", {
    get: function() {return position.y;},
    set: function(value) {
      position.y = value;
      updatePosition();
    }
  });
  Object.defineProperty(this, "positionUnits", {
    get: function() { return position.units; },
    set: function(value) {
      if (WebUnits.isValid(value)) {
        position.units = value;
        updatePosition();
      }
    }
  });
  Object.defineProperty(this, "borderRadius", {
    get: function() { return borderRadius; },
    set: function(value) {
      if (Number.isFinite(value)) {
        borderRadius = value;
        updateBorder();
      }
    }
  });
  Object.defineProperty(this, "borderRadiusUnits", {
    get: function() { return borderRadiusUnits; },
    set: function(value) {
      if (WebUnits.isValid(value)) {
        borderRadiusUnits = value;
        updateBorder();
      }
    }
  });
  Object.defineProperty(this, "width", {
    get: function() { return width; },
    set: function(value) {
      if (Number.isFinite(value)) {
        width = value;
        updateSize();
      }
    }
  });
  Object.defineProperty(this, "height", {
    get: function() { return height; },
    set: function(value) {
      if (Number.isFinite(value)) {
        height = value;
        updateSize();
      }
    }
  });
  Object.defineProperty(this, "sizeUnits", {
    get: function() { return sizeUnits; },
    set: function(value) {
      if (WebUnits.isValid(value)) {
        sizeUnits = value;
        updateSize();
      }
    }
  });
  Object.defineProperty(this, "backgroundColor", {
    get: function() { return backgroundColor; },
    set: function(value) {
      if (WebColors.isValid(value)) {
        backgroundColor = value;
        updateColoring();
      }
    }
  });

  /* Public Methods */
  this.appendToElement = (function(containerElement) {
    containerElement.append(element);
    return this;
  }).bind(this);
  this.removeFromElement = function(containerElement) {
    if (containerElement.contains(element))
      containerElement.remove(element);
    element = createPositionableElement();
  };

  /* Initialization */
  updateColoring();
  updateBorder();
  updateSize();
  updatePosition();
  if (e !== undefined) element = e;
}

/* Portfolio */

/* There And Back Again; or, The Hobbit */
function thereAndBackAgain(r, h, k, c1, c2) {
  function f(m) {
    return (x) => k + m * Math.sqrt(r*r - (x-h)*(x-h));
  }
  plotXY(f(1), r+h, -r+h, -1, c1, Circle, 8);    
  plotXY(f(-1), -r+h, r+h, 1, c2, Circle, 8);
  function plotXY(f, from, to, step, color, penShape, penWidth) {
    // Don't step over it, Baggins.
    for (let x = from; x != to; x += step)
      draw(penShape(penWidth), {left: x, top: f(x), color: color});
  }
}

/* Here We Go Round the Mulberry Bush */
function hereWeGoRound(r, h, k, c1, c2) {
  // The Mulberry Bush is at (h, k).
  const PI = Math.PI, DEG = 2*PI/360;
  const x = (a) => h + r * Math.cos(a);
  const y = (a) => k + r * Math.sin(a);
  plotPolar(y, x, -PI, 0, DEG, c1, Circle, 8);
  plotPolar(y, x, 0, PI, DEG, c2, Circle, 8);
  function plotPolar(f, g, from, to, step, color, penShape, penWidth) {
    for (let a = from; a < to; a += step)
      draw(penShape(penWidth), {left: g(a), top: f(a), color: color});
  }
}

/* Le Rouge et le Noir */
function theRedAndTheBlack() {
  const r = 125, c1 = "red", c2 = "black";
  const delta = 0.02, times = 7;
  thereAndBackAgain(125, 150, 150, c1, c2);
  hereWeGoRound(125, 150, 425, c1, c2);
}

/** riverrun past Eve and Adam's
 *  from swerve of shore to bend of bay
 */
function riverRun(r, h, k, d, t) {
  // Environs.
  const PI = Math.PI, DEG = 2*PI/360;
  const x = (a) => h + (r -= d) * Math.cos(a);
  const y = (a) => k + (r -= d) * Math.sin(a);
  while (t-- > 0) {
    plotPolar(y, x, -PI, 0, DEG, WebColors.random(), Circle, 8);
    plotPolar(y, x, 0, PI, DEG, WebColors.random(), Circle, 8);
  }
  function plotPolar(f, g, from, to, step, color, penShape, penWidth) {
    for (let a = from; a < to; a += step)
      draw(penShape(penWidth), {left: g(a), top: f(a), color: color});
  }
}

function drawingNo1(left, top) {
  const margin = {left: 10, top: 20};
  const start = {left: left, top: top};
  for (let i=0, j=0; i < 100; i+=3, j+=2) {
    const shape = Circle(200-i);
    const options = {
      id: "rec-" + i,
      color: WebColors.random(),
      left: start.left + i + margin.left,
      top: start.top + j + margin.top
    };
    draw(shape, options);
  }
}

function drawingNo2(n) {
  for (let i = 0; i < n; i++)
    drawingNo1(Math.random()*800, Math.random()*600);
}

function drawingNo3(n) {
  for (let i = 0; i < n; i++)
    riverRun(125, 200+Math.random()*800, 200+Math.random()*600, 0.02, 7);
}