1
0
mirror of https://github.com/Pomax/BezierInfo-2.git synced 2025-08-24 01:03:25 +02:00

offsetting

This commit is contained in:
Pomax
2020-09-04 22:01:35 -07:00
parent 83dcab57cb
commit 7e5c6e2eba
38 changed files with 15520 additions and 412 deletions

View File

@@ -12,5 +12,10 @@ Like normal offsetting we cut up our curve in sub-curves, and then check at whic
At each of the relevant points (start, end, and the projections of the control points onto the curve) we know the curve's normal, so offsetting is simply a matter of taking our original point, and moving it along the normal vector by the offset distance for each point. Doing so will give us the following result (these have with a starting width of 0, and an end width of 40 pixels, but can be controlled with your up and down arrow keys): At each of the relevant points (start, end, and the projections of the control points onto the curve) we know the curve's normal, so offsetting is simply a matter of taking our original point, and moving it along the normal vector by the offset distance for each point. Doing so will give us the following result (these have with a starting width of 0, and an end width of 40 pixels, but can be controlled with your up and down arrow keys):
<Graphic title="Offsetting a quadratic Bézier curve" setup={this.setupQuadratic} draw={this.draw} onKeyDown={this.props.onKeyDown}/> <graphics-element title="Offsetting a quadratic Bézier curve" src="./offsetting.js" data-type="quadratic">
<Graphic title="Offsetting a cubic Bézier curve" setup={this.setupCubic} draw={this.draw} onKeyDown={this.props.onKeyDown}/> <input type="range" min="5" max="50" step="1" value="20" class="slide-control">
</graphics-element>
<graphics-element title="Offsetting a cubic Bézier curve" src="./offsetting.js" data-type="cubic">
<input type="range" min="5" max="50" step="1" value="20" class="slide-control">
</graphics-element>

View File

@@ -1,37 +0,0 @@
module.exports = {
statics: {
keyHandlingOptions: {
propName: "distance",
values: {
"38": 1, // up arrow
"40": -1 // down arrow
}
}
},
setup: function(api, curve) {
api.setCurve(curve);
api.distance = 20;
},
setupQuadratic: function(api) {
var curve = api.getDefaultQuadratic();
this.setup(api, curve);
},
setupCubic: function(api) {
var curve = api.getDefaultCubic();
this.setup(api, curve);
},
draw: function(api, curve) {
api.reset();
api.drawSkeleton(curve);
api.drawCurve(curve);
api.setColor("blue");
var outline = curve.outline(0,0,api.distance,api.distance);
outline.curves.forEach(c => api.drawCurve(c));
}
};

View File

@@ -0,0 +1,63 @@
let curve;
setup() {
const type = this.parameters.type ?? `quadratic`;
if (type === `quadratic`) {
curve = Bezier.defaultQuadratic(this);
} else {
curve = Bezier.defaultCubic(this);
}
setMovable(curve.points);
setSlider(`.slide-control`, `distance`, 20);
}
draw() {
clear();
noFill();
curve.drawSkeleton();
curve.drawCurve(`lightblue`);
this.outline(curve, this.distance).forEach(c => this.drawCurve(c));
curve.drawPoints();
};
drawCurve(c) {
setStroke( randomColor() );
start()
c.getLUT(16).forEach(p => vertex(p.x, p.y));
end();
}
outline(curve, d) {
const reduced = curve.reduce(),
fcurves = [];
let bcurves = [],
alen = 0,
tlen = curve.length();
// form curve oulines
reduced.forEach(segment => {
let slen = segment.length();
fcurves.push(segment.scale(this.linearDistanceFunction(d, tlen, alen, slen)));
bcurves.push(segment.scale(this.linearDistanceFunction(-d, tlen, alen, slen)));
alen += slen;
});
// reverse the "return" outline
bcurves = bcurves
.map(s => {
s.points = s.points.reverse();
return s;
})
.reverse();
return [...fcurves, ...bcurves];
}
linearDistanceFunction(d, tlen, alen, slen) {
return v => {
const f1 = alen / tlen,
f2 = (alen + slen) / tlen;
return map(v, 0, 1, f1 * d, f2 * d);
};
}

View File

@@ -57,9 +57,14 @@ So, you cannot offset a Bézier curve perfectly with another Bézier curve, no m
A good way to do this reduction is to first find the curve's extreme points, as explained in the earlier section on curve extremities, and use these as initial splitting points. After this initial split, we can check each individual segment to see if it's "safe enough" based on where the center of the curve is. If the on-curve point for `t=0.5` is too far off from the center, we simply split the segment down the middle. Generally this is more than enough to end up with safe segments. A good way to do this reduction is to first find the curve's extreme points, as explained in the earlier section on curve extremities, and use these as initial splitting points. After this initial split, we can check each individual segment to see if it's "safe enough" based on where the center of the curve is. If the on-curve point for `t=0.5` is too far off from the center, we simply split the segment down the middle. Generally this is more than enough to end up with safe segments.
The following graphics show off curve offsetting, and you can use your up and down arrow keys to control the distance at which the curve gets offset. The curve first gets reduced to safe segments, each of which is then offset at the desired distance. Especially for simple curves, particularly easily set up for quadratic curves, no reduction is necessary, but the more twisty the curve gets, the more the curve needs to be reduced in order to get segments that can safely be scaled. The following graphics show off curve offsetting, and you can use the slider to control the distance at which the curve gets offset. The curve first gets reduced to safe segments, each of which is then offset at the desired distance. Especially for simple curves, particularly easily set up for quadratic curves, no reduction is necessary, but the more twisty the curve gets, the more the curve needs to be reduced in order to get segments that can safely be scaled.
<Graphic title="Offsetting a quadratic Bézier curve" setup={this.setupQuadratic} draw={this.draw} onKeyDown={this.props.onKeyDown} /> <graphics-element title="Offsetting a quadratic Bézier curve" src="./offsetting.js" data-type="quadratic">
<Graphic title="Offsetting a cubic Bézier curve" setup={this.setupCubic} draw={this.draw} onKeyDown={this.props.onKeyDown} /> <input type="range" min="5" max="50" step="1" value="20" class="slide-control">
</graphics-element>
<graphics-element title="Offsetting a cubic Bézier curve" src="./offsetting.js" data-type="cubic">
<input type="range" min="5" max="50" step="1" value="20" class="slide-control">
</graphics-element>
You may notice that this may still lead to small 'jumps' in the sub-curves when moving the curve around. This is caused by the fact that we're still performing a naive form of offsetting, moving the control points the same distance as the start and end points. If the curve is large enough, this may still lead to incorrect offsets. You may notice that this may still lead to small 'jumps' in the sub-curves when moving the curve around. This is caused by the fact that we're still performing a naive form of offsetting, moving the control points the same distance as the start and end points. If the curve is large enough, this may still lead to incorrect offsets.

View File

@@ -1,58 +0,0 @@
module.exports = {
statics: {
keyHandlingOptions: {
propName: "distance",
values: {
"38": 1, // up arrow
"40": -1 // down arrow
}
}
},
setup: function(api, curve) {
api.setCurve(curve);
api.distance = 20;
},
setupQuadratic: function(api) {
var curve = api.getDefaultQuadratic();
this.setup(api, curve);
},
setupCubic: function(api) {
var curve = api.getDefaultCubic();
this.setup(api, curve);
},
draw: function(api, curve) {
api.reset();
api.drawSkeleton(curve);
var reduced = curve.reduce();
reduced.forEach(c => {
api.setRandomColor();
api.drawCurve(c);
api.drawCircle(c.points[0], 1);
});
var last = reduced.slice(-1)[0];
api.drawPoint(last.points[3] || last.points[2]);
api.setColor("red");
var offset = curve.offset(api.distance);
offset.forEach(c => {
api.drawPoint(c.points[0]);
api.drawCurve(c);
});
last = offset.slice(-1)[0];
api.drawPoint(last.points[3] || last.points[2]);
api.setColor("blue");
offset = curve.offset(-api.distance);
offset.forEach(c => {
api.drawPoint(c.points[0]);
api.drawCurve(c);
});
last = offset.slice(-1)[0];
api.drawPoint(last.points[3] || last.points[2]);
}
};

View File

@@ -0,0 +1,118 @@
let curve;
setup() {
const type = this.parameters.type ?? `quadratic`;
if (type === `quadratic`) {
curve = Bezier.defaultQuadratic(this);
} else {
curve = Bezier.defaultCubic(this);
}
setMovable(curve.points);
setSlider(`.slide-control`, `distance`, 20);
}
draw() {
clear();
noFill();
curve.drawSkeleton();
var reduced = this.reduce(curve);
reduced.forEach(c => {
setStroke( randomColor() );
this.drawCurve(c);
circle(c.points[0].x, c.points[0].y, 2);
});
var last = reduced.slice(-1)[0];
let p = last.points[3] ?? last.points[2];
circle(p.x, p.y, 3);
setStroke(`#FF000050`);
var offset = curve.offset(this.distance);
offset.forEach(c => {
circle(c.points[0].x, c.points[0].y, 2);
this.drawCurve(c);
});
last = offset.slice(-1)[0];
p = last.points[3] ?? last.points[2];
circle(p.x, p.y, 3);
setStroke(`#0000FF50`);
var offset = curve.offset(-this.distance);
offset.forEach(c => {
circle(c.points[0].x, c.points[0].y, 2);
this.drawCurve(c);
});
last = offset.slice(-1)[0];
p = last.points[3] ?? last.points[2];
circle(p.x, p.y, 3);
curve.drawPoints();
}
drawCurve(c) {
start()
c.getLUT(16).forEach(p => vertex(p.x, p.y));
end();
}
reduce(curve) {
let i,
t1 = 0,
t2 = 0,
step = 0.01,
segment,
pass1 = [],
pass2 = [];
// first pass: split on extrema
let extrema = curve.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 = curve.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(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 = map(t1, 0, 1, p1._t1, p1._t2);
segment._t2 = map(t2, 0, 1, p1._t1, p1._t2);
pass2.push(segment);
t1 = t2;
break;
}
}
}
if (t1 < 1) {
segment = p1.split(t1, 1);
segment._t1 = map(t1, 0, 1, p1._t1, p1._t2);
segment._t2 = p1._t2;
pass2.push(segment);
}
});
return pass2;
}

View File

@@ -0,0 +1,133 @@
let shape1, shape2, ox=50, oy=50;
setup() {
let d = 30;
let p = [
{ x: d*2, y: d*1}, { x: d*1, y: d*2}, { x: d*1, y: d*4},
{ x: d*2, y: d*5}, { x: d*3, y: d*4}, { x: d*5, y: d*4},
{ x: d*6, y: d*5}, { x: d*7, y: d*4}, { x: d*7, y: d*2},
{ x: d*6, y: d*1}, { x: d*5, y: d*2}, { x: d*3, y: d*2},
]
shape1 = [
new Bezier(this, p[0].x, p[0].y, p[1].x, p[1].y, p[2].x, p[2].y, p[3].x, p[3].y),
new Bezier(this, p[3].x, p[3].y, p[4].x, p[4].y, p[5].x, p[5].y, p[6].x, p[6].y),
new Bezier(this, p[6].x, p[6].y, p[7].x, p[7].y, p[8].x, p[8].y, p[9].x, p[9].y),
new Bezier(this, p[9].x, p[9].y, p[10].x, p[10].y, p[11].x, p[11].y, p[0].x, p[0].y),
];
shape2 = [
new Bezier(this, p[0].x, p[0].y, p[1].x, p[1].y, p[2].x, p[2].y, p[3].x, p[3].y),
new Bezier(this, p[3].x, p[3].y, p[4].x, p[4].y, p[5].x, p[5].y, p[6].x, p[6].y),
new Bezier(this, p[6].x, p[6].y, p[7].x, p[7].y, p[8].x, p[8].y, p[9].x, p[9].y),
new Bezier(this, p[9].x, p[9].y, p[10].x, p[10].y, p[11].x, p[11].y, p[0].x, p[0].y),
];
// link the curves into paths
shape1.forEach((s, i) => (s.next = shape1[(i+1) % shape1.length]));
shape2.forEach((s, i) => (s.next = shape2[(i+1) % shape2.length]));
shape2.forEach(s => s.points.forEach(p => { p.x += ox; p.y += oy; }));
}
draw() {
clear();
/*
noFill();
shape2.forEach(c => c.points.forEach(p => {
p.x -= ox;
p.y -= oy;
}));
if (this.cursor.down) {
ox += this.cursor.diff.x;
oy += this.cursor.diff.y;
}
shape2.forEach(c => c.points.forEach(p => {
p.x += ox;
p.y += oy;
}));
shape1.forEach(curve => this.drawSegment(curve));
shape2.forEach(curve => this.drawSegment(curve));
this.drawBoolean(shape1, shape2);
*/
}
drawSegment(segment) {
setStroke( randomColor(0.2) );
start()
segment.getLUT(16).forEach(p => vertex(p.x, p.y))
end();
}
drawBoolean(s1, s2) {
let intersections = [];
// We're going to test for intersections in an *incredibly* naive fashion,
// simply checking each curve against each other curve. Normally you'd first
// throw way all curves that can't intersect, using bounding box checks or
// oct-tree space reduction, etc.
s1.forEach(curve1 => {
s2.forEach(curve2 => {
let ti = curve1.intersects(curve2);
if (ti.length) {
ti = ti.map(s => s.split(`/`).map(parseFloat));
// remove "near enough to be considered duplicate" t pairs
for(let i=ti.length-1; i>0; i--) {
if ( abs(ti[i][0] - ti[i-1][0]) < 0.01 && abs(ti[i][1] - ti[i-1][1]) < 0.01) {
ti.splice(i,1);
}
}
// store this curve pair with intersection t value(s)
intersections.push({ curve1, curve2, ti })
}
});
});
intersections.forEach(i => {
const { curve1, curve2, ti } = i;
ti.forEach(t => {
let c1 = curve1.get(t[0]); circle(c1.x, c1.y, 3);
let c2 = curve2.get(t[1]); circle(c2.x, c2.y, 3);
});
});
let nodes = [...s1, ...s2];
let sequence = [];
let node = nodes[0];
for(let i=0; i<2; i++) {
let entry = intersections.find(v => v.curve1 === node || v.curve2 === node);
if (entry) {
const { curve1, curve2, ti } = entry;
console.log(ti);
let c1, c2;
c1 = curve1.split(t[0]);
c2 = curve2.split(t[1]);
// resolve the intersections
console.log(`resolve `,entry);
sequence.push(node);
node = node.next;
} else {
sequence.push(node);
node = node.next;
}
}
sequence.forEach(c => c.drawCurve(`black`));
}
onMouseMove() {
if (this.cursor.down) {
redraw();
}
}

View File

