pts 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +283 -2
- package/dist/index.d.mts.map +1 -1
- package/dist/index.d.ts +283 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6018 -4596
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +6017 -4597
- package/dist/index.mjs.map +1 -1
- package/dist/pts.js +4362 -2940
- package/dist/pts.js.map +1 -1
- package/dist/pts.min.js +2 -2
- package/dist/pts.min.js.map +1 -1
- package/package.json +3 -2
- package/src/Canvas.ts +99 -4
- package/src/Create.ts +288 -1
- package/src/Dom.ts +1 -1
- package/src/Num.ts +3 -1
- package/src/Op.ts +315 -0
- package/src/Pt.ts +8 -1
- package/src/Svg.ts +79 -0
- package/src/Types.ts +17 -0
- package/src/_path.ts +1402 -0
- package/src/_triangulate.ts +41 -0
package/src/Op.ts
CHANGED
|
@@ -4,12 +4,14 @@ import { Util } from "./Util";
|
|
|
4
4
|
import { Geom, Num } from "./Num";
|
|
5
5
|
import { Pt, Group } from "./Pt";
|
|
6
6
|
import { Mat } from "./LinearAlgebra";
|
|
7
|
+
import { overlay } from "./_path";
|
|
7
8
|
import {
|
|
8
9
|
type PtLike,
|
|
9
10
|
type GroupLike,
|
|
10
11
|
type PtLikeIterable,
|
|
11
12
|
type IntersectContext,
|
|
12
13
|
type PtIterable,
|
|
14
|
+
type PolygonLike,
|
|
13
15
|
} from "./Types";
|
|
14
16
|
|
|
15
17
|
let _errorLength = (obj: any, param: number | string = "expected") =>
|
|
@@ -1725,6 +1727,94 @@ export class Polygon {
|
|
|
1725
1727
|
}
|
|
1726
1728
|
}
|
|
1727
1729
|
|
|
1730
|
+
/**
|
|
1731
|
+
* Path class provides static functions to combine polygons with boolean operations:
|
|
1732
|
+
* unite, intersect, exclude, subtract the shapes in front or behind, divide into faces, or crop by the top shape.
|
|
1733
|
+
* Shapes are listed in stacking order, the first at the back and the last in front, like the order you would draw them in.
|
|
1734
|
+
* A shape is a polygon (a Group, or any iterable of points), or a list of rings that together form a polygon with holes,
|
|
1735
|
+
* such as the result of another Path function. Every result is such a list of rings: an outer ring followed by its
|
|
1736
|
+
* holes, in opposite orientations, which [`CanvasForm.compound`](#link) draws as one path. Rings are open (the first point
|
|
1737
|
+
* is not repeated), 2D, and never share Pts with the input. Clockwise and counterclockwise rings are the same shape, and
|
|
1738
|
+
* a self-intersecting ring covers what `form.polygon` would fill (the nonzero rule). Input vertices closer together than a
|
|
1739
|
+
* millionth of the largest absolute coordinate are merged before finding intersections. For small shapes at large offsets,
|
|
1740
|
+
* work in local coordinates to avoid losing detail to this tolerance or the Float32 output.
|
|
1741
|
+
* See [Op guide](../guide/Op-0400.html) for details.
|
|
1742
|
+
*/
|
|
1743
|
+
export class Path {
|
|
1744
|
+
/**
|
|
1745
|
+
* Unite: merge all shapes into one polygon (the area inside any shape).
|
|
1746
|
+
* @param shapes an Array/Iterable of polygons in stacking order, back to front. Each is a Group or an Iterable<PtLike>, or a list of rings for a polygon with holes. A single ring is taken as one shape.
|
|
1747
|
+
* @returns the rings of the merged polygon: each outer ring followed by its holes; empty if the shapes have no area
|
|
1748
|
+
* @example `form.fillOnly("#f03").compound( Path.unite( [star, disc] ) )`
|
|
1749
|
+
*/
|
|
1750
|
+
static unite(shapes: Iterable<PolygonLike> | PtLikeIterable): Group[] {
|
|
1751
|
+
return overlay(shapes, "unite");
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
/**
|
|
1755
|
+
* Intersect: keep only the area inside every shape.
|
|
1756
|
+
* @param shapes an Array/Iterable of polygons in stacking order, back to front. Each is a Group or an Iterable<PtLike>, or a list of rings for a polygon with holes. A single ring is taken as one shape.
|
|
1757
|
+
* @returns the rings of the common polygon: each outer ring followed by its holes; empty if the shapes do not all overlap
|
|
1758
|
+
* @example `Path.intersect( [a, b, c] )`
|
|
1759
|
+
*/
|
|
1760
|
+
static intersect(shapes: Iterable<PolygonLike> | PtLikeIterable): Group[] {
|
|
1761
|
+
return overlay(shapes, "intersect");
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
/**
|
|
1765
|
+
* Exclude: keep the area inside an odd number of shapes, so where two shapes overlap becomes a hole.
|
|
1766
|
+
* @param shapes an Array/Iterable of polygons in stacking order, back to front. Each is a Group or an Iterable<PtLike>, or a list of rings for a polygon with holes. A single ring is taken as one shape.
|
|
1767
|
+
* @returns the rings of the result: each outer ring followed by its holes; empty if the shapes cancel out
|
|
1768
|
+
* @example `Path.exclude( [a, b] )`
|
|
1769
|
+
*/
|
|
1770
|
+
static exclude(shapes: Iterable<PolygonLike> | PtLikeIterable): Group[] {
|
|
1771
|
+
return overlay(shapes, "exclude");
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
/**
|
|
1775
|
+
* Minus Front: subtract every shape in front from the backmost (first) shape.
|
|
1776
|
+
* @param shapes an Array/Iterable of polygons in stacking order, back to front. Each is a Group or an Iterable<PtLike>, or a list of rings for a polygon with holes. A single ring is taken as one shape.
|
|
1777
|
+
* @returns the rings of what remains of the first shape: each outer ring followed by its holes; empty if nothing remains
|
|
1778
|
+
* @example `Path.minusFront( [disc, hole] )` cuts `hole` out of `disc`
|
|
1779
|
+
*/
|
|
1780
|
+
static minusFront(shapes: Iterable<PolygonLike> | PtLikeIterable): Group[] {
|
|
1781
|
+
return overlay(shapes, "minusFront");
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
/**
|
|
1785
|
+
* Minus Back: subtract every shape behind from the frontmost (last) shape.
|
|
1786
|
+
* @param shapes an Array/Iterable of polygons in stacking order, back to front. Each is a Group or an Iterable<PtLike>, or a list of rings for a polygon with holes. A single ring is taken as one shape.
|
|
1787
|
+
* @returns the rings of what remains of the last shape: each outer ring followed by its holes; empty if nothing remains
|
|
1788
|
+
* @example `Path.minusBack( [wall, window] )` keeps the part of `window` not covered by `wall`
|
|
1789
|
+
*/
|
|
1790
|
+
static minusBack(shapes: Iterable<PolygonLike> | PtLikeIterable): Group[] {
|
|
1791
|
+
return overlay(shapes, "minusBack");
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
/**
|
|
1795
|
+
* Divide: split the shapes at every crossing into separate faces. Each face is the largest area not cut by any edge, so
|
|
1796
|
+
* a region inside two shapes is its own face, and a self-overlapping region of one shape is too.
|
|
1797
|
+
* @param shapes an Array/Iterable of polygons in stacking order, back to front. Each is a Group or an Iterable<PtLike>, or a list of rings for a polygon with holes. A single ring is taken as one shape.
|
|
1798
|
+
* @returns an array of polygons, one per face, each an outer ring followed by its holes
|
|
1799
|
+
* @example `Path.divide( [a, b] ).forEach( (face, i) => form.fillOnly( colors[i] ).compound( face ) )`
|
|
1800
|
+
*/
|
|
1801
|
+
static divide(shapes: Iterable<PolygonLike> | PtLikeIterable): Group[][] {
|
|
1802
|
+
return overlay(shapes, "divide");
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
/**
|
|
1806
|
+
* Crop: use the frontmost (last) shape as a mask, keeping the faces of the other shapes inside it and deleting the mask itself.
|
|
1807
|
+
* Like [`Path.divide`](#link), the shapes under the mask stay divided where they overlap.
|
|
1808
|
+
* See a [demo here](https://ptsjs.org/demo/?name=path.crop).
|
|
1809
|
+
* @param shapes an Array/Iterable of polygons in stacking order, back to front. Each is a Group or an Iterable<PtLike>, or a list of rings for a polygon with holes. A single ring is taken as one shape.
|
|
1810
|
+
* @returns an array of polygons, one per face inside the mask, each an outer ring followed by its holes
|
|
1811
|
+
* @example `Path.crop( [photo, frame] )`
|
|
1812
|
+
*/
|
|
1813
|
+
static crop(shapes: Iterable<PolygonLike> | PtLikeIterable): Group[][] {
|
|
1814
|
+
return overlay(shapes, "crop");
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1728
1818
|
/**
|
|
1729
1819
|
* Curve class provides static functions to interpolate curves. A curve is usually represented as a Group of 3 or more control points.
|
|
1730
1820
|
* You can use the static functions as-is, or apply the [`Group.op`](#link) or [`Pt.op`](#link) to enable functional programming.
|
|
@@ -1975,6 +2065,124 @@ export class Curve {
|
|
|
1975
2065
|
);
|
|
1976
2066
|
}
|
|
1977
2067
|
|
|
2068
|
+
/**
|
|
2069
|
+
* Convert the anchors of a Cardinal curve into cubic Bezier control points, so the same curve can be drawn as a native path
|
|
2070
|
+
* with [`CanvasForm.bezier`](#link) or sampled with [`Curve.bezier`](#link).
|
|
2071
|
+
* With the default `alpha`, the Bezier traces the curve that [`Curve.cardinal`](#link) approximates with line segments,
|
|
2072
|
+
* subject to the float32 rounding of a Pt.
|
|
2073
|
+
* See a [demo here](https://ptsjs.org/demo/?name=curve.cardinal).
|
|
2074
|
+
*
|
|
2075
|
+
* Set `alpha` to 0.5 for centripetal or to 1 for chordal parameterization. At the default tension of 0.5,
|
|
2076
|
+
* centripetal Catmull-Rom segments with distinct adjacent anchors have no internal loops or cusps
|
|
2077
|
+
* (Yuksel, Schaefer and Keyser, 2011); changing tension can introduce them.
|
|
2078
|
+
* For non-uniform curves, coincident consecutive anchors give a constant segment and zero tangents at its ends.
|
|
2079
|
+
* @param pts a Group or an Iterable<PtLike> of anchor points
|
|
2080
|
+
* @param tension optional value between 0 to 1 to specify a "tension". Default to 0.5 which is the tension for Catmull-Rom curve.
|
|
2081
|
+
* @param alpha optional knot parameterization: 0 (default) is uniform, 0.5 is centripetal, 1 is chordal
|
|
2082
|
+
* @returns a Group of `3(n-1)+1` Pts in the layout that [`Curve.bezier`](#link) takes: each anchor is followed by the 2 control points of the segment that starts there
|
|
2083
|
+
* @example `form.bezier( Curve.cardinalToBezier( pts ) )`
|
|
2084
|
+
*/
|
|
2085
|
+
static cardinalToBezier(
|
|
2086
|
+
pts: PtLikeIterable,
|
|
2087
|
+
tension: number = 0.5,
|
|
2088
|
+
alpha: number = 0,
|
|
2089
|
+
): Group {
|
|
2090
|
+
const out = new Group();
|
|
2091
|
+
if (!(alpha >= 0) || alpha === Infinity) {
|
|
2092
|
+
return Util.warn(
|
|
2093
|
+
"cardinalToBezier needs a finite alpha of 0 or more",
|
|
2094
|
+
out,
|
|
2095
|
+
);
|
|
2096
|
+
}
|
|
2097
|
+
const p = Util.iterToArray(pts);
|
|
2098
|
+
const n = p.length;
|
|
2099
|
+
if (n < 2) return out;
|
|
2100
|
+
const dim3 = p[0].length > 2;
|
|
2101
|
+
|
|
2102
|
+
// Uniform curves need no knot array. Keep small nonzero distances: replacing them
|
|
2103
|
+
// with an absolute epsilon changes the curve when coordinates are scaled.
|
|
2104
|
+
const dt = alpha === 0 ? undefined : new Float64Array(n - 1);
|
|
2105
|
+
if (dt) {
|
|
2106
|
+
for (let i = 0; i < n - 1; i++) {
|
|
2107
|
+
const dx = p[i + 1][0] - p[i][0];
|
|
2108
|
+
const dy = p[i + 1][1] - p[i][1];
|
|
2109
|
+
const dz = dim3 ? p[i + 1][2] - p[i][2] : 0;
|
|
2110
|
+
const d = Math.pow(Math.hypot(dx, dy, dz), alpha);
|
|
2111
|
+
dt[i] = d < Infinity ? d : 0; // an overflowing interval acts like a repeated anchor
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
// Each segment is a cubic Hermite between two anchors, and a Hermite converts to a Bezier by
|
|
2116
|
+
// placing the control points a third of the way along the end tangents. The tangent at an
|
|
2117
|
+
// anchor comes from its two neighbors in the non-uniform Catmull-Rom form; an end anchor stands
|
|
2118
|
+
// in for its missing neighbor, which is the duplicated-endpoint convention of `Curve.cardinal`.
|
|
2119
|
+
const control = (
|
|
2120
|
+
o: PtLike,
|
|
2121
|
+
k: number,
|
|
2122
|
+
mx: number,
|
|
2123
|
+
my: number,
|
|
2124
|
+
mz: number,
|
|
2125
|
+
) => {
|
|
2126
|
+
const pt = new Pt(dim3 ? 3 : 2);
|
|
2127
|
+
pt[0] = o[0] + k * mx;
|
|
2128
|
+
pt[1] = o[1] + k * my;
|
|
2129
|
+
if (dim3) pt[2] = o[2] + k * mz;
|
|
2130
|
+
return pt;
|
|
2131
|
+
};
|
|
2132
|
+
|
|
2133
|
+
for (let i = 0; i < n; i++) {
|
|
2134
|
+
const p0 = p[Math.max(i - 1, 0)];
|
|
2135
|
+
const p1 = p[i];
|
|
2136
|
+
const p2 = p[Math.min(i + 1, n - 1)];
|
|
2137
|
+
const a = dt ? dt[Math.max(i - 1, 0)] : 1;
|
|
2138
|
+
const b = dt ? dt[Math.min(i, n - 2)] : 1;
|
|
2139
|
+
// Weighted adjacent differences, with weights shared across coordinates.
|
|
2140
|
+
// A repeated anchor has zero tangent on both sides: constant segments stay
|
|
2141
|
+
// constant, and reversing the anchors applies the same degeneracy policy.
|
|
2142
|
+
let wa = 0;
|
|
2143
|
+
let wb = 0;
|
|
2144
|
+
if (dt && a > 0 && b > 0) {
|
|
2145
|
+
const s = (2 * tension) / (a + b);
|
|
2146
|
+
wa = s * (b / a);
|
|
2147
|
+
wb = s * (a / b);
|
|
2148
|
+
}
|
|
2149
|
+
const mx = dt
|
|
2150
|
+
? wa * (p1[0] - p0[0]) + wb * (p2[0] - p1[0])
|
|
2151
|
+
: tension * (p2[0] - p0[0]);
|
|
2152
|
+
const my = dt
|
|
2153
|
+
? wa * (p1[1] - p0[1]) + wb * (p2[1] - p1[1])
|
|
2154
|
+
: tension * (p2[1] - p0[1]);
|
|
2155
|
+
const mz = dim3
|
|
2156
|
+
? dt
|
|
2157
|
+
? wa * (p1[2] - p0[2]) + wb * (p2[2] - p1[2])
|
|
2158
|
+
: tension * (p2[2] - p0[2])
|
|
2159
|
+
: 0;
|
|
2160
|
+
|
|
2161
|
+
if (i > 0) out.push(control(p1, -a / 3, mx, my, mz));
|
|
2162
|
+
out.push(new Pt(p1));
|
|
2163
|
+
if (i < n - 1) out.push(control(p1, b / 3, mx, my, mz));
|
|
2164
|
+
}
|
|
2165
|
+
return out;
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
/**
|
|
2169
|
+
* Convert a chain of cubic Bezier curves into the anchors of a Cardinal curve: the inverse of [`Curve.cardinalToBezier`](#link).
|
|
2170
|
+
* A Cardinal curve's tangents come from its neighboring anchors, so this keeps every Bezier anchor and drops the Bezier handles.
|
|
2171
|
+
* The Cardinal curve still passes through the same anchors; between them it follows the handles only when they were
|
|
2172
|
+
* in Cardinal form to begin with and the original tension and alpha are reused. Coordinates are rounded to float32.
|
|
2173
|
+
* @param pts a Group or an Iterable<PtLike> in the layout of [`Curve.bezier`](#link); an incomplete trailing segment is ignored
|
|
2174
|
+
* @returns a Group of anchors for [`Curve.cardinal`](#link) or [`Curve.cardinalToBezier`](#link), with the tension and alpha of your choice
|
|
2175
|
+
* @example `Curve.cardinal( Curve.bezierToCardinal( chain ), 10, 0.5 )`
|
|
2176
|
+
*/
|
|
2177
|
+
static bezierToCardinal(pts: PtLikeIterable): Group {
|
|
2178
|
+
const p = Util.iterToArray(pts);
|
|
2179
|
+
const m = Math.floor((p.length - 1) / 3); // complete segments
|
|
2180
|
+
const out = new Group();
|
|
2181
|
+
if (m < 1) return out;
|
|
2182
|
+
for (let k = 0; k <= m; k++) out.push(new Pt(p[3 * k]));
|
|
2183
|
+
return out;
|
|
2184
|
+
}
|
|
2185
|
+
|
|
1978
2186
|
/**
|
|
1979
2187
|
* Create a Bezier curve. In a cubic bezier curve, the first and 4th anchors are end-points, and 2nd and 3rd anchors are control-points.
|
|
1980
2188
|
* @param pts a group of anchor Pt
|
|
@@ -2124,4 +2332,111 @@ export class Curve {
|
|
|
2124
2332
|
tension * (t3 / 6),
|
|
2125
2333
|
);
|
|
2126
2334
|
}
|
|
2335
|
+
|
|
2336
|
+
/**
|
|
2337
|
+
* Convert the anchors of a B-spline curve into cubic Bezier control points, so the same curve can be drawn as a native path
|
|
2338
|
+
* with [`CanvasForm.bezier`](#link) or sampled with [`Curve.bezier`](#link).
|
|
2339
|
+
* The Bezier traces the curve that [`Curve.bspline`](#link) approximates with line segments,
|
|
2340
|
+
* subject to the float32 rounding of a Pt. See a [demo here](https://ptsjs.org/demo/?name=curve.bspline).
|
|
2341
|
+
* @param pts a Group or an Iterable<PtLike> of at least 4 anchor points
|
|
2342
|
+
* @param tension optional value between 0 to n to specify a "tension". Default is 1 which is the usual tension.
|
|
2343
|
+
* @returns a Group of `3(n-3)+1` Pts in the layout that [`Curve.bezier`](#link) takes
|
|
2344
|
+
* @example `form.bezier( Curve.bsplineToBezier( pts ) )`
|
|
2345
|
+
*/
|
|
2346
|
+
static bsplineToBezier(pts: PtLikeIterable, tension: number = 1): Group {
|
|
2347
|
+
const p = Util.iterToArray(pts);
|
|
2348
|
+
const n = p.length;
|
|
2349
|
+
const out = new Group();
|
|
2350
|
+
if (n < 4) return out;
|
|
2351
|
+
const dim3 = p[0].length > 2;
|
|
2352
|
+
|
|
2353
|
+
// A segment spans anchors p1..p2 with neighbors p0 and p3. Its end points blend three
|
|
2354
|
+
// anchors (a, 1-2a, a) and its control points blend p1 and p2 only; at tension 1 these
|
|
2355
|
+
// are the (1, 4, 1)/6 and (2, 1)/3 weights of Böhm's knot insertion.
|
|
2356
|
+
const a = tension / 6;
|
|
2357
|
+
const c = 1 - 2 * a;
|
|
2358
|
+
const blend = (
|
|
2359
|
+
u: PtLike,
|
|
2360
|
+
v: PtLike,
|
|
2361
|
+
w: PtLike,
|
|
2362
|
+
wu: number,
|
|
2363
|
+
wv: number,
|
|
2364
|
+
ww: number,
|
|
2365
|
+
) => {
|
|
2366
|
+
const pt = new Pt(dim3 ? 3 : 2);
|
|
2367
|
+
pt[0] = wu * u[0] + wv * v[0] + ww * w[0];
|
|
2368
|
+
pt[1] = wu * u[1] + wv * v[1] + ww * w[1];
|
|
2369
|
+
if (dim3) pt[2] = wu * u[2] + wv * v[2] + ww * w[2];
|
|
2370
|
+
return pt;
|
|
2371
|
+
};
|
|
2372
|
+
|
|
2373
|
+
out.push(blend(p[0], p[1], p[2], a, c, a));
|
|
2374
|
+
for (let i = 1; i < n - 2; i++) {
|
|
2375
|
+
const p1 = p[i];
|
|
2376
|
+
const p2 = p[i + 1];
|
|
2377
|
+
out.push(
|
|
2378
|
+
blend(p1, p2, p2, c, 2 * a, 0),
|
|
2379
|
+
blend(p1, p2, p2, 2 * a, c, 0),
|
|
2380
|
+
blend(p1, p2, p[i + 2], a, c, a),
|
|
2381
|
+
);
|
|
2382
|
+
}
|
|
2383
|
+
return out;
|
|
2384
|
+
}
|
|
2385
|
+
|
|
2386
|
+
/**
|
|
2387
|
+
* Convert a chain of cubic Bezier curves into the anchors of a B-spline curve: the inverse of [`Curve.bsplineToBezier`](#link) at its default tension.
|
|
2388
|
+
* A B-spline does not pass through its anchors, so they are solved for: the result is the B-spline that passes through
|
|
2389
|
+
* every Bezier anchor and starts and ends along the Bezier's end handles (a tridiagonal system, solved in linear time).
|
|
2390
|
+
* For a chain that `bsplineToBezier` produced at tension 1 this recovers its anchors up to float32 rounding; for any other chain the B-spline keeps
|
|
2391
|
+
* the anchors and the end tangents, and its interior, being smooth to the second derivative, can only approximate the other handles.
|
|
2392
|
+
* @param pts a Group or an Iterable<PtLike> in the layout of [`Curve.bezier`](#link); an incomplete trailing segment is ignored
|
|
2393
|
+
* @returns a Group of `m+3` anchors for `m` Bezier segments, for [`Curve.bspline`](#link) or [`Curve.bsplineToBezier`](#link)
|
|
2394
|
+
* @example `Curve.bspline( Curve.bezierToBspline( chain ) )`
|
|
2395
|
+
*/
|
|
2396
|
+
static bezierToBspline(pts: PtLikeIterable): Group {
|
|
2397
|
+
const p = Util.iterToArray(pts);
|
|
2398
|
+
const m = Math.floor((p.length - 1) / 3); // complete segments
|
|
2399
|
+
const out = new Group();
|
|
2400
|
+
if (m < 1) return out;
|
|
2401
|
+
const dim = p[0].length > 2 ? 3 : 2;
|
|
2402
|
+
|
|
2403
|
+
// Unknowns are the anchors P1..P(m+1). Each Bezier anchor gives one row of (1, 4, 1)·P = 6·A,
|
|
2404
|
+
// and the two end rows fold in the end tangents, which also fix P0 and P(m+2) afterwards.
|
|
2405
|
+
// The right-hand sides then simplify to 3× the first handle, 6× each interior anchor, and 3× the last handle.
|
|
2406
|
+
const n = m + 1;
|
|
2407
|
+
const r = new Float64Array(n * dim);
|
|
2408
|
+
const d = new Float64Array(n); // pivots of the tridiagonal (2, 4, ..., 4, 2) with unit off-diagonals
|
|
2409
|
+
for (let k = 0; k < n; k++) {
|
|
2410
|
+
const src = k === 0 ? p[1] : k === m ? p[3 * m - 1] : p[3 * k];
|
|
2411
|
+
const w = k === 0 || k === m ? 3 : 6;
|
|
2412
|
+
for (let j = 0; j < dim; j++) r[k * dim + j] = w * src[j];
|
|
2413
|
+
}
|
|
2414
|
+
d[0] = 2;
|
|
2415
|
+
for (let k = 1; k < n; k++) {
|
|
2416
|
+
const w = 1 / d[k - 1];
|
|
2417
|
+
d[k] = (k < m ? 4 : 2) - w;
|
|
2418
|
+
for (let j = 0; j < dim; j++) r[k * dim + j] -= w * r[(k - 1) * dim + j];
|
|
2419
|
+
}
|
|
2420
|
+
for (let j = 0; j < dim; j++) r[(n - 1) * dim + j] /= d[n - 1];
|
|
2421
|
+
for (let k = n - 2; k >= 0; k--) {
|
|
2422
|
+
for (let j = 0; j < dim; j++) {
|
|
2423
|
+
r[k * dim + j] = (r[k * dim + j] - r[(k + 1) * dim + j]) / d[k];
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2427
|
+
const first = new Pt(dim);
|
|
2428
|
+
const last = new Pt(dim);
|
|
2429
|
+
for (let j = 0; j < dim; j++) {
|
|
2430
|
+
first[j] = r[dim + j] - 6 * (p[1][j] - p[0][j]);
|
|
2431
|
+
last[j] = r[(m - 1) * dim + j] + 6 * (p[3 * m][j] - p[3 * m - 1][j]);
|
|
2432
|
+
}
|
|
2433
|
+
out.push(first);
|
|
2434
|
+
for (let k = 0; k < n; k++) {
|
|
2435
|
+
const pt = new Pt(dim);
|
|
2436
|
+
for (let j = 0; j < dim; j++) pt[j] = r[k * dim + j];
|
|
2437
|
+
out.push(pt);
|
|
2438
|
+
}
|
|
2439
|
+
out.push(last);
|
|
2440
|
+
return out;
|
|
2441
|
+
}
|
|
2127
2442
|
}
|
package/src/Pt.ts
CHANGED
|
@@ -1137,7 +1137,14 @@ export class Bound extends Group implements IPt {
|
|
|
1137
1137
|
const n = b ? b.length : 0;
|
|
1138
1138
|
if (this._size.length !== n) this._size = new Pt(n);
|
|
1139
1139
|
for (let i = 0; i < n; i++) {
|
|
1140
|
-
|
|
1140
|
+
let lo = a ? a[i] || 0 : 0;
|
|
1141
|
+
if (a && b[i] < lo) {
|
|
1142
|
+
// corners given in the other order: keep top-left the smaller one
|
|
1143
|
+
a[i] = b[i];
|
|
1144
|
+
b[i] = lo;
|
|
1145
|
+
lo = a[i];
|
|
1146
|
+
}
|
|
1147
|
+
this._size[i] = Math.abs(b[i] - lo);
|
|
1141
1148
|
}
|
|
1142
1149
|
this._updateCenter();
|
|
1143
1150
|
}
|
package/src/Svg.ts
CHANGED
|
@@ -1316,6 +1316,36 @@ export class SVGForm extends CanvasForm<SVGSpace> {
|
|
|
1316
1316
|
: CanvasForm.rect(ctx, pts);
|
|
1317
1317
|
}
|
|
1318
1318
|
|
|
1319
|
+
/** Draw through a rendering context, or use the legacy per-element DOM context. */
|
|
1320
|
+
static bezier(
|
|
1321
|
+
ctx: DOMFormContext,
|
|
1322
|
+
pts: PtLikeIterable,
|
|
1323
|
+
): SVGElement | undefined;
|
|
1324
|
+
static bezier(ctx: RenderingContext2D, pts: PtLikeIterable): void;
|
|
1325
|
+
static bezier(ctx: DOMFormContext | RenderingContext2D, pts: PtLikeIterable) {
|
|
1326
|
+
return "style" in ctx
|
|
1327
|
+
? SVGForm.bezierElement(ctx, pts)
|
|
1328
|
+
: CanvasForm.bezier(ctx, pts);
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
/** Draw through a rendering context, or use the legacy per-element DOM context. */
|
|
1332
|
+
static compound(
|
|
1333
|
+
ctx: DOMFormContext,
|
|
1334
|
+
rings: Iterable<PtLikeIterable>,
|
|
1335
|
+
): SVGElement | undefined;
|
|
1336
|
+
static compound(
|
|
1337
|
+
ctx: RenderingContext2D,
|
|
1338
|
+
rings: Iterable<PtLikeIterable>,
|
|
1339
|
+
): void;
|
|
1340
|
+
static compound(
|
|
1341
|
+
ctx: DOMFormContext | RenderingContext2D,
|
|
1342
|
+
rings: Iterable<PtLikeIterable>,
|
|
1343
|
+
) {
|
|
1344
|
+
return "style" in ctx
|
|
1345
|
+
? SVGForm.compoundElement(ctx, rings)
|
|
1346
|
+
: CanvasForm.compound(ctx, rings);
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1319
1349
|
/** Draw through a rendering context, or use the legacy per-element DOM context. */
|
|
1320
1350
|
static text(ctx: DOMFormContext, pt: PtLike, txt: string): SVGElement;
|
|
1321
1351
|
static text(
|
|
@@ -1519,6 +1549,55 @@ export class SVGForm extends CanvasForm<SVGSpace> {
|
|
|
1519
1549
|
return SVGForm._poly(ctx, points.string, true);
|
|
1520
1550
|
}
|
|
1521
1551
|
|
|
1552
|
+
/**
|
|
1553
|
+
* A static function to draw a chain of cubic Bezier curves as one path element.
|
|
1554
|
+
* @param ctx a context object of SVGForm
|
|
1555
|
+
* @param pts a Group or an Iterable<PtLike> in the layout of [`Curve.bezier`](#link); an incomplete trailing segment is ignored
|
|
1556
|
+
*/
|
|
1557
|
+
static bezierElement(
|
|
1558
|
+
ctx: DOMFormContext,
|
|
1559
|
+
pts: PtLikeIterable,
|
|
1560
|
+
): SVGElement | undefined {
|
|
1561
|
+
const p = Util.iterToArray(pts);
|
|
1562
|
+
if (p.length < 4) return;
|
|
1563
|
+
let d = `M${p[0][0]} ${p[0][1]}`;
|
|
1564
|
+
for (let i = 3; i < p.length; i += 3) {
|
|
1565
|
+
d += `C${p[i - 2][0]} ${p[i - 2][1]} ${p[i - 1][0]} ${p[i - 1][1]} ${p[i][0]} ${p[i][1]}`;
|
|
1566
|
+
}
|
|
1567
|
+
return SVGForm._pathElement(ctx, d);
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
/**
|
|
1571
|
+
* A static function to draw a compound polygon as one path element, so that a ring inside another with the opposite orientation becomes a hole.
|
|
1572
|
+
* @param ctx a context object of SVGForm
|
|
1573
|
+
* @param rings an Array/Iterable of rings, each a Group or an Iterable<PtLike>; rings with fewer than 2 points are skipped
|
|
1574
|
+
*/
|
|
1575
|
+
static compoundElement(
|
|
1576
|
+
ctx: DOMFormContext,
|
|
1577
|
+
rings: Iterable<PtLikeIterable>,
|
|
1578
|
+
): SVGElement | undefined {
|
|
1579
|
+
let d = "";
|
|
1580
|
+
for (const ring of rings) {
|
|
1581
|
+
const p = Util.iterToArray(ring);
|
|
1582
|
+
if (p.length < 2 || typeof p[0][0] !== "number") continue;
|
|
1583
|
+
d += `M${p[0][0]} ${p[0][1]}`;
|
|
1584
|
+
for (let i = 1; i < p.length; i++) d += `L${p[i][0]} ${p[i][1]}`;
|
|
1585
|
+
d += "Z";
|
|
1586
|
+
}
|
|
1587
|
+
if (d === "") return;
|
|
1588
|
+
return SVGForm._pathElement(ctx, d);
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
protected static _pathElement(ctx: DOMFormContext, d: string): SVGElement {
|
|
1592
|
+
const elem = SVGSpace.svgElement(ctx.group, "path", SVGForm.getID(ctx));
|
|
1593
|
+
DOMSpace.setAttr(elem, {
|
|
1594
|
+
d,
|
|
1595
|
+
class: `pts-svgform pts-path ${ctx.currentClass}`,
|
|
1596
|
+
});
|
|
1597
|
+
SVGForm.style(elem, ctx.style);
|
|
1598
|
+
return elem;
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1522
1601
|
/**
|
|
1523
1602
|
* A static function to draw a rectangle element.
|
|
1524
1603
|
* @param ctx a context object of SVGForm
|
package/src/Types.ts
CHANGED
|
@@ -36,6 +36,12 @@ export type PtIterable = GroupLike | Pt[] | Iterable<Pt>;
|
|
|
36
36
|
*/
|
|
37
37
|
export type PtLikeIterable = GroupLike | PtLike[] | Iterable<PtLike>;
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Typescript type: PolygonLike represents a polygon for [`Path`](#link): either one ring of points (any `PtLikeIterable`),
|
|
41
|
+
* or a list of rings combined by the nonzero winding rule, such as the `Group[]` a Path function returns (an outer ring followed by its holes).
|
|
42
|
+
*/
|
|
43
|
+
export type PolygonLike = PtLikeIterable | Iterable<PtLikeIterable>;
|
|
44
|
+
|
|
39
45
|
/**
|
|
40
46
|
* Typescript type: TextMeasure represents a function that returns the rendered width of a string of text, such as canvas context's `measureText` or an estimator created via [`Typography.textWidthEstimator`](#link).
|
|
41
47
|
*/
|
|
@@ -184,6 +190,17 @@ export type FlockOptions = {
|
|
|
184
190
|
initialSpeed?: number;
|
|
185
191
|
};
|
|
186
192
|
|
|
193
|
+
/**
|
|
194
|
+
* Typescript type: PoissonDiskOptions are the settings accepted by [`Create.sampling`](#link)
|
|
195
|
+
* and [`PoissonDisk.setup`](#link). Every field is optional.
|
|
196
|
+
*/
|
|
197
|
+
export type PoissonDiskOptions = {
|
|
198
|
+
/** Maximum candidates tried per visit to an active sample. More candidates generally pack tighter and take longer. Rounded down; default is 8. */
|
|
199
|
+
candidates?: number;
|
|
200
|
+
/** The first sample, which must lie on or after the bound's top-left edges and strictly before its bottom-right edges (the bound is half-open). Default is a random point inside the bound. */
|
|
201
|
+
start?: PtLike;
|
|
202
|
+
};
|
|
203
|
+
|
|
187
204
|
/**
|
|
188
205
|
* Typescript type: DOMFormContext represents the current context for an DOMForm.
|
|
189
206
|
*/
|