# Generation Canvas

A dotted plane for generative work. Nodes that hold a prompt, its settings or a result, edges that carry light while the result is on its way, and a dock that floats above the plane — three parts in one file, built on the flow edge and the generation overlay.

> For the complete documentation index, see [llms.txt](/llms.txt). Markdown variants are available by appending `.md` to any URL or sending an `Accept: text/markdown` header. An agent skill is available at [/.well-known/agent-skills/site-skill.md](/.well-known/agent-skills/site-skill.md).

<ComponentPreview
  align="start"
  name="generation-canvas"
  previewClassName="p-4 sm:p-6"
>
  <GenerationCanvasDemo />
</ComponentPreview>

## Installation [#installation]

```bash
npx shadcn@latest add https://motif.sweetretry.com/r/generation-canvas.json
```

It writes one file, `components/ui/motif/generation-canvas.tsx`, and brings two registry
items along with it: [Flow Edge](/docs/media/flow-edge) and
[Generation Overlay](/docs/media/generation-overlay). It adds no page and no route, and the only package it needs is `motion`.

## What it is made of [#what-it-is-made-of]

The block is the plane and nothing on it. What a node holds is yours, and the controls
in the demo above are pieces of this registry that you add only if you use them:

| Part               | What it does here                                                      |
| ------------------ | ---------------------------------------------------------------------- |
| `GenerationCanvas` | The frame and the plane: a dotted ground that scrolls, and a dock slot |
| `CanvasNode`       | A card at a pixel position, with ports where edges attach              |
| `CanvasEdge`       | A curve between two ports that carries light while work moves along it |
| `CanvasMedia`      | A frame in the requested ratio that flows while it waits               |

The demo's settings node is [Aspect Ratio Picker](/docs/media/aspect-ratio-picker),
[Resolution Segmented](/docs/media/resolution-segmented) and
[Strength Select](/docs/media/strength-select), and its dock is a
[Toolbar](/docs/media/toolbar). Add them one at a time, with the same command and their
own name, when a node of yours needs them.

## Usage [#usage]

A node is a box at a position, and an edge is two points. `portOf` turns a node into the
point an edge should leave from or arrive at, so moving a node moves its lines with it.

```tsx
import {
  CanvasEdge,
  CanvasMedia,
  CanvasNode,
  GenerationCanvas,
  portOf,
} from "@/components/ui/motif/generation-canvas";

const prompt = { x: 24, y: 24, width: 272 };
const image = { x: 368, y: 24, width: 336 };

export function Canvas({ pending, src }: { pending: boolean; src?: string }) {
  return (
    <GenerationCanvas className="h-[34rem]" width={744} height={400}>
      <CanvasEdge
        active={pending}
        from={portOf(prompt, "out")}
        to={portOf(image, "in")}
      />

      <CanvasNode {...prompt} ports={["out"]} title="Prompt">
        {/* anything */}
      </CanvasNode>

      <CanvasNode {...image} ports={["in"]} title="Image">
        <div className="px-3 pb-3">
          <CanvasMedia aspect="16:9" pending={pending} src={src} />
        </div>
      </CanvasNode>
    </GenerationCanvas>
  );
}
```

| Part               | Prop              | What it is                                                                                      |
| ------------------ | ----------------- | ----------------------------------------------------------------------------------------------- |
| `GenerationCanvas` | `width`, `height` | The plane's size, when the nodes need more room than the frame. It scrolls                      |
| `GenerationCanvas` | `dock`            | Floats at the bottom centre and stays put while the plane scrolls. Omit it and nothing is drawn |
| `CanvasNode`       | `x`, `y`, `width` | Pixel position on the plane. Height is the node's content                                       |
| `CanvasNode`       | `ports`           | `"in"`, `"out"` or both. A side with no edge has no port                                        |
| `CanvasNode`       | `title`           | A 40px header row. Omit it and the node is only its content                                     |
| `CanvasEdge`       | `from`, `to`      | Two points, normally `portOf(source, "out")` and `portOf(target, "in")`                         |
| `CanvasEdge`       | `active`          | Light is travelling. Off leaves the line at rest                                                |
| `CanvasMedia`      | `aspect`          | `"16:9"` — the string `AspectRatioPicker` hands back                                            |
| `CanvasMedia`      | `pending`, `src`  | Whether a generation is in flight, and the result once there is one                             |

## The three states of a node [#the-three-states-of-a-node]

A node waits, finishes or fails, and the line feeding it says which. While work is on its
way the edge carries light into the node and the media flows behind it. When the result
decodes the flow gives way to a blur-to-focus reveal and the line goes quiet. If the
source never loads, the flow gives way to a failure line instead.

<ComponentPreview
  name="generation-canvas"
  previewClassName="p-4 sm:p-6"
  hideCode
>
  <GenerationCanvasStatesDemo />
</ComponentPreview>

`CanvasMedia` leaves status copy and actions to you. Pass `overlay` for a retry or a
progress line, and `failureLabel` for the words along the bottom when the source fails.

## Edges run left to right [#edges-run-left-to-right]

The light travels in x, so an edge wants its source on the left of its target. A target
above or below the source is fine — the curve leaves and arrives level, and bends in the
middle — but a target to the left of the source makes the light run backwards through the
curve. Lay a canvas out in columns, and the lines take care of themselves.

## Ports are measured from the top [#ports-are-measured-from-the-top]

`portOf` puts a port 20px below a node's top edge, the middle of the header row, whether
or not a header is drawn. Nodes can therefore be any height without their edges being
told: an edge only ever needs a node's position and width.

## Not in the box [#not-in-the-box]

Nothing pans, zooms or drags. A node is a box at a position, and moving it is a change to
`x` and `y` in whatever state you keep. If you need a viewport or free dragging, keep
these parts and put your own around them — they do not know what is moving them. And
`CanvasEdge` is a thin wrapper over [Flow Edge](/docs/media/flow-edge), so a graph library
that draws its own edges can use that directly.

## Colours [#colours]

Every colour is a theme token. The plane's dots are `border`, a node is `card`, the light
is `primary` over an idle line in `muted-foreground`, and a failed source reads as
`destructive` through the overlay. Restyle the theme and the canvas follows.

## Component source [#component-source]

<ComponentSource
  name="generation-canvas"
  src="registry/new-york/blocks/generation-canvas/generation-canvas.tsx"
  title="generation-canvas.tsx"
/>
