jhobby

package
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 13, 2021 License: BSD-3-Clause Imports: 7 Imported by: 1

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

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

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.

func T

func T() tracing.Trace

T is tracing to the graphics tracer.

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

func (path *Path) AppendSubpath(sp *Path) JoinAdder

AppendSubpath concatenates two paths at an overlapping knot. Part of builder functionality.

func (*Path) CurlKnot

func (path *Path) CurlKnot(p arithm.Pair, precurl, postcurl float64) JoinAdder

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) Curve

func (path *Path) Curve() KnotAdder

Curve connects two knots with a smooth curve. 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

func (path *Path) DirKnot(p arithm.Pair, dir arithm.Pair) JoinAdder

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) IsCycle

func (path *Path) IsCycle() bool

IsCycle is a predicate: is this path cyclic?

Interface HobbyPath.

func (*Path) Knot

func (path *Path) Knot(pr arithm.Pair) JoinAdder

Knot adds a standard smooth knot to a path. Part of builder functionality.

func (*Path) Line

func (path *Path) Line() KnotAdder

Line connects two knots with a straight line. Part of builder functionality.

func (*Path) N

func (path *Path) N() int

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) PostCurl

func (path *Path) PostCurl(i int) float64

PostCurl gets the curl after z.i.

Interface HobbyPath.

func (*Path) PostDir

func (path *Path) PostDir(i int) arithm.Pair

PostDir gets the outgoing tangent / direction vector at z.i .

Interface HobbyPath.

func (*Path) PostTension

func (path *Path) PostTension(i int) float64

PostTension returns the tension after z.i.

Interface HobbyPath.

func (*Path) PreCurl

func (path *Path) PreCurl(i int) float64

PreCurl gets the curl before z.i.

Interface HobbyPath.

func (*Path) PreDir

func (path *Path) PreDir(i int) arithm.Pair

PreDir gets the incoming tangent / direction vector at z.i .

Interface HobbyPath.

func (*Path) PreTension

func (path *Path) PreTension(i int) float64

PreTension returns the tension before z.i.

Interface HobbyPath.

func (*Path) SetPostCurl

func (path *Path) SetPostCurl(i int, curl float64) *Path

SetPostCurl is a property setter.

func (*Path) SetPostDir

func (path *Path) SetPostDir(i int, dir arithm.Pair) *Path

SetPostDir is a property setter.

func (*Path) SetPostTension

func (path *Path) SetPostTension(i int, tension float64) *Path

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

func (path *Path) SetPreCurl(i int, curl float64) *Path

SetPreCurl is a property setter.

func (*Path) SetPreDir

func (path *Path) SetPreDir(i int, dir arithm.Pair) *Path

SetPreDir is a property setter.

func (*Path) SetPreTension

func (path *Path) SetPreTension(i int, tension float64) *Path

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

func (path *Path) SmoothKnot(p arithm.Pair) JoinAdder

SmoothKnot adds a standard smooth knot to a path (same as Knot(pr)). Part of builder functionality.

func (*Path) TensionCurve

func (path *Path) TensionCurve(t1, t2 float64) KnotAdder

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.

func (*Path) Z

func (path *Path) Z(i int) arithm.Pair

Z returns the knot at position (i mod N).

Interface HobbyPath.

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL