diff --git a/article.js b/article.js index bce02dea..17b898cf 100644 --- a/article.js +++ b/article.js @@ -63,9 +63,15 @@ var React = __webpack_require__(9); var ReactDOM = __webpack_require__(166); var Article = __webpack_require__(167); - var style = __webpack_require__(202); + var style = __webpack_require__(203); - ReactDOM.render(React.createElement(Article, null), document.getElementById("article")); + ReactDOM.render(React.createElement(Article, null), document.getElementById("article"), function () { + // trigger a #hash navigation + if (window.location.hash) { + var hash = window.location.hash; + window.location.hash = hash; + } + }); /***/ }, /* 9 */ @@ -19810,11 +19816,11 @@ tracing: __webpack_require__(199), intersections: __webpack_require__(200), - curveintersection: __webpack_require__(201) + curveintersection: __webpack_require__(201), + moulding: __webpack_require__(202) }; /* - moulding: require("./moulding"), pointcurves: require("./pointcurves"), catmullconv: require("./catmullconv"), @@ -20156,6 +20162,11 @@ fix(evt); this.mx = evt.offsetX; this.my = evt.offsetY; + + this.moving = false; + this.dragging = false; + this.down = true; + this.lpts.forEach(function (p) { if (Math.abs(_this.mx - p.x) < 10 && Math.abs(_this.my - p.y) < 10) { _this.moving = true; @@ -20165,8 +20176,8 @@ } }); - if (this.props.mouseDown) { - this.props.mouseDown(evt, this); + if (this.props.onMouseDown) { + this.props.onMouseDown(evt, this); } }, @@ -20174,6 +20185,10 @@ fix(evt); if (!this.props.static) { + if (this.down) { + this.dragging = true; + } + var found = false; this.lpts.forEach(function (p) { var mx = evt.offsetX; @@ -20209,8 +20224,12 @@ } } - if (this.props.mouseMove) { - this.props.mouseMove(evt, this); + if (this.props.onMouseMove) { + this.props.onMouseMove(evt, this); + } + + if (this.dragging && this.props.onMouseDrag) { + this.props.onMouseDrag(evt, this); } if (!this.playing && this.props.draw) { @@ -20219,7 +20238,14 @@ }, mouseUp: function mouseUp(evt) { - if (!this.moving) return; + this.down = false; + this.dragging = false; + if (!this.moving) { + if (this.props.onMouseUp) { + this.props.onMouseUp(evt, this); + } + return; + } this.moving = false; this.mp = false; if (this.props.onMouseUp) { @@ -20231,7 +20257,7 @@ fix(evt); this.mx = evt.offsetX; this.my = evt.offsetY; - if (this.props.onClick) { + if (!this.dragging && this.props.onClick) { this.props.onClick(evt, this); } }, @@ -20285,7 +20311,7 @@ setCurve: function setCurve(c) { var pts = []; - c = Array.from(arguments); + c = Array.prototype.slice.call(arguments); c.forEach(function (nc) { pts = pts.concat(nc.points); }); @@ -23271,6 +23297,12 @@ dot = dx1*dx2 + dy1*dy2; return atan2(cross, dot); }, + // round as string, to avoid rounding errors + round: function(v, d) { + var s = '' + v; + var pos = s.indexOf("."); + return parseFloat(s.substring(0,pos+1+d)); + }, dist: function(p1, p2) { var dx = p1.x - p2.x, dy = p1.y - p2.y; @@ -23762,6 +23794,19 @@ } return this._lut; }, + on: function(point, error) { + error = error || 5; + var lut = this.getLUT(), hits = [], c, t=0; + for(var i=0; i 0.95) api.t = false; + }, + + markQB: function markQB(evt, api) { + this.onClick(evt, api); + if (api.t) { + var t = api.t, + t2 = 2 * t, + top = t2 * t - t2, + bottom = top + 1, + ratio = abs(top / bottom), + curve = api.curve, + A = api.A = curve.points[1], + B = api.B = curve.get(t); + api.C = curve.getUtils().lli4(A, B, curve.points[0], curve.points[2]); + api.ratio = ratio; + } + }, + + markCB: function markCB(evt, api) { + this.onClick(evt, api); + if (api.t) { + var t = api.t, + mt = 1 - t, + t3 = t * t * t, + mt3 = mt * mt * mt, + bottom = t3 + mt3, + top = bottom - 1, + ratio = abs(top / bottom), + curve = api.curve, + hull = curve.hull(t), + A = api.A = hull[5], + B = api.B = curve.get(t), + db = api.db = curve.derivative(t); + api.C = curve.getUtils().lli4(A, B, curve.points[0], curve.points[3]); + api.ratio = ratio; + } + }, + + drag: function drag(evt, api) { + if (!api.t) return; + + var newB = api.newB = { + x: evt.offsetX, + y: evt.offsetY + }; + // find the current ABC and ratio values: + var A = api.A; + var B = api.B; + var C = api.C; + + // now that we know A, B, C and the AB:BC ratio, we can compute the new A' based on the desired B' + var newA = api.newA = { + x: newB.x - (C.x - newB.x) / api.ratio, + y: newB.y - (C.y - newB.y) / api.ratio + }; + }, + + dragQB: function dragQB(evt, api) { + if (!api.t) return; + this.drag(evt, api); + + var curve = api.curve; + api.update = [api.newA]; + }, + + dragCB: function dragCB(evt, api) { + if (!api.t) return; + this.drag(evt, api); + + // preserve struts for B when repositioning + var curve = api.curve, + hull = curve.hull(api.t), + B = api.B, + Bl = hull[7], + Br = hull[8], + dbl = { x: Bl.x - B.x, y: Bl.y - B.y }, + dbr = { x: Br.x - B.x, y: Br.y - B.y }, + pts = curve.points, + + // find new point on s--c1 + p1 = { x: api.newB.x + dbl.x, y: api.newB.y + dbl.y }, + sc1 = { + x: api.newA.x - (api.newA.x - p1.x) / (1 - api.t), + y: api.newA.y - (api.newA.y - p1.y) / (1 - api.t) + }, + + // find new point on c2--e + p2 = { x: api.newB.x + dbr.x, y: api.newB.y + dbr.y }, + sc2 = { + x: api.newA.x + (p2.x - api.newA.x) / api.t, + y: api.newA.y + (p2.y - api.newA.y) / api.t + }, + + // construct new c1` based on the fact that s--sc1 is s--c1 * t + nc1 = { + x: pts[0].x + (sc1.x - pts[0].x) / api.t, + y: pts[0].y + (sc1.y - pts[0].y) / api.t + }, + + // construct new c2` based on the fact that e--sc2 is e--c2 * (1-t) + nc2 = { + x: pts[3].x - (pts[3].x - sc2.x) / (1 - api.t), + y: pts[3].y - (pts[3].y - sc2.y) / (1 - api.t) + }; + + api.p1 = p1; + api.p2 = p2; + api.sc1 = sc1; + api.sc2 = sc2; + api.nc1 = nc1; + api.nc2 = nc2; + + api.update = [nc1, nc2]; + }, + + commit: function commit(evt, api) { + if (!api.t) return; + api.setCurve(api.newcurve); + api.t = false; + api.redraw(); + }, + + drawMould: function drawMould(api, curve) { + api.reset(); + api.drawSkeleton(curve); + api.drawCurve(curve); + + if (api.t) { + api.npts = [curve.points[0]].concat(api.update).concat([curve.points.slice(-1)[0]]); + api.newcurve = new api.Bezier(api.npts); + api.drawCurve(api.newcurve); + + api.setColor("lightgrey"); + api.drawHull(api.newcurve, api.t); + api.drawLine(api.npts[0], api.npts.slice(-1)[0]); + api.drawLine(api.newA, api.C); + + api.setColor("grey"); + api.drawCircle(api.newB, 3); + api.drawCircle(api.newA, 3); + api.drawCircle(api.C, 3); + } + }, + + render: function render() { + return React.createElement( + "section", + null, + React.createElement(SectionHeader, this.props), + React.createElement( + "p", + null, + "De Casteljau's algorithm is the pivotal algorithm when it comes to Bézier curves. You can use it not just to split curves, but also to draw them efficiently (especially for high-order Bézier curves), as well as to come up with curves based on three points and a tangent. Particularly this last thing is really useful because it lets us \"mould\" a curve, by picking it up at some point, and dragging that point around to change the curve's shape." + ), + React.createElement( + "p", + null, + "How does that work? Succinctly: we run de Casteljau's algorithm in reverse!" + ), + React.createElement( + "p", + null, + "Let's start out with a pre-existing curve, defined by ", + React.createElement( + "i", + null, + "start" + ), + ", two control points, and ", + React.createElement( + "i", + null, + "end" + ), + ". We can mould this curve by picking a point somewhere on the curve, at some ", + React.createElement( + "i", + null, + "t" + ), + " value, and the moving it to a new location and reconstructing the curve that goes through ", + React.createElement( + "i", + null, + "start" + ), + ", our new point with the original tangent, and ", + React.createElement( + "i", + null, + "end" + ), + ". In order to see how and why we can do this, let's look at some identity information for Bézier curves. There's actually a hidden goldmine of identities that we can exploit when doing Bézier operations, and this will only scratch the surface. But, in a good way!" + ), + React.createElement( + "p", + null, + "In the following graphic, click anywhere on the curves to see the identity information that we'll be using to run de Casteljau in reverse (you can manipulate the curve even after picking a point. Note the \"ratio\" value when you do so: does it change?):" + ), + React.createElement( + "div", + { className: "figure" }, + React.createElement(Graphic, { inline: true, preset: "abc", title: "Projections in a quadratic Bézier curve", setup: this.setupQuadratic, draw: this.draw, onClick: this.onClick }), + React.createElement(Graphic, { inline: true, preset: "abc", title: "Projections in a cubic Bézier curve", setup: this.setupCubic, draw: this.draw, onClick: this.onClick }) + ), + React.createElement( + "p", + null, + "So, what exactly do we see in these graphics? First off, there's the three points ", + React.createElement( + "i", + null, + "A" + ), + ", ", + React.createElement( + "i", + null, + "B" + ), + " and ", + React.createElement( + "i", + null, + "C" + ), + "." + ), + React.createElement( + "p", + null, + "Point ", + React.createElement( + "i", + null, + "B" + ), + " is our \"on curve\" point, A is the first \"strut\" point when running de Casteljau's algorithm in reverse; for quadratic curves, this happens to also be the curve's control point. For cubic curves, it's the \"top of the triangle\" for the struts that lead to point ", + React.createElement( + "i", + null, + "B" + ), + ". Point ", + React.createElement( + "i", + null, + "C" + ), + ", finally, is the intersection of the line that goes through ", + React.createElement( + "i", + null, + "A" + ), + " and ", + React.createElement( + "i", + null, + "B" + ), + " and the baseline, between our start and end points." + ), + React.createElement( + "p", + null, + "There is some important identity information here: as long as we don't pick a new ", + React.createElement( + "i", + null, + "t" + ), + " coordinate, the location of point ", + React.createElement( + "i", + null, + "C" + ), + " on the line ", + React.createElement( + "i", + null, + "start-end" + ), + " represents a fixed ratio distance. We can drag around the control points as much as we like, that point won't move at all, and if we can drag around the start or end point, C will stay at the same ratio-value. For instance, if it was located midway between start and end, it'll stay midway between start and end, even if the line segment between start and end becomes longer or shorter." + ), + React.createElement( + "p", + null, + "We can also see that the distances for the lines ", + React.createElement( + "i", + null, + "d1 = A-B" + ), + " and ", + React.createElement( + "i", + null, + "d2 = B-C" + ), + " may vary, but the ratio between them, ", + React.createElement( + "i", + null, + "d1/d2" + ), + ", is a constant value. We can drag any of the start, end, or control points around as much as we like, but that value also stays the same." + ), + React.createElement( + "div", + { className: "note" }, + React.createElement( + "p", + null, + "In fact, because the distance ratio is a fixed value for each point ", + React.createElement( + "i", + null, + "B" + ), + ", which we get by picking some ", + React.createElement( + "i", + null, + "t" + ), + " value on our curve, the distance ratio is actually an identity function for Bézier curves. If we were to plot all the ratio values for all possible ", + React.createElement( + "i", + null, + "t" + ), + " values for quadratic and cubic curves, we'd see two very interesting functions: asymptotic at ", + React.createElement( + "i", + null, + "t=0" + ), + " and ", + React.createElement( + "i", + null, + "t=1" + ), + ", tending towards positive infinity, with a zero-derivative minimum at ", + React.createElement( + "i", + null, + "t=0.5" + ), + "." + ), + React.createElement( + "p", + null, + "Since these are ratios, we can actually express the ratio values as a function of ", + React.createElement( + "i", + null, + "t" + ), + ". I actually failed at coming up with the precise functions, but thanks to some help from", + React.createElement( + "a", + { href: "http://mathoverflow.net/questions/122257/finding-the-formula-for-Bézier-curve-ratios-hull-point-point-baseline" }, + "Boris Zbarsky" + ), + " we can see that the ratio functions are actually remarkably simple:" + ), + React.createElement( + "table", + { style: { width: "100%", border: 0 } }, + React.createElement( + "tbody", + null, + React.createElement( + "tr", + null, + React.createElement( + "td", + null, + React.createElement( + "p", + null, + "Quadratic curves:", + React.createElement("img", { className: "LaTeX SVG", src: "images/latex/3607174eca6cb8780f98cc902e2b6eab50237b3e.svg", style: { width: "11.775150000000002rem", height: "2.7rem" } }) + ) + ), + React.createElement( + "td", + null, + React.createElement( + "p", + null, + "Cubic curves: ", + React.createElement("img", { className: "LaTeX SVG", src: "images/latex/7f5e48e56c0a0fb80040fdc84a9713c62eb2c416.svg", style: { width: "13.80015rem", height: "2.77515rem" } }) + ) + ) + ) + ) + ), + React.createElement( + "p", + null, + "Unfortunately, this trick only works for quadratic and cubic curves. Once we hit higher order curves, things become a lot less predictable; the \"fixed point ", + React.createElement( + "i", + null, + "C" + ), + "\" is no longer fixed, moving around as we move the control points, and projections of ", + React.createElement( + "i", + null, + "B" + ), + " onto the line between start and end may actually lie on that line before the start, or after the end, and there are no simple ratios that we can exploit." + ) + ), + React.createElement( + "p", + null, + "So, with this knowledge, let's change a curve's shape by click-dragging some part of it. The follow graphics let us click-drag somewhere on the curve, repositioning point ", + React.createElement( + "i", + null, + "B" + ), + " according to a simple rule: we keep the original point ", + React.createElement( + "i", + null, + "B" + ), + "'s tangent:" + ), + React.createElement( + "div", + { className: "figure" }, + React.createElement(Graphic, { inline: true, preset: "moulding", title: "Moulding a quadratic Bézier curve", + setup: this.setupQuadratic, draw: this.drawMould, + onClick: this.placeMouldPoint, onMouseDown: this.markQB, onMouseDrag: this.dragQB, onMouseUp: this.commit }), + React.createElement(Graphic, { inline: true, preset: "moulding", title: "Moulding a cubic Bézier curve", + setup: this.setupCubic, draw: this.drawMould, + onClick: this.placeMouldPoint, onMouseDown: this.markCB, onMouseDrag: this.dragCB, onMouseUp: this.commit }) + ) + ); + } + }); + + module.exports = Moulding; + +/***/ }, +/* 203 */ /***/ function(module, exports, __webpack_require__) { // style-loader: Adds some css to the DOM by adding a