A shared canvas on Issho: the canvas's graph is a document of an Issho room, the canvas's LiveView is one of the room's sessions, and Issho is the room's people and their cursors.
<.canvas id="schema" graph={@graph} issho={[instance: MyApp.Issho, document: "schema:42"]} user={@current_user} />issho takes the keys of every component (Issho.Join): :instance
and :document (required; the room is the part before the first
"/"), :can (:read for a viewer), :people (false hides the
canvas's avatars, on a page that lists the room's people), :follow
(false for a canvas that takes no part in following),
:on_behalf_of and :claims.
The first canvas to open the room's document gives it its graph, its
rules and its on_change; the others get the document as it is. Each
change goes to the document's authority, which checks the rules,
applies it, asks on_change (once, whoever made it), and only then
sends it to every canvas of the room. The document lasts as long as a
canvas has it open: your app's tables, which on_change writes, are
the truth, and the next canvas starts from the graph you give it.
change/4 makes your app's own change, from anywhere (a job, another
LiveView), on a canvas someone has open. A place on the canvas is
Hakushi.Issho.Place.
Events
Each change of a shared canvas is an event of its room (Issho.Event),
once the room has it: an agent waits for what people do on the canvas
(wait_for_events with "canvas.*"), a component of the page hears
it (Issho.Live.subscribe/3's :events), and your app acts on it
(Issho.Events.subscribe/2). The event's document is the canvas's,
its by who made the change, and its name is the change's op:
"canvas.added": nodes added (dropped, pasted, an undone delete),%{nodes: [%{id: id, type: type}], edges: [%{id: id, source: id, target: id}]}."canvas.connected": edges drawn,%{edges: [%{id: id, source: id, target: id}]}."canvas.reconnected": an edge's end moved to another node, the same, with its new ends."canvas.moved": nodes moved or resized,%{nodes: [%{id: id, x: x, y: y}]}(withwidthandheightfor a resize,parentfor a node put in a group or out), the nodes inside a moved node too."canvas.updated": nodes or edges changed (a node's data, an edge's label),%{nodes: [id], edges: [id]}."canvas.deleted": nodes and edges deleted,%{nodes: [id], edges: [id]}, with the nodes inside the nodes deleted and the edges that went with them.
A change of the paper makes none: Issho's "document.changed" says it,
as it says every change.
Summary
Functions
Makes your app's own change on a shared canvas: the way of a person's
(the rules, on_change), to everyone in the room, and not a person's to
undo. changes is a Hakushi.Change or a list of them, applied
together. Gives :ok, {:error, reason}, or {:error, :not_running}
when no canvas has the document open (then change your own copy, in
your database).
A shared canvas's document, for your policy's document/2
(Issho.Policy): what the room's canvas is, whoever opens it first (its
page, an embed of it in a document, an agent with no browser). Give it
the graph your app keeps, and the canvas's rules and on_change (as
canvas/1 takes them): a canvas that opens the room gets the document
as it is, and its own graph, rules and on_change are not used.
The graph of a shared canvas as its room has it, or nil when no canvas has it open.
The place module of this binding, or nil.
Hakushi's tools for agents (Issho.Tool): an agent reads a shared
canvas, points (the people see its pointer, with its name), selects,
adds nodes, drags them (they see the node go), connects them, changes
and deletes them, as a person does, through the canvas's rules and its
on_change. Give them to your app's model (Issho.Tools), or serve
them to MCP clients (Issho.MCP), with Issho's room tools
Functions
@spec change(atom(), String.t(), Hakushi.Change.t() | [Hakushi.Change.t()], keyword()) :: :ok | {:error, term()}
Makes your app's own change on a shared canvas: the way of a person's
(the rules, on_change), to everyone in the room, and not a person's to
undo. changes is a Hakushi.Change or a list of them, applied
together. Gives :ok, {:error, reason}, or {:error, :not_running}
when no canvas has the document open (then change your own copy, in
your database).
Options: :author, who makes it: a key ("server", the default) or
your actor (current_user); on_change gets it as :by, an
Issho.Identity, as everyone's.
@spec document(Hakushi.Graph.t(), keyword()) :: {:ok, map()}
A shared canvas's document, for your policy's document/2
(Issho.Policy): what the room's canvas is, whoever opens it first (its
page, an embed of it in a document, an agent with no browser). Give it
the graph your app keeps, and the canvas's rules and on_change (as
canvas/1 takes them): a canvas that opens the room gets the document
as it is, and its own graph, rules and on_change are not used.
def document(_identity, "board:" <> id = room) do
board = MyApp.Boards.get!(id)
Hakushi.Issho.document(board.graph, on_change: &MyApp.Boards.on_change(room, &1, &2))
endOptions: :rules (a list, as the canvas's), :on_change and :canvas
(the id on_change gets as :canvas, "agent" unless given).
@spec graph(atom(), String.t()) :: Hakushi.Graph.t() | nil
The graph of a shared canvas as its room has it, or nil when no canvas has it open.
The place module of this binding, or nil.
@spec tools() :: [module()]
Hakushi's tools for agents (Issho.Tool): an agent reads a shared
canvas, points (the people see its pointer, with its name), selects,
adds nodes, drags them (they see the node go), connects them, changes
and deletes them, as a person does, through the canvas's rules and its
on_change. Give them to your app's model (Issho.Tools), or serve
them to MCP clients (Issho.MCP), with Issho's room tools:
tools = Issho.Tools.room() ++ Hakushi.Issho.tools()A tool names its canvas by its document ("schema:42", the canvas's
issho :document);
an agent works on a canvas someone has open, or on one your policy's
document/2 describes with document/2.
A person may pause or stop an agent at work (Issho.Work): then its
tools read the canvas and change nothing on it, not even where its
pointer is, and say why, as Issho's room tools do.
Its pointer travels to where it works, and drags, as a hand does: only
where it goes is sent, and each browser draws the way. For changes that
land at once: config :hakushi, Hakushi.Issho, travel: false.
On a canvas with ink (Hakushi.Kits.Ink), draw_on_canvas draws
strokes, or a ring around a node: the people see its pen draw each
stroke as they see a person's, and each stroke lands as a node when
the pen lifts. How it draws is the app's to set, and the agent's for
a drawing (its live argument):
config :hakushi, Hakushi.Issho,
ink: [
live: true, # false: the strokes land at once, unseen as drawn
speed: 900, # how fast the pen goes, in pixels of the canvas a second
max: 8_000 # the most a drawing takes, in milliseconds
]It draws only for someone who sees it: when no one in the room shows
the canvas, or each of them prefers reduced motion, or with travel: false (Hakushi's or Issho's), the strokes land at once.