Drawing support lets users create committed canvas vector items: freehand strokes, polylines, arrows, lines, rectangles, ellipses, diamonds, triangles, and text.

This guide is the short integration path. For a full drawing-app track, read Drawing Apps With LivePanels after this guide.

The browser owns in-progress drafts so the pointer feels immediate. When the gesture is finished, the browser sends one drawing command. The server validates the payload, creates a canvas item, and projects it like other canvas state.

That gives vector items normal canvas behavior: move, resize, select, remove, persist, undo, redo, copy, paste, and share.

Use The Drawing Toolkit

Import the drawing toolkit on drawing-capable canvases:

defmodule MyAppWeb.SketchLive do
  use MyAppWeb, :live_view

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

  use LivePanels.Toolkit.Drawing
end

The drawing preset includes canvas helpers plus drawing controls, style JS helpers, vector item rendering helpers, erase attrs, and point-edit attrs.

Add Drawing Mode Controls

Use set_cursor_mode_js/3 to switch the browser cursor mode:

<button
  phx-click={
    set_cursor_mode_js(@livepanels.canvas_id, :draw,
      tool: :freehand,
      style: %{
        color: "#1f2937",
        width: 3,
        fill_color: "none",
        stroke_style: "solid",
        opacity: 100,
        roundness: 0
      }
    )
  }
>
  Freehand
</button>

<button
  phx-click={
    set_cursor_mode_js(@livepanels.canvas_id, :draw,
      tool: :rectangle,
      style: %{
        color: "#1f2937",
        width: 2,
        fill_color: "#dbeafe",
        stroke_style: "solid",
        opacity: 100,
        roundness: 8
      }
    )
  }
>
  Rectangle
</button>

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

If the application mirrors cursor mode in LiveView assigns, pass the mode back into the runtime component:

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

The browser still owns the active draft. The application mirror is for visible toolbar state and LiveView patch consistency.

Update Drawing Style

Use set_draw_style_js/3 to change browser-owned draft style without changing mode:

<button
  phx-click={
    set_draw_style_js(@livepanels.canvas_id, %{
      color: "#dc2626",
      width: 4,
      fill_color: "none",
      stroke_style: "dashed",
      opacity: 100,
      roundness: 0
    })
  }
>
  Red dashed
</button>

For committed vector items, use update_style_attrs/3:

<button
  :if={@selected_vector_item}
  {update_style_attrs(@selected_vector_item, %{
    color: "#2563eb",
    width: 3,
    fill_color: "none",
    stroke_style: "solid",
    opacity: 100,
    roundness: 0
  })}
>
  Blue
</button>

Draft style and committed vector item style are separate because a draft is not canvas state until it is committed.

Create Vector Items From Server Code

Use LivePanels.Drawing.vector_item/4 for server-authored vector items, imports, tests, and derived annotations:

socket =
  LivePanels.Drawing.vector_item!(
    socket,
    :line,
    %{
      space: :world,
      anchor: :top_left,
      x: 120,
      y: 80,
      w: 240,
      h: 160
    },
    points: [%{x: 0, y: 0}, %{x: 240, y: 160}],
    style: %{
      color: "#444444",
      width: 3,
      fill_color: "none",
      stroke_style: "solid",
      opacity: 100,
      roundness: 0
    }
  )

Text vector items use the same item placement model:

socket =
  LivePanels.Drawing.vector_item!(socket, :text, placement,
    text: "Review this",
    style: %{
      color: "#111827",
      width: 1,
      fill_color: "none",
      stroke_style: "solid",
      opacity: 100,
      roundness: 0
    }
  )

The command creates an item with vector state. It does not append SVG directly to the DOM.

Committed vector items set interaction: %{promote_on_focus: false}. Selecting or focusing a vector item keeps the canvas stack order intact, so drawing layers can coexist with graph nodes and LiveView-backed items. Use normal z-order selection commands when a user chooses to move vector items forward or back.

Render Committed Vector Items

The standard canvas_runtime/1 renders committed vector items in the world vector layer. The :item slot can stay focused on HTML item frames:

<.canvas_runtime {canvas_runtime_attrs(assigns)}>
  <:item :let={context}>
    <section class="item-frame">
      <header {drag_handle_attrs()}>{context.item.label}</header>
      {context.content}
      <.resize_handles :if={context.view.resizable?} />
    </section>
  </:item>
</.canvas_runtime>

For manual render loops, use the manual render helpers:

<LivePanels.Toolkit.Integration.Render.svg_vector_item
  id={"vector-item-#{context.item.id}"}
  item={context.item}
  selected={context.selected?}
  active={context.focused?}
/>

Applications can also render vector items manually from LivePanels.Drawing.vector_state/1 when they need a custom SVG contract.

Edit Points And Remove Vector Items

Point-backed vector items such as freehand, polyline, arrow, and line store points in vector state. Render point handles when selected:

<circle
  :for={{point, index} <- Enum.with_index(points)}
  cx={point.x}
  cy={point.y}
  r="5"
  {vector_point_handle_attrs(item, index)}
/>

Use erase target attrs on the SVG geometry you want erase mode to recognize:

<path
  d={path}
  {erase_vector_item_target_attrs(item)}
/>

Or render a normal removal control:

<button {erase_attrs(item)}>Remove vector item</button>

The server verifies that the target is a committed vector item before removing it.

Flatten Dense Vector Item Sets

For dense drawing workflows, flatten committed vector items into one canvas item:

socket = LivePanels.Drawing.flatten!(socket, selected_vector_item_ids)

The flattened item renders as one unit. Its vector state keeps each source vector item as a serialized source item record in the flattened item's local coordinate space, so the runtime can act on the flattened item as one object until it is restored:

socket = LivePanels.Drawing.unflatten!(socket, flattened_vector_item)

Applications can also run threshold-based flattening after commits or on their own schedule:

socket =
  LivePanels.Drawing.flatten_over_limit!(socket, 250,
    scope: :viewport
  )

Supported scopes are :all, :owner, :viewport, and :selection.

Add Content With Normal Items

Drawing tools create vector items. For screenshots, remote references, nested panels, dashboards, or app-specific content, declare a normal item type and use the standard item command path:

<button {add_item_attrs(:reference)}>
  Reference
</button>

That item can still be selected, moved, resized, connected with ports, or given vector capability state when the application wants it to participate in drawing workflows. The drawing toolkit does not create a separate content-item path.

Persistence, History, Clipboard, And Shared Mode

Committed vector items are canvas items. Because of that:

  • moving and resizing vector items uses normal item commands;
  • selection can include vector items;
  • history can undo vector item creation and edits;
  • layouts can restore vector items;
  • clipboard can copy and paste vector items;
  • shared canvases can coordinate committed vector item changes.

Browser drafts are intentionally not canonical state. In shared canvases the runtime can broadcast throttled draw intent for live peer previews, while the accepted vector_item command remains the source of durable canvas state. Pending draw commits are kept client-side until the command ack resolves, with a runtime option for app-provided draft persistence.

What LivePanels Owns

LivePanels owns drawing command validation, vector item state, committed vector item creation, draft-to-command protocol, drawing attrs, neutral vector item rendering helpers, persistence/history participation, and shared coordination.

Your application owns the toolbar, palette, selected-vector-item inspector, SVG styling, product labels, and any domain data attached to drawings.