1
0
mirror of https://github.com/Pomax/BezierInfo-2.git synced 2025-08-27 10:15:05 +02:00

this rename is absolutely stupid

This commit is contained in:
Pomax
2020-08-20 13:01:32 -07:00
parent 59fdafb2c5
commit d92e370bd1
470 changed files with 22 additions and 9 deletions

View File

@@ -0,0 +1,250 @@
/**
* The base API class, responsible for such things as setting up canvas event
* handling, method accumulation, custom element binding, etc. etc.
*
* Basically if it's not part of the "public" API, it's in this class.
*/
class BaseAPI {
static get privateMethods() {
return [`constructor`, `addListeners`, `getCursorCoords`]
.concat(this.superCallers)
.concat(this.eventHandlers);
}
static get superCallers() {
return [`setup`, `draw`];
}
static get eventHandlers() {
return [`onMouseDown`, `onMouseMove`, `onMouseUp`, `onKeyDown`, `onKeyUp`];
}
static get methods() {
const priv = this.privateMethods;
const names = Object.getOwnPropertyNames(this.prototype).concat([
`setSize`,
`redraw`,
]);
return names.filter((v) => priv.indexOf(v) < 0);
}
/**
*
*/
constructor(uid, width = 200, height = 200, canvasBuildFunction) {
if (uid) {
this.element = window[uid];
delete window[uid];
}
if (canvasBuildFunction) {
const { canvas, ctx } = canvasBuildFunction(width, height);
this.canvas = canvas;
this.ctx = enhanceContext(ctx);
this.preSized = true;
} else {
this.canvas = document.createElement(`canvas`);
}
this.addListeners();
this.setSize(width, height);
this.setup();
this.draw();
}
/**
*
*/
addListeners() {
const canvas = this.canvas;
if (this.element) {
this.element.setGraphic(this);
}
this.cursor = {};
const root = typeof document !== "undefined" ? document : canvas;
[`touchstart`, `mousedown`].forEach((evtName) =>
canvas.addEventListener(evtName, (evt) => this.onMouseDown(evt))
);
[`touchmove`, `mousemove`].forEach((evtName) =>
canvas.addEventListener(evtName, (evt) => this.onMouseMove(evt))
);
[`touchend`, `mouseup`].forEach((evtName) =>
root.addEventListener(evtName, (evt) => this.onMouseUp(evt))
);
this.keyboard = {};
[`keydown`].forEach((evtName) =>
canvas.addEventListener(evtName, (evt) => this.onKeyDown(evt))
);
[`keyup`].forEach((evtName) =>
canvas.addEventListener(evtName, (evt) => this.onKeyUp(evt))
);
}
stopEvent(evt) {
if (evt.target === this.canvas) {
evt.preventDefault();
evt.stopPropagation();
}
}
/**
*
*/
getCursorCoords(evt) {
const left = evt.target.offsetLeft,
top = evt.target.offsetTop;
let x, y;
if (evt.targetTouches) {
const touch = evt.targetTouches;
for (let i = 0; i < touch.length; i++) {
if (!touch[i] || !touch[i].pageX) continue;
x = touch[i].pageX - left;
y = touch[i].pageY - top;
}
} else {
x = evt.pageX - left;
y = evt.pageY - top;
}
this.cursor.x = x;
this.cursor.y = y;
}
/**
*
*/
onMouseDown(evt) {
this.stopEvent(evt);
this.cursor.down = true;
this.getCursorCoords(evt);
}
/**
*
*/
onMouseMove(evt) {
this.stopEvent(evt);
this.cursor.move = true;
this.getCursorCoords(evt);
}
/**
*
*/
onMouseUp(evt) {
this.stopEvent(evt);
this.cursor.down = false;
this.cursor.move = false;
this.getCursorCoords(evt);
}
/**
*
*/
safelyInterceptKey(evt) {
// We don't want to interfere with the browser, so we're only
// going to allow unmodified keys, or shift-modified keys,
// and tab has to always work. For obvious reasons.
const tab = evt.key !== "Tab";
const functionKey = evt.key.match(/F\d+/) === null;
const specificCheck = tab && functionKey;
if (!evt.altKey && !evt.ctrlKey && !evt.metaKey && specificCheck) {
this.stopEvent(evt);
}
}
/**
*
*/
onKeyDown(evt) {
this.safelyInterceptKey(evt);
// FIXME: Known bug: this approach means that "shift + r + !shift + !r" leaves "R" set to true
this.keyboard[evt.key] = true;
this.keyboard.currentKey = evt.key;
}
/**
*
*/
onKeyUp(evt) {
this.safelyInterceptKey(evt);
// FIXME: Known bug: this approach means that "shift + r + !shift + !r" leaves "R" set to true
this.keyboard[evt.key] = false;
this.keyboard.currentKey = evt.key;
}
/**
* This function is critical in correctly sizing the canvas.
*/
setSize(w, h) {
this.width = w || this.width;
this.height = h || this.height;
if (!this.preSized) {
this.canvas.width = this.width;
this.canvas.style.width = `${this.width}px`;
this.canvas.height = this.height;
this.ctx = enhanceContext(this.canvas.getContext(`2d`));
}
}
/**
* This is the main entry point.
*/
setup() {
// console.log(`setup`);
this.movable = [];
}
/**
* This is the draw (loop) function.
*/
draw() {
// console.log(`draw`);
}
/**
* This is mostly a safety function, to
* prevent direct calls to draw().. it might
* disappear.
*/
redraw() {
this.draw();
}
}
// Ensure there are cacheStyle/restoreStyle functions
// on the Canvas context, so that it's trivial to make
// temporary changes.
function enhanceContext(ctx) {
const styles = [];
ctx.cacheStyle = () => {
let m = ctx.currentTransform || ctx.getTransform();
let e = {
strokeStyle: ctx.strokeStyle,
fillStyle: ctx.fillStyle,
lineWidth: ctx.lineWidth,
textAlign: ctx.textAlign,
transform: [m.a, m.b, m.c, m.d, m.e, m.f],
};
styles.push(e);
};
ctx.restoreStyle = () => {
const v = styles.pop();
Object.keys(v).forEach((k) => {
let val = v[k];
if (k !== `transform`) ctx[k] = val;
else ctx.setTransform(val[0], val[1], val[2], val[3], val[4], val[5]);
});
};
return ctx;
}
export { BaseAPI };

View File

@@ -0,0 +1,557 @@
import { enrich } from "../lib/enrich.js";
import { Bezier } from "./types/bezier.js";
import { Vector } from "./types/vector.js";
import { Shape } from "./util/shape.js";
import { Matrix } from "./util/matrix.js";
import { BaseAPI } from "./base-api.js";
const MOUSE_PRECISION_ZONE = 5;
const TOUCH_PRECISION_ZONE = 30;
let CURRENT_HUE = 0;
/**
* Our Graphics API, which is the "public" side of the API.
*/
class GraphicsAPI extends BaseAPI {
static get constants() {
return [
`POINTER`,
`HAND`,
`PI`,
`TAU`,
`POLYGON`,
`CURVE`,
`BEZIER`,
`CENTER`,
`LEFT`,
`RIGHT`,
];
}
draw() {
CURRENT_HUE = 0;
super.draw();
}
get PI() {
return 3.14159265358979;
}
get TAU() {
return 6.28318530717958;
}
get POINTER() {
return `default`;
}
get HAND() {
return `pointer`;
}
get POLYGON() {
return Shape.POLYGON;
}
get CURVE() {
return Shape.CURVE;
}
get BEZIER() {
return Shape.BEZIER;
}
get CENTER() {
return `center`;
}
get LEFT() {
return `left`;
}
get RIGHT() {
return `right`;
}
onMouseDown(evt) {
super.onMouseDown(evt);
const cdist = evt.targetTouches
? TOUCH_PRECISION_ZONE
: MOUSE_PRECISION_ZONE;
for (let i = 0, e = this.movable.length, p, d; i < e; i++) {
p = this.movable[i];
d = new Vector(p).dist(this.cursor);
if (d <= cdist) {
this.currentPoint = p;
break;
}
}
}
onMouseMove(evt) {
super.onMouseMove(evt);
if (this.currentPoint) {
this.currentPoint.x = this.cursor.x;
this.currentPoint.y = this.cursor.y;
} else {
for (let i = 0, e = this.movable.length, p; i < e; i++) {
p = this.movable[i];
if (new Vector(p).dist(this.cursor) <= 5) {
this.setCursor(this.HAND);
return; // NOTE: this is a return, not a break.
}
}
this.setCursor(this.POINTER);
}
}
onMouseUp(evt) {
super.onMouseUp(evt);
this.currentPoint = undefined;
}
resetMovable(points) {
this.movable.splice(0, this.movable.length);
if (points) this.setMovable(points);
}
setMovable(points) {
points.forEach((p) => this.movable.push(p));
}
/**
* transforms: translate
*/
translate(x, y) {
this.ctx.translate(x, y);
}
/**
* transforms: rotate
*/
rotate(angle) {
this.ctx.rotate(angle);
}
/**
* transforms: scale
*/
scale(x, y) {
y = y ? y : x; // NOTE: this turns y=0 into y=x, which is fine. Scaling by 0 is really silly =)
this.ctx.scale(x, y);
}
/**
* transforms: screen to world
*/
screenToWorld(x, y) {
if (y === undefined) {
y = x.y;
x = x.x;
}
let M = this.ctx.getTransform().invertSelf();
let ret = {
x: x * M.a + y * M.c + M.e,
y: x * M.b + y * M.d + M.f,
};
return ret;
}
/**
* transforms: world to screen
*/
worldToScreen(x, y) {
if (y === undefined) {
y = x.y;
x = x.x;
}
let M = this.ctx.getTransform();
let ret = {
x: x * M.a + y * M.c + M.e,
y: x * M.b + y * M.d + M.f,
};
return ret;
}
/**
* transforms: reset
*/
resetTransform() {
this.ctx.resetTransform();
}
/**
* custom element scoped querySelector
*/
find(qs) {
if (!this.element) return false;
return enrich(this.element.querySelector(qs));
}
/**
* custom element scoped querySelectorAll
*/
findAll(qs) {
if (!this.element) return false;
return Array.from(this.element.querySelectorAll(qs)).map((e) => enrich(e));
}
/**
* Set a (CSS) border on the canvas
*/
setBorder(width = 1, color = `black`) {
if (width === false) {
this.canvas.style.border = `none`;
} else {
this.canvas.style.border = `${width}px solid ${color}`;
}
}
/**
* Set the cursor type while the cursor is over the canvas
*/
setCursor(type) {
this.canvas.style.cursor = type;
}
/**
* Get a random color
*/
randomColor(a = 1.0) {
CURRENT_HUE = (CURRENT_HUE + 73) % 360;
return `hsla(${CURRENT_HUE},50%,50%,${a})`;
}
/**
* Set the context fillStyle
*/
setFill(color) {
if (color === false) color = `transparent`;
this.ctx.fillStyle = color;
}
/**
* Convenience alias
*/
noFill() {
this.setFill(false);
}
/**
* Set the context strokeStyle
*/
setStroke(color) {
if (color === false) color = `transparent`;
this.ctx.strokeStyle = color;
}
/**
* Convenience alias
*/
noStroke() {
this.setStroke(false);
}
/**
* Set the context lineWidth
*/
setWidth(width) {
this.ctx.lineWidth = `${width}px`;
}
/**
* Cache all styling values
*/
cacheStyle() {
this.ctx.cacheStyle();
}
/**
* restore all previous styling values
*/
restoreStyle() {
this.ctx.restoreStyle();
}
/**
* Reset the canvas bitmap to a uniform color.
*/
clear(color = `white`) {
this.ctx.cacheStyle();
this.resetTransform();
this.ctx.fillStyle = color;
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
this.ctx.restoreStyle();
}
/**
* Draw a Point (or {x,y,z?} conformant) object on the canvas
*/
point(x, y) {
this.circle(x, y, 5);
}
/**
* Draw a line between two Points
*/
line(x1, y1, x2, y2) {
this.ctx.beginPath();
this.ctx.moveTo(x1, y1);
this.ctx.lineTo(x2, y2);
this.ctx.stroke();
}
/**
* Draw a circle around a Point
*/
circle(x, y, r) {
this.ctx.beginPath();
this.ctx.arc(x, y, r, 0, this.TAU);
this.ctx.fill();
this.ctx.stroke();
}
/**
* Draw text on the canvas
*/
text(str, x, y, alignment) {
if (y === undefined) {
y = x.y;
x = x.x;
}
const ctx = this.ctx;
if (alignment) {
ctx.cacheStyle();
ctx.textAlign = alignment;
}
this.ctx.fillText(str, x, y);
if (alignment) ctx.restoreStyle();
}
/**
* Draw a rectangle start with {p} in the upper left
*/
rect(x, y, w, h) {
this.ctx.fillRect(x, y, w, h);
this.ctx.strokeRect(x, y, w, h);
}
/**
* Draw a function plot from [start] to [end] in [steps] steps.
* Returns the plot shape so that it can be cached for redrawing.
*/
plot(fn, start = 0, end = 1, steps = 24) {
const ctx = this.ctx;
ctx.cacheStyle();
ctx.fillStyle = `transparent`;
const interval = end - start;
this.start();
for (let i = 0, e = steps - 1, v; i < steps; i++) {
v = fn(start + (interval * i) / e);
this.vertex(v.x, v.y);
}
this.end();
ctx.restoreStyle();
return this.currentShape;
}
/**
* A signal for starting a complex shape
*/
start(type) {
this.currentShape = new Shape(type || this.POLYGON);
}
/**
* A signal for starting a new segment of a complex shape
*/
segment(type, factor) {
this.currentShape.addSegment(type, factor);
}
/**
* Add a plain point to the current shape
*/
vertex(x, y) {
this.currentShape.vertex({ x, y });
}
/**
* Draw a previously created shape
*/
drawShape(shape) {
this.currentShape = shape;
this.end();
}
/**
* A signal to draw the current complex shape
*/
end(close = false) {
this.ctx.beginPath();
let { x, y } = this.currentShape.first;
this.ctx.moveTo(x, y);
this.currentShape.segments.forEach((s) =>
this[`draw${s.type}`](s.points, s.factor)
);
if (close) this.ctx.closePath();
this.ctx.fill();
this.ctx.stroke();
}
/**
* Polygon draw function
*/
drawPolygon(points) {
points.forEach((p) => this.ctx.lineTo(p.x, p.y));
}
/**
* Curve draw function, which draws a CR curve as a series of Beziers
*/
drawCatmullRom(points, f) {
// invent a virtual first and last point
const f0 = points[0],
f1 = points[1],
fn = f0.reflect(f1),
l1 = points[points.length - 2],
l0 = points[points.length - 1],
ln = l0.reflect(l1),
cpoints = [fn, ...points, ln];
// four point sliding window over the segment
for (let i = 0, e = cpoints.length - 3; i < e; i++) {
let [c1, c2, c3, c4] = cpoints.slice(i, i + 4);
let p2 = {
x: c2.x + (c3.x - c1.x) / (6 * f),
y: c2.y + (c3.y - c1.y) / (6 * f),
};
let p3 = {
x: c3.x - (c4.x - c2.x) / (6 * f),
y: c3.y - (c4.y - c2.y) / (6 * f),
};
this.ctx.bezierCurveTo(p2.x, p2.y, p3.x, p3.y, c3.x, c3.y);
}
}
/**
* Curve draw function, which assumes Bezier coordinates
*/
drawBezier(points) {
for (let i = 0, e = points.length; i < e; i += 3) {
let [p1, p2, p3] = points.slice(i, i + 3);
this.ctx.bezierCurveTo(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y);
}
}
/**
* Yield a snapshot of the current shape.
*/
save() {
return this.currentShape;
}
/**
* convenient grid drawing function
*/
drawGrid(division = 20) {
for (let x = (division / 2) | 0; x < this.width; x += division) {
this.line({ x, y: 0 }, { x, y: this.height });
}
for (let y = (division / 2) | 0; y < this.height; y += division) {
this.line({ x: 0, y }, { x: this.width, y });
}
}
/**
* convenient axis drawing function
*
* api.drawAxes(pad, "t",0,1, "S","0%","100%");
*
*/
drawAxes(hlabel, hs, he, vlabel, vs, ve, w, h) {
h = h || this.height;
w = w || this.width;
this.line(0, 0, w, 0);
this.line(0, 0, 0, h);
const hpos = 0 - 5;
this.text(`${hlabel}`, this.width / 2, hpos, this.CENTER);
this.text(hs, 0, hpos, this.CENTER);
this.text(he, w, hpos, this.CENTER);
const vpos = -10;
this.text(`${vlabel}\n`, vpos, this.height / 2, this.RIGHT);
this.text(vs, vpos, 0 + 5, this.RIGHT);
this.text(ve, vpos, h, this.RIGHT);
}
/**
* math functions
*/
floor(v) {
return Math.floor(v);
}
ceil(v) {
return Math.ceil(v);
}
round(v) {
return Math.round(v);
}
random(v = 1) {
return Math.random() * v;
}
abs(v) {
return Math.abs(v);
}
min(...v) {
return Math.min(...v);
}
max(...v) {
return Math.max(...v);
}
sin(v) {
return Math.sin(v);
}
cos(v) {
return Math.cos(v);
}
tan(v) {
return Math.tan(v);
}
sqrt(v) {
return Math.sqrt(v);
}
atan2(dy, dx) {
return Math.atan2(dy, dx);
}
pow(v, p) {
return Math.pow(v, p);
}
map(v, s, e, ns, ne, constrain = false) {
const i1 = e - s,
i2 = ne - ns,
p = v - s;
let r = ns + (p * i2) / i1;
if (constrain) return this.constrain(r, ns, ne);
return r;
}
constrain(v, s, e) {
return v < s ? s : v > e ? e : v;
}
}
export { GraphicsAPI, Bezier, Vector, Matrix };

