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;