Search documentation of mob

# Mob.Canvas (mob v0.6.18)

Copy Markdown View Source

Drawing-op constructors for `Mob.UI.canvas/1`.

Each function returns a plain map describing one draw operation. The canvas widget takes a `:draw` list of these and renders them in order. Color values can be theme tokens (e.g. `:primary`, `:on_surface`) or raw strings ("#ff0000") — they are resolved by `Mob.Renderer` against the active theme before serialisation to the native side.

## Coordinate system (important — read this once)

All coordinates are canvas-local logical units, top-left origin. The unit is whatever the host app's `<Canvas>` component declared via the `width` and `height` props on the canvas — a draw op at `(width / 2, height / 2)` lands in the dead centre of the rendered canvas regardless of the canvas's actual on-screen pixel size.

This deliberately differs from raw Compose `DrawScope.size` (which is in pixels) and from raw SwiftUI `Canvas` (which is in points). The renderer multiplies every coordinate by `(actual_pixels / declared_logical_units)` per axis so callers don't have to thread density and parent-constraint information through every draw call.

Practical consequence: a YOLO model that outputs bbox coords in `0..640` can be drawn directly on a `<Canvas width=640 height=640>` and the boxes will line up with the underlying preview image regardless of the actual on-screen size or device density.

See "Implementing the renderer" below for the contract the host app's Kotlin / Swift `MobBridge` must honor.

## Implementing the renderer (host app's `MobBridge`)

Mob ships no host-app code; each app's `MobBridge.kt` / `MobBridge.swift` contains the Canvas renderer. The viewport-scaling contract above is non-obvious and easy to get wrong — the original per-app implementations interpreted coordinates as raw pixels, which made bounding-box overlays drift on every device where 1 dp ≠ 1 px (i.e., every modern Android device). Reference recipe for Compose:

    @Composable
    private fun MobCanvas(node: MobNode, modifier: Modifier) {
      val width  = floatProp(node.props, "width")  ?: 0f
      val height = floatProp(node.props, "height") ?: 0f
      val ops    = ... // List<Map<String, Any?>>

      val sized = if (width > 0f && height > 0f)
        modifier.size(width.dp, height.dp) else modifier

      Canvas(modifier = sized) {
        // size.width / size.height are in PIXELS.
        val sx = if (width  > 0f) size.width  / width  else 1f
        val sy = if (height > 0f) size.height / height else 1f
        ops.forEach { op -> drawCanvasOp(op, sx, sy) }
      }
    }

Every coord then passes through `coord * sx` / `coord * sy` in the draw step. Scalar sizes (stroke widths, circle radii, text sizes) use the average `(sx + sy) / 2` so they don't squash when the declared viewport is non-square.

See `nxeigen_probe`'s `android/app/src/main/java/com/example/nxeigen_probe/MobBridge.kt` for the full working implementation.

## Op map equivalence

Helpers and raw maps produce identical output. These are the same:

    Mob.Canvas.line(0, 0, 100, 100, color: :primary, width: 4)
    %{op: :line, x1: 0, y1: 0, x2: 100, y2: 100, color: :primary, width: 4}

Use whichever you prefer; the renderer doesn't care.

## Available ops

- `line/5` — straight stroke between two points
- `circle/4` — circle (outline or filled)
- `ellipse/5` — ellipse with separate rx, ry
- `arc/6` — circular arc between two angles in degrees
- `rect/5` — rectangle (outline or filled, optional corner radius)
- `path/2` — sequence of points (open or closed; outline or filled)
- `text/4` — text at a point with anchor
- `image/5` — image from an asset name into a rect

## Common modifiers (accepted on every op where they make sense)

- `:opacity` — float 0.0–1.0
- `:width` — stroke width in points/dp (ignored on filled-only ops)
- `:dash` — list of [on, off] floats for dashed strokes, e.g. `[4, 4]`

- `:cap` — `:butt` | `:round` | `:square` (line/arc/path)

- `:join` — `:miter` | `:round` | `:bevel` (path/rect outline)

- `:fill` — boolean (circle/ellipse/rect/path); default false (stroke)

## Text-specific

- `:weight` — `:thin` | `:light` | `:regular` | `:medium` | `:semibold` | `:bold`

- `:family` — string font family name; platform default if omitted

- `:anchor` — `:start` | `:center` | `:end` (horizontal); default `:start`

## Summary

### Functions

