const Shapes = (function () { "use strict"; /*************/ /* UTILITIES */ /*************/ /** * isDigit */ function isDigit(string, i) { return CharCode.isDigit(string.charCodeAt(i)); } /** * indexOfLongestSuffixSansDigits */ function indexOfLongestSuffixSansDigits(string) { let i = string.length - 1; while (i > 0 && !isDigit(string, i)) { i--; } return i + 1; } /** * CharCode */ const CharCode = { ZERO: '0'.charCodeAt(0), NINE: '9'.charCodeAt(0), isDigit: function(code) { return CharCode.ZERO <= code && code <= CharCode.NINE; } }; /** * Measure */ const Measure = { indexOfUnits: function (string) { return indexOfLongestSuffixSansDigits(string); }, parseMagnitude: function (value) { const number = parseFloat(value); if (isFinite(number)) return number < 0 ? 0 : number; else return undefined; }, separate: function (string) { const i = Measure.indexOfUnits(string); return { magnitude: Measure.parseMagnitude(string.slice(0, i)), units: string.slice(i) }; } }; /** * FunctionCall */ const FunctionCall = { arguments: { value: function (string, fname) { if (fname === undefined) { fname = ""; } const s = fname + '('; const i = string.indexOf(s); const j = string.indexOf(')', i); return i < 0 || j < 0 ? "" : string.substring(i + s.length, j); } } }; /********************************/ /* FACADE CONSTRUCTOR FUNCTIONS */ /********************************/ /** * FunctionCall.Facade */ FunctionCall.Facade = function (descriptor) { Facade.call(this, getArgs, setArgs); const fname = descriptor.functionName; function getArgs() { return FunctionCall.arguments(descriptor.mapFrom(), fname); } function setArgs(value) { descriptor.mapTo(fname + '(' + value + ')'); } } /** * Measure.Facade */ Measure.Facade = function (descriptor) { Facade.call(this, getMagnitude, setMeasure); function getMagnitude() { return Measure.separate(stringifyProperty()).magnitude; } function getUnits() { return Measure.separate(stringifyProperty()).units; } function setMeasure(value) { const fork = Measure.separate(value.toString()); if (fork.units.length > 0) setProperties(fork.magnitude + fork.units); else setProperties(fork.magnitude + getUnits()); } function setProperties(value) { descriptor.mapTo(value); } function getProperty() { return descriptor.mapFrom(); } function stringifyProperty() { const value = getProperty(); if (value === undefined || value === null) return ""; else return value.toString(); } } /** * Facade */ function Facade(getter, setter) { this.defineProperty = function (obj, prop) { return Object.defineProperty(obj, prop, { get: getter, set: setter }); }; } /********************/ /* FACADE UTILITIES */ /********************/ /** * FunctionCall.Facade.defineProperty */ FunctionCall.Facade.defineProperty = function (obj, prop, descriptor) { Facade.defineProperty.call(new FunctionCall.Facade(descriptor), obj, prop, descriptor); }; /** * FunctionCall.Facade.defineProperties */ FunctionCall.Facade.defineProperties = function (obj, descriptors) { for (let prop in descriptors) FunctionCall.Facade.defineProperty(obj, prop, descriptors[prop]); }; /** * Measure.Facade.defineProperty */ Measure.Facade.defineProperty = function (obj, prop, descriptor) { Facade.defineProperty.call(new Measure.Facade(descriptor), obj, prop, descriptor); }; /** * Measure.Facade.defineProperties */ Measure.Facade.defineProperties = function (obj, descriptors) { for (let prop in descriptors) Measure.Facade.defineProperty(obj, prop, descriptors[prop]); }; /** * Facade.defineProperty */ Facade.defineProperty = function (obj, prop, descriptor) { if (descriptor.mapFrom === undefined) descriptor.mapFrom = () => descriptor.source[prop]; if (descriptor.mapTo === undefined) descriptor.mapTo = (value) => descriptor.source[prop] = value; this.defineProperty(obj, prop); }; /********************/ /* SHAPE PROTOTYPES */ /********************/ /** * Shape.prototype */ Shape.prototype = Object.assign(Object.create(Object.prototype), { constructor: Shape, scrubStyle: function (object) { return typeof object === 'object' ? Object.assign({}, object) : {}; }, defaultStyle: { backgroundColor: 'black', position: "absolute", top: '0px', left: '0px', width: '100px', height: '100px', rotation: '0deg' }, draw: function () { this.appendTo(); return this; }, drawOn: function(element) { this.appendTo(element); return this; } }); /** * Polygon.prototype */ Polygon.prototype = Object.create(Shape.prototype); /** * Triangle.prototype */ Triangle.prototype = Object.assign(Object.create(Polygon.prototype), { constructor: Triangle, defaultStyle: Object.assign({}, Shape.prototype.defaultStyle, { path: '0% 100%, 50% 0%, 100% 100%' }) }); /** * Rectangle.prototype */ Rectangle.prototype = Object.assign(Object.create(Polygon.prototype), { constructor: Rectangle }); /** * Square.prototype */ Square.prototype = Object.assign(Object.create(Rectangle.prototype), { constructor: Square, scrubStyle: function (object) { const obj = Shape.prototype.scrubStyle(object); if (obj.width !== undefined) obj.height = obj.width; if (obj.height !== undefined) obj.width = obj.height; return obj; } }); /** * Oval.prototype */ Oval.prototype = Object.assign(Object.create(Shape.prototype), { constructor: Oval, scrubStyle: function (object) { const obj = Shape.prototype.scrubStyle(object); obj.borderRadius = "50%"; return obj; } }); /** * Circle.prototype */ Circle.prototype = Object.assign(Object.create(Oval.prototype), { constructor: Circle, scrubStyle: function (object) { const obj = Oval.prototype.scrubStyle(object); if (obj.width !== undefined) obj.height = obj.width; if (obj.height !== undefined) obj.width = obj.height; return obj; } }); /****************************************/ /* ABSTRACT SHAPE CONSTRUCTOR FUNCTIONS */ /****************************************/ /** * Polygon */ function Polygon(n, descriptor) { /* Private Fields */ const style = Rectangle.call(this, descriptor, {isParent: true}); const vertexCount = n; const thisPolygon = this; /* Public Properties and Methods */ Object.defineProperties(this, { x: { value: (i) => thisPolygon[toX(i)] }, y: { value: (i) => thisPolygon[toY(i)] } }); FunctionCall.Facade.defineProperty(this, 'path', { source: style, functionName: 'polygon', mapTo: function (value) { style.clipPath = value; }, mapFrom: function () { return style.clipPath; } }); for (let i = 1; i <= vertexCount; i++) { Measure.Facade.defineProperty(this, toX(i), { source: this, mapFrom: () => getX(i), mapTo: (value) => thisPolygon.path = setX(i, value) }); Measure.Facade.defineProperty(this, toY(i), { source: this, mapFrom: () => getY(i), mapTo: (value) => thisPolygon.path = setY(i, value) }); } return (this.constructor === Polygon) ? Object.freeze(this.style.reset()) : style; /* Private Functions */ function toX(i) { return 'x' + i; } function toY(i) { return 'y' + i; } function getX(i) { const coords = getCoordsArray(); return coords[0][i-1]; } function getY(i) { const coords = getCoordsArray(); return coords[1][i-1]; } function scrubCoord(value, prop) { value = value.toString(); const numValue = parseFloat(value); return Number.isFinite(numValue) ? numValue.toString() + prop.units : value; } function setX(i, value) { const coords = getCoordsArray(); const prop = thisPolygon[toX(i)]; coords[0][i-1] = scrubCoord(value, prop); return coordsArrayToString(coords); } function setY(i, value) { const coords = getCoordsArray(); const prop = thisPolygon[toY(i)]; coords[1][i-1] = scrubCoord(value, prop); return coordsArrayToString(coords); } function getCoordsArray() { const xCoords = [], yCoords = []; const coords = [xCoords, yCoords]; const xyCoords = thisPolygon.path.split(', '); for (let i = 0; i < vertexCount; i++) { const xy = xyCoords[i].split(' '); const len = xy.length; xCoords[i] = len > 0 && xy[0].length > 0 ? xy[0] : "0%"; yCoords[i] = len > 1 && xy[1].length > 0 ? xy[1] : "0%"; } return coords; } function coordsArrayToString(coords) { const xCoords = coords[0], yCoords = coords[1]; let string = xCoords[0] + ' ' + yCoords[0]; for (let i = 1; i < vertexCount; i++) { string += ', ' + xCoords[i] + ' ' + yCoords[i]; } return string; } } /** * Shape */ function Shape(descriptor) { /* Private and Protected Fields */ const thisShape = this; const fauxStyle = Object.create(null); const element = document.createElement('div'); const style = element.style; // protected /* Public Properties */ Object.defineProperty(this, 'style', { get: getStyle, set: setStyle }); Shape.simpleMeasureProperties.forEach(function (propName) { Measure.Facade.defineProperty(thisShape, propName, {source: style}); }); FunctionCall.Facade.defineProperty(fauxStyle, 'transform', { source: style, functionName: 'rotate' }); Measure.Facade.defineProperties(this, { rotation: { source: fauxStyle, mapTo: function (value) { fauxStyle.transform = value; }, mapFrom: function () { return fauxStyle.transform; } } }); /* Public Methods */ this.appendTo = function (other) { if (other === undefined) other = document.body; other.append(element); return thisShape; }; this.append = function (other) { if (other instanceof Shape) other.appendTo(element); else element.append(other); return thisShape; }; this.remove = function () { element.remove(); }; this.toString = function () { return thisShape.constructor.name; }; Shape.simpleProperties.forEach(function (propName) { Object.defineProperty(thisShape, propName, { get: () => style[propName], set: (value) => style[propName] = value }); }); /* Return this or style. */ return (this.constructor === Shape) ? Object.freeze(this.style.reset()) : style; /* Private Functions */ function updateStyle(object) { const obj = thisShape.scrubStyle(object); for (let prop in obj) { if (style.hasOwnProperty(prop)) style[prop] = obj[prop]; if (thisShape.hasOwnProperty(prop)) thisShape[prop] = obj[prop]; } return thisShape; } function resetStyle() { clearStyle(); thisShape.style = thisShape.defaultStyle; thisShape.style.update(descriptor); return thisShape; } function clearStyle() { const n = style.length; for (let i = 0; i < n; i++) { style[i] = ""; } return thisShape; } function setStyle(object) { clearStyle(); updateStyle(object); return thisShape; } function getStyle() { return { update: updateStyle, clear: clearStyle, reset: resetStyle, toString: function () { return style.cssText; }, valueOf: function (prop) { if (prop === undefined) return Object.assign({}, style); else return style[prop]; } }; } } Shape.simpleProperties = [ 'backgroundColor', 'borderStyle', 'borderColor' ]; Shape.simpleMeasureProperties = [ 'top', 'left', 'borderWidth' ]; /****************************************/ /* CONCRETE SHAPE CONSTRUCTOR FUNCTIONS */ /****************************************/ /** * Triangle */ function Triangle(descriptor) { const style = Polygon.call(this, 3, descriptor); return (this.constructor === Triangle) ? Object.freeze(this.style.reset()) : style; } /** * Oval */ function Oval(descriptor) { const style = Rectangle.call(this, descriptor); return (this.constructor === Oval) ? Object.freeze(this.style.reset()) : style; } /** * Circle */ function Circle(descriptor) { const style = Square.call(this, descriptor); Measure.Facade.defineProperties(this, { diameter: { mapTo: function (value) { style.width = value; style.height = value; }, mapFrom: function () { return style.width; } }, radius: { mapTo: (function (value) { this.diameter = value; const mag = this.diameter.magnitude; if (Number.isFinite(mag)) this.diameter.magnitude = mag * 2; }).bind(this), mapFrom: (function () { const mag = this.diameter.magnitude; return (Number.isFinite(mag)) ? (mag / 2) + this.diameter.units : ""; }).bind(this) } }); return (this.constructor === Circle) ? Object.freeze(this.style.reset()) : style; } /** * Rectangle */ function Rectangle(descriptor) { const style = Shape.call(this, descriptor); Measure.Facade.defineProperties(this, { width: {source: style}, height: {source: style} }); return (this.constructor === Rectangle) ? Object.freeze(this.style.reset()) : style; } /** * Square */ function Square(descriptor) { const style = Shape.call(this, descriptor); Measure.Facade.defineProperties(this, { width: { mapTo: function (value) { style.width = value; style.height = value; }, mapFrom: function () { return style.width; } }, height: { mapTo: function (value) { style.width = value; style.height = value; }, mapFrom: function () { return style.height; } } }); return (this.constructor === Square) ? Object.freeze(this.style.reset()) : style; } /**************************/ /* FREEZE PRIVATE OBJECTS */ /**************************/ Object.freeze(CharCode); Object.freeze(Facade) Object.freeze(FunctionCall); Object.freeze(FunctionCall.Facade); Object.freeze(Measure); Object.freeze(Measure.Facade); Object.freeze(Shape); Object.freeze(Polygon); /************************************/ /* FREEZE AND RETURN PUBLIC OBJECTS */ /************************************/ return Object.freeze({ Triangle: Object.freeze(Triangle), Rectangle: Object.freeze(Rectangle), Square: Object.freeze(Square), Circle: Object.freeze(Circle), Oval: Object.freeze(Oval) }); })(); /***************/ /* SHORT NAMES */ /***************/ const Triangle = Shapes.Triangle, Rectangle = Shapes.Rectangle, Square = Shapes.Square, Circle = Shapes.Circle, Oval = Shapes.Oval;