The Evolution of Dragons

by Ian Macphail on April 22, 2015 No comments

One of the new features introduced in the upcoming LiveCode version 8 release is the ability to create self-contained custom controls called widgets. These widgets are written in a variant of LiveCode called LiveCode Builder (LCB) and have full control over their appearance and behaviour.

In a similar way to Hanson’s previous blog post on fractals, I’ll be showing you how to draw a fractal shape. However this time, I’ll show you how to do it with a custom widget control that can draw the fractal for us.

dragon

The dragon curve is a fractal that begins with a single line segment. It is built up by replacing each line section with two lines joined by a right angle, alternately turning left and right for each segment. Repeated iterations of this process produce fractals with double the number of line segments for each iteration:

When the LiveCode engine requires a widget to draw itself, it will call the widget’s OnPaint handler. Within this handler, the widget has access to a canvas, which acts as a target for drawing commands. If any changes to the widget would alter its appearance (for instance, setting a color property), the widget can request the engine redraw with the “redraw all” command.

We’ll be drawing the fractal by first creating a Path value that defines the fractal shape, setting up the canvas paint and transform. Then we’ll draw the path to the canvas.

The OnPaint handler:


-- called whenever LiveCode needs to redraw the widget
public handler OnPaint()
	// draw the background
	set the paint of this canvas to mBackgroundPaint
	fill rectangle path of my bounds on this canvas
	
	// create the path if needed
	updatePath()
	
	// scale the canvas so that the fractal fills the bounds of the widget
	variable tScaleX
	variable tScaleY
	
	variable tBounds
	put the bounding box of mFractalPath into tBounds
	
	put my width / the width of tBounds into tScaleX
	put my height / the height of tBounds into tScaleY
	
	scale this canvas by [tScaleX, tScaleY]
	
	// draw the fractal
	set the paint of this canvas to mFractalPaint
	stroke mFractalPath on this canvas
end handler

The canvas has two ways of drawing a path – fill or stroke:

The canvas fill command covers the whole area enclosed by a path with the current paint

The canvas stroke command covers the line of a path with the current paint. The shape of the line covered can be modified by setting different canvas properties (stroke width, dashes, join style, and cap style)

The canvas paint describes how to fill in an area of the canvas. The simplest type of paint is a solid color, but it can also be a pattern created from an image, or a gradient.

In this example, the variables mBackgroundPaint and mFractalPaint have been initialised within the widget’s OnCreate handler. This handler is called when a new copy of this widget is created.

The OnCreate handler:


-- called when widget is created
public handler OnCreate()
	-- paints
	put solid paint with color [0,0,0] into mFractalPaint
	put solid paint with color [1,1,1] into mBackgroundPaint
	
	-- paths
	put nothing into mFractalPath
	
	-- iterations
	put 10 into mIterations
end handler

The variable that will contain the fractal shape is a Path. A path is a list of points and instructions describing a series of lines that form a shape. Paths can be combined with other paths or modified with path commands. In this example we will build up the path by adding lines of unit length. To do this we use the “line to on ” command.

Rather than replacing line segments at each iteration, we can add the segments of the final iteration with a recursive handler. On each iteration, the handler will work out the new direction for the next two line segments and call itself recursively, passing in the new direction. When the handler reaches the final iteration, a line will be added to the path at that point in that direction.


-- direction values represent one of eight compass points: North, NorthEast, East, SouthEast, South, SouthWest, West, NorthWest
-- add unit line to path in the given direction
private handler addLine(in pDirection as Integer) returns nothing
-- note that the diagonal vectors here are longer than the vertical+horizontal ones.
-- This could be fixed but it’s not important here as the fractal produced will have either all diagonal or all non-diagonal lines so the lines in the fractal will have all the same length.
	variable tVector
	put element pDirection of [point [1,0], point [1,1], point [0,1], point [-1,1], point [-1,0], point [-1,-1], point [0,-1], point [1,-1]] into tVector
	
	put point [the x of mCurrentPoint + the x of tVector, the y of mCurrentPoint + the y of tVector] into mCurrentPoint
	line to mCurrentPoint on mFractalPath
end handler

-- return direction that is 1 turn to the left
private handler turnLeft(in pDirection as Integer)
	return (pDirection + 7) wrap 8
end handler

-- return direction that is 1 turn to the right
private handler turnRight(in pDirection as Integer)
	return (pDirection + 1) wrap 8
end handler

private handler fractalIterate(in pIteration as Integer, in pDirection as Integer, in pRight as Boolean)
	if pIteration > 0 then
		if pRight then
			fractalIterate(pIteration - 1, turnRight(pDirection), true)
			fractalIterate(pIteration - 1, turnLeft(pDirection), false)
		else
			fractalIterate(pIteration - 1, turnLeft(pDirection), true)
			fractalIterate(pIteration - 1, turnRight(pDirection), false)
		end if
	else
		addLine(pDirection)
	end if
end handler
	
private handler updatePath() returns nothing
	-- test if we need to create the fractal path
	if mFractalPath is not nothing then
		return
	end if
	
	put the empty path into mFractalPath
	put point [0,0] into mCurrentPoint
	
	fractalIterate(mIterations, 1, true)

	-- translate path so top-left of bounds is (0,0)
	variable tBounds
	put the bounding box of mFractalPath into tBounds
	
	translate mFractalPath by [- the left of tBounds, - the top of tBounds]
end handler
Ian MacphailThe Evolution of Dragons

Join the conversation

*