Giter Club home page Giter Club logo

spline's Introduction

A spline for interactive curve design.

This crate implements a new spline designed and optimized for interactive design of 2D curves. A major motivation is fonts, but it can be used in other domains.

The work builds on previous iterations, notably the Spiro spline, and then another research spline.

Hyperbeziers

The major innovation of this spline is the "hyperbezier" curve family. Like cubic Béziers and the Spiro curve, it is a four-parameter curve family. In fact, it's closely based on Spiro and there is significant overlap of the parameter space, including Euler spirals.

There is a significant difference, however. In the Spiro curve family, curvature is bounded, so it is not capable of cusp-like behavior. Rather, when "pushed," Spiro tends to wiggly, Shmoo-like shapes. Béziers are of course capable of high curvature regions, as are elastica when placed under very high tension.

A good way to parametrize the hyperbezier is by tangent angle and "tension," which correlates strongly with curvature at the endpoint. At low tension, the hyperbezier is equivalent to the Spiro curve. A natural tension value produces the Euler spiral (curvature is a linear function of arclength). But for higher tension values, a different function takes over, which approaches a cusp at the endpoint as tension increases.

Unlike Béziers, the cusp happens only at the endpoint. Curvature maxima in the interior of a curve are ugly. With the hyperbezier, if the designer wants a sharp curvature maximum, simply place an on-curve point there.

A particular strength of the hyperbezier is smooth (G2-continuous) transitions from straight to curved sections. The hyperbezier is capable (unlike a cubic Bézier) of an S-shaped curve with zero curvature at both ends. It's also capable of a wide range of Euler spiral like behavior where one end has zero curvature and the other is a nice rounded shape (in the general case a designer would use at least two Béziers to create this effect).

The name "hyperbezier" clearly references its roots in the cubic Bézier, and the "hyper" part is a reference to the fact that the Euler spiral, an important section of its parameter space, is an instance of the Hypergeometric function.

Focus on UX

A persistent challenge with spline-based curve design is getting the UX right. Bézier curves are not easy to master, but the pen tool has become highly refined over time, and is an extremely productive interface for designers. A major motivation for this work is to retain the good parts of the Bézier UX.

In particular, the "control handle" maps to hyperbezier parameters in a natural, intuitive way. The tangent angle is obvious, and tension similarly dependent on the length of the control arm. So it's completely valid to use hyperbeziers simply as a drop-in replacement for Béziers.

The intended UX for use as an interpolating spline is simply to designate a control point as "auto." As is traditional, the spline solver solves these for G2 continuity. Where the tension is a free parameter (which generally happens when there is an "auto" point on either side of an on-curve point), it is assigned a reasonable default, in particular the Euler spiral value for small to medium deviations, and a value similar to the research spline as the deviation increases.

To further refine a curve, the designer can click on an auto point and drag it to the desired location. That gesture enforces tangents at extrema, and in general allows for fine tuning of tension, for example to make quadrants more superelliptical (a strength of Bézier editing and a relative weakness of Spiro).

Note: as of this release, the interpolating spline is still work in progress.

spline's People

Contributors

cmyr avatar matthewblanchard avatar piaoger avatar raphlinus avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

spline's Issues

missing term in equation?

I was snooping through the code and I found something peculiar.

spline/src/hyperbezier.rs

Lines 261 to 263 in e4718e9

let a = (bias - 1.0).min(MAX_A);
let norm = 1.0 / (1.0 - a) + (1.0 - a).ln() - 1.0;
(1.0 / (1.0 - a * s) + (1.0 - a * s).ln()) / norm

it may be that it doesn't matter in the context of compute_theta, but when bias ≥ 1.0002, integrate_basis has a huge y-offset for smaller values of bias. for instance, integrate_basis(1.0002, 0.0) = 49986667.306. I believe the numerator is missing a -1 term, like so:

    (1.0 / (1.0 - a * s) + (1.0 - a * s).ln() - 1.0) / norm

Is it possible to get a lossless (or even lossy) upgrade of a Spiro spline to a Hyperbezier?

Now that I have implemented https://github.com/MFEK/spiro.rlib, I am very curious if it's possible to upgrade my Spiros to the superior Hyperbezier.

I have a lot of Spiros, that's why I maintain it.

In raphlinus/raphlinus.github.io#40 @raphlinus wrote:

Introduction: hyperbezier is a synthesis of three curves: cubic bezier, euler spiral, and elastica. Tension is a central concept.

This is very similar to how Spiro works, if I understood the thesis right. So really perhaps hyperbezier is "spiro v2", but perhaps that's ignorant of the maths of it all.

Is there a way to upgrade Spiros, Raph?

Split / merge segments

For use in an interactive tool, there needs to be an ability to split a segment at some point, as well as the ability to take two adjoining segments and join them, adjusting control points so as to approximate the original curve.