@@ -1,3 +1,5 @@
# THIS SECTION IS CURRENTLY NOT PART OF THE MAIN DOCUMENT, AS IT DOES NOT ACTUALLY TEACH ANYTHING USEFUL.
# Boolean shape operations # Boolean shape operations
We can apply the topics covered so far in this primer to effect boolean shape operations: getting the union, intersection, or exclusion, between two or more shapes that involve Bézier curves. For simplicity (well... sort of, more homogeneity), we'll be looking at poly-Bézier shapes only, but a shape that consists of a mix of lines and Bézier curves is technically a simplification. (Although it does mean we need to write a definition for the class of shapes that mix lines and Bézier curves. Since poly-Bézier curves are a superset, we'll be using those in the following examples.) We can apply the topics covered so far in this primer to effect boolean shape operations: getting the union, intersection, or exclusion, between two or more shapes that involve Bézier curves. For simplicity (well... sort of, more homogeneity), we'll be looking at poly-Bézier shapes only, but a shape that consists of a mix of lines and Bézier curves is technically a simplification. (Although it does mean we need to write a definition for the class of shapes that mix lines and Bézier curves. Since poly-Bézier curves are a superset, we'll be using those in the following examples.)
@@ -17,33 +19,32 @@ Once we have all the new poly-Bézier curves, we run the first step of the desir
- Union: discard all poly-Bézier curves that lie "inside" our union of our shapes. E.g. if we want the union of two overlapping circles, the resulting shape is the outline. - Union: discard all poly-Bézier curves that lie "inside" our union of our shapes. E.g. if we want the union of two overlapping circles, the resulting shape is the outline.
- Intersection: discard all poly-Bézier curves that lie "outside" the intersection of the two shapes. E.g. if we want the intersection of two overlapping circles, the resulting shape is the tapered ellipse where they overlap. - Intersection: discard all poly-Bézier curves that lie "outside" the intersection of the two shapes. E.g. if we want the intersection of two overlapping circles, the resulting shape is the tapered ellipse where they overlap.
- Exclusion: none of the sections are discarded, but we will need to link the shapes back up in a special way. Flip any section that would qualify for removal under UNION rules. - Exclusion: none of the sections are discarded, but we will need to link the shapes back up in a special way. Flip any section that would qualify for removal under "union" rules.
<table class="sketch"><tbody><tr> <div class="grid">
<td class="labeled-image"> <figure>
<img src="images/op_base.gif" height="169"/> <img src="images/op_base.gif" height="169"/>
Two overlapping shapes. <figcaption>Two overlapping shapes</figcaption>
</td> </figure>
<td class="labeled-image"> <figure class="labeled-image">
<img src="images/op_union.gif" height="169"/> <img src="images/op_union.gif" height="169"/>
The unified region. <figcaption>Their union</figcaption>
</td> </figure>
<td class="labeled-image"> <figure class="labeled-image">
<img src="images/op_intersection.gif" height="169"/> <img src="images/op_intersection.gif" height="169"/>
Their intersection. <figcaption>Their intersection</figcaption>
</td> </figure>
<td class="labeled-image"> <figure class="labeled-image">
<img src="images/op_exclusion.gif" height="169"/> <img src="images/op_exclusion.gif" height="169"/>
Their exclusion regions. <figcaption>Their exclusion (union minus intersection)</figcaption>
</td> </figure>
</tr></tbody></table> </div>
The main complication in the outlined procedure here is determining how sections qualify in terms of being "inside" and "outside" of our shapes. For this, we need to be able to perform point-in-shape detection, for which we'll use a classic algorithm: getting the "crossing number" by using ray casting, and then testing for "insidedness" by applying the [even-odd rule](http://folk.uio.no/bjornw/doc/bifrost-ref/bifrost-ref-12.html): For any point and any shape, we can cast a ray from our point, to some point that we know lies outside of the shape (such as a corner of our drawing surface). We then count how many times that line crosses our shape (remember that we can perform line/curve intersection detection quite easily). If the number of times it crosses the shape's outline is even, the point did not actually lie inside our shape. If the number of intersections is odd, our point did lie inside out shape. With that knowledge, we can decide whether to treat a section that such a point lies on "needs removal" (under union rules), "needs preserving" (under intersection rules), or "needs flipping" (under exclusion rules). The main complication in the outlined procedure here is determining how sections qualify in terms of being "inside" and "outside" of our shapes. For this, we need to be able to perform point-in-shape detection, for which we'll use a classic algorithm: getting the "crossing number" by using ray casting, and then testing for "insidedness" by applying the [even-odd rule](http://folk.uio.no/bjornw/doc/bifrost-ref/bifrost-ref-12.html): For any point and any shape, we can cast a ray from our point, to some point that we know lies outside of the shape (such as a corner of our drawing surface). We then count how many times that line crosses our shape (remember that we can perform line/curve intersection detection quite easily). If the number of times it crosses the shape's outline is even, the point did not actually lie inside our shape. If the number of intersections is odd, our point did lie inside out shape. With that knowledge, we can decide whether to treat a section that such a point lies on "needs removal" (under union rules), "needs preserving" (under intersection rules), or "needs flipping" (under exclusion rules).
These operations are expensive, and implementing your own code for this is generally a bad idea if there is already a geometry package available for your language of choice. In this case, for JavaScript the most excellent [Paper.js](http://paperjs.org) already comes with all the code in place to perform efficient boolean shape operations, so rather that implement an inferior version here, I can strongly recommend the Paper.js library if you intend to do any boolean shape work. These operations are expensive, and implementing your own code for this is generally a bad idea if there is already a geometry package available for your language of choice. In this case, for JavaScript the most excellent [Paper.js](http://paperjs.org) already comes with all the code in place to perform efficient boolean shape operations, so rather that implement an inferior version here, I can strongly recommend the Paper.js library if you intend to do any boolean shape work.
The following graphic shows Paper.js doing its thing for two shapes: one static, and one that is linked to your mouse pointer. If you move the mouse around, you'll see how the shape intersections are resolved. The base shapes are outlined in blue, and the boolean result is coloured red. (Of course, as a general geometry library, Paper.js is also roughly the size of this entire primer, so for illustrative purposes the following graphic implements its own boolean operations, and may not do quite the right thing on all edge cases!)
<graphics-element title="Boolean shape operations" src="./boolean.js"></graphics-element>
<Graphic title="Boolean shape operations with Paper.js" paperjs={true} setup={this.setup} draw={this.draw} onMouseMove={this.onMouseMove}>
<button onclick="() => this.setMode(mode)">mode</button>
</Graphic>

File diff suppressed because it is too large Load Diff

View File

@@ -56,7 +56,7 @@ export default [
// "things made of more than on curve" // "things made of more than on curve"
'polybezier', 'polybezier',
'shapes', // 'shapes', // I am not happy with how this section basically doesn't teach anything
// 'drawing', // still just waiting to be finished...... // 'drawing', // still just waiting to be finished......
// curve offsetting // curve offsetting

BIN
docs/images/arc-c-2pi.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
docs/images/arc-c-pi.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
docs/images/arc-c-pi2.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
docs/images/arc-q-pi.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

BIN
docs/images/arc-q-pi2.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
docs/images/arc-q-pi4.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="108" height="16" viewBox="0 0 81 12"><defs><symbol overflow="visible" id="a"><path d="M9.078-5.219c0-2.125-1.375-3.484-3.531-3.484-2.828 0-5.063 2.312-5.063 5.328C.484-1.125 1.75.219 3.86.219c1.016 0 1.97-.266 2.735-.75C8.109-1.47 9.078-3.328 9.078-5.22zm-1.328.203c0 1.5-.453 2.75-1.266 3.594-.578.594-1.25.875-2.187.875-1.719 0-2.484-.906-2.484-2.89 0-1.563.453-2.86 1.265-3.688.563-.578 1.188-.813 2.094-.813.578 0 1.062.11 1.453.344.781.453 1.125 1.266 1.125 2.578zm0 0"/></symbol><symbol overflow="visible" id="b"><path d="M4.203-7.828a.735.735 0 01-.187-.14c-.063-.063-.11-.126-.22-.329-1.593 1.61-2.5 3.266-2.5 4.781v.797c0 1.516.907 3.172 2.5 4.781.11-.203.157-.265.22-.328.062-.062.125-.109.312-.203C2.875.063 2.281-1.344 2.281-2.719v-.797c0-1.39.594-2.78 2.047-4.25zm0 0"/></symbol><symbol overflow="visible" id="c"><path d="M3.703-5.516c-.453.047-.86.063-1.156.063.172-.984.297-1.578.531-2.25l-.25-.328a7.16 7.16 0 01-1.094.531l-.296 2.031c-.391.203-.704.328-1.063.407l-.047.406h1l-.64 3.25C.625-1.11.53-.813.53-.5c0 .297.266.61.5.61.422 0 .922-.282 1.86-1.032.218-.172.14-.125.437-.36l-.25-.437-.672.469c-.36.25-.484.313-.625.313-.093 0-.031.046-.031-.11 0-.297.156-1.234.516-3l.14-.61h1.266l.203-.89zm0 0"/></symbol><symbol overflow="visible" id="d"><path d="M3.766-2.719v-.797c0-1.515-.907-3.171-2.516-4.78-.11.202-.156.265-.203.327-.063.063-.125.11-.313.203 1.438 1.47 2.032 2.86 2.032 4.25v.797c0 1.375-.594 2.781-2.032 4.25.188.094.25.14.313.203.047.063.094.125.203.329C2.86.452 3.766-1.204 3.766-2.72zm0 0"/></symbol><symbol overflow="visible" id="e"><path d="M8.266-4.078a1.419 1.419 0 01-.047-.36c0-.109.015-.234.062-.484h-7.5c.063.25.063.375.063.484 0 .125 0 .235-.063.5h7.5zm0 2.625a1.332 1.332 0 01-.047-.36c0-.109.015-.234.062-.484h-7.5c.063.25.063.375.063.485 0 .125 0 .25-.063.5h7.5zm0 0"/></symbol><symbol overflow="visible" id="f"><path d="M6.813-6.844c0-.984-1.094-1.687-2.016-1.687-.594 0-1.203.031-1.797.031l-1.938-.047-.062.61.797.03c.281 0 .266-.03.266.188 0 .172-.047.547-.11.907L.922-.782C.89-.577.875-.624.25-.531L.14.048A21.28 21.28 0 011.407 0c.64 0 1.266.031 1.922.031.563 0 1.219-.156 1.781-.5.954-.562 1.516-1.469 1.516-2.375 0-.515-.203-1-.531-1.265-.313-.25-.766-.391-1.594-.516v.266c.688-.25 1.063-.438 1.453-.75.531-.485.86-1.125.86-1.735zM5.296-2.656c0 1.234-.734 1.984-1.922 1.984-.25 0-.594-.015-.984-.047-.079-.015-.22-.015-.25-.031l.562-3.313h.906c1.235 0 1.688.375 1.688 1.407zm.219-3.985c0 1.266-.75 1.891-2.235 1.891h-.453l.531-3.016c.22 0 .532-.062.844-.062.969 0 1.313.297 1.313 1.187zm0 0"/></symbol><symbol overflow="visible" id="g"><path d="M8.266-2.766a1.332 1.332 0 01-.047-.359c0-.11.015-.234.062-.484h-3.25v-3.266c-.25.063-.375.078-.484.078-.125 0-.25-.016-.5-.078v3.266H.78c.063.25.063.375.063.484 0 .125 0 .25-.063.5h3.266V.641c.25-.063.375-.079.5-.079.11 0 .234.016.484.079v-3.266h3.25zm0 0"/></symbol><symbol overflow="visible" id="h"><path d="M5.875-8.86l-.219-.187c-.75.375-1.078.438-2.062.531l-.094.5h.75c.281 0 .25-.046.25.157 0 .109 0 .203-.016.296L4.188-5.89a3.109 3.109 0 00-.875-.14c-.829 0-2.063.906-2.61 1.937-.375.703-.64 1.938-.64 2.938C.063-.375.374.125.734.125c.313 0 .86-.234 1.266-.563.64-.515 1.031-.984 1.844-2.187l-.25-.094-.266 1.078c-.14.532-.203.907-.203 1.22 0 .25.25.546.453.546.219 0 .594-.203 1-.5l1.047-.766-.266-.468-.656.468c-.172.125-.25.172-.36.172-.093 0-.046.031-.046-.14 0-.094.031-.204.11-.547l1.515-7.157zM4.031-5.03l-.14.703A5.233 5.233 0 012.375-1.61c-.422.39-.781.64-1.016.64-.187 0-.171-.031-.171-.328 0-1.516.515-3.281 1.109-3.734.156-.125.281-.14.61-.14.53 0 .765.03 1.109.218zm0 0"/></symbol></defs><use xlink:href="#a" x="-.6" y="9.082"/><use xlink:href="#b" x="8.701" y="9.082"/><use xlink:href="#c" x="13.77" y="9.082"/><use xlink:href="#d" x="17.799" y="9.082"/><use xlink:href="#e" x="26.192" y="9.082"/><use xlink:href="#f" x="38.601" y="9.082"/><use xlink:href="#b" x="45.906" y="9.082"/><use xlink:href="#c" x="50.975" y="9.082"/><g><use xlink:href="#d" x="55.016" y="9.082"/></g><g><use xlink:href="#g" x="62.739" y="9.082"/></g><g><use xlink:href="#h" x="74.491" y="9.082"/></g></svg> <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="111px" height="17px" viewBox="0 0 83 13"><defs><symbol overflow="visible" id="a"><path d="M9.031-5.344c0-1.922-1.453-3.343-3.234-3.343-2.578 0-5.422 2.843-5.422 5.624C.375-1.077 1.922.267 3.641.267c2.546 0 5.39-2.75 5.39-5.61zm-1.453-.468c0 .859-.266 2.64-1.453 4.109-.563.719-1.406 1.39-2.406 1.39-1.188 0-1.828-.828-1.828-2.406 0-.531.109-2.187 1.03-3.578.813-1.25 1.86-1.828 2.798-1.828.984 0 1.86.531 1.86 2.313zm0 0"/></symbol><symbol overflow="visible" id="b"><path d="M4.156 2.719c0-.047-.078-.172-.11-.203-1.234-.922-2-3.22-2-5.141v-1c0-1.906.767-4.203 2-5.14a.457.457 0 00.11-.188c0-.063-.25-.25-.312-.25a1.01 1.01 0 00-.203.062C2.312-8.156 1-5.67 1-3.625v1C1-.578 2.313 1.906 3.64 2.906c.016.016.188.063.204.063.062 0 .312-.203.312-.25zm0 0"/></symbol><symbol overflow="visible" id="c"><path d="M4.14-5.156c0-.125-.312-.266-.53-.266H2.78c.39-1.562.453-1.797.453-1.875 0-.203-.328-.453-.53-.453-.032 0-.579.14-.688.563l-.422 1.765H.64c-.25 0-.563.14-.563.36 0 .156.297.28.531.28h.813C.594-1.515.547-1.311.547-1.093c0 .64.656 1.219 1.312 1.219 1.22 0 2.079-1.875 2.079-1.969 0-.11-.282-.25-.329-.25-.109 0-.328.172-.375.297C2.72-.547 2.281-.39 1.875-.39c-.25 0-.172-.03-.172-.421 0-.282.016-.375.063-.579l.859-3.39h.953c.25 0 .563-.125.563-.375zm0 0"/></symbol><symbol overflow="visible" id="d"><path d="M3.64-2.625v-1c0-2.047-1.328-4.531-2.64-5.516a.774.774 0 00-.203-.062c-.063 0-.313.187-.313.25 0 .031.079.172.094.187 1.25.938 2.016 3.235 2.016 5.141v1c0 1.922-.766 4.219-2.016 5.14-.015.032-.094.157-.094.204 0 .047.25.25.313.25A.774.774 0 001 2.906c1.313-1 2.64-3.484 2.64-5.531zm0 0"/></symbol><symbol overflow="visible" id="e"><path d="M8.828-4.281c0-.125-.312-.375-.437-.375H.906c-.125 0-.437.25-.437.375 0 .14.312.375.437.375h7.485c.125 0 .437-.235.437-.375zm0 2.328c0-.14-.312-.375-.437-.375H.906c-.125 0-.437.234-.437.375 0 .125.312.36.437.36h7.485c.125 0 .437-.235.437-.36zm0 0"/></symbol><symbol overflow="visible" id="f"><path d="M9.234-6.64c0-.891-.984-1.782-2.421-1.782H2.796c-.235 0-.547.125-.547.36 0 .14.313.265.531.265l.14-.031s.11.031.313.062c.22.016.125-.093.125.063 0 .047-.015.078-.046.219l-1.61 6.421c-.11.47.063.422-.875.422-.203 0-.531.141-.531.375 0 .141.312.266.531.266h4.266c1.89 0 3.484-1.547 3.484-2.719 0-.86-.875-1.687-2.047-1.812v.265c1.25-.234 2.703-1.25 2.703-2.375zM7.75-6.689c0 1.047-.828 2.047-2.281 2.047H3.937l.72-2.828c.093-.422-.063-.328.452-.328h1.532c1.062 0 1.109.578 1.109 1.11zm-.672 3.844c0 1.188-.86 2.203-2.266 2.203h-1.89c-.125-.015.031.11.031 0 0-.03 0-.046.063-.265l.78-3.203h2.11c1.14 0 1.172.75 1.172 1.265zm0 0"/></symbol><symbol overflow="visible" id="g"><path d="M8.828-3.125c0-.125-.312-.36-.437-.36H5.078v-3.374c0-.141-.297-.375-.422-.375-.14 0-.453.234-.453.375v3.375H.906c-.125 0-.437.234-.437.359 0 .14.312.375.437.375h3.297V.625c0 .125.313.36.453.36.125 0 .422-.235.422-.36V-2.75h3.313c.125 0 .437-.234.437-.375zm0 0"/></symbol><symbol overflow="visible" id="h"><path d="M6.297-8.203s-.125-.36-.281-.36c-.188 0-1.313.11-1.516.141-.094 0-.375.203-.375.36 0 .14.313.265.484.265.579 0 .407-.047.407.078l-.047.235-.719 2.828h.406c-.218-.438-.765-.89-1.297-.89-1.406 0-3.078 1.89-3.078 3.64 0 1.125.86 2.031 1.781 2.031.25 0 .97-.078 1.422-.61.032.094.657.61 1.22.61.421 0 .827-.297 1.015-.688l.062-.093c.203-.438.297-1.094.297-1.094l.063-.094c0-.11-.297-.25-.329-.25-.125 0-.343.188-.375.344-.203.781-.218 1.36-.703 1.36-.328 0-.171-.188-.171-.422 0-.282.03-.375.078-.579l1.718-6.906zm-2.188 4.11c0 .062-.015.14-.03.202l-.595 2.344c-.062.203 0 .14-.187.344-.531.656-.875.812-1.203.812-.594 0-.578-.53-.578-1 0-.593.328-1.968.593-2.515.375-.703.782-1.11 1.266-1.11.766 0 .734.844.734.922zm0 0"/></symbol></defs><use xlink:href="#a" x="-.425" y="9.082"/><use xlink:href="#b" x="8.745" y="9.082"/><use xlink:href="#c" x="13.395" y="9.082"/><use xlink:href="#d" x="17.711" y="9.082"/><use xlink:href="#e" x="25.685" y="9.082"/><use xlink:href="#f" x="38.31" y="9.082"/><use xlink:href="#b" x="47.683" y="9.082"/><use xlink:href="#c" x="52.333" y="9.082"/><use xlink:href="#d" x="56.649" y="9.082"/><g><use xlink:href="#g" x="63.954" y="9.082"/></g><g><use xlink:href="#h" x="75.921" y="9.082"/></g></svg>

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 9.6 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 967 B

BIN
docs/images/op_base.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

BIN
docs/images/op_union.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@@ -102,7 +102,6 @@
<li><a href="#catmullconv">Bézier curves and Catmull-Rom curves</a></li> <li><a href="#catmullconv">Bézier curves and Catmull-Rom curves</a></li>
<li><a href="#catmullfitting">Creating a Catmull-Rom curve from three points</a></li> <li><a href="#catmullfitting">Creating a Catmull-Rom curve from three points</a></li>
<li><a href="#polybezier">Forming poly-Bézier curves</a></li> <li><a href="#polybezier">Forming poly-Bézier curves</a></li>
<li><a href="#shapes">Boolean shape operations</a></li>
<li><a href="#offsetting">Curve offsetting</a></li> <li><a href="#offsetting">Curve offsetting</a></li>
<li><a href="#graduatedoffset">Graduated curve offsetting</a></li> <li><a href="#graduatedoffset">Graduated curve offsetting</a></li>
<li><a href="#circles">Circles and quadratic Bézier curves</a></li> <li><a href="#circles">Circles and quadratic Bézier curves</a></li>
@@ -2055,51 +2054,6 @@ for p = 1 to points.length-3 (inclusive):
<p>Again, we see that cubic curves are now rather nice to work with, but quadratic curves have a new, very serious problem: we can move an on-curve point in such a way that we can't compute what needs to "happen next". Move the top point down, below the left and right points, for instance. There is no way to preserve correct control points without a kink at the bottom point. Quadratic curves: just not that good...</p> <p>Again, we see that cubic curves are now rather nice to work with, but quadratic curves have a new, very serious problem: we can move an on-curve point in such a way that we can't compute what needs to "happen next". Move the top point down, below the left and right points, for instance. There is no way to preserve correct control points without a kink at the bottom point. Quadratic curves: just not that good...</p>
<p>A final improvement is to offer fine-level control over which points behave which, so that you can have "kinks" or individually controlled segments when you need them, with nicely well-behaved curves for the rest of the path. Implementing that, is left as an exercise for the reader.</p> <p>A final improvement is to offer fine-level control over which points behave which, so that you can have "kinks" or individually controlled segments when you need them, with nicely well-behaved curves for the rest of the path. Implementing that, is left as an exercise for the reader.</p>
</section>
<section id="shapes">
<h1><a href="#shapes">Boolean shape operations</a></h1>
<p>We can apply the topics covered so far in this primer to effect boolean shape operations: getting the union, intersection, or exclusion, between two or more shapes that involve Bézier curves. For simplicity (well... sort of, more homogeneity), we'll be looking at poly-Bézier shapes only, but a shape that consists of a mix of lines and Bézier curves is technically a simplification. (Although it does mean we need to write a definition for the class of shapes that mix lines and Bézier curves. Since poly-Bézier curves are a superset, we'll be using those in the following examples.)</p>
<p>The procedure for performing boolean operations consists, broadly, of four steps:</p>
<ol>
<li>Find the intersection points between both shapes,</li>
<li>cut up the shapes into multiple sections between these intersections,</li>
<li>discard any section that isn't part of the desired operation's resultant shape, and</li>
<li>link up the remaining sections to form the new shape.</li>
</ol>
<p>Finding all intersections between two poly-Bézier curves, or any poly-line-section shape, is similar to the iterative algorithm discussed in the section on curve/curve intersection. For each segment in the poly-Bézier curve, we check whether its bounding box overlaps with any of the segment bounding boxes in the other poly-Bézier curve. If so, we run normal intersection detection.</p>
<p>After finding all intersection points, we split up our poly-Bézier curves, and make sure to record which of the newly formed poly-Bézier curves might potentially link up at the points we split the originals up at. This will let us quickly glue poly-Bézier curves back together after the next step.</p>
<p>Once we have all the new poly-Bézier curves, we run the first step of the desired boolean operation.</p>
<ul>
<li>Union: discard all poly-Bézier curves that lie "inside" our union of our shapes. E.g. if we want the union of two overlapping circles, the resulting shape is the outline.</li>
<li>Intersection: discard all poly-Bézier curves that lie "outside" the intersection of the two shapes. E.g. if we want the intersection of two overlapping circles, the resulting shape is the tapered ellipse where they overlap.</li>
<li>Exclusion: none of the sections are discarded, but we will need to link the shapes back up in a special way. Flip any section that would qualify for removal under UNION rules.</li>
</ul>
<table class="sketch"><tbody><tr>
<td class="labeled-image">
<img src="images/op_base.gif" height="169"/>
Two overlapping shapes.
</td>
<td class="labeled-image">
<img src="images/op_union.gif" height="169"/>
The unified region.
</td>
<td class="labeled-image">
<img src="images/op_intersection.gif" height="169"/>
Their intersection.
</td>
<td class="labeled-image">
<img src="images/op_exclusion.gif" height="169"/>
Their exclusion regions.
</td>
</tr></tbody></table>
<p>The main complication in the outlined procedure here is determining how sections qualify in terms of being "inside" and "outside" of our shapes. For this, we need to be able to perform point-in-shape detection, for which we'll use a classic algorithm: getting the "crossing number" by using ray casting, and then testing for "insidedness" by applying the <a href="http://folk.uio.no/bjornw/doc/bifrost-ref/bifrost-ref-12.html">even-odd rule</a>: For any point and any shape, we can cast a ray from our point, to some point that we know lies outside of the shape (such as a corner of our drawing surface). We then count how many times that line crosses our shape (remember that we can perform line/curve intersection detection quite easily). If the number of times it crosses the shape's outline is even, the point did not actually lie inside our shape. If the number of intersections is odd, our point did lie inside out shape. With that knowledge, we can decide whether to treat a section that such a point lies on "needs removal" (under union rules), "needs preserving" (under intersection rules), or "needs flipping" (under exclusion rules).</p>
<p>These operations are expensive, and implementing your own code for this is generally a bad idea if there is already a geometry package available for your language of choice. In this case, for JavaScript the most excellent <a href="http://paperjs.org">Paper.js</a> already comes with all the code in place to perform efficient boolean shape operations, so rather that implement an inferior version here, I can strongly recommend the Paper.js library if you intend to do any boolean shape work.</p>
<p>The following graphic shows Paper.js doing its thing for two shapes: one static, and one that is linked to your mouse pointer. If you move the mouse around, you'll see how the shape intersections are resolved. The base shapes are outlined in blue, and the boolean result is coloured red.</p>
<Graphic title="Boolean shape operations with Paper.js" paperjs={true} setup={this.setup} draw={this.draw} onMouseMove={this.onMouseMove}>
<button onclick="() => this.setMode(mode)">mode</button>
</Graphic>
</section> </section>
<section id="offsetting"> <section id="offsetting">
<h1><a href="#offsetting">Curve offsetting</a></h1> <h1><a href="#offsetting">Curve offsetting</a></h1>
@@ -2111,16 +2065,16 @@ for p = 1 to points.length-3 (inclusive):
<h3>"What do you mean, you can't? Prove it."</h3> <h3>"What do you mean, you can't? Prove it."</h3>
<p>First off, when I say "you can't," what I really mean is "you can't offset a Bézier curve with another Bézier curve", not even by using a really high order curve. You can find the function that describes the offset curve, but it won't be a polynomial, and as such it cannot be represented as a Bézier curve, which <strong>has</strong> to be a polynomial. Let's look at why this is:</p> <p>First off, when I say "you can't," what I really mean is "you can't offset a Bézier curve with another Bézier curve", not even by using a really high order curve. You can find the function that describes the offset curve, but it won't be a polynomial, and as such it cannot be represented as a Bézier curve, which <strong>has</strong> to be a polynomial. Let's look at why this is:</p>
<p>From a mathematical point of view, an offset curve <code>O(t)</code> is a curve such that, given our original curve <code>B(t)</code>, any point on <code>O(t)</code> is a fixed distance <code>d</code> away from coordinate <code>B(t)</code>. So let's math that:</p> <p>From a mathematical point of view, an offset curve <code>O(t)</code> is a curve such that, given our original curve <code>B(t)</code>, any point on <code>O(t)</code> is a fixed distance <code>d</code> away from coordinate <code>B(t)</code>. So let's math that:</p>
<img class="LaTeX SVG" src="./images/chapters/offsetting/1d4be24e5896dce3c16c8e71f9cc8881.svg" width="108px" height="16px" loading="lazy"> <img class="LaTeX SVG" src="./images/chapters/offsetting/1d4be24e5896dce3c16c8e71f9cc8881.svg" width="111px" height="17px" loading="lazy">
<p>However, we're working in 2D, and <code>d</code> is a single value, so we want to turn it into a vector. If we want a point distance <code>d</code> "away" from the curve <code>B(t)</code> then what we really mean is that we want a point at <code>d</code> times the "normal vector" from point <code>B(t)</code>, where the "normal" is a vector that runs perpendicular ("at a right angle") to the tangent at <code>B(t)</code>. Easy enough:</p> <p>However, we're working in 2D, and <code>d</code> is a single value, so we want to turn it into a vector. If we want a point distance <code>d</code> "away" from the curve <code>B(t)</code> then what we really mean is that we want a point at <code>d</code> times the "normal vector" from point <code>B(t)</code>, where the "normal" is a vector that runs perpendicular ("at a right angle") to the tangent at <code>B(t)</code>. Easy enough:</p>
<img class="LaTeX SVG" src="./images/chapters/offsetting/5bfee4f2ae27304475673d0596e42f9a.svg" width="151px" height="16px" loading="lazy"> <img class="LaTeX SVG" src="./images/chapters/offsetting/5bfee4f2ae27304475673d0596e42f9a.svg" width="153px" height="17px" loading="lazy">
<p>Now this still isn't very useful unless we know what the formula for <code>N(t)</code> is, so let's find out. <code>N(t)</code> runs perpendicular to the original curve tangent, and we know that the tangent is simply <code>B'(t)</code>, so we could just rotate that 90 degrees and be done with it. However, we need to ensure that <code>N(t)</code> has the same magnitude for every <code>t</code>, or the offset curve won't be at a uniform distance, thus not being an offset curve at all. The easiest way to guarantee this is to make sure <code>N(t)</code> always has length 1, which we can achieve by dividing <code>B'(t)</code> by its magnitude:</p> <p>Now this still isn't very useful unless we know what the formula for <code>N(t)</code> is, so let's find out. <code>N(t)</code> runs perpendicular to the original curve tangent, and we know that the tangent is simply <code>B'(t)</code>, so we could just rotate that 90 degrees and be done with it. However, we need to ensure that <code>N(t)</code> has the same magnitude for every <code>t</code>, or the offset curve won't be at a uniform distance, thus not being an offset curve at all. The easiest way to guarantee this is to make sure <code>N(t)</code> always has length 1, which we can achieve by dividing <code>B'(t)</code> by its magnitude:</p>
<img class="LaTeX SVG" src="./images/chapters/offsetting/fa6c243de2aa78b7451e0086848dfdfc.svg" width="120px" height="40px" loading="lazy"> <img class="LaTeX SVG" src="./images/chapters/offsetting/fa6c243de2aa78b7451e0086848dfdfc.svg" width="128px" height="40px" loading="lazy">
<p>Determining the length requires computing an arc length, and this is where things get Tricky with a capital T. First off, to compute arc length from some start <code>a</code> to end <code>b</code>, we must use the formula we saw earlier. Noting that "length" is usually denoted with double vertical bars:</p> <p>Determining the length requires computing an arc length, and this is where things get Tricky with a capital T. First off, to compute arc length from some start <code>a</code> to end <code>b</code>, we must use the formula we saw earlier. Noting that "length" is usually denoted with double vertical bars:</p>
<img class="LaTeX SVG" src="./images/chapters/offsetting/b262e50c085815421d94e120fc17f1c8.svg" width="169px" height="36px" loading="lazy"> <img class="LaTeX SVG" src="./images/chapters/offsetting/b262e50c085815421d94e120fc17f1c8.svg" width="189px" height="44px" loading="lazy">
<p>So if we want the length of the tangent, we plug in <code>B'(t)</code>, with <code>t = 0</code> as start and <p>So if we want the length of the tangent, we plug in <code>B'(t)</code>, with <code>t = 0</code> as start and
<code>t = 1</code> as end:</p> <code>t = 1</code> as end:</p>
<img class="LaTeX SVG" src="./images/chapters/offsetting/1d586b939b44ff9bdb42562a12ac2779.svg" width="209px" height="36px" loading="lazy"> <img class="LaTeX SVG" src="./images/chapters/offsetting/1d586b939b44ff9bdb42562a12ac2779.svg" width="229px" height="44px" loading="lazy">
<p>And that's where things go wrong. It doesn't even really matter what the second derivative for <code>B(t)</code> is, that square root is screwing everything up, because it turns our nice polynomials into things that are no longer polynomials.</p> <p>And that's where things go wrong. It doesn't even really matter what the second derivative for <code>B(t)</code> is, that square root is screwing everything up, because it turns our nice polynomials into things that are no longer polynomials.</p>
<p>There is a small class of polynomials where the square root is also a polynomial, but they're utterly useless to us: any polynomial with unweighted binomial coefficients has a square root that is also a polynomial. Now, you might think that Bézier curves are just fine because they do, but they don't; remember that only the <strong>base</strong> function has binomial coefficients. That's before we factor in our coordinates, which turn it into a non-binomial polygon. The only way to make sure the functions stay binomial is to make all our coordinates have the same value. And that's not a curve, that's a point. We can already create offset curves for points, we call them circles, and they have much simpler functions than Bézier curves.</p> <p>There is a small class of polynomials where the square root is also a polynomial, but they're utterly useless to us: any polynomial with unweighted binomial coefficients has a square root that is also a polynomial. Now, you might think that Bézier curves are just fine because they do, but they don't; remember that only the <strong>base</strong> function has binomial coefficients. That's before we factor in our coordinates, which turn it into a non-binomial polygon. The only way to make sure the functions stay binomial is to make all our coordinates have the same value. And that's not a curve, that's a point. We can already create offset curves for points, we call them circles, and they have much simpler functions than Bézier curves.</p>
<p>So, since the tangent length isn't a polynomial, the normalised tangent won't be a polynomial either, which means <code>N(t)</code> won't be a polynomial, which means that <code>d</code> times <code>N(t)</code> won't be a polynomial, which means that, ultimately, <code>O(t)</code> won't be a polynomial, which means that even if we can determine the function for <code>O(t)</code> just fine (and that's far from trivial!), it simply cannot be represented as a Bézier curve.</p> <p>So, since the tangent length isn't a polynomial, the normalised tangent won't be a polynomial either, which means <code>N(t)</code> won't be a polynomial, which means that <code>d</code> times <code>N(t)</code> won't be a polynomial, which means that, ultimately, <code>O(t)</code> won't be a polynomial, which means that even if we can determine the function for <code>O(t)</code> just fine (and that's far from trivial!), it simply cannot be represented as a Bézier curve.</p>
@@ -2129,9 +2083,24 @@ for p = 1 to points.length-3 (inclusive):
<p>So, you cannot offset a Bézier curve perfectly with another Bézier curve, no matter how high-order you make that other Bézier curve. However, we can chop up a curve into "safe" sub-curves (where "safe" means that all the control points are always on a single side of the baseline, and the midpoint of the curve at <code>t=0.5</code> is roughly in the center of the polygon defined by the curve coordinates) and then point-scale each sub-curve with respect to its scaling origin (which is the intersection of the point normals at the start and end points).</p> <p>So, you cannot offset a Bézier curve perfectly with another Bézier curve, no matter how high-order you make that other Bézier curve. However, we can chop up a curve into "safe" sub-curves (where "safe" means that all the control points are always on a single side of the baseline, and the midpoint of the curve at <code>t=0.5</code> is roughly in the center of the polygon defined by the curve coordinates) and then point-scale each sub-curve with respect to its scaling origin (which is the intersection of the point normals at the start and end points).</p>
<p>A good way to do this reduction is to first find the curve's extreme points, as explained in the earlier section on curve extremities, and use these as initial splitting points. After this initial split, we can check each individual segment to see if it's "safe enough" based on where the center of the curve is. If the on-curve point for <code>t=0.5</code> is too far off from the center, we simply split the segment down the middle. Generally this is more than enough to end up with safe segments.</p> <p>A good way to do this reduction is to first find the curve's extreme points, as explained in the earlier section on curve extremities, and use these as initial splitting points. After this initial split, we can check each individual segment to see if it's "safe enough" based on where the center of the curve is. If the on-curve point for <code>t=0.5</code> is too far off from the center, we simply split the segment down the middle. Generally this is more than enough to end up with safe segments.</p>
<p>The following graphics show off curve offsetting, and you can use your up and down arrow keys to control the distance at which the curve gets offset. The curve first gets reduced to safe segments, each of which is then offset at the desired distance. Especially for simple curves, particularly easily set up for quadratic curves, no reduction is necessary, but the more twisty the curve gets, the more the curve needs to be reduced in order to get segments that can safely be scaled.</p> <p>The following graphics show off curve offsetting, and you can use the slider to control the distance at which the curve gets offset. The curve first gets reduced to safe segments, each of which is then offset at the desired distance. Especially for simple curves, particularly easily set up for quadratic curves, no reduction is necessary, but the more twisty the curve gets, the more the curve needs to be reduced in order to get segments that can safely be scaled.</p>
<Graphic title="Offsetting a quadratic Bézier curve" setup={this.setupQuadratic} draw={this.draw} onKeyDown={this.props.onKeyDown} /> <graphics-element title="Offsetting a quadratic Bézier curve" width="275" height="275" src="./chapters/offsetting/offsetting.js" data-type="quadratic">
<Graphic title="Offsetting a cubic Bézier curve" setup={this.setupCubic} draw={this.draw} onKeyDown={this.props.onKeyDown} /> <fallback-image>
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
<img width="275px" height="275px" src="images\chapters\offsetting\91cd824148a388762d27e40fabb34e0d.png">
<label>Offsetting a quadratic Bézier curve</label>
</fallback-image>
<input type="range" min="5" max="50" step="1" value="20" class="slide-control">
</graphics-element>
<graphics-element title="Offsetting a cubic Bézier curve" width="275" height="275" src="./chapters/offsetting/offsetting.js" data-type="cubic">
<fallback-image>
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
<img width="275px" height="275px" src="images\chapters\offsetting\953485135c6d856ae1327fd47e55c920.png">
<label>Offsetting a cubic Bézier curve</label>
</fallback-image>
<input type="range" min="5" max="50" step="1" value="20" class="slide-control">
</graphics-element>
<p>You may notice that this may still lead to small 'jumps' in the sub-curves when moving the curve around. This is caused by the fact that we're still performing a naive form of offsetting, moving the control points the same distance as the start and end points. If the curve is large enough, this may still lead to incorrect offsets.</p> <p>You may notice that this may still lead to small 'jumps' in the sub-curves when moving the curve around. This is caused by the fact that we're still performing a naive form of offsetting, moving the control points the same distance as the start and end points. If the curve is large enough, this may still lead to incorrect offsets.</p>
@@ -2148,8 +2117,23 @@ for p = 1 to points.length-3 (inclusive):
<li>end: <code>map(&lt;strong&gt;S+length(subcurve)&lt;/strong&gt;, 0,L, s,e)</code></li> <li>end: <code>map(&lt;strong&gt;S+length(subcurve)&lt;/strong&gt;, 0,L, s,e)</code></li>
</ul> </ul>
<p>At each of the relevant points (start, end, and the projections of the control points onto the curve) we know the curve's normal, so offsetting is simply a matter of taking our original point, and moving it along the normal vector by the offset distance for each point. Doing so will give us the following result (these have with a starting width of 0, and an end width of 40 pixels, but can be controlled with your up and down arrow keys):</p> <p>At each of the relevant points (start, end, and the projections of the control points onto the curve) we know the curve's normal, so offsetting is simply a matter of taking our original point, and moving it along the normal vector by the offset distance for each point. Doing so will give us the following result (these have with a starting width of 0, and an end width of 40 pixels, but can be controlled with your up and down arrow keys):</p>
<Graphic title="Offsetting a quadratic Bézier curve" setup={this.setupQuadratic} draw={this.draw} onKeyDown={this.props.onKeyDown}/> <graphics-element title="Offsetting a quadratic Bézier curve" width="275" height="275" src="./chapters/graduatedoffset/offsetting.js" data-type="quadratic">
<Graphic title="Offsetting a cubic Bézier curve" setup={this.setupCubic} draw={this.draw} onKeyDown={this.props.onKeyDown}/> <fallback-image>
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
<img width="275px" height="275px" src="images\chapters\graduatedoffset\204745d093e479adaf1fc6fbba0d9d41.png">
<label>Offsetting a quadratic Bézier curve</label>
</fallback-image>
<input type="range" min="5" max="50" step="1" value="20" class="slide-control">
</graphics-element>
<graphics-element title="Offsetting a cubic Bézier curve" width="275" height="275" src="./chapters/graduatedoffset/offsetting.js" data-type="cubic">
<fallback-image>
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
<img width="275px" height="275px" src="images\chapters\graduatedoffset\99ff5ed1aec246be4490cc617c629138.png">
<label>Offsetting a cubic Bézier curve</label>
</fallback-image>
<input type="range" min="5" max="50" step="1" value="20" class="slide-control">
</graphics-element>
</section> </section>
<section id="circles"> <section id="circles">