View File

@@ -0,0 +1,165 @@
import { Vector } from "./vector.js";
import { Bezier as Original } from "../../lib/bezierjs/bezier.js";
/**
* A canvas-aware Bezier curve class
*/
class Bezier extends Original {
static defaultQuadratic(apiInstance) {
return new Bezier(apiInstance, 70, 250, 20, 110, 220, 60);
}
static defaultCubic(apiInstance) {
return new Bezier(apiInstance, 110, 150, 25, 190, 210, 250, 210, 30);
}
constructor(apiInstance, ...coords) {
super(...coords);
this.api = apiInstance;
this.ctx = apiInstance.ctx;
}
getPointNear(point, d = 5) {
const { x, y } = point;
const p = this.points;
for (let i = 0, e = p.length; i < e; i++) {
let dx = Math.abs(p[i].x - x);
let dy = Math.abs(p[i].y - y);
if ((dx * dx + dy * dy) ** 0.5 <= d) {
return p[i];
}
}
}
getProjectionPoint(point) {
const { x, y } = point;
// project this point onto the curve and return _that_ point
const n = this.lut.length - 1,
p = this.points;
let d,
closest,
smallestDistance = Number.MAX_SAFE_INTEGER;
// coarse check
this.lut.forEach((p, i) => {
d = p.dist(x, y);
if (d < smallestDistance) {
smallestDistance = d;
p.t = i / n;
closest = p;
}
});
// fine check
for (let o = -0.1, t, np, st = closest.t; o <= 0.1; o += 0.005) {
t = st + o;
if (t < 0) continue;
if (t > 1) continue;
np = new Point(
compute(t, p[0].x, p[1].x, p[2].x, p[3].x),
compute(t, p[0].y, p[1].y, p[2].y, p[3].y)
);
d = np.dist(x, y);
if (d < smallestDistance) {
smallestDistance = d;
closest = np;
closest.t = t;
}
}
return closest;
}
drawCurve(color = `#333`) {
const ctx = this.ctx;
ctx.cacheStyle();
ctx.lineWidth = 2;
ctx.strokeStyle = color;
ctx.beginPath();
const lut = this.getLUT().slice();
let p = lut.shift();
ctx.moveTo(p.x, p.y);
while (lut.length) {
p = lut.shift();
ctx.lineTo(p.x, p.y);
}
ctx.stroke();
ctx.restoreStyle();
}
drawPoints(labels = true) {
const colors = [`red`, `green`, `blue`, `yellow`];
const api = this.api;
const ctx = this.ctx;
ctx.cacheStyle();
ctx.lineWidth = 2;
ctx.strokeStyle = `#999`;
this.points.forEach((p, i) => {
api.setFill(colors[i % colors.length]);
api.circle(p.x, p.y, 5);
if (labels) {
api.setFill(`black`);
api.text(`(${p.x},${p.y})`, p.x + 10, p.y + 10);
}
});
ctx.restoreStyle();
}
drawSkeleton(color = `#555`) {
const api = this.api;
const ctx = this.ctx;
ctx.cacheStyle();
const p = this.points;
api.noFill();
api.setStroke(color);
api.start();
p.forEach((v) => api.vertex(v.x, v.y));
api.end();
ctx.restoreStyle();
}
getStrutPoints(t) {
const p = this.points.map((p) => new Vector(p));
const mt = 1 - t;
let s = 0;
let n = p.length + 1;
while (--n > 1) {
let list = p.slice(s, s + n);
for (let i = 0, e = list.length - 1; i < e; i++) {
let pt = list[i + 1].subtract(list[i + 1].subtract(list[i]).scale(mt));
p.push(pt);
}
s += n;
}
return p;
}
drawStruts(t) {
const p = t.forEach ? t : this.getStrutPoints(t);
const api = this.api;
const ctx = api.ctx;
ctx.cacheStyle();
api.noFill();
let s = this.points.length;
let n = this.points.length;
while (--n > 1) {
api.start();
for (let i = 0; i < n; i++) {
let pt = p[s + i];
api.vertex(pt.x, pt.y);
api.circle(pt.x, pt.y, 5);
}
api.end();
s += n;
}
ctx.restoreStyle();
}
}
export { Bezier };

View File

@@ -0,0 +1,77 @@
class Vector {
constructor(x, y, z) {
if (arguments.length === 1) {
z = x.z;
y = x.y;
x = x.x;
}
this.x = x;
this.y = y;
if (z !== undefined) {
this.z = z;
}
}
dist(other, y, z = 0) {
if (y !== undefined) other = { x: other, y, z };
let sum = 0;
sum += (this.x - other.x) ** 2;
sum += (this.y - other.y) ** 2;
let z1 = this.z ? this.z : 0;
let z2 = other.z ? other.z : 0;
sum += (z1 - z2) ** 2;
return sum ** 0.5;
}
normalize(f) {
let mag = this.dist(0, 0, 0);
return new Vector(
(f * this.x) / mag,
(f * this.y) / mag,
(f * this.z) / mag
);
}
getAngle() {
return -Math.atan2(this.y, this.x);
}
reflect(other) {
let p = new Vector(other.x - this.x, other.y - this.y);
if (other.z !== undefined) {
p.z = other.z;
if (this.z !== undefined) {
p.z -= this.z;
}
}
return this.subtract(p);
}
add(other) {
let p = new Vector(this.x + other.x, this.y + other.y);
if (this.z !== undefined) {
p.z = this.z;
if (other.z !== undefined) {
p.z += other.z;
}
}
return p;
}
subtract(other) {
let p = new Vector(this.x - other.x, this.y - other.y);
if (this.z !== undefined) {
p.z = this.z;
if (other.z !== undefined) {
p.z -= other.z;
}
}
return p;
}
scale(f = 1) {
if (f === 0) {
return new Vector(0, 0, this.z === undefined ? undefined : 0);
}
let p = new Vector(this.x * f, this.y * f);
if (this.z !== undefined) {
p.z = this.z * f;
}
return p;
}
}
export { Vector };

View File

@@ -0,0 +1,142 @@
// Copied from http://blog.acipo.com/matrix-inversion-in-javascript/
function invert(M) {
// I use Guassian Elimination to calculate the inverse:
// (1) 'augment' the matrix (left) by the identity (on the right)
// (2) Turn the matrix on the left into the identity by elemetry row ops
// (3) The matrix on the right is the inverse (was the identity matrix)
// There are 3 elemtary row ops: (I combine b and c in my code)
// (a) Swap 2 rows
// (b) Multiply a row by a scalar
// (c) Add 2 rows
//if the matrix isn't square: exit (error)
if (M.length !== M[0].length) {
console.log("not square");
return;
}
//create the identity matrix (I), and a copy (C) of the original
var i = 0,
ii = 0,
j = 0,
dim = M.length,
e = 0,
t = 0;
var I = [],
C = [];
for (i = 0; i < dim; i += 1) {
// Create the row
I[I.length] = [];
C[C.length] = [];
for (j = 0; j < dim; j += 1) {
//if we're on the diagonal, put a 1 (for identity)
if (i == j) {
I[i][j] = 1;
} else {
I[i][j] = 0;
}
// Also, make the copy of the original
C[i][j] = M[i][j];
}
}
// Perform elementary row operations
for (i = 0; i < dim; i += 1) {
// get the element e on the diagonal
e = C[i][i];
// if we have a 0 on the diagonal (we'll need to swap with a lower row)
if (e == 0) {
//look through every row below the i'th row
for (ii = i + 1; ii < dim; ii += 1) {
//if the ii'th row has a non-0 in the i'th col
if (C[ii][i] != 0) {
//it would make the diagonal have a non-0 so swap it
for (j = 0; j < dim; j++) {
e = C[i][j]; //temp store i'th row
C[i][j] = C[ii][j]; //replace i'th row by ii'th
C[ii][j] = e; //repace ii'th by temp
e = I[i][j]; //temp store i'th row
I[i][j] = I[ii][j]; //replace i'th row by ii'th
I[ii][j] = e; //repace ii'th by temp
}
//don't bother checking other rows since we've swapped
break;
}
}
//get the new diagonal
e = C[i][i];
//if it's still 0, not invertable (error)
if (e == 0) {
return;
}
}
// Scale this row down by e (so we have a 1 on the diagonal)
for (j = 0; j < dim; j++) {
C[i][j] = C[i][j] / e; //apply to original matrix
I[i][j] = I[i][j] / e; //apply to identity
}
// Subtract this row (scaled appropriately for each row) from ALL of
// the other rows so that there will be 0's in this column in the
// rows above and below this one
for (ii = 0; ii < dim; ii++) {
// Only apply to other rows (we want a 1 on the diagonal)
if (ii == i) {
continue;
}
// We want to change this element to 0
e = C[ii][i];
// Subtract (the row above(or below) scaled by e) from (the
// current row) but start at the i'th column and assume all the
// stuff left of diagonal is 0 (which it should be if we made this
// algorithm correctly)
for (j = 0; j < dim; j++) {
C[ii][j] -= e * C[i][j]; //apply to original matrix
I[ii][j] -= e * I[i][j]; //apply to identity
}
}
}
//we've done all operations, C should be the identity
//matrix I should be the inverse:
return I;
}
function multiply(m1, m2) {
var M = [];
var m2t = transpose(m2);
m1.forEach((row, r) => {
M[r] = [];
m2t.forEach((col, c) => {
M[r][c] = row.map((v, i) => col[i] * v).reduce((a, v) => a + v, 0);
});
});
return M;
}
function transpose(M) {
return M[0].map((col, i) => M.map((row) => row[i]));
}
class Matrix {
constructor(data) {
this.data = data;
}
multiply(other) {
return new Matrix(multiply(this.data, other.data));
}
invert() {
return new Matrix(invert(this.data));
}
transpose() {
return new Matrix(transpose(this.data));
}
}
export { Matrix };

