Etcher is the annotation layer for Fresco-based image viewers in Phoenix.
An etcher is the tool that incises marks into a surface — Etcher the library does the same digitally: users draw shapes (rectangle, circle, polygon, freehand) on top of any Fresco viewer; the LiveView fires events with the resulting geometry; the consumer decides what to persist.
Two pieces
- Client side: an
<Etcher.layer>component that attaches to a named Fresco viewer, adds a pencil button to its nav, opens a bottom toolbar with drawing tools when active. Built from scratch — no Annotorious dependency. - Server side: a pluggable storage adapter (
Etcher.Storagebehaviour) with a sensible default (Etcher.Storage.Default) backed by a bundledetcher_annotationstable. Consumers with custom needs implement their own adapter.
Quick start
Install (in your mix.exs):
def deps do
[
{:fresco, "~> 0.2"},
{:etcher, "~> 0.1"}
]
endGenerate the default schema:
mix etcher.gen.migration
mix ecto.migrateConfigure (in config/config.exs):
config :etcher, repo: MyApp.RepoWire the JS hook (in assets/js/app.js):
import "../../deps/fresco/priv/static/fresco.js"
import "../../deps/etcher/priv/static/etcher.js"
let liveSocket = new LiveSocket("/live", Socket, {
hooks: { ...window.FrescoHooks, ...window.EtcherHooks, ...colocatedHooks }
})Drop in a LiveView:
<Fresco.viewer id="demo" src={~p"/uploads/photo.jpg"} class="w-full h-[80vh]" />
<Etcher.layer fresco_id="demo" target_type="file" target_uuid={@file.uuid} />Then handle the etcher:created event in your LiveView:
def handle_event("etcher:created", attrs, socket) do
case Etcher.create_annotation(attrs) do
{:ok, annotation} ->
{:noreply, push_event(socket, "etcher:annotation-saved", annotation)}
{:error, changeset} ->
{:noreply, put_flash(socket, :error, "Failed to save annotation")}
end
endCustom storage
Skip the bundled schema and implement your own adapter:
defmodule MyApp.AnnotationStorage do
@behaviour Etcher.Storage
def create(attrs), do: # your insert logic
def list_for(target_type, target_uuid), do: # your query
def update(uuid, attrs), do: # your update
def delete(uuid), do: # your delete
endThen from your event handler:
MyApp.AnnotationStorage.create(attrs)Etcher's component doesn't run any persistence itself — it fires
events and lets the consumer decide what happens. The bundled
Etcher.create_annotation/1 etc. are just shortcuts for consumers
who want the default backend.
See Etcher.Layer for the component reference, Etcher.Storage for
the behaviour, and Etcher.Annotation for the bundled schema.