@codehz/draw-call 0.1.1 → 0.1.2

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.
@@ -1,606 +0,0 @@
1
- //#region src/types/base.ts
2
- function linearGradient(angle, ...stops) {
3
- return {
4
- type: "linear-gradient",
5
- angle,
6
- stops: stops.map((stop, index) => {
7
- if (typeof stop === "string") return {
8
- offset: stops.length > 1 ? index / (stops.length - 1) : 0,
9
- color: stop
10
- };
11
- return {
12
- offset: stop[0],
13
- color: stop[1]
14
- };
15
- })
16
- };
17
- }
18
- function radialGradient(options, ...stops) {
19
- const colorStops = stops.map((stop, index) => {
20
- if (typeof stop === "string") return {
21
- offset: stops.length > 1 ? index / (stops.length - 1) : 0,
22
- color: stop
23
- };
24
- return {
25
- offset: stop[0],
26
- color: stop[1]
27
- };
28
- });
29
- return {
30
- type: "radial-gradient",
31
- ...options,
32
- stops: colorStops
33
- };
34
- }
35
- function normalizeSpacing(value) {
36
- if (value === void 0) return {
37
- top: 0,
38
- right: 0,
39
- bottom: 0,
40
- left: 0
41
- };
42
- if (typeof value === "number") return {
43
- top: value,
44
- right: value,
45
- bottom: value,
46
- left: value
47
- };
48
- return {
49
- top: value.top ?? 0,
50
- right: value.right ?? 0,
51
- bottom: value.bottom ?? 0,
52
- left: value.left ?? 0
53
- };
54
- }
55
- function normalizeBorderRadius(value) {
56
- if (value === void 0) return [
57
- 0,
58
- 0,
59
- 0,
60
- 0
61
- ];
62
- if (typeof value === "number") return [
63
- value,
64
- value,
65
- value,
66
- value
67
- ];
68
- return value;
69
- }
70
-
71
- //#endregion
72
- //#region src/types/layout.ts
73
- function resolveSize(size, available, auto) {
74
- if (size === void 0 || size === "auto") return auto;
75
- if (size === "fill") return available;
76
- if (typeof size === "number") return size;
77
- return available * parseFloat(size) / 100;
78
- }
79
- function sizeNeedsParent(size) {
80
- if (size === void 0 || size === "auto") return false;
81
- if (size === "fill") return true;
82
- if (typeof size === "string" && size.endsWith("%")) return true;
83
- return false;
84
- }
85
-
86
- //#endregion
87
- //#region src/layout/measure.ts
88
- function buildFontString(font) {
89
- return `${font.style ?? "normal"} ${font.weight ?? "normal"} ${font.size ?? 16}px ${font.family ?? "sans-serif"}`;
90
- }
91
- function createCanvasMeasureContext(ctx) {
92
- return { measureText(text, font) {
93
- ctx.font = buildFontString(font);
94
- ctx.textBaseline = "middle";
95
- const metrics = ctx.measureText(text);
96
- const height = metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent;
97
- return {
98
- width: metrics.width,
99
- height: height || font.size || 16,
100
- offset: (metrics.actualBoundingBoxAscent - metrics.actualBoundingBoxDescent) / 2
101
- };
102
- } };
103
- }
104
- function wrapText(ctx, text, maxWidth, font) {
105
- if (maxWidth <= 0) {
106
- const { offset } = ctx.measureText(text, font);
107
- return {
108
- lines: [text],
109
- offsets: [offset]
110
- };
111
- }
112
- const lines = [];
113
- const offsets = [];
114
- const paragraphs = text.split("\n");
115
- for (const paragraph of paragraphs) {
116
- if (paragraph === "") {
117
- lines.push("");
118
- offsets.push(0);
119
- continue;
120
- }
121
- const words = paragraph.split(/(\s+)/);
122
- let currentLine = "";
123
- for (const word of words) {
124
- const testLine = currentLine + word;
125
- const { width } = ctx.measureText(testLine, font);
126
- if (width > maxWidth && currentLine !== "") {
127
- const trimmed = currentLine.trim();
128
- lines.push(trimmed);
129
- offsets.push(ctx.measureText(trimmed, font).offset);
130
- currentLine = word.trimStart();
131
- } else currentLine = testLine;
132
- }
133
- if (currentLine) {
134
- const trimmed = currentLine.trim();
135
- lines.push(trimmed);
136
- offsets.push(ctx.measureText(trimmed, font).offset);
137
- }
138
- }
139
- return lines.length > 0 ? {
140
- lines,
141
- offsets
142
- } : {
143
- lines: [""],
144
- offsets: [0]
145
- };
146
- }
147
- function truncateText(ctx, text, maxWidth, font, ellipsis = "...") {
148
- const measured = ctx.measureText(text, font);
149
- if (measured.width <= maxWidth) return {
150
- text,
151
- offset: measured.offset
152
- };
153
- const availableWidth = maxWidth - ctx.measureText(ellipsis, font).width;
154
- if (availableWidth <= 0) {
155
- const { offset } = ctx.measureText(ellipsis, font);
156
- return {
157
- text: ellipsis,
158
- offset
159
- };
160
- }
161
- let left = 0;
162
- let right = text.length;
163
- while (left < right) {
164
- const mid = Math.floor((left + right + 1) / 2);
165
- const truncated = text.slice(0, mid);
166
- const { width: truncatedWidth } = ctx.measureText(truncated, font);
167
- if (truncatedWidth <= availableWidth) left = mid;
168
- else right = mid - 1;
169
- }
170
- const result = text.slice(0, left) + ellipsis;
171
- const { offset } = ctx.measureText(result, font);
172
- return {
173
- text: result,
174
- offset
175
- };
176
- }
177
-
178
- //#endregion
179
- //#region src/layout/engine.ts
180
- function measureIntrinsicSize(element, ctx, availableWidth) {
181
- switch (element.type) {
182
- case "text": {
183
- const font = element.font ?? {};
184
- const lineHeightPx = (font.size ?? 16) * (element.lineHeight ?? 1.2);
185
- if (element.wrap && availableWidth > 0 && availableWidth < Infinity) {
186
- const { lines } = wrapText(ctx, element.content, availableWidth, font);
187
- const { width: maxLineWidth } = lines.reduce((max, line) => {
188
- const { width } = ctx.measureText(line, font);
189
- return width > max.width ? { width } : max;
190
- }, { width: 0 });
191
- return {
192
- width: maxLineWidth,
193
- height: lines.length * lineHeightPx
194
- };
195
- }
196
- const { width, height } = ctx.measureText(element.content, font);
197
- return {
198
- width,
199
- height: Math.max(height, lineHeightPx)
200
- };
201
- }
202
- case "box":
203
- case "stack": {
204
- const padding = normalizeSpacing(element.padding);
205
- const gap = element.type === "box" ? element.gap ?? 0 : 0;
206
- const direction = element.direction ?? "row";
207
- const isRow = direction === "row" || direction === "row-reverse";
208
- let contentWidth = 0;
209
- let contentHeight = 0;
210
- const children = element.children ?? [];
211
- if (element.type === "stack") for (const child of children) {
212
- const childMargin = normalizeSpacing(child.margin);
213
- const childSize = measureIntrinsicSize(child, ctx, availableWidth - padding.left - padding.right - childMargin.left - childMargin.right);
214
- contentWidth = Math.max(contentWidth, childSize.width + childMargin.left + childMargin.right);
215
- contentHeight = Math.max(contentHeight, childSize.height + childMargin.top + childMargin.bottom);
216
- }
217
- else for (let i = 0; i < children.length; i++) {
218
- const child = children[i];
219
- const childMargin = normalizeSpacing(child.margin);
220
- const childSize = measureIntrinsicSize(child, ctx, availableWidth - padding.left - padding.right - childMargin.left - childMargin.right);
221
- if (isRow) {
222
- contentWidth += childSize.width + childMargin.left + childMargin.right;
223
- contentHeight = Math.max(contentHeight, childSize.height + childMargin.top + childMargin.bottom);
224
- if (i > 0) contentWidth += gap;
225
- } else {
226
- contentHeight += childSize.height + childMargin.top + childMargin.bottom;
227
- contentWidth = Math.max(contentWidth, childSize.width + childMargin.left + childMargin.right);
228
- if (i > 0) contentHeight += gap;
229
- }
230
- }
231
- const intrinsicWidth = contentWidth + padding.left + padding.right;
232
- const intrinsicHeight = contentHeight + padding.top + padding.bottom;
233
- return {
234
- width: typeof element.width === "number" ? element.width : intrinsicWidth,
235
- height: typeof element.height === "number" ? element.height : intrinsicHeight
236
- };
237
- }
238
- case "image": return {
239
- width: 0,
240
- height: 0
241
- };
242
- case "shape": return {
243
- width: 0,
244
- height: 0
245
- };
246
- default: return {
247
- width: 0,
248
- height: 0
249
- };
250
- }
251
- }
252
- function computeLayout(element, ctx, constraints, x = 0, y = 0) {
253
- const margin = normalizeSpacing(element.margin);
254
- const padding = normalizeSpacing("padding" in element ? element.padding : void 0);
255
- const availableWidth = constraints.maxWidth - margin.left - margin.right;
256
- const availableHeight = constraints.maxHeight - margin.top - margin.bottom;
257
- const intrinsic = measureIntrinsicSize(element, ctx, availableWidth);
258
- let width = constraints.minWidth === constraints.maxWidth && constraints.minWidth > 0 ? constraints.maxWidth - margin.left - margin.right : resolveSize(element.width, availableWidth, intrinsic.width);
259
- let height = constraints.minHeight === constraints.maxHeight && constraints.minHeight > 0 ? constraints.maxHeight - margin.top - margin.bottom : resolveSize(element.height, availableHeight, intrinsic.height);
260
- if (element.minWidth !== void 0) width = Math.max(width, element.minWidth);
261
- if (element.maxWidth !== void 0) width = Math.min(width, element.maxWidth);
262
- if (element.minHeight !== void 0) height = Math.max(height, element.minHeight);
263
- if (element.maxHeight !== void 0) height = Math.min(height, element.maxHeight);
264
- const actualX = x + margin.left;
265
- const actualY = y + margin.top;
266
- const contentX = actualX + padding.left;
267
- const contentY = actualY + padding.top;
268
- const contentWidth = width - padding.left - padding.right;
269
- const contentHeight = height - padding.top - padding.bottom;
270
- const node = {
271
- element,
272
- layout: {
273
- x: actualX,
274
- y: actualY,
275
- width,
276
- height,
277
- contentX,
278
- contentY,
279
- contentWidth,
280
- contentHeight
281
- },
282
- children: []
283
- };
284
- if (element.type === "text") {
285
- const font = element.font ?? {};
286
- if (element.wrap && contentWidth > 0) {
287
- let { lines, offsets } = wrapText(ctx, element.content, contentWidth, font);
288
- if (element.maxLines && lines.length > element.maxLines) {
289
- lines = lines.slice(0, element.maxLines);
290
- offsets = offsets.slice(0, element.maxLines);
291
- if (element.ellipsis && lines.length > 0) {
292
- const lastIdx = lines.length - 1;
293
- const truncated = truncateText(ctx, lines[lastIdx], contentWidth, font);
294
- lines[lastIdx] = truncated.text;
295
- offsets[lastIdx] = truncated.offset;
296
- }
297
- }
298
- node.lines = lines;
299
- node.lineOffsets = offsets;
300
- } else {
301
- const { text, offset } = truncateText(ctx, element.content, contentWidth > 0 && element.ellipsis ? contentWidth : Infinity, font);
302
- node.lines = [text];
303
- node.lineOffsets = [offset];
304
- }
305
- }
306
- if (element.type === "box" || element.type === "stack") {
307
- const children = element.children ?? [];
308
- if (element.type === "stack") for (const child of children) {
309
- const childNode = computeLayout(child, ctx, {
310
- minWidth: 0,
311
- maxWidth: contentWidth,
312
- minHeight: 0,
313
- maxHeight: contentHeight
314
- }, contentX, contentY);
315
- node.children.push(childNode);
316
- }
317
- else {
318
- const direction = element.direction ?? "row";
319
- const justify = element.justify ?? "start";
320
- const align = element.align ?? "stretch";
321
- const gap = element.gap ?? 0;
322
- const isRow = direction === "row" || direction === "row-reverse";
323
- const isReverse = direction === "row-reverse" || direction === "column-reverse";
324
- const childInfos = [];
325
- let totalFixed = 0;
326
- let totalFlex = 0;
327
- let totalGap = children.length > 1 ? gap * (children.length - 1) : 0;
328
- for (const child of children) {
329
- const childMargin = normalizeSpacing(child.margin);
330
- const childFlex = child.flex ?? 0;
331
- if (childFlex > 0) {
332
- totalFlex += childFlex;
333
- childInfos.push({
334
- element: child,
335
- width: 0,
336
- height: 0,
337
- flex: childFlex,
338
- margin: childMargin
339
- });
340
- } else {
341
- const size = measureIntrinsicSize(child, ctx, isRow ? contentWidth - childMargin.left - childMargin.right : contentWidth - childMargin.left - childMargin.right);
342
- const shouldStretchWidth = !isRow && child.width === void 0 && align === "stretch";
343
- const shouldStretchHeight = isRow && child.height === void 0 && align === "stretch";
344
- let w = sizeNeedsParent(child.width) ? resolveSize(child.width, contentWidth - childMargin.left - childMargin.right, size.width) : resolveSize(child.width, 0, size.width);
345
- let h = sizeNeedsParent(child.height) ? resolveSize(child.height, contentHeight - childMargin.top - childMargin.bottom, size.height) : resolveSize(child.height, 0, size.height);
346
- if (shouldStretchWidth) w = contentWidth - childMargin.left - childMargin.right;
347
- if (shouldStretchHeight) h = contentHeight - childMargin.top - childMargin.bottom;
348
- if (isRow) totalFixed += w + childMargin.left + childMargin.right;
349
- else totalFixed += h + childMargin.top + childMargin.bottom;
350
- childInfos.push({
351
- element: child,
352
- width: w,
353
- height: h,
354
- flex: 0,
355
- margin: childMargin
356
- });
357
- }
358
- }
359
- const availableForFlex = isRow ? Math.max(0, contentWidth - totalFixed - totalGap) : Math.max(0, contentHeight - totalFixed - totalGap);
360
- for (const info of childInfos) if (info.flex > 0) {
361
- const flexSize = availableForFlex * info.flex / totalFlex;
362
- if (isRow) {
363
- info.width = flexSize;
364
- const size = measureIntrinsicSize(info.element, ctx, flexSize);
365
- info.height = sizeNeedsParent(info.element.height) ? resolveSize(info.element.height, contentHeight - info.margin.top - info.margin.bottom, size.height) : resolveSize(info.element.height, 0, size.height);
366
- } else {
367
- info.height = flexSize;
368
- const size = measureIntrinsicSize(info.element, ctx, contentWidth - info.margin.left - info.margin.right);
369
- info.width = sizeNeedsParent(info.element.width) ? resolveSize(info.element.width, contentWidth - info.margin.left - info.margin.right, size.width) : resolveSize(info.element.width, 0, size.width);
370
- }
371
- }
372
- const totalSize = childInfos.reduce((sum, info) => {
373
- if (isRow) return sum + info.width + info.margin.left + info.margin.right;
374
- else return sum + info.height + info.margin.top + info.margin.bottom;
375
- }, 0) + totalGap;
376
- const freeSpace = (isRow ? contentWidth : contentHeight) - totalSize;
377
- let mainStart = 0;
378
- let mainGap = gap;
379
- switch (justify) {
380
- case "start":
381
- mainStart = 0;
382
- break;
383
- case "end":
384
- mainStart = freeSpace;
385
- break;
386
- case "center":
387
- mainStart = freeSpace / 2;
388
- break;
389
- case "space-between":
390
- mainStart = 0;
391
- if (children.length > 1) mainGap = gap + freeSpace / (children.length - 1);
392
- break;
393
- case "space-around":
394
- if (children.length > 0) {
395
- const spacing = freeSpace / children.length;
396
- mainStart = spacing / 2;
397
- mainGap = gap + spacing;
398
- }
399
- break;
400
- case "space-evenly":
401
- if (children.length > 0) {
402
- const spacing = freeSpace / (children.length + 1);
403
- mainStart = spacing;
404
- mainGap = gap + spacing;
405
- }
406
- break;
407
- }
408
- let mainOffset = mainStart;
409
- const orderedInfos = isReverse ? [...childInfos].reverse() : childInfos;
410
- for (let i = 0; i < orderedInfos.length; i++) {
411
- const info = orderedInfos[i];
412
- const crossAxisSize = isRow ? contentHeight : contentWidth;
413
- const childCrossSize = isRow ? info.height + info.margin.top + info.margin.bottom : info.width + info.margin.left + info.margin.right;
414
- let crossOffset = 0;
415
- switch (info.element.alignSelf === "auto" || info.element.alignSelf === void 0 ? align : info.element.alignSelf) {
416
- case "start":
417
- crossOffset = 0;
418
- break;
419
- case "end":
420
- crossOffset = crossAxisSize - childCrossSize;
421
- break;
422
- case "center":
423
- crossOffset = (crossAxisSize - childCrossSize) / 2;
424
- break;
425
- case "stretch":
426
- crossOffset = 0;
427
- if (isRow && info.element.height === void 0) info.height = crossAxisSize - info.margin.top - info.margin.bottom;
428
- else if (!isRow && info.element.width === void 0) info.width = crossAxisSize - info.margin.left - info.margin.right;
429
- break;
430
- case "baseline":
431
- crossOffset = 0;
432
- break;
433
- }
434
- const childX = isRow ? contentX + mainOffset + info.margin.left : contentX + crossOffset + info.margin.left;
435
- const childY = isRow ? contentY + crossOffset + info.margin.top : contentY + mainOffset + info.margin.top;
436
- const stretchWidth = !isRow && info.element.width === void 0 && align === "stretch" ? contentWidth - info.margin.left - info.margin.right : null;
437
- const stretchHeight = isRow && info.element.height === void 0 && align === "stretch" ? contentHeight - info.margin.top - info.margin.bottom : null;
438
- const childNode = computeLayout(info.element, ctx, {
439
- minWidth: stretchWidth ?? 0,
440
- maxWidth: stretchWidth ?? info.width,
441
- minHeight: stretchHeight ?? 0,
442
- maxHeight: stretchHeight ?? info.height
443
- }, childX - info.margin.left, childY - info.margin.top);
444
- node.children.push(childNode);
445
- mainOffset += isRow ? info.width + info.margin.left + info.margin.right : info.height + info.margin.top + info.margin.bottom;
446
- if (i < orderedInfos.length - 1) mainOffset += mainGap;
447
- }
448
- if (isReverse) node.children.reverse();
449
- }
450
- }
451
- return node;
452
- }
453
-
454
- //#endregion
455
- //#region src/render/engine.ts
456
- function isGradientDescriptor(color) {
457
- return typeof color === "object" && color !== null && "type" in color && (color.type === "linear-gradient" || color.type === "radial-gradient");
458
- }
459
- function resolveGradient(ctx, descriptor, x, y, width, height) {
460
- if (descriptor.type === "linear-gradient") {
461
- const angleRad = (descriptor.angle - 90) * Math.PI / 180;
462
- const centerX = x + width / 2;
463
- const centerY = y + height / 2;
464
- const diagLength = Math.sqrt(width * width + height * height) / 2;
465
- const x0 = centerX - Math.cos(angleRad) * diagLength;
466
- const y0 = centerY - Math.sin(angleRad) * diagLength;
467
- const x1 = centerX + Math.cos(angleRad) * diagLength;
468
- const y1 = centerY + Math.sin(angleRad) * diagLength;
469
- const gradient = ctx.createLinearGradient(x0, y0, x1, y1);
470
- for (const stop of descriptor.stops) gradient.addColorStop(stop.offset, stop.color);
471
- return gradient;
472
- } else {
473
- const diagLength = Math.sqrt(width * width + height * height);
474
- const startX = x + (descriptor.startX ?? .5) * width;
475
- const startY = y + (descriptor.startY ?? .5) * height;
476
- const startRadius = (descriptor.startRadius ?? 0) * diagLength;
477
- const endX = x + (descriptor.endX ?? .5) * width;
478
- const endY = y + (descriptor.endY ?? .5) * height;
479
- const endRadius = (descriptor.endRadius ?? .5) * diagLength;
480
- const gradient = ctx.createRadialGradient(startX, startY, startRadius, endX, endY, endRadius);
481
- for (const stop of descriptor.stops) gradient.addColorStop(stop.offset, stop.color);
482
- return gradient;
483
- }
484
- }
485
- function resolveColor(ctx, color, x, y, width, height) {
486
- if (isGradientDescriptor(color)) return resolveGradient(ctx, color, x, y, width, height);
487
- return color;
488
- }
489
- function roundRectPath(ctx, x, y, width, height, radius) {
490
- const [tl, tr, br, bl] = radius;
491
- ctx.beginPath();
492
- ctx.moveTo(x + tl, y);
493
- ctx.lineTo(x + width - tr, y);
494
- ctx.quadraticCurveTo(x + width, y, x + width, y + tr);
495
- ctx.lineTo(x + width, y + height - br);
496
- ctx.quadraticCurveTo(x + width, y + height, x + width - br, y + height);
497
- ctx.lineTo(x + bl, y + height);
498
- ctx.quadraticCurveTo(x, y + height, x, y + height - bl);
499
- ctx.lineTo(x, y + tl);
500
- ctx.quadraticCurveTo(x, y, x + tl, y);
501
- ctx.closePath();
502
- }
503
- function applyShadow(ctx, shadow) {
504
- if (shadow) {
505
- ctx.shadowOffsetX = shadow.offsetX ?? 0;
506
- ctx.shadowOffsetY = shadow.offsetY ?? 0;
507
- ctx.shadowBlur = shadow.blur ?? 0;
508
- ctx.shadowColor = shadow.color ?? "rgba(0,0,0,0.5)";
509
- } else {
510
- ctx.shadowOffsetX = 0;
511
- ctx.shadowOffsetY = 0;
512
- ctx.shadowBlur = 0;
513
- ctx.shadowColor = "transparent";
514
- }
515
- }
516
- function clearShadow(ctx) {
517
- ctx.shadowOffsetX = 0;
518
- ctx.shadowOffsetY = 0;
519
- ctx.shadowBlur = 0;
520
- ctx.shadowColor = "transparent";
521
- }
522
- function renderBox(ctx, node) {
523
- const element = node.element;
524
- const { x, y, width, height } = node.layout;
525
- const border = element.border;
526
- const radius = normalizeBorderRadius(border?.radius);
527
- const hasRadius = radius.some((r) => r > 0);
528
- if (element.opacity !== void 0 && element.opacity < 1) ctx.globalAlpha = element.opacity;
529
- if (element.shadow && element.background) applyShadow(ctx, element.shadow);
530
- if (element.background) {
531
- ctx.fillStyle = resolveColor(ctx, element.background, x, y, width, height);
532
- if (hasRadius) {
533
- roundRectPath(ctx, x, y, width, height, radius);
534
- ctx.fill();
535
- } else ctx.fillRect(x, y, width, height);
536
- clearShadow(ctx);
537
- }
538
- if (border && border.width && border.width > 0) {
539
- ctx.strokeStyle = border.color ? resolveColor(ctx, border.color, x, y, width, height) : "#000";
540
- ctx.lineWidth = border.width;
541
- if (hasRadius) {
542
- roundRectPath(ctx, x, y, width, height, radius);
543
- ctx.stroke();
544
- } else ctx.strokeRect(x, y, width, height);
545
- }
546
- if (element.opacity !== void 0 && element.opacity < 1) ctx.globalAlpha = 1;
547
- }
548
- function renderText(ctx, node) {
549
- const element = node.element;
550
- const { contentX, contentY, contentWidth, contentHeight } = node.layout;
551
- const lines = node.lines ?? [element.content];
552
- const font = element.font ?? {};
553
- const lineHeightPx = (font.size ?? 16) * (element.lineHeight ?? 1.2);
554
- ctx.font = buildFontString(font);
555
- ctx.fillStyle = element.color ? resolveColor(ctx, element.color, contentX, contentY, contentWidth, contentHeight) : "#000";
556
- let textAlign = "left";
557
- if (element.align === "center") textAlign = "center";
558
- else if (element.align === "right") textAlign = "right";
559
- ctx.textAlign = textAlign;
560
- ctx.textBaseline = "middle";
561
- const totalTextHeight = lines.length * lineHeightPx;
562
- let verticalOffset = 0;
563
- if (element.verticalAlign === "middle") verticalOffset = (contentHeight - totalTextHeight) / 2;
564
- else if (element.verticalAlign === "bottom") verticalOffset = contentHeight - totalTextHeight;
565
- let textX = contentX;
566
- if (element.align === "center") textX = contentX + contentWidth / 2;
567
- else if (element.align === "right") textX = contentX + contentWidth;
568
- if (element.shadow) applyShadow(ctx, element.shadow);
569
- for (let i = 0; i < lines.length; i++) {
570
- const correctedLineY = contentY + verticalOffset + i * lineHeightPx + lineHeightPx / 2 + (node.lineOffsets?.[i] ?? 0);
571
- if (element.stroke) {
572
- ctx.strokeStyle = resolveColor(ctx, element.stroke.color, contentX, contentY, contentWidth, contentHeight);
573
- ctx.lineWidth = element.stroke.width;
574
- ctx.strokeText(lines[i], textX, correctedLineY);
575
- }
576
- ctx.fillText(lines[i], textX, correctedLineY);
577
- }
578
- if (element.shadow) clearShadow(ctx);
579
- }
580
- function renderNode(ctx, node) {
581
- const element = node.element;
582
- switch (element.type) {
583
- case "box":
584
- case "stack": {
585
- renderBox(ctx, node);
586
- const shouldClip = element.clip === true;
587
- if (shouldClip) {
588
- ctx.save();
589
- const { x, y, width, height } = node.layout;
590
- roundRectPath(ctx, x, y, width, height, normalizeBorderRadius(element.border?.radius));
591
- ctx.clip();
592
- }
593
- for (const child of node.children) renderNode(ctx, child);
594
- if (shouldClip) ctx.restore();
595
- break;
596
- }
597
- case "text":
598
- renderText(ctx, node);
599
- break;
600
- case "image": break;
601
- case "shape": break;
602
- }
603
- }
604
-
605
- //#endregion
606
- export { radialGradient as a, linearGradient as i, computeLayout as n, createCanvasMeasureContext as r, renderNode as t };