Markup
Show a model where and what to edit: paint over an area, box things in, or comment on a spot, as with comments on an image. Numbered for the prompt, exported at full resolution.

Paint over what to change. Tab reaches the marks: arrow keys move one, Enter edits its note, Delete removes it.
In use
The Image Edit Studio's canvas. Marks and a prompt go to FLUX 3 with the marks drawn in, and the result is laid back only inside what's painted, so nothing else moves. Paint with no prompt erases instead.
Your image will appear here
Your image will appear here
Your image will appear here
Your image will appear here

Paint over what to change. Tab reaches the marks: arrow keys move one, Enter edits its note, Delete removes it.
Your video will appear here
Installation
pnpm dlx shadcn@latest add https://mgp.paukraft.com/r/markup.jsonSending marks
Marks are shapes, not pixels, so the same marks become whatever the endpoint takes. For an instruction to FLUX 3 or Kontext, send the image with the marks drawn in and the notes in the prompt: flux3Image({ prompt: [prompt, ...markupNotes(marks)].join("\n"), images: [await drawMarkup(marks, { src })] }). To erase or fill, send the mask: erase({ image, mask: markup.toMask() }).
| Prop | Type | Default |
|---|---|---|
| drawMarkup | (marks, { src?, size?, color? }) => Promise<string>The marks drawn into the image at full resolution, numbered, as a JPEG data URL: the reference to send with an instruction. Reading the image needs CORS on cross-origin URLs. | — |
| markupNotes | (marks) => string[]The notes as "1: make it blue", for the prompt. | — |
| markupMask | (marks, size) => stringThe mask of any marks, at any size. | — |
Markup
Click a box or comment to edit its note or delete it; a click elsewhere closes it, and a comment left empty goes. B, E, R and C pick the brush, eraser, box and comment; Enter adds a box or comment in the middle; [ and ] size the brush. Tab reaches the marks: arrow keys move one, Alt + arrows resize a box, Enter edits its note, Delete removes it. Changes are announced to screen readers. Zoom with Ctrl/⌘ + scroll, a pinch, or + and −, and 0 to fit; pan by scrolling, or dragging with Space held or two fingers.
| Prop | Type | Default |
|---|---|---|
| markup | MarkupStateFrom useMarkup, which holds the marks for submitting. | — |
| src | stringThe image to mark up; marks are in its pixels. A new one starts over. Without it, a blank sheet at useMarkup's size, for sketching a layout. | — |
| alt | stringWhat the image shows, e.g. "the living room". | — |
| toolbar | booleanFloating tools, brush size, undo, redo, clear and zoom. Hide it to place MarkupToolbar yourself. | true |
useMarkup
| Prop | Type | Default |
|---|---|---|
| tools | ("brush" | "eraser" | "box" | "comment")[]The tools offered, e.g. ["brush", "eraser"] for a mask only, or ["comment"] for comments only. | all |
| defaultMarks | Mark[]Marks to start with, e.g. a saved draft. | — |
| brushSize | numberIn screen pixels, so it feels the same at any zoom. | 40 |
| size | { width, height }The blank sheet's size, without an image. | — |
| Prop | Type | Default |
|---|---|---|
| marks / setMarks | Mark[]Strokes, boxes and comments, in image pixels. setMarks is one undo step. | — |
| callouts | (BoxMark | CommentMark)[]Boxes and comments, numbered from 1 in this order. | — |
| painted | booleanSome area is painted or boxed, after erasing: there's a mask to send. | — |
| toMask | () => string | undefinedPainted areas and boxes as a PNG data URL at the image's size: white to change, black to keep. | — |
| tool / setTool, brushSize / setBrushSize | The current tool and brush. | — |
| selected / select | string | nullThe box or comment whose note is open. | — |
| undo, redo, clear | () => voidEach undoable; also Ctrl/⌘ + Z and Shift + Ctrl/⌘ + Z. | — |