Drawing starts in the browser.
The server owns committed canvas state, but the browser owns the active pointer gesture. That split is what makes drawing feel immediate without turning every pointer move into a LiveView event.
In shared canvases the browser sends throttled draw intent while the gesture is
active so peers can see live draft previews. The server still treats the final
add_vector_item command as the durable source of truth. The browser keeps
a pending draw commit locally until that command ack resolves; applications can
replace or disable the default draft persistence through
createLivePanelsCanvasHook runtime options.
Use The Drawing Toolkit
Drawing canvases should import the drawing toolkit:
use LivePanels.Canvas,
pubsub: MyApp.PubSub,
domains: [:drawing]
use LivePanels.Toolkit.DrawingThe drawing preset includes the canvas preset plus drawing controls and viewport JS helpers:
set_cursor_mode_js/3;set_draw_style_js/3;- drawing element attrs;
- committed vector item style attrs;
- erase target attrs;
- point handle attrs.
Cursor Modes
The browser runtime recognizes these cursor modes:
| Mode | Purpose |
|---|---|
:drag | Select, drag, and resize committed canvas items. |
:marquee | Draw a selection marquee. |
:pan | Pan the viewport. |
:draw | Create a drawing draft with the selected tool. |
:erase | Remove committed vector items through erase targets. |
Set a mode with set_cursor_mode_js/3:
<button phx-click={set_cursor_mode_js(@livepanels.canvas_id, :drag)}>
Select
</button>
<button phx-click={set_cursor_mode_js(@livepanels.canvas_id, :marquee)}>
Marquee
</button>
<button phx-click={set_cursor_mode_js(@livepanels.canvas_id, :erase)}>
Erase
</button>Drawing Tools
When the mode is :draw, pass a drawing tool and a complete style map:
<button
phx-click={
set_cursor_mode_js(@livepanels.canvas_id, :draw,
tool: :arrow,
style: %{
color: "#111827",
width: 3,
fill_color: "none",
stroke_style: "solid",
opacity: 100,
roundness: 0
}
)
}
>
Arrow
</button>Supported drawing tools are:
:pen;:freehand;:polyline;:arrow;:line;:rectangle;:ellipse;:diamond;:triangle;:text.
pen is the toolbar-friendly name for freehand drawing.
Draft Style
Drawing style is explicit. The style map must include all fields:
%{
color: "#2563eb",
width: 4,
fill_color: "none",
stroke_style: "solid",
opacity: 100,
roundness: 0
}stroke_style must be "solid", "dashed", or "dotted". opacity is an
integer from 0 through 100. roundness is a non-negative integer.
Update 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>Draft style affects the current and next browser draft. It does not restyle already committed vector items unless the application also sends committed vector item updates.
Mirror Mode In LiveView Assigns
The browser can own mode and style without any LiveView assigns. Most products still mirror them so toolbar buttons can show active state.
Pass push: "event_name" to append a LiveView event after the browser dispatch:
<button
phx-click={
set_cursor_mode_js(@livepanels.canvas_id, :draw,
tool: :rectangle,
style: @draw_style,
push: "set_cursor_mode"
)
}
>
Rectangle
</button>Handle the event in the application:
def handle_event("set_cursor_mode", params, socket) do
params = Map.get(params, "value", params)
cursor_mode =
case params["mode"] do
"draw" -> :draw
"erase" -> :erase
"marquee" -> :marquee
"pan" -> :pan
_ -> :drag
end
{:noreply, assign(socket, :cursor_mode, cursor_mode)}
endPass the mirrored mode into the runtime:
<.canvas_runtime {canvas_runtime_attrs(assigns, cursor_mode: @cursor_mode)} />This keeps LiveView patches from snapping visible toolbar state back to an old mode.
Mirror Style In LiveView Assigns
Style controls use the same pattern:
<button
phx-click={
set_draw_style_js(@livepanels.canvas_id, Map.put(@draw_style, :color, "#16a34a"),
push: "set_draw_style"
)
}
>
Green
</button>Handle the pushed style:
def handle_event("set_draw_style", style, socket) do
{:noreply, assign(socket, :draw_style, normalize_draw_style(style))}
endIf your product applies palette changes to selected committed vector items, do that explicitly in the same event. Draft style and committed vector item style are separate state.
Marquee Selection
Drawing canvases often need both drag selection and marquee selection. Pass the
marquee attrs through canvas_runtime_attrs/2:
<.canvas_runtime
{canvas_runtime_attrs(
assigns,
marquee_selection_attrs(:items, :overlap, cursor_mode: @cursor_mode)
)}
/>Use :overlap when touching the selection rectangle should select an item. Use
:contain when the item must be fully enclosed.
Draft Lifecycle
During a drawing gesture:
- the browser creates and updates a draft locally;
- the canvas renders the draft immediately;
- the finished gesture becomes one drawing command;
- the server validates the command;
- the accepted command creates a committed canvas item.
The active draft is not persisted, copied, undone, redone, or broadcast to other participants. The committed vector item is.
That boundary keeps collaboration and persistence quiet while the pointer is moving.