View File

@@ -0,0 +1,43 @@
/**
* A complex shape, represented as a collection of paths
* that can be either polygon, Catmull-Rom curves, or
* cubic Bezier curves.
*/
class Shape {
constructor(type, factor) {
this.first = false;
this.segments = [];
this.addSegment(type, factor);
}
addSegment(type, factor) {
this.currentSegment = new Segment(type, factor);
this.segments.push(this.currentSegment);
}
vertex(p) {
if (!this.first) this.first = p;
else this.currentSegment.add(p);
}
}
/**
* Pathing type constants
*/
Shape.POLYGON = `Polygon`;
Shape.CURVE = `CatmullRom`;
Shape.BEZIER = `Bezier`;
/**
* A shape subpath
*/
class Segment {
constructor(type, factor) {
this.type = type;
this.factor = factor;
this.points = [];
}
add(p) {
this.points.push(p);
}
}
export { Shape, Segment };

View File

@@ -0,0 +1,135 @@
const REG_KEY = `registered as custom element`;
// helper function
function NotImplemented(instance, fname) {
console.warn(
`missing implementation for ${fname}(...data) in ${instance.__proto__.constructor.name}`
);
}
// helper function for turning "ClassName" into "class-name"
function getElementTagName(cls) {
return cls.prototype.constructor.name.replace(
/([A-Z])([a-z])/g,
(a, b, c, d) => {
const r = `${b.toLowerCase()}${c}`;
return d > 0 ? `-${r}` : r;
}
);
}
/**
* This is an enrichment class to make working with custom elements
* actually pleasant, rather than a ridiculous exercise in figuring
* out a low-level spec.
*/
class CustomElement extends HTMLElement {
static register(cls) {
if (!cls[REG_KEY]) {
const tagName = cls.tagName || getElementTagName(cls);
customElements.define(tagName, cls);
cls[REG_KEY] = true;
return customElements.whenDefined(tagName);
}
return Promise.resolve();
}
static get tagName() {
return getElementTagName(this);
}
constructor(options = {}) {
super();
if (!customElements.resolveScope) {
customElements.resolveScope = function (scope) {
try {
return scope.getRootNode().host;
} catch (e) {
console.warn(e);
}
return window;
};
}
this._options = options;
const route = {
childList: (record) => {
this.handleChildChanges(
Array.from(record.addedNodes),
Array.from(record.removedNodes)
);
this.render();
},
attributes: (record) => {
this.handleAttributeChange(
record.attributeName,
record.oldValue,
this.getAttribute(record.attributeName)
);
this.render();
},
};
// Set up a mutation observer because there are no custom
// element lifecycle functions for changes to the childNodes
// nodelist.
this._observer = new MutationObserver((records) => {
if (this.isConnected) {
records.forEach((record) => {
// console.log(record);
route[record.type](record);
});
}
});
this._observer.observe(this, {
childList: true,
attributes: true,
});
// Set up an open shadow DOM, because the web is open,
// and hiding your internals is ridiculous.
const shadowProps = { mode: `open` };
this._shadow = this.attachShadow(shadowProps);
this._style = document.createElement(`style`);
this._style.textContent = this.getStyle();
if (this._options.header !== false)
this._header = document.createElement(`header`);
if (this._options.slot !== false && this._options.void !== true)
this._slot = document.createElement(`slot`);
if (this._options.footer !== false)
this._footer = document.createElement(`footer`);
}
connectedCallback() {
this.render();
}
handleChildChanges(added, removed) {
if (!this._options.void) NotImplemented(this, `handleChildChanges`);
}
handleAttributeChange(name, oldValue, newValue) {
NotImplemented(this, `handleAttributeChange`);
}
getStyle() {
return ``;
}
render() {
this._shadow.innerHTML = ``;
this._shadow.append(this._style);
if (this._options.header !== false) this._shadow.append(this._header);
if (this._options.slot !== false) this._shadow.append(this._slot);
if (this._options.footer !== false) this._shadow.append(this._footer);
}
}
export { CustomElement };

View File

@@ -0,0 +1,75 @@
/*
Let's look at how we can give the <graphics-element> a decent look,
but also how we can make sure that the <fallback> content only shows
when the <graphics-element> isn't actually defined, because scripts
are disabled (or blocked!).
*/
graphics-element {
display: inline-block;
border: 1px solid grey;
}
/*
We can use the :defined pseudo-class to check whether a particular
element is considered a "real" element (either by being a built-in
standard HTML element, or a registered custom element) or whether it's
just "a tag name that isn't tied to anything".
If JS is disabled, as well as when the JS for registering our custom
element is still loading, our <graphics-element> will just be "a word"
and so we want to make sure to not show any content, except for the
<fallback-image>, if there is one.
So, first off: hide everything!
*/
graphics-element:not(:defined) > * {
display: none;
}
/*
And then we declare a more specific rule that does NOT hide the
<fallback-image> element and its content.
*/
graphics-element:not(:defined) fallback-image {
display: block;
font-size: 60%;
text-align: center;
padding-bottom: 0.2em;
}
/*
Normally, images are inline elements, but in our case we want it to be
treated as a full block, so that the caption text shows up underneath
it, rather than next to it:
*/
graphics-element:not(:defined) fallback-image > img {
display: block;
margin: 0.9em;
}
/*
Then, we say what should happen once our <graphics-element> element
is considered a proper, registered HTML tag:
*/
graphics-element:defined {
display: inline-block;
padding: 0.5em;
margin: 1em auto;
justify-self: center;
font-size: revert;
text-align: revert;
}
/*
And of course, once that's the case we actually want to make sure that
the <fallback-image> does NOT show anymore!
*/
graphics-element:defined fallback-image {
display: none;
}

View File

@@ -0,0 +1,283 @@
import { CustomElement } from "./custom-element.js";
import splitCodeSections from "./lib/split-code-sections.js";
import performCodeSurgery from "./lib/perform-code-surgery.js";
const MODULE_URL = import.meta.url;
const MODULE_PATH = MODULE_URL.slice(0, MODULE_URL.lastIndexOf(`/`));
/**
* A simple "for programming code" element, for holding entire
* programs, rather than code snippets.
*/
CustomElement.register(class ProgramCode extends HTMLElement {});
/**
* Our custom element
*/
class GraphicsElement extends CustomElement {
constructor() {
super({ header: false, footer: false });
// Strip out fallback images: if we can get here,
// we should not be loading fallback images because
// we know we're showing live content instead.
let fallback = this.querySelector(`fallback-image`);
if (fallback) this.removeChild(fallback);
this.loadSource();
if (this.title) {
this.label = document.createElement(`label`);
this.label.textContent = this.title;
}
}
/**
* part of the CustomElement API
*/
getStyle() {
return `
:host([hidden]) { display: none; }
:host style { display: none; }
:host canvas { position: relative; z-index: 1; display: block; margin: auto; border-radius: 0; box-sizing: content-box!important; padding: 1px; }
:host canvas:focus { border: 1px solid red; padding: 0px; }
:host a.view-source { display: block; position:relative; top: -0.6em; margin-bottom: -0.2em; font-size: 60%; text-decoration: none; }
:host label { display: block; font-style:italic; font-size: 0.9em; text-align: right; }
`;
}
/**
* part of the CustomElement API
*/
handleChildChanges(added, removed) {
// console.log(`child change:`, added, removed);
}
/**
* part of the CustomElement API
*/
handleAttributeChange(name, oldValue, newValue) {
if (name === `title`) {
this.label.textContent = this.getAttribute(`title`);
}
if (this.apiInstance) {
let instance = this.apiInstance;
if (name === `width`) {
instance.setSize(parseInt(newValue), false);
instance.redraw();
}
if (name === `height`) {
instance.setSize(false, parseInt(newValue));
instance.redraw();
}
}
}
/**
* Load the graphics code, either from a src URL, a <program-code> element, or .textContent
*/
async loadSource() {
let src = false;
let codeElement = this.querySelector(`program-code`);
let code = ``;
if (codeElement) {
src = codeElement.getAttribute("src");
if (src) {
code = await fetch(src).then((response) => response.text());
} else {
code = codeElement.textContent;
}
} else {
src = this.getAttribute("src");
if (src) {
code = await fetch(src).then((response) => response.text());
} else {
code = this.textContent;
}
}
if (!codeElement) {
codeElement = document.createElement(`program-code`);
codeElement.textContent = code;
this.prepend(codeElement);
}
codeElement.setAttribute(`hidden`, `hidden`);
new MutationObserver((_records) => {
// nornmally we don't want to completely recreate the shadow DOM
this.processSource(src, codeElement.textContent);
}).observe(codeElement, {
characterData: true,
attributes: false,
childList: true,
subtree: true,
});
// But on the first pass, we do.
this.processSource(src, code, true);
}
/**
* Transform the graphics source code into global and class code.
*/
processSource(src, code, rerender = false) {
this.rawCode = code;
if (this.script) {
if (this.script.parentNode) {
this.script.parentNode.removeChild(this.script);
}
this.canvas.parentNode.removeChild(this.canvas);
rerender = true;
}
const uid = `bg-uid-${Date.now()}-${`${Math.random()}`.replace(`0.`, ``)}`;
window[uid] = this;
// Step 1: fix the imports. This is ... a bit of work.
let path;
let base = document.querySelector(`base`);
if (base) {
path = base.href;
} else {
let loc = window.location.toString();
path = loc.substring(0, loc.lastIndexOf(`/`) + 1);
}
let modulepath = `${path}${src}`;
let modulebase = modulepath.substring(0, modulepath.lastIndexOf(`/`) + 1);
// okay, I lied, it's actually quite a lot of work.
code = code.replace(/(import .+? from) "([^"]+)"/g, (_, main, group) => {
return `${main} "${modulebase}${group}"`;
});
// Then, step 2: split up the code into "global" vs. "class" code.
const split = splitCodeSections(code);
const globalCode = split.quasiGlobal;
const classCode = performCodeSurgery(split.classCode);
this.setupCodeInjection(uid, globalCode, classCode, rerender);
}
/**
* Form the final, perfectly valid JS module code, and create the <script>
* element for it, to be inserted into the shadow DOM during render().
*/
setupCodeInjection(uid, globalCode, classCode, rerender) {
const width = this.getAttribute(`width`, 200);
const height = this.getAttribute(`height`, 200);
this.code = `
import { GraphicsAPI, Bezier, Vector, Matrix } from "${MODULE_PATH}/api/graphics-api.js";
${globalCode}
class Example extends GraphicsAPI {
${classCode}
}
new Example('${uid}', ${width}, ${height});
`;
const script = (this.script = document.createElement(`script`));
script.type = "module";
script.src = `data:application/javascript;charset=utf-8,${encodeURIComponent(
this.code
)}`;
if (rerender) this.render();
}
/**
* Hand the <graphics-element> a reference to the "Example" instance that it built.
*/
setGraphic(apiInstance) {
this.apiInstance = apiInstance;
this.setCanvas(apiInstance.canvas);
}
/**
* Locally bind the Example's canvas, since it needs to get added to the shadow DOM.
*/
setCanvas(canvas) {
this.canvas = canvas;
// Make sure focus works, Which is a CLUSTERFUCK OF BULLSHIT in the August, 2020,
// browser landscape, so this is the best I can give you right now. I am more
// disappointed about this than you will ever be.
this.canvas.setAttribute(`tabindex`, `0`);
this.canvas.addEventListener(`touchstart`, (evt) => this.canvas.focus());
this.canvas.addEventListener(`mousedown`, (evt) => this.canvas.focus());
// If we get here, there were no source code errors: undo the scheduled error print.
clearTimeout(this.errorPrintTimeout);
this.render();
}
/**
* This is a helper to aid debugging, mostly because dev tools are not super
* great at pointing you to the right line for an injected script that it
* can't actually find anywhere in the document or shadow DOM...
*/
printCodeDueToError() {
console.log(
this.code
.split(`\n`)
.map((l, pos) => `${pos + 1}: ${l}`)
.join(`\n`)
);
}
/**
* Regenerate the shadow DOM content.
*/
render() {
super.render();
if (this.script) {
if (!this.script.__inserted) {
// Schedule an error print, which will get cleared if there
// were no source code errors.
this.errorPrintTimeout = setTimeout(
() => this.printCodeDueToError(),
1000
);
this.script.__inserted = true;
this._shadow.appendChild(this.script);
}
}
const slotParent = this._slot.parentNode;
if (this.canvas) slotParent.insertBefore(this.canvas, this._slot);
if (this.label) slotParent.insertBefore(this.label, this._slot);
const a = document.createElement("a");
a.classList.add("view-source");
a.textContent = `view source`;
a.href = new URL(
`data:text/plain;charset=utf-8,${encodeURIComponent(this.rawCode)}`
);
a.target = `_blank`;
if (this.label) slotParent.insertBefore(a, this.canvas);
}
}
CustomElement.register(GraphicsElement);
// This is ridiculous, but the easiest way to fix the mess that is
// Firefox's handling of scroll position when custom elements contain
// focussabable, slotted elements is to just force-scroll the document,
// so that it knows where it actually is. Similarly, if this is a hash
// navigation, force that hash. Chrome does not have this problem.
if (typeof window !== undefined) {
window.addEventListener(`DOMContentReady`, (evt) => window.scrollBy(0, 1));
setTimeout(() => {
if (window.location.hash) {
window.location.hash = window.location.hash;
} else {
window.scrollBy(0, 1);
}
}, 200);
}
export { GraphicsElement };

