Turtle is one of the most powerful tools in TPVector. It
implements turtle graphics,
where you can draw by issuing commands like forward
, right
, curve
, and
more. The ability to create complex shapes consisting of lines is highly
compatible with the cutting capabilities of a laser cutter.
This tutorial explains the most important features of Turtle, from the simple figures, to the more complex control structures.
Just like all the classes in TPVector, Turtle is immutable. To make sure you are familiar with the concept, and the consequences, see the Immutability document for more information, and in particular the Turtle section.
A single Turtle corresponds to a single
<path>
element in the SVG. The path is suitable for use in a cut run, but it is also
possible to give it attributes
(fill,
stroke-width
or any other attribute supported by <path>
), and place it in a print run. See
Layers and runs for more information.
A Turtle is by default created at the origin (point [0, 0]
), but different
coordinates can be specified. The Turtle is initially pointing upwards, in the
direction of the negative Y half-axis, which corresponds to the angle of 0.
Rotation clockwise (right) increases the angle.
The Turtle can have its pen down, meaning it is on, or up, meaning it is off and does not draw. The draw commands only produce output with the pen down - if it is up, they only update the position and angle of the Turtle, but don't create any lines.
Methods:
penDown
andpenUp
- set the pen to the desired state; can also take a boolean argument to set the state based on a condition.withPenDown
andwithPenUp
- execute the provided function (TurtleFunc) with pen up or down, as specified, and then restore the previous pen state; can also take a boolean argument to set the state based on a condition.
Note that the pen does not have color. The attributes like color or opacity can
be changed using setAttributes
(inherited from Piece), but the color itself
does not make much sense for a laser anyway.
The most basic methods of Turtle are:
forward
andback
- move the Turtle forward or back, drawing a line if the pen is down, or just updating the position if the pen is up.right
andleft
- rotate the Turtle in place by the specified angle (in degrees), by default 90°.turnBack
- rotate by 180°.strafeRight
andstrafeLeft
- move the Turtle sideways, without changing its orientation.
Code
Turtle.create()
.left().forward(3)
.right().forward(1).strafeRight(1).forward(0.5).strafeLeft(1).forward(0.5)
.right().forward(3)
.penUp().forward(0.3).left(155).back(0.2).penDown()
.forward(3.4).left(85).forward(1.5)
goTo
- move to the given absolute coordinates (draw a line if the pen is down)jumpTo
- jump to the given absolute coordinates (never draw a line)setAngle
- sets the absolute direction of the Turtle (see Coordinates)lookUp
,lookDown
,lookRight
,lookLeft
- set the absolute angle so that the Turtle faces the given directionlookAt
- set the direction towards the specified absolute coordinates
Code
Turtle.create([-1, 0])
.repeat(18, t => t
.arcRight(20, 1)
.branch(t => t
.lookAt([1.5, 0.5])
.forward(0.5)
.lookUp()
.forward(0.1)
)
)
There is a large selection of method for drawing circle and ellipse arcs:
arcRight
andarcLeft
- draw a circle arc, over the specified angle and radiusroundCornerRight
androundCornerLeft
- draw a rounded 90° corner, with the specified radiihalfEllipseRight
andhalfEllipseLeft
- draw half of an ellipse, with the specified radiicircle
andellipse
- draw a circle or an ellipse centered at the current position.
Code
Turtle.create()
.circle(0.2)
.arcRight(45, 1).forward(Math.sqrt(2)).arcRight(45, 1)
.forward(1).ellipse(0.5, 1).forward(1)
.roundCornerRight(3, 1)
.forward(1)
.halfEllipseRight(1, 7)
Turtle can draw the quadratic and cubic
Bézier curves
supported by the SVG <path>
element.
The most elementary function for drawing a Bézier curve is curveTo
. The start
of the curve is defined by this
Turtle, the target is passed as an argument,
and the curve control points are found by executing forward
command on the
start Turtle, and back
command on the target Turtle, with certain parameter
values (possibly negative). With these assumptions, the following curves can be
drawn:
- Quadratic curve (the default): As it has only one control point, so it must lie on the intersection of the lines defined by the start and the target Turtles, and is found automatically.
- Cubic curve: The two control points are defined by
startSpeed
andtargetSpeed
, namely the control points arethis.forward(startSpeed).pos
andtarget.back(targetSpeed).pos
. Any of the speeds can be specified as"auto"
(the default), which means that the intersection point will be used, just like for quadratic curve. Note however, that{speed: "auto"}
(or equivalent{}
) is still different from a quadratic cube, because it still uses two control points.
Note: The default is a quadratic curve, which might give unexpected results
when the start and target Turtles are parallel or almost parallel, when the
intersection point is far away from them. In that case, just specify
{speed: 1}
and tweak until the result is as needed.
Other methods for drawing Bézier curves:
curve
- often the most comfortable method to use, accepts a function and draws a curve from the current position to the result of the function, discarding any path the function might have drawnsmoothRight
andsmoothLeft
- draw a (quadratic by default) curve from the current position to another point on the edge of an imagined circle the Turtle lies on, pointing to its centre.curveFromPop
andcurveFromPeek
- executepop
orpeek
and draw a curve from that point to the current position.
(The imagined circles for smoothRight
and smoothLeft
methods are also
drawn.)
Code
// Starting on the left side.
Turtle.create()
.curve(t => t.forward(1).right(80).forward(0.5))
// Store this point.
.push()
.curve(t => t.lookRight().forward(4).strafeLeft(1), {speed: 2})
.curve(t => t.right(5).forward(4).strafeRight(1), {speed: 2})
// Draw an additional curve from the stored point.
.curveFromPop({speed: 3})
.curve(t => t.forward(3).right(120).forward(2))
// Illustrate the imagined circle on which
// the start and end of a smooth turn lies.
.branch(t => t.left().arcRight(360, 1)).smoothRight(55, 1)
.forward(7)
.branch(t => t.left().arcRight(360, 1)).smoothRight(80, 1)
// Go back to start.
.curveTo(Turtle.create(), {speed: 1})
The Turtle has a stack, or, more precisely, a collection of named stacks, where it can save its position, angle and/or pen state.
Each of the stack-related functions can accept the stack key - a string or a number identifying a stack. Stacks don't need to be declared beforehand, just push to a stack with any key. Without the stack key, the methods operate on the default stack.
push*
- push the current state, or the specified subset thereof, to the specified stack.pop
- remove the state from the specified stack, and apply it to the Turtle. If this modifies the position, a line is never drawn, regardless of the pen state. If a partial state was pushed, only that part of the state is applied.peek
- apply the state from the stack just likepop
, but don't remove it from the stack.
Stacks often come in handy, but sometimes it's easier to use one of the other mechanisms, like branches, or storing a Turtle in a variable (some examples in the Immutability document).
repeat
- execute the given function multiple times, passing the result of each call to the next callbranch
- execute the given function, keep whatever lines it generated, and finally restore the original state of the Turtlebranches
- execute the given function multiple times in a branch (the state of the Turtle is reset after each call)
Code
Turtle.create().right()
.repeat([2, 3, 5, 7], (t, numFeathers) => t
.forward(1.5)
.branch(t => t.right().forward((numFeathers + 1) * 0.2))
.branches(numFeathers, (t, j) => t
.left((j + 1) * 20)
.forward(0.8)
.circle(0.1)
)
)
closePath
- close the path (see theZ
command in Path); note that the result is no longer a Turtle, but rather a Path (which is another subclass of Piece)dropPath
- forget the path drawn by this Turtle, and only retain its state and stacks; this is useful in some advanced scenarioscopy*
- copy the state, or the specified subset thereof, from another Turtle; this is useful in some advanced scenarios
TurtleFunc is a function that takes a Turtle (and optionally some more
parameters) and returns a Turtle. A TurtleFunc can be considered a building
block, encoding a reusable part of Turtle's path. For example, the wave
function represents a wavy line going forward by ` unit:
const wave: TurtleFunc = t => t
.curveTo(t.forward(0.5).left(70), {speed: 0.3})
.curveTo(t.forward(1), {speed: 0.3});
Or equivalent:
function wave(t: Turtle) {
return t
.curveTo(t.forward(0.5).left(70), {speed: 0.3})
.curveTo(t.forward(1), {speed: 0.3});
}
The function can be used (and reused) for example like this:
Turtle.create().right().andThen(wave).forward(0.5).andThen(wave)
A parametrised version of the function
Let's make the size of the wave a parameter:
const wave: TurtleFunc<[number?]> = (t, len = 1) => t
.curveTo(t.forward(len / 2).left(70), {speed: 0.3 * len})
.curveTo(t.forward(len), {speed: 0.3 * len});
Or equivalent:
function wave(t: Turtle, len = 1) {
return t
.curveTo(t.forward(len / 2).left(70), {speed: 0.3 * len})
.curveTo(t.forward(len), {speed: 0.3 * len});
}
Now let's use the function:
Turtle.create().right().andThen(wave).andThen(wave, 2).andThen(wave)
Of course the speed could be made into a separate parameter, with 0.3 * len
as
the default, to create even more interesting results.
Another use, a pentagon with wavy sides:
Turtle.create().right(18).repeat(5, t => t.andThen(wave).right(144))
A TurtleFunc can be passed to some of Turtle's methods, like branch
, repeat
,
withPenDown
or curve
.
Turtle extends the Piece class, which means that all the
methods like transform
, setLayer
, setAttributes
etc. are available on it,
however note that they must be called last, after all the drawing calls, because
the result is no longer a Turtle, but rather only a Piece.
A single Turtle can also be used multiple times, e.g. in different scales, with different attributes, and on different layers, because it is immutable. See the Immutability document for more information.