Drawing Paths
Doodle provides another type of Image
that makes particular use of sequences.
Paths
represent arbitrary shapes created using sequences of pen movements:
val image = OpenPath(List(
MoveTo(Vec(0, 0).toPoint),
LineTo(Vec(100, 0).toPoint),
LineTo(Vec(50, 100).toPoint),
BezierCurveTo(Vec(50, 0).toPoint, Vec(0, 100).toPoint, Vec(0, 0).toPoint)
))
// image: Path = // ...
image.draw
Pen movements come in three varieties:
MoveTo(point)
---move the pen topoint
without drawing a line;
LineTo(point)
---move the pen topoint
drawing a straight line;
BezierCurveTo(cp1, cp2, point)
---move the pen topoint
drawing a bezier curve---cp1
andcp2
are "control points" determining the shape of the curve.
The arguments in each case are objects of type Vec
, which are 2D vectors representing x,y
points (the name Vector
is already taken by the scala.collection.Vector
class). There are various ways we can create and transform Vecs
:
Code Result Description Example
-------------------------- --------- ------------------------------- -----------------------------------
Vec(num, num)
Vec
Create a vector using Vec(3, 4)
x,y
coordinates
Vec.polar(angle, length)
Vec
Create a vector using Vec.polar(30.degrees, 100)
polar coordinates
Vec.zero
Vec
A zero vector (0,0
) Vec.zero
Vec.unitX
Vec
A unit X vector (1,0
) Vec.unitX
Vec.unitY
Vec
A unit Y vector (0,1
) Vec.unitY
vec * num
Vec
Multiply vec
by num
Vec(2, 1) * 10
vec / num
Vec
Divide vec
by num
Vec(20, 10) / 10
vec + vec
Vec
Add vectors Vec(2, 1) + Vec(1, 3)
vec - vec
Vec
Subtract vectors Vec(5, 5) - Vec(2, 1)
vec rotate angle
Vec
Rotate anticlockwise by angle
Vec(5, 5) rotate 45.degrees
vec.x
Double
Get the X component of vec
Vec(3, 4).x
vec.x
Double
Get the Y component of vec
Vec(3, 4).y
vec.length
Double
Get the length of vec
Vec(3, 4).length`
We can use these operations to create paths quickly by adding vectors. Notice how we start the shape with a MoveTo
element (all paths implicitly start at the origin). This is a very common pattern.
val elements = (0 to 360 by 36).map { angle =>
val point = (Vec.unitX * 100) rotate angle.degrees toPoint
val element =
if(angle == 0)
MoveTo(point)
else
LineTo(point)
element
}
// elements: scala.collection.immutable.IndexedSeq[doodle.core.PathElement] = // ...
val decagon = OpenPath(elements)
// decagon: doodle.core.Path = // ...
decagon.draw
Exercise: My God, It's Full of Stars!
Let's use this pattern to draw some stars.
For the purpose of this exercise let's assume that a star is a polygon with p
points.
However, instead of connecting each point to its neighbours,
we'll connect them to the nth
point around the circumference.
For example, the diagram below shows stars with p=11
and n=1 to 5
.
n=1
produces a regular polygon while
values of n
from 2
upwards produce stars with increasingly sharp points:
Write code to draw the diagram above.
Start by writing a method to draw a star
given p
and n
:
def star(p: Int, n: Int, radius: Double): Image =
???
Create the points for your star using ranges and Vec.polar
:
Use your choice of recursion and beside
or iteration and allBeside
to create the row of stars.
<div class="solution">
Here's the star
method. We've renamed p
and n
to points
and skip
for clarity:
def star(sides: Int, skip: Int, radius: Double) = {
val centerAngle = 360.degrees * skip / sides
val elements = (0 to sides) map { index =>
val point = Vec.polar(centerAngle * index, radius)
if(index == 0)
MoveTo(point)
else
LineTo(point)
}
Path(elements) strokeWidth 2
}
We'll use allBeside
to create the row of stars.
We only need to use values of skip
from 1
to sides/2
rounded down:
(allBeside((1 to 5) map { skip =>
star(sides, skip, 100)
})).draw
</div>
When you've finished your row of stars,
try constructing a larger image from different values of p
and n
.
Here's an example:
<div class="solution">
To create the image above, we started by adding colours
and a chunkier outline to the definition of star
:
def star(sides: Int, skip: Int, radius: Double) = {
val centerAngle = 360.degrees * skip / sides
val elements = (0 to sides) map { index =>
val point = Vec.polar(centerAngle * index, radius).toPoint
if(index == 0)
MoveTo(point)
else
LineTo(point)
}
OpenPath(elements).
strokeWidth(2).
strokeColor(Color.hsl(centerAngle, 1.normalized, .25.normalized)).
fillColor(Color.hsl(centerAngle, 1.normalized, .75.normalized))
}
The updated scene then becomes:
allAbove((3 to 33 by 2) map { sides =>
allBeside((1 to sides/2) map { skip =>
star(sides, skip, 20)
})
})
</div>