@hypen-space/web 0.4.35 → 0.4.37
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/canvas/accessibility.js +174 -174
- package/dist/canvas/accessibility.js.map +1 -10
- package/dist/canvas/events.js +264 -360
- package/dist/canvas/events.js.map +1 -11
- package/dist/canvas/index.js +11 -47
- package/dist/canvas/index.js.map +1 -9
- package/dist/canvas/input.js +178 -289
- package/dist/canvas/input.js.map +1 -11
- package/dist/canvas/layout.js +349 -547
- package/dist/canvas/layout.js.map +1 -12
- package/dist/canvas/paint.js +1122 -1146
- package/dist/canvas/paint.js.map +1 -12
- package/dist/canvas/renderer.js +352 -2259
- package/dist/canvas/renderer.js.map +1 -17
- package/dist/canvas/text.js +129 -264
- package/dist/canvas/text.js.map +1 -11
- package/dist/canvas/types.js +7 -2
- package/dist/canvas/types.js.map +1 -9
- package/dist/canvas/utils.js +158 -161
- package/dist/canvas/utils.js.map +1 -10
- package/dist/client.js +19 -45
- package/dist/client.js.map +1 -9
- package/dist/dom/applicators/advanced-layout.js +105 -138
- package/dist/dom/applicators/advanced-layout.js.map +1 -10
- package/dist/dom/applicators/background.js +36 -76
- package/dist/dom/applicators/background.js.map +1 -10
- package/dist/dom/applicators/border.js +82 -106
- package/dist/dom/applicators/border.js.map +1 -10
- package/dist/dom/applicators/color.js +17 -58
- package/dist/dom/applicators/color.js.map +1 -10
- package/dist/dom/applicators/display.js +39 -79
- package/dist/dom/applicators/display.js.map +1 -10
- package/dist/dom/applicators/effects.js +121 -147
- package/dist/dom/applicators/effects.js.map +1 -10
- package/dist/dom/applicators/events.js +413 -450
- package/dist/dom/applicators/events.js.map +1 -11
- package/dist/dom/applicators/font.js +119 -140
- package/dist/dom/applicators/font.js.map +1 -10
- package/dist/dom/applicators/index.js +386 -1770
- package/dist/dom/applicators/index.js.map +1 -26
- package/dist/dom/applicators/layout.js +143 -149
- package/dist/dom/applicators/layout.js.map +1 -10
- package/dist/dom/applicators/margin.js +64 -105
- package/dist/dom/applicators/margin.js.map +1 -10
- package/dist/dom/applicators/padding.js +64 -105
- package/dist/dom/applicators/padding.js.map +1 -10
- package/dist/dom/applicators/size.js +169 -172
- package/dist/dom/applicators/size.js.map +1 -10
- package/dist/dom/applicators/transform.js +73 -114
- package/dist/dom/applicators/transform.js.map +1 -10
- package/dist/dom/applicators/transition.js +47 -88
- package/dist/dom/applicators/transition.js.map +1 -10
- package/dist/dom/applicators/types.js +7 -2
- package/dist/dom/applicators/types.js.map +1 -9
- package/dist/dom/applicators/typography.js +68 -109
- package/dist/dom/applicators/typography.js.map +1 -10
- package/dist/dom/canvas/index.js +49 -83
- package/dist/dom/canvas/index.js.map +1 -10
- package/dist/dom/components/app.js +16 -63
- package/dist/dom/components/app.js.map +1 -10
- package/dist/dom/components/audio.js +31 -76
- package/dist/dom/components/audio.js.map +1 -10
- package/dist/dom/components/avatar.js +41 -87
- package/dist/dom/components/avatar.js.map +1 -10
- package/dist/dom/components/badge.js +35 -83
- package/dist/dom/components/badge.js.map +1 -10
- package/dist/dom/components/button.js +26 -69
- package/dist/dom/components/button.js.map +1 -10
- package/dist/dom/components/card.js +13 -63
- package/dist/dom/components/card.js.map +1 -10
- package/dist/dom/components/center.js +21 -65
- package/dist/dom/components/center.js.map +1 -10
- package/dist/dom/components/checkbox.js +37 -82
- package/dist/dom/components/checkbox.js.map +1 -10
- package/dist/dom/components/column.js +16 -62
- package/dist/dom/components/column.js.map +1 -10
- package/dist/dom/components/container.js +11 -59
- package/dist/dom/components/container.js.map +1 -10
- package/dist/dom/components/divider.js +27 -73
- package/dist/dom/components/divider.js.map +1 -10
- package/dist/dom/components/grid.js +41 -80
- package/dist/dom/components/grid.js.map +1 -10
- package/dist/dom/components/heading.js +28 -74
- package/dist/dom/components/heading.js.map +1 -10
- package/dist/dom/components/hypenapp.js +291 -288
- package/dist/dom/components/hypenapp.js.map +1 -10
- package/dist/dom/components/image.js +18 -67
- package/dist/dom/components/image.js.map +1 -10
- package/dist/dom/components/index.js +109 -1427
- package/dist/dom/components/index.js.map +1 -42
- package/dist/dom/components/input.js +19 -69
- package/dist/dom/components/input.js.map +1 -10
- package/dist/dom/components/link.js +23 -70
- package/dist/dom/components/link.js.map +1 -10
- package/dist/dom/components/list.js +26 -72
- package/dist/dom/components/list.js.map +1 -10
- package/dist/dom/components/paragraph.js +14 -63
- package/dist/dom/components/paragraph.js.map +1 -10
- package/dist/dom/components/progressbar.js +38 -85
- package/dist/dom/components/progressbar.js.map +1 -10
- package/dist/dom/components/route.js +27 -73
- package/dist/dom/components/route.js.map +1 -10
- package/dist/dom/components/router.js +15 -62
- package/dist/dom/components/router.js.map +1 -10
- package/dist/dom/components/row.js +25 -70
- package/dist/dom/components/row.js.map +1 -10
- package/dist/dom/components/select.js +42 -85
- package/dist/dom/components/select.js.map +1 -10
- package/dist/dom/components/slider.js +31 -76
- package/dist/dom/components/slider.js.map +1 -10
- package/dist/dom/components/spacer.js +10 -60
- package/dist/dom/components/spacer.js.map +1 -10
- package/dist/dom/components/spinner.js +41 -88
- package/dist/dom/components/spinner.js.map +1 -10
- package/dist/dom/components/stack.js +19 -67
- package/dist/dom/components/stack.js.map +1 -10
- package/dist/dom/components/switch.js +50 -93
- package/dist/dom/components/switch.js.map +1 -10
- package/dist/dom/components/text.js +24 -69
- package/dist/dom/components/text.js.map +1 -10
- package/dist/dom/components/textarea.js +35 -79
- package/dist/dom/components/textarea.js.map +1 -10
- package/dist/dom/components/video.js +35 -79
- package/dist/dom/components/video.js.map +1 -10
- package/dist/dom/debug.js +159 -160
- package/dist/dom/debug.js.map +1 -10
- package/dist/dom/element-data.js +181 -143
- package/dist/dom/element-data.js.map +1 -10
- package/dist/dom/events.js +133 -142
- package/dist/dom/events.js.map +1 -10
- package/dist/dom/index.js +11 -48
- package/dist/dom/index.js.map +1 -9
- package/dist/dom/renderer.js +311 -3596
- package/dist/dom/renderer.js.map +1 -62
- package/dist/hypen.js +385 -3880
- package/dist/hypen.js.map +1 -63
- package/dist/index.js +36 -53
- package/dist/index.js.map +1 -9
- package/package.json +2 -2
package/dist/canvas/paint.js
CHANGED
|
@@ -1,1218 +1,1194 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Paint System
|
|
3
|
+
*
|
|
4
|
+
* Drawing virtual nodes to canvas
|
|
5
|
+
*/
|
|
6
|
+
import { renderText } from "./text.js";
|
|
7
|
+
/**
|
|
8
|
+
* Custom painters registry
|
|
9
|
+
*/
|
|
10
|
+
const customPainters = new Map();
|
|
11
|
+
/**
|
|
12
|
+
* Register a custom painter for a component type
|
|
13
|
+
*/
|
|
14
|
+
export function registerPainter(type, painter) {
|
|
15
|
+
customPainters.set(type.toLowerCase(), painter);
|
|
7
16
|
}
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if (px > x + width - radius && py > y + height - radius) {
|
|
99
|
-
return Math.pow(px - (x + width - radius), 2) + Math.pow(py - (y + height - radius), 2) <= Math.pow(radius, 2);
|
|
100
|
-
}
|
|
101
|
-
return true;
|
|
102
|
-
}
|
|
103
|
-
function mergeRects(rects) {
|
|
104
|
-
if (rects.length === 0)
|
|
105
|
-
return null;
|
|
106
|
-
if (rects.length === 1)
|
|
107
|
-
return rects[0];
|
|
108
|
-
let minX = Infinity;
|
|
109
|
-
let minY = Infinity;
|
|
110
|
-
let maxX = -Infinity;
|
|
111
|
-
let maxY = -Infinity;
|
|
112
|
-
for (const rect of rects) {
|
|
113
|
-
minX = Math.min(minX, rect.x);
|
|
114
|
-
minY = Math.min(minY, rect.y);
|
|
115
|
-
maxX = Math.max(maxX, rect.x + rect.width);
|
|
116
|
-
maxY = Math.max(maxY, rect.y + rect.height);
|
|
117
|
-
}
|
|
118
|
-
return {
|
|
119
|
-
x: minX,
|
|
120
|
-
y: minY,
|
|
121
|
-
width: maxX - minX,
|
|
122
|
-
height: maxY - minY
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
function createFontString(fontSize, fontWeight, fontFamily) {
|
|
126
|
-
return `${fontWeight} ${fontSize}px ${fontFamily}`;
|
|
127
|
-
}
|
|
128
|
-
function walkTree(node, callback) {
|
|
129
|
-
callback(node);
|
|
130
|
-
for (const child of node.children) {
|
|
131
|
-
walkTree(child, callback);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
function findNodeById(root, id) {
|
|
135
|
-
if (root.id === id)
|
|
136
|
-
return root;
|
|
137
|
-
for (const child of root.children) {
|
|
138
|
-
const found = findNodeById(child, id);
|
|
139
|
-
if (found)
|
|
140
|
-
return found;
|
|
141
|
-
}
|
|
142
|
-
return null;
|
|
143
|
-
}
|
|
144
|
-
function getAbsoluteBounds(node) {
|
|
145
|
-
if (!node.layout)
|
|
146
|
-
return null;
|
|
147
|
-
let x = node.layout.x;
|
|
148
|
-
let y = node.layout.y;
|
|
149
|
-
let current = node.parent;
|
|
150
|
-
while (current && current.layout) {
|
|
151
|
-
x += current.layout.contentX;
|
|
152
|
-
y += current.layout.contentY;
|
|
153
|
-
current = current.parent;
|
|
154
|
-
}
|
|
155
|
-
return {
|
|
156
|
-
x,
|
|
157
|
-
y,
|
|
158
|
-
width: node.layout.width,
|
|
159
|
-
height: node.layout.height
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// src/canvas/text.ts
|
|
164
|
-
import { frameworkLoggers } from "@hypen-space/core/logger";
|
|
165
|
-
var log = frameworkLoggers.canvas;
|
|
166
|
-
var textMetricsCache = new Map;
|
|
167
|
-
function getCacheKey(text, fontStyle, maxWidth) {
|
|
168
|
-
return `${text}|${fontStyle.fontSize}|${fontStyle.fontWeight}|${fontStyle.fontFamily}|${maxWidth || "auto"}`;
|
|
169
|
-
}
|
|
170
|
-
function measureText(ctx, text, fontStyle, maxWidth) {
|
|
171
|
-
const cacheKey = getCacheKey(text, fontStyle, maxWidth);
|
|
172
|
-
const cached = textMetricsCache.get(cacheKey);
|
|
173
|
-
if (cached)
|
|
174
|
-
return cached;
|
|
175
|
-
const font = createFontString(fontStyle.fontSize, fontStyle.fontWeight, fontStyle.fontFamily);
|
|
176
|
-
ctx.save();
|
|
177
|
-
ctx.font = font;
|
|
178
|
-
const lineHeight = fontStyle.lineHeight || fontStyle.fontSize * 1.2;
|
|
179
|
-
if (!maxWidth) {
|
|
180
|
-
const metrics = ctx.measureText(text);
|
|
181
|
-
const result2 = {
|
|
182
|
-
width: metrics.width,
|
|
183
|
-
height: lineHeight,
|
|
184
|
-
lines: [text],
|
|
185
|
-
lineHeight
|
|
186
|
-
};
|
|
187
|
-
ctx.restore();
|
|
188
|
-
textMetricsCache.set(cacheKey, result2);
|
|
189
|
-
return result2;
|
|
190
|
-
}
|
|
191
|
-
const lines = wrapText(ctx, text, maxWidth);
|
|
192
|
-
const width = Math.max(...lines.map((line) => ctx.measureText(line).width));
|
|
193
|
-
const height = lines.length * lineHeight;
|
|
194
|
-
const result = {
|
|
195
|
-
width,
|
|
196
|
-
height,
|
|
197
|
-
lines,
|
|
198
|
-
lineHeight
|
|
199
|
-
};
|
|
200
|
-
ctx.restore();
|
|
201
|
-
textMetricsCache.set(cacheKey, result);
|
|
202
|
-
return result;
|
|
203
|
-
}
|
|
204
|
-
function wrapText(ctx, text, maxWidth) {
|
|
205
|
-
const lines = [];
|
|
206
|
-
const paragraphs = text.split(`
|
|
207
|
-
`);
|
|
208
|
-
for (const paragraph of paragraphs) {
|
|
209
|
-
const words = paragraph.split(" ");
|
|
210
|
-
let currentLine = "";
|
|
211
|
-
for (const word of words) {
|
|
212
|
-
const testLine = currentLine ? `${currentLine} ${word}` : word;
|
|
213
|
-
const metrics = ctx.measureText(testLine);
|
|
214
|
-
if (metrics.width > maxWidth && currentLine) {
|
|
215
|
-
lines.push(currentLine);
|
|
216
|
-
currentLine = word;
|
|
217
|
-
} else {
|
|
218
|
-
currentLine = testLine;
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
if (currentLine) {
|
|
222
|
-
lines.push(currentLine);
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
return lines.length > 0 ? lines : [""];
|
|
226
|
-
}
|
|
227
|
-
function renderText(ctx, text, x, y, width, height, style) {
|
|
228
|
-
const font = createFontString(style.fontSize, style.fontWeight, style.fontFamily);
|
|
229
|
-
ctx.save();
|
|
230
|
-
ctx.font = font;
|
|
231
|
-
ctx.fillStyle = style.color;
|
|
232
|
-
ctx.textBaseline = "top";
|
|
233
|
-
const metrics = measureText(ctx, text, style, width);
|
|
234
|
-
let startY = y;
|
|
235
|
-
if (style.verticalAlign === "middle") {
|
|
236
|
-
startY = y + (height - metrics.height) / 2;
|
|
237
|
-
} else if (style.verticalAlign === "bottom") {
|
|
238
|
-
startY = y + height - metrics.height;
|
|
239
|
-
}
|
|
240
|
-
for (let i = 0;i < metrics.lines.length; i++) {
|
|
241
|
-
const line = metrics.lines[i];
|
|
242
|
-
const lineY = startY + i * metrics.lineHeight;
|
|
243
|
-
let lineX = x;
|
|
244
|
-
if (style.textAlign === "center") {
|
|
245
|
-
const lineWidth = ctx.measureText(line).width;
|
|
246
|
-
lineX = x + (width - lineWidth) / 2;
|
|
247
|
-
} else if (style.textAlign === "right") {
|
|
248
|
-
const lineWidth = ctx.measureText(line).width;
|
|
249
|
-
lineX = x + width - lineWidth;
|
|
250
|
-
}
|
|
251
|
-
ctx.fillText(line, lineX, lineY);
|
|
252
|
-
}
|
|
253
|
-
ctx.restore();
|
|
254
|
-
}
|
|
255
|
-
function clearTextCache() {
|
|
256
|
-
textMetricsCache.clear();
|
|
257
|
-
}
|
|
258
|
-
async function loadFont(fontFamily, fontWeight = "normal") {
|
|
259
|
-
if (!("fonts" in document))
|
|
260
|
-
return;
|
|
261
|
-
const font = `${fontWeight} 16px ${fontFamily}`;
|
|
262
|
-
try {
|
|
263
|
-
await document.fonts.load(font);
|
|
264
|
-
} catch (error) {
|
|
265
|
-
log.warn(`Failed to load font: ${font}`, error);
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// src/canvas/paint.ts
|
|
270
|
-
var customPainters = new Map;
|
|
271
|
-
function registerPainter(type, painter) {
|
|
272
|
-
customPainters.set(type.toLowerCase(), painter);
|
|
273
|
-
}
|
|
274
|
-
function paintNode(ctx, node) {
|
|
275
|
-
if (!node.visible || !node.layout)
|
|
276
|
-
return;
|
|
277
|
-
ctx.save();
|
|
278
|
-
applyTransforms(ctx, node);
|
|
279
|
-
if (node.opacity < 1) {
|
|
280
|
-
ctx.globalAlpha = node.opacity;
|
|
281
|
-
}
|
|
282
|
-
const customPainter = customPainters.get(node.type.toLowerCase());
|
|
283
|
-
if (customPainter) {
|
|
284
|
-
customPainter(ctx, node);
|
|
285
|
-
ctx.restore();
|
|
286
|
-
return;
|
|
287
|
-
}
|
|
288
|
-
switch (node.type.toLowerCase()) {
|
|
289
|
-
case "column":
|
|
290
|
-
case "row":
|
|
291
|
-
case "stack":
|
|
292
|
-
paintContainer(ctx, node);
|
|
293
|
-
break;
|
|
294
|
-
case "text":
|
|
295
|
-
paintText(ctx, node);
|
|
296
|
-
break;
|
|
297
|
-
case "button":
|
|
298
|
-
paintButton(ctx, node);
|
|
299
|
-
break;
|
|
300
|
-
case "input":
|
|
301
|
-
paintInput(ctx, node);
|
|
302
|
-
break;
|
|
303
|
-
case "image":
|
|
304
|
-
paintImage(ctx, node);
|
|
305
|
-
break;
|
|
306
|
-
case "spacer":
|
|
307
|
-
break;
|
|
308
|
-
case "divider":
|
|
309
|
-
case "separator":
|
|
310
|
-
paintDivider(ctx, node);
|
|
311
|
-
break;
|
|
312
|
-
case "checkbox":
|
|
313
|
-
paintCheckbox(ctx, node);
|
|
314
|
-
break;
|
|
315
|
-
case "radio":
|
|
316
|
-
paintRadio(ctx, node);
|
|
317
|
-
break;
|
|
318
|
-
case "switch":
|
|
319
|
-
case "toggle":
|
|
320
|
-
paintSwitch(ctx, node);
|
|
321
|
-
break;
|
|
322
|
-
case "slider":
|
|
323
|
-
paintSlider(ctx, node);
|
|
324
|
-
break;
|
|
325
|
-
case "progress":
|
|
326
|
-
case "progressbar":
|
|
327
|
-
paintProgress(ctx, node);
|
|
328
|
-
break;
|
|
329
|
-
case "spinner":
|
|
330
|
-
case "loading":
|
|
331
|
-
paintSpinner(ctx, node);
|
|
332
|
-
break;
|
|
333
|
-
case "card":
|
|
334
|
-
paintCard(ctx, node);
|
|
335
|
-
break;
|
|
336
|
-
case "badge":
|
|
337
|
-
paintBadge(ctx, node);
|
|
338
|
-
break;
|
|
339
|
-
case "avatar":
|
|
340
|
-
paintAvatar(ctx, node);
|
|
341
|
-
break;
|
|
342
|
-
case "icon":
|
|
343
|
-
paintIcon(ctx, node);
|
|
344
|
-
break;
|
|
345
|
-
case "link":
|
|
346
|
-
paintLink(ctx, node);
|
|
347
|
-
break;
|
|
348
|
-
case "app":
|
|
349
|
-
case "container":
|
|
350
|
-
case "box":
|
|
351
|
-
paintContainer(ctx, node);
|
|
352
|
-
break;
|
|
353
|
-
default:
|
|
354
|
-
paintContainer(ctx, node);
|
|
355
|
-
}
|
|
356
|
-
ctx.restore();
|
|
357
|
-
for (const child of node.children) {
|
|
358
|
-
paintNode(ctx, child);
|
|
359
|
-
}
|
|
360
|
-
if (node._needsRestore) {
|
|
17
|
+
/**
|
|
18
|
+
* Paint a virtual node and its children
|
|
19
|
+
*/
|
|
20
|
+
export function paintNode(ctx, node) {
|
|
21
|
+
if (!node.visible || !node.layout)
|
|
22
|
+
return;
|
|
23
|
+
ctx.save();
|
|
24
|
+
// Apply transforms
|
|
25
|
+
applyTransforms(ctx, node);
|
|
26
|
+
// Apply opacity
|
|
27
|
+
if (node.opacity < 1) {
|
|
28
|
+
ctx.globalAlpha = node.opacity;
|
|
29
|
+
}
|
|
30
|
+
// Check for custom painter
|
|
31
|
+
const customPainter = customPainters.get(node.type.toLowerCase());
|
|
32
|
+
if (customPainter) {
|
|
33
|
+
customPainter(ctx, node);
|
|
34
|
+
ctx.restore();
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
// Default painting based on type
|
|
38
|
+
switch (node.type.toLowerCase()) {
|
|
39
|
+
case "column":
|
|
40
|
+
case "row":
|
|
41
|
+
case "stack":
|
|
42
|
+
paintContainer(ctx, node);
|
|
43
|
+
break;
|
|
44
|
+
case "text":
|
|
45
|
+
paintText(ctx, node);
|
|
46
|
+
break;
|
|
47
|
+
case "button":
|
|
48
|
+
paintButton(ctx, node);
|
|
49
|
+
break;
|
|
50
|
+
case "input":
|
|
51
|
+
paintInput(ctx, node);
|
|
52
|
+
break;
|
|
53
|
+
case "image":
|
|
54
|
+
paintImage(ctx, node);
|
|
55
|
+
break;
|
|
56
|
+
case "spacer":
|
|
57
|
+
// Spacer is invisible, just takes up space
|
|
58
|
+
break;
|
|
59
|
+
case "divider":
|
|
60
|
+
case "separator":
|
|
61
|
+
paintDivider(ctx, node);
|
|
62
|
+
break;
|
|
63
|
+
case "checkbox":
|
|
64
|
+
paintCheckbox(ctx, node);
|
|
65
|
+
break;
|
|
66
|
+
case "radio":
|
|
67
|
+
paintRadio(ctx, node);
|
|
68
|
+
break;
|
|
69
|
+
case "switch":
|
|
70
|
+
case "toggle":
|
|
71
|
+
paintSwitch(ctx, node);
|
|
72
|
+
break;
|
|
73
|
+
case "slider":
|
|
74
|
+
paintSlider(ctx, node);
|
|
75
|
+
break;
|
|
76
|
+
case "progress":
|
|
77
|
+
case "progressbar":
|
|
78
|
+
paintProgress(ctx, node);
|
|
79
|
+
break;
|
|
80
|
+
case "spinner":
|
|
81
|
+
case "loading":
|
|
82
|
+
paintSpinner(ctx, node);
|
|
83
|
+
break;
|
|
84
|
+
case "card":
|
|
85
|
+
paintCard(ctx, node);
|
|
86
|
+
break;
|
|
87
|
+
case "badge":
|
|
88
|
+
paintBadge(ctx, node);
|
|
89
|
+
break;
|
|
90
|
+
case "avatar":
|
|
91
|
+
paintAvatar(ctx, node);
|
|
92
|
+
break;
|
|
93
|
+
case "icon":
|
|
94
|
+
paintIcon(ctx, node);
|
|
95
|
+
break;
|
|
96
|
+
case "link":
|
|
97
|
+
paintLink(ctx, node);
|
|
98
|
+
break;
|
|
99
|
+
case "app":
|
|
100
|
+
case "container":
|
|
101
|
+
case "box":
|
|
102
|
+
paintContainer(ctx, node);
|
|
103
|
+
break;
|
|
104
|
+
default:
|
|
105
|
+
paintContainer(ctx, node);
|
|
106
|
+
}
|
|
361
107
|
ctx.restore();
|
|
362
|
-
|
|
363
|
-
|
|
108
|
+
// Paint children
|
|
109
|
+
for (const child of node.children) {
|
|
110
|
+
paintNode(ctx, child);
|
|
111
|
+
}
|
|
112
|
+
// Restore context if overflow clipping was applied
|
|
113
|
+
if (node._needsRestore) {
|
|
114
|
+
ctx.restore();
|
|
115
|
+
delete node._needsRestore;
|
|
116
|
+
}
|
|
364
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* Paint a container (Column, Row, Stack, Box)
|
|
120
|
+
*/
|
|
365
121
|
function paintContainer(ctx, node) {
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
122
|
+
const layout = node.layout;
|
|
123
|
+
const props = node.props;
|
|
124
|
+
const x = layout.x;
|
|
125
|
+
const y = layout.y;
|
|
126
|
+
const width = layout.width;
|
|
127
|
+
const height = layout.height;
|
|
128
|
+
const radius = layout.border.radius;
|
|
129
|
+
// Apply shadow if specified
|
|
130
|
+
const shadow = props.shadow || props.boxShadow;
|
|
131
|
+
if (shadow) {
|
|
132
|
+
applyShadow(ctx, shadow);
|
|
133
|
+
}
|
|
134
|
+
// Draw background
|
|
135
|
+
const backgroundColor = props.backgroundColor || props.background;
|
|
136
|
+
if (backgroundColor) {
|
|
137
|
+
// Support for gradients
|
|
138
|
+
if (typeof backgroundColor === "string" && backgroundColor.includes("gradient")) {
|
|
139
|
+
ctx.fillStyle = parseGradient(ctx, backgroundColor, x, y, width, height);
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
ctx.fillStyle = backgroundColor;
|
|
143
|
+
}
|
|
144
|
+
if (radius > 0) {
|
|
145
|
+
drawRoundedRect(ctx, x, y, width, height, radius);
|
|
146
|
+
ctx.fill();
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
ctx.fillRect(x, y, width, height);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// Reset shadow for border
|
|
153
|
+
if (shadow) {
|
|
154
|
+
ctx.shadowColor = "transparent";
|
|
155
|
+
ctx.shadowBlur = 0;
|
|
156
|
+
ctx.shadowOffsetX = 0;
|
|
157
|
+
ctx.shadowOffsetY = 0;
|
|
158
|
+
}
|
|
159
|
+
// Draw border
|
|
160
|
+
if (layout.border.width > 0 && layout.border.color !== "transparent") {
|
|
161
|
+
ctx.strokeStyle = layout.border.color;
|
|
162
|
+
ctx.lineWidth = layout.border.width;
|
|
163
|
+
if (radius > 0) {
|
|
164
|
+
drawRoundedRect(ctx, x, y, width, height, radius);
|
|
165
|
+
ctx.stroke();
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
ctx.strokeRect(x, y, width, height);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// Apply overflow clipping for children
|
|
172
|
+
const overflow = props.overflow || "visible";
|
|
173
|
+
if (overflow === "hidden" || overflow === "scroll" || overflow === "auto") {
|
|
174
|
+
ctx.save();
|
|
175
|
+
ctx.beginPath();
|
|
176
|
+
if (radius > 0) {
|
|
177
|
+
drawRoundedRect(ctx, x, y, width, height, radius);
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
ctx.rect(x, y, width, height);
|
|
181
|
+
}
|
|
182
|
+
ctx.clip();
|
|
183
|
+
// Mark that we need to restore later
|
|
184
|
+
node._needsRestore = true;
|
|
415
185
|
}
|
|
416
|
-
ctx.clip();
|
|
417
|
-
node._needsRestore = true;
|
|
418
|
-
}
|
|
419
186
|
}
|
|
187
|
+
/**
|
|
188
|
+
* Paint text node
|
|
189
|
+
*/
|
|
420
190
|
function paintText(ctx, node) {
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
191
|
+
const layout = node.layout;
|
|
192
|
+
const props = node.props;
|
|
193
|
+
let text = String(props[0] || props.text || "");
|
|
194
|
+
const color = props.color || "#000000";
|
|
195
|
+
const fontSize = parseFloat(props.fontSize) || 16;
|
|
196
|
+
const fontWeight = props.fontWeight || "normal";
|
|
197
|
+
const fontFamily = props.fontFamily || "system-ui, sans-serif";
|
|
198
|
+
const textAlign = props.textAlign || "left";
|
|
199
|
+
const lineHeight = parseFloat(props.lineHeight) || fontSize * 1.2;
|
|
200
|
+
const textDecoration = props.textDecoration || "none";
|
|
201
|
+
const textTransform = props.textTransform || "none";
|
|
202
|
+
const letterSpacing = parseFloat(props.letterSpacing) || 0;
|
|
203
|
+
// Apply text transform
|
|
204
|
+
if (textTransform === "uppercase") {
|
|
205
|
+
text = text.toUpperCase();
|
|
206
|
+
}
|
|
207
|
+
else if (textTransform === "lowercase") {
|
|
208
|
+
text = text.toLowerCase();
|
|
209
|
+
}
|
|
210
|
+
else if (textTransform === "capitalize") {
|
|
211
|
+
text = text.replace(/\b\w/g, (char) => char.toUpperCase());
|
|
212
|
+
}
|
|
213
|
+
// Apply text shadow if specified
|
|
214
|
+
const textShadow = props.textShadow || props.shadow;
|
|
215
|
+
if (textShadow) {
|
|
216
|
+
applyShadow(ctx, textShadow);
|
|
217
|
+
}
|
|
218
|
+
// Handle letter spacing
|
|
219
|
+
const x = layout.x + layout.contentX;
|
|
220
|
+
const y = layout.y + layout.contentY;
|
|
221
|
+
if (letterSpacing !== 0) {
|
|
222
|
+
// Manual letter spacing
|
|
223
|
+
ctx.fillStyle = color;
|
|
224
|
+
ctx.font = `${fontWeight} ${fontSize}px ${fontFamily}`;
|
|
225
|
+
ctx.textAlign = "left";
|
|
226
|
+
ctx.textBaseline = "top";
|
|
227
|
+
let currentX = x;
|
|
228
|
+
for (let i = 0; i < text.length; i++) {
|
|
229
|
+
ctx.fillText(text[i], currentX, y);
|
|
230
|
+
currentX += ctx.measureText(text[i]).width;
|
|
231
|
+
// Add letter spacing after each character except the last
|
|
232
|
+
if (i < text.length - 1) {
|
|
233
|
+
currentX += letterSpacing;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
// Text decoration with letter spacing (now correct width)
|
|
237
|
+
if (textDecoration !== "none") {
|
|
238
|
+
applyTextDecoration(ctx, textDecoration, color, x, y, currentX - x, fontSize);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
// Normal text rendering
|
|
243
|
+
renderText(ctx, text, x, y, layout.contentWidth, layout.contentHeight, {
|
|
244
|
+
color,
|
|
245
|
+
fontSize,
|
|
246
|
+
fontWeight,
|
|
247
|
+
fontFamily,
|
|
248
|
+
textAlign: textAlign,
|
|
249
|
+
verticalAlign: "top",
|
|
250
|
+
lineHeight,
|
|
251
|
+
});
|
|
252
|
+
// Text decoration (need to set font again for measurement)
|
|
253
|
+
if (textDecoration !== "none") {
|
|
254
|
+
ctx.font = `${fontWeight} ${fontSize}px ${fontFamily}`;
|
|
255
|
+
const textWidth = ctx.measureText(text).width;
|
|
256
|
+
applyTextDecoration(ctx, textDecoration, color, x, y, textWidth, fontSize);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
// Reset shadow
|
|
260
|
+
if (textShadow) {
|
|
261
|
+
ctx.shadowColor = "transparent";
|
|
262
|
+
ctx.shadowBlur = 0;
|
|
263
|
+
ctx.shadowOffsetX = 0;
|
|
264
|
+
ctx.shadowOffsetY = 0;
|
|
265
|
+
}
|
|
484
266
|
}
|
|
267
|
+
/**
|
|
268
|
+
* Paint button node
|
|
269
|
+
*/
|
|
485
270
|
function paintButton(ctx, node) {
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
backgroundColor = props.
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
ctx.shadowOffsetX = 0;
|
|
515
|
-
ctx.shadowOffsetY = 0;
|
|
516
|
-
}
|
|
517
|
-
if (layout.border.width > 0) {
|
|
518
|
-
ctx.strokeStyle = layout.border.color;
|
|
519
|
-
ctx.lineWidth = layout.border.width;
|
|
271
|
+
const layout = node.layout;
|
|
272
|
+
const props = node.props;
|
|
273
|
+
const x = layout.x;
|
|
274
|
+
const y = layout.y;
|
|
275
|
+
const width = layout.width;
|
|
276
|
+
const height = layout.height;
|
|
277
|
+
const radius = layout.border.radius || 4;
|
|
278
|
+
// Apply shadow if specified
|
|
279
|
+
const shadow = props.shadow || props.boxShadow;
|
|
280
|
+
if (shadow) {
|
|
281
|
+
applyShadow(ctx, shadow);
|
|
282
|
+
}
|
|
283
|
+
// Background color based on state
|
|
284
|
+
let backgroundColor = props.backgroundColor || "#007bff";
|
|
285
|
+
if (node.hovered) {
|
|
286
|
+
backgroundColor = props.hoverColor || "#0056b3";
|
|
287
|
+
}
|
|
288
|
+
if (node.focused) {
|
|
289
|
+
backgroundColor = props.focusColor || "#004085";
|
|
290
|
+
}
|
|
291
|
+
// Support gradients for button background
|
|
292
|
+
if (typeof backgroundColor === "string" && backgroundColor.includes("gradient")) {
|
|
293
|
+
ctx.fillStyle = parseGradient(ctx, backgroundColor, x, y, width, height);
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
ctx.fillStyle = backgroundColor;
|
|
297
|
+
}
|
|
298
|
+
// Draw button background
|
|
520
299
|
drawRoundedRect(ctx, x, y, width, height, radius);
|
|
521
|
-
ctx.
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
300
|
+
ctx.fill();
|
|
301
|
+
// Reset shadow for border
|
|
302
|
+
if (shadow) {
|
|
303
|
+
ctx.shadowColor = "transparent";
|
|
304
|
+
ctx.shadowBlur = 0;
|
|
305
|
+
ctx.shadowOffsetX = 0;
|
|
306
|
+
ctx.shadowOffsetY = 0;
|
|
307
|
+
}
|
|
308
|
+
// Draw border
|
|
309
|
+
if (layout.border.width > 0) {
|
|
310
|
+
ctx.strokeStyle = layout.border.color;
|
|
311
|
+
ctx.lineWidth = layout.border.width;
|
|
312
|
+
drawRoundedRect(ctx, x, y, width, height, radius);
|
|
313
|
+
ctx.stroke();
|
|
314
|
+
}
|
|
315
|
+
// Paint children (typically Text)
|
|
316
|
+
for (const child of node.children) {
|
|
317
|
+
paintNode(ctx, child);
|
|
318
|
+
}
|
|
526
319
|
}
|
|
320
|
+
/**
|
|
321
|
+
* Paint input node
|
|
322
|
+
*/
|
|
527
323
|
function paintInput(ctx, node) {
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
const
|
|
550
|
-
const
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
324
|
+
const layout = node.layout;
|
|
325
|
+
const props = node.props;
|
|
326
|
+
const x = layout.x;
|
|
327
|
+
const y = layout.y;
|
|
328
|
+
const width = layout.width;
|
|
329
|
+
const height = layout.height;
|
|
330
|
+
const radius = layout.border.radius || 4;
|
|
331
|
+
// Background
|
|
332
|
+
ctx.fillStyle = props.backgroundColor || "#ffffff";
|
|
333
|
+
drawRoundedRect(ctx, x, y, width, height, radius);
|
|
334
|
+
ctx.fill();
|
|
335
|
+
// Border (thicker if focused)
|
|
336
|
+
const borderColor = node.focused ? "#007bff" : (layout.border.color || "#cccccc");
|
|
337
|
+
const borderWidth = node.focused ? 2 : (layout.border.width || 1);
|
|
338
|
+
ctx.strokeStyle = borderColor;
|
|
339
|
+
ctx.lineWidth = borderWidth;
|
|
340
|
+
drawRoundedRect(ctx, x, y, width, height, radius);
|
|
341
|
+
ctx.stroke();
|
|
342
|
+
// Input value text
|
|
343
|
+
const value = props.value || "";
|
|
344
|
+
const placeholder = props.placeholder || "";
|
|
345
|
+
const text = value || placeholder;
|
|
346
|
+
const textColor = value ? (props.color || "#000000") : "#999999";
|
|
347
|
+
if (text) {
|
|
348
|
+
const fontSize = parseFloat(props.fontSize) || 16;
|
|
349
|
+
const fontWeight = props.fontWeight || "normal";
|
|
350
|
+
const fontFamily = props.fontFamily || "system-ui, sans-serif";
|
|
351
|
+
const lineHeight = parseFloat(props.lineHeight) || fontSize * 1.2;
|
|
352
|
+
renderText(ctx, text, layout.x + layout.contentX, layout.y + layout.contentY, layout.contentWidth, layout.contentHeight, {
|
|
353
|
+
color: textColor,
|
|
354
|
+
fontSize,
|
|
355
|
+
fontWeight,
|
|
356
|
+
fontFamily,
|
|
357
|
+
textAlign: "left",
|
|
358
|
+
verticalAlign: "middle",
|
|
359
|
+
lineHeight,
|
|
360
|
+
});
|
|
361
|
+
}
|
|
563
362
|
}
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
363
|
+
/**
|
|
364
|
+
* Image cache for canvas renderer
|
|
365
|
+
*/
|
|
366
|
+
const MAX_IMAGE_CACHE_SIZE = 100;
|
|
367
|
+
const MAX_FAIL_CACHE_SIZE = 200;
|
|
368
|
+
const imageCache = new Map();
|
|
369
|
+
const imagePending = new Set();
|
|
370
|
+
const imageInFlight = new Map();
|
|
371
|
+
const imageFailCache = new Map();
|
|
372
|
+
const IMAGE_FAIL_COOLDOWN_MS = 30_000;
|
|
373
|
+
const IMAGE_MAX_ATTEMPTS = 3;
|
|
374
|
+
/**
|
|
375
|
+
* Evict oldest entries from a Map when it exceeds the given maximum size.
|
|
376
|
+
*/
|
|
572
377
|
function evictMap(map, max) {
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
378
|
+
while (map.size > max) {
|
|
379
|
+
const oldest = map.keys().next().value;
|
|
380
|
+
if (oldest === undefined)
|
|
381
|
+
break;
|
|
382
|
+
map.delete(oldest);
|
|
383
|
+
}
|
|
579
384
|
}
|
|
385
|
+
/**
|
|
386
|
+
* Purge expired entries from imageFailCache.
|
|
387
|
+
*/
|
|
580
388
|
function purgeExpiredFailures() {
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
389
|
+
const now = Date.now();
|
|
390
|
+
for (const [key, entry] of imageFailCache) {
|
|
391
|
+
if (entry.attempts >= IMAGE_MAX_ATTEMPTS)
|
|
392
|
+
continue; // Keep max-attempt entries until evicted by size
|
|
393
|
+
if (now - entry.failedAt >= IMAGE_FAIL_COOLDOWN_MS) {
|
|
394
|
+
imageFailCache.delete(key);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
589
397
|
}
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
398
|
+
/**
|
|
399
|
+
* Clear the image cache and cancel all in-flight loads, freeing resources.
|
|
400
|
+
*/
|
|
401
|
+
export function clearImageCache() {
|
|
402
|
+
// Cancel in-flight Image loads so their callbacks cannot repopulate caches
|
|
403
|
+
for (const img of imageInFlight.values()) {
|
|
404
|
+
img.onload = null;
|
|
405
|
+
img.onerror = null;
|
|
406
|
+
img.src = "";
|
|
407
|
+
}
|
|
408
|
+
imageInFlight.clear();
|
|
409
|
+
for (const img of imageCache.values()) {
|
|
410
|
+
img.src = "";
|
|
411
|
+
}
|
|
412
|
+
imageCache.clear();
|
|
413
|
+
imagePending.clear();
|
|
414
|
+
imageFailCache.clear();
|
|
603
415
|
}
|
|
416
|
+
/**
|
|
417
|
+
* Paint image node
|
|
418
|
+
*/
|
|
604
419
|
function paintImage(ctx, node) {
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
420
|
+
const layout = node.layout;
|
|
421
|
+
const props = node.props;
|
|
422
|
+
const src = props.src || props[0];
|
|
423
|
+
if (!src)
|
|
424
|
+
return;
|
|
425
|
+
const x = layout.x;
|
|
426
|
+
const y = layout.y;
|
|
427
|
+
const width = layout.width;
|
|
428
|
+
const height = layout.height;
|
|
429
|
+
const cached = imageCache.get(src);
|
|
430
|
+
if (cached && cached.complete && cached.naturalWidth > 0) {
|
|
431
|
+
// Draw the loaded image
|
|
432
|
+
ctx.drawImage(cached, x, y, width, height);
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
// Draw placeholder while loading
|
|
436
|
+
ctx.fillStyle = "#e0e0e0";
|
|
437
|
+
ctx.fillRect(x, y, width, height);
|
|
438
|
+
ctx.strokeStyle = "#999999";
|
|
439
|
+
ctx.lineWidth = 1;
|
|
440
|
+
ctx.strokeRect(x, y, width, height);
|
|
441
|
+
ctx.fillStyle = "#666666";
|
|
442
|
+
ctx.font = "14px sans-serif";
|
|
443
|
+
ctx.textAlign = "center";
|
|
444
|
+
ctx.textBaseline = "middle";
|
|
445
|
+
// Check if this URL has failed recently
|
|
446
|
+
const failEntry = imageFailCache.get(src);
|
|
447
|
+
if (failEntry) {
|
|
448
|
+
if (failEntry.attempts >= IMAGE_MAX_ATTEMPTS || Date.now() - failEntry.failedAt < IMAGE_FAIL_COOLDOWN_MS) {
|
|
449
|
+
ctx.fillText("Failed", x + width / 2, y + height / 2);
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
ctx.fillText("Loading...", x + width / 2, y + height / 2);
|
|
454
|
+
// Start loading if not already pending
|
|
455
|
+
if (!imagePending.has(src)) {
|
|
456
|
+
imagePending.add(src);
|
|
457
|
+
const canvas = ctx.canvas;
|
|
458
|
+
const img = new Image();
|
|
459
|
+
img.crossOrigin = "anonymous";
|
|
460
|
+
imageInFlight.set(src, img);
|
|
461
|
+
img.onload = () => {
|
|
462
|
+
imageInFlight.delete(src);
|
|
463
|
+
imageCache.set(src, img);
|
|
464
|
+
imagePending.delete(src);
|
|
465
|
+
imageFailCache.delete(src);
|
|
466
|
+
evictMap(imageCache, MAX_IMAGE_CACHE_SIZE);
|
|
467
|
+
// Request a repaint so the loaded image is drawn
|
|
468
|
+
canvas.dispatchEvent(new CustomEvent("hypen:redraw"));
|
|
469
|
+
};
|
|
470
|
+
img.onerror = () => {
|
|
471
|
+
imageInFlight.delete(src);
|
|
472
|
+
imagePending.delete(src);
|
|
473
|
+
purgeExpiredFailures();
|
|
474
|
+
imageFailCache.set(src, {
|
|
475
|
+
failedAt: Date.now(),
|
|
476
|
+
attempts: (imageFailCache.get(src)?.attempts ?? 0) + 1,
|
|
477
|
+
});
|
|
478
|
+
evictMap(imageFailCache, MAX_FAIL_CACHE_SIZE);
|
|
479
|
+
};
|
|
480
|
+
img.src = src;
|
|
481
|
+
}
|
|
662
482
|
}
|
|
483
|
+
/**
|
|
484
|
+
* Draw rounded rectangle path
|
|
485
|
+
*/
|
|
663
486
|
function drawRoundedRect(ctx, x, y, width, height, radius) {
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
487
|
+
if (radius <= 0) {
|
|
488
|
+
ctx.rect(x, y, width, height);
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
ctx.beginPath();
|
|
492
|
+
ctx.moveTo(x + radius, y);
|
|
493
|
+
ctx.lineTo(x + width - radius, y);
|
|
494
|
+
ctx.arcTo(x + width, y, x + width, y + radius, radius);
|
|
495
|
+
ctx.lineTo(x + width, y + height - radius);
|
|
496
|
+
ctx.arcTo(x + width, y + height, x + width - radius, y + height, radius);
|
|
497
|
+
ctx.lineTo(x + radius, y + height);
|
|
498
|
+
ctx.arcTo(x, y + height, x, y + height - radius, radius);
|
|
499
|
+
ctx.lineTo(x, y + radius);
|
|
500
|
+
ctx.arcTo(x, y, x + radius, y, radius);
|
|
501
|
+
ctx.closePath();
|
|
679
502
|
}
|
|
503
|
+
/**
|
|
504
|
+
* Apply shadow to canvas context
|
|
505
|
+
* Supports both simple and CSS-like shadow syntax:
|
|
506
|
+
* - Simple: "2 2 4 rgba(0,0,0,0.3)"
|
|
507
|
+
* - CSS-like: "2px 2px 4px rgba(0,0,0,0.3)"
|
|
508
|
+
* - Named: {offsetX: 2, offsetY: 2, blur: 4, color: "rgba(0,0,0,0.3)"}
|
|
509
|
+
*/
|
|
680
510
|
function applyShadow(ctx, shadow) {
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
511
|
+
if (typeof shadow === "string") {
|
|
512
|
+
// Parse CSS-like shadow string: "offsetX offsetY blur color"
|
|
513
|
+
const parts = shadow.trim().split(/\s+/);
|
|
514
|
+
if (parts.length >= 3) {
|
|
515
|
+
const offsetX = parseFloat(parts[0]);
|
|
516
|
+
const offsetY = parseFloat(parts[1]);
|
|
517
|
+
const blur = parseFloat(parts[2]);
|
|
518
|
+
const color = parts.slice(3).join(" ") || "rgba(0,0,0,0.3)";
|
|
519
|
+
ctx.shadowOffsetX = offsetX;
|
|
520
|
+
ctx.shadowOffsetY = offsetY;
|
|
521
|
+
ctx.shadowBlur = blur;
|
|
522
|
+
ctx.shadowColor = color;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
else if (typeof shadow === "object") {
|
|
526
|
+
// Object syntax
|
|
527
|
+
ctx.shadowOffsetX = shadow.offsetX || 0;
|
|
528
|
+
ctx.shadowOffsetY = shadow.offsetY || 0;
|
|
529
|
+
ctx.shadowBlur = shadow.blur || 0;
|
|
530
|
+
ctx.shadowColor = shadow.color || "rgba(0,0,0,0.3)";
|
|
531
|
+
}
|
|
699
532
|
}
|
|
533
|
+
/**
|
|
534
|
+
* Parse gradient string and create canvas gradient
|
|
535
|
+
* Supports:
|
|
536
|
+
* - linear-gradient(direction, color1, color2, ...)
|
|
537
|
+
* - radial-gradient(color1, color2, ...)
|
|
538
|
+
*/
|
|
700
539
|
function parseGradient(ctx, gradientStr, x, y, width, height) {
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
540
|
+
// Simple gradient parsing
|
|
541
|
+
if (gradientStr.startsWith("linear-gradient")) {
|
|
542
|
+
// Extract content between parentheses
|
|
543
|
+
const match = gradientStr.match(/linear-gradient\((.*)\)/);
|
|
544
|
+
if (!match)
|
|
545
|
+
return gradientStr;
|
|
546
|
+
const parts = match[1].split(",").map((s) => s.trim());
|
|
547
|
+
// Determine direction (default to bottom)
|
|
548
|
+
let x0 = x, y0 = y, x1 = x, y1 = y + height;
|
|
549
|
+
let colorStart = 0;
|
|
550
|
+
if (parts[0].includes("deg") || parts[0].includes("to ")) {
|
|
551
|
+
colorStart = 1;
|
|
552
|
+
const direction = parts[0];
|
|
553
|
+
if (direction.includes("to right") || direction === "90deg") {
|
|
554
|
+
x1 = x + width;
|
|
555
|
+
y1 = y;
|
|
556
|
+
}
|
|
557
|
+
else if (direction.includes("to left") || direction === "270deg") {
|
|
558
|
+
x0 = x + width;
|
|
559
|
+
x1 = x;
|
|
560
|
+
y0 = y;
|
|
561
|
+
y1 = y;
|
|
562
|
+
}
|
|
563
|
+
else if (direction.includes("to top") || direction === "0deg") {
|
|
564
|
+
y0 = y + height;
|
|
565
|
+
y1 = y;
|
|
566
|
+
}
|
|
567
|
+
// Default is "to bottom" which we already set
|
|
568
|
+
}
|
|
569
|
+
const gradient = ctx.createLinearGradient(x0, y0, x1, y1);
|
|
570
|
+
// Add color stops
|
|
571
|
+
const colors = parts.slice(colorStart);
|
|
572
|
+
colors.forEach((color, i) => {
|
|
573
|
+
const stop = i / (colors.length - 1);
|
|
574
|
+
gradient.addColorStop(stop, color.trim());
|
|
575
|
+
});
|
|
576
|
+
return gradient;
|
|
577
|
+
}
|
|
578
|
+
else if (gradientStr.startsWith("radial-gradient")) {
|
|
579
|
+
// Extract content between parentheses
|
|
580
|
+
const match = gradientStr.match(/radial-gradient\((.*)\)/);
|
|
581
|
+
if (!match)
|
|
582
|
+
return gradientStr;
|
|
583
|
+
const parts = match[1].split(",").map((s) => s.trim());
|
|
584
|
+
// Create radial gradient from center
|
|
585
|
+
const centerX = x + width / 2;
|
|
586
|
+
const centerY = y + height / 2;
|
|
587
|
+
const radius = Math.max(width, height) / 2;
|
|
588
|
+
const gradient = ctx.createRadialGradient(centerX, centerY, 0, centerX, centerY, radius);
|
|
589
|
+
// Add color stops
|
|
590
|
+
parts.forEach((color, i) => {
|
|
591
|
+
const stop = i / (parts.length - 1);
|
|
592
|
+
gradient.addColorStop(stop, color.trim());
|
|
593
|
+
});
|
|
594
|
+
return gradient;
|
|
595
|
+
}
|
|
596
|
+
return gradientStr;
|
|
747
597
|
}
|
|
598
|
+
/**
|
|
599
|
+
* Apply CSS-like transforms to canvas context
|
|
600
|
+
* Supports:
|
|
601
|
+
* - translate(x, y)
|
|
602
|
+
* - rotate(angle)
|
|
603
|
+
* - scale(x, y)
|
|
604
|
+
* - skew(x, y)
|
|
605
|
+
* - transform: "translate(10, 20) rotate(45) scale(1.5)"
|
|
606
|
+
*/
|
|
748
607
|
function applyTransforms(ctx, node) {
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
608
|
+
const props = node.props;
|
|
609
|
+
const layout = node.layout;
|
|
610
|
+
// Get transform origin (default to center)
|
|
611
|
+
const originX = parseFloat(props.transformOriginX) || 0.5;
|
|
612
|
+
const originY = parseFloat(props.transformOriginY) || 0.5;
|
|
613
|
+
const centerX = layout.x + layout.width * originX;
|
|
614
|
+
const centerY = layout.y + layout.height * originY;
|
|
615
|
+
// Check for individual transform properties
|
|
616
|
+
const translateX = parseFloat(props.translateX) || 0;
|
|
617
|
+
const translateY = parseFloat(props.translateY) || 0;
|
|
618
|
+
const rotate = parseFloat(props.rotate) || 0; // in degrees
|
|
619
|
+
const scaleX = parseFloat(props.scaleX) || parseFloat(props.scale) || 1;
|
|
620
|
+
const scaleY = parseFloat(props.scaleY) || parseFloat(props.scale) || 1;
|
|
621
|
+
const skewX = parseFloat(props.skewX) || parseFloat(props.skew) || 0; // in degrees
|
|
622
|
+
const skewY = parseFloat(props.skewY) || 0; // in degrees
|
|
623
|
+
// Apply transforms in order: translate to origin, scale, rotate, skew, translate back, translate by offset
|
|
624
|
+
if (translateX !== 0 || translateY !== 0 || rotate !== 0 || scaleX !== 1 || scaleY !== 1 || skewX !== 0 || skewY !== 0) {
|
|
625
|
+
// Move to transform origin
|
|
626
|
+
ctx.translate(centerX, centerY);
|
|
627
|
+
// Apply scale
|
|
628
|
+
if (scaleX !== 1 || scaleY !== 1) {
|
|
629
|
+
ctx.scale(scaleX, scaleY);
|
|
630
|
+
}
|
|
631
|
+
// Apply rotation (convert degrees to radians)
|
|
632
|
+
if (rotate !== 0) {
|
|
633
|
+
ctx.rotate((rotate * Math.PI) / 180);
|
|
634
|
+
}
|
|
635
|
+
// Apply skew using transform matrix
|
|
636
|
+
if (skewX !== 0 || skewY !== 0) {
|
|
637
|
+
const skewXRad = (skewX * Math.PI) / 180;
|
|
638
|
+
const skewYRad = (skewY * Math.PI) / 180;
|
|
639
|
+
ctx.transform(1, Math.tan(skewYRad), Math.tan(skewXRad), 1, 0, 0);
|
|
640
|
+
}
|
|
641
|
+
// Move back from origin and apply translation
|
|
642
|
+
ctx.translate(-centerX + translateX, -centerY + translateY);
|
|
643
|
+
}
|
|
644
|
+
// Also support compound transform string (optional, for future extensibility)
|
|
645
|
+
if (props.transform && typeof props.transform === "string") {
|
|
646
|
+
parseTransformString(ctx, props.transform, centerX, centerY);
|
|
647
|
+
}
|
|
780
648
|
}
|
|
649
|
+
/**
|
|
650
|
+
* Parse and apply a CSS-like transform string
|
|
651
|
+
*/
|
|
781
652
|
function parseTransformString(ctx, transformStr, originX, originY) {
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
const
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
653
|
+
// Simple transform parsing - matches translate(), rotate(), scale()
|
|
654
|
+
const transforms = transformStr.match(/(\w+)\(([^)]+)\)/g);
|
|
655
|
+
if (!transforms)
|
|
656
|
+
return;
|
|
657
|
+
ctx.translate(originX, originY);
|
|
658
|
+
for (const transform of transforms) {
|
|
659
|
+
const match = transform.match(/(\w+)\(([^)]+)\)/);
|
|
660
|
+
if (!match)
|
|
661
|
+
continue;
|
|
662
|
+
const [, func, args] = match;
|
|
663
|
+
const values = args.split(",").map((v) => parseFloat(v.trim()));
|
|
664
|
+
switch (func.toLowerCase()) {
|
|
665
|
+
case "translate":
|
|
666
|
+
ctx.translate(values[0] || 0, values[1] || 0);
|
|
667
|
+
break;
|
|
668
|
+
case "rotate":
|
|
669
|
+
ctx.rotate((values[0] * Math.PI) / 180);
|
|
670
|
+
break;
|
|
671
|
+
case "scale":
|
|
672
|
+
ctx.scale(values[0] || 1, values[1] || values[0] || 1);
|
|
673
|
+
break;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
ctx.translate(-originX, -originY);
|
|
805
677
|
}
|
|
678
|
+
/**
|
|
679
|
+
* Paint divider/separator node
|
|
680
|
+
*/
|
|
806
681
|
function paintDivider(ctx, node) {
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
682
|
+
const layout = node.layout;
|
|
683
|
+
const props = node.props;
|
|
684
|
+
const orientation = props.orientation || "horizontal";
|
|
685
|
+
const color = props.color || props.backgroundColor || "#e0e0e0";
|
|
686
|
+
const thickness = parseFloat(props.thickness) || 1;
|
|
687
|
+
ctx.strokeStyle = color;
|
|
688
|
+
ctx.lineWidth = thickness;
|
|
689
|
+
ctx.beginPath();
|
|
690
|
+
if (orientation === "vertical") {
|
|
691
|
+
const x = layout.x + layout.width / 2;
|
|
692
|
+
ctx.moveTo(x, layout.y);
|
|
693
|
+
ctx.lineTo(x, layout.y + layout.height);
|
|
694
|
+
}
|
|
695
|
+
else {
|
|
696
|
+
const y = layout.y + layout.height / 2;
|
|
697
|
+
ctx.moveTo(layout.x, y);
|
|
698
|
+
ctx.lineTo(layout.x + layout.width, y);
|
|
699
|
+
}
|
|
700
|
+
ctx.stroke();
|
|
825
701
|
}
|
|
702
|
+
/**
|
|
703
|
+
* Paint checkbox node
|
|
704
|
+
*/
|
|
826
705
|
function paintCheckbox(ctx, node) {
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
ctx
|
|
846
|
-
ctx.lineWidth = 2;
|
|
847
|
-
ctx.lineCap = "round";
|
|
848
|
-
ctx.lineJoin = "round";
|
|
849
|
-
const padding = size * 0.25;
|
|
850
|
-
ctx.beginPath();
|
|
851
|
-
ctx.moveTo(x + padding, y + size / 2);
|
|
852
|
-
ctx.lineTo(x + size * 0.4, y + size - padding);
|
|
853
|
-
ctx.lineTo(x + size - padding, y + padding);
|
|
706
|
+
const layout = node.layout;
|
|
707
|
+
const props = node.props;
|
|
708
|
+
const size = Math.min(layout.width, layout.height);
|
|
709
|
+
const x = layout.x;
|
|
710
|
+
const y = layout.y;
|
|
711
|
+
// Properly parse boolean values (handle string "false")
|
|
712
|
+
const checkedValue = props.checked !== undefined ? props.checked : props.value;
|
|
713
|
+
const checked = checkedValue === true || checkedValue === "true" || (checkedValue !== false && checkedValue !== "false" && !!checkedValue);
|
|
714
|
+
const radius = parseFloat(props.borderRadius) || 2;
|
|
715
|
+
// Background
|
|
716
|
+
const bgColor = checked ? (props.checkedColor || "#007bff") : (props.backgroundColor || "#ffffff");
|
|
717
|
+
ctx.fillStyle = bgColor;
|
|
718
|
+
drawRoundedRect(ctx, x, y, size, size, radius);
|
|
719
|
+
ctx.fill();
|
|
720
|
+
// Border
|
|
721
|
+
const borderColor = checked ? (props.checkedColor || "#007bff") : (props.borderColor || "#cccccc");
|
|
722
|
+
ctx.strokeStyle = borderColor;
|
|
723
|
+
ctx.lineWidth = node.focused ? 2 : 1;
|
|
724
|
+
drawRoundedRect(ctx, x, y, size, size, radius);
|
|
854
725
|
ctx.stroke();
|
|
855
|
-
|
|
726
|
+
// Checkmark
|
|
727
|
+
if (checked) {
|
|
728
|
+
ctx.strokeStyle = props.checkColor || "#ffffff";
|
|
729
|
+
ctx.lineWidth = 2;
|
|
730
|
+
ctx.lineCap = "round";
|
|
731
|
+
ctx.lineJoin = "round";
|
|
732
|
+
const padding = size * 0.25;
|
|
733
|
+
ctx.beginPath();
|
|
734
|
+
ctx.moveTo(x + padding, y + size / 2);
|
|
735
|
+
ctx.lineTo(x + size * 0.4, y + size - padding);
|
|
736
|
+
ctx.lineTo(x + size - padding, y + padding);
|
|
737
|
+
ctx.stroke();
|
|
738
|
+
}
|
|
856
739
|
}
|
|
740
|
+
/**
|
|
741
|
+
* Paint radio button node
|
|
742
|
+
*/
|
|
857
743
|
function paintRadio(ctx, node) {
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
ctx.fill();
|
|
871
|
-
const borderColor = checked ? props.checkedColor || "#007bff" : props.borderColor || "#cccccc";
|
|
872
|
-
ctx.strokeStyle = borderColor;
|
|
873
|
-
ctx.lineWidth = node.focused ? 2 : 1;
|
|
874
|
-
ctx.beginPath();
|
|
875
|
-
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
|
|
876
|
-
ctx.stroke();
|
|
877
|
-
if (checked) {
|
|
878
|
-
ctx.fillStyle = props.checkedColor || "#007bff";
|
|
744
|
+
const layout = node.layout;
|
|
745
|
+
const props = node.props;
|
|
746
|
+
const size = Math.min(layout.width, layout.height);
|
|
747
|
+
const centerX = layout.x + size / 2;
|
|
748
|
+
const centerY = layout.y + size / 2;
|
|
749
|
+
const radius = size / 2;
|
|
750
|
+
// Properly parse boolean values (handle string "false")
|
|
751
|
+
const checkedValue = props.checked !== undefined ? props.checked : props.value;
|
|
752
|
+
const checked = checkedValue === true || checkedValue === "true" || (checkedValue !== false && checkedValue !== "false" && !!checkedValue);
|
|
753
|
+
// Outer circle background
|
|
754
|
+
const bgColor = props.backgroundColor || "#ffffff";
|
|
755
|
+
ctx.fillStyle = bgColor;
|
|
879
756
|
ctx.beginPath();
|
|
880
|
-
ctx.arc(centerX, centerY, radius
|
|
757
|
+
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
|
|
881
758
|
ctx.fill();
|
|
882
|
-
|
|
759
|
+
// Outer circle border
|
|
760
|
+
const borderColor = checked ? (props.checkedColor || "#007bff") : (props.borderColor || "#cccccc");
|
|
761
|
+
ctx.strokeStyle = borderColor;
|
|
762
|
+
ctx.lineWidth = node.focused ? 2 : 1;
|
|
763
|
+
ctx.beginPath();
|
|
764
|
+
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
|
|
765
|
+
ctx.stroke();
|
|
766
|
+
// Inner filled circle when checked
|
|
767
|
+
if (checked) {
|
|
768
|
+
ctx.fillStyle = props.checkedColor || "#007bff";
|
|
769
|
+
ctx.beginPath();
|
|
770
|
+
ctx.arc(centerX, centerY, radius * 0.5, 0, Math.PI * 2);
|
|
771
|
+
ctx.fill();
|
|
772
|
+
}
|
|
883
773
|
}
|
|
774
|
+
/**
|
|
775
|
+
* Paint switch/toggle node
|
|
776
|
+
*/
|
|
884
777
|
function paintSwitch(ctx, node) {
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
if (props.shadow !== false) {
|
|
906
|
-
ctx.shadowColor = "rgba(0,0,0,0.2)";
|
|
907
|
-
ctx.shadowBlur = 2;
|
|
908
|
-
ctx.shadowOffsetY = 1;
|
|
778
|
+
const layout = node.layout;
|
|
779
|
+
const props = node.props;
|
|
780
|
+
const width = layout.width;
|
|
781
|
+
const height = layout.height;
|
|
782
|
+
const x = layout.x;
|
|
783
|
+
const y = layout.y;
|
|
784
|
+
// Properly parse boolean values (handle string "false")
|
|
785
|
+
const checkedValue = props.checked !== undefined ? props.checked : props.value;
|
|
786
|
+
const checked = checkedValue === true || checkedValue === "true" || (checkedValue !== false && checkedValue !== "false" && !!checkedValue);
|
|
787
|
+
const radius = height / 2;
|
|
788
|
+
// Track background
|
|
789
|
+
const trackColor = checked ? (props.checkedColor || "#4caf50") : (props.backgroundColor || "#cccccc");
|
|
790
|
+
ctx.fillStyle = trackColor;
|
|
791
|
+
drawRoundedRect(ctx, x, y, width, height, radius);
|
|
792
|
+
ctx.fill();
|
|
793
|
+
// Thumb (circle)
|
|
794
|
+
const thumbRadius = radius * 0.8;
|
|
795
|
+
const thumbX = checked ? (x + width - radius) : (x + radius);
|
|
796
|
+
const thumbY = y + radius;
|
|
797
|
+
ctx.fillStyle = props.thumbColor || "#ffffff";
|
|
909
798
|
ctx.beginPath();
|
|
910
799
|
ctx.arc(thumbX, thumbY, thumbRadius, 0, Math.PI * 2);
|
|
911
800
|
ctx.fill();
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
801
|
+
// Thumb shadow
|
|
802
|
+
if (props.shadow !== false) {
|
|
803
|
+
ctx.shadowColor = "rgba(0,0,0,0.2)";
|
|
804
|
+
ctx.shadowBlur = 2;
|
|
805
|
+
ctx.shadowOffsetY = 1;
|
|
806
|
+
ctx.beginPath();
|
|
807
|
+
ctx.arc(thumbX, thumbY, thumbRadius, 0, Math.PI * 2);
|
|
808
|
+
ctx.fill();
|
|
809
|
+
ctx.shadowColor = "transparent";
|
|
810
|
+
ctx.shadowBlur = 0;
|
|
811
|
+
ctx.shadowOffsetY = 0;
|
|
812
|
+
}
|
|
916
813
|
}
|
|
814
|
+
/**
|
|
815
|
+
* Paint slider node
|
|
816
|
+
*/
|
|
917
817
|
function paintSlider(ctx, node) {
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
if (typeof fillColor === "string" && fillColor.includes("gradient")) {
|
|
937
|
-
ctx.fillStyle = parseGradient(ctx, fillColor, x, trackY, fillWidth, trackHeight);
|
|
938
|
-
} else {
|
|
939
|
-
ctx.fillStyle = fillColor;
|
|
940
|
-
}
|
|
941
|
-
drawRoundedRect(ctx, x, trackY, fillWidth, trackHeight, trackHeight / 2);
|
|
942
|
-
ctx.fill();
|
|
943
|
-
const thumbX = x + fillWidth;
|
|
944
|
-
const thumbY = y + height / 2;
|
|
945
|
-
ctx.fillStyle = props.thumbColor || "#007bff";
|
|
946
|
-
ctx.beginPath();
|
|
947
|
-
ctx.arc(thumbX, thumbY, thumbSize / 2, 0, Math.PI * 2);
|
|
948
|
-
ctx.fill();
|
|
949
|
-
ctx.strokeStyle = "#ffffff";
|
|
950
|
-
ctx.lineWidth = 2;
|
|
951
|
-
ctx.beginPath();
|
|
952
|
-
ctx.arc(thumbX, thumbY, thumbSize / 2, 0, Math.PI * 2);
|
|
953
|
-
ctx.stroke();
|
|
954
|
-
}
|
|
955
|
-
function paintProgress(ctx, node) {
|
|
956
|
-
const layout = node.layout;
|
|
957
|
-
const props = node.props;
|
|
958
|
-
const width = layout.width;
|
|
959
|
-
const height = layout.height;
|
|
960
|
-
const x = layout.x;
|
|
961
|
-
const y = layout.y;
|
|
962
|
-
const min = parseFloat(props.min) || 0;
|
|
963
|
-
const max = parseFloat(props.max) || 100;
|
|
964
|
-
const value = parseFloat(props.value) || 0;
|
|
965
|
-
const percentage = Math.min(Math.max((value - min) / (max - min), 0), 1);
|
|
966
|
-
const radius = layout.border.radius || height / 2;
|
|
967
|
-
ctx.fillStyle = props.backgroundColor || "#e0e0e0";
|
|
968
|
-
drawRoundedRect(ctx, x, y, width, height, radius);
|
|
969
|
-
ctx.fill();
|
|
970
|
-
if (percentage > 0) {
|
|
818
|
+
const layout = node.layout;
|
|
819
|
+
const props = node.props;
|
|
820
|
+
const width = layout.width;
|
|
821
|
+
const height = layout.height;
|
|
822
|
+
const x = layout.x;
|
|
823
|
+
const y = layout.y;
|
|
824
|
+
const min = parseFloat(props.min) || 0;
|
|
825
|
+
const max = parseFloat(props.max) || 100;
|
|
826
|
+
const value = parseFloat(props.value) || min;
|
|
827
|
+
const percentage = (value - min) / (max - min);
|
|
828
|
+
const trackHeight = parseFloat(props.trackHeight) || 4;
|
|
829
|
+
const thumbSize = parseFloat(props.thumbSize) || 16;
|
|
830
|
+
const trackY = y + (height - trackHeight) / 2;
|
|
831
|
+
// Track background
|
|
832
|
+
ctx.fillStyle = props.trackColor || "#e0e0e0";
|
|
833
|
+
drawRoundedRect(ctx, x, trackY, width, trackHeight, trackHeight / 2);
|
|
834
|
+
ctx.fill();
|
|
835
|
+
// Track fill
|
|
971
836
|
const fillWidth = width * percentage;
|
|
972
837
|
const fillColor = props.fillColor || props.color || "#007bff";
|
|
838
|
+
// Support gradient fills
|
|
973
839
|
if (typeof fillColor === "string" && fillColor.includes("gradient")) {
|
|
974
|
-
|
|
975
|
-
} else {
|
|
976
|
-
ctx.fillStyle = fillColor;
|
|
840
|
+
ctx.fillStyle = parseGradient(ctx, fillColor, x, trackY, fillWidth, trackHeight);
|
|
977
841
|
}
|
|
978
|
-
|
|
842
|
+
else {
|
|
843
|
+
ctx.fillStyle = fillColor;
|
|
844
|
+
}
|
|
845
|
+
drawRoundedRect(ctx, x, trackY, fillWidth, trackHeight, trackHeight / 2);
|
|
979
846
|
ctx.fill();
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
const
|
|
983
|
-
ctx.fillStyle = props.
|
|
984
|
-
ctx.
|
|
985
|
-
ctx.
|
|
986
|
-
ctx.
|
|
987
|
-
|
|
988
|
-
|
|
847
|
+
// Thumb
|
|
848
|
+
const thumbX = x + fillWidth;
|
|
849
|
+
const thumbY = y + height / 2;
|
|
850
|
+
ctx.fillStyle = props.thumbColor || "#007bff";
|
|
851
|
+
ctx.beginPath();
|
|
852
|
+
ctx.arc(thumbX, thumbY, thumbSize / 2, 0, Math.PI * 2);
|
|
853
|
+
ctx.fill();
|
|
854
|
+
// Thumb border
|
|
855
|
+
ctx.strokeStyle = "#ffffff";
|
|
856
|
+
ctx.lineWidth = 2;
|
|
857
|
+
ctx.beginPath();
|
|
858
|
+
ctx.arc(thumbX, thumbY, thumbSize / 2, 0, Math.PI * 2);
|
|
859
|
+
ctx.stroke();
|
|
989
860
|
}
|
|
861
|
+
/**
|
|
862
|
+
* Paint progress bar node
|
|
863
|
+
*/
|
|
864
|
+
function paintProgress(ctx, node) {
|
|
865
|
+
const layout = node.layout;
|
|
866
|
+
const props = node.props;
|
|
867
|
+
const width = layout.width;
|
|
868
|
+
const height = layout.height;
|
|
869
|
+
const x = layout.x;
|
|
870
|
+
const y = layout.y;
|
|
871
|
+
const min = parseFloat(props.min) || 0;
|
|
872
|
+
const max = parseFloat(props.max) || 100;
|
|
873
|
+
const value = parseFloat(props.value) || 0;
|
|
874
|
+
const percentage = Math.min(Math.max((value - min) / (max - min), 0), 1);
|
|
875
|
+
const radius = layout.border.radius || height / 2;
|
|
876
|
+
// Background
|
|
877
|
+
ctx.fillStyle = props.backgroundColor || "#e0e0e0";
|
|
878
|
+
drawRoundedRect(ctx, x, y, width, height, radius);
|
|
879
|
+
ctx.fill();
|
|
880
|
+
// Progress fill
|
|
881
|
+
if (percentage > 0) {
|
|
882
|
+
const fillWidth = width * percentage;
|
|
883
|
+
// Support gradient fills
|
|
884
|
+
const fillColor = props.fillColor || props.color || "#007bff";
|
|
885
|
+
if (typeof fillColor === "string" && fillColor.includes("gradient")) {
|
|
886
|
+
ctx.fillStyle = parseGradient(ctx, fillColor, x, y, fillWidth, height);
|
|
887
|
+
}
|
|
888
|
+
else {
|
|
889
|
+
ctx.fillStyle = fillColor;
|
|
890
|
+
}
|
|
891
|
+
drawRoundedRect(ctx, x, y, fillWidth, height, radius);
|
|
892
|
+
ctx.fill();
|
|
893
|
+
}
|
|
894
|
+
// Optional text label
|
|
895
|
+
if (props.showLabel) {
|
|
896
|
+
const label = props.label || `${Math.round(percentage * 100)}%`;
|
|
897
|
+
ctx.fillStyle = props.labelColor || "#ffffff";
|
|
898
|
+
ctx.font = `${props.fontSize || 12}px ${props.fontFamily || "sans-serif"}`;
|
|
899
|
+
ctx.textAlign = "center";
|
|
900
|
+
ctx.textBaseline = "middle";
|
|
901
|
+
ctx.fillText(label, x + width / 2, y + height / 2);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
/**
|
|
905
|
+
* Paint spinner/loading node
|
|
906
|
+
*/
|
|
990
907
|
function paintSpinner(ctx, node) {
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
908
|
+
const layout = node.layout;
|
|
909
|
+
const props = node.props;
|
|
910
|
+
const size = Math.min(layout.width, layout.height);
|
|
911
|
+
const centerX = layout.x + size / 2;
|
|
912
|
+
const centerY = layout.y + size / 2;
|
|
913
|
+
const radius = size / 2 - 4;
|
|
914
|
+
const thickness = parseFloat(props.thickness) || 4;
|
|
915
|
+
const color = props.color || "#007bff";
|
|
916
|
+
// Use timestamp for animation if available
|
|
917
|
+
const rotation = (Date.now() / 1000) * Math.PI; // Rotate based on time
|
|
918
|
+
ctx.strokeStyle = color;
|
|
919
|
+
ctx.lineWidth = thickness;
|
|
920
|
+
ctx.lineCap = "round";
|
|
921
|
+
// Draw circular arc
|
|
922
|
+
ctx.beginPath();
|
|
923
|
+
ctx.arc(centerX, centerY, radius, rotation, rotation + Math.PI * 1.5);
|
|
924
|
+
ctx.stroke();
|
|
925
|
+
// Fade out effect
|
|
926
|
+
ctx.globalAlpha = 0.3;
|
|
927
|
+
ctx.beginPath();
|
|
928
|
+
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
|
|
929
|
+
ctx.stroke();
|
|
930
|
+
ctx.globalAlpha = 1;
|
|
1011
931
|
}
|
|
932
|
+
/**
|
|
933
|
+
* Paint card node
|
|
934
|
+
*/
|
|
1012
935
|
function paintCard(ctx, node) {
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
ctx.shadowBlur = 0;
|
|
1032
|
-
ctx.shadowOffsetX = 0;
|
|
1033
|
-
ctx.shadowOffsetY = 0;
|
|
1034
|
-
if (layout.border.width > 0) {
|
|
1035
|
-
ctx.strokeStyle = layout.border.color;
|
|
1036
|
-
ctx.lineWidth = layout.border.width;
|
|
936
|
+
const layout = node.layout;
|
|
937
|
+
const props = node.props;
|
|
938
|
+
const x = layout.x;
|
|
939
|
+
const y = layout.y;
|
|
940
|
+
const width = layout.width;
|
|
941
|
+
const height = layout.height;
|
|
942
|
+
const radius = layout.border.radius || 8;
|
|
943
|
+
// Default card shadow
|
|
944
|
+
const shadow = props.shadow || props.boxShadow || "0 2 8 rgba(0,0,0,0.1)";
|
|
945
|
+
applyShadow(ctx, shadow);
|
|
946
|
+
// Background
|
|
947
|
+
const backgroundColor = props.backgroundColor || "#ffffff";
|
|
948
|
+
if (typeof backgroundColor === "string" && backgroundColor.includes("gradient")) {
|
|
949
|
+
ctx.fillStyle = parseGradient(ctx, backgroundColor, x, y, width, height);
|
|
950
|
+
}
|
|
951
|
+
else {
|
|
952
|
+
ctx.fillStyle = backgroundColor;
|
|
953
|
+
}
|
|
1037
954
|
drawRoundedRect(ctx, x, y, width, height, radius);
|
|
1038
|
-
ctx.
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
955
|
+
ctx.fill();
|
|
956
|
+
// Reset shadow
|
|
957
|
+
ctx.shadowColor = "transparent";
|
|
958
|
+
ctx.shadowBlur = 0;
|
|
959
|
+
ctx.shadowOffsetX = 0;
|
|
960
|
+
ctx.shadowOffsetY = 0;
|
|
961
|
+
// Border
|
|
962
|
+
if (layout.border.width > 0) {
|
|
963
|
+
ctx.strokeStyle = layout.border.color;
|
|
964
|
+
ctx.lineWidth = layout.border.width;
|
|
965
|
+
drawRoundedRect(ctx, x, y, width, height, radius);
|
|
966
|
+
ctx.stroke();
|
|
967
|
+
}
|
|
968
|
+
// Paint children
|
|
969
|
+
for (const child of node.children) {
|
|
970
|
+
paintNode(ctx, child);
|
|
971
|
+
}
|
|
1043
972
|
}
|
|
973
|
+
/**
|
|
974
|
+
* Paint badge node
|
|
975
|
+
*/
|
|
1044
976
|
function paintBadge(ctx, node) {
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
977
|
+
const layout = node.layout;
|
|
978
|
+
const props = node.props;
|
|
979
|
+
const x = layout.x;
|
|
980
|
+
const y = layout.y;
|
|
981
|
+
const width = layout.width;
|
|
982
|
+
const height = layout.height;
|
|
983
|
+
const radius = layout.border.radius || height / 2;
|
|
984
|
+
// Background
|
|
985
|
+
const backgroundColor = props.backgroundColor || "#dc3545";
|
|
986
|
+
ctx.fillStyle = backgroundColor;
|
|
987
|
+
drawRoundedRect(ctx, x, y, width, height, radius);
|
|
988
|
+
ctx.fill();
|
|
989
|
+
// Text content
|
|
990
|
+
const text = String(props[0] || props.text || "");
|
|
991
|
+
if (text) {
|
|
992
|
+
ctx.fillStyle = props.color || "#ffffff";
|
|
993
|
+
ctx.font = `${props.fontWeight || "bold"} ${props.fontSize || 10}px ${props.fontFamily || "sans-serif"}`;
|
|
994
|
+
ctx.textAlign = "center";
|
|
995
|
+
ctx.textBaseline = "middle";
|
|
996
|
+
ctx.fillText(text, x + width / 2, y + height / 2);
|
|
997
|
+
}
|
|
1064
998
|
}
|
|
999
|
+
/**
|
|
1000
|
+
* Paint avatar node
|
|
1001
|
+
*/
|
|
1065
1002
|
function paintAvatar(ctx, node) {
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
|
|
1075
|
-
ctx.clip();
|
|
1076
|
-
const backgroundColor = props.backgroundColor || "#cccccc";
|
|
1077
|
-
ctx.fillStyle = backgroundColor;
|
|
1078
|
-
ctx.beginPath();
|
|
1079
|
-
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
|
|
1080
|
-
ctx.fill();
|
|
1081
|
-
const text = String(props[0] || props.text || props.initials || "");
|
|
1082
|
-
if (text) {
|
|
1083
|
-
ctx.fillStyle = props.color || "#ffffff";
|
|
1084
|
-
ctx.font = `${props.fontWeight || "bold"} ${props.fontSize || size / 2.5}px ${props.fontFamily || "sans-serif"}`;
|
|
1085
|
-
ctx.textAlign = "center";
|
|
1086
|
-
ctx.textBaseline = "middle";
|
|
1087
|
-
ctx.fillText(text, centerX, centerY);
|
|
1088
|
-
}
|
|
1089
|
-
ctx.restore();
|
|
1090
|
-
if (layout.border.width > 0) {
|
|
1091
|
-
ctx.strokeStyle = layout.border.color;
|
|
1092
|
-
ctx.lineWidth = layout.border.width;
|
|
1003
|
+
const layout = node.layout;
|
|
1004
|
+
const props = node.props;
|
|
1005
|
+
const size = Math.min(layout.width, layout.height);
|
|
1006
|
+
const centerX = layout.x + size / 2;
|
|
1007
|
+
const centerY = layout.y + size / 2;
|
|
1008
|
+
const radius = size / 2;
|
|
1009
|
+
// Clip to circle
|
|
1010
|
+
ctx.save();
|
|
1093
1011
|
ctx.beginPath();
|
|
1094
1012
|
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
|
|
1095
|
-
ctx.
|
|
1096
|
-
|
|
1013
|
+
ctx.clip();
|
|
1014
|
+
// Background
|
|
1015
|
+
const backgroundColor = props.backgroundColor || "#cccccc";
|
|
1016
|
+
ctx.fillStyle = backgroundColor;
|
|
1017
|
+
ctx.beginPath();
|
|
1018
|
+
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
|
|
1019
|
+
ctx.fill();
|
|
1020
|
+
// Text initials if provided
|
|
1021
|
+
const text = String(props[0] || props.text || props.initials || "");
|
|
1022
|
+
if (text) {
|
|
1023
|
+
ctx.fillStyle = props.color || "#ffffff";
|
|
1024
|
+
ctx.font = `${props.fontWeight || "bold"} ${props.fontSize || size / 2.5}px ${props.fontFamily || "sans-serif"}`;
|
|
1025
|
+
ctx.textAlign = "center";
|
|
1026
|
+
ctx.textBaseline = "middle";
|
|
1027
|
+
ctx.fillText(text, centerX, centerY);
|
|
1028
|
+
}
|
|
1029
|
+
ctx.restore();
|
|
1030
|
+
// Border
|
|
1031
|
+
if (layout.border.width > 0) {
|
|
1032
|
+
ctx.strokeStyle = layout.border.color;
|
|
1033
|
+
ctx.lineWidth = layout.border.width;
|
|
1034
|
+
ctx.beginPath();
|
|
1035
|
+
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
|
|
1036
|
+
ctx.stroke();
|
|
1037
|
+
}
|
|
1097
1038
|
}
|
|
1039
|
+
/**
|
|
1040
|
+
* Paint icon node (simple placeholder - real icons would need SVG path support)
|
|
1041
|
+
*/
|
|
1098
1042
|
function paintIcon(ctx, node) {
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1043
|
+
const layout = node.layout;
|
|
1044
|
+
const props = node.props;
|
|
1045
|
+
const size = Math.min(layout.width, layout.height);
|
|
1046
|
+
const x = layout.x;
|
|
1047
|
+
const y = layout.y;
|
|
1048
|
+
const color = props.color || "#000000";
|
|
1049
|
+
const iconName = props.icon || props.name || "circle";
|
|
1050
|
+
ctx.fillStyle = color;
|
|
1051
|
+
ctx.strokeStyle = color;
|
|
1052
|
+
ctx.lineWidth = 2;
|
|
1053
|
+
// Simple icon shapes
|
|
1054
|
+
switch (iconName.toLowerCase()) {
|
|
1055
|
+
case "circle":
|
|
1056
|
+
ctx.beginPath();
|
|
1057
|
+
ctx.arc(x + size / 2, y + size / 2, size / 3, 0, Math.PI * 2);
|
|
1058
|
+
ctx.fill();
|
|
1059
|
+
break;
|
|
1060
|
+
case "square":
|
|
1061
|
+
const padding = size * 0.2;
|
|
1062
|
+
ctx.fillRect(x + padding, y + padding, size - padding * 2, size - padding * 2);
|
|
1063
|
+
break;
|
|
1064
|
+
case "star":
|
|
1065
|
+
drawStar(ctx, x + size / 2, y + size / 2, 5, size / 3, size / 6);
|
|
1066
|
+
ctx.fill();
|
|
1067
|
+
break;
|
|
1068
|
+
case "check":
|
|
1069
|
+
case "checkmark":
|
|
1070
|
+
const p = size * 0.2;
|
|
1071
|
+
ctx.beginPath();
|
|
1072
|
+
ctx.moveTo(x + p, y + size / 2);
|
|
1073
|
+
ctx.lineTo(x + size * 0.4, y + size - p);
|
|
1074
|
+
ctx.lineTo(x + size - p, y + p);
|
|
1075
|
+
ctx.stroke();
|
|
1076
|
+
break;
|
|
1077
|
+
case "x":
|
|
1078
|
+
case "close":
|
|
1079
|
+
const pd = size * 0.2;
|
|
1080
|
+
ctx.beginPath();
|
|
1081
|
+
ctx.moveTo(x + pd, y + pd);
|
|
1082
|
+
ctx.lineTo(x + size - pd, y + size - pd);
|
|
1083
|
+
ctx.moveTo(x + size - pd, y + pd);
|
|
1084
|
+
ctx.lineTo(x + pd, y + size - pd);
|
|
1085
|
+
ctx.stroke();
|
|
1086
|
+
break;
|
|
1087
|
+
default:
|
|
1088
|
+
// Default: filled circle
|
|
1089
|
+
ctx.beginPath();
|
|
1090
|
+
ctx.arc(x + size / 2, y + size / 2, size / 3, 0, Math.PI * 2);
|
|
1091
|
+
ctx.fill();
|
|
1092
|
+
}
|
|
1147
1093
|
}
|
|
1094
|
+
/**
|
|
1095
|
+
* Paint link node
|
|
1096
|
+
*/
|
|
1148
1097
|
function paintLink(ctx, node) {
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1098
|
+
const layout = node.layout;
|
|
1099
|
+
const props = node.props;
|
|
1100
|
+
const text = String(props[0] || props.text || "");
|
|
1101
|
+
const color = node.hovered ? (props.hoverColor || "#0056b3") : (props.color || "#007bff");
|
|
1102
|
+
const fontSize = parseFloat(props.fontSize) || 16;
|
|
1103
|
+
const fontWeight = props.fontWeight || "normal";
|
|
1104
|
+
const fontFamily = props.fontFamily || "system-ui, sans-serif";
|
|
1105
|
+
const textDecoration = props.textDecoration !== undefined ? props.textDecoration : "underline";
|
|
1106
|
+
ctx.fillStyle = color;
|
|
1107
|
+
ctx.font = `${fontWeight} ${fontSize}px ${fontFamily}`;
|
|
1108
|
+
ctx.textAlign = "left";
|
|
1109
|
+
ctx.textBaseline = "top";
|
|
1110
|
+
const x = layout.x + layout.contentX;
|
|
1111
|
+
const y = layout.y + layout.contentY;
|
|
1112
|
+
ctx.fillText(text, x, y);
|
|
1113
|
+
// Underline
|
|
1114
|
+
if (textDecoration === "underline") {
|
|
1115
|
+
const textWidth = ctx.measureText(text).width;
|
|
1116
|
+
ctx.strokeStyle = color;
|
|
1117
|
+
ctx.lineWidth = 1;
|
|
1118
|
+
ctx.beginPath();
|
|
1119
|
+
ctx.moveTo(x, y + fontSize + 2);
|
|
1120
|
+
ctx.lineTo(x + textWidth, y + fontSize + 2);
|
|
1121
|
+
ctx.stroke();
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
/**
|
|
1125
|
+
* Apply text decoration (underline, line-through, etc.)
|
|
1126
|
+
*/
|
|
1127
|
+
function applyTextDecoration(ctx, decoration, color, x, y, width, fontSize) {
|
|
1166
1128
|
ctx.strokeStyle = color;
|
|
1167
|
-
ctx.lineWidth = 1;
|
|
1129
|
+
ctx.lineWidth = Math.max(1, fontSize / 16);
|
|
1168
1130
|
ctx.beginPath();
|
|
1169
|
-
|
|
1170
|
-
|
|
1131
|
+
if (decoration === "underline") {
|
|
1132
|
+
const underlineY = y + fontSize + 2;
|
|
1133
|
+
ctx.moveTo(x, underlineY);
|
|
1134
|
+
ctx.lineTo(x + width, underlineY);
|
|
1135
|
+
}
|
|
1136
|
+
else if (decoration === "line-through" || decoration === "strikethrough") {
|
|
1137
|
+
const lineThroughY = y + fontSize / 2;
|
|
1138
|
+
ctx.moveTo(x, lineThroughY);
|
|
1139
|
+
ctx.lineTo(x + width, lineThroughY);
|
|
1140
|
+
}
|
|
1141
|
+
else if (decoration === "overline") {
|
|
1142
|
+
ctx.moveTo(x, y);
|
|
1143
|
+
ctx.lineTo(x + width, y);
|
|
1144
|
+
}
|
|
1171
1145
|
ctx.stroke();
|
|
1172
|
-
}
|
|
1173
1146
|
}
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
const
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1147
|
+
/**
|
|
1148
|
+
* Apply overflow clipping
|
|
1149
|
+
*/
|
|
1150
|
+
function applyOverflowClip(ctx, node) {
|
|
1151
|
+
const props = node.props;
|
|
1152
|
+
const overflow = props.overflow || "visible";
|
|
1153
|
+
if (overflow === "hidden" || overflow === "scroll" || overflow === "auto") {
|
|
1154
|
+
const layout = node.layout;
|
|
1155
|
+
const x = layout.x;
|
|
1156
|
+
const y = layout.y;
|
|
1157
|
+
const width = layout.width;
|
|
1158
|
+
const height = layout.height;
|
|
1159
|
+
const radius = layout.border.radius;
|
|
1160
|
+
ctx.save();
|
|
1161
|
+
ctx.beginPath();
|
|
1162
|
+
if (radius > 0) {
|
|
1163
|
+
drawRoundedRect(ctx, x, y, width, height, radius);
|
|
1164
|
+
}
|
|
1165
|
+
else {
|
|
1166
|
+
ctx.rect(x, y, width, height);
|
|
1167
|
+
}
|
|
1168
|
+
ctx.clip();
|
|
1169
|
+
}
|
|
1191
1170
|
}
|
|
1171
|
+
/**
|
|
1172
|
+
* Helper: Draw a star shape
|
|
1173
|
+
*/
|
|
1192
1174
|
function drawStar(ctx, cx, cy, spikes, outerRadius, innerRadius) {
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1175
|
+
let rot = (Math.PI / 2) * 3;
|
|
1176
|
+
let x = cx;
|
|
1177
|
+
let y = cy;
|
|
1178
|
+
const step = Math.PI / spikes;
|
|
1179
|
+
ctx.beginPath();
|
|
1180
|
+
ctx.moveTo(cx, cy - outerRadius);
|
|
1181
|
+
for (let i = 0; i < spikes; i++) {
|
|
1182
|
+
x = cx + Math.cos(rot) * outerRadius;
|
|
1183
|
+
y = cy + Math.sin(rot) * outerRadius;
|
|
1184
|
+
ctx.lineTo(x, y);
|
|
1185
|
+
rot += step;
|
|
1186
|
+
x = cx + Math.cos(rot) * innerRadius;
|
|
1187
|
+
y = cy + Math.sin(rot) * innerRadius;
|
|
1188
|
+
ctx.lineTo(x, y);
|
|
1189
|
+
rot += step;
|
|
1190
|
+
}
|
|
1191
|
+
ctx.lineTo(cx, cy - outerRadius);
|
|
1192
|
+
ctx.closePath();
|
|
1211
1193
|
}
|
|
1212
|
-
|
|
1213
|
-
registerPainter,
|
|
1214
|
-
paintNode,
|
|
1215
|
-
clearImageCache
|
|
1216
|
-
};
|
|
1217
|
-
|
|
1218
|
-
//# debugId=DB7D7BF5FB8A39B564756E2164756E21
|
|
1194
|
+
//# sourceMappingURL=paint.js.map
|