Files
personal-wiki/skills/canvas/references/canvas-spec.md
Daniel 3b4f62eb0c feat: /canvas command — visual layer of the wiki
New files:
  commands/canvas.md              — slash command with full trigger table
  skills/canvas/SKILL.md          — complete skill: open, new, add image/text/pdf/note,
                                    zone, list, from-banana operations
  skills/canvas/references/canvas-spec.md — Obsidian canvas JSON spec:
                                    coordinate system, all node types, color table,
                                    image sizing by aspect ratio, auto-positioning
                                    pseudocode, common mistakes, full example

Auto-positioning algorithm (bbox math):
  - Finds rightmost node in target zone
  - Places next node at rightmost_x + 40, same row y
  - If overflow: wraps to new row (max_y_in_zone + 20)
  - Falls back to below all content if no zone found

Banana integration:
  - /canvas from banana: checks .recent-images.txt first, then
    filesystem find -newer 10min, presents list for confirmation
  - After any /banana run: suggests /canvas from banana

Updated:
  skills/wiki/SKILL.md        — added /canvas to routing table
  skills/wiki/references/plugins.md — added Calendar + Thino with
                                      pre-installed note + manual install fallback
  README.md                   — /canvas in commands table + file structure
2026-04-07 13:23:33 +03:00

