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.

Shapes

First, we will start with a set of functions, or combinators that form a language that describes shapes on the two-dimensional plane.

Shapes as Boolean Functions

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

Rendering Shapes

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 Strings to Shapes.

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)
                                                  
                                                  
                                                  
                                                  
        ##################################        
        ##################################        
        ##################################        
        ##################################        
        ##################################        
        ##################################        
        ##################################        
        ##################################        
        ##################################        
        ##################################        
        ##################################        
        ##################################        
        ##################################        
        ##################################        
        ##################################        
        ##################################        
        ##################################        
                                                  
                                                  
                                                  
                                                  

Signals

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

Animations

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.

Playing Animations

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"

Demos

With the animation player in place, test it and experiment with demos. For example:

Have fun :-)