“ In particular, it can be tricky to maintain consistent curvature between adjoining segments of a path, which can lead to a ‘lumpy’ look; the following example is contrived, but in practice consistently getting smooth feeling curves with Béziers is challenging.”
It’s not that hard. I’m an artist who specializes in Illustrator, and I have some simple rules for its pen tool:
1. Drag curve handles out to 1/3 of the length of the curve segment they control.
2. Eschew s-curves between two control points.
3. Don’t turn more than about 90º between two control points.
There are people out there who have a similar set of rules but prefer to always put their control points at 12/3/6/9 o’clock; for more circular shapes this works, but it falls apart on anything interesting - try drawing an oval with its long axis at about 45° with my method and with this method, and observe how much more uneven the curve handles have to be for the 12/3/6/9 method.
These kinds of rules are common, but it also means you're only using a fraction of the expressive power of the Béziers. Why not draw actual S-curves? That way you're guaranteed an actual inflection point where curvature goes smoothly through zero, as opposed to two arcs joined. Why not explore control over tension by using curve handles greater than 1/3 (so curvature on endpoints is less) or less than 1/3, cranking up the tension in a way inspired by physical materials? Why not have the computer do the work of ensuring the joins are G2 continuous by construction, instead of having to eyeball it?
These rules create visually pleasing curves that are easy to manipulate. You can break these rules and get curves that have fewer points, but they will be very tough to tweak.
Break these rules and you will pay for it in more time spent when you need to change things.
You can make an analogy to programming rules that tell you to eschew excessive cleverness - sure, it may be more succinct, but are you gonna be able to edit it later? What about the person who comes in once you've left and has to deal with it?
(These are also guidelines, I do not obsessively follow these precisely; to be quite honest I mostly avoid using Illustrator's Pen tool for the organic shapes I mostly draw - but if I need to tweak a path, I'll use these rules and a couple other tools to quickly modify where the points are along a path, and do it easily.)
I think I miscommunicated a bit. I'm not suggesting you should be routinely breaking these rules with cubic Béziers. Rather, I'm saying that if you had a better underlying curve family, you wouldn't need these rules, as you'd get good results even with a freer approach to designing curves. That's basically the hypothesis we're testing.
I got 'em from a book I read standing around at the bookstore like twenty years ago. They were good rules. If I could go back in time and hand Broke 2000 Me twenty bucks to buy that book I would.
It's probably worth adding a css cursor rule so users can tell that things can be dragged when they mouse over. That said: I'd love to see the more detailed explanation that guides your algorithm. Because auto-curvature is always useful (ever since Knuth wrote down metafont ;)
I read this but didn’t find what I was looking for: a construction of the curve.
Firstly, the article talks about G2 continuity. I didn’t find a definition but I didn’t try very hard. I think it is: A curve g : [a,b] -> R^n, parameterised by arclength is G_k-continuous if the kth derivative of g is continuous. So G2 continuity means that 0. The curve is connected; 1. There are no cusps—abrupt changes of angle; and 2. The rate of change of curvature is continuous—if you drove along the curve at constant speed, you could turn the steering wheel smoothly without jerking it.
For a bézier spline I don’t think there’s a good way to get G2 continuity. G1 continuity comes from the editor forcing the control points on either side of curve points to be colinear with the curve point.
I think the best place for the definition of these splines is [1], in particular [2]. It gives the following expression for the curve between two on-curve points:
k(s) = k0 * f(bias0, 1 - s) + k1 * f(bias1, s)
I think s, is a normalised arclength which ranges over the unit interval, k0 and k1 are the (also normalised) curvatures at the endpoints, and f is a seemingly bizarre function defined in [2]. k(s) is an expression for the curvature a distance s along the curve. So to find the points on the curve, one must solve a system of differential equations,
x’^2 + y’^2 = 1
x’’^2 + y’’^2 = k(s)^2,
subject to some suitable initial conditions. The above equation is slightly wrong as it should be using signed curvature, but these are simpler to write down. I think the initial conditions would be initial x and y values and an initial tangent, which doesn’t feel like sufficiently many degrees of freedom to me (you could presumably come up with some initial conditions for the first curve point and control point but I don’t understand how to go from the solution to the equation to the final curve: you can’t seem to move the point at s=1 into the right place as scaling changes the curvature and rotating or skewing changes the initial tangent). Looking more at the code, it seems that the relative angles come from solving the equation and then parameters are chosen such that the solution to the equation gives the desired angles.
I’ll have to look more and try to understand it later but if there’s a better reference I’d be interested to know.
The mathematical treatment of the underlying 4-parameter curve family is coming soonish; this blog focuses on the UX, which I think is also a very interesting and challenging problem.
I'll say a little more, as you've obviously put some time into trying to understand the code. You're generally on the right track.
The curve family is basically the same as the 4-parameter spline curve in my thesis when bias <= 1.0, except the parametrization is different. The magic (the "seemingly bizarre function") happens when bias > 1.0, in which case it describes a curve that's inspired by an elastica as the tension goes up, though the details are a bit different. Also note that when both bias values are 1.0, the curve is an Euler spiral, which I consider especially beautiful and suitable for interactive design.
Regarding solving the curve equations, yes, it has to do some solving in order to get the curve into place. It doesn't have to be a full differential equation solver, though, fundamentally it's a doubly nested integral - the expression of curvature as a function of arclength is a Cesàro equation, and integrating that once gives a Whewell equation.
G2 continuity is enforced by the spline solver, particularly `iterate` (with some help from `adjust_tensions` for cases where an auto point is opposite on off-curve point). The current version of that solver is a bit hacky and not 100% robust. Basically it computes the curvature error at each on-curve point, then pushes the tangent at that point in the direction of reducing the error.
As I said, I'll do a full explanation of the math before long. Hopefully that will address a lot of your questions.
I just want to say how deep my appreciation of your work is. I've skimmed your thesis in preparation for reading it closely and I've seen enough to recognize it as beautiful. This recent work too.
I started thinking about bezier curves in the ... '80s? '90s? – as a child I was basically raised at my grandmother's graphic design office in front of Macintosh computers. I had the Adobe Illustrator pen tool in my hands as a child and later, FreeHand's bezigon tool.
The hyperbezier pen tool is beautifully natural to use ("This is it!") – I'm also very glad that I've almost by chance learned enough mathematics to grasp how it works and how it was arrived at. Just barely. And I'm glad that I'm going to learn more math and understand it more completely.
I think the optimisations in the calculations threw me off a bit, but I understand now:
- to draw a curve, it has some starting angle and you move a small amount in that direction and then adjust the angle. The rate the angle changes is the signed curvature.
- therefore the angle change over the whole curve is the integral of the curvature between 0 and 1. (This simplifies the differential equations into dX/ds = (cos theta, sin theta), dtheta/ds = k.)
- because we don’t care about the initial angle, there’s a trick in the way an integral is computed to simplify the code (\int_{1-s}^1 f(s) ds = \int_0^1 f(s) ds - \int_0^{1-s} f(s) ds, but the first term is a constant so can be folded into the starting angle).
- the k0,k1,bias0,bias1 parameters give curves but they don’t much correspond to the things you care about (initial and final angle relative to the chord and chord length relative to arc length), so to go from the parameters you want to the curve, you do an optimisation step to find a curve for your desired parameters. This feels a bit expensive (you need to effectively solve the differential equation above to find the chord length. Maybe there’s an analytic solution (surely there is when f is polynomial) but there’s 8-18 cases and the integrals look gnarly)
- there’s an error in the definition of the basis function in the bias<1 case, it should be doubled.
I guess a thing I still don’t understand is what motivates the particular choice of the basis function. There doesn’t seem to be anything very natural about either of the cases. I was also thrown off by the definition of k because I expected it to be roughly like interpolation (I.e. f(b,-) would always be strictly increasing with f(b,0)=0,f(b,1)=1), but f(b,1) can range over a pretty wild set of values.)
Is this the same idea as Inkscape's smooth points? ("make selected nodes auto-smooth") Or is it a different method?
EDIT: hmm, the behaviour is not quite the same - inkscape seems to move the handles of its auto-smooth nodes only in response to the positions of neighbouring nodes. This "hyperbezier" demo moves its handles in response to positions of both neighbouring nodes and their handles.
This is correct, the hyperbezier positions auto points to maintain curvature, so anything that impacts curvature of a segment can cause adjustment of the handles of neighbouring segments. In addition, auto points do not need to be smooth; you can have a corner with two auto points, which will be adjusted automatically in response to changes in segments to their 'side' of the corner (see the heart demo on the page for an example of this.)
The solver is still a little rough, and there are some robustness issues in certain circumstances, generally when points are very close together. This will get ironed out eventually.
Very cool, and I like it a lot. Regarding the prototype/POC, did you ever consider writing the editor using SVG instead of canvas? And if you did, why did you go with canvas?
This is mostly circumstance; the prototype is a compilation to wasm of a bunch of code that was originally written for runebender, a desktop font editor. The framework that project is written in uses canvas when targeting the web.
This is a great idea, I always struggle getting Bezier points to look natural. Will definitely play more with this, looking forward to the future posts!
It’s not that hard. I’m an artist who specializes in Illustrator, and I have some simple rules for its pen tool:
1. Drag curve handles out to 1/3 of the length of the curve segment they control.
2. Eschew s-curves between two control points.
3. Don’t turn more than about 90º between two control points.
There are people out there who have a similar set of rules but prefer to always put their control points at 12/3/6/9 o’clock; for more circular shapes this works, but it falls apart on anything interesting - try drawing an oval with its long axis at about 45° with my method and with this method, and observe how much more uneven the curve handles have to be for the 12/3/6/9 method.