@meonode/canvas 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,7 @@
1
1
  # @meonode/canvas
2
2
 
3
- A declarative, component-based library for server-side canvas image generation. Write complex visuals with simple functions, similar to the composition style of @meonode/ui.
3
+ A declarative, component-based library for server-side canvas image generation. Write complex visuals with simple
4
+ functions, similar to the composition style of @meonode/ui.
4
5
  It uses `skia-canvas` for drawing and `yoga-layout` for flexbox-based layouts.
5
6
 
6
7
  This library allows you to build complex image layouts using a familiar component-based approach. You can define your
@@ -23,15 +24,16 @@ rendering to a canvas.
23
24
 
24
25
  <table>
25
26
  <tr>
26
- <td><img src="https://i.ibb.co/VpPZybzF/profile-card.webp" alt="Image 1"></td>
27
- <td><img src="https://i.ibb.co/zTgrBWpT/profile-card-1.webp" alt="Image 2"></td>
27
+ <td width="50%"><img src="https://i.ibb.co/VpPZybzF/profile-card.webp" alt="Image 1"></td>
28
+ <td width="50%"><img src="https://i.ibb.co/zTgrBWpT/profile-card-1.webp" alt="Image 2"></td>
28
29
  </tr>
29
30
  <tr>
30
- <td><img src="https://i.ibb.co/F4xfHdBp/daily-notes.webp" alt="Image 3"></td>
31
- <td><img src="https://i.ibb.co/Jj0x6khB/character-archive-base.webp" alt="Image 4"></td>
31
+ <td width="50%"><img src="https://i.ibb.co/F4xfHdBp/daily-notes.webp" alt="Image 3"></td>
32
+ <td width="50%"><img src="https://i.ibb.co/Jj0x6khB/character-archive-base.webp" alt="Image 4"></td>
32
33
  </tr>
33
34
  <tr>
34
- <td colspan="2"><img src="https://i.ibb.co.com/B24SZ0C9/charts.webp" alt="Image 5"/></td>
35
+ <td width="50%"><img src="https://i.ibb.co.com/B24SZ0C9/charts.webp" alt="Image 5"/></td>
36
+ <td width="50%"><img src="https://i.ibb.co.com/q3sy9r2L/sample-grids.png" alt="Image 6"/></td>
35
37
  </tr>
36
38
  </table>
37
39
 
@@ -50,41 +52,41 @@ import {Root, Box, Text} from '@meonode/canvas';
50
52
  import {writeFile} from 'fs/promises';
51
53
 
52
54
  async function generateImage() {
53
- const canvas = await Root({
54
- width: 500,
55
- height: 300,
56
- fonts: [
57
- {
58
- family: 'Roboto',
59
- paths: ['./fonts/Roboto-Regular.ttf', './fonts/Roboto-Bold.ttf'],
60
- },
61
- ],
62
- children: [
63
- Box({
64
- width: '100%',
65
- height: '100%',
66
- backgroundColor: '#f0f0f0',
67
- padding: 20,
55
+ const canvas = await Root({
56
+ width: 500,
57
+ height: 300,
58
+ fonts: [
59
+ {
60
+ family: 'Roboto',
61
+ paths: ['./fonts/Roboto-Regular.ttf', './fonts/Roboto-Bold.ttf'],
62
+ },
63
+ ],
68
64
  children: [
69
- Text('Hello, World!', {
70
- fontSize: 32,
71
- fontWeight: 'bold',
72
- fontFamily: 'Roboto',
73
- color: '#333',
74
- }),
75
- Text('This is a basic example of using @meonode/canvas.', {
76
- fontSize: 18,
77
- fontFamily: 'Roboto',
78
- color: '#666',
79
- margin: {Top: 10},
80
- }),
65
+ Box({
66
+ width: '100%',
67
+ height: '100%',
68
+ backgroundColor: '#f0f0f0',
69
+ padding: 20,
70
+ children: [
71
+ Text('Hello, World!', {
72
+ fontSize: 32,
73
+ fontWeight: 'bold',
74
+ fontFamily: 'Roboto',
75
+ color: '#333',
76
+ }),
77
+ Text('This is a basic example of using @meonode/canvas.', {
78
+ fontSize: 18,
79
+ fontFamily: 'Roboto',
80
+ color: '#666',
81
+ margin: {Top: 10},
82
+ }),
83
+ ],
84
+ }),
81
85
  ],
82
- }),
83
- ],
84
- });
86
+ });
85
87
 
86
- const buffer = await canvas.toBuffer('png');
87
- await writeFile('output.png', buffer);
88
+ const buffer = await canvas.toBuffer('png');
89
+ await writeFile('output.png', buffer);
88
90
  }