View File

@@ -102,7 +102,6 @@
<li><a href="ja-JP/index.html#catmullconv">Bézier curves and Catmull-Rom curves</a></li> <li><a href="ja-JP/index.html#catmullconv">Bézier curves and Catmull-Rom curves</a></li>
<li><a href="ja-JP/index.html#catmullfitting">Creating a Catmull-Rom curve from three points</a></li> <li><a href="ja-JP/index.html#catmullfitting">Creating a Catmull-Rom curve from three points</a></li>
<li><a href="ja-JP/index.html#polybezier">Forming poly-Bézier curves</a></li> <li><a href="ja-JP/index.html#polybezier">Forming poly-Bézier curves</a></li>
<li><a href="ja-JP/index.html#shapes">Boolean shape operations</a></li>
<li><a href="ja-JP/index.html#offsetting">Curve offsetting</a></li> <li><a href="ja-JP/index.html#offsetting">Curve offsetting</a></li>
<li><a href="ja-JP/index.html#graduatedoffset">Graduated curve offsetting</a></li> <li><a href="ja-JP/index.html#graduatedoffset">Graduated curve offsetting</a></li>
<li><a href="ja-JP/index.html#circles">Circles and quadratic Bézier curves</a></li> <li><a href="ja-JP/index.html#circles">Circles and quadratic Bézier curves</a></li>
@@ -2051,51 +2050,6 @@ for p = 1 to points.length-3 (inclusive):
<p>Again, we see that cubic curves are now rather nice to work with, but quadratic curves have a new, very serious problem: we can move an on-curve point in such a way that we can't compute what needs to "happen next". Move the top point down, below the left and right points, for instance. There is no way to preserve correct control points without a kink at the bottom point. Quadratic curves: just not that good...</p> <p>Again, we see that cubic curves are now rather nice to work with, but quadratic curves have a new, very serious problem: we can move an on-curve point in such a way that we can't compute what needs to "happen next". Move the top point down, below the left and right points, for instance. There is no way to preserve correct control points without a kink at the bottom point. Quadratic curves: just not that good...</p>
<p>A final improvement is to offer fine-level control over which points behave which, so that you can have "kinks" or individually controlled segments when you need them, with nicely well-behaved curves for the rest of the path. Implementing that, is left as an exercise for the reader.</p> <p>A final improvement is to offer fine-level control over which points behave which, so that you can have "kinks" or individually controlled segments when you need them, with nicely well-behaved curves for the rest of the path. Implementing that, is left as an exercise for the reader.</p>
</section>
<section id="shapes">
<h1><a href="ja-JP/index.html#shapes">Boolean shape operations</a></h1>
<p>We can apply the topics covered so far in this primer to effect boolean shape operations: getting the union, intersection, or exclusion, between two or more shapes that involve Bézier curves. For simplicity (well... sort of, more homogeneity), we'll be looking at poly-Bézier shapes only, but a shape that consists of a mix of lines and Bézier curves is technically a simplification. (Although it does mean we need to write a definition for the class of shapes that mix lines and Bézier curves. Since poly-Bézier curves are a superset, we'll be using those in the following examples.)</p>
<p>The procedure for performing boolean operations consists, broadly, of four steps:</p>
<ol>
<li>Find the intersection points between both shapes,</li>
<li>cut up the shapes into multiple sections between these intersections,</li>
<li>discard any section that isn't part of the desired operation's resultant shape, and</li>
<li>link up the remaining sections to form the new shape.</li>
</ol>
<p>Finding all intersections between two poly-Bézier curves, or any poly-line-section shape, is similar to the iterative algorithm discussed in the section on curve/curve intersection. For each segment in the poly-Bézier curve, we check whether its bounding box overlaps with any of the segment bounding boxes in the other poly-Bézier curve. If so, we run normal intersection detection.</p>
<p>After finding all intersection points, we split up our poly-Bézier curves, and make sure to record which of the newly formed poly-Bézier curves might potentially link up at the points we split the originals up at. This will let us quickly glue poly-Bézier curves back together after the next step.</p>
<p>Once we have all the new poly-Bézier curves, we run the first step of the desired boolean operation.</p>
<ul>
<li>Union: discard all poly-Bézier curves that lie "inside" our union of our shapes. E.g. if we want the union of two overlapping circles, the resulting shape is the outline.</li>
<li>Intersection: discard all poly-Bézier curves that lie "outside" the intersection of the two shapes. E.g. if we want the intersection of two overlapping circles, the resulting shape is the tapered ellipse where they overlap.</li>
<li>Exclusion: none of the sections are discarded, but we will need to link the shapes back up in a special way. Flip any section that would qualify for removal under UNION rules.</li>
</ul>
<table class="sketch"><tbody><tr>
<td class="labeled-image">
<img src="images/op_base.gif" height="169"/>
Two overlapping shapes.
</td>
<td class="labeled-image">
<img src="images/op_union.gif" height="169"/>
The unified region.
</td>
<td class="labeled-image">
<img src="images/op_intersection.gif" height="169"/>
Their intersection.
</td>
<td class="labeled-image">
<img src="images/op_exclusion.gif" height="169"/>
Their exclusion regions.
</td>
</tr></tbody></table>
<p>The main complication in the outlined procedure here is determining how sections qualify in terms of being "inside" and "outside" of our shapes. For this, we need to be able to perform point-in-shape detection, for which we'll use a classic algorithm: getting the "crossing number" by using ray casting, and then testing for "insidedness" by applying the <a href="http://folk.uio.no/bjornw/doc/bifrost-ref/bifrost-ref-12.html">even-odd rule</a>: For any point and any shape, we can cast a ray from our point, to some point that we know lies outside of the shape (such as a corner of our drawing surface). We then count how many times that line crosses our shape (remember that we can perform line/curve intersection detection quite easily). If the number of times it crosses the shape's outline is even, the point did not actually lie inside our shape. If the number of intersections is odd, our point did lie inside out shape. With that knowledge, we can decide whether to treat a section that such a point lies on "needs removal" (under union rules), "needs preserving" (under intersection rules), or "needs flipping" (under exclusion rules).</p>
<p>These operations are expensive, and implementing your own code for this is generally a bad idea if there is already a geometry package available for your language of choice. In this case, for JavaScript the most excellent <a href="http://paperjs.org">Paper.js</a> already comes with all the code in place to perform efficient boolean shape operations, so rather that implement an inferior version here, I can strongly recommend the Paper.js library if you intend to do any boolean shape work.</p>
<p>The following graphic shows Paper.js doing its thing for two shapes: one static, and one that is linked to your mouse pointer. If you move the mouse around, you'll see how the shape intersections are resolved. The base shapes are outlined in blue, and the boolean result is coloured red.</p>
<Graphic title="Boolean shape operations with Paper.js" paperjs={true} setup={this.setup} draw={this.draw} onMouseMove={this.onMouseMove}>
<button onclick="() => this.setMode(mode)">mode</button>
</Graphic>
</section> </section>
<section id="offsetting"> <section id="offsetting">
<h1><a href="ja-JP/index.html#offsetting">Curve offsetting</a></h1> <h1><a href="ja-JP/index.html#offsetting">Curve offsetting</a></h1>
@@ -2107,16 +2061,16 @@ for p = 1 to points.length-3 (inclusive):
<h3>"What do you mean, you can't? Prove it."</h3> <h3>"What do you mean, you can't? Prove it."</h3>
<p>First off, when I say "you can't," what I really mean is "you can't offset a Bézier curve with another Bézier curve", not even by using a really high order curve. You can find the function that describes the offset curve, but it won't be a polynomial, and as such it cannot be represented as a Bézier curve, which <strong>has</strong> to be a polynomial. Let's look at why this is:</p> <p>First off, when I say "you can't," what I really mean is "you can't offset a Bézier curve with another Bézier curve", not even by using a really high order curve. You can find the function that describes the offset curve, but it won't be a polynomial, and as such it cannot be represented as a Bézier curve, which <strong>has</strong> to be a polynomial. Let's look at why this is:</p>
<p>From a mathematical point of view, an offset curve <code>O(t)</code> is a curve such that, given our original curve <code>B(t)</code>, any point on <code>O(t)</code> is a fixed distance <code>d</code> away from coordinate <code>B(t)</code>. So let's math that:</p> <p>From a mathematical point of view, an offset curve <code>O(t)</code> is a curve such that, given our original curve <code>B(t)</code>, any point on <code>O(t)</code> is a fixed distance <code>d</code> away from coordinate <code>B(t)</code>. So let's math that:</p>
<img class="LaTeX SVG" src="./images/chapters/offsetting/1d4be24e5896dce3c16c8e71f9cc8881.svg" width="108px" height="16px" loading="lazy"> <img class="LaTeX SVG" src="./images/chapters/offsetting/1d4be24e5896dce3c16c8e71f9cc8881.svg" width="111px" height="17px" loading="lazy">
<p>However, we're working in 2D, and <code>d</code> is a single value, so we want to turn it into a vector. If we want a point distance <code>d</code> "away" from the curve <code>B(t)</code> then what we really mean is that we want a point at <code>d</code> times the "normal vector" from point <code>B(t)</code>, where the "normal" is a vector that runs perpendicular ("at a right angle") to the tangent at <code>B(t)</code>. Easy enough:</p> <p>However, we're working in 2D, and <code>d</code> is a single value, so we want to turn it into a vector. If we want a point distance <code>d</code> "away" from the curve <code>B(t)</code> then what we really mean is that we want a point at <code>d</code> times the "normal vector" from point <code>B(t)</code>, where the "normal" is a vector that runs perpendicular ("at a right angle") to the tangent at <code>B(t)</code>. Easy enough:</p>
<img class="LaTeX SVG" src="./images/chapters/offsetting/5bfee4f2ae27304475673d0596e42f9a.svg" width="151px" height="16px" loading="lazy"> <img class="LaTeX SVG" src="./images/chapters/offsetting/5bfee4f2ae27304475673d0596e42f9a.svg" width="153px" height="17px" loading="lazy">
<p>Now this still isn't very useful unless we know what the formula for <code>N(t)</code> is, so let's find out. <code>N(t)</code> runs perpendicular to the original curve tangent, and we know that the tangent is simply <code>B'(t)</code>, so we could just rotate that 90 degrees and be done with it. However, we need to ensure that <code>N(t)</code> has the same magnitude for every <code>t</code>, or the offset curve won't be at a uniform distance, thus not being an offset curve at all. The easiest way to guarantee this is to make sure <code>N(t)</code> always has length 1, which we can achieve by dividing <code>B'(t)</code> by its magnitude:</p> <p>Now this still isn't very useful unless we know what the formula for <code>N(t)</code> is, so let's find out. <code>N(t)</code> runs perpendicular to the original curve tangent, and we know that the tangent is simply <code>B'(t)</code>, so we could just rotate that 90 degrees and be done with it. However, we need to ensure that <code>N(t)</code> has the same magnitude for every <code>t</code>, or the offset curve won't be at a uniform distance, thus not being an offset curve at all. The easiest way to guarantee this is to make sure <code>N(t)</code> always has length 1, which we can achieve by dividing <code>B'(t)</code> by its magnitude:</p>
<img class="LaTeX SVG" src="./images/chapters/offsetting/fa6c243de2aa78b7451e0086848dfdfc.svg" width="120px" height="40px" loading="lazy"> <img class="LaTeX SVG" src="./images/chapters/offsetting/fa6c243de2aa78b7451e0086848dfdfc.svg" width="128px" height="40px" loading="lazy">
<p>Determining the length requires computing an arc length, and this is where things get Tricky with a capital T. First off, to compute arc length from some start <code>a</code> to end <code>b</code>, we must use the formula we saw earlier. Noting that "length" is usually denoted with double vertical bars:</p> <p>Determining the length requires computing an arc length, and this is where things get Tricky with a capital T. First off, to compute arc length from some start <code>a</code> to end <code>b</code>, we must use the formula we saw earlier. Noting that "length" is usually denoted with double vertical bars:</p>
<img class="LaTeX SVG" src="./images/chapters/offsetting/b262e50c085815421d94e120fc17f1c8.svg" width="169px" height="36px" loading="lazy"> <img class="LaTeX SVG" src="./images/chapters/offsetting/b262e50c085815421d94e120fc17f1c8.svg" width="189px" height="44px" loading="lazy">
<p>So if we want the length of the tangent, we plug in <code>B'(t)</code>, with <code>t = 0</code> as start and <p>So if we want the length of the tangent, we plug in <code>B'(t)</code>, with <code>t = 0</code> as start and
<code>t = 1</code> as end:</p> <code>t = 1</code> as end:</p>
<img class="LaTeX SVG" src="./images/chapters/offsetting/1d586b939b44ff9bdb42562a12ac2779.svg" width="209px" height="36px" loading="lazy"> <img class="LaTeX SVG" src="./images/chapters/offsetting/1d586b939b44ff9bdb42562a12ac2779.svg" width="229px" height="44px" loading="lazy">
<p>And that's where things go wrong. It doesn't even really matter what the second derivative for <code>B(t)</code> is, that square root is screwing everything up, because it turns our nice polynomials into things that are no longer polynomials.</p> <p>And that's where things go wrong. It doesn't even really matter what the second derivative for <code>B(t)</code> is, that square root is screwing everything up, because it turns our nice polynomials into things that are no longer polynomials.</p>
<p>There is a small class of polynomials where the square root is also a polynomial, but they're utterly useless to us: any polynomial with unweighted binomial coefficients has a square root that is also a polynomial. Now, you might think that Bézier curves are just fine because they do, but they don't; remember that only the <strong>base</strong> function has binomial coefficients. That's before we factor in our coordinates, which turn it into a non-binomial polygon. The only way to make sure the functions stay binomial is to make all our coordinates have the same value. And that's not a curve, that's a point. We can already create offset curves for points, we call them circles, and they have much simpler functions than Bézier curves.</p> <p>There is a small class of polynomials where the square root is also a polynomial, but they're utterly useless to us: any polynomial with unweighted binomial coefficients has a square root that is also a polynomial. Now, you might think that Bézier curves are just fine because they do, but they don't; remember that only the <strong>base</strong> function has binomial coefficients. That's before we factor in our coordinates, which turn it into a non-binomial polygon. The only way to make sure the functions stay binomial is to make all our coordinates have the same value. And that's not a curve, that's a point. We can already create offset curves for points, we call them circles, and they have much simpler functions than Bézier curves.</p>
<p>So, since the tangent length isn't a polynomial, the normalised tangent won't be a polynomial either, which means <code>N(t)</code> won't be a polynomial, which means that <code>d</code> times <code>N(t)</code> won't be a polynomial, which means that, ultimately, <code>O(t)</code> won't be a polynomial, which means that even if we can determine the function for <code>O(t)</code> just fine (and that's far from trivial!), it simply cannot be represented as a Bézier curve.</p> <p>So, since the tangent length isn't a polynomial, the normalised tangent won't be a polynomial either, which means <code>N(t)</code> won't be a polynomial, which means that <code>d</code> times <code>N(t)</code> won't be a polynomial, which means that, ultimately, <code>O(t)</code> won't be a polynomial, which means that even if we can determine the function for <code>O(t)</code> just fine (and that's far from trivial!), it simply cannot be represented as a Bézier curve.</p>
@@ -2125,9 +2079,24 @@ for p = 1 to points.length-3 (inclusive):
<p>So, you cannot offset a Bézier curve perfectly with another Bézier curve, no matter how high-order you make that other Bézier curve. However, we can chop up a curve into "safe" sub-curves (where "safe" means that all the control points are always on a single side of the baseline, and the midpoint of the curve at <code>t=0.5</code> is roughly in the center of the polygon defined by the curve coordinates) and then point-scale each sub-curve with respect to its scaling origin (which is the intersection of the point normals at the start and end points).</p> <p>So, you cannot offset a Bézier curve perfectly with another Bézier curve, no matter how high-order you make that other Bézier curve. However, we can chop up a curve into "safe" sub-curves (where "safe" means that all the control points are always on a single side of the baseline, and the midpoint of the curve at <code>t=0.5</code> is roughly in the center of the polygon defined by the curve coordinates) and then point-scale each sub-curve with respect to its scaling origin (which is the intersection of the point normals at the start and end points).</p>
<p>A good way to do this reduction is to first find the curve's extreme points, as explained in the earlier section on curve extremities, and use these as initial splitting points. After this initial split, we can check each individual segment to see if it's "safe enough" based on where the center of the curve is. If the on-curve point for <code>t=0.5</code> is too far off from the center, we simply split the segment down the middle. Generally this is more than enough to end up with safe segments.</p> <p>A good way to do this reduction is to first find the curve's extreme points, as explained in the earlier section on curve extremities, and use these as initial splitting points. After this initial split, we can check each individual segment to see if it's "safe enough" based on where the center of the curve is. If the on-curve point for <code>t=0.5</code> is too far off from the center, we simply split the segment down the middle. Generally this is more than enough to end up with safe segments.</p>
<p>The following graphics show off curve offsetting, and you can use your up and down arrow keys to control the distance at which the curve gets offset. The curve first gets reduced to safe segments, each of which is then offset at the desired distance. Especially for simple curves, particularly easily set up for quadratic curves, no reduction is necessary, but the more twisty the curve gets, the more the curve needs to be reduced in order to get segments that can safely be scaled.</p> <p>The following graphics show off curve offsetting, and you can use the slider to control the distance at which the curve gets offset. The curve first gets reduced to safe segments, each of which is then offset at the desired distance. Especially for simple curves, particularly easily set up for quadratic curves, no reduction is necessary, but the more twisty the curve gets, the more the curve needs to be reduced in order to get segments that can safely be scaled.</p>
<Graphic title="Offsetting a quadratic Bézier curve" setup={this.setupQuadratic} draw={this.draw} onKeyDown={this.props.onKeyDown} /> <graphics-element title="Offsetting a quadratic Bézier curve" width="275" height="275" src="./chapters/offsetting/offsetting.js" data-type="quadratic">
<Graphic title="Offsetting a cubic Bézier curve" setup={this.setupCubic} draw={this.draw} onKeyDown={this.props.onKeyDown} /> <fallback-image>
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
<img width="275px" height="275px" src="images\chapters\offsetting\91cd824148a388762d27e40fabb34e0d.png">
<label>Offsetting a quadratic Bézier curve</label>
</fallback-image>
<input type="range" min="5" max="50" step="1" value="20" class="slide-control">
</graphics-element>
<graphics-element title="Offsetting a cubic Bézier curve" width="275" height="275" src="./chapters/offsetting/offsetting.js" data-type="cubic">
<fallback-image>
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
<img width="275px" height="275px" src="images\chapters\offsetting\953485135c6d856ae1327fd47e55c920.png">
<label>Offsetting a cubic Bézier curve</label>
</fallback-image>
<input type="range" min="5" max="50" step="1" value="20" class="slide-control">
</graphics-element>
<p>You may notice that this may still lead to small 'jumps' in the sub-curves when moving the curve around. This is caused by the fact that we're still performing a naive form of offsetting, moving the control points the same distance as the start and end points. If the curve is large enough, this may still lead to incorrect offsets.</p> <p>You may notice that this may still lead to small 'jumps' in the sub-curves when moving the curve around. This is caused by the fact that we're still performing a naive form of offsetting, moving the control points the same distance as the start and end points. If the curve is large enough, this may still lead to incorrect offsets.</p>
@@ -2144,8 +2113,23 @@ for p = 1 to points.length-3 (inclusive):
<li>end: <code>map(&lt;strong&gt;S+length(subcurve)&lt;/strong&gt;, 0,L, s,e)</code></li> <li>end: <code>map(&lt;strong&gt;S+length(subcurve)&lt;/strong&gt;, 0,L, s,e)</code></li>
</ul> </ul>
<p>At each of the relevant points (start, end, and the projections of the control points onto the curve) we know the curve's normal, so offsetting is simply a matter of taking our original point, and moving it along the normal vector by the offset distance for each point. Doing so will give us the following result (these have with a starting width of 0, and an end width of 40 pixels, but can be controlled with your up and down arrow keys):</p> <p>At each of the relevant points (start, end, and the projections of the control points onto the curve) we know the curve's normal, so offsetting is simply a matter of taking our original point, and moving it along the normal vector by the offset distance for each point. Doing so will give us the following result (these have with a starting width of 0, and an end width of 40 pixels, but can be controlled with your up and down arrow keys):</p>
<Graphic title="Offsetting a quadratic Bézier curve" setup={this.setupQuadratic} draw={this.draw} onKeyDown={this.props.onKeyDown}/> <graphics-element title="Offsetting a quadratic Bézier curve" width="275" height="275" src="./chapters/graduatedoffset/offsetting.js" data-type="quadratic">
<Graphic title="Offsetting a cubic Bézier curve" setup={this.setupCubic} draw={this.draw} onKeyDown={this.props.onKeyDown}/> <fallback-image>
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
<img width="275px" height="275px" src="images\chapters\graduatedoffset\204745d093e479adaf1fc6fbba0d9d41.png">
<label>Offsetting a quadratic Bézier curve</label>
</fallback-image>
<input type="range" min="5" max="50" step="1" value="20" class="slide-control">
</graphics-element>
<graphics-element title="Offsetting a cubic Bézier curve" width="275" height="275" src="./chapters/graduatedoffset/offsetting.js" data-type="cubic">
<fallback-image>
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
<img width="275px" height="275px" src="images\chapters\graduatedoffset\99ff5ed1aec246be4490cc617c629138.png">
<label>Offsetting a cubic Bézier curve</label>
</fallback-image>
<input type="range" min="5" max="50" step="1" value="20" class="slide-control">
</graphics-element>
</section> </section>
<section id="circles"> <section id="circles">

