@meonode/canvas 1.1.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.
@@ -1,7 +1,7 @@
1
1
  # @meonode/canvas
2
2
 
3
- A declarative, component-based library for **server-side** generation of high-quality images on a canvas, inspired by
4
- the MeoNode UI library for React.
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.
5
5
  It uses `skia-canvas` for drawing and `yoga-layout` for flexbox-based layouts.
6
6
 
7
7
  This library allows you to build complex image layouts using a familiar component-based approach. You can define your
@@ -24,15 +24,16 @@ rendering to a canvas.
24
24
 
25
25
  <table>
26
26
  <tr>
27
- <td><img src="https://i.ibb.co/VpPZybzF/profile-card.webp" alt="Image 1"></td>
28
- <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>
29
29
  </tr>
30
30
  <tr>
31
- <td><img src="https://i.ibb.co/F4xfHdBp/daily-notes.webp" alt="Image 3"></td>
32
- <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>
33
33
  </tr>
34
34
  <tr>
35
- <td colspan="2"><img src="https://i.ibb.co.com/JRQ72Gj8/dashboard.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>
36
37
  </tr>
37
38
  </table>
38
39
 
@@ -51,41 +52,41 @@ import {Root, Box, Text} from '@meonode/canvas';
51
52
  import {writeFile} from 'fs/promises';
52
53
 
53
54
  async function generateImage() {
54
- const canvas = await Root({
55
- width: 500,
56
- height: 300,
57
- fonts: [
58
- {
59
- family: 'Roboto',
60
- paths: ['./fonts/Roboto-Regular.ttf', './fonts/Roboto-Bold.ttf'],
61
- },
62
- ],
63
- children: [
64
- Box({
65
- width: '100%',
66
- height: '100%',
67
- backgroundColor: '#f0f0f0',
68
- 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
+ ],
69
64
  children: [
70
- Text('Hello, World!', {
71
- fontSize: 32,
72
- fontWeight: 'bold',
73
- fontFamily: 'Roboto',
74
- color: '#333',
75
- }),
76
- Text('This is a basic example of using @meonode/canvas.', {
77
- fontSize: 18,
78
- fontFamily: 'Roboto',
79
- color: '#666',
80
- margin: {Top: 10},
81
- }),
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
+ }),
82
85
  ],
83
- }),
84
- ],
85
- });
86
+ });
86
87
 
87
- const buffer = await canvas.toBuffer('png');
88
- await writeFile('output.png', buffer);
88
+ const buffer = await canvas.toBuffer('png');
89
+ await writeFile('output.png', buffer);
89
90
  }
90
91
 
91
92
  generateImage().catch(console.error);
@@ -101,109 +102,109 @@ import {Root, Column, Row, Text, Image, Style} from '@meonode/canvas';
101
102
  import {writeFile} from 'fs/promises';
102
103
 
103
104
  async function generateComplexImage() {
104
- const canvas = await Root({
105
- width: 800,
106
- height: 600,
107
- fonts: [
108
- {
109
- family: 'Roboto',
110
- paths: ['./fonts/Roboto-Regular.ttf', './fonts/Roboto-Bold.ttf'],
111
- },
112
- {
113
- family: 'Open Sans',
114
- paths: ['./fonts/OpenSans-Regular.ttf'],
115
- },
116
- ],
117
- children: [
118
- Column({
119
- width: '100%',
120
- height: '100%',
121
- backgroundColor: '#f0f0f0',
122
- padding: 20,
123
- 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
+ ],
124
118
  children: [
125
- // Header Section
126
- Row({
127
- width: '100%',
128
- alignItems: Style.Align.Center,
129
- marginBottom: 20,
130
- children: [
131
- Image({
132
- src: 'https://via.placeholder.com/80x80/FF0000/FFFFFF?text=Logo',
133
- width: 80,
134
- height: 80,
135
- borderRadius: 40,
136
- marginRight: 20,
137
- objectFit: 'cover',
138
- }),
139
- Text('Welcome to MeoNode Canvas!', {
140
- fontSize: 40,
141
- fontWeight: 'bold',
142
- fontFamily: 'Roboto',
143
- color: '#333',
144
- }),
145
- ],
146
- }),
147
-
148
- // Main Content Section
149
- Column({
150
- flexGrow: 1,
151
- width: '100%',
152
- backgroundColor: '#ffffff',
153
- borderRadius: 10,
154
- padding: 30,
155
- boxShadow: {blur: 10, color: 'rgba(0,0,0,0.1)'},
156
- children: [
157
- Text('A New Way to Render Graphics', {
158
- fontSize: 28,
159
- fontWeight: 'bold',
160
- fontFamily: 'Open Sans',
161
- color: '#555',
162
- marginBottom: 15,
163
- }),
164
- Text(
165
- `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.
166
167
  We have a header with a logo and title, a main content area with text,
167
168
  and a footer. Notice how flexbox properties are used to arrange elements.`,
168
- {
169
- fontSize: 18,
170
- fontFamily: 'Open Sans',
171
- color: '#777',
172
- lineHeight: 24,
173
- },
174
- ),
175
- Image({
176
- src: 'https://via.placeholder.com/600x200/007bff/ffffff?text=Feature+Image',
177
- width: '100%',
178
- height: 200,
179
- marginTop: 20,
180
- borderRadius: 8,
181
- objectFit: 'contain',
182
- objectPosition: {Top: '50%', Left: '50%'},
183
- }),
184
- ],
185
- }),
186
-
187
- // Footer Section
188
- Row({
189
- width: '100%',
190
- marginTop: 20,
191
- justifyContent: Style.Justify.Center,
192
- children: [
193
- Text('© 2025 MeoNode Canvas. All rights reserved.', {
194
- fontSize: 14,
195
- fontFamily: 'Open Sans',
196
- color: '#999',
197
- }),
198
- ],
199
- }),
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
+ }),
200
203
  ],
201
- }),
202
- ],
203
- });
204
+ });
204
205
 