89
91
 
90
92
  generateImage().catch(console.error);
@@ -100,109 +102,109 @@ import {Root, Column, Row, Text, Image, Style} from '@meonode/canvas';
100
102
  import {writeFile} from 'fs/promises';
101
103
 
102
104
  async function generateComplexImage() {
103
- const canvas = await Root({
104
- width: 800,
105
- height: 600,
106
- fonts: [
107
- {
108
- family: 'Roboto',
109
- paths: ['./fonts/Roboto-Regular.ttf', './fonts/Roboto-Bold.ttf'],
110
- },
111
- {
112
- family: 'Open Sans',
113
- paths: ['./fonts/OpenSans-Regular.ttf'],
114
- },
115
- ],
116
- children: [
117
- Column({
118
- width: '100%',
119
- height: '100%',
120
- backgroundColor: '#f0f0f0',
121
- padding: 20,
122
- justifyContent: Style.Justify.SpaceBetween,
105
+ const canvas = await Root({
106
+ width: 800,
107
+ height: 600,
108
+ fonts: [
109
+ {
110
+ family: 'Roboto',
111
+ paths: ['./fonts/Roboto-Regular.ttf', './fonts/Roboto-Bold.ttf'],
112
+ },
113
+ {
114
+ family: 'Open Sans',
115
+ paths: ['./fonts/OpenSans-Regular.ttf'],
116
+ },
117
+ ],
123
118
  children: [
124
- // Header Section
125
- Row({
126
- width: '100%',
127
- alignItems: Style.Align.Center,
128
- marginBottom: 20,
129
- children: [
130
- Image({
131
- src: 'https://via.placeholder.com/80x80/FF0000/FFFFFF?text=Logo',
132
- width: 80,
133
- height: 80,
134
- borderRadius: 40,
135
- marginRight: 20,
136
- objectFit: 'cover',
137
- }),
138
- Text('Welcome to MeoNode Canvas!', {
139
- fontSize: 40,
140
- fontWeight: 'bold',
141
- fontFamily: 'Roboto',
142
- color: '#333',
143
- }),
144
- ],
145
- }),
146
-
147
- // Main Content Section
148
- Column({
149
- flexGrow: 1,
150
- width: '100%',
151
- backgroundColor: '#ffffff',
152
- borderRadius: 10,
153
- padding: 30,
154
- boxShadow: {blur: 10, color: 'rgba(0,0,0,0.1)'},
155
- children: [
156
- Text('A New Way to Render Graphics', {
157
- fontSize: 28,
158
- fontWeight: 'bold',
159
- fontFamily: 'Open Sans',
160
- color: '#555',
161
- marginBottom: 15,
162
- }),
163
- Text(
164
- `This example demonstrates a more complex layout using various components.
119
+ Column({
120
+ width: '100%',
121
+ height: '100%',
122
+ backgroundColor: '#f0f0f0',
123
+ padding: 20,
124
+ justifyContent: Style.Justify.SpaceBetween,
125
+ children: [
126
+ // Header Section
127
+ Row({
128
+ width: '100%',
129
+ alignItems: Style.Align.Center,
130
+ marginBottom: 20,
131
+ children: [
132
+ Image({
133
+ src: 'https://via.placeholder.com/80x80/FF0000/FFFFFF?text=Logo',
134
+ width: 80,
135
+ height: 80,
136
+ borderRadius: 40,
137
+ marginRight: 20,
138
+ objectFit: 'cover',
139
+ }),
140
+ Text('Welcome to MeoNode Canvas!', {
141
+ fontSize: 40,
142
+ fontWeight: 'bold',
143
+ fontFamily: 'Roboto',
144
+ color: '#333',
145
+ }),
146
+ ],
147
+ }),
148
+
149
+ // Main Content Section
150
+ Column({
151
+ flexGrow: 1,
152
+ width: '100%',
153
+ backgroundColor: '#ffffff',
154
+ borderRadius: 10,
155
+ padding: 30,
156
+ boxShadow: {blur: 10, color: 'rgba(0,0,0,0.1)'},
157
+ children: [
158
+ Text('A New Way to Render Graphics', {
159
+ fontSize: 28,
160
+ fontWeight: 'bold',
161
+ fontFamily: 'Open Sans',
162
+ color: '#555',
163
+ marginBottom: 15,
164
+ }),
165
+ Text(
166
+ `This example demonstrates a more complex layout using various components.
165
167
  We have a header with a logo and title, a main content area with text,
166
168
  and a footer. Notice how flexbox properties are used to arrange elements.`,
167
- {
168
- fontSize: 18,
169
- fontFamily: 'Open Sans',
170
- color: '#777',
171
- lineHeight: 24,
172
- },
173
- ),
174
- Image({
175
- src: 'https://via.placeholder.com/600x200/007bff/ffffff?text=Feature+Image',
176
- width: '100%',
177
- height: 200,
178
- marginTop: 20,
179
- borderRadius: 8,
180
- objectFit: 'contain',
181
- objectPosition: {Top: '50%', Left: '50%'},
182
- }),
183
- ],
184
- }),
185
-
186
- // Footer Section
187
- Row({
188
- width: '100%',
189
- marginTop: 20,
190
- justifyContent: Style.Justify.Center,
191
- children: [
192
- Text('© 2025 MeoNode Canvas. All rights reserved.', {
193
- fontSize: 14,
194
- fontFamily: 'Open Sans',
195
- color: '#999',
196
- }),
197
- ],
198
- }),
169
+ {
170
+ fontSize: 18,
171
+ fontFamily: 'Open Sans',
172
+ color: '#777',
173
+ lineHeight: 24,
174
+ },
175
+ ),
176
+ Image({
177
+ src: 'https://via.placeholder.com/600x200/007bff/ffffff?text=Feature+Image',
178
+ width: '100%',
179
+ height: 200,
180
+ marginTop: 20,
181
+ borderRadius: 8,
182
+ objectFit: 'contain',
183
+ objectPosition: {Top: '50%', Left: '50%'},
184
+ }),
185
+ ],
186
+ }),
187
+
188
+ // Footer Section
189
+ Row({
190
+ width: '100%',
191
+ marginTop: 20,
192
+ justifyContent: Style.Justify.Center,
193
+ children: [
194
+ Text('© 2025 MeoNode Canvas. All rights reserved.', {
195
+ fontSize: 14,
196
+ fontFamily: 'Open Sans',
197
+ color: '#999',
198
+ }),
199
+ ],
200
+ }),
201
+ ],
202
+ }),
199
203
  ],
200
- }),
201
- ],
202
- });
204
+ });
203
205
 
204
- const buffer = await canvas.toBuffer('png');
205
- await writeFile('complex_output.png', buffer);
206
+ const buffer = await canvas.toBuffer('png');
207
+ await writeFile('complex_output.png', buffer);
206
208
  }
207
209
 
208
210
  generateComplexImage().catch(console.error);
@@ -219,39 +221,39 @@ import {Root, Chart} from '@meonode/canvas';
219
221
  import {writeFile} from 'fs/promises';
220
222
 
221
223
  async function generateBarChart() {
222
- const canvas = await Root({
223
- width: 600,
224
- height: 400,
225
- children: [
226
- Chart({
227
- type: 'bar',
228
- width: '100%',
229
- height: '100%',
230
- data: {
231
- labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May'],
232
- datasets: [
233
- {
234
- label: 'Sales',
235
- data: [120, 150, 180, 90, 200],
236
- color: '#36A2EB',
237
- },
238
- ],
239
- },
240
- options: {
241
- gridOptions: {show: true, style: 'dashed'},
242
- axisColor: '#333',
243
- labelColor: '#333',
244
- showValues: true,
245
- valueFontSize: 12,
246
- showYAxis: true,
247
- yAxisColor: '#666',
248
- },
249
- }),
250
- ],
251
- });
224
+ const canvas = await Root({
225
+ width: 600,
226
+ height: 400,
227
+ children: [
228
+ Chart({
229
+ type: 'bar',
230
+ width: '100%',
231
+ height: '100%',
232
+ data: {
233
+ labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May'],
234
+ datasets: [
235
+ {
236
+ label: 'Sales',
237
+ data: [120, 150, 180, 90, 200],
238
+ color: '#36A2EB',
239
+ },
240
+ ],
241
+ },
242
+ options: {
243
+ gridOptions: {show: true, style: 'dashed'},
244
+ axisColor: '#333',
245
+ labelColor: '#333',
246
+ showValues: true,
247
+ valueFontSize: 12,
248
+ showYAxis: true,
249
+ yAxisColor: '#666',
250
+ },
251
+ }),
252
+ ],
253
+ });
252
254
 
253
- const buffer = await canvas.toBuffer('png');
254
- await writeFile('bar_chart.png', buffer);
255
+ const buffer = await canvas.toBuffer('png');
256
+ await writeFile('bar_chart.png', buffer);
255
257
  }
256
258
 
257
259
  generateBarChart().catch(console.error);
@@ -264,42 +266,128 @@ import {Root, Chart, Row, Box, Text} from '@meonode/canvas';
264
266
  import {writeFile} from 'fs/promises';
265
267
 
266
268
  async function generateDoughnutChart() {
267
- const canvas = await Root({
268
- width: 600,
269
- height: 400,
270
- children: [
271
- Chart({
272
- type: 'doughnut',
273
- width: '100%',
274
- height: '100%',
275
- data: [
276
- {label: 'Red', value: 300, color: '#FF6384'},
277
- {label: 'Blue', value: 50, color: '#36A2EB'},
278
- {label: 'Yellow', value: 100, color: '#FFCE56'},
279
- ],
280
- options: {
281
- innerRadius: 0.7,
282
- sliceBorderRadius: 5,
283
- renderLegendItem: ({item, color}) =>
284
- Row({
285
- alignItems: 'center',
286
- children: [
287
- Box({width: 12, height: 12, backgroundColor: color, borderRadius: 6}),
288
- Text(`${item.label}: ${item.value}`, {fontSize: 16, marginLeft: 8}),
289
- ],
269
+ const canvas = await Root({
270
+ width: 600,
271
+ height: 400,
272
+ children: [
273
+ Chart({
274
+ type: 'doughnut',
275
+ width: '100%',
276
+ height: '100%',
277
+ data: [
278
+ {label: 'Red', value: 300, color: '#FF6384'},
279
+ {label: 'Blue', value: 50, color: '#36A2EB'},
280
+ {label: 'Yellow', value: 100, color: '#FFCE56'},
281
+ ],
282
+ options: {
283
+ innerRadius: 0.7,
284
+ sliceBorderRadius: 5,
285
+ renderLegendItem: ({item, color}) =>
286
+ Row({
287
+ alignItems: 'center',
288
+ children: [
289
+ Box({width: 12, height: 12, backgroundColor: color, borderRadius: 6}),
290
+ Text(`${item.label}: ${item.value}`, {fontSize: 16, marginLeft: 8}),
291
+ ],
292
+ }),
293
+ },
290
294
  }),
291
- },
292
- }),
293
- ],
294
- });
295
+ ],
296
+ });
295
297
 
296
- const buffer = await canvas.toBuffer('png');
297
- await writeFile('doughnut_chart.png', buffer);
298
+ const buffer = await canvas.toBuffer('png');
299
+ await writeFile('doughnut_chart.png', buffer);
298
300
  }
299
301
 
300
302
  generateDoughnutChart().catch(console.error);
301
303
  ```
302
304
 
305
+ ## Grid Layout Examples
306
+
307
+ The `Grid` component simplifies creating complex layouts. It mimics CSS Grid Layout.
308
+
309
+ ### Basic Grid
310
+
311
+ A simple grid with 3 columns, each 100 pixels wide.
312
+
313
+ ```typescript
314
+ import {Root, Grid, Box, Text} from '@meonode/canvas';
315
+ import {writeFile} from 'fs/promises';
316
+
317
+ async function generateBasicGrid() {
318
+ const canvas = await Root({
319
+ width: 400,
320
+ height: 300,
321
+ children: [
322
+ Grid({
323
+ columns: 3,
324
+ templateColumns: [100, 100, 100], // or ['100px', '100px', '100px']
325
+ gap: 10,
326
+ children: [
327
+ Box({backgroundColor: 'red', height: 50, children: [Text('1')]}),
328
+ Box({backgroundColor: 'blue', height: 50, children: [Text('2')]}),
329
+ Box({backgroundColor: 'green', height: 50, children: [Text('3')]}),
330
+ Box({backgroundColor: 'yellow', height: 50, children: [Text('4')]}),
331
+ ],
332
+ }),
333
+ ],
334
+ });
335
+
336
+ await canvas.toFile('grid_basic.png');
337
+ }
338
+
339
+ generateBasicGrid();
340
+ ```
341
+
342
+ ### Responsive Grid (Fractional Units)
343
+
344
+ Using fractional units (`fr`) allows columns to take up proportional space.
345
+
346
+ ```typescript
347
+ Grid({
348
+ // First column takes 1 part, second takes 2 parts, third takes 1 part
349
+ templateColumns: ['1fr', '2fr', '1fr'],
350
+ gap: 10,
351
+ children: [
352
+ Box({backgroundColor: 'red', height: 50, children: [Text('1fr')]}),
353
+ Box({backgroundColor: 'blue', height: 50, children: [Text('2fr')]}),
354
+ Box({backgroundColor: 'green', height: 50, children: [Text('1fr')]}),
355
+ ],
356
+ });
357
+ ```
358
+
359
+ ### Spanning Items
360
+
361
+ Use `GridItem` (or just passing `gridColumn`/`gridRow` props to any child) to span multiple columns or rows.
362
+
363
+ ```typescript
364
+ import {Grid, GridItem, Box, Text} from '@meonode/canvas';
365
+
366
+ Grid({
367
+ templateColumns: ['1fr', '1fr', '1fr'],
368
+ gap: 10,
369
+ children: [
370
+ // Spans all 3 columns
371
+ GridItem({
372
+ gridColumn: 'span 3',
373
+ height: 50,
374
+ backgroundColor: '#333',
375
+ children: [Text('Header', {color: 'white'})],
376
+ }),
377
+ // Standard items
378
+ Box({backgroundColor: '#eee', height: 100, children: [Text('Content')]}),
379
+ Box({backgroundColor: '#ccc', height: 100, children: [Text('Sidebar')]}),
380
+ // Spans 2 columns
381
+ GridItem({
382
+ gridColumn: 'span 2',
383
+ height: 50,
384
+ backgroundColor: '#555',
385
+ children: [Text('Footer', {color: 'white'})],
386
+ }),
387
+ ],
388
+ });
389
+ ```
390
+
303
391
  ## Using Yoga Layout Properties
304
392
 
305
393
  This library leverages `yoga-layout` for its powerful flexbox engine. Many layout properties directly map to Yoga's
@@ -311,19 +399,19 @@ For example, to set `flexDirection` to `row` or `positionType` to `absolute`, yo
311
399
  import {Box, Style} from '@meonode/canvas';
312
400
 
313
401
  Box({
314
- flexDirection: Style.FlexDirection.Row,
315
- justifyContent: Style.Justify.Center,
316
- alignItems: Style.Align.Center,
317
- children: [
318
- Box({
319
- width: 100,
320
- height: 100,
321
- backgroundColor: 'red',
322
- positionType: Style.PositionType.Absolute,
323
- position: {Top: 10, Left: 10},
324
- }),
325
- // ... other children
326
- ],
402
+ flexDirection: Style.FlexDirection.Row,
403
+ justifyContent: Style.Justify.Center,
404
+ alignItems: Style.Align.Center,
405
+ children: [
406
+ Box({
407
+ width: 100,
408
+ height: 100,
409
+ backgroundColor: 'red',
410
+ positionType: Style.PositionType.Absolute,
411
+ position: {Top: 10, Left: 10},
412
+ }),
413
+ // ... other children
414
+ ],
327
415
  });
328
416
  ```
329
417
 
@@ -442,10 +530,17 @@ The `Grid` component arranges its children in a grid layout. It is a specialized
442
530
 
443
531
  #### Grid-Specific Props
444
532
 
445
- | Prop | Type | Description |
446
- |-------------|----------------------------------------------------------|-----------------------------------------------------|
447
- | `columns` | `number` | The number of columns in the grid. Default is 1. |
448
- | `direction` | `'row' \| 'column' \| 'row-reverse' \| 'column-reverse'` | The direction of the grid layout. Default is 'row'. |
533
+ | Prop | Type | Description |
534
+ |-------------------|----------------------------------------------------------|---------------------------------------------------------|
535
+ | `columns` | `number` | The number of columns in the grid. Default is 1. |
536
+ | `templateColumns` | `GridTrackSize[]` | Defines the columns of the grid (e.g., `[100, '1fr']`). |
537
+ | `templateRows` | `GridTrackSize[]` | Defines the rows of the grid. |
538
+ | `autoRows` | `GridTrackSize` | Specifies the size of implicitly created rows. |
539
+ | `autoFlow` | `'row' \| 'column' \| 'row-dense' \| 'column-dense'` | Controls how the auto-placement algorithm works. |
540
+ | `gap` | `number \| string` | Defines the gap between grid items. |
541
+ | `rowGap` | `number \| string` | Defines the row gap. |
542
+ | `columnGap` | `number \| string` | Defines the column gap. |
543
+ | `direction` | `'row' \| 'column' \| 'row-reverse' \| 'column-reverse'` | The direction of the grid layout. Default is 'row'. |
449
544
 
450
545
  ---
451
546
 
@@ -479,17 +574,17 @@ The `options` prop is a conditional type that changes based on the chart `type`.
479
574
 
480
575
  ##### Cartesian Chart Options (`bar`, `line`)
481
576
 
482
- | Prop | Type | Description |
483
- |---------------|---------------|-------------------------------------------------------------------|
484
- | `gridOptions` | `GridOptions` | An object to configure the grid lines (`show`, `color`, `style`). |
485
- | `axisColor` | `string` | The color of the chart axes. |
486
- | `showValues` | `boolean` | If `true`, displays values on top of bars or points. |
487
- | `valueColor` | `string` | Color of the value labels. |
488
- | `valueFontSize`| `number` | Font size of the value labels. |
489
- | `showYAxis` | `boolean` | If `true`, displays the Y-axis labels on the left. |
490
- | `yAxisColor` | `string` | Color of the Y-axis labels. |
491
- | `yAxisFontSize`| `number` | Font size of the Y-axis labels. |
492
- | `yAxisLabelFormatter` | `(value: number) => string` | Custom formatter for Y-axis labels. |
577
+ | Prop | Type | Description |
578
+ |-----------------------|-----------------------------|-------------------------------------------------------------------|
579
+ | `gridOptions` | `GridOptions` | An object to configure the grid lines (`show`, `color`, `style`). |
580
+ | `axisColor` | `string` | The color of the chart axes. |
581
+ | `showValues` | `boolean` | If `true`, displays values on top of bars or points. |
582
+ | `valueColor` | `string` | Color of the value labels. |
583
+ | `valueFontSize` | `number` | Font size of the value labels. |
584
+ | `showYAxis` | `boolean` | If `true`, displays the Y-axis labels on the left. |
585
+ | `yAxisColor` | `string` | Color of the Y-axis labels. |
586
+ | `yAxisFontSize` | `number` | Font size of the Y-axis labels. |
587
+ | `yAxisLabelFormatter` | `(value: number) => string` | Custom formatter for Y-axis labels. |
493
588
 
494
589
  ##### Pie & Doughnut Chart Options (`pie`, `doughnut`)
495
590