# `Easel.LiveView`
[🔗](https://github.com/jasonstiebs/easel/blob/v0.3.4/lib/easel/live_view.ex#L2)

A Phoenix LiveView component for rendering and drawing on an HTML canvas.

## Setup

No JavaScript build step is required. The hook is colocated and injected
at runtime automatically.

If you use colocated hooks elsewhere in your app, ensure your LiveSocket
merges them:

    import {hooks as colocatedHooks} from "phoenix-colocated/my_app"

    const liveSocket = new LiveSocket("/live", Socket, {
      hooks: {...colocatedHooks},
      // ...
    })

## Usage

Render a canvas element in your LiveView template:

    <Easel.LiveView.canvas id="my-canvas" width={300} height={300} />

Then draw to it from any event handler:

    def handle_event("draw", _, socket) do
      canvas =
        Easel.new(300, 300)
        |> Easel.set_fill_style("blue")
        |> Easel.fill_rect(0, 0, 100, 100)
        |> Easel.render()

      {:noreply, Easel.LiveView.draw(socket, "my-canvas", canvas)}
    end

You can also pass initial ops to draw on mount:

    <Easel.LiveView.canvas id="my-canvas" width={300} height={300} ops={@canvas.ops} />

## Clearing

To clear the canvas before drawing:

    {:noreply, Easel.LiveView.clear(socket, "my-canvas")}

Or clear and draw in one step:

    {:noreply, Easel.LiveView.draw(socket, "my-canvas", canvas, clear: true)}

## Events

Enable mouse and keyboard events by setting the corresponding attributes.
Events are sent as LiveView events with the canvas id as a prefix:

    <Easel.LiveView.canvas
      id="my-canvas"
      width={300}
      height={300}
      on_click
      on_mouse_move
      on_key_down
    />

Then handle them in your LiveView:

    def handle_event("my-canvas:click", %{"x" => x, "y" => y}, socket) do
      # ...
      {:noreply, socket}
    end

    def handle_event("my-canvas:mousemove", %{"x" => x, "y" => y}, socket) do
      # ...
      {:noreply, socket}
    end

    def handle_event("my-canvas:keydown", %{"key" => key}, socket) do
      # ...
      {:noreply, socket}
    end

Available event attributes: `on_click`, `on_mouse_down`, `on_mouse_up`,
`on_mouse_move`, `on_key_down`.

## Layers

Use `canvas_stack/1` to layer multiple canvases. Each layer is an
independent `<canvas>` element stacked via CSS. Only layers whose
assigns change get re-patched — static layers like backgrounds are
sent once:

    <Easel.LiveView.canvas_stack id="game" width={800} height={600}>
      <:layer id="background" ops={@background.ops} />
      <:layer id="sprites" ops={@sprites.ops} templates={@sprites.templates} />
      <:layer id="ui" ops={@ui.ops} />
    </Easel.LiveView.canvas_stack>

## Templates and Instances

For scenes with many similar shapes, define a template once and stamp
out instances. The template ops are cached client-side; only the
per-instance data (position, rotation, color) is sent each frame:

    canvas =
      Easel.new(800, 600)
      |> Easel.template(:boid, fn c ->
        c
        |> Easel.begin_path()
        |> Easel.move_to(12, 0)
        |> Easel.line_to(-4, -5)
        |> Easel.line_to(-4, 5)
        |> Easel.close_path()
        |> Easel.fill()
      end)
      |> Easel.instances(:boid, instances)
      |> Easel.render()

Pass templates alongside ops in your template:

    <Easel.LiveView.canvas id="c" ops={@canvas.ops} templates={@canvas.templates} ... />

## Animation

Start a server-side animation loop:

    def mount(_params, _session, socket) do
      socket =
        socket
        |> assign(:state, initial_state())
        |> assign(:canvas, Easel.new(600, 400) |> Easel.render())
        |> Easel.LiveView.animate("my-canvas", :state, fn state ->
          new_state = tick(state)
          canvas = render(new_state)
          {canvas, new_state}
        end, interval: 16, canvas_assign: :canvas)

      {:ok, socket}
    end

    def handle_info({:easel_tick, id}, socket) do
      {:noreply, Easel.LiveView.tick(socket, id)}
    end

The canvas is redrawn each tick via LiveView's normal rendering cycle.
The hook uses `requestAnimationFrame` to sync draws with the browser's
refresh rate — multiple server updates between frames are coalesced.

# `animate`

Starts a server-side animation loop that redraws a canvas at a fixed interval.

The `tick_fn` receives the current state and must return `{%Easel{}, new_state}`.
Each frame, the rendered ops are stored in a canvas assign so the template
re-renders and the hook redraws automatically.

Returns the socket with the animation state stored in assigns.

## Options

  * `:interval` - milliseconds between frames (default `16`, ~60fps)
  * `:canvas_assign` - assign key to store the rendered canvas
    (default: same as `state_key`). Your template should bind
    `ops={@canvas_assign_key.ops}` (or wherever you read ops from).

## Example

In your LiveView `mount/3`:

    def mount(_params, _session, socket) do
      initial = %{balls: [...], canvas: Easel.new(600, 400)}

      socket =
        socket
        |> assign(:state, initial)
        |> Easel.LiveView.animate("my-canvas", :state, fn state ->
          new_balls = tick(state.balls)
          canvas = render_balls(new_balls)
          {canvas, %{state | balls: new_balls, canvas: canvas}}
        end)

      {:ok, socket}
    end

Your LiveView must include a `handle_info` clause to receive ticks:

    def handle_info({:easel_tick, id}, socket) do
      {:noreply, Easel.LiveView.tick(socket, id)}
    end

To stop the animation:

    Easel.LiveView.stop_animation(socket, "my-canvas")

# `canvas`

Renders a `<canvas>` element wired to a colocated LiveView hook.

## Attributes

  * `id` (required) - unique DOM id, also used to target draw commands
  * `width` - canvas width in pixels
  * `height` - canvas height in pixels
  * `ops` - list of ops to draw (default `[]`). When this changes, the
    hook automatically clears and redraws.
  * `class` - CSS class for the canvas element
  * `on_click` - enable click events (pushes `"#{id}:click"`)
  * `on_mouse_down` - enable mousedown events (pushes `"#{id}:mousedown"`)
  * `on_mouse_up` - enable mouseup events (pushes `"#{id}:mouseup"`)
  * `on_mouse_move` - enable mousemove events (pushes `"#{id}:mousemove"`)
  * `mouse_move_fps` - optional max rate for mousemove push events (default: frame-synced)
  * `on_key_down` - enable keydown events (pushes `"#{id}:keydown"`)

Any additional attributes are passed through to the `<canvas>` element.

## Attributes

* `id` (`:string`) (required)
* `width` (`:integer`) - Defaults to `nil`.
* `height` (`:integer`) - Defaults to `nil`.
* `ops` (`:list`) - Defaults to `[]`.
* `templates` (`:map`) - Defaults to `%{}`.
* `class` (`:string`) - Defaults to `nil`.
* `on_click` (`:boolean`) - Defaults to `false`.
* `on_mouse_down` (`:boolean`) - Defaults to `false`.
* `on_mouse_up` (`:boolean`) - Defaults to `false`.
* `on_mouse_move` (`:boolean`) - Defaults to `false`.
* `mouse_move_fps` (`:integer`) - Defaults to `nil`.
* `on_key_down` (`:boolean`) - Defaults to `false`.
* Global attributes are accepted.

# `canvas_stack`

Renders a stack of layered canvases. Each layer is an independent
`<canvas>` element, stacked via CSS `position: absolute`. Only layers
whose ops change get redrawn — LiveView's normal diffing handles this.

## Example

    <Easel.LiveView.canvas_stack id="game" width={800} height={600}>
      <:layer id="background" ops={@background.ops} />
      <:layer id="sprites" ops={@sprites.ops} templates={@sprites.templates} />
      <:layer id="ui" ops={@ui.ops} />
    </Easel.LiveView.canvas_stack>

## Slots

Each `:layer` slot accepts:

  * `id` (required) — unique DOM id for this layer's canvas
  * `ops` — list of drawing operations
  * `templates` — map of template definitions (for instance rendering)
  * `on_click`, `on_mouse_down`, `on_mouse_up`, `on_mouse_move`, `on_key_down` — event flags
  * `mouse_move_fps` — optional max rate for mousemove push events on this layer

Only the topmost layer with event flags will receive pointer events.
Lower layers have `pointer-events: none` by default.

## Attributes

* `id` (`:string`) (required)
* `width` (`:integer`) (required)
* `height` (`:integer`) (required)
* `class` (`:string`) - Defaults to `nil`.
* Global attributes are accepted.
## Slots

* `layer` (required) - Accepts attributes:

  * `id` (`:string`) (required)
  * `ops` (`:list`)
  * `templates` (`:map`)
  * `on_click` (`:boolean`)
  * `on_mouse_down` (`:boolean`)
  * `on_mouse_up` (`:boolean`)
  * `on_mouse_move` (`:boolean`)
  * `mouse_move_fps` (`:integer`)
  * `on_key_down` (`:boolean`)

# `clear`

Clears the entire canvas.

# `draw`

Pushes draw operations to a canvas element on the client.

This uses `push_event` to send ops directly to the hook without
going through the normal render cycle. Useful for one-off draws
from event handlers.

## Options

  * `:clear` - if `true`, clears the canvas before drawing (default `false`)

# `export_button`

Renders an export button that downloads the canvas as a PNG image.

When clicked, converts the target canvas to a PNG and triggers a
browser file download.

## Attributes

  * `for` (required) — the DOM id of the canvas to export
  * `filename` — download filename (default `"canvas.png"`)
  * `class` — CSS class for the button

Any additional attributes are passed through to the `<button>` element.

## Example

    <Easel.LiveView.canvas id="my-canvas" width={300} height={300} ops={@ops} />
    <Easel.LiveView.export_button for="my-canvas" filename="drawing.png">
      Export PNG
    </Easel.LiveView.export_button>

## Attributes

* `for` (`:string`) (required)
* `filename` (`:string`) - Defaults to `"canvas.png"`.
* `class` (`:string`) - Defaults to `nil`.
* Global attributes are accepted.
## Slots

* `inner_block` (required)

# `stop_animation`

Stops a running animation.

# `tick`

Processes an animation tick. Call this from your `handle_info`:

    def handle_info({:easel_tick, id}, socket) do
      {:noreply, Easel.LiveView.tick(socket, id)}
    end

---

*Consult [api-reference.md](api-reference.md) for complete listing*
