annotation tools for making the iterations publication (Manetta & Jara) - https://iterations.space/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

770 lines
21 KiB

// Copyright 2018 Raph Levien
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
//! A library of primitives for curves and splines.
/// A simple container for 2-vectors
class Vec2 {
constructor(x, y) {
this.x = x;
this.y = y;
}
norm() {
return Math.hypot(this.x, this.y);
}
dot(other) {
return this.x * other.x + this.y * other.y;
}
cross(other) {
return this.x * other.y - this.y * other.x;
}
}
class CubicBez {
/// Argument is array of coordinate values [x0, y0, x1, y1, x2, y2, x3, y3].
constructor(coords) {
this.c = coords;
}
weightsum(c0, c1, c2, c3) {
let x = c0 * this.c[0] + c1 * this.c[2] + c2 * this.c[4] + c3 * this.c[6];
let y = c0 * this.c[1] + c1 * this.c[3] + c2 * this.c[5] + c3 * this.c[7];
return new Vec2(x, y);
}
eval(t) {
let mt = 1 - t;
let c0 = mt * mt * mt;
let c1 = 3 * mt * mt * t;
let c2 = 3 * mt * t * t;
let c3 = t * t * t;
return this.weightsum(c0, c1, c2, c3);
}
deriv(t) {
let mt = 1 - t;
let c0 = -3 * mt * mt;
let c3 = 3 * t * t;
let c1 = -6 * t * mt - c0;
let c2 = 6 * t * mt - c3;
return this.weightsum(c0, c1, c2, c3);
}
deriv2(t) {
let mt = 1 - t;
let c0 = 6 * mt;
let c3 = 6 * t;
let c1 = 6 - 18 * mt;
let c2 = 6 - 18 * t;
return this.weightsum(c0, c1, c2, c3);
}
curvature(t) {
let d = this.deriv(t);
let d2 = this.deriv2(t);
return d.cross(d2) / Math.pow(d.norm(), 3);
}
atanCurvature(t) {
let d = this.deriv(t);
let d2 = this.deriv2(t);
return Math.atan2(d.cross(d2), Math.pow(d.norm(), 3));
}
// de Casteljau's algorithm
leftHalf() {
let c = new Float64Array(8);
c[0] = this.c[0];
c[1] = this.c[1];
c[2] = 0.5 * (this.c[0] + this.c[2]);
c[3] = 0.5 * (this.c[1] + this.c[3]);
c[4] = 0.25 * (this.c[0] + 2 * this.c[2] + this.c[4]);
c[5] = 0.25 * (this.c[1] + 2 * this.c[3] + this.c[5]);
c[6] = 0.125 * (this.c[0] + 3 * (this.c[2] + this.c[4]) + this.c[6]);
c[7] = 0.125 * (this.c[1] + 3 * (this.c[3] + this.c[5]) + this.c[7]);
return new CubicBez(c);
}
rightHalf() {
let c = new Float64Array(8);
c[0] = 0.125 * (this.c[0] + 3 * (this.c[2] + this.c[4]) + this.c[6]);
c[1] = 0.125 * (this.c[1] + 3 * (this.c[3] + this.c[5]) + this.c[7]);
c[2] = 0.25 * (this.c[2] + 2 * this.c[4] + this.c[6]);
c[3] = 0.25 * (this.c[3] + 2 * this.c[5] + this.c[7]);
c[4] = 0.5 * (this.c[4] + this.c[6]);
c[5] = 0.5 * (this.c[5] + this.c[7]);
c[6] = this.c[6];
c[7] = this.c[7];
return new CubicBez(c);
}
}
function testCubicBez() {
let c = new Float64Array(8);
for (var i = 0; i < 8; i++) {
c[i] = Math.random();
}
let cb = new CubicBez(c);
let t = Math.random();
let epsilon = 1e-6;
let xy0 = cb.eval(t);
let xy1 = cb.eval(t + epsilon);
console.log(new Vec2((xy1.x - xy0.x) / epsilon, (xy1.y - xy0.y) / epsilon));
console.log(cb.deriv(t));
let dxy0 = cb.deriv(t);
let dxy1 = cb.deriv(t + epsilon);
console.log(new Vec2((dxy1.x - dxy0.x) / epsilon, (dxy1.y - dxy0.y) / epsilon));
console.log(cb.deriv2(t));
}
class Polynomial {
constructor(c) {
this.c = c;
}
eval(x) {
let xi = 1;
let s = 0;
for (let a of this.c) {
s += a * xi;
xi *= x;
}
return s;
}
deriv() {
let c = new Float64Array(this.c.length - 1);
for (let i = 0; i < c.length; i++) {
c[i] = (i + 1) * this.c[i + 1];
}
return new Polynomial(c);
}
}
function hermite5(x0, x1, v0, v1, a0, a1) {
return new Polynomial([x0,
v0,
0.5 * a0,
-10 * x0 + 10 * x1 - 6 * v0 - 4 * v1 - 1.5 * a0 + 0.5 * a1,
15 * x0 - 15 * x1 + 8 * v0 + 7 * v1 + 1.5 * a0 - a1,
-6 * x0 + 6 * x1 - 3 * v0 - 3 * v1 + -.5 * a0 + 0.5 * a1]);
}
/// Solve tridiagonal matrix system. Destroys inputs, leaves output in x.
///
/// Solves a[i] * x[i - 1] + b[i] * x[i] + c[i] * x[i + 1] = d[i]
///
/// Inputs are array-like objects (typed arrays are good for performance).
///
/// Note: this is not necessarily the fastest, see:
/// https://en.wikibooks.org/wiki/Algorithm_Implementation/Linear_Algebra/Tridiagonal_matrix_algorithm
function tridiag(a, b, c, d, x) {
let n = x.length;
for (var i = 1; i < n; i++) {
let m = a[i] / b[i - 1];
b[i] -= m * c[i - 1];
d[i] -= m * d[i - 1];
}
x[n - 1] = d[n - 1] / b[n - 1];
for (var i = n - 2; i >= 0; i--) {
x[i] = (d[i] - c[i] * x[i + 1]) / b[i];
}
}
function testTridiag(n) {
let a = new Float64Array(n);
let b = new Float64Array(n);
let c = new Float64Array(n);
let d = new Float64Array(n);
let x = new Float64Array(n);
for (var i = 0; i < n; i++) {
a[i] = Math.random();
b[i] = 2 + Math.random();
c[i] = Math.random();
d[i] = Math.random();
x[i] = Math.random();
}
let bsave = new Float64Array(b);
let dsave = new Float64Array(d);
let xsave = new Float64Array(x);
tridiag(a, b, c, d, x);
b = bsave; d = dsave;
console.log(b[0] * x[0] + c[0] * x[1] - d[0]);
for (var i = 1; i < n - 1; i++) {
console.log(a[i] * x[i - 1] + b[i] * x[i] + c[i] * x[i + 1] - d[i]);
}
console.log(a[n - 1] * x[n - 2] + b[n - 1] * x[n - 1] - d[n - 1]);
}
//testTridiag(10);
//testCubicBez();
/// Create a smooth cubic bezier.
function myCubic(th0, th1) {
function myCubicLen(th0, th1) {
let offset = 0.3 * Math.sin(th1 * 2 - 0.4 * Math.sin(th1 * 2));
let newShape = true;
if (newShape) {
let scale = 1.0 / (3 * 0.8);
let len = scale * (Math.cos(th0 - offset) - 0.2 * Math.cos((3 * (th0 - offset))));
return len;
} else {
let drive = 2.0;
let scale = 1.0 / (3 * Math.tanh(drive));
let len = scale * Math.tanh(drive * Math.cos(th0 - offset));
return len;
}
}
var coords = new Float64Array(8);
let len0 = myCubicLen(th0, th1);
coords[2] = Math.cos(th0) * len0;
coords[3] = Math.sin(th0) * len0;
let len1 = myCubicLen(th1, th0);
coords[4] = 1 - Math.cos(th1) * len1;
coords[5] = Math.sin(th1) * len1;
coords[6] = 1;
return coords;
}
//! Base class for two parameter curve families
class TwoParamCurve {
/// Render the curve, providing an array of _interior_ cubic bezier
/// control points only. Return value is an array of 3n-1 Vec2's.
// render(th0, th1)
/// Compute curvature.
///
/// Result is an object with ak0 and ak1 (arctan of curvature at endpoints).
/// Quadrant is significant - a value outside -pi/2 to pi/2 means a reversal
/// of direction.
// computeCurvature(th0, th1)
/// Get endpoint condition.
///
/// Return tangent at endpoint given next-to-endpoint tangent.
// endpointTangent(th)
/// Compute curvature derivatives.
///
/// Result is an object with dak0dth0 and friends.
/// Default implementation is approximate through central differencing, but
/// curves can override.
computeCurvatureDerivs(th0, th1) {
let epsilon = 1e-6;
let scale = 2.0 / epsilon;
let k0plus = this.computeCurvature(th0 + epsilon, th1);
let k0minus = this.computeCurvature(th0 - epsilon, th1);
let dak0dth0 = scale * (k0plus.ak0 - k0minus.ak0);
let dak1dth0 = scale * (k0plus.ak1 - k0minus.ak1);
let k1plus = this.computeCurvature(th0, th1 + epsilon);
let k1minus = this.computeCurvature(th0, th1 - epsilon);
let dak0dth1 = scale * (k1plus.ak0 - k1minus.ak0);
let dak1dth1 = scale * (k1plus.ak1 - k1minus.ak1);
return {dak0dth0: dak0dth0, dak1dth0: dak1dth0, dak0dth1: dak0dth1, dak1dth1: dak1dth1};
}
}
class MyCurve extends TwoParamCurve {
render(th0, th1) {
let c = myCubic(th0, th1);
return [new Vec2(c[2], c[3]), new Vec2(c[4], c[5])];
}
/// Render as a 4-parameter curve with optional adjusted endpoint curvatures.
render4Quintic(th0, th1, k0, k1) {
//let cb = new CubicBez(myCubic(th0, th1));
let cb = this.convCubic(this.render4Cubic(th0, th1, k0, k1));
// compute second deriv tweak to match curvature
function curvAdjust(t, th, k) {
if (k === null) return new Vec2(0, 0);
let c = Math.cos(th);
let s = Math.sin(th);
let d2 = cb.deriv2(t);
let d2cross = d2.y * c - d2.x * s;
let d = cb.deriv(t);
let ddot = d.x * c + d.y * s;
// TODO: if ddot = 0, cusp, no adjustment
let oldK = d2cross / (ddot * ddot);
let kAdjust = k - oldK;
let aAdjust = kAdjust * (ddot * ddot);
return new Vec2(-s * aAdjust, c * aAdjust);
}
let a0 = curvAdjust(0, th0, k0);
let a1 = curvAdjust(1, -th1, k1);
let hx = hermite5(0, 0, 0, 0, a0.x, a1.x);
let hy = hermite5(0, 0, 0, 0, a0.y, a1.y);
let hxd = hx.deriv();
let hyd = hy.deriv();
// This really would be cleaner if we had arbitrary deCasteljau...
let c0 = cb.leftHalf();
let c1 = cb.rightHalf();
let cs = [c0.leftHalf(), c0.rightHalf(), c1.leftHalf(), c1.rightHalf()];
let result = [];
let scale = 1./12;
for (let i = 0; i < 4; i++) {
let t = 0.25 * i;
let t1 = t + 0.25;
let c = cs[i].c;
let x0 = hx.eval(t);
let y0 = hy.eval(t);
let x1 = x0 + scale * hxd.eval(t);
let y1 = y0 + scale * hyd.eval(t);
let x3 = hx.eval(t1);
let y3 = hy.eval(t1);
let x2 = x3 - scale * hxd.eval(t1);
let y2 = y3 - scale * hyd.eval(t1);
if (i != 0) {
result.push(new Vec2(c[0] + x0, c[1] + y0));
}
result.push(new Vec2(c[2] + x1, c[3] + y1));
result.push(new Vec2(c[4] + x2, c[5] + y2));
}
return result;
}
convCubic(pts) {
let coords = new Float64Array(8);
coords[2] = pts[0].x;
coords[3] = pts[0].y;
coords[4] = pts[1].x;
coords[5] = pts[1].y;
coords[6] = 1;
return new CubicBez(coords);
}
// Ultimately we want to exactly match the endpoint curvatures (probably breaking
// into two cubic segments), but for now, just approximate...
render4Cubic(th0, th1, k0, k1) {
let cb = new CubicBez(myCubic(th0, th1));
let result = [];
function deriv_scale(t, th, k) {
if (k === null) return 1/3;
let c = Math.cos(th);
let s = Math.sin(th);
let d = cb.deriv(t);
let d2 = cb.deriv2(t);
let d2cross = d2.y * c - d2.x * s;
let ddot = d.x * c + d.y * s;
let oldK = d2cross / (ddot * ddot);
// fudge to avoid divide-by-zero
if (Math.abs(oldK) < 1e-6) oldK = 1e-6;
let ratio = k / oldK;
// TODO: fine tune this dodgy formula
//let scale = ratio < 1 ? 1/2 - ratio/6 : 1/(3*ratio);
let scale = 1/(2 + ratio);
return scale;
}
let scale0 = deriv_scale(0, th0, k0);
let d0 = cb.deriv(0);
result.push(new Vec2(d0.x * scale0, d0.y * scale0));
let d1 = cb.deriv(1);
let scale1 = deriv_scale(1, -th1, k1);
result.push(new Vec2(1 - d1.x * scale1, - d1.y * scale1));
return result;
}
render4(th0, th1, k0, k1) {
if (k0 === null && k1 === null) {
return this.render(th0, th1);
}
return this.render4Quintic(th0, th1, k0, k1);
}
computeCurvature(th0, th1) {
let cb = new CubicBez(myCubic(th0, th1));
function curv(t, th) {
let c = Math.cos(th);
let s = Math.sin(th);
let d2 = cb.deriv2(t);
let d2cross = d2.y * c - d2.x * s;
let d = cb.deriv(t);
let ddot = d.x * c + d.y * s;
return Math.atan2(d2cross, ddot * Math.abs(ddot));
}
//let ak0 = cb.atanCurvature(0);
//let ak1 = cb.atanCurvature(1);
let ak0 = curv(0, th0);
let ak1 = curv(1, -th1);
return {ak0: ak0, ak1: ak1};
}
endpointTangent(th) {
// Same value as parabola:
//return Math.atan(2 * Math.tan(th)) - th;
return 0.5 * Math.sin(2 * th);
}
}
//! Global spline solver
// normalize theta to -pi..pi
function mod2pi(th) {
let twopi = 2 * Math.PI;
let frac = th * (1 / twopi);
return twopi * (frac - Math.round(frac));
}
class TwoParamSpline {
constructor(curve, ctrlPts) {
this.curve = curve;
this.ctrlPts = ctrlPts;
this.startTh = null;
this.endTh = null;
}
/// Determine initial tangent angles, given array of Vec2 control points.
initialThs() {
var ths = new Float64Array(this.ctrlPts.length);
for (var i = 1; i < ths.length - 1; i++) {
let dx0 = this.ctrlPts[i].x - this.ctrlPts[i - 1].x;
let dy0 = this.ctrlPts[i].y - this.ctrlPts[i - 1].y;
let l0 = Math.hypot(dx0, dy0);
let dx1 = this.ctrlPts[i + 1].x - this.ctrlPts[i].x;
let dy1 = this.ctrlPts[i + 1].y - this.ctrlPts[i].y;
let l1 = Math.hypot(dx1, dy1);
let th0 = Math.atan2(dy0, dx0);
let th1 = Math.atan2(dy1, dx1);
let bend = mod2pi(th1 - th0);
let th = mod2pi(th0 + bend * l0 / (l0 + l1));
ths[i] = th;
if (i == 1) { ths[0] = th0; }
if (i == ths.length - 2) { ths[i + 1] = th1; }
}
if (this.startTh !== null) {
ths[0] = this.startTh;
}
if (this.endTh !== null) {
ths[ths.length - 1] = this.endTh;
}
this.ths = ths;
return ths;
}
/// Get tangent angles relative to endpoints, and chord length.
getThs(i) {
let dx = this.ctrlPts[i + 1].x - this.ctrlPts[i].x;
let dy = this.ctrlPts[i + 1].y - this.ctrlPts[i].y;
let th = Math.atan2(dy, dx);
let th0 = mod2pi(this.ths[i] - th);
let th1 = mod2pi(th - this.ths[i + 1]);
let chord = Math.hypot(dx, dy);
return {th0: th0, th1: th1, chord: chord};
}
/// Crawl towards a curvature continuous solution.
iterDumb(iter) {
function computeErr(ths0, ak0, ths1, ak1) {
// rescale tangents by geometric mean of chordlengths
let ch0 = Math.sqrt(ths0.chord);
let ch1 = Math.sqrt(ths1.chord);
let a0 = Math.atan2(Math.sin(ak0.ak1) * ch1, Math.cos(ak0.ak1) * ch0);
let a1 = Math.atan2(Math.sin(ak1.ak0) * ch0, Math.cos(ak1.ak0) * ch1);
return a0 - a1;
/*
return ths1.chord * Math.sin(ak0.ak1) * Math.cos(ak1.ak0)
- ths0.chord * Math.sin(ak1.ak0) * Math.cos(ak0.ak1);
*/
}
let n = this.ctrlPts.length;
// Fix endpoint tangents; we rely on iteration for this to converge
if (this.startTh === null) {
let ths0 = this.getThs(0);
this.ths[0] += this.curve.endpointTangent(ths0.th1) - ths0.th0;
}
if (this.endTh === null) {
let ths0 = this.getThs(n - 2);
this.ths[n - 1] -= this.curve.endpointTangent(ths0.th0) - ths0.th1;
}
if (n < 3) return 0;
var absErr = 0;
var x = new Float64Array(n - 2);
var ths0 = this.getThs(0);
var ak0 = this.curve.computeCurvature(ths0.th0, ths0.th1);
//console.log('');
for (var i = 0; i < n - 2; i++) {
let ths1 = this.getThs(i + 1);
let ak1 = this.curve.computeCurvature(ths1.th0, ths1.th1);
let err = computeErr(ths0, ak0, ths1, ak1);
absErr += Math.abs(err);
let epsilon = 1e-3;
let ak0p = this.curve.computeCurvature(ths0.th0, ths0.th1 + epsilon);
let ak1p = this.curve.computeCurvature(ths1.th0 - epsilon, ths1.th1);
let errp = computeErr(ths0, ak0p, ths1, ak1p);
let derr = (errp - err) * (1 / epsilon);
//console.log(err, derr, ak0, ak1, ak0p, ak1p);
x[i] = err / derr;
ths0 = ths1;
ak0 = ak1;
}
for (var i = 0; i < n - 2; i++) {
let scale = Math.tanh(0.25 * (iter + 1));
this.ths[i + 1] += scale * x[i];
}
return absErr;
}
/// Perform one step of a Newton solver.
// Not yet implemented
iterate() {
let n = this.ctrlPts.length;
if (n < 3) return;
var a = new Float64Array(n - 2);
var b = new Float64Array(n - 2);
var c = new Float64Array(n - 2);
var d = new Float64Array(n - 2);
var x = new Float64Array(n - 2);
let ths0 = this.getThs(0);
var last_ak = this.curve.computeCurvature(ths0.th0, ths0.th1);
var last_dak = this.curve.computeCurvatureDerivs(ths0.th0, ths0.th1);
var last_a = Math.hypot(this.ctrlPts[1].x - this.ctrlPts[0].x,
this.ctrlPts[1].y - this.ctrlPts[0].y);
for (var i = 0; i < n - 2; i++) {
let ths = this.getThs(i + 1);
let ak = this.curve.computeCurvature(ths.th0, ths.th1);
let dak = this.curve.computeCurvatureDerivs(ths.th0, ths.th1);
var a = Math.hypot(this.ctrlPts[i + 2].x - this.ctrlPts[i + 1].x,
this.ctrlPts[i + 2].y - this.ctrlPts[i + 1].y);
let c0 = Math.cos(last_ak.ak1);
let s0 = Math.sin(last_ak.ak1);
let c1 = Math.cos(ak.ak0);
let s1 = Math.sin(ak.ak0);
// TODO: fill in derivatives properly
d[i] = a * s0 * c1 - last_a * s1 * c0;
last_ak = ak;
last_dak = dak;
last_a = a;
}
tridiag(a, b, c, d, x);
for (var i = 0; i < n - 2; i++) {
this.ths[i + 1] -= x[i];
}
}
/// Return an SVG path string.
renderSvg() {
let c = this.ctrlPts;
if (c.length == 0) { return ""; }
let path = `M${c[0].x} ${c[0].y}`;
let cmd = " C";
for (var i = 0; i < c.length - 1; i++) {
let ths = this.getThs(i);
let render = this.curve.render(ths.th0, ths.th1);
let dx = c[i + 1].x - c[i].x;
let dy = c[i + 1].y - c[i].y;
for (var j = 0; j < render.length; j++) {
let pt = render[j];
let x = c[i].x + dx * pt.x - dy * pt.y;
let y = c[i].y + dy * pt.x + dx * pt.y;
path += `${cmd}${x} ${y}`;
cmd = " ";
}
path += ` ${c[i + 1].x} ${c[i + 1].y}`;
}
return path;
}
}
/// Spline handles more general cases, including corners.
class Spline {
constructor(ctrlPts, isClosed) {
this.ctrlPts = ctrlPts;
this.isClosed = isClosed;
this.curve = new MyCurve();
}
pt(i, start) {
let length = this.ctrlPts.length;
return this.ctrlPts[(i + start + length) % length];
}
startIx() {
if (!this.isClosed) {
return 0;
}
for (let i = 0; i < this.ctrlPts.length; i++) {
let pt = this.ctrlPts[i];
if (pt.ty === "corner" || pt.lth !== null) {
return i;
}
}
// Path is all-smooth and closed.
return 0;
}
solve() {
let start = this.startIx();
let length = this.ctrlPts.length - (this.isClosed ? 0 : 1);
let i = 0;
while (i < length) {
let ptI = this.pt(i, start);
let ptI1 = this.pt(i + 1, start);
if ((i + 1 == length || ptI1.ty === "corner")
&& ptI.rth === null && ptI1.lth === null) {
let dx = ptI1.pt.x - ptI.pt.x;
let dy = ptI1.pt.y - ptI.pt.y;
let th = Math.atan2(dy, dx);
ptI.rTh = th;
ptI1.lTh = th;
i += 1;
} else {
// We have a curve.
let innerPts = [ptI.pt];
let j = i + 1;
while (j < length + 1) {
let ptJ = this.pt(j, start);
innerPts.push(ptJ.pt);
j += 1;
if (ptJ.ty === "corner" || ptJ.lth !== null) {
break;
}
}
//console.log(innerPts);
let inner = new TwoParamSpline(this.curve, innerPts);
inner.startTh = this.pt(i, start).rth;
inner.endTh = this.pt(j - 1, start).lth;
let nIter = 10;
inner.initialThs();
for (let k = 0; k < nIter; k++) {
inner.iterDumb(k);
}
for (let k = i; k + 1 < j; k++) {
this.pt(k, start).rTh = inner.ths[k - i];
this.pt(k + 1, start).lTh = inner.ths[k + 1 - i];
// Record curvatures (for blending, not all will be used)
let ths = inner.getThs(k - i);
let aks = this.curve.computeCurvature(ths.th0, ths.th1);
this.pt(k, start).rAk = aks.ak0;
this.pt(k + 1, start).lAk = aks.ak1;
}
i = j - 1;
}
}
}
chordLen(i) {
let ptI = this.pt(i, 0).pt;
let ptI1 = this.pt(i + 1, 0).pt;
return Math.hypot(ptI1.x - ptI.x, ptI1.y - ptI.y);
}
// Determine whether a control point requires curvature blending, and if so,
// the blended curvature. To be invoked after solving.
computeCurvatureBlending() {
function myTan(th) {
if (th > Math.PI / 2) {
return Math.tan(Math.PI - th);
} else if (th < -Math.PI / 2) {
return Math.tan(-Math.PI - th);
} else {
return Math.tan(th);
}
}
for (let pt of this.ctrlPts) {
pt.kBlend = null;
}
let length = this.ctrlPts.length - (this.isClosed ? 0 : 1);
for (let i = 0; i < length; i++) {
let pt = this.pt(i, 0);
if (pt.ty === "smooth" && pt.lth !== null) {
let thresh = Math.PI / 2 - 1e-6;
//if (Math.abs(pt.rAk) > thresh || Math.abs(pt.lAk) > thresh) {
// // Don't blend reversals. We might reconsider this, but punt for now.
// continue;
//}
if (Math.sign(pt.rAk) != Math.sign(pt.lAk)) {
pt.kBlend = 0;
} else {
let rK = myTan(pt.rAk) / this.chordLen(i - 1);
let lK = myTan(pt.lAk) / this.chordLen(i);
pt.kBlend = 2 / (1 / rK + 1 / lK);
//console.log(`point ${i}: kBlend = ${pt.kBlend}`);
}
}
}
}
render() {
let path = new BezPath;
if (this.ctrlPts.length == 0) {
return path;
}
let pt0 = this.ctrlPts[0];
path.moveto(pt0.pt.x, pt0.pt.y);
let length = this.ctrlPts.length - (this.isClosed ? 0 : 1);
let i = 0;
for (let i = 0; i < length; i++) {
path.mark(i);
let ptI = this.pt(i, 0);
let ptI1 = this.pt(i + 1, 0);
let dx = ptI1.pt.x - ptI.pt.x;
let dy = ptI1.pt.y - ptI.pt.y;
let chth = Math.atan2(dy, dx);
let chord = Math.hypot(dy, dx);
let th0 = mod2pi(ptI.rTh - chth);
let th1 = mod2pi(chth - ptI1.lTh);
// Apply curvature blending
let k0 = ptI.kBlend !== null ? ptI.kBlend * chord : null;
let k1 = ptI1.kBlend !== null ? ptI1.kBlend * chord : null;
//console.log(`segment ${i}: ${k0} ${k1}`);
let render = this.curve.render4(th0, th1, k0, k1);
let c = [];
for (let j = 0; j < render.length; j++) {
let pt = render[j];
c.push(ptI.pt.x + dx * pt.x - dy * pt.y);
c.push(ptI.pt.y + dy * pt.x + dx * pt.y);
}
c.push(ptI1.pt.x);
c.push(ptI1.pt.y);
for (let j = 0; j < c.length; j += 6) {
path.curveto(c[j], c[j + 1], c[j + 2], c[j + 3], c[j + 4], c[j + 5]);
}
}
if (this.isClosed) {
path.closepath();
}
return path;
}
renderSvg() {
return this.render().renderSvg();
}
}
/// ControlPoint is a lot like `Knot` but has no UI, is used for spline solving.
class ControlPoint {
constructor(pt, ty, lth, rth) {
this.pt = pt;
this.ty = ty;
this.lth = lth;
this.rth = rth;
}
}