@meonode/canvas 1.0.0-beta.1

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.
Files changed (65) hide show
  1. package/CONTRIBUTING.md +75 -0
  2. package/LICENSE +21 -0
  3. package/Readme.md +382 -0
  4. package/dist/cjs/canvas/canvas.helper.d.ts +57 -0
  5. package/dist/cjs/canvas/canvas.helper.d.ts.map +1 -0
  6. package/dist/cjs/canvas/canvas.helper.js +239 -0
  7. package/dist/cjs/canvas/canvas.helper.js.map +1 -0
  8. package/dist/cjs/canvas/canvas.type.d.ts +657 -0
  9. package/dist/cjs/canvas/canvas.type.d.ts.map +1 -0
  10. package/dist/cjs/canvas/grid.canvas.util.d.ts +39 -0
  11. package/dist/cjs/canvas/grid.canvas.util.d.ts.map +1 -0
  12. package/dist/cjs/canvas/grid.canvas.util.js +263 -0
  13. package/dist/cjs/canvas/grid.canvas.util.js.map +1 -0
  14. package/dist/cjs/canvas/image.canvas.util.d.ts +34 -0
  15. package/dist/cjs/canvas/image.canvas.util.d.ts.map +1 -0
  16. package/dist/cjs/canvas/image.canvas.util.js +310 -0
  17. package/dist/cjs/canvas/image.canvas.util.js.map +1 -0
  18. package/dist/cjs/canvas/layout.canvas.util.d.ts +123 -0
  19. package/dist/cjs/canvas/layout.canvas.util.d.ts.map +1 -0
  20. package/dist/cjs/canvas/layout.canvas.util.js +785 -0
  21. package/dist/cjs/canvas/layout.canvas.util.js.map +1 -0
  22. package/dist/cjs/canvas/root.canvas.util.d.ts +42 -0
  23. package/dist/cjs/canvas/root.canvas.util.d.ts.map +1 -0
  24. package/dist/cjs/canvas/root.canvas.util.js +140 -0
  25. package/dist/cjs/canvas/root.canvas.util.js.map +1 -0
  26. package/dist/cjs/canvas/text.canvas.util.d.ts +148 -0
  27. package/dist/cjs/canvas/text.canvas.util.d.ts.map +1 -0
  28. package/dist/cjs/canvas/text.canvas.util.js +1112 -0
  29. package/dist/cjs/canvas/text.canvas.util.js.map +1 -0
  30. package/dist/cjs/constant/common.const.d.ts +37 -0
  31. package/dist/cjs/constant/common.const.d.ts.map +1 -0
  32. package/dist/cjs/constant/common.const.js +51 -0
  33. package/dist/cjs/constant/common.const.js.map +1 -0
  34. package/dist/cjs/index.d.ts +7 -0
  35. package/dist/cjs/index.d.ts.map +1 -0
  36. package/dist/cjs/index.js +31 -0
  37. package/dist/cjs/index.js.map +1 -0
  38. package/dist/esm/canvas/canvas.helper.d.ts +57 -0
  39. package/dist/esm/canvas/canvas.helper.d.ts.map +1 -0
  40. package/dist/esm/canvas/canvas.helper.js +214 -0
  41. package/dist/esm/canvas/canvas.type.d.ts +657 -0
  42. package/dist/esm/canvas/canvas.type.d.ts.map +1 -0
  43. package/dist/esm/canvas/grid.canvas.util.d.ts +39 -0
  44. package/dist/esm/canvas/grid.canvas.util.d.ts.map +1 -0
  45. package/dist/esm/canvas/grid.canvas.util.js +259 -0
  46. package/dist/esm/canvas/image.canvas.util.d.ts +34 -0
  47. package/dist/esm/canvas/image.canvas.util.d.ts.map +1 -0
  48. package/dist/esm/canvas/image.canvas.util.js +306 -0
  49. package/dist/esm/canvas/layout.canvas.util.d.ts +123 -0
  50. package/dist/esm/canvas/layout.canvas.util.d.ts.map +1 -0
  51. package/dist/esm/canvas/layout.canvas.util.js +777 -0
  52. package/dist/esm/canvas/root.canvas.util.d.ts +42 -0
  53. package/dist/esm/canvas/root.canvas.util.d.ts.map +1 -0
  54. package/dist/esm/canvas/root.canvas.util.js +116 -0
  55. package/dist/esm/canvas/text.canvas.util.d.ts +148 -0
  56. package/dist/esm/canvas/text.canvas.util.d.ts.map +1 -0
  57. package/dist/esm/canvas/text.canvas.util.js +1108 -0
  58. package/dist/esm/constant/common.const.d.ts +37 -0
  59. package/dist/esm/constant/common.const.d.ts.map +1 -0
  60. package/dist/esm/constant/common.const.js +23 -0
  61. package/dist/esm/index.d.ts +7 -0
  62. package/dist/esm/index.d.ts.map +1 -0
  63. package/dist/esm/index.js +7 -0
  64. package/dist/meonode-canvas-1.0.0-beta.1.tgz +0 -0
  65. package/package.json +79 -0