View File

@@ -0,0 +1,968 @@
/**
A javascript Bezier curve library by Pomax.
Based on http://pomax.github.io/bezierinfo
This code is MIT licensed.
**/
import { utils } from "./utils.js";
import { PolyBezier } from "./poly-bezier.js";
import { convertPath } from "./svg-to-beziers.js";
// math-inlining.
const { abs, min, max, cos, sin, acos, sqrt } = Math;
const pi = Math.PI;
// a zero coordinate, which is surprisingly useful
const ZERO = { x: 0, y: 0, z: 0 };
// TODO: figure out where this function goes, it has no reason to exist on its lonesome.
function getABC(n, S, B, E, t) {
if (typeof t === "undefined") {
t = 0.5;
}
const u = utils.projectionratio(t, n),
um = 1 - u,
C = {
x: u * S.x + um * E.x,
y: u * S.y + um * E.y,
},
s = utils.abcratio(t, n),
A = {
x: B.x + (B.x - C.x) / s,
y: B.y + (B.y - C.y) / s,
};
return { A: A, B: B, C: C };
}
/**
* Bezier curve constructor.
*
* ...docs pending...
*/
class Bezier {
constructor(coords) {
let args =
coords && coords.forEach ? coords : Array.from(arguments).slice();
let coordlen = false;
if (typeof args[0] === "object") {
coordlen = args.length;
const newargs = [];
args.forEach(function (point) {
["x", "y", "z"].forEach(function (d) {
if (typeof point[d] !== "undefined") {
newargs.push(point[d]);
}
});
});
args = newargs;
}
let higher = false;
const len = args.length;
if (coordlen) {
if (coordlen > 4) {
if (arguments.length !== 1) {
throw new Error(
"Only new Bezier(point[]) is accepted for 4th and higher order curves"
);
}
higher = true;
}
} else {
if (len !== 6 && len !== 8 && len !== 9 && len !== 12) {
if (arguments.length !== 1) {
throw new Error(
"Only new Bezier(point[]) is accepted for 4th and higher order curves"
);
}
}
}
const _3d = (this._3d =
(!higher && (len === 9 || len === 12)) ||
(coords && coords[0] && typeof coords[0].z !== "undefined"));
const points = (this.points = []);
for (let idx = 0, step = _3d ? 3 : 2; idx < len; idx += step) {
var point = {
x: args[idx],
y: args[idx + 1],
};
if (_3d) {
point.z = args[idx + 2];
}
points.push(point);
}
const order = (this.order = points.length - 1);
const dims = (this.dims = ["x", "y"]);
if (_3d) dims.push("z");
this.dimlen = dims.length;
const aligned = utils.align(points, { p1: points[0], p2: points[order] });
this._linear = !aligned.some((p) => abs(p.y) > 0.0001);
this._lut = [];
this._t1 = 0;
this._t2 = 1;
this.update();
}
static SVGtoBeziers = function (d) {
return convertPath(Bezier, d);
};
static quadraticFromPoints(p1, p2, p3, t) {
if (typeof t === "undefined") {
t = 0.5;
}
// shortcuts, although they're really dumb
if (t === 0) {
return new Bezier(p2, p2, p3);
}
if (t === 1) {
return new Bezier(p1, p2, p2);
}
// real fitting.
const abc = getABC(2, p1, p2, p3, t);
return new Bezier(p1, abc.A, p3);
}
static cubicFromPoints(S, B, E, t, d1) {
if (typeof t === "undefined") {
t = 0.5;
}
const abc = getABC(3, S, B, E, t);
if (typeof d1 === "undefined") {
d1 = utils.dist(B, abc.C);
}
const d2 = (d1 * (1 - t)) / t;
const selen = utils.dist(S, E),
lx = (E.x - S.x) / selen,
ly = (E.y - S.y) / selen,
bx1 = d1 * lx,
by1 = d1 * ly,
bx2 = d2 * lx,
by2 = d2 * ly;
// derivation of new hull coordinates
const e1 = { x: B.x - bx1, y: B.y - by1 },
e2 = { x: B.x + bx2, y: B.y + by2 },
A = abc.A,
v1 = { x: A.x + (e1.x - A.x) / (1 - t), y: A.y + (e1.y - A.y) / (1 - t) },
v2 = { x: A.x + (e2.x - A.x) / t, y: A.y + (e2.y - A.y) / t },
nc1 = { x: S.x + (v1.x - S.x) / t, y: S.y + (v1.y - S.y) / t },
nc2 = {
x: E.x + (v2.x - E.x) / (1 - t),
y: E.y + (v2.y - E.y) / (1 - t),
};
// ...done
return new Bezier(S, nc1, nc2, E);
}
static getUtils() {
return utils;
}
getUtils() {
return Bezier.getUtils();
}
static get PolyBezier() {
return PolyBezier;
}
valueOf() {
return this.toString();
}
toString() {
return utils.pointsToString(this.points);
}
toSVG() {
if (this._3d) return false;
const p = this.points,
x = p[0].x,
y = p[0].y,
s = ["M", x, y, this.order === 2 ? "Q" : "C"];
for (let i = 1, last = p.length; i < last; i++) {
s.push(p[i].x);
s.push(p[i].y);
}
return s.join(" ");
}
setRatios(ratios) {
if (ratios.length !== this.points.length) {
throw new Error("incorrect number of ratio values");
}
this.ratios = ratios;
this._lut = []; // invalidate any precomputed LUT
}
verify() {
const print = this.coordDigest();
if (print !== this._print) {
this._print = print;
this.update();
}
}
coordDigest() {
return this.points
.map(function (c, pos) {
return "" + pos + c.x + c.y + (c.z ? c.z : 0);
})
.join("");
}
update() {
// invalidate any precomputed LUT
this._lut = [];
this.dpoints = utils.derive(this.points, this._3d);
this.computedirection();
}
computedirection() {
const points = this.points;
const angle = utils.angle(points[0], points[this.order], points[1]);
this.clockwise = angle > 0;
}
length() {
return utils.length(this.derivative.bind(this));
}
getLUT(steps) {
this.verify();
steps = steps || 100;
if (this._lut.length === steps) {
return this._lut;
}
this._lut = [];
// We want a range from 0 to 1 inclusive, so
// we decrement and then use <= rather than <:
steps--;
for (let t = 0; t <= steps; t++) {
this._lut.push(this.compute(t / steps));
}
return this._lut;
}
on(point, error) {
error = error || 5;
const lut = this.getLUT(),
hits = [];
for (let i = 0, c, t = 0; i < lut.length; i++) {
c = lut[i];
if (utils.dist(c, point) < error) {
hits.push(c);
t += i / lut.length;
}
}
if (!hits.length) return false;
return (t /= hits.length);
}
project(point) {
// step 1: coarse check
const LUT = this.getLUT(),
l = LUT.length - 1,
closest = utils.closest(LUT, point),
mpos = closest.mpos,
t1 = (mpos - 1) / l,
t2 = (mpos + 1) / l,
step = 0.1 / l;
// step 2: fine check
let mdist = closest.mdist,
t = t1,
ft = t,
p;
mdist += 1;
for (let d; t < t2 + step; t += step) {
p = this.compute(t);
d = utils.dist(point, p);
if (d < mdist) {
mdist = d;
ft = t;
}
}
p = this.compute(ft);
p.t = ft;
p.d = mdist;
return p;
}
get(t) {
return this.compute(t);
}
point(idx) {
return this.points[idx];
}
compute(t) {
if (this.ratios) {
return utils.computeWithRatios(t, this.points, this.ratios, this._3d);
}
return utils.compute(t, this.points, this._3d, this.ratios);
}
raise() {
const p = this.points,
np = [p[0]],
k = p.length;
for (let i = 1, pi, pim; i < k; i++) {
pi = p[i];
pim = p[i - 1];
np[i] = {
x: ((k - i) / k) * pi.x + (i / k) * pim.x,
y: ((k - i) / k) * pi.y + (i / k) * pim.y,
};
}
np[k] = p[k - 1];
return new Bezier(np);
}
derivative(t) {
const mt = 1 - t;
let a,
b,
c = 0,
p = this.dpoints[0];
if (this.order === 2) {
p = [p[0], p[1], ZERO];
a = mt;
b = t;
}
if (this.order === 3) {
a = mt * mt;
b = mt * t * 2;
c = t * t;
}
const ret = {
x: a * p[0].x + b * p[1].x + c * p[2].x,
y: a * p[0].y + b * p[1].y + c * p[2].y,
};
if (this._3d) {
ret.z = a * p[0].z + b * p[1].z + c * p[2].z;
}
return ret;
}
align() {
let p = this.points;
return new Bezier(utils.align(p, { p1: p[0], p2: p[p.length - 1] }));
}
curvature(t) {
return utils.curvature(t, this.points, this._3d);
}
inflections() {
return utils.inflections(this.points);
}
normal(t) {
return this._3d ? this.__normal3(t) : this.__normal2(t);
}
__normal2(t) {
const d = this.derivative(t);
const q = sqrt(d.x * d.x + d.y * d.y);
return { x: -d.y / q, y: d.x / q };
}
__normal3(t) {
// see http://stackoverflow.com/questions/25453159
const r1 = this.derivative(t),
r2 = this.derivative(t + 0.01),
q1 = sqrt(r1.x * r1.x + r1.y * r1.y + r1.z * r1.z),
q2 = sqrt(r2.x * r2.x + r2.y * r2.y + r2.z * r2.z);
r1.x /= q1;
r1.y /= q1;
r1.z /= q1;
r2.x /= q2;
r2.y /= q2;
r2.z /= q2;
// cross product
const c = {
x: r2.y * r1.z - r2.z * r1.y,
y: r2.z * r1.x - r2.x * r1.z,
z: r2.x * r1.y - r2.y * r1.x,
};
const m = sqrt(c.x * c.x + c.y * c.y + c.z * c.z);
c.x /= m;
c.y /= m;
c.z /= m;
// rotation matrix
const R = [
c.x * c.x,
c.x * c.y - c.z,
c.x * c.z + c.y,
c.x * c.y + c.z,
c.y * c.y,
c.y * c.z - c.x,
c.x * c.z - c.y,
c.y * c.z + c.x,
c.z * c.z,
];
// normal vector:
const n = {
x: R[0] * r1.x + R[1] * r1.y + R[2] * r1.z,
y: R[3] * r1.x + R[4] * r1.y + R[5] * r1.z,
z: R[6] * r1.x + R[7] * r1.y + R[8] * r1.z,
};
return n;
}
hull(t) {
let p = this.points,
_p = [],
q = [],
idx = 0;
q[idx++] = p[0];
q[idx++] = p[1];
q[idx++] = p[2];
if (this.order === 3) {
q[idx++] = p[3];
}
// we lerp between all points at each iteration, until we have 1 point left.
while (p.length > 1) {
_p = [];
for (let i = 0, pt, l = p.length - 1; i < l; i++) {
pt = utils.lerp(t, p[i], p[i + 1]);
q[idx++] = pt;
_p.push(pt);
}
p = _p;
}
return q;
}
split(t1, t2) {
// shortcuts
if (t1 === 0 && !!t2) {
return this.split(t2).left;
}
if (t2 === 1) {
return this.split(t1).right;
}
// no shortcut: use "de Casteljau" iteration.
const q = this.hull(t1);
const result = {
left:
this.order === 2
? new Bezier([q[0], q[3], q[5]])
: new Bezier([q[0], q[4], q[7], q[9]]),
right:
this.order === 2
? new Bezier([q[5], q[4], q[2]])
: new Bezier([q[9], q[8], q[6], q[3]]),
span: q,
};
// make sure we bind _t1/_t2 information!
result.left._t1 = utils.map(0, 0, 1, this._t1, this._t2);
result.left._t2 = utils.map(t1, 0, 1, this._t1, this._t2);
result.right._t1 = utils.map(t1, 0, 1, this._t1, this._t2);
result.right._t2 = utils.map(1, 0, 1, this._t1, this._t2);
// if we have no t2, we're done
if (!t2) {
return result;
}
// if we have a t2, split again:
t2 = utils.map(t2, t1, 1, 0, 1);
return result.right.split(t2).left;
}
extrema() {
const result = {};
let roots = [];
this.dims.forEach(
function (dim) {
let mfn = function (v) {
return v[dim];
};
let p = this.dpoints[0].map(mfn);
result[dim] = utils.droots(p);
if (this.order === 3) {
p = this.dpoints[1].map(mfn);
result[dim] = result[dim].concat(utils.droots(p));
}
result[dim] = result[dim].filter(function (t) {
return t >= 0 && t <= 1;
});
roots = roots.concat(result[dim].sort(utils.numberSort));
}.bind(this)
);
result.values = roots.sort(utils.numberSort).filter(function (v, idx) {
return roots.indexOf(v) === idx;
});
return result;
}
bbox() {
const extrema = this.extrema(),
result = {};
this.dims.forEach(
function (d) {
result[d] = utils.getminmax(this, d, extrema[d]);
}.bind(this)
);
return result;
}
overlaps(curve) {
const lbbox = this.bbox(),
tbbox = curve.bbox();
return utils.bboxoverlap(lbbox, tbbox);
}
offset(t, d) {
if (typeof d !== "undefined") {
const c = this.get(t),
n = this.normal(t);
const ret = {
c: c,
n: n,
x: c.x + n.x * d,
y: c.y + n.y * d,
};
if (this._3d) {
ret.z = c.z + n.z * d;
}
return ret;
}
if (this._linear) {
const nv = this.normal(0),
coords = this.points.map(function (p) {
const ret = {
x: p.x + t * nv.x,
y: p.y + t * nv.y,
};
if (p.z && nv.z) {
ret.z = p.z + t * nv.z;
}
return ret;
});
return [new Bezier(coords)];
}
return this.reduce().map(function (s) {
if (s._linear) {
return s.offset(t)[0];
}
return s.scale(t);
});
}
simple() {
if (this.order === 3) {
const a1 = utils.angle(this.points[0], this.points[3], this.points[1]);
const a2 = utils.angle(this.points[0], this.points[3], this.points[2]);
if ((a1 > 0 && a2 < 0) || (a1 < 0 && a2 > 0)) return false;
}
const n1 = this.normal(0);
const n2 = this.normal(1);
let s = n1.x * n2.x + n1.y * n2.y;
if (this._3d) {
s += n1.z * n2.z;
}
return abs(acos(s)) < pi / 3;
}
reduce() {
// TODO: examine these var types in more detail...
let i,
t1 = 0,
t2 = 0,
step = 0.01,
segment,
pass1 = [],
pass2 = [];
// first pass: split on extrema
let extrema = this.extrema().values;
if (extrema.indexOf(0) === -1) {
extrema = [0].concat(extrema);
}
if (extrema.indexOf(1) === -1) {
extrema.push(1);
}
for (t1 = extrema[0], i = 1; i < extrema.length; i++) {
t2 = extrema[i];
segment = this.split(t1, t2);
segment._t1 = t1;
segment._t2 = t2;
pass1.push(segment);
t1 = t2;
}
// second pass: further reduce these segments to simple segments
pass1.forEach(function (p1) {
t1 = 0;
t2 = 0;
while (t2 <= 1) {
for (t2 = t1 + step; t2 <= 1 + step; t2 += step) {
segment = p1.split(t1, t2);
if (!segment.simple()) {
t2 -= step;
if (abs(t1 - t2) < step) {
// we can never form a reduction
return [];
}
segment = p1.split(t1, t2);
segment._t1 = utils.map(t1, 0, 1, p1._t1, p1._t2);
segment._t2 = utils.map(t2, 0, 1, p1._t1, p1._t2);
pass2.push(segment);
t1 = t2;
break;
}
}
}
if (t1 < 1) {
segment = p1.split(t1, 1);
segment._t1 = utils.map(t1, 0, 1, p1._t1, p1._t2);
segment._t2 = p1._t2;
pass2.push(segment);
}
});
return pass2;
}
scale(d) {
const order = this.order;
let distanceFn = false;
if (typeof d === "function") {
distanceFn = d;
}
if (distanceFn && order === 2) {
return this.raise().scale(distanceFn);
}
// TODO: add special handling for degenerate (=linear) curves.
const clockwise = this.clockwise;
const r1 = distanceFn ? distanceFn(0) : d;
const r2 = distanceFn ? distanceFn(1) : d;
const v = [this.offset(0, 10), this.offset(1, 10)];
const points = this.points;
const np = [];
const o = utils.lli4(v[0], v[0].c, v[1], v[1].c);
if (!o) {
throw new Error("cannot scale this curve. Try reducing it first.");
}
// move all points by distance 'd' wrt the origin 'o'
// move end points by fixed distance along normal.
[0, 1].forEach(function (t) {
const p = (np[t * order] = utils.copy(points[t * order]));
p.x += (t ? r2 : r1) * v[t].n.x;
p.y += (t ? r2 : r1) * v[t].n.y;
});
if (!distanceFn) {
// move control points to lie on the intersection of the offset
// derivative vector, and the origin-through-control vector
[0, 1].forEach((t) => {
if (order === 2 && !!t) return;
const p = np[t * order];
const d = this.derivative(t);
const p2 = { x: p.x + d.x, y: p.y + d.y };
np[t + 1] = utils.lli4(p, p2, o, points[t + 1]);
});
return new Bezier(np);
}
// move control points by "however much necessary to
// ensure the correct tangent to endpoint".
[0, 1].forEach(function (t) {
if (order === 2 && !!t) return;
var p = points[t + 1];
var ov = {
x: p.x - o.x,
y: p.y - o.y,
};
var rc = distanceFn ? distanceFn((t + 1) / order) : d;
if (distanceFn && !clockwise) rc = -rc;
var m = sqrt(ov.x * ov.x + ov.y * ov.y);
ov.x /= m;
ov.y /= m;
np[t + 1] = {
x: p.x + rc * ov.x,
y: p.y + rc * ov.y,
};
});
return new Bezier(np);
}
outline(d1, d2, d3, d4) {
d2 = typeof d2 === "undefined" ? d1 : d2;
const reduced = this.reduce(),
len = reduced.length,
fcurves = [],
bcurves = [];
let p,
alen = 0,
tlen = this.length();
const graduated = typeof d3 !== "undefined" && typeof d4 !== "undefined";
function linearDistanceFunction(s, e, tlen, alen, slen) {
return function (v) {
const f1 = alen / tlen,
f2 = (alen + slen) / tlen,
d = e - s;
return utils.map(v, 0, 1, s + f1 * d, s + f2 * d);
};
}
// form curve oulines
reduced.forEach(function (segment) {
slen = segment.length();
if (graduated) {
fcurves.push(
segment.scale(linearDistanceFunction(d1, d3, tlen, alen, slen))
);
bcurves.push(
segment.scale(linearDistanceFunction(-d2, -d4, tlen, alen, slen))
);
} else {
fcurves.push(segment.scale(d1));
bcurves.push(segment.scale(-d2));
}
alen += slen;
});
// reverse the "return" outline
bcurves = bcurves
.map(function (s) {
p = s.points;
if (p[3]) {
s.points = [p[3], p[2], p[1], p[0]];
} else {
s.points = [p[2], p[1], p[0]];
}
return s;
})
.reverse();
// form the endcaps as lines
const fs = fcurves[0].points[0],
fe = fcurves[len - 1].points[fcurves[len - 1].points.length - 1],
bs = bcurves[len - 1].points[bcurves[len - 1].points.length - 1],
be = bcurves[0].points[0],
ls = utils.makeline(bs, fs),
le = utils.makeline(fe, be),
segments = [ls].concat(fcurves).concat([le]).concat(bcurves),
slen = segments.length;
return new PolyBezier(segments);
}
outlineshapes(d1, d2, curveIntersectionThreshold) {
d2 = d2 || d1;
const outline = this.outline(d1, d2).curves;
const shapes = [];
for (let i = 1, len = outline.length; i < len / 2; i++) {
const shape = utils.makeshape(
outline[i],
outline[len - i],
curveIntersectionThreshold
);
shape.startcap.virtual = i > 1;
shape.endcap.virtual = i < len / 2 - 1;
shapes.push(shape);
}
return shapes;
}
intersects(curve, curveIntersectionThreshold) {
if (!curve) return this.selfintersects(curveIntersectionThreshold);
if (curve.p1 && curve.p2) {
return this.lineIntersects(curve);
}
if (curve instanceof Bezier) {
curve = curve.reduce();
}
return this.curveintersects(
this.reduce(),
curve,
curveIntersectionThreshold
);
}
lineIntersects(line) {
const mx = min(line.p1.x, line.p2.x),
my = min(line.p1.y, line.p2.y),
MX = max(line.p1.x, line.p2.x),
MY = max(line.p1.y, line.p2.y);
return utils.roots(this.points, line).filter((t) => {
var p = this.get(t);
return utils.between(p.x, mx, MX) && utils.between(p.y, my, MY);
});
}
selfintersects(curveIntersectionThreshold) {
// "simple" curves cannot intersect with their direct
// neighbour, so for each segment X we check whether
// it intersects [0:x-2][x+2:last].
const reduced = this.reduce(),
len = reduced.length - 2,
results = [];
for (let i = 0, result, left, right; i < len; i++) {
left = reduced.slice(i, i + 1);
right = reduced.slice(i + 2);
result = this.curveintersects(left, right, curveIntersectionThreshold);
results = results.concat(result);
}
return results;
}
curveintersects(c1, c2, curveIntersectionThreshold) {
const pairs = [];
// step 1: pair off any overlapping segments
c1.forEach(function (l) {
c2.forEach(function (r) {
if (l.overlaps(r)) {
pairs.push({ left: l, right: r });
}
});
});
// step 2: for each pairing, run through the convergence algorithm.
let intersections = [];
pairs.forEach(function (pair) {
const result = utils.pairiteration(
pair.left,
pair.right,
curveIntersectionThreshold
);
if (result.length > 0) {
intersections = intersections.concat(result);
}
});
return intersections;
}
arcs(errorThreshold) {
errorThreshold = errorThreshold || 0.5;
return this._iterate(errorThreshold, []);
}
_error(pc, np1, s, e) {
const q = (e - s) / 4,
c1 = this.get(s + q),
c2 = this.get(e - q),
ref = utils.dist(pc, np1),
d1 = utils.dist(pc, c1),
d2 = utils.dist(pc, c2);
return abs(d1 - ref) + abs(d2 - ref);
}
_iterate(errorThreshold, circles) {
let t_s = 0,
t_e = 1,
safety;
// we do a binary search to find the "good `t` closest to no-longer-good"
do {
safety = 0;
// step 1: start with the maximum possible arc
t_e = 1;
// points:
let np1 = this.get(t_s),
np2,
np3,
arc,
prev_arc;
// booleans:
let curr_good = false,
prev_good = false,
done;
// numbers:
let t_m = t_e,
prev_e = 1,
step = 0;
// step 2: find the best possible arc
do {
prev_good = curr_good;
prev_arc = arc;
t_m = (t_s + t_e) / 2;
step++;
np2 = this.get(t_m);
np3 = this.get(t_e);
arc = utils.getccenter(np1, np2, np3);
//also save the t values
arc.interval = {
start: t_s,
end: t_e,
};
let error = this._error(arc, np1, t_s, t_e);
curr_good = error <= errorThreshold;
done = prev_good && !curr_good;
if (!done) prev_e = t_e;
// this arc is fine: we can move 'e' up to see if we can find a wider arc
if (curr_good) {
// if e is already at max, then we're done for this arc.
if (t_e >= 1) {
// make sure we cap at t=1
arc.interval.end = prev_e = 1;
prev_arc = arc;
// if we capped the arc segment to t=1 we also need to make sure that
// the arc's end angle is correct with respect to the bezier end point.
if (t_e > 1) {
let d = {
x: arc.x + arc.r * cos(arc.e),
y: arc.y + arc.r * sin(arc.e),
};
arc.e += utils.angle({ x: arc.x, y: arc.y }, d, this.get(1));
}
break;
}
// if not, move it up by half the iteration distance
t_e = t_e + (t_e - t_s) / 2;
} else {
// this is a bad arc: we need to move 'e' down to find a good arc
t_e = t_m;
}
} while (!done && safety++ < 100);
if (safety >= 100) {
break;
}
// console.log("L835: [F] arc found", t_s, prev_e, prev_arc.x, prev_arc.y, prev_arc.s, prev_arc.e);
prev_arc = prev_arc ? prev_arc : arc;
circles.push(prev_arc);
t_s = prev_e;
} while (t_e < 1);
return circles;
}
}
export { Bezier };

