Skip to content
John Horigan edited this page May 25, 2020 · 2 revisions

A path declaration creates a new primitive shape. The declaration contains a number of moving and drawing operations that define a path (or several paths). These paths can be filled or stroked to actually draw the new primitive shape.

path box {
    MOVETO( 0.5,  0.5)
    LINETO(-0.5,  0.5)
    LINETO(-0.5, -0.5)
    LINETO( 0.5, -0.5)
    CLOSEPOLY()     // go back to (0.5, 0.5) and close the path
    STROKE()[]      // draw a line on the path using the default line width (10%)
}

Like shapes, paths can have parameters:

path flower(number petals, number filled) {
  MOVETO(cos(-180/petals), sin(-180/petals))
  loop petals [r (360/petals)]
    ARCTO(cos(180/petals), sin(180/petals), 
          0.4, 0.4 + 0.2 * (petals - 5), 90)
  CLOSEPOLY(CF::Align)
  if (filled) FILL[a -0.5]
  MOVETO(0.65, 0)
  ARCTO(-0.65, 0, 0.65, CF::ArcCW)
  ARCTO( 0.65, 0, 0.65, CF::ArcCW)
  CLOSEPOLY()
  FILL[a -0.5]
}

Path Operations

Path Operation Description
MOVETO(x, y) Moves the current position to the point (x, y) without drawing, begins a new path sequence
LINETO(x, y) Draws from the current position to the point (x, y)
ARCTO(x, y, x_radius, y_radius, ellipse_angle, flags) draws an elliptical arc segment to the point (x, y), the ellipse has a radius (rx, ry) and is rotated by the ellipse angle (in degrees). The flags argument is optional.
ARCTO(x, y, radius, flags) draws a circular arc segment with the specified radius to the point (x, y). The flags argument is optional.
CURVETO(x, y, CF::Continuous) draw a smooth quadratic bezier curve to point (x, y) with a control point that is the mirror of the previous bezier curve †
CURVETO(x, y, x1, y1) draw a quadratic bezier curve to point (x, y) with a control point at (x1, y1)
CURVETO(x, y, x2, y2, CF::Continuous) draw a smooth cubic bezier curve to point (x, y) with a starting control point that is the mirror of the ending control point of the previous bezier curve and an ending control point at (x2, y2) †
CURVETO(x, y, x1, y1, x2, y2) draw a cubic bezier curve to point (x, y) with a starting control point at (x1, y1) and an ending control point at (x2, y2)
CLOSEPOLY() Closes the path sequence by joining the end-point with the beginning of the sequence. If the end-point matches the beginning then they are simply joined (no end-caps if stroked). If the end-point does not match the beginning then there is an implicit LINETO to the beginning, followed by a join.
CLOSEPOLY(CF::Align) Closes the path sequence. The end-point is changed to match the beginning if they do not match already. Useful if rounding errors cause the beginning and end to be different when your intent is for them to be the same

† The smooth forms of the quadratic and cubic curve operations infer the unspecified control point by looking at the preceding curve operation. If the preceding operation is not a curve operation (CURVETO, CURVEREL, ARCTO, or ARCREL) then a smooth curve operation is not permitted.

Relative Variants

The path operations all have relative variants: MOVEREL(), LINEREL(), ARCREL(), and CURVEREL(). In the relative path operations the end point (or bezier control points) are specified relative to the current position instead of in absolute terms.

Arc Flags

The basic arc drawing operation specifies a start point, an end point, and an ellipse. The ellipse is positioned such that the start point and end points touch the ellipse and the arc is drawn from the start to the finish. However, in general there are two possible ellipse positions for any pair of starting and ending points, and two different arcs on each ellipse that can be drawn. The CF::ArcCW and CF::ArcLarge flags indicate which of the four possible arcs are drawn.

Arc types

Two of the four arcs are large, i.e., more than 180°. Specifying the CF::ArcLarge flag indicates that one of these arcs should be drawn. Otherwise one of the arcs that are less than 180° will be drawn. Two of the four arcs draw from start to end clockwise around the ellipse and two draw counter-clockwise. Specifying the CF::ArcCW flag indicates that a clockwise arc should be drawn. Otherwise a counter-clockwise arc will be drawn.

