Drawing Apps With LivePanels

Copy Markdown View Source

Use this track when the canvas is primarily a whiteboard, sketch surface, annotation tool, diagramming app, review board, or drawing-heavy editor.

The shorter Add Drawing Tools guide shows how drawing support fits into a canvas. This track goes deeper: toolbars, cursor modes, browser drafts, committed vector items, vector item rendering, point editing, erasing, drawing-owned content elements, persistence, shared canvases, import/export, and recipes.

What LivePanels Gives A Drawing App

LivePanels gives drawing apps a server-authoritative canvas model:

  • in-progress vector items are browser-owned drafts;
  • completed drafts become drawing commands;
  • accepted drawing commands create canvas items;
  • committed vector items render in the world vector layer;
  • drawing elements use the same item placement model;
  • selection, history, clipboard, persistence, and shared mode keep working because drawings become canvas state.

Your application still owns the product:

  • the toolbar and palette;
  • which tools are available;
  • selected-vector-item inspectors;
  • import/export screens;
  • annotation meaning;
  • document, asset, or domain records associated with drawings;
  • any custom SVG or HTML presentation around vector items and elements.

The Drawing App Loop

A drawing app usually follows this loop:

use the drawing toolkit
  -> render a canvas runtime
  -> render application toolbar controls
  -> set browser cursor mode and draft style
  -> let the browser own the active draft
  -> commit the finished draft as a drawing command
  -> render committed vector items from canvas state
  -> edit, erase, persist, copy, and share committed vector items as items

The important part is that drawing does not become a separate client-side document store. The drawing surface is a capability on the canvas.

Minimum Drawing Canvas

Start with the drawing toolkit:

defmodule MyAppWeb.SketchLive do
  use MyAppWeb, :live_view

  use LivePanels.Canvas,
    pubsub: MyApp.PubSub,
    domains: [:drawing]

  use LivePanels.Toolkit.Drawing

  @default_style %{
    color: "#111827",
    width: 3,
    fill_color: "none",
    stroke_style: "solid",
    opacity: 100,
    roundness: 0
  }

  @impl true
  def mount(_params, _session, socket) do
    {:ok,
     socket
     |> assign(:cursor_mode, :drag)
     |> assign(:draw_tool, :pen)
     |> assign(:draw_style, @default_style)}
  end

  @impl true
  def render(assigns) do
    ~H"""
    <main class="sketch">
      <nav class="sketch-toolbar">
        <button phx-click={set_cursor_mode_js(@livepanels.canvas_id, :drag, push: "set_cursor_mode")}>
          Select
        </button>

        <button
          phx-click={
            set_cursor_mode_js(@livepanels.canvas_id, :draw,
              tool: :pen,
              style: @draw_style,
              push: "set_cursor_mode"
            )
          }
        >
          Pen
        </button>

        <button
          phx-click={
            set_cursor_mode_js(@livepanels.canvas_id, :draw,
              tool: :rectangle,
              style: @draw_style,
              push: "set_cursor_mode"
            )
          }
        >
          Rectangle
        </button>

        <button phx-click={set_cursor_mode_js(@livepanels.canvas_id, :erase, push: "set_cursor_mode")}>
          Erase
        </button>
      </nav>

      <.canvas_runtime {canvas_runtime_attrs(assigns, cursor_mode: @cursor_mode)} />
    </main>
    """
  end

  @impl true
  def handle_event("set_cursor_mode", %{"mode" => mode} = params, socket) do
    cursor_mode =
      case mode do
        "draw" -> :draw
        "erase" -> :erase
        "marquee" -> :marquee
        _ -> :drag
      end

    draw_tool =
      case Map.get(params, "tool") do
        "pen" -> :pen
        "freehand" -> :pen
        "rectangle" -> :rectangle
        "arrow" -> :arrow
        "line" -> :line
        "polyline" -> :polyline
        "ellipse" -> :ellipse
        "diamond" -> :diamond
        "triangle" -> :triangle
        "text" -> :text
        _ -> socket.assigns.draw_tool
      end

    {:noreply,
     socket
     |> assign(:cursor_mode, cursor_mode)
     |> assign(:draw_tool, draw_tool)}
  end
end

The runtime renders committed vector items automatically. The toolbar tells the browser what to do next.

Track Map

Read the guides in this order when building a drawing app:

  • Tools, Cursor Modes, And Drafts for the browser interaction model.
  • Commit Vector Items To Canvas State for vector item creation and server commands.
  • Render And Style Vector Items for the vector layer and application presentation.
  • Edit Points, Labels, Text, And Arrowheads for vector item inspectors and geometry editing.
  • Erase, Select, Move, And Resize for item behavior around committed vector items.
  • Images, Frames, Embeds, And Drawing Elements for drawing-owned content.
  • Persist, Share, Import, And Export for recovery and collaboration.
  • Drawing App Recipes for common application patterns.

Drawing And Other Capabilities

Drawing can coexist with ordinary item content, graph connectors, shared mode, and custom application UI because committed drawings are canvas items.

For example, a review app can use vector items for annotations, ordinary content items for comments, shared mode for collaboration, and graph connectors for relationships between frames. LivePanels does not need a separate runtime for each capability. The application decides which capabilities are useful for the product.

What To Keep Out Of The Core Runtime

Do not put product meaning into the drawing runtime.

A rectangle might mean a highlight, a frame, a bounding box, a selected region, or a step in a diagram. LivePanels only needs to know that a committed vector item has kind, placement, points, style, and vector state.

Keep domain records in your app. Use LivePanels to make the drawing behavior consistent.