arc(x, y, r, start_deg, end_deg, opts \\ [])

Draw a circular arc centered at (x, y), radius r, from `start_deg` sweeping clockwise to `end_deg`. 0° points to the right, 90° points down (matching SwiftUI / Compose conventions).

circle(x, y, r, opts \\ [])

Draw a circle. Defaults to stroke; pass `fill: true` for a filled disc.

ellipse(x, y, rx, ry, opts \\ [])

Draw an ellipse with separate horizontal and vertical radii.

image(x, y, w, h, source, opts \\ [])

Draw an image into the rect at (x, y, w, h). `source` is an asset name resolved by the platform (e.g. an iOS asset catalog name or Android drawable name).

line(x1, y1, x2, y2, opts \\ [])

Stroke a line from (x1, y1) to (x2, y2).

path(points, opts \\ [])

Draw a path through a list of points. Points are 2-element lists or 2-tuples; tuples are normalised to lists for JSON serialisation.

rect(x, y, w, h, opts \\ [])

Draw a rectangle. Defaults to stroke; pass `fill: true` for filled. `radius:` rounds the corners (single value, all four corners).

text(x, y, content, opts \\ [])

Draw text at (x, y). The anchor controls horizontal alignment of the text relative to x; vertical baseline is ascender (text grows downward from y, matching SwiftUI/Compose Canvas defaults).

## Functions

# arc(x, y, r, start_deg, end_deg, opts \\ [])

    @spec arc(number(), number(), number(), number(), number(), keyword() | map()) ::
      map()

Draw a circular arc centered at (x, y), radius r, from `start_deg` sweeping clockwise to `end_deg`. 0° points to the right, 90° points down (matching SwiftUI / Compose conventions).

    Mob.Canvas.arc(100, 100, 50, 0, 90, color: :primary, width: 2)

# circle(x, y, r, opts \\ [])

    @spec circle(number(), number(), number(), keyword() | map()) :: map()

Draw a circle. Defaults to stroke; pass `fill: true` for a filled disc.

    Mob.Canvas.circle(120, 120, 60, color: :primary)
    Mob.Canvas.circle(120, 120, 60, color: :primary, fill: true)

# ellipse(x, y, rx, ry, opts \\ [])

    @spec ellipse(number(), number(), number(), number(), keyword() | map()) :: map()

Draw an ellipse with separate horizontal and vertical radii.

    Mob.Canvas.ellipse(100, 80, 60, 30, color: :primary, fill: true)

# image(x, y, w, h, source, opts \\ [])

    @spec image(number(), number(), number(), number(), String.t(), keyword() | map()) ::
      map()

Draw an image into the rect at (x, y, w, h). `source` is an asset name resolved by the platform (e.g. an iOS asset catalog name or Android drawable name).

    Mob.Canvas.image(0, 0, 100, 100, "logo")

# line(x1, y1, x2, y2, opts \\ [])

    @spec line(number(), number(), number(), number(), keyword() | map()) :: map()

Stroke a line from (x1, y1) to (x2, y2).

    Mob.Canvas.line(0, 0, 100, 100, color: :primary, width: 4, cap: :round)

# path(points, opts \\ [])

    @spec path([{number(), number()} | [number()]], keyword() | map()) :: map()

Draw a path through a list of points. Points are 2-element lists or 2-tuples; tuples are normalised to lists for JSON serialisation.

Closed paths (`closed: true`) are wrapped back to the first point. Filled paths (`fill: true`) are filled regardless of `:closed`.

    Mob.Canvas.path([{0, 0}, {100, 0}, {50, 80}], color: :primary, closed: true)

# rect(x, y, w, h, opts \\ [])

    @spec rect(number(), number(), number(), number(), keyword() | map()) :: map()

Draw a rectangle. Defaults to stroke; pass `fill: true` for filled. `radius:` rounds the corners (single value, all four corners).

    Mob.Canvas.rect(10, 10, 100, 50, color: :primary, fill: true, radius: 8)

# text(x, y, content, opts \\ [])

    @spec text(number(), number(), String.t(), keyword() | map()) :: map()

Draw text at (x, y). The anchor controls horizontal alignment of the text relative to x; vertical baseline is ascender (text grows downward from y, matching SwiftUI/Compose Canvas defaults).

    Mob.Canvas.text(120, 50, "Hello", color: :on_surface, size: 18, anchor: :center)