graphics-ts: Functional Bindings for the HTML 5 Canvas API 
Tags: functional, javascript, typescript
Published: 2020-06-05 19:24:05

In this post, I will describe the functional design of `graphics-ts`, a part of the `fp-ts` [ecosystem](https://gcanti.github.io/fp-ts/ecosystem/) that provides functional bindings for the HTML 5 Canvas API. 

For those unfamiliar with `fp-ts`, I encourage you to read @gcanti's excellent series on Getting starting with fp-ts.

{% post https://dev.to/gcanti/getting-started-with-fp-ts-setoid-39f3 %}

### The HTML 5 Canvas API

As described by the [Mozilla Developer Network Web Docs](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API): 

> The Canvas API provides a means for drawing graphics via  JavaScript and the HTML `<canvas>` element. Among other things, it can be used for animation, game graphics, data visualization, photo manipulation, and real-time video processing.

Here is a basic example of using the Canvas API to [draw a house](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D#Basic_example): 

##### Html

```html
<canvas id="my-house" width="300" height="300"></canvas>
```

##### JavaScript

```javascript
const draw = () => {
  const canvas = document.getElementById('my-house')
  
  if (canvas) {
    const context = canvas.getContext('2d')
    // Set line width
    context.lineWidth = 10;
    // Wall
    context.strokeRect(75, 140, 150, 110);
    // Door
    context.fillRect(130, 190, 40, 60);
    // Roof
    context.beginPath();
    context.moveTo(50, 140);
    context.lineTo(150, 60);
    context.lineTo(250, 140);
    context.closePath();
    context.stroke();
  }
}
```

##### Output
![House](https://dev-to-uploads.s3.amazonaws.com/i/ssr03aeuafe081fml41g.png)

As you can see, rendering to a `<canvas>` element is imperative by nature and involves repeated mutation of the canvas `context`.

### Functional Programming and the Canvas API

### The Canvas Module

In `graphics-ts`, operations made against the canvas are modeled by the `IO` type class. 

From [Getting started with fp-ts](https://dev.to/gcanti/getting-started-with-fp-ts-io-36p6):

> In `fp-ts` a synchronous effectful computation is represented by the IO type, which is basically a thunk, i.e. a function with the following signature: `() => A`.

By representing canvas operations as instances of `IO`, we are pushing evaluation of the canvas to the boundary of our program's execution. This is because an `IO` is just a value which *represents* an effectful computation, so in order to execute any side effect you must execute the `IO` action.

#### Getting Access to the Canvas

Before we can start drawing to a `<canvas>` element, we need to get a reference to it, as well as to its `context`. 

```typescript
export const unsafeGetCanvasElementById: (id: string) => HTMLCanvasElement = (id) =>
  document.getElementById(id) as HTMLCanvasElement

export const unsafeGetContext2D: (canvas: HTMLCanvasElement) => CanvasRenderingContext2D = (c) =>
  c.getContext('2d') as CanvasRenderingContext2D
```

But there is a problem here - these operations are not running in the `IO` context. To solve this, we can `lift` these functions into the `IO` context. 

```typescript
import * as IO from 'fp-ts/lib/IO'

export const getCanvasElementById: (id: string) => IO.IO<O.Option<HTMLCanvasElement>> = (id) => () => {
  const canvas = unsafeGetCanvasElementById(id)
  return canvas instanceof HTMLCanvasElement ? O.some(canvas) : O.none
}

export const getContext2D: (canvas: HTMLCanvasElement) => IO.IO<CanvasRenderingContext2D> = (c) =>
  IO.of(unsafeGetContext2D(c))
```

#### Abstracting Canvas Operations

Now we can start working on implementing the rest of our API.

Example (canvas dimensions)

```typescript
import * as IO from 'fp-ts/lib/IO'

export const getWidth: (canvas: HTMLCanvasElement) => IO.IO<number> = (c) => () => c.width

export const setWidth: (width: number) => (canvas: HTMLCanvasElement) => IO.IO<void> = (w) => (c) => () => {
  c.width = w
}

export const getHeight: (canvas: HTMLCanvasElement) => IO.IO<number> = (c) => () => c.height

export const setHeight: (height: number) => (canvas: HTMLCanvasElement) => IO.IO<void> = (h) => (c) => {
c.height = h


export interface CanvasDimensions {
  readonly width: number
  readonly height: number
}

export const getDimensions: (canvas: HTMLCanvasElement) => IO.IO<CanvasDimensions> = (c) =>
  sequenceS(IO.io)({ height: getHeight(c), width: getWidth(c) })

export const setDimensions: (dimensions: CanvasDimensions) => (canvas: HTMLCanvasElement) => IO.IO<void> = (d) => (c) =>
  pipe(
    c,
    setWidth(d.width),
    IO.chain(() => pipe(c, setHeight(d.height)))
  )
```

Example (stroke a path)

```typescript
export const strokePath: <A>(
  f: (ctx: CanvasRenderingContext2D) => IO.IO<A>
) => (ctx: CanvasRenderingContext2D) => IO.IO<A> = (f) => (ctx) =>
  pipe(
    ctx,
    beginPath,
    IO.chain(() => f(ctx)),
    IO.chain((a) =>
      pipe(
        ctx,
        stroke(),
        IO.map(() => a)
      )
    )
  )
```

#### Refactoring our Domain Model

If we examine our API as we continue, we will notice that almost all functions have the following signatures:

`HTMLCanvasElement`

```typescript
(canvas: HTMLCanvasElement) => IO.IO<A>
```

`CanvasRenderingContext2D`

```typescript
(ctx: CanvasRenderingContext2D) => IO.IO<CanvasRenderingContext2D>
```

Essentially, we are *reading* from an `HTMLCanvasElement` or `CanvasRenderingContext2D` and returning a type `A` wrapped in an `IO`.

So we could say that when managing `HTMLCanvasElement` we are yielding an `Html` effect, when managing `CanvasRenderingContext2D` we are yielding a `Render` effect, and when managing `CanvasGradient` we are yielding a `Gradient` effect. 

We can model these effects using the `Reader` module from `fp-ts`.

```typescript
import * as R from 'fp-ts/lib/Reader'

export interface Html<A> extends R.Reader<HTMLCanvasElement, IO.IO<A>> {}

export interface Render<A> extends R.Reader<CanvasRenderingContext2D, IO.IO<A>> {}

export interface Gradient<A> extends R.Reader<CanvasGradient, IO.IO<A>> {}
```

So our examples from above become the following:

Example (canvas dimensions)

```typescript
export const getWidth: Html<number> = (c) => () => c.width

export const setWidth: (width: number) => Html<HTMLCanvasElement> = (w) => (c) => () => {
  c.width = w
  return c
}

export const getHeight: Html<number> = (c) => () => c.height

export const setHeight: (height: number) => Html<HTMLCanvasElement> = (h) => (c) => () => {
  c.height = h
  return c
}

export const getDimensions: Html<CanvasDimensions> = (c) =>
  sequenceS(IO.io)({ height: getHeight(c), width: getWidth(c) })

export const setDimensions: (dimensions: CanvasDimensions) => Html<HTMLCanvasElement> = (d) => (ctx) =>
  pipe(ctx, setWidth(d.width), IO.chain(setHeight(d.height)))
```

However, if we continue to examine our code we will see that in many instances we are manually threading the `ctx` through our API. 

Example (stroke a path)

```typescript
export const strokePath: <A>(f: Render<A>) => Render<A> = (f) => (ctx) =>
  pipe(
    ctx,
    beginPath,
    IO.chain(() => f(ctx)),
    IO.chainFirst(() => pipe(ctx, stroke()))
  )
```

Example (preserve the canvas context):

```typescript
export const withContext: <A>(f: Render<A>) => Render<A> = (f) => (ctx) =>
  pipe(
    save(ctx),
    IO.chain(() => f(ctx)),
    IO.chainFirst(() => restore(ctx))
  )
```

From [Getting started with fp-ts](https://dev.to/gcanti/getting-started-with-fp-ts-reader-1ie5):

> The purpose of the Reader monad is to **avoid** threading arguments through multiple functions in order to only get them where they are needed.

What if we could simply chain `Render` effects? We would need a `Monad` instance of `Render`. We know that `Render` admits a `Monad` instance because theory tells us that

```typescript
Reader<R, M<A>>
```

admits a `Monad` instance for any effect `M` as long as `M` admits a `Monad` instance. In our case we have

```typescript
Reader<R, IO<A>>
```

and, since `IO` admits a `Monad` instance, we know that 

```typescript
Render<A> = Reader<CanvasRenderingContext2D, IO<A>> 
```

admits a `Monad` instance too.

To create our `Monad` instance of `Render`, we can utilize the `ReaderIO` module from `fp-ts-contrib`.

```typescript
ReaderIO<R, A> = Reader<R, IO<A>>
```

So our effect models now become the following

```typescript
import * as R from 'fp-ts-contrib/lib/ReaderIO'

export interface Gradient<A> extends R.ReaderIO<CanvasGradient, A> {}

export interface Html<A> extends R.ReaderIO<HTMLCanvasElement, A> {}

export interface Render<A> extends R.ReaderIO<CanvasRenderingContext2D, A> {}
```

and we can refactor `strokePath` and `withContext` from above to

```typescript
export const strokePath: <A>(f: Render<A>) => Render<A> = (f) =>
  pipe(
    beginPath,
    R.chain(() => f),
    R.chainFirst(() => stroke())
  )

export const withContext: <A>(f: Render<A>) => Render<A> = (f) =>
  pipe(
    save,
    R.chain(() => f),
    R.chainFirst(() => restore)
  )
```

#### Putting it all Together

Using the `Canvas` module from `graphics-ts`, we can rewrite our example of rendering a house from above as

```typescript
import { error } from 'fp-ts/lib/Console'
import * as R from 'fp-ts-contrib/lib/ReaderIO'
import * as C from 'graphics-ts/lib/Canvas'
import * as S from 'graphics-ts/lib/Shape'
import { pipe } from 'fp-ts/lib/pipeable'

const canvasId = 'my-house'

const wall = C.strokeRect(S.rect(75, 140, 150, 110))

const door = C.fillRect(S.rect(130, 190, 40, 60))

const roof = C.strokePath(
  pipe(
    C.moveTo(S.point(50, 140)),
    R.chain(() => C.lineTo(S.point(150, 60))),
    R.chain(() => C.lineTo(S.point(250, 140)))
  )
)

C.renderTo(canvasId, () => error(`[ERROR]: Unable to find canvas with id ${canvasId}`))(
  pipe(
    // Set line width
    C.setLineWidth(10),
    R.chain(() => wall),
    R.chain(() => door),
    R.chain(() => roof)
  )
)
```

### The Drawing Module

`graphics-ts` also provides another layer of abstraction on top of the HTML 5 Canvas API through the `Drawing` module. 

The `Drawing` module abstracts away the repetitive calls to the HTML Canvas API that are required when using the `Canvas` module directly and allows for composition of different shapes, styles, and drawings.

If we refactor the example of rendering a house from above one last time using the `Drawing` module, we get the following

```typescript
import { error } from 'fp-ts/lib/Console'
import * as M from 'fp-ts/lib/Monoid'
import * as RA from 'fp-ts/lib/ReadonlyArray'
import * as R from 'fp-ts-contrib/lib/ReaderIO'
import * as C from 'graphics-ts/lib/Canvas'
import * as Color from 'graphics-ts/lib/Color'
import * as D from 'graphics-ts/lib/Drawing'
import * as S from 'graphics-ts/lib/Shape'
import { pipe } from 'fp-ts/lib/pipeable'

const canvasId = 'my-house'

const wall = D.outline(
  S.rect(75, 140, 150, 110),
  M.fold(D.monoidOutlineStyle)([D.lineWidth(10), D.outlineColor(Color.black)])
)

const door = D.fill(S.rect(130, 190, 40, 60), D.fillStyle(Color.black))

const roof = D.outline(
  S.path(RA.readonlyArray)([S.point(50, 140), S.point(150, 60), S.point(250, 40)]),
  D.outlineColor(Color.black)
)

C.renderTo(canvasId, () => error(`[ERROR]: Unable to find canvas with id ${canvasId}`))(
  D.render(D.many([wall, door, roof]))
)
```

### Repository

I encourage anyone who is interested in experimenting with the HTML 5 Canvas API to give the library a try!

{% github gcanti/graphics-ts no-readme %} 

### Acknowledgements

I would like to thank @gcanti for giving me the opportunity to work on this rewrite as well as for providing teaching and guidance on use of functional programming theory and the `fp-ts` ecosystem.