'use strict';
window.addEventListener('DOMContentLoaded', function () {
const P = [
new Point(0, 0),
new Point(80, 160),
new Point(160, 80)
];
const grapher = new Grapher();
const viewBox = grapher.getViewBox(P);
const svg = grapher.getSvg(viewBox, P);
document.body.appendChild(svg);
console.log(svg.outerHTML);
});
function Point (x, y)
{
this.x = x;
this.y = y;
}
Point.plus = function (p, q)
{
return new Point(p.x + q.x, p.y + q.y);
}
Point.minus = function (p, q)
{
return new Point(p.x - q.x, p.y - q.y);
}
Point.times = function (c, a)
{
return new Point(c * a.x, c * a.y);
}
Point.divide = function (a, c)
{
return new Point(a.x / c, a.y / c);
}
function Grapher ()
{
this.margin = 5;
this.circleRadius = 3;
this.lineWidth = 2;
}
Grapher.prototype.getViewBox = function (points)
{
const [min, max] = this.__getMinMax(points);
min.x -= this.margin;
min.y -= this.margin;
max.x += this.margin;
max.y += this.margin;
for (const p of points) {
p.x -= min.x;
p.y -= min.y;
}
return new Point(max.x - min.x, max.y - min.y);
}
Grapher.prototype.__getMinMax = function (points)
{
const min = new Point(points[0].x, points[0].y);
const max = new Point(points[0].x, points[0].y);
for (const point of points) {
if (point.x < min.x) {
min.x = point.x;
} else if (max.x < point.x) {
max.x = point.x;
}
if (point.y < min.y) {
min.y = point.y;
} else if (max.y < point.y) {
max.y = point.y;
}
}
return [min, max];
}
Grapher.prototype.getSvg = function (viewBox, P)
{
const parser = new DOMParser();
const svgXml = this.__getSvgXml(viewBox, P);
const fragment = parser.parseFromString(svgXml, 'image/svg+xml');
return fragment.firstChild;
}
Grapher.prototype.__getSvgXml = function (viewBox, P)
{
const curveXml = this.__getCurveXml(P);
const pointsXml = this.__getPointsXml(P);
return `<svg viewBox="0 0 ${viewBox.x} ${viewBox.y}" xmlns="http://www.w3.org/2000/svg">
<style>
path {
fill: none;
stroke: #000;
stroke-width: ${this.lineWidth};
}
</style>
${curveXml}
${pointsXml}
</svg>`;
}
Grapher.prototype.__getCurveXml = function (P)
{
if (2 < P.length) {
return this.__getSplineXml(P);
} else if (P.length === 2) {
return this.__getLineXml(P[0], P[1]);
} else {
return '';
}
}
Grapher.prototype.__getSplineXml = function (P)
{
const solver = new Solver();
const [Q, R] = solver.getControlPoints(P);
const d = this.__getSplineD(P, Q, R);
return this.__getPathXml(d);
}
Grapher.prototype.__getSplineD = function (P, Q, R)
{
const segments = [
`M ${P[0].x} ${P[0].y} c`
];
for (let i = 0; i < Q.length; ++i) {
const qx = this.__getNumberText(Q[i].x - P[i].x);
const qy = this.__getNumberText(Q[i].y - P[i].y);
const rx = this.__getNumberText(R[i].x - P[i].x);
const ry = this.__getNumberText(R[i].y - P[i].y);
const sx = this.__getNumberText(P[i+1].x - P[i].x);
const sy = this.__getNumberText(P[i+1].y - P[i].y);
segments.push(`${qx} ${qy} ${rx} ${ry} ${sx} ${sy}`);
}
return segments.join(' ');
}
Grapher.prototype.__getPathXml = function (d)
{
return `<path d="${d}"/>`;
}
Grapher.prototype.__getLineXml = function (a, b)
{
const d = this.__getLineD(a, b);
return this.__getPathXml(d);
}
Grapher.prototype.__getLineD = function (a, b)
{
const ax = this.__getNumberText(a.x);
const ay = this.__getNumberText(a.y);
const dx = this.__getNumberText(b.x - a.x);
const dy = this.__getNumberText(b.y - a.y);
return `M ${ax} ${ay} l ${dx} ${dy}`;
}
Grapher.prototype.__getPointsXml = function (points)
{
const circles = [];
for (const p of points) {
const circleXml = this.__getCircleXml(p);
circles.push(circleXml);
}
return circles.join("\n");
}
Grapher.prototype.__getCircleXml = function (center)
{
const cx = this.__getNumberText(center.x);
const cy = this.__getNumberText(center.y);
const r = this.__getNumberText(this.circleRadius);
return `<circle cx="${cx}" cy="${cy}" r="${r}"/>`;
}
Grapher.prototype.__getNumberText = function (number)
{
return number.toFixed(3).replace(/\.?0+$/, '');
}
function Solver ()
{
}
Solver.prototype.getControlPoints = function (P)
{
const Q = this.__getQ(P);
const R = this.__getR(P, Q);
return [Q, R];
}
Solver.prototype.__getQ = function (P)
{
const [a, b, c, d] = this.__getProblem(P);
return this.__getSolution(a, b, c, d);
}
Solver.prototype.__getProblem = function (P)
{
const a = [];
const b = [];
const c = [];
const d = [];
const n = P.length - 1;
a[0] = 0;
b[0] = 2;
c[0] = 1;
d[0] = Point.plus(P[0], Point.times(2, P[1]));
for (let i = 1; i < n-1; ++i) {
a[i] = 1;
b[i] = 4;
c[i] = 1;
d[i] = Point.plus(Point.times(4, P[i]), Point.times(2, P[i+1]));
}
a[n-1] = 2;
b[n-1] = 7;
c[n-1] = 0;
d[n-1] = Point.plus(Point.times(8, P[n-1]), P[n]);
return [a, b, c, d];
}
Solver.prototype.__getSolution = function (a, b, c, d)
{
const x = [];
const n = a.length;
for (let i = 1; i < n; ++i) {
const w = a[i] / b[i-1];
b[i] = b[i] - (w * c[i-1]);
d[i] = Point.minus(d[i], Point.times(w, d[i-1]));
}
x[n-1] = Point.divide(d[n-1], b[n-1]);
for (let i = n - 2; -1 < i; --i) {
x[i] = Point.divide(Point.minus(d[i], Point.times(c[i], x[i+1])), b[i]);
}
return x;
}
Solver.prototype.__getR = function (P, Q)
{
const R = [];
const n = Q.length;
for (let i = 0; i < n - 1; ++i) {
R[i] = Point.minus(Point.times(2, P[i+1]), Q[i+1]);
}
R[n-1] = Point.divide(Point.plus(P[n], Q[n-1]), 2);
return R;
}