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:
250
docs/js/custom-element/api/base-api.js
Normal file
250
docs/js/custom-element/api/base-api.js
Normal 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 };
|
557
docs/js/custom-element/api/graphics-api.js
Normal file
557
docs/js/custom-element/api/graphics-api.js
Normal 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 };
|
165
docs/js/custom-element/api/types/bezier.js
Normal file
165
docs/js/custom-element/api/types/bezier.js
Normal 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 };
|
77
docs/js/custom-element/api/types/vector.js
Normal file
77
docs/js/custom-element/api/types/vector.js
Normal 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 };
|
142
docs/js/custom-element/api/util/matrix.js
Normal file
142
docs/js/custom-element/api/util/matrix.js
Normal 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 };
|
43
docs/js/custom-element/api/util/shape.js
Normal file
43
docs/js/custom-element/api/util/shape.js
Normal 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 };
|
135
docs/js/custom-element/custom-element.js
Normal file
135
docs/js/custom-element/custom-element.js
Normal 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 };
|
75
docs/js/custom-element/graphics-element.css
Normal file
75
docs/js/custom-element/graphics-element.css
Normal 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;
|
||||
}
|
283
docs/js/custom-element/graphics-element.js
Normal file
283
docs/js/custom-element/graphics-element.js
Normal 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 };
|
968
docs/js/custom-element/lib/bezierjs/bezier.js
Normal file
968
docs/js/custom-element/lib/bezierjs/bezier.js
Normal 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 };
|
267
docs/js/custom-element/lib/bezierjs/normalise-svg.js
Normal file
267
docs/js/custom-element/lib/bezierjs/normalise-svg.js
Normal 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();
|
||||
}
|
70
docs/js/custom-element/lib/bezierjs/poly-bezier.js
Normal file
70
docs/js/custom-element/lib/bezierjs/poly-bezier.js
Normal 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 };
|
45
docs/js/custom-element/lib/bezierjs/svg-to-beziers.js
Normal file
45
docs/js/custom-element/lib/bezierjs/svg-to-beziers.js
Normal 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 };
|
908
docs/js/custom-element/lib/bezierjs/utils.js
Normal file
908
docs/js/custom-element/lib/bezierjs/utils.js
Normal 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 };
|
32
docs/js/custom-element/lib/enrich.js
Normal file
32
docs/js/custom-element/lib/enrich.js
Normal 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 };
|
33
docs/js/custom-element/lib/perform-code-surgery.js
Normal file
33
docs/js/custom-element/lib/perform-code-surgery.js
Normal 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;
|
||||
}
|
39
docs/js/custom-element/lib/split-code-sections.js
Normal file
39
docs/js/custom-element/lib/split-code-sections.js
Normal 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
51
docs/js/site/referrer.js
Normal 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());
|
91
docs/js/site/social-updater.js
Normal file
91
docs/js/site/social-updater.js
Normal 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 }
|
||||
);
|
||||
})();
|
Reference in New Issue
Block a user