View File

@@ -0,0 +1,267 @@
/**
* Normalise an SVG path to absolute coordinates
* and full commands, rather than relative coordinates
* and/or shortcut commands.
*/
export default function normalizePath(d) {
// preprocess "d" so that we have spaces between values
d = d
.replace(/,/g, " ") // replace commas with spaces
.replace(/-/g, " - ") // add spacing around minus signs
.replace(/-\s+/g, "-") // remove spacing to the right of minus signs.
.replace(/([a-zA-Z])/g, " $1 ");
// set up the variables used in this function
const instructions = d.replace(/([a-zA-Z])\s?/g, "|$1").split("|"),
instructionLength = instructions.length;
let i,
instruction,
op,
lop,
args = [],
alen,
a,
sx = 0,
sy = 0,
x = 0,
y = 0,
cx = 0,
cy = 0,
cx2 = 0,
cy2 = 0,
rx = 0,
ry = 0,
xrot = 0,
lflag = 0,
sweep = 0,
normalized = "";
// we run through the instruction list starting at 1, not 0,
// because we split up "|M x y ...." so the first element will
// always be an empty string. By design.
for (i = 1; i < instructionLength; i++) {
// which instruction is this?
instruction = instructions[i];
op = instruction.substring(0, 1);
lop = op.toLowerCase();
// what are the arguments? note that we need to convert
// all strings into numbers, or + will do silly things.
args = instruction.replace(op, "").trim().split(" ");
args = args
.filter(function (v) {
return v !== "";
})
.map(parseFloat);
alen = args.length;
// we could use a switch, but elaborate code in a "case" with
// fallthrough is just horrid to read. So let's use ifthen
// statements instead.
// moveto command (plus possible lineto)
if (lop === "m") {
normalized += "M ";
if (op === "m") {
x += args[0];
y += args[1];
} else {
x = args[0];
y = args[1];
}
// records start position, for dealing
// with the shape close operator ('Z')
sx = x;
sy = y;
normalized += x + " " + y + " ";
if (alen > 2) {
for (a = 0; a < alen; a += 2) {
if (op === "m") {
x += args[a];
y += args[a + 1];
} else {
x = args[a];
y = args[a + 1];
}
normalized += "L " + x + " " + y + " ";
}
}
}
// lineto commands
else if (lop === "l") {
for (a = 0; a < alen; a += 2) {
if (op === "l") {
x += args[a];
y += args[a + 1];
} else {
x = args[a];
y = args[a + 1];
}
normalized += "L " + x + " " + y + " ";
}
} else if (lop === "h") {
for (a = 0; a < alen; a++) {
if (op === "h") {
x += args[a];
} else {
x = args[a];
}
normalized += "L " + x + " " + y + " ";
}
} else if (lop === "v") {
for (a = 0; a < alen; a++) {
if (op === "v") {
y += args[a];
} else {
y = args[a];
}
normalized += "L " + x + " " + y + " ";
}
}
// quadratic curveto commands
else if (lop === "q") {
for (a = 0; a < alen; a += 4) {
if (op === "q") {
cx = x + args[a];
cy = y + args[a + 1];
x += args[a + 2];
y += args[a + 3];
} else {
cx = args[a];
cy = args[a + 1];
x = args[a + 2];
y = args[a + 3];
}
normalized += "Q " + cx + " " + cy + " " + x + " " + y + " ";
}
} else if (lop === "t") {
for (a = 0; a < alen; a += 2) {
// reflect previous cx/cy over x/y
cx = x + (x - cx);
cy = y + (y - cy);
// then get real end point
if (op === "t") {
x += args[a];
y += args[a + 1];
} else {
x = args[a];
y = args[a + 1];
}
normalized += "Q " + cx + " " + cy + " " + x + " " + y + " ";
}
}
// cubic curveto commands
else if (lop === "c") {
for (a = 0; a < alen; a += 6) {
if (op === "c") {
cx = x + args[a];
cy = y + args[a + 1];
cx2 = x + args[a + 2];
cy2 = y + args[a + 3];
x += args[a + 4];
y += args[a + 5];
} else {
cx = args[a];
cy = args[a + 1];
cx2 = args[a + 2];
cy2 = args[a + 3];
x = args[a + 4];
y = args[a + 5];
}
normalized +=
"C " +
cx +
" " +
cy +
" " +
cx2 +
" " +
cy2 +
" " +
x +
" " +
y +
" ";
}
} else if (lop === "s") {
for (a = 0; a < alen; a += 4) {
// reflect previous cx2/cy2 over x/y
cx = x + (x - cx2);
cy = y + (y - cy2);
// then get real control and end point
if (op === "s") {
cx2 = x + args[a];
cy2 = y + args[a + 1];
x += args[a + 2];
y += args[a + 3];
} else {
cx2 = args[a];
cy2 = args[a + 1];
x = args[a + 2];
y = args[a + 3];
}
normalized +=
"C " +
cx +
" " +
cy +
" " +
cx2 +
" " +
cy2 +
" " +
x +
" " +
y +
" ";
}
}
// rx ry x-axis-rotation large-arc-flag sweep-flag x y
// a 25,25 -30 0, 1 50,-25
// arc command
else if (lop === "a") {
for (a = 0; a < alen; a += 7) {
rx = args[a];
ry = args[a + 1];
xrot = args[a + 2];
lflag = args[a + 3];
sweep = args[a + 4];
if (op === "a") {
x += args[a + 5];
y += args[a + 6];
} else {
x = args[a + 5];
y = args[a + 6];
}
normalized +=
"A " +
rx +
" " +
ry +
" " +
xrot +
" " +
lflag +
" " +
sweep +
" " +
x +
" " +
y +
" ";
}
} else if (lop === "z") {
normalized += "Z ";
// not unimportant: path closing changes the current x/y coordinate
x = sx;
y = sy;
}
}
return normalized.trim();
}

