svg-turtle
Version:
a turtle graphics library with SVG output
770 lines (767 loc) • 34.2 kB
JavaScript
//----------------------------------------------------------------------------//
/**** throwableError - simplifies construction of named errors ****/
function throwableError(Message) {
var Match = /^([$a-zA-Z][$a-zA-Z0-9]*):\s*(\S.+)\s*$/.exec(Message);
if (Match == null) {
return new Error(Message);
}
else {
var namedError = new Error(Match[2]);
namedError.name = Match[1];
return namedError;
}
}
/**** throwError - throws a named error ****/
function throwError(Message) {
throw throwableError(Message);
}
/**** ValueIsNumber ****/
function ValueIsNumber(Value) {
return (typeof Value === 'number') || (Value instanceof Number);
}
/**** ValueIsFiniteNumber (pure "isFinite" breaks on objects) ****/
function ValueIsFiniteNumber(Value) {
return ((typeof Value === 'number') || (Value instanceof Number)) && isFinite(Value.valueOf());
}
/**** ValueIsNumberInRange ****/
function ValueIsNumberInRange(Value, minValue, maxValue, withMin, withMax) {
if (withMin === void 0) { withMin = true; }
if (withMax === void 0) { withMax = true; }
if (!ValueIsNumber(Value) || isNaN(Value)) {
return false;
}
if (ValueIsFiniteNumber(minValue)) { // more robust than "isFinite" alone
if (ValueIsFiniteNumber(maxValue)) { // more robust than "isFinite" alone
if ((Value < minValue) || (!withMin && (Value === minValue)) ||
(Value > maxValue) || (!withMax && (Value === maxValue))) {
return false;
}
}
else {
if ((Value < minValue) || (!withMin && (Value === minValue))) {
return false;
}
}
}
else {
if (ValueIsFiniteNumber(maxValue)) { // more robust than "isFinite" alone
if ((Value > maxValue) || (!withMax && (Value === maxValue))) {
return false;
}
}
}
return true;
}
/**** ValueIsString ****/
function ValueIsString(Value) {
return (typeof Value === 'string') || (Value instanceof String);
}
/**** ValueIsPlainObject ****/
function ValueIsPlainObject(Value) {
return ((Value != null) && (typeof Value === 'object') &&
(Object.getPrototypeOf(Value) === Object.prototype));
}
/**** ValueIsOneOf ****/
function ValueIsOneOf(Value, ValueList) {
return (ValueList.indexOf(Value) >= 0);
} // no automatic unboxing of boxed values and vice-versa!
/**** ValueIsColor ****/
function ValueIsColor(Value) {
return ValueIsString(Value) && (ColorSet.hasOwnProperty(Value) ||
/^#[a-fA-F0-9]{6}$/.test(Value) ||
/^#[a-fA-F0-9]{8}$/.test(Value) ||
/^rgb\([0-9]+,\s*[0-9]+,\s*[0-9]+\)$/.test(Value) || // not perfect
/^rgba\([0-9]+,\s*[0-9]+,\s*[0-9]+,([01]|[0]?[.][0-9]+)\)$/.test(Value) // dto.
);
}
//------------------------------------------------------------------------------
//-- Argument Validation Functions --
//------------------------------------------------------------------------------
var rejectNil = false;
var acceptNil = true;
/**** validatedArgument ****/
function validatedArgument(Description, Argument, ValueIsValid, NilIsAcceptable, Expectation) {
if (Argument == null) {
if (NilIsAcceptable) {
return Argument;
}
else {
throwError("MissingArgument: no " + escaped(Description) + " given");
}
}
else {
if (ValueIsValid(Argument)) {
switch (true) {
case Argument instanceof Boolean:
case Argument instanceof Number:
case Argument instanceof String:
return Argument.valueOf(); // unboxes any primitives
default:
return Argument;
}
}
else {
throwError("InvalidArgument: the given " + escaped(Description) + " is no valid " + escaped(Expectation));
}
}
}
/**** ValidatorForClassifier ****/
function ValidatorForClassifier(Classifier, NilIsAcceptable, Expectation) {
var Validator = function (Description, Argument) {
return validatedArgument(Description, Argument, Classifier, NilIsAcceptable, Expectation);
};
var ClassifierName = Classifier.name;
if ((ClassifierName != null) && /^ValueIs/.test(ClassifierName)) {
var ValidatorName = ClassifierName.replace(// derive name from validator
/^ValueIs/, NilIsAcceptable ? 'allow' : 'expect');
return FunctionWithName(Validator, ValidatorName);
}
else {
return Validator; // without any specific name
}
}
/**** FunctionWithName (works with older JS engines as well) ****/
function FunctionWithName(originalFunction, desiredName) {
if (originalFunction == null) {
throwError('MissingArgument: no function given');
}
if (typeof originalFunction !== 'function') {
throwError('InvalidArgument: the given 1st Argument is not a JavaScript function');
}
if (desiredName == null) {
throwError('MissingArgument: no desired name given');
}
if ((typeof desiredName !== 'string') && !(desiredName instanceof String)) {
throwError('InvalidArgument: the given desired name is not a string');
}
if (originalFunction.name === desiredName) {
return originalFunction;
}
try {
Object.defineProperty(originalFunction, 'name', { value: desiredName });
if (originalFunction.name === desiredName) {
return originalFunction;
}
}
catch (signal) { /* ok - let's take the hard way */ }
var renamed = new Function('originalFunction', 'return function ' + desiredName + ' () {' +
'return originalFunction.apply(this,Array.prototype.slice.apply(arguments))' +
'}');
return renamed(originalFunction);
} // also works with older JavaScript engines
/**** allow/expect[ed]FiniteNumber ****/
var allowFiniteNumber = /*#__PURE__*/ ValidatorForClassifier(ValueIsFiniteNumber, acceptNil, 'finite numeric value');
var expectFiniteNumber = /*#__PURE__*/ ValidatorForClassifier(ValueIsFiniteNumber, rejectNil, 'finite numeric value');
/**** allow[ed]OneOf ****/
function allowOneOf(Description, Argument, ValueList) {
return (Argument == null
? Argument
: expectedOneOf(Description, Argument, ValueList));
}
/**** expect[ed]OneOf ****/
function expectOneOf(Description, Argument, ValueList) {
if (Argument == null) {
throwError("MissingArgument: no " + escaped(Description) + " given");
}
if (ValueIsOneOf(Argument, ValueList)) {
return ( // unboxes any primitives
(Argument == null) || (typeof Argument.valueOf !== 'function')
? Argument
: Argument.valueOf());
}
else {
throwError("InvalidArgument: the given " + escaped(Description) + " is not among the supported values");
}
}
var expectedOneOf = expectOneOf;
/**** escaped - escapes all control characters in a given string ****/
function escaped(Text) {
var EscapeSequencePattern = /\\x[0-9a-zA-Z]{2}|\\u[0-9a-zA-Z]{4}|\\[0bfnrtv'"\\\/]?/g;
var CtrlCharCodePattern = /[\x00-\x1f\x7f-\x9f]/g;
return Text
.replace(EscapeSequencePattern, function (Match) {
return (Match === '\\' ? '\\\\' : Match);
})
.replace(CtrlCharCodePattern, function (Match) {
switch (Match) {
case '\0': return '\\0';
case '\b': return '\\b';
case '\f': return '\\f';
case '\n': return '\\n';
case '\r': return '\\r';
case '\t': return '\\t';
case '\v': return '\\v';
default: {
var HexCode = Match.charCodeAt(0).toString(16);
return '\\x' + '00'.slice(HexCode.length) + HexCode;
}
}
});
}
//------------------------------------------------------------------------------
//-- Color Utilities --
//------------------------------------------------------------------------------
// built-in color names (see http://www.w3.org/TR/SVG/types.html#ColorKeywords) ----
var ColorSet = {
transparent: 'rgba(0,0,0,0,0.0)',
aliceblue: 'rgba(240,248,255,1.0)', lightpink: 'rgba(255,182,193,1.0)',
antiquewhite: 'rgba(250,235,215,1.0)', lightsalmon: 'rgba(255,160,122,1.0)',
aqua: 'rgba(0,255,255,1.0)', lightseagreen: 'rgba(32,178,170,1.0)',
aquamarine: 'rgba(127,255,212,1.0)', lightskyblue: 'rgba(135,206,250,1.0)',
azure: 'rgba(240,255,255,1.0)', lightslategray: 'rgba(119,136,153,1.0)',
beige: 'rgba(245,245,220,1.0)', lightslategrey: 'rgba(119,136,153,1.0)',
bisque: 'rgba(255,228,196,1.0)', lightsteelblue: 'rgba(176,196,222,1.0)',
black: 'rgba(0,0,0,1.0)', lightyellow: 'rgba(255,255,224,1.0)',
blanchedalmond: 'rgba(255,235,205,1.0)', lime: 'rgba(0,255,0,1.0)',
blue: 'rgba(0,0,255,1.0)', limegreen: 'rgba(50,205,50,1.0)',
blueviolet: 'rgba(138,43,226,1.0)', linen: 'rgba(250,240,230,1.0)',
brown: 'rgba(165,42,42,1.0)', magenta: 'rgba(255,0,255,1.0)',
burlywood: 'rgba(222,184,135,1.0)', maroon: 'rgba(128,0,0,1.0)',
cadetblue: 'rgba(95,158,160,1.0)', mediumaquamarine: 'rgba(102,205,170,1.0)',
chartreuse: 'rgba(127,255,0,1.0)', mediumblue: 'rgba(0,0,205,1.0)',
chocolate: 'rgba(210,105,30,1.0)', mediumorchid: 'rgba(186,85,211,1.0)',
coral: 'rgba(255,127,80,1.0)', mediumpurple: 'rgba(147,112,219,1.0)',
cornflowerblue: 'rgba(100,149,237,1.0)', mediumseagreen: 'rgba(60,179,113,1.0)',
cornsilk: 'rgba(255,248,220,1.0)', mediumslateblue: 'rgba(123,104,238,1.0)',
crimson: 'rgba(220,20,60,1.0)', mediumspringgreen: 'rgba(0,250,154,1.0)',
cyan: 'rgba(0,255,255,1.0)', mediumturquoise: 'rgba(72,209,204,1.0)',
darkblue: 'rgba(0,0,139,1.0)', mediumvioletred: 'rgba(199,21,133,1.0)',
darkcyan: 'rgba(0,139,139,1.0)', midnightblue: 'rgba(25,25,112,1.0)',
darkgoldenrod: 'rgba(184,134,11,1.0)', mintcream: 'rgba(245,255,250,1.0)',
darkgray: 'rgba(169,169,169,1.0)', mistyrose: 'rgba(255,228,225,1.0)',
darkgreen: 'rgba(0,100,0,1.0)', moccasin: 'rgba(255,228,181,1.0)',
darkgrey: 'rgba(169,169,169,1.0)', navajowhite: 'rgba(255,222,173,1.0)',
darkkhaki: 'rgba(189,183,107,1.0)', navy: 'rgba(0,0,128,1.0)',
darkmagenta: 'rgba(139,0,139,1.0)', oldlace: 'rgba(253,245,230,1.0)',
darkolivegreen: 'rgba(85,107,47,1.0)', olive: 'rgba(128,128,0,1.0)',
darkorange: 'rgba(255,140,0,1.0)', olivedrab: 'rgba(107,142,35,1.0)',
darkorchid: 'rgba(153,50,204,1.0)', orange: 'rgba(255,165,0,1.0)',
darkred: 'rgba(139,0,0,1.0)', orangered: 'rgba(255,69,0,1.0)',
darksalmon: 'rgba(233,150,122,1.0)', orchid: 'rgba(218,112,214,1.0)',
darkseagreen: 'rgba(143,188,143,1.0)', palegoldenrod: 'rgba(238,232,170,1.0)',
darkslateblue: 'rgba(72,61,139,1.0)', palegreen: 'rgba(152,251,152,1.0)',
darkslategray: 'rgba(47,79,79,1.0)', paleturquoise: 'rgba(175,238,238,1.0)',
darkslategrey: 'rgba(47,79,79,1.0)', palevioletred: 'rgba(219,112,147,1.0)',
darkturquoise: 'rgba(0,206,209,1.0)', papayawhip: 'rgba(255,239,213,1.0)',
darkviolet: 'rgba(148,0,211,1.0)', peachpuff: 'rgba(255,218,185,1.0)',
deeppink: 'rgba(255,20,147,1.0)', peru: 'rgba(205,133,63,1.0)',
deepskyblue: 'rgba(0,191,255,1.0)', pink: 'rgba(255,192,203,1.0)',
dimgray: 'rgba(105,105,105,1.0)', plum: 'rgba(221,160,221,1.0)',
dimgrey: 'rgba(105,105,105,1.0)', powderblue: 'rgba(176,224,230,1.0)',
dodgerblue: 'rgba(30,144,255,1.0)', purple: 'rgba(128,0,128,1.0)',
firebrick: 'rgba(178,34,34,1.0)', red: 'rgba(255,0,0,1.0)',
floralwhite: 'rgba(255,250,240,1.0)', rosybrown: 'rgba(188,143,143,1.0)',
forestgreen: 'rgba(34,139,34,1.0)', royalblue: 'rgba(65,105,225,1.0)',
fuchsia: 'rgba(255,0,255,1.0)', saddlebrown: 'rgba(139,69,19,1.0)',
gainsboro: 'rgba(220,220,220,1.0)', salmon: 'rgba(250,128,114,1.0)',
ghostwhite: 'rgba(248,248,255,1.0)', sandybrown: 'rgba(244,164,96,1.0)',
gold: 'rgba(255,215,0,1.0)', seagreen: 'rgba(46,139,87,1.0)',
goldenrod: 'rgba(218,165,32,1.0)', seashell: 'rgba(255,245,238,1.0)',
gray: 'rgba(128,128,128,1.0)', sienna: 'rgba(160,82,45,1.0)',
green: 'rgba(0,128,0,1.0)', silver: 'rgba(192,192,192,1.0)',
greenyellow: 'rgba(173,255,47,1.0)', skyblue: 'rgba(135,206,235,1.0)',
grey: 'rgba(128,128,128,1.0)', slateblue: 'rgba(106,90,205,1.0)',
honeydew: 'rgba(240,255,240,1.0)', slategray: 'rgba(112,128,144,1.0)',
hotpink: 'rgba(255,105,180,1.0)', slategrey: 'rgba(112,128,144,1.0)',
indianred: 'rgba(205,92,92,1.0)', snow: 'rgba(255,250,250,1.0)',
indigo: 'rgba(75,0,130,1.0)', springgreen: 'rgba(0,255,127,1.0)',
ivory: 'rgba(255,255,240,1.0)', steelblue: 'rgba(70,130,180,1.0)',
khaki: 'rgba(240,230,140,1.0)', tan: 'rgba(210,180,140,1.0)',
lavender: 'rgba(230,230,250,1.0)', teal: 'rgba(0,128,128,1.0)',
lavenderblush: 'rgba(255,240,245,1.0)', thistle: 'rgba(216,191,216,1.0)',
lawngreen: 'rgba(124,252,0,1.0)', tomato: 'rgba(255,99,71,1.0)',
lemonchiffon: 'rgba(255,250,205,1.0)', turquoise: 'rgba(64,224,208,1.0)',
lightblue: 'rgba(173,216,230,1.0)', violet: 'rgba(238,130,238,1.0)',
lightcoral: 'rgba(240,128,128,1.0)', wheat: 'rgba(245,222,179,1.0)',
lightcyan: 'rgba(224,255,255,1.0)', white: 'rgba(255,255,255,1.0)',
lightgoldenrodyellow: 'rgba(250,250,210,1.0)', whitesmoke: 'rgba(245,245,245,1.0)',
lightgray: 'rgba(211,211,211,1.0)', yellow: 'rgba(255,255,0,1.0)',
lightgreen: 'rgba(144,238,144,1.0)', yellowgreen: 'rgba(154,205,50,1.0)',
lightgrey: 'rgba(211,211,211,1.0)',
};
//----------------------------------------------------------------------------//
var TUR_Lineatures = ['solid', 'dotted', 'dashed'];
var TUR_Joins = ['bevel', 'miter', 'round'];
var TUR_Caps = ['butt', 'round', 'square'];
/**** ValueIsPosition ****/
function ValueIsPosition(Value) {
return (ValueIsPlainObject(Value) &&
ValueIsFiniteNumber(Value.x) &&
ValueIsFiniteNumber(Value.y));
}
/**** allow/expect[ed]Position ****/
var allowPosition = ValidatorForClassifier(ValueIsPosition, acceptNil, 'turtle position'), allowedPosition = allowPosition;
var expectPosition = ValidatorForClassifier(ValueIsPosition, rejectNil, 'turtle position'), expectedPosition = expectPosition;
/**** ValueIsAlignment ****/
function ValueIsAlignment(Value) {
return (ValueIsPlainObject(Value) &&
ValueIsFiniteNumber(Value.x) &&
ValueIsFiniteNumber(Value.y) &&
ValueIsFiniteNumber(Value.Direction));
}
/**** allow/expect[ed]Alignment ****/
var allowAlignment = ValidatorForClassifier(ValueIsAlignment, acceptNil, 'turtle alignment'), allowedAlignment = allowAlignment;
var expectAlignment = ValidatorForClassifier(ValueIsAlignment, rejectNil, 'turtle alignment'), expectedAlignment = expectAlignment;
/**** ValueIsPathOptionSet ****/
function ValueIsPathOptionSet(Value) {
return (ValueIsPlainObject(Value) &&
((Value.x == null) || ValueIsFiniteNumber(Value.x)) &&
((Value.y == null) || ValueIsFiniteNumber(Value.y)) &&
((Value.Direction == null) || ValueIsFiniteNumber(Value.Direction)) &&
((Value.Width == null) || ValueIsNumberInRange(Value.Width, 0)) &&
((Value.Color == null) || ValueIsColor(Value.Color)) &&
((Value.Lineature == null) || ValueIsOneOf(Value.Lineature, TUR_Lineatures)) &&
((Value.Join == null) || ValueIsOneOf(Value.Join, TUR_Joins)) &&
((Value.Cap == null) || ValueIsOneOf(Value.Cap, TUR_Caps)));
}
/**** allow/expect[ed]PathOptionSet ****/
var allowPathOptionSet = ValidatorForClassifier(ValueIsPathOptionSet, acceptNil, 'turtle path option set'), allowedPathOptionSet = allowPathOptionSet;
var expectPathOptionSet = ValidatorForClassifier(ValueIsPathOptionSet, rejectNil, 'turtle path option set'), expectedPathOptionSet = expectPathOptionSet;
/**** Graphic ****/
var Graphic = /** @class */ (function () {
function Graphic() {
this.SVGContent = '';
this.currentPath = undefined;
this.currentX = 0;
this.currentY = 0;
this.currentDirection = 0;
this.currentWidth = 1;
this.currentColor = '#000000';
this.currentLineature = 'solid';
this.currentJoin = 'round';
this.currentCap = 'round';
}
/**** _initialize ****/
Graphic.prototype._initialize = function () {
if (this.currentX == null) {
this.currentX = 0;
}
if (this.currentY == null) {
this.currentY = 0;
}
if (this.currentDirection == null) {
this.currentDirection = 0;
}
if (this.currentWidth == null) {
this.currentWidth = 1;
}
if (this.currentColor == null) {
this.currentColor = '#000000';
}
if (this.currentLineature == null) {
this.currentLineature = 'solid';
}
if (this.currentJoin == null) {
this.currentJoin = 'round';
}
if (this.currentCap == null) {
this.currentCap = 'round';
}
};
/**** reset ****/
Graphic.prototype.reset = function () {
this.currentX = 0;
this.currentY = 0;
this.currentDirection = 0;
this.currentWidth = 1;
this.currentColor = '#000000';
this.currentLineature = 'solid';
this.currentJoin = 'round';
this.currentCap = 'round';
return this;
};
/**** beginPath ****/
Graphic.prototype.beginPath = function (PathOptionSet) {
allowPathOptionSet('option set', PathOptionSet);
if (this.currentPath != null) {
this.endPath();
}
this._initialize();
if (PathOptionSet != null) {
if (PathOptionSet.x != null) {
this.currentX = PathOptionSet.x;
}
if (PathOptionSet.y != null) {
this.currentY = PathOptionSet.y;
}
if (PathOptionSet.Direction != null) {
this.currentDirection = PathOptionSet.Direction;
}
if (PathOptionSet.Width != null) {
this.currentWidth = PathOptionSet.Width;
}
if (PathOptionSet.Color != null) {
this.currentColor = PathOptionSet.Color;
}
if (PathOptionSet.Lineature != null) {
this.currentLineature = PathOptionSet.Lineature;
}
if (PathOptionSet.Join != null) {
this.currentJoin = PathOptionSet.Join;
}
if (PathOptionSet.Cap != null) {
this.currentCap = PathOptionSet.Cap;
}
}
if (this.minX == null) {
this.minX = this.maxX = this.currentX;
this.minY = this.maxY = this.currentY;
}
this.currentPath = '<path ' +
'fill="none" ' +
'stroke="' + this.currentColor + '" ' +
'stroke-width="' + this.currentWidth + '" ' +
'stroke-linejoin="' + this.currentJoin + '" ' +
'stroke-linecap="' + this.currentCap + '" ';
switch (this.currentLineature) {
case 'dotted':
this.currentPath += 'stroke-dasharray="1" ';
break;
case 'dashed':
this.currentPath += 'stroke-dasharray="3 1" ';
break;
case 'solid':
default:
this.currentPath += 'stroke-dasharray="none" ';
}
this.currentPath += 'd="';
this.moveTo(this.currentX, this.currentY);
return this;
};
/**** turn ****/
Graphic.prototype.turn = function (DirectionChange) {
expectFiniteNumber('direction change', DirectionChange);
this.currentDirection += DirectionChange;
return this;
};
/**** turnTo ****/
Graphic.prototype.turnTo = function (Direction) {
expectFiniteNumber('direction', Direction);
this.currentDirection = Direction;
return this;
};
/**** turnLeft ****/
Graphic.prototype.turnLeft = function (DirectionChange) {
expectFiniteNumber('direction change', DirectionChange);
this.currentDirection -= DirectionChange;
return this;
};
/**** turnRight ****/
Graphic.prototype.turnRight = function (DirectionChange) {
expectFiniteNumber('direction change', DirectionChange);
this.currentDirection += DirectionChange;
return this;
};
/**** move ****/
Graphic.prototype.move = function (Distance) {
expectFiniteNumber('distance', Distance);
var DirectionInRadians = this.currentDirection * Math.PI / 180;
this.moveTo(// DRY approach
(this.currentX || 0) + Distance * Math.cos(DirectionInRadians), (this.currentY || 0) + Distance * Math.sin(DirectionInRadians));
return this;
};
/**** moveTo ****/
Graphic.prototype.moveTo = function (x, y) {
expectFiniteNumber('x coordinate', x);
expectFiniteNumber('y coordinate', y);
this.currentX = x;
this.currentY = y;
if (this.currentPath != null) {
this.currentPath += 'M ' + rounded(x) + ',' + rounded(y) + ' ';
}
return this;
};
/**** draw ****/
Graphic.prototype.draw = function (Distance) {
expectFiniteNumber('distance', Distance);
var DirectionInRadians = this.currentDirection * Math.PI / 180;
this.drawTo(// DRY approach
(this.currentX || 0) + Distance * Math.cos(DirectionInRadians), (this.currentY || 0) + Distance * Math.sin(DirectionInRadians));
return this;
};
/**** drawTo ****/
Graphic.prototype.drawTo = function (x, y) {
expectFiniteNumber('x coordinate', x);
expectFiniteNumber('y coordinate', y);
if (this.currentPath == null) {
this.beginPath();
}
this._updateBoundingBox(this.currentX - this.currentWidth / 2, this.currentX + this.currentWidth / 2, this.currentY - this.currentWidth / 2, this.currentY + this.currentWidth / 2);
this.currentX = x;
this.currentY = y;
this.currentPath += 'L ' + rounded(x) + ',' + rounded(y) + ' ';
this._updateBoundingBox(this.currentX - this.currentWidth / 2, this.currentX + this.currentWidth / 2, this.currentY - this.currentWidth / 2, this.currentY + this.currentWidth / 2);
return this;
};
/**** curveLeft/Right ****/
Graphic.prototype.curveLeft = function (Angle, rx, ry) {
return this._curve(Angle, rx, ry, false);
};
Graphic.prototype.curveRight = function (Angle, rx, ry) {
return this._curve(Angle, rx, ry, true);
};
/**** _curve ****/
Graphic.prototype._curve = function (Angle, rx, ry, clockwise) {
expectFiniteNumber('turn angle', Angle);
expectFiniteNumber('x radius', rx);
allowFiniteNumber('y radius', ry);
if (ry == null) {
ry = rx;
}
var absAngle = Math.abs(Angle);
if (absAngle < 1e-6) {
return this;
}
var pi = Math.PI;
var sin = Math.sin;
var deg2rad = pi / 180;
var cos = Math.cos;
if (this.currentPath == null) {
this.beginPath();
}
/**** fix ellipse starting point ****/
var x0 = this.currentX;
var y0 = this.currentY;
this._updateBoundingBox(x0 - this.currentWidth / 2, x0 + this.currentWidth / 2, y0 - this.currentWidth / 2, y0 + this.currentWidth / 2);
/**** compute ellipse center ****/
var Direction = this.currentDirection;
var DirectionInRadians = Direction * deg2rad;
var NormalInRadians = DirectionInRadians + (clockwise ? pi / 2 : -pi / 2);
var cx = x0 + ry * cos(NormalInRadians); // "ry" is correct!
var cy = y0 + ry * sin(NormalInRadians); // dto.
/**** compute ellipse end point ****/
var AngleInRadians = (clockwise ? -pi / 2 + Angle * deg2rad : pi / 2 - Angle * deg2rad);
var auxX = rx * cos(AngleInRadians);
var auxY = ry * sin(AngleInRadians);
var x1 = cx + auxX * cos(DirectionInRadians) - auxY * sin(DirectionInRadians);
var y1 = cy + auxX * sin(DirectionInRadians) + auxY * cos(DirectionInRadians);
/**** construct SVG path ****/
var fullEllipse = (absAngle >= 360);
var largeArcFlag = (absAngle >= 180 ? 1 : 0);
var SweepFlag = (clockwise ? (Angle >= 0 ? 1 : 0) : (Angle >= 0 ? 0 : 1));
if (fullEllipse) {
auxX = cx + (cx - x0);
auxY = cy + (cy - y0);
this.currentPath += ('A ' + rounded(rx) + ' ' + rounded(ry) + ' ' +
rounded(Direction) + ' 1 ' + SweepFlag + ' ' +
rounded(auxX) + ' ' + rounded(auxY) + ' ') + ('A ' + rounded(rx) + ' ' + rounded(ry) + ' ' +
rounded(Direction) + ' 1 ' + SweepFlag + ' ' +
rounded(x0) + ' ' + rounded(y0) + ' ') + 'M ' + rounded(x1) + ' ' + rounded(y1) + ' ';
}
else {
this.currentPath += ('A ' + rounded(rx) + ' ' + rounded(ry) + ' ' +
rounded(Direction) + ' ' + largeArcFlag + ' ' + SweepFlag + ' ' +
rounded(x1) + ' ' + rounded(y1) + ' ');
}
/**** compute ellipse x/y bounds in rotated coordinate system ****/
// see https://math.stackexchange.com/questions/91132/how-to-get-the-limits-of-rotated-ellipse
var xMax = Math.sqrt(// still centered at origin, not cx/cy
rx * rx * Math.pow(cos(DirectionInRadians), 2) +
ry * ry * Math.pow(sin(DirectionInRadians), 2));
var yMax = Math.sqrt(// dto.
rx * rx * Math.pow(sin(DirectionInRadians), 2) +
ry * ry * Math.pow(cos(DirectionInRadians), 2));
for (var i = 0; i < 4; i++) {
var xSign = (i % 2 === 0 ? 1 : -1);
var ySign = (i < 2 ? 1 : -1);
var x = xSign * xMax;
var y = ySign * yMax;
var PointShouldBeUsed = void 0;
if (fullEllipse) {
PointShouldBeUsed = true;
}
else {
/**** rotate extremal points back into ellipse coordinates ****/
var maxX = x * cos(-DirectionInRadians) - y * sin(-DirectionInRadians);
var maxY = x * sin(-DirectionInRadians) + y * cos(-DirectionInRadians);
maxX = maxX / rx;
maxY = maxY / ry;
/**** compute extremal point angles and check if within arc ****/
var PointAngleInRadians = Math.atan2(maxY, maxX);
var StartAngleInRadians = (clockwise ? -pi / 2 : pi / 2);
var EndAngleInRadians = AngleInRadians; // already computed
if ((StartAngleInRadians < -pi) || (EndAngleInRadians < -pi)) {
StartAngleInRadians += 2 * pi; // that's sufficient, because...
EndAngleInRadians += 2 * pi; // ..."fullEllipse" is false here
}
if (StartAngleInRadians > EndAngleInRadians) {
var temp = StartAngleInRadians;
StartAngleInRadians = EndAngleInRadians;
EndAngleInRadians = temp;
}
PointShouldBeUsed = ( // common cases
(StartAngleInRadians <= PointAngleInRadians) &&
(PointAngleInRadians <= EndAngleInRadians)) || ( // rare cases
(PointAngleInRadians < 0) &&
(StartAngleInRadians <= PointAngleInRadians + 2 * pi) &&
(PointAngleInRadians + 2 * pi <= EndAngleInRadians));
}
if (PointShouldBeUsed) {
this._updateBoundingBox(cx + x - this.currentWidth / 2, cx + x + this.currentWidth / 2, cy + y - this.currentWidth / 2, cy + y + this.currentWidth / 2);
}
}
/**** update turtle ****/
this.currentDirection += (Angle >= 0 ? Angle : 180 + Angle) * (clockwise ? 1 : -1);
this.currentX = x1;
this.currentY = y1;
this._updateBoundingBox(x1 - this.currentWidth / 2, x1 + this.currentWidth / 2, y1 - this.currentWidth / 2, y1 + this.currentWidth / 2);
return this;
};
/**** endPath ****/
Graphic.prototype.endPath = function () {
if (this.currentPath != null) {
this.currentPath += '"/>';
this.SVGContent += this.currentPath;
this.currentPath = undefined;
}
return this;
};
/**** closePath ****/
Graphic.prototype.closePath = function () {
if (this.currentPath != null) {
this.currentPath += 'Z';
this.endPath();
}
return this;
};
/**** currentPosition ****/
Graphic.prototype.currentPosition = function () {
return { x: this.currentX, y: this.currentY };
};
/**** positionAt ****/
Graphic.prototype.positionAt = function (Position) {
allowPosition('turtle position', Position);
if (this.currentPath == null) {
this.currentX = Position.x;
this.currentY = Position.y;
}
else {
this.moveTo(Position.x, Position.y);
}
return this;
};
/**** currentAlignment ****/
Graphic.prototype.currentAlignment = function () {
return {
x: this.currentX, y: this.currentY, Direction: this.currentDirection
};
};
/**** alignAt ****/
Graphic.prototype.alignAt = function (Alignment) {
allowAlignment('turtle alignment', Alignment);
this.currentDirection = Alignment.Direction;
if (this.currentPath == null) {
this.currentX = Alignment.x;
this.currentY = Alignment.y;
}
else {
this.moveTo(Alignment.x, Alignment.y);
}
return this;
};
/**** Limits ****/
Graphic.prototype.Limits = function () {
return {
xMin: this.minX || 0, yMin: this.minY || 0,
xMax: this.maxX || 0, yMax: this.maxY || 0
};
};
/**** asSVG ****/
Graphic.prototype.asSVG = function (Unit, xMin, yMin, xMax, yMax) {
allowOneOf('SVG unit', Unit, ['px', 'mm', 'cm', 'in']);
allowFiniteNumber('minimal x', xMin);
allowFiniteNumber('maximal x', xMax);
allowFiniteNumber('minimal y', yMin);
allowFiniteNumber('maximal y', yMax);
if (this.minX == null) { // very special case: nothing has been drawn yet
this.minX = this.maxX = this.minY = this.maxY = 0;
}
if (Unit == null) {
Unit = 'px';
}
if (xMin == null) {
xMin = this.minX;
}
if (xMax == null) {
xMax = this.maxX;
}
if (yMin == null) {
yMin = this.minY;
}
if (yMax == null) {
yMax = this.maxY;
}
// @ts-ignore TS2532 we know that xMax and xMin are defined
var Width = xMax - xMin;
// @ts-ignore TS2532 we know that yMax and yMin are defined
var Height = yMax - yMin;
if (Width < 0)
throwError('InvalidArgument: invalid x range given');
if (Height < 0)
throwError('InvalidArgument: invalid y range given');
if (this.currentPath != null) { // if need be: end an ongoing path
this.endPath();
}
return ('<svg xmlns="http://www.w3.org/2000/svg" ' +
'width="' + rounded(Width) + Unit + '" ' +
'height="' + rounded(Height) + Unit + '" ' +
// @ts-ignore TS2532 we know that xMin and yMin are defined
'viewBox="' + floored(xMin) + ' ' + floored(yMin) + ' ' +
ceiled(Width) + ' ' + ceiled(Height) + '" ' +
'vector-effect="non-scaling-stroke"' +
'>' +
this.SVGContent +
'</svg>');
};
/**** asSVGwith72dpi ****/
Graphic.prototype.asSVGwith72dpi = function (Unit, xMin, yMin, xMax, yMax) {
var SVG = this.asSVG(Unit, xMin, yMin, xMax, yMax); // also validates arg.s
var Scale = 72 / {
'px': 25.4, 'mm': 25.4, 'cm': 2.54, 'in': 1
}[Unit || 'mm'];
if (xMin == null) {
xMin = this.minX;
}
if (xMax == null) {
xMax = this.maxX;
}
if (yMin == null) {
yMin = this.minY;
}
if (yMax == null) {
yMax = this.maxY;
}
return ('<svg xmlns="http://www.w3.org/2000/svg" ' +
// @ts-ignore TS2532 we know that xMin and yMin are defined
'viewBox="' + floored(Scale * xMin) + ' ' + floored(Scale * yMin) + ' ' +
// @ts-ignore TS2532 we know that xMin,xMax,yMin and yMax are defined
ceiled(Scale * (xMax - xMin)) + ' ' + ceiled(Scale * (yMax - yMin)) + '" ' +
'vector-effect="non-scaling-stroke"' +
'>' +
'<g transform="scale(' + Scale + ',' + Scale + ')">' +
SVG +
'</g></svg>');
};
/**** _updateBoundingBox ****/
Graphic.prototype._updateBoundingBox = function (minX, maxX, minY, maxY) {
this.minX = Math.min(this.minX, minX);
this.maxX = Math.max(this.maxX, maxX);
this.minY = Math.min(this.minY, minY);
this.maxY = Math.max(this.maxY, maxY);
};
return Graphic;
}());
/**** rounded ****/
function rounded(Value) {
return Math.round(Value * 100) / 100;
}
/**** ceiled ****/
function ceiled(Value) {
return Math.ceil(Value * 100) / 100;
}
/**** floored ****/
function floored(Value) {
return Math.floor(Value * 100) / 100;
}
export { Graphic, TUR_Caps, TUR_Joins, TUR_Lineatures, ValueIsAlignment, ValueIsPathOptionSet, ValueIsPosition, allowAlignment, allowPathOptionSet, allowPosition, allowedAlignment, allowedPathOptionSet, allowedPosition, expectAlignment, expectPathOptionSet, expectPosition, expectedAlignment, expectedPathOptionSet, expectedPosition };
//# sourceMappingURL=svg-turtle.esm.js.map