Documentation ¶
Overview ¶
Package jhobby deals with MetaFont/MetaPost-like paths. It provides an implementation of John Hobby's spline interpolation algorithm.
Spline interpolation by Hobby's algorithm results in aesthetically pleasing curves superior to "normal" spline interpolation (as used in many graphics programs). The primary source of information for "Hobby-splines" is:
Smooth, Easy to Compute Interpolating Splines -- John D. Hobby Computer Science Dept. Stanford University Report No. STAN-CS-85-1047, Jan 1985 http://i.stanford.edu/pub/cstr/reports/cs/tr/85/1047/CS-TR-85-1047.pdf
The practical algorithm is explained in
Computers & Typesetting, Vol. B & D. http://www-cs-faculty.stanford.edu/~knuth/abcde.html
A good discussion of the implementation may be found in:
(1) Implementing Hobby Curve posted on 2015-04-28 by Hui Zhou Perl code embedded at http://hz2.org/blog/hobby_curve.html (no copyright information)
Other implementations are available in Python:
(2) Curve through a sequence of points with Metapost and TikZ https://tex.stackexchange.com/questions/54771/curve-through-a-sequence-of-points-with-metapost-and-tikz (Python code (c) Copyright 2012 JL Diaz)
and
(3) Module metapost.path -- PyX Manual 0.14.1 http://pyx.sourceforge.net/manual/metapost.html (GNU license (c) Copyright the PyX team)
This Go implementation is not the result of transcoding any of these implementations, but it is of course inspired by them. The notation sticks closely to the original code in MetaFont. The API's concept for path building very loosely follows the ideas in PyX.
Usage ¶
Clients of the package usually build a "skeleton" path, without any spline control point information. Such a path is called a "HobbyPath" and it may contain various parameters at knots and/or joins. In the MetaFont/MetaPost DSL one may specify it as follows:
(0,0)..(2,3)..tension 1.4..(5,3)..(3,-1){left}..cycle
On evaluation of this path expression, MetaFont/MetaPost immediately will find the control points in a clever way to construct a smooth curve through the knots of the path. When using the methods of package "path", clients will build a skeleton path with a kind of builder pattern (package qualifiers omitted for clarity and brevity):
Nullpath().Knot(P(0,0)).Curve().Knot(P(2,3)).TensionCurve(N(1.4),N(1.4)).Knot(P(5,3)) .Curve().DirKnot(P(3,-1),P(-1,0)).Curve().Cycle()
Alternatively clients may put interface HobbyCurve over their own path data structure. Either way, a HobbyPath will then be subjected to a call to FindHobbyControls(...)
controls = FindHobbyControls(path, nil)
which returns the necessary control point information to produce a smooth curve:
(0,0) .. controls (-0.5882,1.2616) and (0.4229,2.6442) .. (2,3) .. controls (2.7160,3.1616) and (4.3325,3.2937) .. (5,3) .. controls (6.5505,2.3177) and (6.2401,-0.4348) .. (3,-1) .. controls (1.8036,-1.2085) and (0.4731,-1.0144) .. cycle
Caveats ¶
(1) The development of this package is still in a very early phase. Please do use with caution!
(2) Currently there are slight deviations from MetaFont's calculation, probably due to different rounding. These are under investigation.
(3) Currently it isn't possible to explicitly set control points, as I don't need this functionality. This may or may not change in the future. Please note that the goal of this project is ultimately to support graphical requirements for typesetting, not implementing a graphical system. If you need a full fledged engine for preparing illustrations, you should stick to MetaPost, which is a really great piece of software!
BSD License ¶
Copyright (c) 2017–21, Norbert Pillmayer ¶
All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
3. Neither the name of this software nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Index ¶
- func AsString(path HobbyPath, contr SplineControls) string
- func T() tracing.Trace
- type HobbyPath
- type JoinAdder
- type KnotAdder
- type Path
- func (path *Path) AppendSubpath(sp *Path) JoinAdder
- func (path *Path) CurlKnot(p arithm.Pair, precurl, postcurl float64) JoinAdder
- func (path *Path) Curve() KnotAdder
- func (path *Path) Cycle() (HobbyPath, SplineControls)
- func (path *Path) DirKnot(p arithm.Pair, dir arithm.Pair) JoinAdder
- func (path *Path) End() (HobbyPath, SplineControls)
- func (path *Path) IsCycle() bool
- func (path *Path) Knot(pr arithm.Pair) JoinAdder
- func (path *Path) Line() KnotAdder
- func (path *Path) N() int
- func (path *Path) PostCurl(i int) float64
- func (path *Path) PostDir(i int) arithm.Pair
- func (path *Path) PostTension(i int) float64
- func (path *Path) PreCurl(i int) float64
- func (path *Path) PreDir(i int) arithm.Pair
- func (path *Path) PreTension(i int) float64
- func (path *Path) SetPostCurl(i int, curl float64) *Path
- func (path *Path) SetPostDir(i int, dir arithm.Pair) *Path
- func (path *Path) SetPostTension(i int, tension float64) *Path
- func (path *Path) SetPreCurl(i int, curl float64) *Path
- func (path *Path) SetPreDir(i int, dir arithm.Pair) *Path
- func (path *Path) SetPreTension(i int, tension float64) *Path
- func (path *Path) SmoothKnot(p arithm.Pair) JoinAdder
- func (path *Path) TensionCurve(t1, t2 float64) KnotAdder
- func (path *Path) Z(i int) arithm.Pair
- type SplineControls
- Bugs
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func AsString ¶
func AsString(path HobbyPath, contr SplineControls) string
AsString returns a path -- optionally including spline control points -- as a (debugging) string. The string contains newlines if control point information is present. Otherwise it will include the knot coordinates in one line.
Example, a circle of diameter 1 around (2,1):
(1,1) .. controls (1.0000,1.5523) and (1.4477,2.0000) .. (2,2) .. controls (2.5523,2.0000) and (3.0000,1.5523) .. (3,1) .. controls (3.0000,0.4477) and (2.5523,0.0000) .. (2,0) .. controls (1.4477,0.0000) and (1.0000,0.4477) .. cycle
The format is not fully equivalent to MetaFont's, but close.
Types ¶
type HobbyPath ¶
type HobbyPath interface { IsCycle() bool // is this a cyclic path? N() int // number of knots in the path Z(int) arithm.Pair // knot #i modulo N PreDir(int) arithm.Pair // explicit dir before knot #i PostDir(int) arithm.Pair // explicit dir after knot #i PreCurl(int) float64 // explicit curl before knot #i PostCurl(int) float64 // explicit curl after knot #i PreTension(int) float64 // explict tension before knot #i PostTension(int) float64 // explicit tension after knot #i }
HobbyPath is the path type we're dealing with. We base the implementation on an interface, with numeric values and point values represented by Go's native float types.
The interface is a "read only" interface in the sense that it provides the input path's parameters for the MetaFont spline interpolation. A path may have parameters provided for knots or for joins/curves. Possible knot parameters are: dir (= an explicit angle for the tangent at the knot) or curl (= the amount of curvature at the knot). Curves may be given tension parameters, which control the "tightness" of the line between knots. A negative value for a tension means "at least" the amount of tension and is used to prevent the spline from leaving the bounding box of its control point.
Paths may be cyclic, i.e. closed. Knots, addressed by path.Z(i), must adhere to the following requirement: Z() must accept subscipts >= N, i.e. larger than the length of the path, and return knot[i mod N]. The last knot of a cyclic path is identical to the first one, but it must not be included twice! Instead, the algorithm relies on the modulo-subscripting mechanism for adressing all knots of the cycle.
A HobbyPath does not contain information about spline control points (the path's properties are understood as "input parameters" for the Hobby algorithm). Control point information is handled using a separate interface.
see type SplineControls.
type JoinAdder ¶
type JoinAdder interface { Line() KnotAdder Curve() KnotAdder TensionCurve(t1, t2 float64) KnotAdder End() (HobbyPath, SplineControls) }
JoinAdder is an interface for helping control the construction of a path. It is used to ensure that every knot is followed by a join/curve (or ends the path).
It is probably only useful in conjunction with type Path. It is made public to support source code editors with code completion.
type KnotAdder ¶
type KnotAdder interface { Knot(arithm.Pair) JoinAdder SmoothKnot(arithm.Pair) JoinAdder CurlKnot(pr arithm.Pair, precurl, postcurl float64) JoinAdder DirKnot(pr arithm.Pair, dir arithm.Pair) JoinAdder AppendSubpath(sp *Path) JoinAdder Cycle() (HobbyPath, SplineControls) }
KnotAdder is an interface for helping control the construction of a path. It is used to ensure that evey path-join (curve) is followed by a knot (or by a cycle statement).
It is probably only useful in conjunction with type Path. It is made public to support source code editors with code completion.
type Path ¶
type Path struct { Controls *splcntrls // control points to be calculated // contains filtered or unexported fields }
Path is a concrete implementation of interface HobbyPath. To construct a path, start with Nullpath(), which creates an empty path, and then extend it.
func Nullpath ¶
func Nullpath() *Path
Nullpath creates an empty path, to be extended by subsequent builder calls. the following example builds a closed path of three knots, which are connected by a curve, then a straight line, and a curve again.
var path HobbyPath var controls SplineControls path, controls = Nullpath().Knot(0,0).Curve().Knot(3,2).Line().Knot(5,2.5).Curve().Cycle()
Calling Cycle() or End() returns a path and a container for spline control point information. The latter is empty and to be filled by calculating the Hobby spline control points.
func (*Path) AppendSubpath ¶
AppendSubpath concatenates two paths at an overlapping knot. Part of builder functionality.
func (*Path) CurlKnot ¶
CurlKnot adds a path with curl information to a path. Callers may specify pre- and/or post-curl. A curl value of 1.0 is considered neutral. Part of builder functionality.
func (*Path) Cycle ¶
func (path *Path) Cycle() (HobbyPath, SplineControls)
Cycle closes a cyclic path. Part of builder functionality.
func (*Path) DirKnot ¶
DirKnot adds a knot with a given tangent direction. Part of builder functionality.
func (*Path) End ¶
func (path *Path) End() (HobbyPath, SplineControls)
End an open path. Part of builder functionality.
func (*Path) N ¶
N returns the length of this path (knot count). For cyclic paths, the first and last knot should count as one.
Interface HobbyPath.
func (*Path) PostDir ¶
PostDir gets the outgoing tangent / direction vector at z.i .
Interface HobbyPath.
func (*Path) PreDir ¶
PreDir gets the incoming tangent / direction vector at z.i .
Interface HobbyPath.
func (*Path) SetPostCurl ¶
SetPostCurl is a property setter.
func (*Path) SetPostDir ¶
SetPostDir is a property setter.
func (*Path) SetPostTension ¶
SetPostTension is a property setter.
Tensions are adapted to lie between 3/4 and 4 (absolute). Negative tensions are interpreted as "at least" tensions to ensure the spline stays within the bounding box at its control point.
func (*Path) SetPreCurl ¶
SetPreCurl is a property setter.
func (*Path) SetPreTension ¶
SetPreTension is a property setter.
Tensions are adapted to lie between 3/4 and 4 (absolute). Negative tensions are interpreted as "at least" tensions to ensure the spline stays within the bounding box at its control point.
func (*Path) SmoothKnot ¶
SmoothKnot adds a standard smooth knot to a path (same as Knot(pr)). Part of builder functionality.
func (*Path) TensionCurve ¶
TensionCurve connects two knots with a tense curve. Part of builder functionality.
Tensions are adapted to lie between 3/4 and 4 (absolute). Negative tensions are interpreted as "at least" tensions to ensure the spline stays within the bounding box at its control point.
BUG(norbert@pillmayer.com): Tension spec "at least" currently not completely implemented.
type SplineControls ¶
type SplineControls interface { PreControl(i int) arithm.Pair PostControl(i int) arithm.Pair SetPreControl(int, arithm.Pair) // set control point (after calculation) SetPostControl(int, arithm.Pair) // set control point (after calculation) }
SplineControls is used for gathering the spline control points // calculated by Hobby's algorithm.
A HobbyPath starts out being void of spline control points (a skeleton path, which may be interpreted as a polygon). The path may include tension or tangent angle information (dir and/or curl). Clients then use FindHobbyControls(...) to fill in appropriate control point information for a curved path through the path's knots.
see FindHobbyControls(...)
Example (Usage) ¶
Draw a cicle with diameter 1 around (2,1). The builder statement returns a HobbyPath (type Path under the hood) and SplineControls. Type Path actually contains a link to its spline controls (field path.Controls). These controls are initially empty and then used for the call to FindHobbyControls(...), where they get filled.
path, controls := Nullpath().Knot(arithm.P(1, 1)).Curve().Knot(arithm.P(2, 2)).Curve().Knot(arithm.P(3, 1)). Curve().Knot(arithm.P(2, 0)).Curve().Cycle() fmt.Printf("skeleton path = %s\n\n", AsString(path, nil)) fmt.Printf("unknown path = \n%s\n\n", AsString(path, controls)) controls = FindHobbyControls(path, controls) fmt.Printf("smooth path = \n%s\n\n", AsString(path, controls)) // skeleton path = (1,1) .. (2,2) .. (3,1) .. (2,0) .. cycle // unknown path = // (1,1) .. controls (<unknown>) and (<unknown>) // .. (2,2) .. controls (<unknown>) and (<unknown>) // .. (3,1) .. controls (<unknown>) and (<unknown>) // .. (2,0) .. controls (<unknown>) and (<unknown>) // .. cycle // smooth path = // (1,1) .. controls (1.0000,1.5523) and (1.4477,2.0000) // .. (2,2) .. controls (2.5523,2.0000) and (3.0000,1.5523) // .. (3,1) .. controls (3.0000,0.4477) and (2.5523,0.0000) // .. (2,0) .. controls (1.4477,0.0000) and (1.0000,0.4477) // .. cycle
Output:
func FindHobbyControls ¶
func FindHobbyControls(path HobbyPath, controls SplineControls) SplineControls
FindHobbyControls finds the parameters for Hobby-spline control points for a given skeletion path.
BUG(norbert@pillmayer.com): Currently there are slight deviations from MetaFont's calculation, probably due to different rounding. These are under investigation.
Notes ¶
Bugs ¶
Tension spec "at least" currently not completely implemented.
Currently it isn't possible to explicitly set control points. This may or may not change in the future.
Currently there are slight deviations from MetaFont's calculation, probably due to different rounding. These are under investigation.