Version: 0.1 (Draft)
Date: March 2026
Status: Proposal
Depends on: Stroke Format Spec
Used by: Bridge, Edge, Tutor
Visual Primitives are the device-agnostic "drawing commands" that Bridge sends to Edge. They define what to render without specifying how each platform renders it.
Think of this as the "assembly language" between Tutor's semantic intent and Edge's native rendering.
| Goal | Implication |
|---|---|
| Edge-agnostic | Same primitives work on BOOX, reMarkable, iPad, web |
| Capability-adaptive | Graceful degradation when Edge lacks features (e.g., no color) |
| Minimal | Small vocabulary — easy to implement on new Edge platforms |
| Composable | Complex visuals built from simple primitives |
| Semantic hints | Include why something is drawn, not just what |
All primitives use normalized coordinates matching the Stroke Format:
Sizes and dimensions also normalized (e.g., width: 0.1 = 10% of content width).
┌─────────────────────────────────────────────────────────────┐
│ VISUAL PRIMITIVES │
├─────────────────────────────────────────────────────────────┤
│ │
│ CONTENT ANNOTATION FEEDBACK │
│ ───────── ────────── ──────── │
│ Text Highlight Mark (✓/✗) │
│ MathBlock Underline Pulse │
│ Image Circle Pointer │
│ Line Arrow │
│ Box Bracket CONTROL │
│ Divider Strikethrough ─────── │
│ Clear │
│ INPUT HINT ClearLayer │
│ ───── ──── Transition │
│ InputZone HintBubble │
│ InputGuide HintPointer │
│ │
└─────────────────────────────────────────────────────────────┘
Content primitives render static exercise content.
Renders text at a position.
{
"type": "TEXT",
"id": "txt_001",
"content": "Solve for x:",
"position": {"x": 0.05, "y": 0.08},
"style": {
"size": "medium",
"weight": "normal",
"align": "left"
}
}
| Field | Type | Required | Values |
|---|---|---|---|
content |
string | ✓ | UTF-8 text |
position |
{x, y} | ✓ | Normalized coordinates |
style.size |
enum | small, medium, large, xlarge |
|
style.weight |
enum | normal, bold |
|
style.align |
enum | left, center, right |
|
style.semantic |
enum | body, heading, label, caption |
Semantic hint: The semantic field tells Edge the purpose, allowing platform-appropriate rendering (e.g., e-ink might render headings with more contrast).
Renders mathematical notation.
{
"type": "MATH_BLOCK",
"id": "math_001",
"content": "3x + 5 = 14",
"format": "asciimath",
"position": {"x": 0.1, "y": 0.15},
"size": "large"
}
| Field | Type | Required | Values |
|---|---|---|---|
content |
string | ✓ | Math notation |
format |
enum | ✓ | asciimath, latex, mathml |
position |
{x, y} | ✓ | Normalized coordinates |
size |
enum | small, medium, large |
Note: Edge must include a math renderer (e.g., KaTeX, MathJax). If unsupported, fall back to plain text.
Renders an image.
{
"type": "IMAGE",
"id": "img_001",
"src": "asset://exercise/graph_001.png",
"bounds": {"x": 0.1, "y": 0.3, "w": 0.4, "h": 0.3},
"alt": "Coordinate plane with point at (3, 4)"
}
| Field | Type | Required | Values |
|---|---|---|---|
src |
string | ✓ | Asset URI or data URI |
bounds |
{x, y, w, h} | ✓ | Bounding rectangle |
alt |
string | Accessibility description | |
grayscale |
bool | Force grayscale (for e-ink) |
Renders a straight line.
{
"type": "LINE",
"id": "line_001",
"from": {"x": 0.1, "y": 0.5},
"to": {"x": 0.9, "y": 0.5},
"style": {
"weight": "normal",
"pattern": "solid"
}
}
| Field | Type | Values |
|---|---|---|
from |
{x, y} | Start point |
to |
{x, y} | End point |
style.weight |
enum | hairline, normal, thick |
style.pattern |
enum | solid, dashed, dotted |
Renders a rectangle (outline or filled).
{
"type": "BOX",
"id": "box_001",
"bounds": {"x": 0.05, "y": 0.1, "w": 0.9, "h": 0.2},
"style": {
"fill": "none",
"stroke": "normal",
"corner": "rounded"
}
}
| Field | Type | Values |
|---|---|---|
bounds |
{x, y, w, h} | Bounding rectangle |
style.fill |
enum | none, light, medium |
style.stroke |
enum | none, hairline, normal, thick |
style.corner |
enum | square, rounded |
Renders a horizontal divider line (semantic: section break).
{
"type": "DIVIDER",
"id": "div_001",
"y": 0.45,
"inset": 0.05
}
| Field | Type | Description |
|---|---|---|
y |
float | Vertical position |
inset |
float | Horizontal margin from edges |
Define where and how students can write.
Defines an area where student writing is expected.
{
"type": "INPUT_ZONE",
"id": "input_answer",
"bounds": {"x": 0.1, "y": 0.5, "w": 0.8, "h": 0.15},
"label": "Your answer",
"semantic": "answer",
"showBorder": true
}
| Field | Type | Required | Description |
|---|---|---|---|
bounds |
{x, y, w, h} | ✓ | Writable area |
label |
string | Hint text (shown until writing starts) | |
semantic |
enum | answer, work, scratch, equation |
|
showBorder |
bool | Draw border around zone |
Semantic zones: Vision uses semantic to interpret strokes differently:
answer: Final answer expectedwork: Show-your-work areascratch: Rough work, may be ignoredequation: Expecting structured equationVisual guides for structured input (e.g., grid lines, ruled lines).
{
"type": "INPUT_GUIDE",
"id": "guide_001",
"bounds": {"x": 0.1, "y": 0.5, "w": 0.8, "h": 0.3},
"guideType": "ruled",
"spacing": 0.03
}
| Field | Type | Values |
|---|---|---|
guideType |
enum | ruled (horizontal lines), grid, graph, none |
spacing |
float | Distance between lines (normalized) |
Tutor-driven overlays on student work. These render on a separate layer above content and strokes.
Highlights a rectangular region.
{
"type": "HIGHLIGHT",
"id": "hl_001",
"bounds": {"x": 0.3, "y": 0.52, "w": 0.15, "h": 0.04},
"intent": "warning",
"opacity": 0.3
}
| Field | Type | Required | Values |
|---|---|---|---|
bounds |
{x, y, w, h} | ✓ | Region to highlight |
intent |
enum | ✓ | focus, warning, error, success |
opacity |
float | 0.0 - 1.0 (default 0.3) |
Intent colors (Edge interprets per capability):
| Intent | Color display | E-ink (no color) |
|---|---|---|
focus |
Blue | Light gray fill |
warning |
Yellow/orange | Dashed border |
error |
Red | Dark border + pattern |
success |
Green | Light fill + checkmark |
Underlines a region (typically under text or math).
{
"type": "UNDERLINE",
"id": "ul_001",
"from": {"x": 0.3, "y": 0.56},
"to": {"x": 0.45, "y": 0.56},
"intent": "error",
"style": "wavy"
}
| Field | Type | Values |
|---|---|---|
from, to |
{x, y} | Line endpoints |
intent |
enum | focus, warning, error, success |
style |
enum | solid, wavy, double |
Draws a circle around a region (for emphasis).
{
"type": "CIRCLE",
"id": "circ_001",
"center": {"x": 0.4, "y": 0.54},
"radius": 0.05,
"intent": "focus"
}
Draws an arrow pointing to something.
{
"type": "ARROW",
"id": "arr_001",
"from": {"x": 0.6, "y": 0.4},
"to": {"x": 0.42, "y": 0.52},
"intent": "focus"
}
Draws a bracket grouping a region.
{
"type": "BRACKET",
"id": "brk_001",
"from": {"x": 0.25, "y": 0.5},
"to": {"x": 0.25, "y": 0.6},
"side": "left",
"intent": "focus"
}
| Field | Type | Values |
|---|---|---|
from, to |
{x, y} | Bracket endpoints |
side |
enum | left, right, top, bottom |
Strikes through content (for corrections).
{
"type": "STRIKETHROUGH",
"id": "strike_001",
"from": {"x": 0.3, "y": 0.54},
"to": {"x": 0.5, "y": 0.54},
"intent": "error"
}
Quick visual feedback on student work.
Shows a check or X mark.
{
"type": "MARK",
"id": "mark_001",
"position": {"x": 0.85, "y": 0.53},
"markType": "correct",
"size": "medium"
}
| Field | Type | Values |
|---|---|---|
markType |
enum | correct (✓), incorrect (✗), partial (△) |
size |
enum | small, medium, large |
Brief attention-drawing pulse on a region (Edge controls animation).
{
"type": "PULSE",
"id": "pulse_001",
"center": {"x": 0.4, "y": 0.54},
"radius": 0.08,
"intent": "focus"
}
On e-ink: may render as a brief invert or thickened border. On LCD: may animate as expanding/fading circle.
Shows a pointing indicator (like a finger pointing).
{
"type": "POINTER",
"id": "ptr_001",
"position": {"x": 0.38, "y": 0.52},
"direction": "right"
}
Hints from the Tutor.
Shows a hint in a speech-bubble style.
{
"type": "HINT_BUBBLE",
"id": "hint_001",
"content": "What happens when you move a term to the other side?",
"anchor": {"x": 0.4, "y": 0.5},
"position": "above",
"size": "medium"
}
| Field | Type | Values |
|---|---|---|
content |
string | Hint text |
anchor |
{x, y} | Point the bubble points to |
position |
enum | above, below, left, right |
Arrow from hint bubble to target (if hint and target are separated).
{
"type": "HINT_POINTER",
"id": "hintptr_001",
"from": {"x": 0.5, "y": 0.35},
"to": {"x": 0.42, "y": 0.52}
}
Commands that change state rather than draw.
Clears annotations or content.
{
"type": "CLEAR",
"target": "annotations"
}
| Target | Effect |
|---|---|
annotations |
Clear all annotation layer primitives |
hints |
Clear hint bubbles and pointers |
feedback |
Clear marks, pulses, pointers |
all |
Clear everything (full reset) |
Clears a specific primitive by ID.
{
"type": "CLEAR_ID",
"id": "hl_001"
}
Signals Edge to prepare for content change (allows Edge to optimize refresh).
{
"type": "TRANSITION",
"transitionType": "next_exercise",
"hint": "full_refresh"
}
| Field | Values |
|---|---|
transitionType |
next_exercise, next_step, reveal_hint |
hint |
full_refresh, partial_refresh, instant |
Edge maintains three layers:
┌─────────────────────────────────┐
│ ANNOTATION LAYER │ ← Highlights, marks, hints (top)
├─────────────────────────────────┤
│ STROKE LAYER │ ← Student's writing (middle)
├─────────────────────────────────┤
│ CONTENT LAYER │ ← Exercise content (bottom)
└─────────────────────────────────┘
Primitives specify which layer via their category:
Not all Edges support all features. Bridge adapts primitives based on Edge capabilities.
When Edge reports color: false:
| Primitive with intent | Adaptation |
|---|---|
HIGHLIGHT intent:error |
Hatched pattern fill |
HIGHLIGHT intent:success |
Light solid fill |
UNDERLINE intent:error |
Thicker line + wavy |
MARK correct |
Standard checkmark (already works) |
When Edge reports refreshRate: slow (e-ink):
| Primitive | Adaptation |
|---|---|
PULSE |
Single flash (invert region briefly) |
TRANSITION hint:instant |
Ignored, use partial refresh |
When Edge reports small screen:
| Adaptation |
|---|
HINT_BUBBLE text truncated, tap to expand |
size: large items capped at medium |
{
"type": "PRIMITIVE",
"primitive": {
"type": "HIGHLIGHT",
"id": "hl_001",
...
}
}
{
"type": "PRIMITIVE_BATCH",
"primitives": [
{"type": "HIGHLIGHT", "id": "hl_001", ...},
{"type": "ARROW", "id": "arr_001", ...},
{"type": "HINT_BUBBLE", "id": "hint_001", ...}
]
}
Full exercise content sent as primitive batch:
{
"type": "LOAD_EXERCISE",
"exerciseId": "ex_4521",
"primitives": [
{"type": "TEXT", "id": "txt_001", "content": "Solve for x:", ...},
{"type": "MATH_BLOCK", "id": "math_001", "content": "3x + 5 = 14", ...},
{"type": "INPUT_ZONE", "id": "input_work", ...},
{"type": "INPUT_ZONE", "id": "input_answer", ...},
{"type": "DIVIDER", "id": "div_001", ...}
]
}
{
"type": "LOAD_EXERCISE",
"exerciseId": "ex_linear_001",
"primitives": [
{
"type": "TEXT",
"id": "instruction",
"content": "Solve for x:",
"position": {"x": 0.05, "y": 0.05},
"style": {"size": "medium", "semantic": "heading"}
},
{
"type": "MATH_BLOCK",
"id": "equation",
"content": "3x + 5 = 14",
"format": "asciimath",
"position": {"x": 0.05, "y": 0.12},
"size": "large"
},
{
"type": "DIVIDER",
"id": "div1",
"y": 0.22,
"inset": 0.05
},
{
"type": "TEXT",
"id": "work_label",
"content": "Show your work:",
"position": {"x": 0.05, "y": 0.25},
"style": {"size": "small", "semantic": "label"}
},
{
"type": "INPUT_ZONE",
"id": "work_zone",
"bounds": {"x": 0.05, "y": 0.30, "w": 0.9, "h": 0.35},
"semantic": "work",
"showBorder": false
},
{
"type": "INPUT_GUIDE",
"id": "work_guide",
"bounds": {"x": 0.05, "y": 0.30, "w": 0.9, "h": 0.35},
"guideType": "ruled",
"spacing": 0.05
},
{
"type": "DIVIDER",
"id": "div2",
"y": 0.68,
"inset": 0.05
},
{
"type": "TEXT",
"id": "answer_label",
"content": "x =",
"position": {"x": 0.05, "y": 0.73},
"style": {"size": "large", "weight": "bold"}
},
{
"type": "INPUT_ZONE",
"id": "answer_zone",
"bounds": {"x": 0.15, "y": 0.70, "w": 0.25, "h": 0.08},
"semantic": "answer",
"showBorder": true
}
]
}
{
"type": "PRIMITIVE_BATCH",
"primitives": [
{
"type": "HIGHLIGHT",
"id": "error_hl",
"bounds": {"x": 0.20, "y": 0.38, "w": 0.12, "h": 0.04},
"intent": "error"
},
{
"type": "UNDERLINE",
"id": "error_ul",
"from": {"x": 0.20, "y": 0.42},
"to": {"x": 0.32, "y": 0.42},
"intent": "error",
"style": "wavy"
},
{
"type": "ARROW",
"id": "error_arrow",
"from": {"x": 0.45, "y": 0.30},
"to": {"x": 0.33, "y": 0.40},
"intent": "error"
},
{
"type": "HINT_BUBBLE",
"id": "error_hint",
"content": "Check the sign here",
"anchor": {"x": 0.26, "y": 0.38},
"position": "above"
}
]
}
{
"type": "PRIMITIVE_BATCH",
"primitives": [
{
"type": "HIGHLIGHT",
"id": "success_hl",
"bounds": {"x": 0.15, "y": 0.70, "w": 0.25, "h": 0.08},
"intent": "success"
},
{
"type": "MARK",
"id": "checkmark",
"position": {"x": 0.42, "y": 0.73},
"markType": "correct",
"size": "large"
}
]
}
| Question | Options | Notes |
|---|---|---|
| Math format preference | AsciiMath vs LaTeX vs MathML | AsciiMath is simpler, LaTeX more powerful |
| Hint bubble interactivity | Tap to dismiss? Auto-dismiss? | Affects Edge complexity |
| Primitive ID generation | Bridge assigns vs Tutor assigns | Bridge is closer to rendering |
| Sound primitives? | BEEP, CHIME | Or leave all audio to Tutor device |
| Haptics? | VIBRATE primitive | Some tablets support it |
| Intent | Hex (LCD) | E-ink adaptation |
|---|---|---|
focus |
#3B82F6 (blue) | Gray 60% border |
warning |
#F59E0B (amber) | Dashed border |
error |
#EF4444 (red) | Thick border + hatch fill |
success |
#22C55E (green) | Light fill |
| Category | Primitives |
|---|---|
| Content | TEXT, MATH_BLOCK, IMAGE, LINE, BOX, DIVIDER |
| Input | INPUT_ZONE, INPUT_GUIDE |
| Annotation | HIGHLIGHT, UNDERLINE, CIRCLE, ARROW, BRACKET, STRIKETHROUGH |
| Feedback | MARK, PULSE, POINTER |
| Hint | HINT_BUBBLE, HINT_POINTER |
| Control | CLEAR, CLEAR_ID, TRANSITION |
Next spec: Edge Contract (full interface specification)