205
- const buffer = await canvas.toBuffer('png');
206
- await writeFile('complex_output.png', buffer);
206
+ const buffer = await canvas.toBuffer('png');
207
+ await writeFile('complex_output.png', buffer);
207
208
  }
208
209
 
209
210
  generateComplexImage().catch(console.error);
@@ -220,35 +221,39 @@ import {Root, Chart} from '@meonode/canvas';
220
221
  import {writeFile} from 'fs/promises';
221
222
 
222
223
  async function generateBarChart() {
223
- const canvas = await Root({
224
- width: 600,
225
- height: 400,
226
- children: [
227
- Chart({
228
- type: 'bar',
229
- width: '100%',
230
- height: '100%',
231
- data: {
232
- labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May'],
233
- datasets: [
234
- {
235
- label: 'Sales',
236
- data: [120, 150, 180, 90, 200],
237
- color: '#36A2EB',
238
- },
239
- ],
240
- },
241
- options: {
242
- gridOptions: {show: true, style: 'dashed'},
243
- axisColor: '#333',
244
- labelColor: '#333',
245
- },
246
- }),
247
- ],
248
- });
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
+ });
249
254
 
250
- const buffer = await canvas.toBuffer('png');
251
- await writeFile('bar_chart.png', buffer);
255
+ const buffer = await canvas.toBuffer('png');
256
+ await writeFile('bar_chart.png', buffer);
252
257
  }
253
258
 
254
259
  generateBarChart().catch(console.error);
@@ -261,42 +266,128 @@ import {Root, Chart, Row, Box, Text} from '@meonode/canvas';
261
266
  import {writeFile} from 'fs/promises';
262
267
 
263
268
  async function generateDoughnutChart() {
264
- const canvas = await Root({
265
- width: 600,
266
- height: 400,
267
- children: [
268
- Chart({
269
- type: 'doughnut',
270
- width: '100%',
271
- height: '100%',
272
- data: [
273
- {label: 'Red', value: 300, color: '#FF6384'},
274
- {label: 'Blue', value: 50, color: '#36A2EB'},
275
- {label: 'Yellow', value: 100, color: '#FFCE56'},
276
- ],
277
- options: {
278
- innerRadius: 0.7,
279
- sliceBorderRadius: 5,
280
- renderLegendItem: ({item, color}) =>
281
- Row({
282
- alignItems: 'center',
283
- children: [
284
- Box({width: 12, height: 12, backgroundColor: color, borderRadius: 6}),
285
- Text(`${item.label}: ${item.value}`, {fontSize: 16, marginLeft: 8}),
286
- ],
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
+ },
287
294
  }),
288
- },
289
- }),
290
- ],
291
- });
295
+ ],
296
+ });
292
297
 
293
- const buffer = await canvas.toBuffer('png');
294
- await writeFile('doughnut_chart.png', buffer);
298
+ const buffer = await canvas.toBuffer('png');
299
+ await writeFile('doughnut_chart.png', buffer);
295
300
  }
296
301
 
297
302
  generateDoughnutChart().catch(console.error);
298
303
  ```
299
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
+
300
391
  ## Using Yoga Layout Properties
301
392
 
302
393
  This library leverages `yoga-layout` for its powerful flexbox engine. Many layout properties directly map to Yoga's
@@ -308,19 +399,19 @@ For example, to set `flexDirection` to `row` or `positionType` to `absolute`, yo
308
399
  import {Box, Style} from '@meonode/canvas';
309
400
 
310
401
  Box({
311
- flexDirection: Style.FlexDirection.Row,
312
- justifyContent: Style.Justify.Center,
313
- alignItems: Style.Align.Center,
314
- children: [
315
- Box({
316
- width: 100,
317
- height: 100,
318
- backgroundColor: 'red',
319
- positionType: Style.PositionType.Absolute,
320
- position: {Top: 10, Left: 10},
321
- }),
322
- // ... other children
323
- ],
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
+ ],
324
415
  });
325
416
  ```
326
417
 
@@ -439,10 +530,17 @@ The `Grid` component arranges its children in a grid layout. It is a specialized
439
530
 
440
531
  #### Grid-Specific Props
441
532
 
442
- | Prop | Type | Description |
443
- |-------------|----------------------------------------------------------|-----------------------------------------------------|
444
- | `columns` | `number` | The number of columns in the grid. Default is 1. |
445
- | `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'. |
446
544
 
447
545
  ---
448
546
 
@@ -476,10 +574,17 @@ The `options` prop is a conditional type that changes based on the chart `type`.
476
574
 
477
575
  ##### Cartesian Chart Options (`bar`, `line`)
478
576
 
479
- | Prop | Type | Description |
480
- |---------------|---------------|-------------------------------------------------------------------|
481
- | `gridOptions` | `GridOptions` | An object to configure the grid lines (`show`, `color`, `style`). |
482
- | `axisColor` | `string` | The color of the chart axes. |
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. |
483
588
 
484
589
  ##### Pie & Doughnut Chart Options (`pie`, `doughnut`)
485
590