@hypen-space/web 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-2s02mkzs.js +32 -0
- package/dist/chunk-2s02mkzs.js.map +9 -0
- package/dist/src/canvas/accessibility.js +152 -0
- package/dist/src/canvas/accessibility.js.map +10 -0
- package/dist/src/canvas/events.js +198 -0
- package/dist/src/canvas/events.js.map +10 -0
- package/dist/src/canvas/index.js +28 -0
- package/dist/src/canvas/index.js.map +9 -0
- package/dist/src/canvas/input.js +132 -0
- package/dist/src/canvas/input.js.map +10 -0
- package/dist/src/canvas/layout.js +309 -0
- package/dist/src/canvas/layout.js.map +10 -0
- package/dist/src/canvas/paint.js +878 -0
- package/dist/src/canvas/paint.js.map +10 -0
- package/dist/src/canvas/renderer.js +276 -0
- package/dist/src/canvas/renderer.js.map +10 -0
- package/dist/src/canvas/text.js +118 -0
- package/dist/src/canvas/text.js.map +10 -0
- package/dist/src/canvas/types.js +2 -0
- package/dist/src/canvas/types.js.map +9 -0
- package/dist/src/canvas/utils.js +139 -0
- package/dist/src/canvas/utils.js.map +10 -0
- package/dist/src/dom/applicators/advanced-layout.js +111 -0
- package/dist/src/dom/applicators/advanced-layout.js.map +10 -0
- package/dist/src/dom/applicators/background.js +54 -0
- package/dist/src/dom/applicators/background.js.map +10 -0
- package/dist/src/dom/applicators/border.js +33 -0
- package/dist/src/dom/applicators/border.js.map +10 -0
- package/dist/src/dom/applicators/color.js +36 -0
- package/dist/src/dom/applicators/color.js.map +10 -0
- package/dist/src/dom/applicators/display.js +57 -0
- package/dist/src/dom/applicators/display.js.map +10 -0
- package/dist/src/dom/applicators/effects.js +89 -0
- package/dist/src/dom/applicators/effects.js.map +10 -0
- package/dist/src/dom/applicators/events.js +518 -0
- package/dist/src/dom/applicators/events.js.map +10 -0
- package/dist/src/dom/applicators/font.js +39 -0
- package/dist/src/dom/applicators/font.js.map +10 -0
- package/dist/src/dom/applicators/index.js +296 -0
- package/dist/src/dom/applicators/index.js.map +10 -0
- package/dist/src/dom/applicators/layout.js +86 -0
- package/dist/src/dom/applicators/layout.js.map +10 -0
- package/dist/src/dom/applicators/margin.js +32 -0
- package/dist/src/dom/applicators/margin.js.map +10 -0
- package/dist/src/dom/applicators/padding.js +35 -0
- package/dist/src/dom/applicators/padding.js.map +10 -0
- package/dist/src/dom/applicators/size.js +42 -0
- package/dist/src/dom/applicators/size.js.map +10 -0
- package/dist/src/dom/applicators/transform.js +92 -0
- package/dist/src/dom/applicators/transform.js.map +10 -0
- package/dist/src/dom/applicators/transition.js +66 -0
- package/dist/src/dom/applicators/transition.js.map +10 -0
- package/dist/src/dom/applicators/typography.js +87 -0
- package/dist/src/dom/applicators/typography.js.map +10 -0
- package/dist/src/dom/canvas/index.js +50 -0
- package/dist/src/dom/canvas/index.js.map +10 -0
- package/dist/src/dom/components/audio.js +48 -0
- package/dist/src/dom/components/audio.js.map +10 -0
- package/dist/src/dom/components/avatar.js +58 -0
- package/dist/src/dom/components/avatar.js.map +10 -0
- package/dist/src/dom/components/badge.js +55 -0
- package/dist/src/dom/components/badge.js.map +10 -0
- package/dist/src/dom/components/button.js +29 -0
- package/dist/src/dom/components/button.js.map +10 -0
- package/dist/src/dom/components/card.js +33 -0
- package/dist/src/dom/components/card.js.map +10 -0
- package/dist/src/dom/components/center.js +32 -0
- package/dist/src/dom/components/center.js.map +10 -0
- package/dist/src/dom/components/checkbox.js +54 -0
- package/dist/src/dom/components/checkbox.js.map +10 -0
- package/dist/src/dom/components/column.js +31 -0
- package/dist/src/dom/components/column.js.map +10 -0
- package/dist/src/dom/components/container.js +29 -0
- package/dist/src/dom/components/container.js.map +10 -0
- package/dist/src/dom/components/divider.js +45 -0
- package/dist/src/dom/components/divider.js.map +10 -0
- package/dist/src/dom/components/grid.js +44 -0
- package/dist/src/dom/components/grid.js.map +10 -0
- package/dist/src/dom/components/heading.js +47 -0
- package/dist/src/dom/components/heading.js.map +10 -0
- package/dist/src/dom/components/image.js +39 -0
- package/dist/src/dom/components/image.js.map +10 -0
- package/dist/src/dom/components/index.js +217 -0
- package/dist/src/dom/components/index.js.map +10 -0
- package/dist/src/dom/components/input.js +41 -0
- package/dist/src/dom/components/input.js.map +10 -0
- package/dist/src/dom/components/link.js +42 -0
- package/dist/src/dom/components/link.js.map +10 -0
- package/dist/src/dom/components/list.js +42 -0
- package/dist/src/dom/components/list.js.map +10 -0
- package/dist/src/dom/components/paragraph.js +35 -0
- package/dist/src/dom/components/paragraph.js.map +10 -0
- package/dist/src/dom/components/progressbar.js +57 -0
- package/dist/src/dom/components/progressbar.js.map +10 -0
- package/dist/src/dom/components/route.js +44 -0
- package/dist/src/dom/components/route.js.map +10 -0
- package/dist/src/dom/components/router.js +33 -0
- package/dist/src/dom/components/router.js.map +10 -0
- package/dist/src/dom/components/row.js +31 -0
- package/dist/src/dom/components/row.js.map +10 -0
- package/dist/src/dom/components/select.js +57 -0
- package/dist/src/dom/components/select.js.map +10 -0
- package/dist/src/dom/components/slider.js +48 -0
- package/dist/src/dom/components/slider.js.map +10 -0
- package/dist/src/dom/components/spacer.js +30 -0
- package/dist/src/dom/components/spacer.js.map +10 -0
- package/dist/src/dom/components/spinner.js +65 -0
- package/dist/src/dom/components/spinner.js.map +10 -0
- package/dist/src/dom/components/stack.js +45 -0
- package/dist/src/dom/components/stack.js.map +10 -0
- package/dist/src/dom/components/switch.js +83 -0
- package/dist/src/dom/components/switch.js.map +10 -0
- package/dist/src/dom/components/text.js +37 -0
- package/dist/src/dom/components/text.js.map +10 -0
- package/dist/src/dom/components/textarea.js +51 -0
- package/dist/src/dom/components/textarea.js.map +10 -0
- package/dist/src/dom/components/video.js +51 -0
- package/dist/src/dom/components/video.js.map +10 -0
- package/dist/src/dom/debug.js +170 -0
- package/dist/src/dom/debug.js.map +10 -0
- package/dist/src/dom/events.js +112 -0
- package/dist/src/dom/events.js.map +10 -0
- package/dist/src/dom/index.js +73 -0
- package/dist/src/dom/index.js.map +9 -0
- package/dist/src/dom/renderer.js +277 -0
- package/dist/src/dom/renderer.js.map +10 -0
- package/dist/src/index.js +89 -0
- package/dist/src/index.js.map +9 -0
- package/package.json +84 -0
- package/src/canvas/QUICKSTART.md +421 -0
- package/src/canvas/README.md +376 -0
- package/src/canvas/accessibility.ts +218 -0
- package/src/canvas/events.ts +307 -0
- package/src/canvas/index.ts +35 -0
- package/src/canvas/input.ts +210 -0
- package/src/canvas/layout.ts +401 -0
- package/src/canvas/paint.ts +1321 -0
- package/src/canvas/renderer.ts +422 -0
- package/src/canvas/text.ts +182 -0
- package/src/canvas/types.ts +137 -0
- package/src/canvas/utils.ts +218 -0
- package/src/dom/README.md +265 -0
- package/src/dom/applicators/advanced-layout.ts +128 -0
- package/src/dom/applicators/background.ts +50 -0
- package/src/dom/applicators/border.ts +19 -0
- package/src/dom/applicators/color.ts +23 -0
- package/src/dom/applicators/display.ts +54 -0
- package/src/dom/applicators/effects.ts +97 -0
- package/src/dom/applicators/events.ts +689 -0
- package/src/dom/applicators/font.ts +27 -0
- package/src/dom/applicators/index.ts +354 -0
- package/src/dom/applicators/layout.ts +92 -0
- package/src/dom/applicators/margin.ts +18 -0
- package/src/dom/applicators/padding.ts +18 -0
- package/src/dom/applicators/size.ts +31 -0
- package/src/dom/applicators/transform.ts +93 -0
- package/src/dom/applicators/transition.ts +65 -0
- package/src/dom/applicators/typography.ts +91 -0
- package/src/dom/canvas/index.ts +60 -0
- package/src/dom/components/audio.ts +45 -0
- package/src/dom/components/avatar.ts +49 -0
- package/src/dom/components/badge.ts +45 -0
- package/src/dom/components/button.ts +13 -0
- package/src/dom/components/card.ts +19 -0
- package/src/dom/components/center.ts +16 -0
- package/src/dom/components/checkbox.ts +54 -0
- package/src/dom/components/column.ts +15 -0
- package/src/dom/components/container.ts +13 -0
- package/src/dom/components/divider.ts +37 -0
- package/src/dom/components/grid.ts +40 -0
- package/src/dom/components/heading.ts +41 -0
- package/src/dom/components/image.ts +27 -0
- package/src/dom/components/index.ts +115 -0
- package/src/dom/components/input.ts +29 -0
- package/src/dom/components/link.ts +35 -0
- package/src/dom/components/list.ts +30 -0
- package/src/dom/components/paragraph.ts +23 -0
- package/src/dom/components/progressbar.ts +51 -0
- package/src/dom/components/route.ts +37 -0
- package/src/dom/components/router.ts +22 -0
- package/src/dom/components/row.ts +15 -0
- package/src/dom/components/select.ts +56 -0
- package/src/dom/components/slider.ts +45 -0
- package/src/dom/components/spacer.ts +16 -0
- package/src/dom/components/spinner.ts +60 -0
- package/src/dom/components/stack.ts +34 -0
- package/src/dom/components/switch.ts +86 -0
- package/src/dom/components/text.ts +24 -0
- package/src/dom/components/textarea.ts +50 -0
- package/src/dom/components/video.ts +50 -0
- package/src/dom/debug.ts +247 -0
- package/src/dom/events.ts +168 -0
- package/src/dom/index.ts +11 -0
- package/src/dom/renderer.ts +327 -0
- package/src/index.ts +56 -0
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
# Canvas Renderer
|
|
2
|
+
|
|
3
|
+
Browser-only module for rendering Hypen UI to a `<canvas>` element.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
The Canvas Renderer is a complete alternative to the DOM renderer that draws all UI elements directly to a canvas using the Canvas 2D API. This provides:
|
|
8
|
+
|
|
9
|
+
- **Performance**: For very large UIs, canvas can be faster than DOM
|
|
10
|
+
- **Control**: Pixel-perfect control over rendering
|
|
11
|
+
- **Portability**: Can be adapted to other canvas-based platforms (WebGL, native canvas, etc.)
|
|
12
|
+
- **Consistency**: Same rendering behavior across all browsers
|
|
13
|
+
|
|
14
|
+
## Quick Start
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
import { Engine, app } from "@hypen/core";
|
|
18
|
+
import { CanvasRenderer } from "@hypen/core/canvas";
|
|
19
|
+
|
|
20
|
+
// Setup canvas
|
|
21
|
+
const canvas = document.getElementById("app") as HTMLCanvasElement;
|
|
22
|
+
|
|
23
|
+
// Initialize engine
|
|
24
|
+
const engine = new Engine();
|
|
25
|
+
await engine.init();
|
|
26
|
+
|
|
27
|
+
// Create canvas renderer
|
|
28
|
+
const renderer = new CanvasRenderer(canvas, engine);
|
|
29
|
+
|
|
30
|
+
// Set render callback
|
|
31
|
+
engine.setRenderCallback((patches) => {
|
|
32
|
+
renderer.applyPatches(patches);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Render UI
|
|
36
|
+
await engine.renderSource(`
|
|
37
|
+
Column {
|
|
38
|
+
Text("Hello, Canvas!")
|
|
39
|
+
Button("@actions.click") { Text("Click me") }
|
|
40
|
+
}
|
|
41
|
+
`);
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Architecture
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
┌─────────────────────────────────────────┐
|
|
48
|
+
│ CanvasRenderer │
|
|
49
|
+
│ - Orchestrates layout, paint, events │
|
|
50
|
+
│ - Maintains virtual node tree │
|
|
51
|
+
│ - Schedules redraws │
|
|
52
|
+
└─────────────────────────────────────────┘
|
|
53
|
+
↓ ↓ ↓
|
|
54
|
+
┌──────────┐ ┌──────┐ ┌───────┐
|
|
55
|
+
│ Layout │ │ Paint│ │Events │
|
|
56
|
+
│ Engine │ │System│ │Manager│
|
|
57
|
+
└──────────┘ └──────┘ └───────┘
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Virtual Node Tree
|
|
61
|
+
|
|
62
|
+
Unlike the DOM renderer which creates real HTML elements, the canvas renderer maintains an internal virtual node tree:
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
interface VirtualNode {
|
|
66
|
+
id: string
|
|
67
|
+
type: string
|
|
68
|
+
props: Record<string, any>
|
|
69
|
+
children: VirtualNode[]
|
|
70
|
+
layout?: Layout // Computed position/size
|
|
71
|
+
visible: boolean
|
|
72
|
+
opacity: number
|
|
73
|
+
clickable: boolean
|
|
74
|
+
hovered: boolean
|
|
75
|
+
focused: boolean
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Core Systems
|
|
80
|
+
|
|
81
|
+
### 1. Layout Engine (`layout.ts`)
|
|
82
|
+
|
|
83
|
+
Implements a flexbox-like layout system:
|
|
84
|
+
|
|
85
|
+
- **Flexbox model**: Row/Column with `flex`, `gap`, alignment
|
|
86
|
+
- **Box model**: Margin, padding, border
|
|
87
|
+
- **Sizing**: width, height, min/max constraints
|
|
88
|
+
- **Text layout**: Automatic text measurement and wrapping
|
|
89
|
+
|
|
90
|
+
**Supported Properties:**
|
|
91
|
+
- `flexDirection`: "row" | "column"
|
|
92
|
+
- `justifyContent`: "flex-start" | "center" | "flex-end" | "space-between" | "space-around"
|
|
93
|
+
- `alignItems`: "flex-start" | "center" | "flex-end" | "stretch"
|
|
94
|
+
- `gap`: spacing between children
|
|
95
|
+
- `padding`, `margin`: box spacing
|
|
96
|
+
- `width`, `height`, `minWidth`, `maxWidth`, etc.
|
|
97
|
+
|
|
98
|
+
### 2. Paint System (`paint.ts`)
|
|
99
|
+
|
|
100
|
+
Draws virtual nodes to canvas:
|
|
101
|
+
|
|
102
|
+
- **Background**: solid colors, border radius
|
|
103
|
+
- **Border**: width, color, radius
|
|
104
|
+
- **Text**: font rendering with alignment and wrapping
|
|
105
|
+
- **Components**: Button, Input, Image, etc.
|
|
106
|
+
|
|
107
|
+
**Custom Painters:**
|
|
108
|
+
```typescript
|
|
109
|
+
renderer.registerPainter("MyComponent", (ctx, node) => {
|
|
110
|
+
// Custom drawing code
|
|
111
|
+
ctx.fillStyle = node.props.color;
|
|
112
|
+
ctx.fillRect(
|
|
113
|
+
node.layout!.x,
|
|
114
|
+
node.layout!.y,
|
|
115
|
+
node.layout!.width,
|
|
116
|
+
node.layout!.height
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### 3. Event System (`events.ts`)
|
|
122
|
+
|
|
123
|
+
Maps canvas events to virtual nodes:
|
|
124
|
+
|
|
125
|
+
- **Hit Testing**: Determines which node was clicked/hovered
|
|
126
|
+
- **Mouse Events**: click, mousedown, mouseup, mouseenter, mouseleave
|
|
127
|
+
- **Keyboard Events**: keydown, keyup (for focused elements)
|
|
128
|
+
- **Hover States**: Automatic cursor changes and hover effects
|
|
129
|
+
|
|
130
|
+
**How It Works:**
|
|
131
|
+
1. Mouse event occurs on canvas
|
|
132
|
+
2. Convert to canvas coordinates
|
|
133
|
+
3. Walk virtual tree to find node at coordinates (back to front)
|
|
134
|
+
4. Dispatch action to engine if node has handler
|
|
135
|
+
|
|
136
|
+
### 4. Text System (`text.ts`)
|
|
137
|
+
|
|
138
|
+
Handles text measurement and rendering:
|
|
139
|
+
|
|
140
|
+
- **Text Measurement**: Accurate width/height calculation
|
|
141
|
+
- **Line Wrapping**: Automatic word wrapping to fit width
|
|
142
|
+
- **Font Loading**: Ensures fonts are loaded before rendering
|
|
143
|
+
- **Text Alignment**: left, center, right, top, middle, bottom
|
|
144
|
+
- **Caching**: Text metrics are cached for performance
|
|
145
|
+
|
|
146
|
+
### 5. Input Overlay (`input.ts`)
|
|
147
|
+
|
|
148
|
+
Since canvas can't handle text input natively, we use DOM overlays:
|
|
149
|
+
|
|
150
|
+
**Strategy:**
|
|
151
|
+
1. When canvas input is focused, create a real `<input>` element
|
|
152
|
+
2. Position it exactly over the canvas input (invisible to user)
|
|
153
|
+
3. Style it to match the canvas input appearance
|
|
154
|
+
4. Capture input and update state
|
|
155
|
+
5. Remove overlay when focus is lost
|
|
156
|
+
|
|
157
|
+
**Supports:**
|
|
158
|
+
- Single-line text input
|
|
159
|
+
- Multi-line textarea
|
|
160
|
+
- Number input
|
|
161
|
+
- Custom styling
|
|
162
|
+
|
|
163
|
+
### 6. Accessibility (`accessibility.ts`)
|
|
164
|
+
|
|
165
|
+
Maintains a shadow DOM tree for screen readers:
|
|
166
|
+
|
|
167
|
+
**Strategy:**
|
|
168
|
+
1. Create hidden DOM tree that mirrors canvas structure
|
|
169
|
+
2. Use semantic HTML (button, input, etc.)
|
|
170
|
+
3. Update shadow DOM when patches are applied
|
|
171
|
+
4. Support keyboard navigation
|
|
172
|
+
5. ARIA labels and roles
|
|
173
|
+
|
|
174
|
+
This makes the canvas renderer fully accessible without impacting visual rendering.
|
|
175
|
+
|
|
176
|
+
## Supported Components
|
|
177
|
+
|
|
178
|
+
All Hypen components work with the canvas renderer:
|
|
179
|
+
|
|
180
|
+
### Layout Components
|
|
181
|
+
- **Column**: Vertical flex container
|
|
182
|
+
- **Row**: Horizontal flex container
|
|
183
|
+
- **Container/Box**: Generic container
|
|
184
|
+
|
|
185
|
+
### Text Components
|
|
186
|
+
- **Text**: Text with automatic wrapping
|
|
187
|
+
|
|
188
|
+
### Interactive Components
|
|
189
|
+
- **Button**: Clickable button with hover states
|
|
190
|
+
- **Input**: Single-line text input
|
|
191
|
+
- **Textarea**: Multi-line text input (future)
|
|
192
|
+
|
|
193
|
+
### Media Components
|
|
194
|
+
- **Image**: Image rendering (basic support)
|
|
195
|
+
|
|
196
|
+
## Configuration Options
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
const renderer = new CanvasRenderer(canvas, engine, {
|
|
200
|
+
// Display
|
|
201
|
+
devicePixelRatio: window.devicePixelRatio, // HiDPI support
|
|
202
|
+
backgroundColor: "#ffffff", // Canvas background
|
|
203
|
+
|
|
204
|
+
// Features
|
|
205
|
+
enableAccessibility: true, // Shadow DOM for screen readers
|
|
206
|
+
enableHitTesting: true, // Mouse event handling
|
|
207
|
+
enableInputOverlay: true, // DOM overlays for text input
|
|
208
|
+
|
|
209
|
+
// Performance (future)
|
|
210
|
+
enableDirtyRects: false, // Only redraw changed regions
|
|
211
|
+
enableLayerCaching: false, // Cache static subtrees
|
|
212
|
+
maxLayerCacheSize: 10, // Max cached layers
|
|
213
|
+
|
|
214
|
+
// Debug
|
|
215
|
+
showLayoutBounds: false, // Draw red boxes around nodes
|
|
216
|
+
showDirtyRects: false, // Highlight redrawn regions
|
|
217
|
+
logPerformance: false, // Log FPS and render time
|
|
218
|
+
});
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Examples
|
|
222
|
+
|
|
223
|
+
See `examples/canvas-counter.ts` for a complete example.
|
|
224
|
+
|
|
225
|
+
### Running Examples
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
# Using Bun
|
|
229
|
+
bun examples/canvas-counter.ts
|
|
230
|
+
|
|
231
|
+
# Or open HTML file directly
|
|
232
|
+
open examples/canvas-counter.html
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
## Performance
|
|
236
|
+
|
|
237
|
+
### Current Performance
|
|
238
|
+
- **Typical UI**: 60fps with < 100 nodes
|
|
239
|
+
- **Layout**: ~2-5ms for moderate trees
|
|
240
|
+
- **Paint**: ~5-10ms for moderate trees
|
|
241
|
+
- **Text**: Cached measurements are nearly free
|
|
242
|
+
|
|
243
|
+
### Optimization Opportunities
|
|
244
|
+
- ✅ Text metrics caching (implemented)
|
|
245
|
+
- ⬜ Dirty rectangle tracking (planned)
|
|
246
|
+
- ⬜ Layer caching (planned)
|
|
247
|
+
- ⬜ Offscreen canvases (planned)
|
|
248
|
+
- ⬜ WebGL backend (future)
|
|
249
|
+
|
|
250
|
+
## Limitations
|
|
251
|
+
|
|
252
|
+
### Current Limitations
|
|
253
|
+
- ❌ No rich text (bold/italic within text)
|
|
254
|
+
- ❌ No text selection/copy
|
|
255
|
+
- ❌ No gradients
|
|
256
|
+
- ❌ No shadows
|
|
257
|
+
- ❌ No transforms (rotate/scale/skew)
|
|
258
|
+
- ❌ Limited image support
|
|
259
|
+
|
|
260
|
+
### Future Improvements
|
|
261
|
+
All of these are planned for future releases!
|
|
262
|
+
|
|
263
|
+
## Comparison: Canvas vs DOM
|
|
264
|
+
|
|
265
|
+
| Feature | Canvas Renderer | DOM Renderer |
|
|
266
|
+
|---------|----------------|--------------|
|
|
267
|
+
| Performance (large UIs) | ✅ Better | ⚠️ Slower |
|
|
268
|
+
| Text input | ⚠️ Overlay | ✅ Native |
|
|
269
|
+
| Accessibility | ⚠️ Shadow DOM | ✅ Native |
|
|
270
|
+
| Browser DevTools | ❌ No inspection | ✅ Full support |
|
|
271
|
+
| CSS styling | ❌ Props only | ✅ CSS support |
|
|
272
|
+
| Animations | ⚠️ Manual | ✅ CSS animations |
|
|
273
|
+
| Memory usage | ✅ Lower | ⚠️ Higher |
|
|
274
|
+
| Initial render | ✅ Faster | ⚠️ Slower |
|
|
275
|
+
|
|
276
|
+
## When to Use Canvas Renderer
|
|
277
|
+
|
|
278
|
+
**Use Canvas Renderer when:**
|
|
279
|
+
- Building dashboards with many elements (100+)
|
|
280
|
+
- Need consistent cross-browser rendering
|
|
281
|
+
- Building games or creative tools
|
|
282
|
+
- Performance is critical
|
|
283
|
+
- Building for canvas-based platforms
|
|
284
|
+
|
|
285
|
+
**Use DOM Renderer when:**
|
|
286
|
+
- Building standard web apps
|
|
287
|
+
- Need rich text formatting
|
|
288
|
+
- Need CSS animations
|
|
289
|
+
- Want browser DevTools support
|
|
290
|
+
- Accessibility is paramount
|
|
291
|
+
|
|
292
|
+
## API Reference
|
|
293
|
+
|
|
294
|
+
### CanvasRenderer
|
|
295
|
+
|
|
296
|
+
```typescript
|
|
297
|
+
class CanvasRenderer implements Renderer {
|
|
298
|
+
constructor(
|
|
299
|
+
canvas: HTMLCanvasElement,
|
|
300
|
+
engine: Engine,
|
|
301
|
+
options?: Partial<CanvasRendererOptions>
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
// Renderer interface
|
|
305
|
+
applyPatches(patches: Patch[]): void
|
|
306
|
+
getNode(id: string): VirtualNode | undefined
|
|
307
|
+
clear(): void
|
|
308
|
+
|
|
309
|
+
// Canvas-specific
|
|
310
|
+
registerPainter(type: string, painter: PainterFunction): void
|
|
311
|
+
setOptions(options: Partial<CanvasRendererOptions>): void
|
|
312
|
+
destroy(): void
|
|
313
|
+
}
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
### Custom Painters
|
|
317
|
+
|
|
318
|
+
```typescript
|
|
319
|
+
type PainterFunction = (
|
|
320
|
+
ctx: CanvasRenderingContext2D,
|
|
321
|
+
node: VirtualNode
|
|
322
|
+
) => void;
|
|
323
|
+
|
|
324
|
+
renderer.registerPainter("Circle", (ctx, node) => {
|
|
325
|
+
const layout = node.layout!;
|
|
326
|
+
const radius = Math.min(layout.width, layout.height) / 2;
|
|
327
|
+
const centerX = layout.x + layout.width / 2;
|
|
328
|
+
const centerY = layout.y + layout.height / 2;
|
|
329
|
+
|
|
330
|
+
ctx.beginPath();
|
|
331
|
+
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
|
|
332
|
+
ctx.fillStyle = node.props.color || "#000";
|
|
333
|
+
ctx.fill();
|
|
334
|
+
});
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
## Testing
|
|
338
|
+
|
|
339
|
+
The canvas renderer includes comprehensive tests:
|
|
340
|
+
|
|
341
|
+
```bash
|
|
342
|
+
# Run tests
|
|
343
|
+
bun test src/canvas
|
|
344
|
+
|
|
345
|
+
# Run with coverage
|
|
346
|
+
bun test --coverage src/canvas
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
## Contributing
|
|
350
|
+
|
|
351
|
+
The canvas renderer is under active development. Contributions welcome!
|
|
352
|
+
|
|
353
|
+
**Current Priorities:**
|
|
354
|
+
1. Text selection support
|
|
355
|
+
2. Gradient/shadow support
|
|
356
|
+
3. Performance optimizations (dirty rects, layer caching)
|
|
357
|
+
4. Advanced text (rich text, ligatures)
|
|
358
|
+
5. More component painters
|
|
359
|
+
|
|
360
|
+
## See Also
|
|
361
|
+
|
|
362
|
+
- [Main README](../../README.md) - SDK documentation
|
|
363
|
+
- [DOM Renderer](../dom/README.md) - DOM renderer documentation
|
|
364
|
+
- [Plan](./plan.md) - Implementation plan and roadmap
|
|
365
|
+
- [Canvas API Docs](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API)
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Accessibility Layer
|
|
3
|
+
*
|
|
4
|
+
* Maintain shadow DOM for screen readers
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { VirtualNode } from "./types.js";
|
|
8
|
+
import { walkTree } from "./utils.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Accessibility Manager
|
|
12
|
+
*/
|
|
13
|
+
export class AccessibilityLayer {
|
|
14
|
+
private shadowRoot: HTMLElement;
|
|
15
|
+
private nodeMap = new Map<string, HTMLElement>();
|
|
16
|
+
private enabled: boolean;
|
|
17
|
+
|
|
18
|
+
constructor(container: HTMLElement | null, enabled: boolean = true) {
|
|
19
|
+
this.enabled = enabled && typeof document !== "undefined";
|
|
20
|
+
|
|
21
|
+
// Create shadow root container (only in browser)
|
|
22
|
+
if (this.enabled && typeof document !== "undefined") {
|
|
23
|
+
this.shadowRoot = document.createElement("div");
|
|
24
|
+
this.shadowRoot.setAttribute("role", "application");
|
|
25
|
+
this.shadowRoot.setAttribute("aria-label", "Hypen Canvas Application");
|
|
26
|
+
this.shadowRoot.style.position = "absolute";
|
|
27
|
+
this.shadowRoot.style.left = "-9999px";
|
|
28
|
+
this.shadowRoot.style.width = "1px";
|
|
29
|
+
this.shadowRoot.style.height = "1px";
|
|
30
|
+
this.shadowRoot.style.overflow = "hidden";
|
|
31
|
+
|
|
32
|
+
if (container) {
|
|
33
|
+
container.appendChild(this.shadowRoot);
|
|
34
|
+
}
|
|
35
|
+
} else {
|
|
36
|
+
// Fallback for non-browser environments
|
|
37
|
+
this.shadowRoot = {} as HTMLElement;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Sync shadow DOM with virtual tree
|
|
43
|
+
*/
|
|
44
|
+
syncTree(root: VirtualNode): void {
|
|
45
|
+
if (!this.enabled) return;
|
|
46
|
+
|
|
47
|
+
// Clear existing
|
|
48
|
+
this.shadowRoot.innerHTML = "";
|
|
49
|
+
this.nodeMap.clear();
|
|
50
|
+
|
|
51
|
+
// Build shadow DOM
|
|
52
|
+
const shadowNode = this.createShadowNode(root);
|
|
53
|
+
if (shadowNode) {
|
|
54
|
+
this.shadowRoot.appendChild(shadowNode);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Create shadow DOM element for virtual node
|
|
60
|
+
*/
|
|
61
|
+
private createShadowNode(node: VirtualNode): HTMLElement | null {
|
|
62
|
+
if (!node.visible) return null;
|
|
63
|
+
|
|
64
|
+
let element: HTMLElement;
|
|
65
|
+
|
|
66
|
+
// Create appropriate HTML element
|
|
67
|
+
switch (node.type.toLowerCase()) {
|
|
68
|
+
case "button":
|
|
69
|
+
element = document.createElement("button");
|
|
70
|
+
element.textContent = this.getNodeText(node);
|
|
71
|
+
if (node.props.onclick) {
|
|
72
|
+
element.setAttribute("aria-label", "Clickable button");
|
|
73
|
+
}
|
|
74
|
+
break;
|
|
75
|
+
|
|
76
|
+
case "input":
|
|
77
|
+
element = document.createElement("input");
|
|
78
|
+
(element as HTMLInputElement).type = node.props.type || "text";
|
|
79
|
+
(element as HTMLInputElement).value = node.props.value || "";
|
|
80
|
+
if (node.props.placeholder) {
|
|
81
|
+
(element as HTMLInputElement).placeholder = node.props.placeholder;
|
|
82
|
+
}
|
|
83
|
+
break;
|
|
84
|
+
|
|
85
|
+
case "textarea":
|
|
86
|
+
element = document.createElement("textarea");
|
|
87
|
+
(element as HTMLTextAreaElement).value = node.props.value || "";
|
|
88
|
+
if (node.props.placeholder) {
|
|
89
|
+
(element as HTMLTextAreaElement).placeholder = node.props.placeholder;
|
|
90
|
+
}
|
|
91
|
+
break;
|
|
92
|
+
|
|
93
|
+
case "image":
|
|
94
|
+
element = document.createElement("img");
|
|
95
|
+
(element as HTMLImageElement).src = node.props.src || "";
|
|
96
|
+
(element as HTMLImageElement).alt = node.props.alt || "Image";
|
|
97
|
+
break;
|
|
98
|
+
|
|
99
|
+
case "text":
|
|
100
|
+
element = document.createElement("span");
|
|
101
|
+
element.textContent = String(node.props[0] || node.props.text || "");
|
|
102
|
+
break;
|
|
103
|
+
|
|
104
|
+
case "column":
|
|
105
|
+
case "row":
|
|
106
|
+
case "container":
|
|
107
|
+
case "box":
|
|
108
|
+
element = document.createElement("div");
|
|
109
|
+
element.setAttribute("role", node.type === "column" ? "group" : "group");
|
|
110
|
+
break;
|
|
111
|
+
|
|
112
|
+
default:
|
|
113
|
+
element = document.createElement("div");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Set common attributes
|
|
117
|
+
element.setAttribute("data-hypen-id", node.id);
|
|
118
|
+
|
|
119
|
+
if (node.props["aria-label"]) {
|
|
120
|
+
element.setAttribute("aria-label", node.props["aria-label"]);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (node.focusable) {
|
|
124
|
+
element.tabIndex = 0;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Store mapping
|
|
128
|
+
this.nodeMap.set(node.id, element);
|
|
129
|
+
|
|
130
|
+
// Add children
|
|
131
|
+
for (const child of node.children) {
|
|
132
|
+
const childElement = this.createShadowNode(child);
|
|
133
|
+
if (childElement) {
|
|
134
|
+
element.appendChild(childElement);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return element;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Get text content from node tree
|
|
143
|
+
*/
|
|
144
|
+
private getNodeText(node: VirtualNode): string {
|
|
145
|
+
if (node.type === "text") {
|
|
146
|
+
return String(node.props[0] || node.props.text || "");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
let text = "";
|
|
150
|
+
for (const child of node.children) {
|
|
151
|
+
text += this.getNodeText(child);
|
|
152
|
+
}
|
|
153
|
+
return text;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Focus node in shadow DOM
|
|
158
|
+
*/
|
|
159
|
+
focusNode(nodeId: string): void {
|
|
160
|
+
if (!this.enabled) return;
|
|
161
|
+
|
|
162
|
+
const element = this.nodeMap.get(nodeId);
|
|
163
|
+
if (element) {
|
|
164
|
+
element.focus();
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Update single node
|
|
170
|
+
*/
|
|
171
|
+
updateNode(node: VirtualNode): void {
|
|
172
|
+
if (!this.enabled) return;
|
|
173
|
+
|
|
174
|
+
const element = this.nodeMap.get(node.id);
|
|
175
|
+
if (!element) return;
|
|
176
|
+
|
|
177
|
+
// Update text content
|
|
178
|
+
if (node.type === "text") {
|
|
179
|
+
element.textContent = String(node.props[0] || node.props.text || "");
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Update input value
|
|
183
|
+
if (node.type === "input" || node.type === "textarea") {
|
|
184
|
+
(element as HTMLInputElement | HTMLTextAreaElement).value = node.props.value || "";
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Update visibility
|
|
188
|
+
element.style.display = node.visible ? "" : "none";
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Get shadow element by node ID
|
|
193
|
+
*/
|
|
194
|
+
getElement(nodeId: string): HTMLElement | undefined {
|
|
195
|
+
return this.nodeMap.get(nodeId);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Enable or disable accessibility layer
|
|
200
|
+
*/
|
|
201
|
+
setEnabled(enabled: boolean): void {
|
|
202
|
+
this.enabled = enabled;
|
|
203
|
+
if (!enabled && this.shadowRoot.parentElement) {
|
|
204
|
+
this.shadowRoot.remove();
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Cleanup
|
|
210
|
+
*/
|
|
211
|
+
destroy(): void {
|
|
212
|
+
if (this.shadowRoot.parentElement) {
|
|
213
|
+
this.shadowRoot.remove();
|
|
214
|
+
}
|
|
215
|
+
this.nodeMap.clear();
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|