View File

@@ -88,6 +88,12 @@ class GraphicsAPI extends BaseAPI {
onMouseDown(evt) { onMouseDown(evt) {
super.onMouseDown(evt); super.onMouseDown(evt);
// mark this position as "when the cursor came down"
this.cursor.mark = { x: this.cursor.x, y: this.cursor.y };
// as well as for "what it was the previous cursor event"
this.cursor.last = { x: this.cursor.x, y: this.cursor.y };
const cdist = evt.targetTouches const cdist = evt.targetTouches
? TOUCH_PRECISION_ZONE ? TOUCH_PRECISION_ZONE
: MOUSE_PRECISION_ZONE; : MOUSE_PRECISION_ZONE;
@@ -97,8 +103,6 @@ class GraphicsAPI extends BaseAPI {
d = new Vector(p).dist(this.cursor); d = new Vector(p).dist(this.cursor);
if (d <= cdist) { if (d <= cdist) {
this.currentPoint = p; this.currentPoint = p;
this.currentPoint.mark = { x: p.x, y: p.y };
this.currentPoint.last = { x: p.x, y: p.y };
break; break;
} }
} }
@@ -106,27 +110,32 @@ class GraphicsAPI extends BaseAPI {
onMouseMove(evt) { onMouseMove(evt) {
super.onMouseMove(evt); super.onMouseMove(evt);
if (this.currentPoint) {
this.currentPoint.last = { if (this.cursor.down) {
x: this.currentPoint.x, // If we're click-dragging, or touch-moving, update the
y: this.currentPoint.y, // "since last event" as well as "compared to initial event"
}; // cursor positional differences:
this.currentPoint.x = this.cursor.x; this.cursor.diff = {
this.currentPoint.y = this.cursor.y; x: this.cursor.x - this.cursor.last.x,
this.currentPoint.diff = { y: this.cursor.y - this.cursor.last.y,
x: this.cursor.x - this.currentPoint.last.x,
y: this.cursor.y - this.currentPoint.last.y,
total: { total: {
x: this.cursor.x - this.currentPoint.mark.x, x: this.cursor.x - this.cursor.mark.x,
y: this.cursor.y - this.currentPoint.mark.y, y: this.cursor.y - this.cursor.mark.y,
}, },
}; };
this.cursor.last = { x: this.cursor.x, y: this.cursor.y };
}
// Are we dragging a movable point around?
if (this.currentPoint) {
this.currentPoint.x = this.cursor.x;
this.currentPoint.y = this.cursor.y;
} else { } else {
for (let i = 0, e = this.movable.length, p; i < e; i++) { for (let i = 0, e = this.movable.length, p; i < e; i++) {
p = this.movable[i]; p = this.movable[i];
if (new Vector(p).dist(this.cursor) <= 5) { if (new Vector(p).dist(this.cursor) <= 5) {
this.setCursor(this.HAND); this.setCursor(this.HAND);
return; // NOTE: this is a return, not a break. return; // NOTE: this is a return, not a break!
} }
} }
this.setCursor(this.POINTER); this.setCursor(this.POINTER);
@@ -135,9 +144,9 @@ class GraphicsAPI extends BaseAPI {
onMouseUp(evt) { onMouseUp(evt) {
super.onMouseUp(evt); super.onMouseUp(evt);
delete this.currentPoint.mark; delete this.cursor.mark;
delete this.currentPoint.last; delete this.cursor.last;
delete this.currentPoint.diff; delete this.cursor.diff;
this.currentPoint = false; this.currentPoint = false;
} }

View File

@@ -191,18 +191,22 @@ class GraphicsElement extends CustomElement {
const globalCode = split.quasiGlobal; const globalCode = split.quasiGlobal;
const classCode = performCodeSurgery(split.classCode); const classCode = performCodeSurgery(split.classCode);
this.setupCodeInjection(uid, globalCode, classCode, rerender); this.setupCodeInjection(src, uid, globalCode, classCode, rerender);
} }
/** /**
* Form the final, perfectly valid JS module code, and create the <script> * Form the final, perfectly valid JS module code, and create the <script>
* element for it, to be inserted into the shadow DOM during render(). * element for it, to be inserted into the shadow DOM during render().
*/ */
setupCodeInjection(uid, globalCode, classCode, rerender) { setupCodeInjection(src, uid, globalCode, classCode, rerender) {
const width = this.getAttribute(`width`, 200); const width = this.getAttribute(`width`, 200);
const height = this.getAttribute(`height`, 200); const height = this.getAttribute(`height`, 200);
this.code = ` this.code = `
/**
* Program source: ${src}
* Data attributes: ${JSON.stringify(this.dataset)}
*/
import { GraphicsAPI, Bezier, Vector, Matrix, Shape } from "${MODULE_PATH}/api/graphics-api.js"; import { GraphicsAPI, Bezier, Vector, Matrix, Shape } from "${MODULE_PATH}/api/graphics-api.js";
${globalCode} ${globalCode}
@@ -300,22 +304,6 @@ class GraphicsElement extends CustomElement {
CustomElement.register(GraphicsElement); 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);
}
// debugging should be behind a flag // debugging should be behind a flag
function debugLog(...data) { function debugLog(...data) {
if (GraphicsElement.DEBUG) { if (GraphicsElement.DEBUG) {

View File

@@ -700,9 +700,10 @@ class Bezier {
d2 = typeof d2 === "undefined" ? d1 : d2; d2 = typeof d2 === "undefined" ? d1 : d2;
const reduced = this.reduce(), const reduced = this.reduce(),
len = reduced.length, len = reduced.length,
fcurves = [], fcurves = [];
bcurves = [];
let p, let bcurves = [],
p,
alen = 0, alen = 0,
tlen = this.length(); tlen = this.length();

View File

@@ -146,3 +146,8 @@ p code {
display: block; display: block;
width: 100%; width: 100%;
} }
.grid {
display: flex;
flex-wrap: wrap;
}

View File

@@ -102,7 +102,6 @@
<li><a href="zh-CN/index.html#catmullconv">Bézier curves and Catmull-Rom curves</a></li> <li><a href="zh-CN/index.html#catmullconv">Bézier curves and Catmull-Rom curves</a></li>
<li><a href="zh-CN/index.html#catmullfitting">Creating a Catmull-Rom curve from three points</a></li> <li><a href="zh-CN/index.html#catmullfitting">Creating a Catmull-Rom curve from three points</a></li>
<li><a href="zh-CN/index.html#polybezier">Forming poly-Bézier curves</a></li> <li><a href="zh-CN/index.html#polybezier">Forming poly-Bézier curves</a></li>
<li><a href="zh-CN/index.html#shapes">Boolean shape operations</a></li>
<li><a href="zh-CN/index.html#offsetting">Curve offsetting</a></li> <li><a href="zh-CN/index.html#offsetting">Curve offsetting</a></li>
<li><a href="zh-CN/index.html#graduatedoffset">Graduated curve offsetting</a></li> <li><a href="zh-CN/index.html#graduatedoffset">Graduated curve offsetting</a></li>
<li><a href="zh-CN/index.html#circles">Circles and quadratic Bézier curves</a></li> <li><a href="zh-CN/index.html#circles">Circles and quadratic Bézier curves</a></li>
@@ -2045,51 +2044,6 @@ for p = 1 to points.length-3 (inclusive):
<p>Again, we see that cubic curves are now rather nice to work with, but quadratic curves have a new, very serious problem: we can move an on-curve point in such a way that we can't compute what needs to "happen next". Move the top point down, below the left and right points, for instance. There is no way to preserve correct control points without a kink at the bottom point. Quadratic curves: just not that good...</p> <p>Again, we see that cubic curves are now rather nice to work with, but quadratic curves have a new, very serious problem: we can move an on-curve point in such a way that we can't compute what needs to "happen next". Move the top point down, below the left and right points, for instance. There is no way to preserve correct control points without a kink at the bottom point. Quadratic curves: just not that good...</p>
<p>A final improvement is to offer fine-level control over which points behave which, so that you can have "kinks" or individually controlled segments when you need them, with nicely well-behaved curves for the rest of the path. Implementing that, is left as an exercise for the reader.</p> <p>A final improvement is to offer fine-level control over which points behave which, so that you can have "kinks" or individually controlled segments when you need them, with nicely well-behaved curves for the rest of the path. Implementing that, is left as an exercise for the reader.</p>
</section>
<section id="shapes">
<h1><a href="zh-CN/index.html#shapes">Boolean shape operations</a></h1>
<p>We can apply the topics covered so far in this primer to effect boolean shape operations: getting the union, intersection, or exclusion, between two or more shapes that involve Bézier curves. For simplicity (well... sort of, more homogeneity), we'll be looking at poly-Bézier shapes only, but a shape that consists of a mix of lines and Bézier curves is technically a simplification. (Although it does mean we need to write a definition for the class of shapes that mix lines and Bézier curves. Since poly-Bézier curves are a superset, we'll be using those in the following examples.)</p>
<p>The procedure for performing boolean operations consists, broadly, of four steps:</p>
<ol>
<li>Find the intersection points between both shapes,</li>
<li>cut up the shapes into multiple sections between these intersections,</li>
<li>discard any section that isn't part of the desired operation's resultant shape, and</li>
<li>link up the remaining sections to form the new shape.</li>
</ol>
<p>Finding all intersections between two poly-Bézier curves, or any poly-line-section shape, is similar to the iterative algorithm discussed in the section on curve/curve intersection. For each segment in the poly-Bézier curve, we check whether its bounding box overlaps with any of the segment bounding boxes in the other poly-Bézier curve. If so, we run normal intersection detection.</p>
<p>After finding all intersection points, we split up our poly-Bézier curves, and make sure to record which of the newly formed poly-Bézier curves might potentially link up at the points we split the originals up at. This will let us quickly glue poly-Bézier curves back together after the next step.</p>
<p>Once we have all the new poly-Bézier curves, we run the first step of the desired boolean operation.</p>
<ul>
<li>Union: discard all poly-Bézier curves that lie "inside" our union of our shapes. E.g. if we want the union of two overlapping circles, the resulting shape is the outline.</li>
<li>Intersection: discard all poly-Bézier curves that lie "outside" the intersection of the two shapes. E.g. if we want the intersection of two overlapping circles, the resulting shape is the tapered ellipse where they overlap.</li>
<li>Exclusion: none of the sections are discarded, but we will need to link the shapes back up in a special way. Flip any section that would qualify for removal under UNION rules.</li>
</ul>
<table class="sketch"><tbody><tr>
<td class="labeled-image">
<img src="images/op_base.gif" height="169"/>
Two overlapping shapes.
</td>
<td class="labeled-image">
<img src="images/op_union.gif" height="169"/>
The unified region.
</td>
<td class="labeled-image">
<img src="images/op_intersection.gif" height="169"/>
Their intersection.
</td>
<td class="labeled-image">
<img src="images/op_exclusion.gif" height="169"/>
Their exclusion regions.
</td>
</tr></tbody></table>
<p>The main complication in the outlined procedure here is determining how sections qualify in terms of being "inside" and "outside" of our shapes. For this, we need to be able to perform point-in-shape detection, for which we'll use a classic algorithm: getting the "crossing number" by using ray casting, and then testing for "insidedness" by applying the <a href="http://folk.uio.no/bjornw/doc/bifrost-ref/bifrost-ref-12.html">even-odd rule</a>: For any point and any shape, we can cast a ray from our point, to some point that we know lies outside of the shape (such as a corner of our drawing surface). We then count how many times that line crosses our shape (remember that we can perform line/curve intersection detection quite easily). If the number of times it crosses the shape's outline is even, the point did not actually lie inside our shape. If the number of intersections is odd, our point did lie inside out shape. With that knowledge, we can decide whether to treat a section that such a point lies on "needs removal" (under union rules), "needs preserving" (under intersection rules), or "needs flipping" (under exclusion rules).</p>
<p>These operations are expensive, and implementing your own code for this is generally a bad idea if there is already a geometry package available for your language of choice. In this case, for JavaScript the most excellent <a href="http://paperjs.org">Paper.js</a> already comes with all the code in place to perform efficient boolean shape operations, so rather that implement an inferior version here, I can strongly recommend the Paper.js library if you intend to do any boolean shape work.</p>
<p>The following graphic shows Paper.js doing its thing for two shapes: one static, and one that is linked to your mouse pointer. If you move the mouse around, you'll see how the shape intersections are resolved. The base shapes are outlined in blue, and the boolean result is coloured red.</p>
<Graphic title="Boolean shape operations with Paper.js" paperjs={true} setup={this.setup} draw={this.draw} onMouseMove={this.onMouseMove}>
<button onclick="() => this.setMode(mode)">mode</button>
</Graphic>
</section> </section>
<section id="offsetting"> <section id="offsetting">
<h1><a href="zh-CN/index.html#offsetting">Curve offsetting</a></h1> <h1><a href="zh-CN/index.html#offsetting">Curve offsetting</a></h1>
@@ -2101,16 +2055,16 @@ for p = 1 to points.length-3 (inclusive):
<h3>"What do you mean, you can't? Prove it."</h3> <h3>"What do you mean, you can't? Prove it."</h3>
<p>First off, when I say "you can't," what I really mean is "you can't offset a Bézier curve with another Bézier curve", not even by using a really high order curve. You can find the function that describes the offset curve, but it won't be a polynomial, and as such it cannot be represented as a Bézier curve, which <strong>has</strong> to be a polynomial. Let's look at why this is:</p> <p>First off, when I say "you can't," what I really mean is "you can't offset a Bézier curve with another Bézier curve", not even by using a really high order curve. You can find the function that describes the offset curve, but it won't be a polynomial, and as such it cannot be represented as a Bézier curve, which <strong>has</strong> to be a polynomial. Let's look at why this is:</p>
<p>From a mathematical point of view, an offset curve <code>O(t)</code> is a curve such that, given our original curve <code>B(t)</code>, any point on <code>O(t)</code> is a fixed distance <code>d</code> away from coordinate <code>B(t)</code>. So let's math that:</p> <p>From a mathematical point of view, an offset curve <code>O(t)</code> is a curve such that, given our original curve <code>B(t)</code>, any point on <code>O(t)</code> is a fixed distance <code>d</code> away from coordinate <code>B(t)</code>. So let's math that:</p>
<img class="LaTeX SVG" src="./images/chapters/offsetting/1d4be24e5896dce3c16c8e71f9cc8881.svg" width="108px" height="16px" loading="lazy"> <img class="LaTeX SVG" src="./images/chapters/offsetting/1d4be24e5896dce3c16c8e71f9cc8881.svg" width="111px" height="17px" loading="lazy">
<p>However, we're working in 2D, and <code>d</code> is a single value, so we want to turn it into a vector. If we want a point distance <code>d</code> "away" from the curve <code>B(t)</code> then what we really mean is that we want a point at <code>d</code> times the "normal vector" from point <code>B(t)</code>, where the "normal" is a vector that runs perpendicular ("at a right angle") to the tangent at <code>B(t)</code>. Easy enough:</p> <p>However, we're working in 2D, and <code>d</code> is a single value, so we want to turn it into a vector. If we want a point distance <code>d</code> "away" from the curve <code>B(t)</code> then what we really mean is that we want a point at <code>d</code> times the "normal vector" from point <code>B(t)</code>, where the "normal" is a vector that runs perpendicular ("at a right angle") to the tangent at <code>B(t)</code>. Easy enough:</p>
<img class="LaTeX SVG" src="./images/chapters/offsetting/5bfee4f2ae27304475673d0596e42f9a.svg" width="151px" height="16px" loading="lazy"> <img class="LaTeX SVG" src="./images/chapters/offsetting/5bfee4f2ae27304475673d0596e42f9a.svg" width="153px" height="17px" loading="lazy">
<p>Now this still isn't very useful unless we know what the formula for <code>N(t)</code> is, so let's find out. <code>N(t)</code> runs perpendicular to the original curve tangent, and we know that the tangent is simply <code>B'(t)</code>, so we could just rotate that 90 degrees and be done with it. However, we need to ensure that <code>N(t)</code> has the same magnitude for every <code>t</code>, or the offset curve won't be at a uniform distance, thus not being an offset curve at all. The easiest way to guarantee this is to make sure <code>N(t)</code> always has length 1, which we can achieve by dividing <code>B'(t)</code> by its magnitude:</p> <p>Now this still isn't very useful unless we know what the formula for <code>N(t)</code> is, so let's find out. <code>N(t)</code> runs perpendicular to the original curve tangent, and we know that the tangent is simply <code>B'(t)</code>, so we could just rotate that 90 degrees and be done with it. However, we need to ensure that <code>N(t)</code> has the same magnitude for every <code>t</code>, or the offset curve won't be at a uniform distance, thus not being an offset curve at all. The easiest way to guarantee this is to make sure <code>N(t)</code> always has length 1, which we can achieve by dividing <code>B'(t)</code> by its magnitude:</p>
<img class="LaTeX SVG" src="./images/chapters/offsetting/fa6c243de2aa78b7451e0086848dfdfc.svg" width="120px" height="40px" loading="lazy"> <img class="LaTeX SVG" src="./images/chapters/offsetting/fa6c243de2aa78b7451e0086848dfdfc.svg" width="128px" height="40px" loading="lazy">
<p>Determining the length requires computing an arc length, and this is where things get Tricky with a capital T. First off, to compute arc length from some start <code>a</code> to end <code>b</code>, we must use the formula we saw earlier. Noting that "length" is usually denoted with double vertical bars:</p> <p>Determining the length requires computing an arc length, and this is where things get Tricky with a capital T. First off, to compute arc length from some start <code>a</code> to end <code>b</code>, we must use the formula we saw earlier. Noting that "length" is usually denoted with double vertical bars:</p>
<img class="LaTeX SVG" src="./images/chapters/offsetting/b262e50c085815421d94e120fc17f1c8.svg" width="169px" height="36px" loading="lazy"> <img class="LaTeX SVG" src="./images/chapters/offsetting/b262e50c085815421d94e120fc17f1c8.svg" width="189px" height="44px" loading="lazy">
<p>So if we want the length of the tangent, we plug in <code>B'(t)</code>, with <code>t = 0</code> as start and <p>So if we want the length of the tangent, we plug in <code>B'(t)</code>, with <code>t = 0</code> as start and
<code>t = 1</code> as end:</p> <code>t = 1</code> as end:</p>
<img class="LaTeX SVG" src="./images/chapters/offsetting/1d586b939b44ff9bdb42562a12ac2779.svg" width="209px" height="36px" loading="lazy"> <img class="LaTeX SVG" src="./images/chapters/offsetting/1d586b939b44ff9bdb42562a12ac2779.svg" width="229px" height="44px" loading="lazy">
<p>And that's where things go wrong. It doesn't even really matter what the second derivative for <code>B(t)</code> is, that square root is screwing everything up, because it turns our nice polynomials into things that are no longer polynomials.</p> <p>And that's where things go wrong. It doesn't even really matter what the second derivative for <code>B(t)</code> is, that square root is screwing everything up, because it turns our nice polynomials into things that are no longer polynomials.</p>
<p>There is a small class of polynomials where the square root is also a polynomial, but they're utterly useless to us: any polynomial with unweighted binomial coefficients has a square root that is also a polynomial. Now, you might think that Bézier curves are just fine because they do, but they don't; remember that only the <strong>base</strong> function has binomial coefficients. That's before we factor in our coordinates, which turn it into a non-binomial polygon. The only way to make sure the functions stay binomial is to make all our coordinates have the same value. And that's not a curve, that's a point. We can already create offset curves for points, we call them circles, and they have much simpler functions than Bézier curves.</p> <p>There is a small class of polynomials where the square root is also a polynomial, but they're utterly useless to us: any polynomial with unweighted binomial coefficients has a square root that is also a polynomial. Now, you might think that Bézier curves are just fine because they do, but they don't; remember that only the <strong>base</strong> function has binomial coefficients. That's before we factor in our coordinates, which turn it into a non-binomial polygon. The only way to make sure the functions stay binomial is to make all our coordinates have the same value. And that's not a curve, that's a point. We can already create offset curves for points, we call them circles, and they have much simpler functions than Bézier curves.</p>
<p>So, since the tangent length isn't a polynomial, the normalised tangent won't be a polynomial either, which means <code>N(t)</code> won't be a polynomial, which means that <code>d</code> times <code>N(t)</code> won't be a polynomial, which means that, ultimately, <code>O(t)</code> won't be a polynomial, which means that even if we can determine the function for <code>O(t)</code> just fine (and that's far from trivial!), it simply cannot be represented as a Bézier curve.</p> <p>So, since the tangent length isn't a polynomial, the normalised tangent won't be a polynomial either, which means <code>N(t)</code> won't be a polynomial, which means that <code>d</code> times <code>N(t)</code> won't be a polynomial, which means that, ultimately, <code>O(t)</code> won't be a polynomial, which means that even if we can determine the function for <code>O(t)</code> just fine (and that's far from trivial!), it simply cannot be represented as a Bézier curve.</p>
@@ -2119,9 +2073,24 @@ for p = 1 to points.length-3 (inclusive):
<p>So, you cannot offset a Bézier curve perfectly with another Bézier curve, no matter how high-order you make that other Bézier curve. However, we can chop up a curve into "safe" sub-curves (where "safe" means that all the control points are always on a single side of the baseline, and the midpoint of the curve at <code>t=0.5</code> is roughly in the center of the polygon defined by the curve coordinates) and then point-scale each sub-curve with respect to its scaling origin (which is the intersection of the point normals at the start and end points).</p> <p>So, you cannot offset a Bézier curve perfectly with another Bézier curve, no matter how high-order you make that other Bézier curve. However, we can chop up a curve into "safe" sub-curves (where "safe" means that all the control points are always on a single side of the baseline, and the midpoint of the curve at <code>t=0.5</code> is roughly in the center of the polygon defined by the curve coordinates) and then point-scale each sub-curve with respect to its scaling origin (which is the intersection of the point normals at the start and end points).</p>
<p>A good way to do this reduction is to first find the curve's extreme points, as explained in the earlier section on curve extremities, and use these as initial splitting points. After this initial split, we can check each individual segment to see if it's "safe enough" based on where the center of the curve is. If the on-curve point for <code>t=0.5</code> is too far off from the center, we simply split the segment down the middle. Generally this is more than enough to end up with safe segments.</p> <p>A good way to do this reduction is to first find the curve's extreme points, as explained in the earlier section on curve extremities, and use these as initial splitting points. After this initial split, we can check each individual segment to see if it's "safe enough" based on where the center of the curve is. If the on-curve point for <code>t=0.5</code> is too far off from the center, we simply split the segment down the middle. Generally this is more than enough to end up with safe segments.</p>
<p>The following graphics show off curve offsetting, and you can use your up and down arrow keys to control the distance at which the curve gets offset. The curve first gets reduced to safe segments, each of which is then offset at the desired distance. Especially for simple curves, particularly easily set up for quadratic curves, no reduction is necessary, but the more twisty the curve gets, the more the curve needs to be reduced in order to get segments that can safely be scaled.</p> <p>The following graphics show off curve offsetting, and you can use the slider to control the distance at which the curve gets offset. The curve first gets reduced to safe segments, each of which is then offset at the desired distance. Especially for simple curves, particularly easily set up for quadratic curves, no reduction is necessary, but the more twisty the curve gets, the more the curve needs to be reduced in order to get segments that can safely be scaled.</p>
<Graphic title="Offsetting a quadratic Bézier curve" setup={this.setupQuadratic} draw={this.draw} onKeyDown={this.props.onKeyDown} /> <graphics-element title="Offsetting a quadratic Bézier curve" width="275" height="275" src="./chapters/offsetting/offsetting.js" data-type="quadratic">
<Graphic title="Offsetting a cubic Bézier curve" setup={this.setupCubic} draw={this.draw} onKeyDown={this.props.onKeyDown} /> <fallback-image>
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
<img width="275px" height="275px" src="images\chapters\offsetting\91cd824148a388762d27e40fabb34e0d.png">
<label>Offsetting a quadratic Bézier curve</label>
</fallback-image>
<input type="range" min="5" max="50" step="1" value="20" class="slide-control">
</graphics-element>
<graphics-element title="Offsetting a cubic Bézier curve" width="275" height="275" src="./chapters/offsetting/offsetting.js" data-type="cubic">
<fallback-image>
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
<img width="275px" height="275px" src="images\chapters\offsetting\953485135c6d856ae1327fd47e55c920.png">
<label>Offsetting a cubic Bézier curve</label>
</fallback-image>
<input type="range" min="5" max="50" step="1" value="20" class="slide-control">
</graphics-element>
<p>You may notice that this may still lead to small 'jumps' in the sub-curves when moving the curve around. This is caused by the fact that we're still performing a naive form of offsetting, moving the control points the same distance as the start and end points. If the curve is large enough, this may still lead to incorrect offsets.</p> <p>You may notice that this may still lead to small 'jumps' in the sub-curves when moving the curve around. This is caused by the fact that we're still performing a naive form of offsetting, moving the control points the same distance as the start and end points. If the curve is large enough, this may still lead to incorrect offsets.</p>
@@ -2138,8 +2107,23 @@ for p = 1 to points.length-3 (inclusive):
<li>end: <code>map(&lt;strong&gt;S+length(subcurve)&lt;/strong&gt;, 0,L, s,e)</code></li> <li>end: <code>map(&lt;strong&gt;S+length(subcurve)&lt;/strong&gt;, 0,L, s,e)</code></li>
</ul> </ul>
<p>At each of the relevant points (start, end, and the projections of the control points onto the curve) we know the curve's normal, so offsetting is simply a matter of taking our original point, and moving it along the normal vector by the offset distance for each point. Doing so will give us the following result (these have with a starting width of 0, and an end width of 40 pixels, but can be controlled with your up and down arrow keys):</p> <p>At each of the relevant points (start, end, and the projections of the control points onto the curve) we know the curve's normal, so offsetting is simply a matter of taking our original point, and moving it along the normal vector by the offset distance for each point. Doing so will give us the following result (these have with a starting width of 0, and an end width of 40 pixels, but can be controlled with your up and down arrow keys):</p>
<Graphic title="Offsetting a quadratic Bézier curve" setup={this.setupQuadratic} draw={this.draw} onKeyDown={this.props.onKeyDown}/> <graphics-element title="Offsetting a quadratic Bézier curve" width="275" height="275" src="./chapters/graduatedoffset/offsetting.js" data-type="quadratic">
<Graphic title="Offsetting a cubic Bézier curve" setup={this.setupCubic} draw={this.draw} onKeyDown={this.props.onKeyDown}/> <fallback-image>
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
<img width="275px" height="275px" src="images\chapters\graduatedoffset\204745d093e479adaf1fc6fbba0d9d41.png">
<label>Offsetting a quadratic Bézier curve</label>
</fallback-image>
<input type="range" min="5" max="50" step="1" value="20" class="slide-control">
</graphics-element>
<graphics-element title="Offsetting a cubic Bézier curve" width="275" height="275" src="./chapters/graduatedoffset/offsetting.js" data-type="cubic">
<fallback-image>
<span class="view-source">Scripts are disabled. Showing fallback image.</span>
<img width="275px" height="275px" src="images\chapters\graduatedoffset\99ff5ed1aec246be4490cc617c629138.png">
<label>Offsetting a cubic Bézier curve</label>
</fallback-image>
<input type="range" min="5" max="50" step="1" value="20" class="slide-control">
</graphics-element>
</section> </section>
<section id="circles"> <section id="circles">

106
package-lock.json generated
View File

@@ -57,8 +57,7 @@
"@types/q": { "@types/q": {
"version": "1.5.4", "version": "1.5.4",
"resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.4.tgz", "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.4.tgz",
"integrity": "sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug==", "integrity": "sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug=="
"dev": true
}, },
"a-sync-waterfall": { "a-sync-waterfall": {
"version": "1.0.1", "version": "1.0.1",
@@ -82,7 +81,6 @@
"version": "3.2.1", "version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"dev": true,
"requires": { "requires": {
"color-convert": "^1.9.0" "color-convert": "^1.9.0"
} }
@@ -117,7 +115,6 @@
"version": "1.0.10", "version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"dev": true,
"requires": { "requires": {
"sprintf-js": "~1.0.2" "sprintf-js": "~1.0.2"
} }
@@ -170,8 +167,7 @@
"boolbase": { "boolbase": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
"integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=", "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24="
"dev": true
}, },
"brace-expansion": { "brace-expansion": {
"version": "1.1.11", "version": "1.1.11",
@@ -223,7 +219,6 @@
"version": "2.4.2", "version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
"dev": true,
"requires": { "requires": {
"ansi-styles": "^3.2.1", "ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5", "escape-string-regexp": "^1.0.5",
@@ -313,7 +308,6 @@
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/coa/-/coa-2.0.2.tgz", "resolved": "https://registry.npmjs.org/coa/-/coa-2.0.2.tgz",
"integrity": "sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA==", "integrity": "sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA==",
"dev": true,
"requires": { "requires": {
"@types/q": "^1.5.1", "@types/q": "^1.5.1",
"chalk": "^2.4.1", "chalk": "^2.4.1",
@@ -330,7 +324,6 @@
"version": "1.9.3", "version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"dev": true,
"requires": { "requires": {
"color-name": "1.1.3" "color-name": "1.1.3"
} }
@@ -338,8 +331,7 @@
"color-name": { "color-name": {
"version": "1.1.3", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
"dev": true
}, },
"colors": { "colors": {
"version": "1.4.0", "version": "1.4.0",
@@ -394,7 +386,6 @@
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-2.1.0.tgz", "resolved": "https://registry.npmjs.org/css-select/-/css-select-2.1.0.tgz",
"integrity": "sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ==", "integrity": "sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ==",
"dev": true,
"requires": { "requires": {
"boolbase": "^1.0.0", "boolbase": "^1.0.0",
"css-what": "^3.2.1", "css-what": "^3.2.1",
@@ -405,14 +396,12 @@
"css-select-base-adapter": { "css-select-base-adapter": {
"version": "0.1.1", "version": "0.1.1",
"resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz", "resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz",
"integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==", "integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w=="
"dev": true
}, },
"css-tree": { "css-tree": {
"version": "1.0.0-alpha.37", "version": "1.0.0-alpha.37",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz",
"integrity": "sha512-DMxWJg0rnz7UgxKT0Q1HU/L9BeJI0M6ksor0OgqOnF+aRCDWg/N2641HmVyU9KVIu0OVVWOb2IpC9A+BJRnejg==", "integrity": "sha512-DMxWJg0rnz7UgxKT0Q1HU/L9BeJI0M6ksor0OgqOnF+aRCDWg/N2641HmVyU9KVIu0OVVWOb2IpC9A+BJRnejg==",
"dev": true,
"requires": { "requires": {
"mdn-data": "2.0.4", "mdn-data": "2.0.4",
"source-map": "^0.6.1" "source-map": "^0.6.1"
@@ -421,14 +410,12 @@
"css-what": { "css-what": {
"version": "3.3.0", "version": "3.3.0",
"resolved": "https://registry.npmjs.org/css-what/-/css-what-3.3.0.tgz", "resolved": "https://registry.npmjs.org/css-what/-/css-what-3.3.0.tgz",
"integrity": "sha512-pv9JPyatiPaQ6pf4OvD/dbfm0o5LviWmwxNWzblYf/1u9QZd0ihV+PMwy5jdQWQ3349kZmKEx9WXuSka2dM4cg==", "integrity": "sha512-pv9JPyatiPaQ6pf4OvD/dbfm0o5LviWmwxNWzblYf/1u9QZd0ihV+PMwy5jdQWQ3349kZmKEx9WXuSka2dM4cg=="
"dev": true
}, },
"csso": { "csso": {
"version": "4.0.3", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/csso/-/csso-4.0.3.tgz", "resolved": "https://registry.npmjs.org/csso/-/csso-4.0.3.tgz",
"integrity": "sha512-NL3spysxUkcrOgnpsT4Xdl2aiEiBG6bXswAABQVHcMrfjjBisFOKwLDOmf4wf32aPdcJws1zds2B0Rg+jqMyHQ==", "integrity": "sha512-NL3spysxUkcrOgnpsT4Xdl2aiEiBG6bXswAABQVHcMrfjjBisFOKwLDOmf4wf32aPdcJws1zds2B0Rg+jqMyHQ==",
"dev": true,
"requires": { "requires": {
"css-tree": "1.0.0-alpha.39" "css-tree": "1.0.0-alpha.39"
}, },
@@ -437,7 +424,6 @@
"version": "1.0.0-alpha.39", "version": "1.0.0-alpha.39",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.39.tgz", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.39.tgz",
"integrity": "sha512-7UvkEYgBAHRG9Nt980lYxjsTrCyHFN53ky3wVsDkiMdVqylqRt+Zc+jm5qw7/qyOvN2dHSYtX0e4MbCCExSvnA==", "integrity": "sha512-7UvkEYgBAHRG9Nt980lYxjsTrCyHFN53ky3wVsDkiMdVqylqRt+Zc+jm5qw7/qyOvN2dHSYtX0e4MbCCExSvnA==",
"dev": true,
"requires": { "requires": {
"mdn-data": "2.0.6", "mdn-data": "2.0.6",
"source-map": "^0.6.1" "source-map": "^0.6.1"
@@ -446,8 +432,7 @@
"mdn-data": { "mdn-data": {
"version": "2.0.6", "version": "2.0.6",
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.6.tgz", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.6.tgz",
"integrity": "sha512-rQvjv71olwNHgiTbfPZFkJtjNMciWgswYeciZhtvWLO8bmX3TnhyA62I6sTWOyZssWHJJjY6/KiWwqQsWWsqOA==", "integrity": "sha512-rQvjv71olwNHgiTbfPZFkJtjNMciWgswYeciZhtvWLO8bmX3TnhyA62I6sTWOyZssWHJJjY6/KiWwqQsWWsqOA=="
"dev": true
} }
} }
}, },
@@ -503,7 +488,6 @@
"version": "1.1.3", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
"integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==",
"dev": true,
"requires": { "requires": {
"object-keys": "^1.0.12" "object-keys": "^1.0.12"
} }
@@ -524,7 +508,6 @@
"version": "0.2.2", "version": "0.2.2",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz",
"integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==", "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==",
"dev": true,
"requires": { "requires": {
"domelementtype": "^2.0.1", "domelementtype": "^2.0.1",
"entities": "^2.0.0" "entities": "^2.0.0"
@@ -533,22 +516,19 @@
"domelementtype": { "domelementtype": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.0.1.tgz", "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.0.1.tgz",
"integrity": "sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ==", "integrity": "sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ=="
"dev": true
} }
} }
}, },
"domelementtype": { "domelementtype": {
"version": "1.3.1", "version": "1.3.1",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz",
"integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==", "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w=="
"dev": true
}, },
"domutils": { "domutils": {
"version": "1.7.0", "version": "1.7.0",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz",
"integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==",
"dev": true,
"requires": { "requires": {
"dom-serializer": "0", "dom-serializer": "0",
"domelementtype": "1" "domelementtype": "1"
@@ -575,8 +555,7 @@
"entities": { "entities": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/entities/-/entities-2.0.3.tgz", "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.3.tgz",
"integrity": "sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ==", "integrity": "sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ=="
"dev": true
}, },
"error-ex": { "error-ex": {
"version": "1.3.2", "version": "1.3.2",
@@ -591,7 +570,6 @@
"version": "1.17.6", "version": "1.17.6",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz",
"integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==",
"dev": true,
"requires": { "requires": {
"es-to-primitive": "^1.2.1", "es-to-primitive": "^1.2.1",
"function-bind": "^1.1.1", "function-bind": "^1.1.1",
@@ -610,7 +588,6 @@
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
"integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==",
"dev": true,
"requires": { "requires": {
"is-callable": "^1.1.4", "is-callable": "^1.1.4",
"is-date-object": "^1.0.1", "is-date-object": "^1.0.1",
@@ -620,14 +597,12 @@
"escape-string-regexp": { "escape-string-regexp": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
"dev": true
}, },
"esprima": { "esprima": {
"version": "4.0.1", "version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="
"dev": true
}, },
"eventemitter3": { "eventemitter3": {
"version": "4.0.7", "version": "4.0.7",
@@ -709,8 +684,7 @@
"function-bind": { "function-bind": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
"dev": true
}, },
"gauge": { "gauge": {
"version": "2.7.4", "version": "2.7.4",
@@ -779,7 +753,6 @@
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
"dev": true,
"requires": { "requires": {
"function-bind": "^1.1.1" "function-bind": "^1.1.1"
} }
@@ -787,14 +760,12 @@
"has-flag": { "has-flag": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0="
"dev": true
}, },
"has-symbols": { "has-symbols": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz",
"integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg=="
"dev": true
}, },
"has-unicode": { "has-unicode": {
"version": "2.0.1", "version": "2.0.1",
@@ -912,14 +883,12 @@
"is-callable": { "is-callable": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.0.tgz", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.0.tgz",
"integrity": "sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw==", "integrity": "sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw=="
"dev": true
}, },
"is-date-object": { "is-date-object": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz",
"integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==", "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g=="
"dev": true
}, },
"is-docker": { "is-docker": {
"version": "2.1.1", "version": "2.1.1",
@@ -967,7 +936,6 @@
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.0.tgz", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.0.tgz",
"integrity": "sha512-iI97M8KTWID2la5uYXlkbSDQIg4F6o1sYboZKKTDpnDQMLtUL86zxhgDet3Q2SriaYsyGqZ6Mn2SjbRKeLHdqw==", "integrity": "sha512-iI97M8KTWID2la5uYXlkbSDQIg4F6o1sYboZKKTDpnDQMLtUL86zxhgDet3Q2SriaYsyGqZ6Mn2SjbRKeLHdqw==",
"dev": true,
"requires": { "requires": {
"has-symbols": "^1.0.1" "has-symbols": "^1.0.1"
} }
@@ -982,7 +950,6 @@
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz",
"integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==",
"dev": true,
"requires": { "requires": {
"has-symbols": "^1.0.1" "has-symbols": "^1.0.1"
} }
@@ -1024,7 +991,6 @@
"version": "3.14.0", "version": "3.14.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz",
"integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==", "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==",
"dev": true,
"requires": { "requires": {
"argparse": "^1.0.7", "argparse": "^1.0.7",
"esprima": "^4.0.0" "esprima": "^4.0.0"
@@ -1129,8 +1095,7 @@
"mdn-data": { "mdn-data": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz",
"integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==", "integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA=="
"dev": true
}, },
"memorystream": { "memorystream": {
"version": "0.3.1", "version": "0.3.1",
@@ -1187,8 +1152,7 @@
"minimist": { "minimist": {
"version": "1.2.5", "version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
"dev": true
}, },
"minimist-options": { "minimist-options": {
"version": "4.1.0", "version": "4.1.0",
@@ -1224,7 +1188,6 @@
"version": "0.5.5", "version": "0.5.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
"dev": true,
"requires": { "requires": {
"minimist": "^1.2.5" "minimist": "^1.2.5"
} }
@@ -1363,7 +1326,6 @@
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz",
"integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==",
"dev": true,
"requires": { "requires": {
"boolbase": "~1.0.0" "boolbase": "~1.0.0"
} }
@@ -1395,20 +1357,17 @@
"object-inspect": { "object-inspect": {
"version": "1.8.0", "version": "1.8.0",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz",
"integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==", "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA=="
"dev": true
}, },
"object-keys": { "object-keys": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="
"dev": true
}, },
"object.assign": { "object.assign": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz",
"integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==",
"dev": true,
"requires": { "requires": {
"define-properties": "^1.1.2", "define-properties": "^1.1.2",
"function-bind": "^1.1.1", "function-bind": "^1.1.1",
@@ -1420,7 +1379,6 @@
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz", "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz",
"integrity": "sha512-Z53Oah9A3TdLoblT7VKJaTDdXdT+lQO+cNpKVnya5JDe9uLvzu1YyY1yFDFrcxrlRgWrEFH0jJtD/IbuwjcEVg==", "integrity": "sha512-Z53Oah9A3TdLoblT7VKJaTDdXdT+lQO+cNpKVnya5JDe9uLvzu1YyY1yFDFrcxrlRgWrEFH0jJtD/IbuwjcEVg==",
"dev": true,
"requires": { "requires": {
"define-properties": "^1.1.3", "define-properties": "^1.1.3",
"es-abstract": "^1.17.0-next.1" "es-abstract": "^1.17.0-next.1"
@@ -1430,7 +1388,6 @@
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.1.tgz", "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.1.tgz",
"integrity": "sha512-WTa54g2K8iu0kmS/us18jEmdv1a4Wi//BZ/DTVYEcH0XhLM5NYdpDHja3gt57VrZLcNAO2WGA+KpWsDBaHt6eA==", "integrity": "sha512-WTa54g2K8iu0kmS/us18jEmdv1a4Wi//BZ/DTVYEcH0XhLM5NYdpDHja3gt57VrZLcNAO2WGA+KpWsDBaHt6eA==",
"dev": true,
"requires": { "requires": {
"define-properties": "^1.1.3", "define-properties": "^1.1.3",
"es-abstract": "^1.17.0-next.1", "es-abstract": "^1.17.0-next.1",
@@ -1615,8 +1572,7 @@
"q": { "q": {
"version": "1.5.1", "version": "1.5.1",
"resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz",
"integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc="
"dev": true
}, },
"qs": { "qs": {
"version": "6.9.4", "version": "6.9.4",
@@ -1795,8 +1751,7 @@
"sax": { "sax": {
"version": "1.2.4", "version": "1.2.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
"dev": true
}, },
"secure-compare": { "secure-compare": {
"version": "3.0.1", "version": "3.0.1",
@@ -1863,8 +1818,7 @@
"source-map": { "source-map": {
"version": "0.6.1", "version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
"dev": true
}, },
"spdx-correct": { "spdx-correct": {
"version": "3.1.1", "version": "3.1.1",
@@ -1901,14 +1855,12 @@
"sprintf-js": { "sprintf-js": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
"dev": true
}, },
"stable": { "stable": {
"version": "0.1.8", "version": "0.1.8",
"resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz",
"integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w=="
"dev": true
}, },
"string-width": { "string-width": {
"version": "1.0.2", "version": "1.0.2",
@@ -1935,7 +1887,6 @@
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz", "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz",
"integrity": "sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==", "integrity": "sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==",
"dev": true,
"requires": { "requires": {
"define-properties": "^1.1.3", "define-properties": "^1.1.3",
"es-abstract": "^1.17.5" "es-abstract": "^1.17.5"
@@ -1945,7 +1896,6 @@
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz", "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz",
"integrity": "sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==", "integrity": "sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==",
"dev": true,
"requires": { "requires": {
"define-properties": "^1.1.3", "define-properties": "^1.1.3",
"es-abstract": "^1.17.5" "es-abstract": "^1.17.5"
@@ -2005,7 +1955,6 @@
"version": "5.5.0", "version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"dev": true,
"requires": { "requires": {
"has-flag": "^3.0.0" "has-flag": "^3.0.0"
} }
@@ -2014,7 +1963,6 @@
"version": "1.3.2", "version": "1.3.2",
"resolved": "https://registry.npmjs.org/svgo/-/svgo-1.3.2.tgz", "resolved": "https://registry.npmjs.org/svgo/-/svgo-1.3.2.tgz",
"integrity": "sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw==", "integrity": "sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw==",
"dev": true,
"requires": { "requires": {
"chalk": "^2.4.1", "chalk": "^2.4.1",
"coa": "^2.0.2", "coa": "^2.0.2",
@@ -2123,8 +2071,7 @@
"unquote": { "unquote": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/unquote/-/unquote-1.1.1.tgz", "resolved": "https://registry.npmjs.org/unquote/-/unquote-1.1.1.tgz",
"integrity": "sha1-j97XMk7G6IoP+LkF58CYzcCG1UQ=", "integrity": "sha1-j97XMk7G6IoP+LkF58CYzcCG1UQ="
"dev": true
}, },
"url-join": { "url-join": {
"version": "2.0.5", "version": "2.0.5",
@@ -2142,7 +2089,6 @@
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.1.tgz", "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.1.tgz",
"integrity": "sha512-g9JpC/3He3bm38zsLupWryXHoEcS22YHthuPQSJdMy6KNrzIRzWqcsHzD/WUnqe45whVou4VIsPew37DoXWNrA==", "integrity": "sha512-g9JpC/3He3bm38zsLupWryXHoEcS22YHthuPQSJdMy6KNrzIRzWqcsHzD/WUnqe45whVou4VIsPew37DoXWNrA==",
"dev": true,
"requires": { "requires": {
"define-properties": "^1.1.3", "define-properties": "^1.1.3",
"es-abstract": "^1.17.2", "es-abstract": "^1.17.2",