@@ -0,0 +1,785 @@
1
+ 'use strict';
2
+
3
+ var skiaCanvas = require('skia-canvas');
4
+ var canvas_helper = require('./canvas.helper.js');
5
+ var lodashEs = require('lodash-es');
6
+ var tinycolor = require('tinycolor2');
7
+ var common_const = require('../constant/common.const.js');
8
+ var YogaTypes = require('yoga-layout');
9
+
10
+ /**
11
+ * @class BoxNode
12
+ * @classdesc Base node class for rendering rectangular boxes with layout, styling, and children.
13
+ * It uses the Yoga layout engine for positioning and sizing.
14
+ */
15
+ class BoxNode {
16
+ /**
17
+ * @property {Partial<BoxProps>} initialProps - Original props passed to the constructor before any modifications.
18
+ */
19
+ initialProps;
20
+ /**
21
+ * @property {Node} node - The Yoga layout engine node.
22
+ */
23
+ node;
24
+ /**
25
+ * @property {BoxNode[]} children - Child nodes.
26
+ */
27
+ children;
28
+ /**
29
+ * @property {BoxProps & BaseProps} props - Current props including defaults and inherited values.
30
+ */
31
+ props;
32
+ /**
33
+ * @property {string} name - Node type name.
34
+ */
35
+ name;
36
+ /**
37
+ * @property {string} key - Unique node identifier.
38
+ */
39
+ key;
40
+ /**
41
+ * Creates a new BoxNode instance
42
+ * @param props - Initial box properties and styling
43
+ */
44
+ constructor(props = {}) {
45
+ const children = (Array.isArray(props?.children) ? props.children : [props.children]).filter(child => child);
46
+ this.initialProps = { ...props, children };
47
+ this.node = YogaTypes.Node.create();
48
+ this.children = [];
49
+ this.props = {
50
+ key: this.key,
51
+ borderColor: 'black',
52
+ borderStyle: common_const.Style.Border.Solid,
53
+ boxSizing: common_const.Style.BoxSizing.BorderBox,
54
+ opacity: 1,
55
+ flexShrink: 1,
56
+ ...this.initialProps,
57
+ };
58
+ this.name = this.props.name || 'Box';
59
+ this.key = this.props.key || `${this.name}-0`;
60
+ this.setLayout(this.props);
61
+ }
62
+ /**
63
+ * Processes and appends any children passed in the initial props.
64
+ */
65
+ processInitialChildren() {
66
+ if (this.props.children) {
67
+ const childrenToAdd = Array.isArray(this.props.children) ? this.props.children : [this.props.children];
68
+ childrenToAdd.forEach((child, index) => {
69
+ if (child) {
70
+ this.appendChild(child, index);
71
+ }
72
+ });
73
+ }
74
+ }
75
+ /**
76
+ * Inherits styles from the parent node.
77
+ * @param {BoxProps & BaseProps} parentProps - Parent node properties to inherit from.
78
+ */
79
+ resolveInheritedStyles(parentProps) {
80
+ if (parentProps.key) {
81
+ this.key = `${parentProps.key}-${this.key}`;
82
+ this.props.key = this.key;
83
+ }
84
+ const inheritableKeys = [
85
+ 'fontSize',
86
+ 'fontFamily',
87
+ 'fontWeight',
88
+ 'fontStyle',
89
+ 'color',
90
+ 'textAlign',
91
+ 'verticalAlign',
92
+ 'lineHeight',
93
+ 'lineGap',
94
+ 'letterSpacing',
95
+ 'wordSpacing',
96
+ 'textDecoration',
97
+ 'maxLines',
98
+ 'fontVariant',
99
+ ];
100
+ for (const key of inheritableKeys) {
101
+ if (this.initialProps[key] === undefined && parentProps[key] !== undefined) {
102
+ this.props[key] = parentProps[key];
103
+ }
104
+ }
105
+ if (!this.node.isDirty()) {
106
+ this.node.markDirty();
107
+ }
108
+ }
109
+ /**
110
+ * Applies node type-specific default values after inheritance.
111
+ */
112
+ applyDefaults() {
113
+ // Base implementation does nothing; subclasses can override.
114
+ }
115
+ /**
116
+ * Appends a child node at the specified index.
117
+ * @param {BoxNode} child - Child node to append.
118
+ * @param index - Index to insert child at
119
+ */
120
+ appendChild(child, index) {
121
+ if (!child || !child.node) {
122
+ console.warn('Attempted to append an invalid child node.', child);
123
+ return;
124
+ }
125
+ child.resolveInheritedStyles(lodashEs.omit(this.props, 'children'));
126
+ child.applyDefaults();
127
+ this.children.push(child);
128
+ this.node.insertChild(child.node, index);
129
+ child.processInitialChildren();
130
+ }
131
+ /**
132
+ * Performs final layout adjustments recursively after the main layout calculation.
133
+ * @returns {boolean} Whether any node was marked as dirty during finalization.
134
+ */
135
+ finalizeLayout() {
136
+ let wasDirty = false;
137
+ this.updateLayoutBasedOnComputedSize();
138
+ if (this.node.isDirty()) {
139
+ wasDirty = true;
140
+ }
141
+ for (const child of this.children) {
142
+ child.finalizeLayout();
143
+ if (child.node.isDirty()) {
144
+ wasDirty = true;
145
+ }
146
+ }
147
+ return wasDirty;
148
+ }
149
+ /**
150
+ * Hook for subclasses to update layout based on computed size.
151
+ */
152
+ updateLayoutBasedOnComputedSize() {
153
+ // Base implementation does nothing; subclasses can override.
154
+ }
155
+ /**
156
+ * Applies layout properties to the Yoga node.
157
+ * @param props - Box properties containing layout values
158
+ */
159
+ setLayout(props) {
160
+ // --- Yoga layout property application ---
161
+ // (This entire block remains unchanged as it interacts with Yoga, not the canvas library)
162
+ const { width, height, minWidth, minHeight, maxWidth, maxHeight, flexDirection, justifyContent, alignItems, alignSelf, alignContent, flexGrow, flexShrink, flexBasis, positionType, position, gap, margin, padding, border, aspectRatio, overflow, display, boxSizing = common_const.Style.BoxSizing.BorderBox, direction = common_const.Style.Direction.LTR, flexWrap, } = props;
163
+ if (width !== undefined)
164
+ this.node.setWidth(width);
165
+ if (height !== undefined)
166
+ this.node.setHeight(height);
167
+ if (minWidth !== undefined)
168
+ this.node.setMinWidth(minWidth);
169
+ if (minHeight !== undefined)
170
+ this.node.setMinHeight(minHeight);
171
+ if (maxWidth !== undefined)
172
+ this.node.setMaxWidth(maxWidth);
173
+ if (maxHeight !== undefined)
174
+ this.node.setMaxHeight(maxHeight);
175
+ if (flexDirection !== undefined)
176
+ this.node.setFlexDirection(flexDirection);
177
+ if (justifyContent !== undefined)
178
+ this.node.setJustifyContent(justifyContent);
179
+ if (alignItems !== undefined)
180
+ this.node.setAlignItems(alignItems);
181
+ if (alignSelf !== undefined)
182
+ this.node.setAlignSelf(alignSelf);
183
+ if (alignContent !== undefined)
184
+ this.node.setAlignContent(alignContent);
185
+ if (flexGrow !== undefined)
186
+ this.node.setFlexGrow(flexGrow);
187
+ if (flexShrink !== undefined)
188
+ this.node.setFlexShrink(flexShrink);
189
+ if (positionType !== undefined)
190
+ this.node.setPositionType(positionType);
191
+ if (flexBasis !== undefined)
192
+ this.node.setFlexBasis(flexBasis);
193
+ if (position) {
194
+ if (typeof position === 'number') {
195
+ this.node.setPosition(common_const.Style.Edge.All, position);
196
+ }
197
+ else if (typeof position === 'string' && position.endsWith('%')) {
198
+ this.node.setPositionPercent(common_const.Style.Edge.All, parseFloat(position));
199
+ }
200
+ else {
201
+ for (const [edge, value] of Object.entries(position)) {
202
+ if (edge in common_const.Style.Edge) {
203
+ if (typeof value === 'string' && value.endsWith('%')) {
204
+ this.node.setPositionPercent(common_const.Style.Edge[edge], parseFloat(value));
205
+ }
206
+ else {
207
+ this.node.setPosition(common_const.Style.Edge[edge], value);
208
+ }
209
+ }
210
+ }
211
+ }
212
+ }
213
+ if (gap) {
214
+ if (typeof gap === 'number') {
215
+ this.node.setGap(common_const.Style.Gutter.All, gap);
216
+ }
217
+ else if (typeof gap === 'string' && gap.endsWith('%')) {
218
+ this.node.setGapPercent(common_const.Style.Gutter.All, parseFloat(gap));
219
+ }
220
+ else {
221
+ for (const [gutter, value] of Object.entries(gap)) {
222
+ if (gutter in common_const.Style.Gutter) {
223
+ if (typeof value === 'string' && value.endsWith('%')) {
224
+ this.node.setGapPercent(common_const.Style.Gutter[gutter], parseFloat(value));
225
+ }
226
+ else {
227
+ this.node.setGap(common_const.Style.Gutter[gutter], value);
228
+ }
229
+ }
230
+ }
231
+ }
232
+ }
233
+ if (margin) {
234
+ if (typeof margin === 'number' || margin === 'auto') {
235
+ this.node.setMargin(common_const.Style.Edge.All, margin);
236
+ }
237
+ else if (typeof margin === 'string' && margin.endsWith('%')) {
238
+ this.node.setMarginPercent(common_const.Style.Edge.All, parseFloat(margin));
239
+ }
240
+ else {
241
+ for (const [edge, value] of Object.entries(margin)) {
242
+ if (edge in common_const.Style.Edge) {
243
+ if (typeof value === 'string' && value.endsWith('%')) {
244
+ this.node.setMarginPercent(common_const.Style.Edge[edge], parseFloat(value));
245
+ }
246
+ else {
247
+ this.node.setMargin(common_const.Style.Edge[edge], value);
248
+ }
249
+ }
250
+ }
251
+ }
252
+ }
253
+ if (padding) {
254
+ if (typeof padding === 'number') {
255
+ this.node.setPadding(common_const.Style.Edge.All, padding);
256
+ }
257
+ else if (typeof padding === 'string' && padding.endsWith('%')) {
258
+ this.node.setPaddingPercent(common_const.Style.Edge.All, parseFloat(padding));
259
+ }
260
+ else {
261
+ for (const [edge, value] of Object.entries(padding)) {
262
+ if (edge in common_const.Style.Edge) {
263
+ if (typeof value === 'string' && value.endsWith('%')) {
264
+ this.node.setPaddingPercent(common_const.Style.Edge[edge], parseFloat(value));
265
+ }
266
+ else {
267
+ this.node.setPadding(common_const.Style.Edge[edge], value);
268
+ }
269
+ }
270
+ }
271
+ }
272
+ }
273
+ if (border) {
274
+ if (typeof border === 'number') {
275
+ this.node.setBorder(common_const.Style.Edge.All, border);
276
+ }
277
+ else {
278
+ for (const [edge, value] of Object.entries(border)) {
279
+ if (edge in common_const.Style.Edge)
280
+ this.node.setBorder(common_const.Style.Edge[edge], value);
281
+ }
282
+ }
283
+ }
284
+ if (aspectRatio !== undefined)
285
+ this.node.setAspectRatio(aspectRatio);
286
+ if (overflow !== undefined)
287
+ this.node.setOverflow(overflow);
288
+ if (display !== undefined)
289
+ this.node.setDisplay(display);
290
+ if (boxSizing !== undefined)
291
+ this.node.setBoxSizing(boxSizing);
292
+ if (direction !== undefined)
293
+ this.node.setDirection(direction);
294
+ if (flexWrap !== undefined)
295
+ this.node.setFlexWrap(flexWrap);
296
+ // --- End Yoga layout property application ---
297
+ }
298
+ /**
299
+ * Renders the node and its children to the canvas.
300
+ * @param {CanvasRenderingContext2D} ctx - Canvas rendering context (from skia-canvas).
301
+ * @param {number} offsetX - X offset for rendering.
302
+ * @param {number} offsetY - Y offset for rendering.
303
+ */
304
+ render(ctx, offsetX = 0, offsetY = 0) {
305
+ const layout = this.node.getComputedLayout();
306
+ const x = layout.left + offsetX;
307
+ const y = layout.top + offsetY;
308
+ const width = layout.width;
309
+ const height = layout.height;
310
+ // Exit early if the node is invisible or has no dimensions.
311
+ if (width <= 0 || height <= 0 || this.props.display === common_const.Style.Display.None) {
312
+ return;
313
+ }
314
+ // --- Opacity Setup ---
315
+ const desiredOpacity = Math.max(0, Math.min(1, this.props.opacity ?? 1));
316
+ let originalAlpha = undefined;
317
+ let appliedOpacity = false;
318
+ if (desiredOpacity < 1) {
319
+ originalAlpha = ctx.globalAlpha;
320
+ ctx.globalAlpha = originalAlpha * desiredOpacity;
321
+ appliedOpacity = true;
322
+ }
323
+ // --- End Opacity Setup ---
324
+ try {
325
+ // --- Transformation Setup ---
326
+ const transform = this.props.transform;
327
+ const needsTransform = transform &&
328
+ (transform.translateX ||
329
+ transform.translateY ||
330
+ transform.rotate ||
331
+ transform.scale ||
332
+ transform.scaleX ||
333
+ transform.scaleY);
334
+ let savedContextForTransform = false;
335
+ if (needsTransform) {
336
+ ctx.save();
337
+ savedContextForTransform = true;
338
+ const originXRaw = transform.originX ?? '50%';
339
+ const originYRaw = transform.originY ?? '50%';
340
+ const originOffsetX = canvas_helper.parsePercentage(originXRaw, width);
341
+ const originOffsetY = canvas_helper.parsePercentage(originYRaw, height);
342
+ const originAbsX = x + originOffsetX;
343
+ const originAbsY = y + originOffsetY;
344
+ ctx.translate(originAbsX, originAbsY);
345
+ if (transform.translateX || transform.translateY) {
346
+ const tx = canvas_helper.parsePercentage(transform.translateX, width);
347
+ const ty = canvas_helper.parsePercentage(transform.translateY, height);
348
+ if (tx !== 0 || ty !== 0)
349
+ ctx.translate(tx, ty);
350
+ }
351
+ if (transform.rotate) {
352
+ ctx.rotate((transform.rotate * Math.PI) / 180);
353
+ }
354
+ if (transform.scale || transform.scaleX || transform.scaleY) {
355
+ const scaleX = transform.scaleX ?? transform.scale ?? 1;
356
+ const scaleY = transform.scaleY ?? transform.scale ?? 1;
357
+ if (scaleX !== 1 || scaleY !== 1)
358
+ ctx.scale(scaleX, scaleY);
359
+ }
360
+ ctx.translate(-originAbsX, -originAbsY);
361
+ }
362
+ // --- End Transformation Setup ---
363
+ // --- Step 1: Render Parent Background/Borders/Content ---
364
+ // This renders the current node's own visual appearance first.
365
+ this._renderContent(ctx, x, y, width, height);
366
+ // --- Step 2: Prepare Children for Stacking ---
367
+ const positionedChildren = [];
368
+ const inFlowChildren = [];
369
+ this.children.forEach((child, index) => {
370
+ // Check if child participates in zIndex stacking
371
+ if (child.props.positionType === common_const.Style.PositionType.Absolute && child.props.zIndex !== undefined) {
372
+ positionedChildren.push({
373
+ node: child,
374
+ zIndex: child.props.zIndex,
375
+ originalIndex: index, // Keep original order for tie-breaking
376
+ });
377
+ }
378
+ else {
379
+ inFlowChildren.push(child);
380
+ }
381
+ });
382
+ // Sort positioned children by zIndex, then by original order
383
+ positionedChildren.sort((a, b) => {
384
+ return a.zIndex - b.zIndex || a.originalIndex - b.originalIndex;
385
+ });
386
+ // --- Step 3: Handle Clipping (Applies before drawing children) ---
387
+ let savedContextForClip = false;
388
+ if (this.props.overflow === common_const.Style.Overflow.Hidden && (width > 0 || height > 0)) {
389
+ ctx.save();
390
+ savedContextForClip = true;
391
+ const borderLeft = this.node.getComputedBorder(common_const.Style.Edge.Left);
392
+ const borderTop = this.node.getComputedBorder(common_const.Style.Edge.Top);
393
+ const borderRight = this.node.getComputedBorder(common_const.Style.Edge.Right);
394
+ const borderBottom = this.node.getComputedBorder(common_const.Style.Edge.Bottom);
395
+ const innerX = x + borderLeft;
396
+ const innerY = y + borderTop;
397
+ const innerWidth = Math.max(0, width - borderLeft - borderRight);
398
+ const innerHeight = Math.max(0, height - borderTop - borderBottom);
399
+ const outerRadii = canvas_helper.parseBorderRadius(this.props.borderRadius);
400
+ const innerRadii = {
401
+ TopLeft: Math.max(0, outerRadii.TopLeft - Math.max(borderLeft, borderTop)),
402
+ TopRight: Math.max(0, outerRadii.TopRight - Math.max(borderRight, borderTop)),
403
+ BottomRight: Math.max(0, outerRadii.BottomRight - Math.max(borderRight, borderBottom)),
404
+ BottomLeft: Math.max(0, outerRadii.BottomLeft - Math.max(borderLeft, borderBottom)),
405
+ };
406
+ if (innerWidth > 0 && innerHeight > 0) {
407
+ canvas_helper.drawRoundedRectPath(ctx, innerX, innerY, innerWidth, innerHeight, innerRadii);
408
+ ctx.clip();
409
+ }
410
+ else {
411
+ ctx.beginPath();
412
+ ctx.rect(innerX, innerY, 0, 0);
413
+ ctx.clip();
414
+ }
415
+ }
416
+ // --- End Clipping Setup ---
417
+ // --- Step 4: Render Children in Stacking Order ---
418
+ // 4a: Render positioned children with negative zIndex
419
+ for (const item of positionedChildren) {
420
+ if (item.zIndex < 0) {
421
+ // Pass parent's layout origin (x, y) as offset
422
+ item.node.render(ctx, x, y);
423
+ }
424
+ }
425
+ // 4b: Render in-flow children (recursively)
426
+ for (const child of inFlowChildren) {
427
+ // Pass parent's layout origin (x, y) as offset
428
+ child.render(ctx, x, y);
429
+ }
430
+ // 4c: Render positioned children with zero or positive zIndex
431
+ for (const item of positionedChildren) {
432
+ if (item.zIndex >= 0) {
433
+ // Pass parent's layout origin (x, y) as offset
434
+ item.node.render(ctx, x, y);
435
+ }
436
+ }
437
+ // --- End Child Rendering ---
438
+ // --- Step 5: Restore Clipping Context ---
439
+ if (savedContextForClip) {
440
+ ctx.restore();
441
+ }
442
+ // --- End Clipping Restoration ---
443
+ // --- Step 6: Restore Transformation Context ---
444
+ if (savedContextForTransform) {
445
+ ctx.restore();
446
+ }
447
+ // --- End Transformation Restoration ---
448
+ }
449
+ finally {
450
+ // --- Opacity Restoration ---
451
+ if (appliedOpacity && originalAlpha !== undefined) {
452
+ ctx.globalAlpha = originalAlpha;
453
+ }
454
+ // --- End Opacity Restoration ---
455
+ }
456
+ }
457
+ /**
458
+ * Renders the node's visual content including background fills, shadows, and borders.
459
+ * This is an internal method used by the render() pipeline.
460
+ *
461
+ * @param ctx - The skia-canvas 2D rendering context to draw into
462
+ * @param x - The absolute x-coordinate where drawing should begin
463
+ * @param y - The absolute y-coordinate where drawing should begin
464
+ * @param width - The width of the content area to render
465
+ * @param height - The height of the content area to render
466
+ */
467
+ _renderContent(ctx, x, y, width, height) {
468
+ // Calculate border radius values for all corners
469
+ const radii = { TopLeft: 0, TopRight: 0, BottomRight: 0, BottomLeft: 0 };
470
+ if (this.props.borderRadius) {
471
+ // Handle both number and object border radius specifications
472
+ if (typeof this.props.borderRadius === 'number') {
473
+ radii.TopLeft = radii.TopRight = radii.BottomRight = radii.BottomLeft = this.props.borderRadius;
474
+ }
475
+ else {
476
+ // Extract individual corner radii, defaulting to 0 if not specified
477
+ radii.TopLeft = this.props.borderRadius.TopLeft ?? 0;
478
+ radii.TopRight = this.props.borderRadius.TopRight ?? 0;
479
+ radii.BottomRight = this.props.borderRadius.BottomRight ?? 0;
480
+ radii.BottomLeft = this.props.borderRadius.BottomLeft ?? 0;
481
+ }
482
+ // Ensure all radii are non-negative
483
+ radii.TopLeft = Math.max(0, radii.TopLeft);
484
+ radii.TopRight = Math.max(0, radii.TopRight);
485
+ radii.BottomRight = Math.max(0, radii.BottomRight);
486
+ radii.BottomLeft = Math.max(0, radii.BottomLeft);
487
+ }
488
+ // Process shadow configurations
489
+ let shadows = [];
490
+ if (this.props.boxShadow) {
491
+ shadows = Array.isArray(this.props.boxShadow) ? this.props.boxShadow : [this.props.boxShadow];
492
+ }
493
+ // Split shadows into outset (normal) and inset types
494
+ const outsetShadows = shadows.filter(s => !s.inset);
495
+ const insetShadows = shadows.filter(s => s.inset);
496
+ // Determine if background is fully opaque for shadow optimization
497
+ const backgroundColor = this.props.backgroundColor;
498
+ let isOpaque = false;
499
+ if (backgroundColor && !this.props.gradient) {
500
+ const rgba = tinycolor(backgroundColor).toRgb();
501
+ isOpaque = rgba && rgba.a === 1;
502
+ }
503
+ // Render outset shadows if present
504
+ if (outsetShadows.length > 0) {
505
+ const subtractOffset = 0.75;
506
+ if (isOpaque) {
507
+ // Optimized rendering path for opaque backgrounds
508
+ ctx.save();
509
+ ctx.fillStyle = 'black'; // Shadow source color
510
+ for (const shadow of outsetShadows) {
511
+ ctx.shadowColor = shadow.color ?? 'black';
512
+ ctx.shadowOffsetX = shadow.offsetX ?? 0;
513
+ ctx.shadowOffsetY = shadow.offsetY ?? 0;
514
+ ctx.shadowBlur = shadow.blur ?? Math.max(shadow.offsetX ?? 0, shadow.offsetY ?? 0);
515
+ canvas_helper.drawRoundedRectPath(ctx, x + subtractOffset / 2, y + subtractOffset / 2, width - subtractOffset, height - subtractOffset, radii);
516
+ ctx.fill();
517
+ }
518
+ ctx.restore();
519
+ }
520
+ else {
521
+ // Complex shadow rendering for transparent/gradient backgrounds
522
+ let maxBlur = 0;
523
+ let maxOffsetX = 0;
524
+ let maxOffsetY = 0;
525
+ // Calculate maximum shadow extents
526
+ for (const shadow of outsetShadows) {
527
+ const currentOffsetX = shadow.offsetX ?? 0;
528
+ const currentOffsetY = shadow.offsetY ?? 0;
529
+ const currentBlur = shadow.blur ?? Math.max(currentOffsetX, currentOffsetY);
530
+ maxBlur = Math.max(maxBlur, currentBlur);
531
+ maxOffsetX = Math.max(maxOffsetX, Math.abs(currentOffsetX));
532
+ maxOffsetY = Math.max(maxOffsetY, Math.abs(currentOffsetY));
533
+ }
534
+ // Calculate offscreen canvas size with padding for shadows
535
+ const blurPaddingMultiplier = 2;
536
+ const shadowPadding = Math.ceil(maxBlur * blurPaddingMultiplier + Math.max(maxOffsetX, maxOffsetY));
537
+ const offscreenWidth = Math.ceil(width + shadowPadding * 2);
538
+ const offscreenHeight = Math.ceil(height + shadowPadding * 2);
539
+ if (offscreenWidth > 0 && offscreenHeight > 0) {
540
+ // Create temporary canvas for shadow composition
541
+ const offscreenCanvas = new skiaCanvas.Canvas(offscreenWidth, offscreenHeight);
542
+ const offCtx = offscreenCanvas.getContext('2d');
543
+ offCtx.imageSmoothingEnabled = true;
544
+ offCtx.imageSmoothingQuality = 'high';
545
+ const shapeOffsetX = shadowPadding;
546
+ const shapeOffsetY = shadowPadding;
547
+ // Render each shadow individually onto offscreen canvas
548
+ for (const shadow of outsetShadows) {
549
+ offCtx.save();
550
+ const shadowOffsetX = shadow.offsetX ?? 0;
551
+ const shadowOffsetY = shadow.offsetY ?? 0;
552
+ const blur = shadow.blur ?? Math.max(shadowOffsetX, shadowOffsetY);
553
+ offCtx.shadowColor = shadow.color ?? 'black';
554
+ offCtx.shadowOffsetX = shadowOffsetX;
555
+ offCtx.shadowOffsetY = shadowOffsetY;
556
+ offCtx.shadowBlur = Math.max(0, blur);
557
+ canvas_helper.drawRoundedRectPath(offCtx, shapeOffsetX + subtractOffset / 2, shapeOffsetY + subtractOffset / 2, width - subtractOffset, height - subtractOffset, radii);
558
+ offCtx.fillStyle = 'rgba(0,0,0,1)';
559
+ offCtx.fill();
560
+ offCtx.restore();
561
+ }
562
+ // Cut out the shape from accumulated shadows
563
+ offCtx.save();
564
+ offCtx.globalCompositeOperation = 'destination-out';
565
+ canvas_helper.drawRoundedRectPath(offCtx, shapeOffsetX, shapeOffsetY, width, height, radii);
566
+ offCtx.fillStyle = 'rgba(0,0,0,1)';
567
+ offCtx.fill();
568
+ offCtx.restore();
569
+ // Composite shadow result onto main canvas
570
+ ctx.drawImage(offscreenCanvas, x - shadowPadding, y - shadowPadding);
571
+ }
572
+ }
573
+ }
574
+ // Render background fill (solid color or gradient)
575
+ // This logic uses standard context methods and remains unchanged.
576
+ const hasFill = this.props.gradient || this.props.backgroundColor;
577
+ if (hasFill) {
578
+ let fillStyle = this.props.backgroundColor || 'transparent';
579
+ if (this.props.gradient) {
580
+ const { type = 'linear', colors, direction = 'to-bottom' } = this.props.gradient;
581
+ let grad = null;
582
+ if (colors && colors.length > 0 && width > 0 && height > 0) {
583
+ if (type === 'linear') {
584
+ let x0 = 0, y0 = 0, x1 = 0, y1 = 0;
585
+ let directionIsValid = false;
586
+ if (Array.isArray(direction) && direction.length === 4) {
587
+ [x0, y0, x1, y1] = direction;
588
+ directionIsValid = true;
589
+ }
590
+ else if (typeof direction === 'string') {
591
+ switch (direction.toLowerCase()) {
592
+ case 'to-right':
593
+ x0 = 0;
594
+ y0 = 0;
595
+ x1 = width;
596
+ y1 = 0;
597
+ directionIsValid = true;
598
+ break;
599
+ case 'to-left':
600
+ x0 = width;
601
+ y0 = 0;
602
+ x1 = 0;
603
+ y1 = 0;
604
+ directionIsValid = true;
605
+ break;
606
+ case 'to-bottom':
607
+ x0 = 0;
608
+ y0 = 0;
609
+ x1 = 0;
610
+ y1 = height;
611
+ directionIsValid = true;
612
+ break;
613
+ case 'to-top':
614
+ x0 = 0;
615
+ y0 = height;
616
+ x1 = 0;
617
+ y1 = 0;
618
+ directionIsValid = true;
619
+ break;
620
+ case 'to-top-right':
621
+ x0 = 0;
622
+ y0 = height;
623
+ x1 = width;
624
+ y1 = 0;
625
+ directionIsValid = true;
626
+ break;
627
+ case 'to-top-left':
628
+ x0 = width;
629
+ y0 = height;
630
+ x1 = 0;
631
+ y1 = 0;
632
+ directionIsValid = true;
633
+ break;
634
+ case 'to-bottom-right':
635
+ x0 = 0;
636
+ y0 = 0;
637
+ x1 = width;
638
+ y1 = height;
639
+ directionIsValid = true;
640
+ break;
641
+ case 'to-bottom-left':
642
+ x0 = width;
643
+ y0 = 0;
644
+ x1 = 0;
645
+ y1 = height;
646
+ directionIsValid = true;
647
+ break;
648
+ }
649
+ }
650
+ if (directionIsValid) {
651
+ grad = ctx.createLinearGradient(x + x0, y + y0, x + x1, y + y1);
652
+ }
653
+ else {
654
+ console.warn(`[BoxNode ${this.key}] Invalid linear gradient direction:`, direction);
655
+ }
656
+ }
657
+ else if (type === 'radial') {
658
+ const centerX = x + width / 2;
659
+ const centerY = y + height / 2;
660
+ const r0 = 0;
661
+ const r1 = 0.5 * Math.sqrt(width * width + height * height);
662
+ if (r1 > 0) {
663
+ grad = ctx.createRadialGradient(centerX, centerY, r0, centerX, centerY, r1);
664
+ }
665
+ }
666
+ if (grad) {
667
+ colors.forEach((color, i) => {
668
+ const stop = colors.length > 1 ? Math.max(0, Math.min(1, i / (colors.length - 1))) : 0.5;
669
+ grad.addColorStop(stop, color);
670
+ });
671
+ fillStyle = grad;
672
+ }
673
+ else {
674
+ console.warn(`[BoxNode ${this.key}] Could not create ${type} gradient. Falling back to backgroundColor.`);
675
+ }
676
+ }
677
+ else {
678
+ if (!colors?.length) {
679
+ console.warn(`[BoxNode ${this.key}] Gradient specified but no colors provided. Falling back to backgroundColor.`);
680
+ }
681
+ else {
682
+ console.warn(`[BoxNode ${this.key}] Cannot draw gradient with zero width/height.`);
683
+ }
684
+ }
685
+ }
686
+ if (fillStyle && fillStyle !== 'transparent') {
687
+ ctx.fillStyle = fillStyle;
688
+ canvas_helper.drawRoundedRectPath(ctx, x, y, width, height, radii);
689
+ ctx.fill();
690
+ }
691
+ }
692
+ // Render inset shadows
693
+ // This logic uses standard context methods and remains unchanged.
694
+ if (insetShadows.length > 0) {
695
+ for (const shadow of insetShadows) {
696
+ ctx.save();
697
+ const color = shadow.color ?? 'black';
698
+ const shadowOffsetX = shadow.offsetX ?? 0;
699
+ const shadowOffsetY = shadow.offsetY ?? 0;
700
+ const blur = shadow.blur ?? Math.max(shadowOffsetX, shadowOffsetY);
701
+ canvas_helper.drawRoundedRectPath(ctx, x, y, width, height, radii);
702
+ ctx.clip();
703
+ ctx.shadowColor = color;
704
+ ctx.shadowOffsetX = shadowOffsetX;
705
+ ctx.shadowOffsetY = shadowOffsetY;
706
+ ctx.shadowBlur = blur;
707
+ ctx.lineWidth = 1; // Minimal line width for the stroke.
708
+ ctx.strokeStyle = 'transparent'; // Stroke color doesn't matter; only the shadow does.
709
+ // Draw a slightly offset path *inside* the clip to generate the inset shadow.
710
+ canvas_helper.drawRoundedRectPath(ctx, x - shadowOffsetX, y - shadowOffsetY, width, height, radii);
711
+ ctx.stroke(); // The stroke generates the shadow inside the clipped area.
712
+ ctx.restore();
713
+ }
714
+ }
715
+ // Render border strokes
716
+ // (This logic uses standard context methods via drawBorders helper and remains unchanged)
717
+ canvas_helper.drawBorders({
718
+ ctx,
719
+ node: this.node,
720
+ x,
721
+ y,
722
+ width,
723
+ height,
724
+ radii,
725
+ borderColor: this.props.borderColor,
726
+ borderStyle: this.props.borderStyle,
727
+ });
728
+ }
729
+ }
730
+ /**
731
+ * Creates a new BoxNode instance.
732
+ * @param {BoxProps} props - Box properties and configuration.
733
+ * @returns {BoxNode} New BoxNode instance.
734
+ */
735
+ const Box = (props) => new BoxNode(props);
736
+ /**
737
+ * @class ColumnNode
738
+ * Node class for vertical column layout
739
+ */
740
+ class ColumnNode extends BoxNode {
741
+ constructor(props = {}) {
742
+ super({
743
+ display: common_const.Style.Display.Flex,
744
+ flexDirection: common_const.Style.FlexDirection.Column,
745
+ flexShrink: 1,
746
+ flexBasis: props.flexGrow === 1 ? 0 : undefined,
747
+ ...props,
748
+ });
749
+ }
750
+ }
751
+ /**
752
+ * Creates a new ColumnNode instance.
753
+ * @param {BoxProps} props - Column properties and configuration.
754
+ * @returns {ColumnNode} New ColumnNode instance.
755
+ */
756
+ const Column = (props) => new ColumnNode(props);
757
+ /**
758
+ * @class RowNode
759
+ * @classdesc Node class for horizontal row layout.
760
+ */
761
+ class RowNode extends BoxNode {
762
+ constructor(props = {}) {
763
+ super({
764
+ name: 'Row',
765
+ display: common_const.Style.Display.Flex,
766
+ flexDirection: common_const.Style.FlexDirection.Row,
767
+ flexShrink: 1, // Default shrink for rows
768
+ ...props,
769
+ });
770
+ }
771
+ }
772
+ /**
773
+ * Creates a new RowNode instance.
774
+ * @param {BoxProps} props - Row properties and configuration.
775
+ * @returns {RowNode} New RowNode instance.
776
+ */
777
+ const Row = (props) => new RowNode(props);
778
+
779
+ exports.Box = Box;
780
+ exports.BoxNode = BoxNode;
781
+ exports.Column = Column;
782
+ exports.ColumnNode = ColumnNode;
783
+ exports.Row = Row;
784
+ exports.RowNode = RowNode;
785
+ //# sourceMappingURL=layout.canvas.util.js.map