These recipes show common drawing-app patterns. They are app-level patterns: LivePanels owns runtime drawing behavior, and your app owns product meaning.
Add A Viewport Toolbar Item
Toolbars can be ordinary viewport items:
[
drawing_tools:
{MyAppWeb.DrawingToolsLive,
label: "Drawing tools",
placement_space: :viewport,
placement_anchor: :top,
placement_lock: true,
reserved_area: %{edge: :top, policy: :avoid},
frame: :content,
persistent: false,
render: %{frame_overflow: :visible, stream_update: :canvas_state},
interaction: %{presentable: false, removable: false}}
]This keeps the toolbar inside the canvas runtime while excluding it from saved board content.
Build A Tool Button
<button
type="button"
class={["tool-button", @cursor_mode == :draw and @draw_tool == :arrow and "is-active"]}
phx-click={
set_cursor_mode_js(@livepanels.canvas_id, :draw,
tool: :arrow,
style: @draw_style,
push: "set_cursor_mode"
)
}
>
Arrow
</button>Mirror mode when the toolbar needs active state. Skip push: for controls that
do not need LiveView assign state.
Apply Style To Selected Vector Items
def handle_event("set_draw_style", params, socket) do
style = normalize_draw_style(params, socket.assigns.draw_style)
selection = LivePanels.Canvas.Read.selection(socket)
selected_vector_items =
socket
|> LivePanels.Canvas.Read.items()
|> Enum.filter(&LivePanels.Canvas.Selection.selected_item?(selection, &1.id))
|> Enum.filter(fn item ->
match?({:ok, _draw}, LivePanels.Drawing.vector_state(item))
end)
socket =
Enum.reduce(selected_vector_items, assign(socket, :draw_style, style), fn item, acc ->
LivePanels.Drawing.update_style!(acc, item, style)
end)
{:noreply, socket}
endUse this behavior when palette changes should restyle selected vector items and also set the style for the next draft.
Create A Callout Stamp
def handle_event("stamp-callout", _params, socket) do
box = %{x: 160, y: 120, w: 260, h: 120}
arrow = %{x: 60, y: 160, w: 120, h: 80}
socket =
LivePanels.Canvas.transaction!(socket, fn tx ->
tx
|> LivePanels.Drawing.vector_item!(:rectangle, box,
id: "callout-box",
style: %{color: "#f97316", width: 3, fill_color: "#ffedd5", stroke_style: "solid", opacity: 100, roundness: 10}
)
|> LivePanels.Drawing.vector_item!(:arrow, arrow,
id: "callout-arrow",
points: [%{x: 0, y: 0}, %{x: 120, y: 80}],
style: %{color: "#f97316", width: 3, fill_color: "none", stroke_style: "solid", opacity: 100, roundness: 0}
)
|> LivePanels.Canvas.Selection.select!(["callout-box", "callout-arrow"])
end)
{:noreply, socket}
endUse deterministic ids only when the stamp can appear once. Generate unique ids for repeatable stamps.
Add A Vector Item Inspector
defp assign_vector_item_inspector(socket) do
selection = LivePanels.Canvas.Read.selection(socket)
selected_vector_items =
socket
|> LivePanels.Canvas.Read.items()
|> Enum.filter(&LivePanels.Canvas.Selection.selected_item?(selection, &1.id))
|> Enum.flat_map(fn item ->
case LivePanels.Drawing.vector_state(item) do
{:ok, draw} -> [%{item: item, draw: draw}]
:error -> []
end
end)
assign(socket, :selected_vector_items, selected_vector_items)
endRender controls from the inspector:
<aside :if={@selected_vector_items != []} class="vector-item-inspector">
<section :for={selected <- @selected_vector_items}>
<p>{selected.draw.kind}</p>
<button {erase_attrs(selected.item)}>Remove</button>
</section>
</aside>Import An Excalidraw File
def handle_event("import_excalidraw", %{"content" => content}, socket) do
with {:ok, document} <- Jason.decode(content),
{:ok, [_ | _]} <- LivePanels.Drawing.excalidraw_commands(document) do
{:noreply, LivePanels.Drawing.import_excalidraw!(socket, document)}
else
_error ->
{:noreply, put_flash(socket, :error, "No supported drawing elements found.")}
end
endIn a real upload flow, read the uploaded file with LiveView uploads and keep file size limits tight.
Export The Current Drawing
def handle_event("export_excalidraw", _params, socket) do
document =
socket
|> LivePanels.Canvas.Read.state()
|> LivePanels.Drawing.to_excalidraw()
{:noreply,
push_event(socket, "download-file", %{
filename: "board.excalidraw",
mime_type: "application/vnd.excalidraw+json",
content: Jason.encode!(document, pretty: true)
})}
endThe browser event that actually downloads the file belongs to your application.
Combine Drawing And Graph
Use the graph toolkit when the canvas is primarily a graph. Use the drawing toolkit when it is primarily a drawing surface. For hybrid products, import the toolkit helpers you need explicitly:
use LivePanels.Canvas,
pubsub: MyApp.PubSub
use LivePanels.Toolkit.Canvas
import LivePanels.Toolkit.Controls.Drawing
import LivePanels.Toolkit.Controls.Viewport
import LivePanels.Graph.AttrsThen render both capabilities:
<.canvas_runtime {canvas_runtime_attrs(assigns, cursor_mode: @cursor_mode)}>
<:item :let={context}>
<.node_or_content_item context={context} />
</:item>
</.canvas_runtime>Committed vector items and graph connector items both render through the runtime world SVG item projection. Product UI stays in node markup, controls, and CSS.
Annotate A Document
Use one item for the document and vector items for annotations:
<:item
id="page-1"
type="document_page"
at={{0, 0}}
size={{960, 1240}}
props={%{document_id: @document.id, page: 1}}
>
<.document_page document={@document} page={1} />
</:item>Keep annotation ids stable when they mirror database rows:
LivePanels.Drawing.vector_item!(socket, :rectangle, placement,
id: "annotation-#{annotation.id}",
style: highlight_style
)The document remains app content. The vector item remains canvas annotation.
Debug A Missing Vector Item
Check these in order:
- the command created a committed vector item, not just a browser draft;
- the vector item kind is supported;
- the style map includes every required field;
- the item id is unique;
- the placement has positive width and height;
- point-backed vector items have valid points;
- the standard runtime is rendering the world vector layer;
- custom renderers call
LivePanels.Drawing.vector_state/1; - erase or removal controls are not immediately removing the vector item;
- layout restore has the item type and vector state expected by the current code.
Use LivePanels.Drawing.vector_item/4 when creating vector items from
server code and show the returned reason on rejection. Browser-authored drawing
commands also record their result on LivePanels.Canvas.Read.last_command/1.