mathk
mathk

Reputation: 8143

Design a drawing api for Canvas

I am adding some function to threepenny ui api. I would like to had the ability to draw line with canvas.

The function that I can write have the following signature:

moveTo :: Vector -> UI ()
lineTo :: Vector -> UI ()
stroke :: UI ()
strokeStyle :: UI ()
beginPath :: UI ()

Each primitive moveTo and lineTo should happen in between a beginPath .. stroke call. How would you enforce the beginPath ... stroke sequence. By design I would like to give the user no choice for drawing lines. So the user is not aware of the beginPath ... stroke sequence.

Upvotes: 3

Views: 228

Answers (2)

luqui
luqui

Reputation: 60503

Here's how I would design a canvas API.

newtype Drawing = ...
instance Monoid Drawing where ... -- for combining drawings

line :: Vector -> Vector -> Drawing
path :: [Vector] -> Drawing
withStyle :: Style -> Drawing -> Drawing
runDrawing :: Drawing -> UI ()

Here the functions operate on semantically meaningful objects (from the user's perspective), rather than imperative commands. This should be implementable with the type

newtype Drawing = Drawing (UI ())

however sometimes subtleties will require that the type have a bit more structure, so be open to that (e.g. Something -> UI ()).

Upvotes: 1

jberryman
jberryman

Reputation: 16645

It's definitely good to design your API so that it can't be used improperly. One approach you might take here is to create an un-exported wrapper that lets you control how these particular actions are composed (I haven't tried to run this, sorry):

-- Don't export constructor
newtype Line a = Line { runLine :: UI a }

-- Wrap the return types in your current implementation with Line, for:
moveTo :: Vector -> Line ()
lineTo :: Vector -> Line ()
...

instance Monad Line where
         (Line ui) >>= f = Line (ui >>= \a-> beginPath >> (runLine $ f a))
         return = Line . return

A couple other points:

  1. You may want to use a Monoid instance instead, if your API doesn't need to bind any values (i.e. all of your line API functions end in -> Line ()

  2. If you need to do something like wrap the entire sequence of composed line actions in e.g. start and end actions or whatever, you could further extend the above with

    runLine (Line ui) = start >> ui >> end
    

Upvotes: 2

Related Questions