Infinite k0 in toggled control point

Adding this test to spline.rs produces an incorrect result:

#[test]
fn flattening_the_curve() {
    let mut spec = SplineSpec::new();
    spec.move_to(Point::new(191.0, 547.0));
    spec.spline_to(None, None, Point::new(464.0, 359.0), true);
    spec.spline_to(
        Some(Point::new(464.5124106920355, 255.68988704270467)),
        None,
        Point::new(274.0, 181.0),
        true,
    );

    let solved = spec.solve();
    dbg!(solved.segments());
    panic!("ahhhh");
}

In particular, the second segment looks like this:

    Segment {
        p0: (464.0, 359.0),
        p1: (464.5124106920355, 255.68988704270467),
        p2: (363.43976269500405, 204.23933114016464),
        p3: (274.0, 181.0),
        th0: 0.8229551955536701,
        th1: 0.49859013240981914,
        k0: -inf,
        k1: -0.0013425418134944465,
        hb: HyperBezier {
            k0: -1.1329359778056933,
            bias0: 1.0000000000000013,
            k1: -0.18860935015779595,
            bias1: 1.0,
        },
        ch: 0.9266126033818294,
    },

Most of these values look fine, but the k0 of the segment is pretty clearly wrong, though the corresponding value of the HyperBezier is fine.

Dependancies currently reduce portablity, and reduce clarity?

Currently dependancies such as Kurdo make it hard to assess how easy it would be to port to another programming language outside the Rust programming ecosystem.
I guess ideal would be if a rust contributor forked dependancies to just include required code as an alternate choice so that porting is more feasible, so dependancies can be replaced or replicated more clearly, I guess if I coded Rust it might be simpler to port, but I think it's rather the problem is the coupling is hard to untangle ( I am already used to looking at less familar languages so I think it's dependancy that is the main tricky part ).

I am unaware of any ports of relevant parts of Kurdo to other languages and it's very large lib to try port just for better curves.

My primary interest in porting to Haxe ( my preferance language ), and to allow cross compile to php, js, c++, python etc..
I have my own geom library 'geom' that could be extended to perhaps provide some Kurdo features ( certainly points and matricies ).

Also unclear from my previous look at the code base how the shader aspect of the curve algorithm works, currently I use line segments for standard Bezier gl/webgl rendering curves which in turn use triangles to create thickness in one of my libraries in the shader ( use an interleaved shader with xyz rgba uv ), it's not clear if spine is setup to allow similar approaches, or is it doing something smarter with the shaders and curves.

I know you intend to write a more detailed technical write up, but probably some of us don't require full details of the maths which may take time ( although if I follow it all definitely interesting ), but rather would love some more detail on the code/structures spline generates and uses so it's clearer how it could be rebuilt for different ecosystems.

My initial gut feeling ( when looked awhile ago ) was the code was rather tightly coupled to Rust ecosystem but the algorithms really did not need to be, but maybe I misunderstood the code base.

Thoughts or suggestions welcome, obviously as you know I ported hxSpiro which I have largely abandoned touching further due to GPL license complications with my unfortunate choice of sources, but keen for better curves MIT licensed code in place of standard Beziers for use with Haxe and excited by the rust font examples seen.

Even with Haxe c++ it's not simple to work out how to wire up rust library so porting seems simpler.

Another question I have made minor contributions to with hxTrueType that provides font curves, but do you have proposal for extended TTF format to include spine type curves. I guess TTF etc.. use rather horrible tables inside, and do not provide more modern features that should be in next generation font use.

Even more optimized rendering

This is a followon to #15 and also a brain dump of things I thoughts while working on #21. I chose to make that a checkpoint with reasonably good results, rather than proceeding to a highly optimized solution.

First, it would be possible to solve this problem using numerical optimization techniques, much as chapter 9 of my thesis. That would be suitable when generating fonts, but is not at all a workable approach for interactive techniques - it would take way too long to produce the optimized representation. Yet, implementing that is probably a good stepping stone, partly because it is useful in batch contexts, and partly to provide a reference for more heuristic techniques.

One of the lessons of that chapter is that making the arclength of the rendered cubic segment match the arclength of the original curve is important. That leaves a single parameter remaining, which is the ratio of the two control arms. My feeling is that if you leave this ratio the same as it is now (ie the dt calculation derived from mapping u to t) but tweak the control arms to make the arclength match better, it will be a big improvement.

Obviously this could be solved by actually measuring the arclength and iterating a solver, but performance is probably unappealing for interactive use. I propose the following heuristic, though it would need to be validated. Assuming a flat parametrization (t = u), the length of a control arm is 2/(3 + 3 cos θ), where θ is the deflection from the chord to the control point. Note that this formula gives a very good approximation to circular arcs when the deflection is symmetrical (it is equivalent to the standard one, for example in the circles and cubics section of the Bézier primer). I suspect it will also work pretty well in cases other than the symmetrical one, but that would have to be validated. For other parametrizations, it should probably scale by dt/du, but that's also something that would need to be validated.