View File

@@ -0,0 +1,70 @@
import { utils } from "./utils.js";
/**
* Poly Bezier
* @param {[type]} curves [description]
*/
class PolyBezier {
constructor(curves) {
this.curves = [];
this._3d = false;
if (!!curves) {
this.curves = curves;
this._3d = this.curves[0]._3d;
}
}
valueOf() {
return this.toString();
}
toString() {
return (
"[" +
this.curves
.map(function (curve) {
return utils.pointsToString(curve.points);
})
.join(", ") +
"]"
);
}
addCurve(curve) {
this.curves.push(curve);
this._3d = this._3d || curve._3d;
}
length() {
return this.curves
.map(function (v) {
return v.length();
})
.reduce(function (a, b) {
return a + b;
});
}
curve(idx) {
return this.curves[idx];
}
bbox() {
const c = this.curves;
var bbox = c[0].bbox();
for (var i = 1; i < c.length; i++) {
utils.expandbox(bbox, c[i].bbox());
}
return bbox;
}
offset(d) {
const offset = [];
this.curves.forEach(function (v) {
offset = offset.concat(v.offset(d));
});
return new PolyBezier(offset);
}
}
export { PolyBezier };

View File

@@ -0,0 +1,45 @@
import normalise from "./normalise-svg.js";
let M = { x: false, y: false };
/**
* ...
*/
function makeBezier(Bezier, term, values) {
if (term === "Z") return;
if (term === "M") {
M = { x: values[0], y: values[1] };
return;
}
const curve = new Bezier(M.x, M.y, ...values);
const last = values.slice(-2);
M = { x: last[0], y: last[1] };
return curve;
}
/**
* ...
*/
function convertPath(Bezier, d) {
const terms = normalise(d).split(" "),
matcher = new RegExp("[MLCQZ]", "");
let term,
segment,
values,
segments = [],
ARGS = { C: 6, Q: 4, L: 2, M: 2 };
while (terms.length) {
term = terms.splice(0, 1)[0];
if (matcher.test(term)) {
values = terms.splice(0, ARGS[term]).map(parseFloat);
segment = makeBezier(Bezier, term, values);
if (segment) segments.push(segment);
}
}
return new Bezier.PolyBezier(segments);
}
export { convertPath };

View File

