add claude-obsidian
This commit is contained in:
@@ -0,0 +1,292 @@
|
||||
# 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.
|
||||
|
||||
This reference aligns with the [JSON Canvas 1.0 open specification](https://jsoncanvas.org/spec/1.0/). All structures support arbitrary additional fields (`[key: string]: any`) for forward compatibility. Obsidian will preserve unknown fields when reading and writing canvas files.
|
||||
|
||||
**ID format**: The JSON Canvas 1.0 spec recommends 16-character lowercase hexadecimal IDs (e.g., `"a1b2c3d4e5f67890"`). Obsidian itself generates IDs in this format. The descriptive ID examples in this reference (`"text-title-4821"`, `"img-cover-7823"`) are an alternative naming convention that this plugin uses for human readability. Both are valid JSON Canvas. Use whichever fits your workflow.
|
||||
|
||||
---
|
||||
|
||||
## 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/example.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",
|
||||
"background": "_attachments/images/grid-bg.png",
|
||||
"backgroundStyle": "cover"
|
||||
}
|
||||
```
|
||||
|
||||
- `label`: shown at the top of the group box.
|
||||
- `color`: colors the group border and label.
|
||||
- `background` *(optional)*: vault-relative path to a background image for the group.
|
||||
- `backgroundStyle` *(optional)*: how the background is rendered.
|
||||
- `"cover"`: fills the group, cropping if needed (default-ish behavior)
|
||||
- `"ratio"`: preserves aspect ratio, fits inside the group
|
||||
- `"repeat"`: tiles the image
|
||||
- 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",
|
||||
"fromEnd": "none",
|
||||
"toNode": "c-idx",
|
||||
"toSide": "left",
|
||||
"toEnd": "arrow",
|
||||
"label": "concepts",
|
||||
"color": "5"
|
||||
}
|
||||
```
|
||||
|
||||
**Required fields**: `id`, `fromNode`, `toNode`. Everything else is optional.
|
||||
|
||||
- `fromNode` / `toNode`: IDs of the source and target nodes.
|
||||
- `fromSide` / `toSide` *(optional)*: `"top"` `"bottom"` `"left"` `"right"`. If omitted, Obsidian auto-calculates the best side based on relative node positions.
|
||||
- `fromEnd` *(optional)*: end-cap on the source side. Defaults to `"none"`. Values: `"none"` | `"arrow"`.
|
||||
- `toEnd` *(optional)*: end-cap on the target side. **Defaults to `"arrow"`**: note the asymmetric default vs `fromEnd`. Values: `"none"` | `"arrow"`.
|
||||
- `label` *(optional)*: text shown on the edge.
|
||||
- `color` *(optional)*: same color palette as nodes (`"1"`–`"6"` or hex).
|
||||
|
||||
Most edges represent directed relationships, so the asymmetric defaults (`fromEnd: "none"`, `toEnd: "arrow"`) produce a single arrow pointing from source to target without specifying anything explicitly.
|
||||
|
||||
---
|
||||
|
||||
## 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.6–2.0 | 420 | 236 |
|
||||
| 2:1 (ultra wide) | ratio > 2.0 | 440 | 220 |
|
||||
| 4:3 | ratio 1.2–1.6 | 380 | 285 |
|
||||
| 1:1 (square) | ratio 0.9–1.1 | 280 | 280 |
|
||||
| 3:4 | ratio 0.6–0.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/example.png",
|
||||
"x": -900, "y": -2180, "width": 420, "height": 236
|
||||
},
|
||||
{
|
||||
"id": "img-icon-free",
|
||||
"type": "file",
|
||||
"file": "_attachments/images/example-icon.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/example-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.
|
||||
Reference in New Issue
Block a user