cubic-beziers-through-points
Version:
A function to fit fair (bending energy minimizing) cubic bezier curves through a set of given ordered points in the plane.
134 lines (106 loc) • 4.08 kB
text/typescript
import { cubicToAnglesAndSpeeds } from "flo-bezier3";
import { getEα, getPssByα } from './get-e-alpha.js';
import { miniAlphaLoops } from './consts.js';
import { getInfos } from './get-infos/get-infos.js';
import { removeIdenticalPoints } from './remove-identical-points.js';
/**
* Returns an array of cubic bezier curves forming a loop going through all
* the given ordered points such that the binding energy of the loop is near
* a local minimum.
*
* Such loops are 'fair' meaning they look 'pretty' somehow.
*
* * curve terminal speeds are not optimized (even though it would not be too
* hard to do so); only angles are optimized but the resulting curves are quite
* close to each other. In any case, the research paper this library is based
* on suggest speeds to be fixed at the theoretical value of 1 and this is how
* it is implemented.
*
* * see [A CONSTRUCTIVE FRAMEWORK FOR MINIMAL ENERGY PLANAR CURVES by Michael J. Johnson & Hakim S. Johnson](https://www.sciencedirect.com/science/article/abs/pii/S0096300315015490)
*
* * The array of returned curves are order 3 (cubic) bezier curves given as an
* ordered array of its control point coordinates, e.g. `[[0,0], [1,1], [2,1], [2,0]]`
*
* @param points an ordered array of planar points
*/
function cubicsAndEnergyThroughPoints(
points: number[][]): { cubics: number[][][], Energy: number } {
const infos = getInfos(removeIdenticalPoints(points));
const len = infos.length;
let Energy = 0;
for (let j=0; j<miniAlphaLoops; j++) {
Energy = 0; // reset
for (let i=0; i<len; i++) {
const info = infos[i];
const _info = info._info;
const getE_ = getEα(info);
let halfSpan: number;
let { l, m, r } = info;
let L = getE_(l);
let M = getE_(m);
let R = getE_(r);
// 1d pattern search
while (true) {
({ l, m, r } = info);
halfSpan = (r - l)/2;
if ((L >= M && M <= R)) {
info.l = m - halfSpan/2;
info.r = m + halfSpan/2;
Energy += M;
break;
} else if ((L <= M && M <= R)) {
info.l = l - halfSpan;
info.m = l;
info.r = m;
R = M;
M = L;
L = getE_(info.l);
} else if ((L >= M && M >= R)) {
info.l = m;
info.m = r;
info.r = r + halfSpan;
L = M;
M = R;
R = getE_(info.r);
} else if ((L <= M && M >= R)) {
info.l = l - halfSpan;
info.m = l;
info.r = m;
R = M;
M = L;
L = getE_(info.l);
}
}
const α = info.m;
const [_ps, ps_] = getPssByα(α,info);
info.ps = ps_;
info.$ps = cubicToAnglesAndSpeeds(ps_);
_info.ps = _ps;
_info.$ps = cubicToAnglesAndSpeeds(_ps);
}
}
const cubics = infos.map(info => info.ps);
return { cubics, Energy };
}
function energyThroughPoints(
points: number[][]): number {
const infos = getInfos(removeIdenticalPoints(points));
const len = infos.length;
let Energy = 0;
for (let i=0; i<len; i++) {
const info = infos[i];
const getE_ = getEα(info);
let M = getE_(info.m);
Energy += M;
}
return Energy;
}
function cubicBeziersThroughPoints(
points: number[][]): number[][][] {
return cubicsAndEnergyThroughPoints(points).cubics;
}
export {
cubicBeziersThroughPoints,
energyThroughPoints,
cubicsAndEnergyThroughPoints
}