@@ -0,0 +1,908 @@
import { Bezier } from "./bezier.js";
// math-inlining.
const { abs, cos, sin, acos, atan2, sqrt, pow } = Math;
// cube root function yielding real roots
function crt(v) {
return v < 0 ? -pow(-v, 1 / 3) : pow(v, 1 / 3);
}
// trig constants
const pi = Math.PI,
tau = 2 * pi,
quart = pi / 2,
// float precision significant decimal
epsilon = 0.000001,
// extremas used in bbox calculation and similar algorithms
nMax = Number.MAX_SAFE_INTEGER || 9007199254740991,
nMin = Number.MIN_SAFE_INTEGER || -9007199254740991,
// a zero coordinate, which is surprisingly useful
ZERO = { x: 0, y: 0, z: 0 };
// Bezier utility functions
const utils = {
// Legendre-Gauss abscissae with n=24 (x_i values, defined at i=n as the roots of the nth order Legendre polynomial Pn(x))
Tvalues: [
-0.0640568928626056260850430826247450385909,
0.0640568928626056260850430826247450385909,
-0.1911188674736163091586398207570696318404,
0.1911188674736163091586398207570696318404,
-0.3150426796961633743867932913198102407864,
0.3150426796961633743867932913198102407864,
-0.4337935076260451384870842319133497124524,
0.4337935076260451384870842319133497124524,
-0.5454214713888395356583756172183723700107,
0.5454214713888395356583756172183723700107,
-0.6480936519369755692524957869107476266696,
0.6480936519369755692524957869107476266696,
-0.7401241915785543642438281030999784255232,
0.7401241915785543642438281030999784255232,
-0.8200019859739029219539498726697452080761,
0.8200019859739029219539498726697452080761,
-0.8864155270044010342131543419821967550873,
0.8864155270044010342131543419821967550873,
-0.9382745520027327585236490017087214496548,
0.9382745520027327585236490017087214496548,
-0.9747285559713094981983919930081690617411,
0.9747285559713094981983919930081690617411,
-0.9951872199970213601799974097007368118745,
0.9951872199970213601799974097007368118745,
],
// Legendre-Gauss weights with n=24 (w_i values, defined by a function linked to in the Bezier primer article)
Cvalues: [
0.1279381953467521569740561652246953718517,
0.1279381953467521569740561652246953718517,
0.1258374563468282961213753825111836887264,
0.1258374563468282961213753825111836887264,
0.121670472927803391204463153476262425607,
0.121670472927803391204463153476262425607,
0.1155056680537256013533444839067835598622,
0.1155056680537256013533444839067835598622,
0.1074442701159656347825773424466062227946,
0.1074442701159656347825773424466062227946,
0.0976186521041138882698806644642471544279,
0.0976186521041138882698806644642471544279,
0.086190161531953275917185202983742667185,
0.086190161531953275917185202983742667185,
0.0733464814110803057340336152531165181193,
0.0733464814110803057340336152531165181193,
0.0592985849154367807463677585001085845412,
0.0592985849154367807463677585001085845412,
0.0442774388174198061686027482113382288593,
0.0442774388174198061686027482113382288593,
0.0285313886289336631813078159518782864491,
0.0285313886289336631813078159518782864491,
0.0123412297999871995468056670700372915759,
0.0123412297999871995468056670700372915759,
],
arcfn: function (t, derivativeFn) {
const d = derivativeFn(t);
let l = d.x * d.x + d.y * d.y;
if (typeof d.z !== "undefined") {
l += d.z * d.z;
}
return sqrt(l);
},
compute: function (t, points, _3d) {
// shortcuts
if (t === 0) {
return points[0];
}
const order = points.length - 1;
if (t === 1) {
return points[order];
}
const mt = 1 - t;
let p = points;
// constant?
if (order === 0) {
return points[0];
}
// linear?
if (order === 1) {
const ret = {
x: mt * p[0].x + t * p[1].x,
y: mt * p[0].y + t * p[1].y,
};
if (_3d) {
ret.z = mt * p[0].z + t * p[1].z;
}
return ret;
}
// quadratic/cubic curve?
if (order < 4) {
let mt2 = mt * mt,
t2 = t * t,
a,
b,
c,
d = 0;
if (order === 2) {
p = [p[0], p[1], p[2], ZERO];
a = mt2;
b = mt * t * 2;
c = t2;
} else if (order === 3) {
a = mt2 * mt;
b = mt2 * t * 3;
c = mt * t2 * 3;
d = t * t2;
}
const ret = {
x: a * p[0].x + b * p[1].x + c * p[2].x + d * p[3].x,
y: a * p[0].y + b * p[1].y + c * p[2].y + d * p[3].y,
};
if (_3d) {
ret.z = a * p[0].z + b * p[1].z + c * p[2].z + d * p[3].z;
}
return ret;
}
// higher order curves: use de Casteljau's computation
const dCpts = JSON.parse(JSON.stringify(points));
while (dCpts.length > 1) {
for (let i = 0; i < dCpts.length - 1; i++) {
dCpts[i] = {
x: dCpts[i].x + (dCpts[i + 1].x - dCpts[i].x) * t,
y: dCpts[i].y + (dCpts[i + 1].y - dCpts[i].y) * t,
};
if (typeof dCpts[i].z !== "undefined") {
dCpts[i] = dCpts[i].z + (dCpts[i + 1].z - dCpts[i].z) * t;
}
}
dCpts.splice(dCpts.length - 1, 1);
}
return dCpts[0];
},
computeWithRatios: function (t, points, ratios, _3d) {
const mt = 1 - t,
r = ratios,
p = points;
let f1 = r[0],
f2 = r[1],
f3 = r[2],
f4 = r[3],
d;
// spec for linear
f1 *= mt;
f2 *= t;
if (p.length === 2) {
d = f1 + f2;
return {
x: (f1 * p[0].x + f2 * p[1].x) / d,
y: (f1 * p[0].y + f2 * p[1].y) / d,
z: !_3d ? false : (f1 * p[0].z + f2 * p[1].z) / d,
};
}
// upgrade to quadratic
f1 *= mt;
f2 *= 2 * mt;
f3 *= t * t;
if (p.length === 3) {
d = f1 + f2 + f3;
return {
x: (f1 * p[0].x + f2 * p[1].x + f3 * p[2].x) / d,
y: (f1 * p[0].y + f2 * p[1].y + f3 * p[2].y) / d,
z: !_3d ? false : (f1 * p[0].z + f2 * p[1].z + f3 * p[2].z) / d,
};
}
// upgrade to cubic
f1 *= mt;
f2 *= 1.5 * mt;
f3 *= 3 * mt;
f4 *= t * t * t;
if (p.length === 4) {
d = f1 + f2 + f3 + f4;
return {
x: (f1 * p[0].x + f2 * p[1].x + f3 * p[2].x + f4 * p[3].x) / d,
y: (f1 * p[0].y + f2 * p[1].y + f3 * p[2].y + f4 * p[3].y) / d,
z: !_3d
? false
: (f1 * p[0].z + f2 * p[1].z + f3 * p[2].z + f4 * p[3].z) / d,
};
}
},
derive: function (points, _3d) {
const dpoints = [];
for (let p = points, d = p.length, c = d - 1; d > 1; d--, c--) {
const list = [];
for (let j = 0, dpt; j < c; j++) {
dpt = {
x: c * (p[j + 1].x - p[j].x),
y: c * (p[j + 1].y - p[j].y),
};
if (_3d) {
dpt.z = c * (p[j + 1].z - p[j].z);
}
list.push(dpt);
}
dpoints.push(list);
p = list;
}
return dpoints;
},
between: function (v, m, M) {
return (
(m <= v && v <= M) ||
utils.approximately(v, m) ||
utils.approximately(v, M)
);
},
approximately: function (a, b, precision) {
return abs(a - b) <= (precision || epsilon);
},
length: function (derivativeFn) {
const z = 0.5,
len = utils.Tvalues.length;
let sum = 0;
for (let i = 0, t; i < len; i++) {
t = z * utils.Tvalues[i] + z;
sum += utils.Cvalues[i] * utils.arcfn(t, derivativeFn);
}
return z * sum;
},
map: function (v, ds, de, ts, te) {
const d1 = de - ds,
d2 = te - ts,
v2 = v - ds,
r = v2 / d1;
return ts + d2 * r;
},
lerp: function (r, v1, v2) {
const ret = {
x: v1.x + r * (v2.x - v1.x),
y: v1.y + r * (v2.y - v1.y),
};
if (!!v1.z && !!v2.z) {
ret.z = v1.z + r * (v2.z - v1.z);
}
return ret;
},
pointToString: function (p) {
let s = p.x + "/" + p.y;
if (typeof p.z !== "undefined") {
s += "/" + p.z;
}
return s;
},
pointsToString: function (points) {
return "[" + points.map(utils.pointToString).join(", ") + "]";
},
copy: function (obj) {
return JSON.parse(JSON.stringify(obj));
},
angle: function (o, v1, v2) {
const dx1 = v1.x - o.x,
dy1 = v1.y - o.y,
dx2 = v2.x - o.x,
dy2 = v2.y - o.y,
cross = dx1 * dy2 - dy1 * dx2,
dot = dx1 * dx2 + dy1 * dy2;
return atan2(cross, dot);
},
// round as string, to avoid rounding errors
round: function (v, d) {
const s = "" + v;
const pos = s.indexOf(".");
return parseFloat(s.substring(0, pos + 1 + d));
},
dist: function (p1, p2) {
const dx = p1.x - p2.x,
dy = p1.y - p2.y;
return sqrt(dx * dx + dy * dy);
},
closest: function (LUT, point) {
let mdist = pow(2, 63),
mpos,
d;
LUT.forEach(function (p, idx) {
d = utils.dist(point, p);
if (d < mdist) {
mdist = d;
mpos = idx;
}
});
return { mdist: mdist, mpos: mpos };
},
abcratio: function (t, n) {
// see ratio(t) note on http://pomax.github.io/bezierinfo/#abc
if (n !== 2 && n !== 3) {
return false;
}
if (typeof t === "undefined") {
t = 0.5;
} else if (t === 0 || t === 1) {
return t;
}
const bottom = pow(t, n) + pow(1 - t, n),
top = bottom - 1;
return abs(top / bottom);
},
projectionratio: function (t, n) {
// see u(t) note on http://pomax.github.io/bezierinfo/#abc
if (n !== 2 && n !== 3) {
return false;
}
if (typeof t === "undefined") {
t = 0.5;
} else if (t === 0 || t === 1) {
return t;
}
const top = pow(1 - t, n),
bottom = pow(t, n) + top;
return top / bottom;
},
lli8: function (x1, y1, x2, y2, x3, y3, x4, y4) {
const nx =
(x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4),
ny = (x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4),
d = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
if (d == 0) {
return false;
}
return { x: nx / d, y: ny / d };
},
lli4: function (p1, p2, p3, p4) {
const x1 = p1.x,
y1 = p1.y,
x2 = p2.x,
y2 = p2.y,
x3 = p3.x,
y3 = p3.y,
x4 = p4.x,
y4 = p4.y;
return utils.lli8(x1, y1, x2, y2, x3, y3, x4, y4);
},
lli: function (v1, v2) {
return utils.lli4(v1, v1.c, v2, v2.c);
},
makeline: function (p1, p2) {
const x1 = p1.x,
y1 = p1.y,
x2 = p2.x,
y2 = p2.y,
dx = (x2 - x1) / 3,
dy = (y2 - y1) / 3;
return new Bezier(
x1,
y1,
x1 + dx,
y1 + dy,
x1 + 2 * dx,
y1 + 2 * dy,
x2,
y2
);
},
findbbox: function (sections) {
let mx = nMax,
my = nMax,
MX = nMin,
MY = nMin;
sections.forEach(function (s) {
const bbox = s.bbox();
if (mx > bbox.x.min) mx = bbox.x.min;
if (my > bbox.y.min) my = bbox.y.min;
if (MX < bbox.x.max) MX = bbox.x.max;
if (MY < bbox.y.max) MY = bbox.y.max;
});
return {
x: { min: mx, mid: (mx + MX) / 2, max: MX, size: MX - mx },
y: { min: my, mid: (my + MY) / 2, max: MY, size: MY - my },
};
},
shapeintersections: function (
s1,
bbox1,
s2,
bbox2,
curveIntersectionThreshold
) {
if (!utils.bboxoverlap(bbox1, bbox2)) return [];
const intersections = [];
const a1 = [s1.startcap, s1.forward, s1.back, s1.endcap];
const a2 = [s2.startcap, s2.forward, s2.back, s2.endcap];
a1.forEach(function (l1) {
if (l1.virtual) return;
a2.forEach(function (l2) {
if (l2.virtual) return;
const iss = l1.intersects(l2, curveIntersectionThreshold);
if (iss.length > 0) {
iss.c1 = l1;
iss.c2 = l2;
iss.s1 = s1;
iss.s2 = s2;
intersections.push(iss);
}
});
});
return intersections;
},
makeshape: function (forward, back, curveIntersectionThreshold) {
const bpl = back.points.length;
const fpl = forward.points.length;
const start = utils.makeline(back.points[bpl - 1], forward.points[0]);
const end = utils.makeline(forward.points[fpl - 1], back.points[0]);
const shape = {
startcap: start,
forward: forward,
back: back,
endcap: end,
bbox: utils.findbbox([start, forward, back, end]),
};
shape.intersections = function (s2) {
return utils.shapeintersections(
shape,
shape.bbox,
s2,
s2.bbox,
curveIntersectionThreshold
);
};
return shape;
},
getminmax: function (curve, d, list) {
if (!list) return { min: 0, max: 0 };
let min = nMax,
max = nMin,
t,
c;
if (list.indexOf(0) === -1) {
list = [0].concat(list);
}
if (list.indexOf(1) === -1) {
list.push(1);
}
for (let i = 0, len = list.length; i < len; i++) {
t = list[i];
c = curve.get(t);
if (c[d] < min) {
min = c[d];
}
if (c[d] > max) {
max = c[d];
}
}
return { min: min, mid: (min + max) / 2, max: max, size: max - min };
},
align: function (points, line) {
const tx = line.p1.x,
ty = line.p1.y,
a = -atan2(line.p2.y - ty, line.p2.x - tx),
d = function (v) {
return {
x: (v.x - tx) * cos(a) - (v.y - ty) * sin(a),
y: (v.x - tx) * sin(a) + (v.y - ty) * cos(a),
};
};
return points.map(d);
},
roots: function (points, line) {
line = line || { p1: { x: 0, y: 0 }, p2: { x: 1, y: 0 } };
const order = points.length - 1;
const aligned = utils.align(points, line);
const reduce = function (t) {
return 0 <= t && t <= 1;
};
if (order === 2) {
const a = aligned[0].y,
b = aligned[1].y,
c = aligned[2].y,
d = a - 2 * b + c;
if (d !== 0) {
const m1 = -sqrt(b * b - a * c),
m2 = -a + b,
v1 = -(m1 + m2) / d,
v2 = -(-m1 + m2) / d;
return [v1, v2].filter(reduce);
} else if (b !== c && d === 0) {
return [(2 * b - c) / (2 * b - 2 * c)].filter(reduce);
}
return [];
}
// see http://www.trans4mind.com/personal_development/mathematics/polynomials/cubicAlgebra.htm
const pa = aligned[0].y,
pb = aligned[1].y,
pc = aligned[2].y,
pd = aligned[3].y;
let d = -pa + 3 * pb - 3 * pc + pd,
a = 3 * pa - 6 * pb + 3 * pc,
b = -3 * pa + 3 * pb,
c = pa;
if (utils.approximately(d, 0)) {
// this is not a cubic curve.
if (utils.approximately(a, 0)) {
// in fact, this is not a quadratic curve either.
if (utils.approximately(b, 0)) {
// in fact in fact, there are no solutions.
return [];
}
// linear solution:
return [-c / b].filter(reduce);
}
// quadratic solution:
const q = sqrt(b * b - 4 * a * c),
a2 = 2 * a;
return [(q - b) / a2, (-b - q) / a2].filter(reduce);
}
// at this point, we know we need a cubic solution:
a /= d;
b /= d;
c /= d;
const p = (3 * b - a * a) / 3,
p3 = p / 3,
q = (2 * a * a * a - 9 * a * b + 27 * c) / 27,
q2 = q / 2,
discriminant = q2 * q2 + p3 * p3 * p3;
let u1, v1, x1, x2, x3;
if (discriminant < 0) {
const mp3 = -p / 3,
mp33 = mp3 * mp3 * mp3,
r = sqrt(mp33),
t = -q / (2 * r),
cosphi = t < -1 ? -1 : t > 1 ? 1 : t,
phi = acos(cosphi),
crtr = crt(r),
t1 = 2 * crtr;
x1 = t1 * cos(phi / 3) - a / 3;
x2 = t1 * cos((phi + tau) / 3) - a / 3;
x3 = t1 * cos((phi + 2 * tau) / 3) - a / 3;
return [x1, x2, x3].filter(reduce);
} else if (discriminant === 0) {
u1 = q2 < 0 ? crt(-q2) : -crt(q2);
x1 = 2 * u1 - a / 3;
x2 = -u1 - a / 3;
return [x1, x2].filter(reduce);
} else {
const sd = sqrt(discriminant);
u1 = crt(-q2 + sd);
v1 = crt(q2 + sd);
return [u1 - v1 - a / 3].filter(reduce);
}
},
droots: function (p) {
// quadratic roots are easy
if (p.length === 3) {
const a = p[0],
b = p[1],
c = p[2],
d = a - 2 * b + c;
if (d !== 0) {
const m1 = -sqrt(b * b - a * c),
m2 = -a + b,
v1 = -(m1 + m2) / d,
v2 = -(-m1 + m2) / d;
return [v1, v2];
} else if (b !== c && d === 0) {
return [(2 * b - c) / (2 * (b - c))];
}
return [];
}
// linear roots are even easier
if (p.length === 2) {
const a = p[0],
b = p[1];
if (a !== b) {
return [a / (a - b)];
}
return [];
}
return [];
},
curvature: function (t, points, _3d, kOnly) {
const dpoints = utils.derive(points);
const d1 = dpoints[0];
const d2 = dpoints[1];
let num,
dnm,
adk,
dk,
k = 0,
r = 0;
//
// We're using the following formula for curvature:
//
// x'y" - y'x"
// k(t) = ------------------
// (x'² + y'²)^(3/2)
//
// from https://en.wikipedia.org/wiki/Radius_of_curvature#Definition
//
// With it corresponding 3D counterpart:
//
// sqrt( (y'z" - y"z')² + (z'x" - z"x')² + (x'y" - x"y')²)
// k(t) = -------------------------------------------------------
// (x'² + y'² + z'²)^(3/2)
//
const d = utils.compute(t, d1);
const dd = utils.compute(t, d2);
const qdsum = d.x * d.x + d.y * d.y;
if (_3d) {
num = sqrt(
pow(d.y * dd.z - dd.y * d.z, 2) +
pow(d.z * dd.x - dd.z * d.x, 2) +
pow(d.x * dd.y - dd.x * d.y, 2)
);
dnm = pow(qdsum + d.z * d.z, 3 / 2);
} else {
num = d.x * dd.y - d.y * dd.x;
dnm = pow(qdsum, 3 / 2);
}
if (num === 0 || dnm === 0) {
return { k: 0, r: 0 };
}
k = num / dnm;
r = dnm / num;
// We're also computing the derivative of kappa, because
// there is value in knowing the rate of change for the
// curvature along the curve. And we're just going to
// ballpark it based on an epsilon.
if (!kOnly) {
// compute k'(t) based on the interval before, and after it,
// to at least try to not introduce forward/backward pass bias.
const pk = utils.curvature(t - 0.001, points, _3d, true).k;
const nk = utils.curvature(t + 0.001, points, _3d, true).k;
dk = (nk - k + (k - pk)) / 2;
adk = (abs(nk - k) + abs(k - pk)) / 2;
}
return { k: k, r: r, dk: dk, adk: adk };
},
inflections: function (points) {
if (points.length < 4) return [];
// FIXME: TODO: add in inflection abstraction for quartic+ curves?
const p = utils.align(points, { p1: points[0], p2: points.slice(-1)[0] }),
a = p[2].x * p[1].y,
b = p[3].x * p[1].y,
c = p[1].x * p[2].y,
d = p[3].x * p[2].y,
v1 = 18 * (-3 * a + 2 * b + 3 * c - d),
v2 = 18 * (3 * a - b - 3 * c),
v3 = 18 * (c - a);
if (utils.approximately(v1, 0)) {
if (!utils.approximately(v2, 0)) {
let t = -v3 / v2;
if (0 <= t && t <= 1) return [t];
}
return [];
}
const trm = v2 * v2 - 4 * v1 * v3,
sq = Math.sqrt(trm),
d2 = 2 * v1;
if (utils.approximately(d2, 0)) return [];
return [(sq - v2) / d2, -(v2 + sq) / d2].filter(function (r) {
return 0 <= r && r <= 1;
});
},
bboxoverlap: function (b1, b2) {
const dims = ["x", "y"],
len = dims.length;
for (let i = 0, dim, l, t, d; i < len; i++) {
dim = dims[i];
l = b1[dim].mid;
t = b2[dim].mid;
d = (b1[dim].size + b2[dim].size) / 2;
if (abs(l - t) >= d) return false;
}
return true;
},
expandbox: function (bbox, _bbox) {
if (_bbox.x.min < bbox.x.min) {
bbox.x.min = _bbox.x.min;
}
if (_bbox.y.min < bbox.y.min) {
bbox.y.min = _bbox.y.min;
}
if (_bbox.z && _bbox.z.min < bbox.z.min) {
bbox.z.min = _bbox.z.min;
}
if (_bbox.x.max > bbox.x.max) {
bbox.x.max = _bbox.x.max;
}
if (_bbox.y.max > bbox.y.max) {
bbox.y.max = _bbox.y.max;
}
if (_bbox.z && _bbox.z.max > bbox.z.max) {
bbox.z.max = _bbox.z.max;
}
bbox.x.mid = (bbox.x.min + bbox.x.max) / 2;
bbox.y.mid = (bbox.y.min + bbox.y.max) / 2;
if (bbox.z) {
bbox.z.mid = (bbox.z.min + bbox.z.max) / 2;
}
bbox.x.size = bbox.x.max - bbox.x.min;
bbox.y.size = bbox.y.max - bbox.y.min;
if (bbox.z) {
bbox.z.size = bbox.z.max - bbox.z.min;
}
},
pairiteration: function (c1, c2, curveIntersectionThreshold) {
const c1b = c1.bbox(),
c2b = c2.bbox(),
r = 100000,
threshold = curveIntersectionThreshold || 0.5;
if (
c1b.x.size + c1b.y.size < threshold &&
c2b.x.size + c2b.y.size < threshold
) {
return [
(((r * (c1._t1 + c1._t2)) / 2) | 0) / r +
"/" +
(((r * (c2._t1 + c2._t2)) / 2) | 0) / r,
];
}
const cc1 = c1.split(0.5),
cc2 = c2.split(0.5),
pairs = [
{ left: cc1.left, right: cc2.left },
{ left: cc1.left, right: cc2.right },
{ left: cc1.right, right: cc2.right },
{ left: cc1.right, right: cc2.left },
];
pairs = pairs.filter(function (pair) {
return utils.bboxoverlap(pair.left.bbox(), pair.right.bbox());
});
const results = [];
if (pairs.length === 0) return results;
pairs.forEach(function (pair) {
results = results.concat(
utils.pairiteration(pair.left, pair.right, threshold)
);
});
results = results.filter(function (v, i) {
return results.indexOf(v) === i;
});
return results;
},
getccenter: function (p1, p2, p3) {
const dx1 = p2.x - p1.x,
dy1 = p2.y - p1.y,
dx2 = p3.x - p2.x,
dy2 = p3.y - p2.y,
dx1p = dx1 * cos(quart) - dy1 * sin(quart),
dy1p = dx1 * sin(quart) + dy1 * cos(quart),
dx2p = dx2 * cos(quart) - dy2 * sin(quart),
dy2p = dx2 * sin(quart) + dy2 * cos(quart),
// chord midpoints
mx1 = (p1.x + p2.x) / 2,
my1 = (p1.y + p2.y) / 2,
mx2 = (p2.x + p3.x) / 2,
my2 = (p2.y + p3.y) / 2,
// midpoint offsets
mx1n = mx1 + dx1p,
my1n = my1 + dy1p,
mx2n = mx2 + dx2p,
my2n = my2 + dy2p,
// intersection of these lines:
arc = utils.lli8(mx1, my1, mx1n, my1n, mx2, my2, mx2n, my2n),
r = utils.dist(arc, p1);
// arc start/end values, over mid point:
let s = atan2(p1.y - arc.y, p1.x - arc.x),
m = atan2(p2.y - arc.y, p2.x - arc.x),
e = atan2(p3.y - arc.y, p3.x - arc.x),
_;
// determine arc direction (cw/ccw correction)
if (s < e) {
// if s<m<e, arc(s, e)
// if m<s<e, arc(e, s + tau)
// if s<e<m, arc(e, s + tau)
if (s > m || m > e) {
s += tau;
}
if (s > e) {
_ = e;
e = s;
s = _;
}
} else {
// if e<m<s, arc(e, s)
// if m<e<s, arc(s, e + tau)
// if e<s<m, arc(s, e + tau)
if (e < m && m < s) {
_ = e;
e = s;
s = _;
} else {
e += tau;
}
}
// assign and done.
arc.s = s;
arc.e = e;
arc.r = r;
return arc;
},
numberSort: function (a, b) {
return a - b;
},
};
export { utils };