The parametrization (calc_t()) also hasn't been carefully tuned. It's flat for low tensions (bias < 1), but it should probably have a higher derivative near the endpoints, especially for bias near 0. Note that this should improve the rendering of low-tension segments, as the arclength is longer and thus they need longer control arms.

Lastly, render_subdivisions should be carefully tuned to produce a minimum number of subdivisions that meet some error criterion (and the error criterion should probably be tunable as well). I think special care is needed for low bias values - a value of 1 is reasonable when bias=1 (for low deflections), and possibly higher, but cubic Béziers don't do a good job modeling cases where the curvature is zero at one endpoint. The only reasonable way to tune this is to have a test framework that reports the end-to-end error, and then add terms to make it fit better. I suspect there may be a k0 - k1 term, as well as obviously some effect of bias.

NaN in low-segment spline

The following test case fails. I haven't dug into this at all.

    #[test]
    fn line_to_spline_to_crash() {
        let mut spec = SplineSpec::new();
        spec.move_to(Point::new(0., 0.));
        spec.spline_to(Some(Point::new(100.0, 0.0)), None, Point::new(0.0, 100.0), true);
        spec.line_to(Point::new(0.0, 0.0), false);
        for segment in spec.solve().segments() {
            assert!(segment.p1.x.is_normal(), segment.p2.x.is_normal());
        }
    }

A more perfect curve family

I've been feeling a bit stuck on moving this crate forward, because I feel the current draft is not perfect. I talked about that a bit in #25 but will summarize here. Basically, I feel the curves are very good for most of the parameter space, but there are a few areas in which they are not great, and I also had a feeling that some of the definitions were a bit arbitrary, lacking deep mathematical beauty.

Two specific areas that were not great:

  • A curve with high tension on one side and neutral tension on the other has a curvature dip in the middle, which I don't find appealing. It should be more like a spiral (or a semicubical parabola).

  • It can only go to a moderate degree of superellipticity (by setting tension low on the endpoints), even less than a cubic Bézier. Superellipses are extremely common in font design; classic fonts such as Eurostile, and Melior are directly inspired by them.

And the mathematical critiques:

  • There is a case analysis between >1 tension and <1 tension, with the Euler spiral in the middle. The math is pretty different in the two cases.

  • The curve family is not closed under subdivision, a property very closely related to "extensionality" in my thesis (also see related CAD 2009 paper). In addition to mathematical beauty, this affects the UI, as inserting a new subdivision point changes the existing curve.

I have considered it an open question whether extensionality (or subdivision closure) is a highly desirable property or one that can be sacrificed in a tradeoff with other properties. If the proposal in this issue succeeds, I believe that will resolve the question to the former.

The proposal, very simply, is to represent the curve as a Cesàro equation (curvature as a function of arclength) where curvature is a rational function consisting of a linear divided by quadratic polynomial, in other words:

$$\kappa(s) = \frac{as+b}{cs^2 + ds + e}$$

I have been experimenting with this some and find it promising, though haven't fully developed it. I can make some observations:

  • Subdivision is straightforward.

  • The family is characterized by the sign of c/e:

    • If $c/e &lt; 0$, there are two cusps (zeros of the denominator), corresponding to high tension. Tension is lowered by subdividing the full curve at a point away from the cusp.

    • If $c = 0$ and $d \neq 0$, there is one cusp. This corresponds to the case of high tension on one side and neutral tension on the other.

    • if $c = 0$ and $d = 0$, the curve is an Euler spiral.

    • if $c/e &gt; 0$, there is no cusp, but there is a curvature maximum, with curvature tailing to zero in the asymptotes. This corresponds to the superellipses.

  • There is in general one inflection point. This is the biggest difference from a cubic Bézier (which has two in the general case) and a 4-parameter Spiro (which has three).

  • Curvature is monotonic for much of the parameter space, which means that in usage in a spline, curvature extrema will generally be at knots. This is a desirable property, and one of the main flaws of Béziers in splines.

  • I'm fairly confident that this can be integrated analytically, resulting in a Whewell equation. There's a case analysis as above, with the result expressed in terms of $\tan^{-1}$ and $\log(1 + x^2)$ for $c/e &gt; 0$, and variations of $\tanh^{-1}$ for $c/e &lt; 0$.

This curve family has an appealing mathematical beauty, and there's a good chance it will work well in practice. It should definitely be investigated more fully.

Improve rendering algorithm

The current renderer converts the hyperbezier to a bézier path by arbitrarily subdividing it into 64 segments, and computing a cubic for each segment. This is a reasonable general-purpose solution, but is inefficient in many cases, where the path could be reasonably approximated by fewer segments.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.