274 lines
6.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Obsidian Canvas JSON Specification
Canvas files are JSON with two top-level keys: `nodes` (array) and `edges` (array).
Obsidian reads and writes them as UTF-8 JSON files with `.canvas` extension.
---
## Coordinate System
```
x increases →
┌─────────────────────────────────
│ (-920, -2400) (0, -2400)
y │ (-920, 0) (0, 0) ← origin
↓ │
│ (-920, 540) (500, 540)
```
- **Origin** (0, 0) is the center of the canvas viewport.
- **x increases rightward.** Negative x = left of center.
- **y increases downward.** Negative y = above center.
- Node `x` and `y` are the **top-left corner** of the node, not the center.
- Obsidian pans to fit all nodes on first open.
---
## Node Types
### Text node
Renders markdown content as a styled card.
```json
{
"id": "text-title-4821",
"type": "text",
"text": "# Heading\n\nParagraph with **bold** and `code`.",
"x": -400,
"y": -300,
"width": 400,
"height": 120,
"color": "6"
}
```
- `text`: markdown string. Use `\n` for newlines.
- Minimum readable size: width ≥ 200, height ≥ 60.
- `color` is optional. Omit for default (no color).
---
### File node
Renders an image, PDF, markdown note, or other vault file inline.
```json
{
"id": "img-cover-7823",
"type": "file",
"file": "_attachments/images/skool-hub/logo-pro.png",
"x": -900,
"y": -100,
"width": 420,
"height": 236
}
```
- `file`: **vault-relative path** (not absolute, not `~/`).
- Supported: `.png` `.jpg` `.webp` `.gif` `.pdf` `.md` `.canvas`
- For `.md` files: renders as a preview card.
- For `.pdf` files: renders the first page as preview.
- No `color` field for file nodes — color is ignored.
---
### Group node (Zone)
A labeled rectangular region. Does not clip or contain nodes — it's a visual guide.
Nodes placed "inside" a group are just positioned within its bounding box.
```json
{
"id": "zone-branding-3391",
"type": "group",
"label": "Brand Identity",
"x": -920,
"y": -880,
"width": 1060,
"height": 290,
"color": "6"
}
```
- `label`: shown at the top of the group box.
- `color`: colors the group border and label.
- Groups do not affect auto-layout — they are purely visual containers.
---
### Link node
Renders a web URL as an embedded preview card.
```json
{
"id": "link-karpathy-2233",
"type": "link",
"url": "https://github.com/karpathy",
"x": 200,
"y": -300,
"width": 400,
"height": 120
}
```
- `url`: must be a valid `https://` URL.
- Obsidian fetches the Open Graph preview (title, description, thumbnail).
---
## Edges
Connections between nodes. Usually empty for mood boards.
```json
{
"id": "e-hub-cidx",
"fromNode": "hub",
"fromSide": "right",
"toNode": "c-idx",
"toSide": "left",
"toEnd": "arrow",
"label": "concepts",
"color": "5"
}
```
- `fromSide` / `toSide`: `"top"` `"bottom"` `"left"` `"right"`
- `toEnd`: `"arrow"` (directed) or `"none"` (undirected line)
- `label` and `color` are optional.
---
## Color Reference
| Code | Color | Hex (approx) | Use case |
|------|-------|-------------|----------|
| `"1"` | Red / Tomato | #e03e3e | Warnings, archive |
| `"2"` | Orange | #d09035 | Active work |
| `"3"` | Yellow / Gold | #d0a023 | WIP, notes |
| `"4"` | Green / Teal | #448361 | Content, sources |
| `"5"` | Blue / Cyan | #3ea7d3 | Navigation, info |
| `"6"` | Purple / Violet | #9063d2 | Title, identity |
Omit `color` entirely for the default (no border color, transparent label).
---
## Image Sizing Guidelines
Calculate from actual image dimensions using PIL or `identify`:
```bash
python3 -c "from PIL import Image; img=Image.open('path.png'); print(img.width, img.height)"
# or
identify -format '%w %h' path.png
```
| Aspect ratio | Condition | Canvas width | Canvas height |
|-------------|-----------|-------------|--------------|
| 16:9 (wide) | ratio 1.62.0 | 420 | 236 |
| 2:1 (ultra wide) | ratio > 2.0 | 440 | 220 |
| 4:3 | ratio 1.21.6 | 380 | 285 |
| 1:1 (square) | ratio 0.91.1 | 280 | 280 |
| 3:4 | ratio 0.60.9 | 240 | 320 |
| 9:16 (portrait) | ratio < 0.6 | 200 | 356 |
| PDF | any | 400 | 520 |
| Unknown | fallback | 320 | 240 |
---
## Auto-Positioning Pseudocode
```
function place_node(canvas, zone_label, new_w, new_h):
zone = find group node where label == zone_label
padding = 20
if zone not found:
max_y = max(n.y + n.height for n in canvas.nodes) + 60
return (-400, max_y)
# Nodes visually inside zone
inside = [n for n in canvas.nodes
if n.type != 'group'
and zone.x <= n.x < zone.x + zone.width
and zone.y <= n.y < zone.y + zone.height]
if inside is empty:
return (zone.x + padding, zone.y + padding)
# Rightmost point in zone
rightmost = max(n.x + n.width for n in inside)
next_x = rightmost + 40
if next_x + new_w > zone.x + zone.width - padding:
# Overflow → new row
bottom_of_row = max(n.y + n.height for n in inside)
return (zone.x + padding, bottom_of_row + padding)
# Same row
row_y = min(n.y for n in inside) # align to top of existing row
return (next_x, row_y)
```
---
## Full Example: Two-Zone Canvas
```json
{
"nodes": [
{
"id": "title-0001",
"type": "text",
"text": "# Brand Reference\n\n**AI Marketing Hub** visual assets",
"x": -920, "y": -2440, "width": 560, "height": 180, "color": "6"
},
{
"id": "zone-logos",
"type": "group",
"label": "Logos & Icons",
"x": -920, "y": -2200, "width": 1800, "height": 320, "color": "6"
},
{
"id": "img-logo-pro",
"type": "file",
"file": "_attachments/images/skool-hub/logo-pro.png",
"x": -900, "y": -2180, "width": 420, "height": 236
},
{
"id": "img-icon-free",
"type": "file",
"file": "_attachments/images/skool-hub/icon-free-v1.png",
"x": -440, "y": -2180, "width": 280, "height": 280
},
{
"id": "zone-covers",
"type": "group",
"label": "Skill Covers",
"x": -920, "y": -1820, "width": 1800, "height": 340, "color": "3"
},
{
"id": "img-seo",
"type": "file",
"file": "_attachments/images/skool-hub/claude-seo-cover.png",
"x": -900, "y": -1800, "width": 420, "height": 236
}
],
"edges": []
}
```
---
## Common Mistakes
- **Wrong path format**: use `_attachments/images/file.png` not `/home/user/...` or `~/...`
- **ID collision**: always read existing IDs before generating a new one
- **Negative y confusion**: `y: -2400` is ABOVE `y: -1000` (more negative = higher up)
- **Group does not clip**: placing a node "inside" a group is just positioning it within the group's bounding box — there is no parent-child relationship in the JSON
- **Missing height on text nodes**: Obsidian will render the text but may clip it if height is too small. Use height ≥ content-lines × 24.