View File

@@ -0,0 +1,32 @@
function enrich(element) {
if (!element) return element;
element.__listeners = {};
element.listen = function (evtNames, handler) {
if (!evtNames.map) evtNames = [evtNames];
evtNames.forEach((evtName) => {
element.addEventListener(evtName, handler);
if (!element.__listeners[evtName]) {
element.__listeners[evtName] = [];
}
element.__listeners[evtName].push(handler);
});
}.bind(element);
element.ignore = function (evtNames, handler) {
if (!evtNames.map) evtNames = [evtNames];
evtNames.forEach((evtName) => {
if (!handler) {
return element.__listeners[evtName].forEach((h) =>
element.removeEventListener(evtName, h)
);
}
element.removeEventListener(evtName, handler);
});
}.bind(element);
return element;
}
export { enrich };

View File

@@ -0,0 +1,33 @@
import { GraphicsAPI } from "../api/graphics-api.js";
export default function performCodeSurgery(code) {
// 1. ensure that anything that needs to run by first calling its super function, does so.
GraphicsAPI.superCallers.forEach((name) => {
const re = new RegExp(`${name}\\(([^)]*)\\)[\\s\\r\\n]*{[\\s\\r\\n]*`, `g`);
code = code.replace(re, `${name}($1) { super.${name}($1);\n`);
});
// 2. rewrite event handlers so that they capture the event and forward it to the super function.
GraphicsAPI.eventHandlers.forEach((name) => {
const re = new RegExp(`\\b${name}\\(\\)[\\s\\r\\n]*{[\\s\\r\\n]*`, `g`);
code = code.replace(re, `${name}(evt) { super.${name}(evt);\n`);
});
// 3. rewrite all public GraphicsAPI functions to have the required `this.` prefix
GraphicsAPI.methods.forEach((fn) => {
const re = new RegExp(`([\\s\\r\\n])${fn}\\(`, `g`);
code = code.replace(re, `$1this.${fn}(`);
});
// 4. do the same for all GraphicsAPI constants.
GraphicsAPI.constants.forEach((name) => {
const re = new RegExp(`(\\b)${name}(\\b)`, `g`);
code = code.replace(re, `$1this.${name}$2`);
});
return code;
}

View File

@@ -0,0 +1,39 @@
/**
* Get all code that isn't contained in functions.
* We're going to regexp our way to flawed victory here.
*/
export default function splitCodeSections(code) {
const re = /\b[\w\W][^\s]*?\([^)]*\)[\r\n\s]*{/;
const cuts = [];
for (let result = code.match(re); result; result = code.match(re)) {
result = result[0];
let start = code.indexOf(result);
let end = start + result.length;
let depth = 0;
let slice = Array.from(code).slice(start + result.length);
slice.some((c, pos) => {
if (c === `{`) {
depth++;
return false;
}
if (c === `}`) {
if (depth > 0) {
depth--;
return false;
}
end += pos + 1;
return true;
}
});
let cut = code.slice(start, end);
cuts.push(cut);
code = code.replace(cut, ``);
}
return {
quasiGlobal: code,
classCode: cuts.join(`\n`),
};
}

51
docs/js/site/referrer.js Normal file
View File

@@ -0,0 +1,51 @@
/**
* This script collects information on visitors, based
* on their client session. It finds out which page they
* were on when they go there, and by virtue of sending
* that information to the logger, their IP. This is
* information that you're sending already, anyway, but
* I want you to know that this is what's going on.
*
* Which is why this script is not obfuscated. It simply
* grabs your document.referrer value, which (unless Do
* Not Track is enabled) will contain the location of
* the page you were on before you clicked a link to this
* page, and GETs that to my logger. That GET operation
* comes from your computer, so will have your IP as part
* of the HTTP headers.
*
* And that's all I really care about, because I want to
* know how many people visit this page, and roughly where
* they're from (gasp! IPs can be turned into rough
* geographical location O_O).
*
* If you want to know what logger.php looks like, hit up
* github. It's in referrer/logger.php
*
*/
(function referrer(l) {
var page = l.substring(l.lastIndexOf("/") + 1).replace(".html", "");
page = page || "index.html";
// we don't care about file or localhost, for obvious reasons
var loc = window.location.toString();
if (loc.indexOf("file:///") !== -1) return;
if (loc.indexOf("localhost") !== -1) return;
// right, continue
var url = "http://pomax.nihongoresources.com/pages/bezierinfo/logger.php";
var xhr = new XMLHttpRequest();
xhr.open(
"GET",
url +
"?" +
"referrer=" +
encodeURIComponent(document.referrer) +
"&for=" +
page,
true
);
try {
xhr.send(null);
} catch (e) {
/* you don't care about this error, and I can't see it, so why would we do anything with it? */
}
})(window.location.toString());

View File

@@ -0,0 +1,91 @@
/**
* This file is responsible for making sure that the reddit, hn, twitter, etc. linkouts
* are updated to reflect the section someone is reading, rather than always pointing to
* base article itself.
*/
(function tryToBind() {
if (!document.querySelector(`map[name="rhtimap"]`)) {
return setTimeout(tryToBind, 300);
}
class Tracker {
constructor() {
this.section = false;
this.hash = false;
this.socials = ["rdt", "hn", "twt"];
}
update(data) {
this.section = data.section;
this.hash = data.hash;
this.socials.forEach((social) => {
var area = document.querySelector(`map area.sclnk-${social}`);
area.href = this[`get_${social}`]();
});
}
get url() {
return encodeURIComponent(
`https://pomax.github.io/bezierinfo${this.hash ? this.hash : ""}`
);
}
getTitle() {
var title = `A Primer on Bézier Curves`;
if (this.section) {
title = `${this.section}-${title}`;
}
return encodeURIComponent(title);
}
get_rdt() {
var title = this.getTitle();
var text = encodeURIComponent(
`A free, online book for when you really need to know how to do Bézier things.`
);
return `https://www.reddit.com/submit?url=${this.url}&title=${title}&text=${text}`;
}
get_hn() {
var title = this.getTitle();
return `https://news.ycombinator.com/submitlink?u=${this.url}&t=${title}`;
}
get_twt() {
var text =
encodeURIComponent(
`Reading "${this.section}" by @TheRealPomax over on `
) + this.url;
return `https://twitter.com/intent/tweet?original_referer=${this.url}&text=${text}&hashtags=bezier,curves,maths`;
}
}
// we set the section and fragmentid based on which ever section's heading is nearest
// the top of the screen, either just off-screen or in-screen
var tracker = new Tracker();
var anchors = Array.from(document.querySelectorAll("section h2 a"));
var sections = anchors.map((a) => a.parentNode);
var sectionData = sections.map((section) => {
return {
section: section.textContent,
hash: section.querySelector("a").hash,
};
});
window.addEventListener(
"scroll",
function (evt) {
var min = 99999999999999999;
var element = false;
sections.forEach((s, pos) => {
var v = Math.abs(s.getBoundingClientRect().top);
if (v < min) {
min = v;
element = pos;
}
});
tracker.update(sectionData[element]);
},
{ passive: true }
);
})();