Setting the radius of the arc to be negative has the effect of inverting the arc drawing direction. A counter-clockwise arc will be drawn clockwise if the radius is negative. A clockwise arc will be drawn counter-clockwise if the radius is negative.

Bezier Control Points

The control points for bezier curve segments control the slope of the curve at the ends. For cubic bezier curves each end has its own control point and the slope at each end is indicated by the slope of the line from the end to the control point. For quadratic bezier curves both ends share a single control point and the slope at each end is indicated by the slope of the line from each end to the shared control point.

Bezier types

For smooth cubic bezier curves the starting control point is the mirror of the ending control point on the previous bezier curve or arc curve. For smooth quadratic bezier curves the single control point is the mirror of the ending control point on the previous bezier curve or arc curve. The preceding curve does not need to be of the same order (quadratic or cubic) as the smooth curve. The preceding curve can even be an ARCTO. Context Free will figure out what control point will match the slope and curvature between the smooth curve and the curve the precedes it.

Path Commands

After a path sequence there can be one or more path commands. Path commands instruct Context Free to draw (stroke or fill) all of the path sequences between the path command and the previous group of path commands.

Path Command Description
STROKE(width, flags) [shape adjustments] Stroke the preceding path sequences with a pen of the specified width. Stroke width is relative to the size of the shape. If the stroke size is not specified then the default of 0.1 is used. If color or shape modifications are specified then the path sequences are modified when they are drawn. The flags argument is also optional.
FILL(flags) [shape adjustments] Fill the preceding path sequences. If color or shape modifications are specified then the path sequences are modified when they are drawn. If the path sequences are intersecting or self-intersecting then a filling rule determines whether a given piece is filled or not. The default filling rule is non-zero, but even-odd filling can also be specified (see below). The flags argument is optional.

Stroke Flags

Stroke Flag Description
CF::MiterJoin The join between path sequence segments (and between the beginning and end of closed paths) should have miter joins.
CF::RoundJoin The join between path sequence segments (and between the beginning and end of closed paths) should have round joins.
CF::BevelJoin The that join between path sequence segments (and between the beginning and end of closed paths) should have bevel joins.

Join types

Stroke Flag Description
CF::ButtCap The end points of unclosed path sequences should have butt caps.
CF::RoundCap The end points of unclosed path sequences should have round caps.
CF::SquareCap The end points of unclosed path sequences should have square caps.

Cap types

Stroke Flag Description
CF::IsoWidth Indicates that a transformed stroke is transformed before stroking, instead of afterward. This makes the stroke width the same for all orientations. If the stroke has been isomorphically transformed (no skews, no size x y) then this makes no difference. If the stroke has been anisomorphically transformed then the stroke width will vary unless the CF::IsoWidth flag is provided.
path iso {
  ARCTO(0, 1, 0.5)
  ARCTO(0, 0, 0.5)
  CLOSEPOLY()
  STROKE() [s 2 0.5]
  STROKE(CF::IsoWidth) [s 2 0.5 y -0.75]
}

Iso types

Fill Flags

Fill Flag Description Examples
The default filling rule is non-zero Non-zero filling
CF::EvenOdd This flag activates the even-odd filling rule Even/Odd filling

Note that the shape adjustments in path commands can either be basic or ordered (see basic vs. ordered).

If a path is completed (by a closing curly brace, '}') with no path command following the last set of path sequences then an implicit fill command is appended to the path.

Control Flow

Paths support all of the control flow elements that shapes support: loops, if statements, switch statements, variables, and transform blocks.

Subpaths

Paths can invoke other paths. The path sequences from the invoked path are inserted directly into the current path. This is useful for including the same path sequence into multiple paths. Also, subpath invocations can be passed to a path as a shape parameter, but only if the the subpath contains only path operations. Context Free cannot silently drop small path sequences the way that it can silently drop small shapes. If you manage to make a very, very large path sequence you will crash Context Free.

path star {
    MOVETO(0,1)
    loop 4 [r 144]
        LINETO(cos(234), sin(234))
    CLOSEPOLY()
}

path stars {
    loop 5 [r 72]
        path star[y ((sqrt(5)+1)/2)]
    STROKE[]
}

Note that SQUARE, CIRCLE, and TRIANGLE are built-in paths and can be included as subpaths.

path ring {
    path CIRCLE[]
    STROKE[]
}