/* 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); }