An installer for GHC can be downloaded from its home page or as part of the Haskell Platform. For reference, the full documentation is available that can be searched through Hoogle.
In this lab session, we are going to implement two little languages, the language of shapes, and the language of signals, and combine them into animations.
First, we will start with a set of functions, or combinators that form a language that describes shapes on the two-dimensional plane.
Define the Shape
type that represents each of the shapes as a predicate function that takes a two-dimensional point and tells if it is inside the shape or not. The points on the plane are captured by the first-order Point
type. That is basically a pair of the two coordinates, each of those given by Double
values.
Note that Shape
should be an abstract type that hides the fact that it contains a function inside.
type Point = (Double, Double)
Shape :: *
Shapes are interpreted by the inside
function that takes a point and tests it with a shape.
inside :: Point -> Shape -> Bool
We will split shapes into two main categories. There will be primitives that are defined directly as Shape
-typed objects, and there will be shapes that can be constructed from other shapes with specific combinators.
Let us start with the primitives first, and give the definitions for the following shapes.
Define a shape that has no points.
empty :: Shape
For example:
(0,0) `inside` empty == False
(1,1) `inside` empty == False
(-1,-1) `inside` empty == False
Define a shape that represents a unit circle centered at the origin.
circle :: Shape
For example:
(0,0) `inside` circle == True
(2,0) `inside` circle == False
(-0.25,-0.25) `inside` circle == True
Define a shape that represents a unit square centered at the origin.
square :: Shape
For example:
(0,2) `inside` square == False
(0.5,0.5) `inside` square == True
(0,0) `inside` square == True
Now continue with the combinators.
Define a function for creating the union of two shapes, that is merging them into a single shape.
union :: Shape -> Shape -> Shape
Define a function for creating the intersection of two shapes.
intersect :: Shape -> Shape -> Shape
Define a function for inverting a shape. That is actually the complementer image of the shape, so every point that was in inside the original shape shall not be the point of the derived one and vice versa.
invert :: Shape -> Shape
Define a function for subtracing shapes from each other. Note that this can be expressed as taking the intersection with the inverted version of the shape to subtract.
difference :: Shape -> Shape -> Shape
The following functions rely on point transformations, that is, they should take a shape and transform all of its points in the described way. For translation and scaling, we introduce a type synonym for vectors in the two-dimensional space, and treat them as a pair of Double
values:
type Vector = (Double, Double)
Note that they are actually represented in the same way as they were points. For the sake of simplicity, we will not make this difference clear (and therefore our program safer), but it would be possible to do so in Haskell.
Define a function for translating shapes. It takes a translation vector (that has the amount of translation in each dimension) and the shape to be translated, and it creates a translated shape.
translate :: Vector -> Shape -> Shape
For example:
(1,1) `inside` (translate (1,1) empty) == False
(1,1) `inside` (translate (1,1) circle) == True
(2,2) `inside` (translate (1,1) circle) == False
Define a function for scaling shapes. It takes a scale vector (that has the amount of scaling factors in each of the dimensions) and the shape to be scaled, and it creates a scaled shape.
scale :: Vector -> Shape -> Shape
For example:
(1,1) `inside` (scale (2,2) square) == True
(0.5,0.5) `inside` (scale (0.5,0.1) square) == False
(-0.1,-0.05) `inside` (scale (0.5,0.1) square) == True
Define a function for rotating shapes clockwise. It takes the angle of rotation, which is simply represented as a Double
value, and it should be measured in radians:
type Angle = Double
And here is the type signature for the function itself:
rotate :: Angle -> Shape -> Shape
For example:
(0,0) `inside` (rotate pi circle) == True
(0.5,0.5) `inside` (rotate pi circle) == False
(0.5,0.5) `inside` (rotate pi square) == False
So far we had to make use of our imagination in order to verify if we have defined the previous primitives and functions properly. But it would be much better if we could see how the actual shapes look like. Thanks to the functional representation, it is worthwhile to note that we obtained a lossless format that resembles to the Scalable Vector Graphics (SVG).
All we need is another set of functions for rendering shapes. There are many ways to do that, but for now, we will stick to textual images, so the following functions should assign String
s to Shape
s.
Before we start with the actual rendering, introduce a type for the canvas, where the drawings will be presented. A canvas is described by three properties:
For the simplicity, we will again use a type synonym for that:
type Canvas = (Point, Point, (Int, Int))
Define a default value for the canvas. The top left corner should be the point (-1.5, 1.5)
, and bottom right corner should be the point (1.5, -1.5)
, and the resolution in both dimensions should be 25
pixels.
defaultCanvas :: Canvas
Define a function that takes a point and a shape, and returns a full pixel if the point is inside the shape, and an empty pixel otherwise. For the correct aspect, let full pixels be represented as the "##"
constant, while let empty pixels be two consecutive spaces.
getPixel :: Point -> Shape -> String
For example:
getPixel (0,0) circle == "##"
getPixel (1,1) circle == " "
getPixel (0,0) empty == " "
For rendering the complete shape, we should be able to lay down a raster grid on the two-dimensional plane. As a step towards this, define a function that generates a list that contains all the division points in a closed interval specified by the number of divisions. This will give us the samples for the given interval:
samples :: Double -> Double -> Int -> [Double]
For example:
samples 0 0 10 == []
samples (-1.5) 1.5 4 == [-1.5,-0.5,0.5,1.5]
samples 0 1 9 == [0.0,0.125,0.25,0.375,0.5,0.625,0.75,0.875,1.0]
Finally, compose the function that produces the textual image for the whole shape. The algorithm of rendering is as follows. Take all the points of the canvas, which is given as the first parameter, and test each of them for being inside the shape, and map them to the respective pixel values.
renderShape :: Canvas -> Shape -> String
Note that each of the rows on the canvas correspond to lines of a text, and they should be delimited by line breaks, that is the \n
character. With the help of this, a single String
value could be computed at the end that contains the complete image. GHCi will be able to show them if the putStr
action is used for printing them.
Hint. Useful functions: unlines
, concat
, and ++
.
For example:
putStr (renderShape ((-0.75,0.75),(0.75,-0.75),(25,25)) (scale (0.5,0.5) circle))
##
##############
######################
##########################
##########################
##############################
##############################
##############################
##################################
##############################
##############################
##############################
##########################
##########################
######################
##############
##
putStr (renderShape ((-0.75,0.75),(0.75,-0.75),(25,25)) square)
##################################
##################################
##################################
##################################
##################################
##################################
##################################
##################################
##################################
##################################
##################################
##################################
##################################
##################################
##################################
##################################
##################################
The second language is about signals, that is values that may change over time. The time is represented by the Double
type, thus we create a synonym for that:
type Time = Double
Similarly to shapes, signals are also represented by functions. In this case the function is to tell the value of the signal at the given moment of time. It should be polymorphic, so Signal
is going to be a higher-order type:
Signal :: * -> *
Define the time
constant that always returns the value of the current time.
time :: Signal Time
Signals can be interpreted by computing their values at a given time. Define the at
function that takes a signal and a time value and determines the value of the signal at the moment.
at :: Signal a -> Time -> a
For example:
time `at` 0 == 0
time `at` 2 == 2
time `at` 5 == 5
The definition of signals can be kept quite simple and minimal if we employed functors to make it possible to use regular functions to work with signals.
Functors are higher-order types of one parameter that implement the Functor
type class, whose definitions is as follows. (It is not necessary to include this in the source or import any module, it comes with the Prelude
. Its definition is listed here for the presentation.)
class Functor (f :: * -> *) where
fmap :: (a -> b) -> f a -> f b
That is, we should have an fmap
function that takes an arbitrary function from type a
to type b
, and the applies it to a functorial type that is of type a
to obtain a wrapped value of type b
.
Hint. The Functor
class could be extended to the Signal
type by the following skeleton:
instance Functor Signal where
fmap = ...
For example:
(fmap (+1) time) `at` 0 == 1
(fmap (*10) time) `at` 2 == 20
Functors are easier and more intuitive to use if they are also applicative functors. Such functors are described by the Applicative
type class from the Control.Applicative
module
class Functor f => Applicative f where
pure :: a -> f a
(<*>) :: f (a -> b) -> f a -> f b -- infixl 4
The Applicative
class has two methods. The pure
is a function that converts a regular value to a functorial one. For signals, this means that it creates a constant signal out of a simple value.
For example:
[ pure 1 `at` t | t <- [0..4] ] == [1,1,1,1,1]
[ pure 'a' `at` t | t <- [0..4] ] == "aaaa"
The <*>
function helps with curried application of regular multi-parameter functions over the functorial values. It can be nicely used together with the infix synonym of fmap
from Control.Applicative
, called <$>
.
For example:
((+) <$> time <*> time) `at` 4 == 8
((+) <$> time <*> pure 10) `at` 4 == 14
((+) <$> (pure 1) <*> (pure 2)) `at` 0 == 3
((+) <$> (pure 1) <*> (pure 2)) `at` 5 == 3
When shapes are combined with signals, we get animations. That is, animations are shapes that can change over time. For playing such animations, we will need a canvas to show each of the states, and a time of start and end -- everything else is served by the previously implemented functor architecture.
The player function is an IO action:
playAnimation :: Canvas -> Time -> Time -> Signal Shape -> IO ()
This action can be implemented along the following lines. Pick points in time of regular intervals between the start and stop time (that is where the samples
function may come handy again), and render the animation at those points.
Note that for the better result, the System.Console.ANSI
module from the ansi-terminal
Cabal package is recommended to use. (It is platform-independent.) This can be installed with the following command in the OS shell:
$ cabal install ansi-terminal
Once the installation is complete, the GHCi must be restarted for the changes to take effect, and the module become visible.
This module exports actions such as clearScreen
that clears the screen of the terminal, and setCursorPosition
that can be used to reposition the cursor to the top left corner of the screen, so the rendering could be started from there. Furthermore, the threadDelay
action is also recommended from the Control.Concurrent
module, which is to pause the evaluation for a given amount of time.
Another useful combinator to use is the forM_
action from the Control.Monad
module. It basically simulates a for
loop: it takes a list that contains the values for the loop variable and passes each of them to its second parameter, an action that represents the body of the loop.
For example, here is a program that animates a ticker:
ticker :: IO ()
ticker = do
forM_ (take 100 $ cycle ["\\", "|", "/", "-"]) $ \k -> do
clearScreen
setCursorPosition 0 0
putStr k
threadDelay $ 10^6 `div` 10
putStr "\n"
With the animation player in place, test it and experiment with demos. For example:
Create an animation of a bouncing ball.
Create an animation of a rotating box.
Create an animation that is the difference of a bouncing ball and a rotating square.
Have fun :-)