declare-render 1.0.3 → 1.0.5

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,11 +1,16 @@
1
1
  import type {
2
2
  ContainerRenderData,
3
3
  ImgRenderData,
4
+ ShapeRenderData,
4
5
  TextRenderData,
5
6
  } from "../types";
6
7
 
7
8
  export abstract class BaseRender<
8
- T extends TextRenderData | ImgRenderData | ContainerRenderData,
9
+ T extends
10
+ | TextRenderData
11
+ | ImgRenderData
12
+ | ContainerRenderData
13
+ | ShapeRenderData,
9
14
  > {
10
15
  protected data!: T;
11
16
  abstract get container(): { x1: number; x2: number; y1: number; y2: number };
@@ -2,6 +2,7 @@ import {
2
2
  ChildRenderers,
3
3
  ImgRenderData,
4
4
  RendererType,
5
+ ShapeRenderData,
5
6
  TextRenderData,
6
7
  } from "../../types";
7
8
  import type { ContainerRenderData } from "../../types";
@@ -10,135 +11,179 @@ import { cloneDeep, isNumber, isObject, isUndefined } from "lodash-es";
10
11
  import { BaseRender } from "../base-renderer";
11
12
  import { ImgRender } from "../img-renderer/index";
12
13
  import { TextRender } from "../text-renderer/index";
14
+ import { ShapeRender } from "../shape-render/index";
13
15
 
14
16
  export type { ContainerRenderData } from "../../types";
15
17
 
16
18
  export class ContainerRenderer extends BaseRender<ContainerRenderData> {
17
- private ctx: CanvasRenderingContext2D
18
- private renderers: Array<ImgRender | TextRender | ContainerRenderer> = []
19
+ private ctx: CanvasRenderingContext2D;
20
+ private layers: Array<
21
+ ImgRender | TextRender | ShapeRender | ContainerRenderer
22
+ > = [];
19
23
 
20
24
  constructor(ctx: CanvasRenderingContext2D, data: ContainerRenderData) {
21
- super()
22
- this.ctx = ctx
23
- this.data = cloneDeep(data)
24
- this.ctx.patternQuality = 'best'
25
- this.ctx.quality = 'best'
25
+ super();
26
+ this.ctx = ctx;
27
+ this.data = cloneDeep(data);
28
+ this.ctx.patternQuality = "best";
29
+ this.ctx.quality = "best";
26
30
  }
27
31
 
28
32
  get container() {
29
- return this.getContainerCoordinates()
33
+ return this.getContainerCoordinates();
30
34
  }
31
35
 
32
- private createRenderer = (renderData: ImgRenderData | TextRenderData | ContainerRenderData) => {
33
- switch (renderData.type) {
36
+ private createLayer = (
37
+ layerData:
38
+ | ImgRenderData
39
+ | TextRenderData
40
+ | ShapeRenderData
41
+ | ContainerRenderData,
42
+ ) => {
43
+ switch (layerData.type) {
34
44
  case RendererType.TEXT: {
35
- return new TextRender(this.ctx, renderData, { inFlexFlow: isUndefined(renderData.x) || isUndefined(renderData.y) })
45
+ return new TextRender(this.ctx, layerData, {
46
+ inFlexFlow: isUndefined(layerData.x) || isUndefined(layerData.y),
47
+ });
36
48
  }
37
49
  case RendererType.IMG:
38
- return new ImgRender(this.ctx, renderData)
50
+ return new ImgRender(this.ctx, layerData);
51
+ case RendererType.SHAPE:
52
+ return new ShapeRender(this.ctx, layerData);
39
53
  case RendererType.CONTAINER:
40
- return new ContainerRenderer(this.ctx, renderData)
54
+ return new ContainerRenderer(this.ctx, layerData);
41
55
  default:
42
- throw new Error("[Renderer] unknown renderer type")
56
+ throw new Error("[Renderer] unknown layer type");
43
57
  }
44
- }
58
+ };
45
59
 
46
- // TODO center the renderers, vertical center, left align, wrap the renderers;
60
+ // TODO center the layers, vertical center, left align, wrap the layers;
47
61
  public layout = async () => {
48
- const layoutedRenderers = [] as Array<ImgRender | TextRender | ContainerRenderer>
49
-
50
- for (const index in this.data.renderers) {
51
- const renderData = cloneDeep(this.data.renderers[index]) as ImgRenderData | TextRenderData | ContainerRenderData
52
-
53
- let renderX = renderData.x
54
- let renderY = renderData.y
55
- // 子 renderer 的 x、y 相对容器的 x、y 进行定位
56
- if (isNumber(renderX)) {
57
- renderX += this.data.x
62
+ const layoutedLayers = [] as Array<
63
+ ImgRender | TextRender | ShapeRender | ContainerRenderer
64
+ >;
65
+
66
+ for (const index in this.data.layers) {
67
+ const layerData = cloneDeep(this.data.layers[index]) as
68
+ | ImgRenderData
69
+ | TextRenderData
70
+ | ShapeRenderData
71
+ | ContainerRenderData;
72
+
73
+ let layerX = layerData.x;
74
+ let layerY = layerData.y;
75
+ // 子 layer 的 x、y 相对容器的 x、y 进行定位
76
+ if (isNumber(layerX)) {
77
+ layerX += this.data.x;
58
78
  }
59
79
 
60
- if (isNumber(renderY)) {
61
- renderY += this.data.y
80
+ if (isNumber(layerY)) {
81
+ layerY += this.data.y;
62
82
  }
63
83
 
64
84
  // justify
65
- if (isUndefined(renderX) || isUndefined(renderY)) {
66
- const preRenderer = layoutedRenderers.at(-1) as undefined | ImgRender | TextRender | ContainerRenderer
67
- const { x2, y1, x1, y2 } = preRenderer?.container || { x1: this.data.x, x2: this.data.x, y1: this.data.y, y2: this.data.y }
68
-
69
- const gapX = !preRenderer ?
70
- 0 : isObject(this.data.gap) ?
71
- this.data.gap.x : (this.data.gap || 0)
72
-
73
- const gapY = !preRenderer ?
74
- 0 : isObject(this.data.gap) ?
75
- this.data.gap.y : (this.data.gap || 0)
76
-
77
- if (this.data.direction === 'column') {
78
- renderX = renderX || x1
79
- renderY = renderY || (y2 + gapY)
85
+ if (isUndefined(layerX) || isUndefined(layerY)) {
86
+ const preLayer = layoutedLayers.at(-1) as
87
+ | undefined
88
+ | ImgRender
89
+ | TextRender
90
+ | ShapeRender
91
+ | ContainerRenderer;
92
+ const { x2, y1, x1, y2 } = preLayer?.container || {
93
+ x1: this.data.x,
94
+ x2: this.data.x,
95
+ y1: this.data.y,
96
+ y2: this.data.y,
97
+ };
98
+
99
+ const gapX = !preLayer
100
+ ? 0
101
+ : isObject(this.data.gap)
102
+ ? this.data.gap.x
103
+ : this.data.gap || 0;
104
+
105
+ const gapY = !preLayer
106
+ ? 0
107
+ : isObject(this.data.gap)
108
+ ? this.data.gap.y
109
+ : this.data.gap || 0;
110
+
111
+ if (this.data.direction === "column") {
112
+ layerX = layerX || x1;
113
+ layerY = layerY || y2 + gapY;
80
114
  } else {
81
- renderX = renderX || (x2 + gapX)
82
- renderY = renderY || y1
115
+ layerX = layerX || x2 + gapX;
116
+ layerY = layerY || y1;
83
117
  }
84
118
  }
85
119
 
86
- const currentRenderer = this.createRenderer({ ...renderData, x: renderX, y: renderY })
120
+ const currentLayer = this.createLayer({
121
+ ...layerData,
122
+ x: layerX,
123
+ y: layerY,
124
+ });
87
125
 
88
- await currentRenderer.layout()
89
-
90
- layoutedRenderers.push(currentRenderer)
126
+ await currentLayer.layout();
127
+
128
+ layoutedLayers.push(currentLayer);
91
129
  }
92
130
 
93
131
  // align items
94
- if (this.data.itemAlign === 'center') {
95
- const squares = layoutedRenderers.map(r => {
96
- const { x1, x2, y1, y2 } = r.container
97
- return { ...r.container, width: x2 - x1, height: y2 - y1, renderer: r }
98
- })
99
-
100
- const maxWidth = Math.max(...squares.map(s => s.width))
101
- const maxHeight = Math.max(...squares.map(s => s.height))
102
-
103
- squares.forEach(s => {
104
- if (this.data.direction === 'column') {
105
- const offset = (maxWidth - s.width) / 2
106
- s.renderer.setPosition(s.x1 + offset, s.y1)
132
+ if (this.data.itemAlign === "center") {
133
+ const squares = layoutedLayers.map((r) => {
134
+ const { x1, x2, y1, y2 } = r.container;
135
+ return { ...r.container, width: x2 - x1, height: y2 - y1, layer: r };
136
+ });
137
+
138
+ const maxWidth = Math.max(...squares.map((s) => s.width));
139
+ const maxHeight = Math.max(...squares.map((s) => s.height));
140
+
141
+ squares.forEach((s) => {
142
+ if (this.data.direction === "column") {
143
+ const offset = (maxWidth - s.width) / 2;
144
+ s.layer.setPosition(s.x1 + offset, s.y1);
107
145
  } else {
108
- const offset = (maxHeight - s.height) / 2
109
- s.renderer.setPosition(s.x1, s.y1 + offset)
146
+ const offset = (maxHeight - s.height) / 2;
147
+ s.layer.setPosition(s.x1, s.y1 + offset);
110
148
  }
111
- })
149
+ });
112
150
  }
113
151
 
114
- this.renderers = layoutedRenderers
115
- return this
116
- }
152
+ this.layers = layoutedLayers;
153
+ return this;
154
+ };
117
155
 
118
- // TODO rotate the whole renderer
119
- public draw = async() => {
120
- if (!this.renderers.length) {
121
- return this
156
+ // TODO rotate the whole layer
157
+ public draw = async () => {
158
+ if (!this.layers.length) {
159
+ return this;
122
160
  }
123
161
 
124
- const pickedOne = this.renderers.shift()!
125
- await pickedOne.draw()
162
+ const pickedOne = this.layers.shift()!;
163
+ await pickedOne.draw();
126
164
 
127
- await this.draw()
165
+ await this.draw();
128
166
 
129
- return this
130
- }
167
+ return this;
168
+ };
131
169
 
132
- // TODO handle the renderers wrapping, add gap;
170
+ // TODO handle the layers wrapping, add gap;
133
171
  private getContainerCoordinates = () => {
134
- if (!this.renderers.length) throw new Error("can not get container coordinates before layouted")
135
- const firstChild = this.renderers.at(0) as ImgRender | TextRender
136
- const lastChild = this.renderers.at(-1) as ImgRender | TextRender
172
+ if (!this.layers.length)
173
+ throw new Error("can not get container coordinates before layouted");
174
+ const firstChild = this.layers.at(0) as
175
+ | ImgRender
176
+ | TextRender
177
+ | ShapeRender;
178
+ const lastChild = this.layers.at(-1) as
179
+ | ImgRender
180
+ | TextRender
181
+ | ShapeRender;
137
182
  return {
138
183
  x1: firstChild.container.x1,
139
184
  y1: firstChild.container.y1,
140
185
  x2: lastChild.container.x2,
141
186
  y2: lastChild.container.y2,
142
- }
143
- }
187
+ };
188
+ };
144
189
  }
@@ -0,0 +1,279 @@
1
+ import { BaseRender } from "../base-renderer";
2
+ import type { ShapeRenderData, ShapeCommand } from "../../types";
3
+ import { cloneDeep, isNumber } from "lodash-es";
4
+ import { type CanvasRenderingContext2D } from "canvas";
5
+
6
+ export type { ShapeRenderData } from "../../types";
7
+
8
+ export class ShapeRender extends BaseRender<ShapeRenderData> {
9
+ private ctx: CanvasRenderingContext2D;
10
+ private computedWidth: number = 0;
11
+ private computedHeight: number = 0;
12
+
13
+ constructor(ctx: CanvasRenderingContext2D, data: ShapeRenderData) {
14
+ super();
15
+ this.ctx = ctx;
16
+ this.data = cloneDeep(data);
17
+ }
18
+
19
+ get container() {
20
+ return this.getContainerCoordinates();
21
+ }
22
+
23
+ private applyStyle() {
24
+ const style = this.data.style;
25
+ if (!style) return;
26
+
27
+ if (style.fillStyle) {
28
+ this.ctx.fillStyle = style.fillStyle;
29
+ }
30
+ if (style.strokeStyle) {
31
+ this.ctx.strokeStyle = style.strokeStyle;
32
+ }
33
+ if (isNumber(style.lineWidth)) {
34
+ this.ctx.lineWidth = style.lineWidth;
35
+ }
36
+ if (style.lineCap) {
37
+ this.ctx.lineCap = style.lineCap;
38
+ }
39
+ if (style.lineJoin) {
40
+ this.ctx.lineJoin = style.lineJoin;
41
+ }
42
+ if (isNumber(style.miterLimit)) {
43
+ this.ctx.miterLimit = style.miterLimit;
44
+ }
45
+ if (style.lineDash) {
46
+ this.ctx.setLineDash(style.lineDash);
47
+ }
48
+ if (isNumber(style.lineDashOffset)) {
49
+ this.ctx.lineDashOffset = style.lineDashOffset;
50
+ }
51
+ if (isNumber(style.globalAlpha)) {
52
+ this.ctx.globalAlpha = style.globalAlpha;
53
+ }
54
+ }
55
+
56
+ private applyShadow() {
57
+ if (this.data.shadow) {
58
+ this.ctx.shadowBlur = this.data.shadow.blur;
59
+ this.ctx.shadowColor = this.data.shadow.color;
60
+ this.ctx.shadowOffsetX = this.data.shadow.X;
61
+ this.ctx.shadowOffsetY = this.data.shadow.Y;
62
+ }
63
+ }
64
+
65
+ private executeCommand(cmd: ShapeCommand, offsetX: number, offsetY: number) {
66
+ switch (cmd.type) {
67
+ case "rect":
68
+ this.ctx.rect(cmd.x + offsetX, cmd.y + offsetY, cmd.width, cmd.height);
69
+ break;
70
+ case "fillRect":
71
+ this.ctx.fillRect(
72
+ cmd.x + offsetX,
73
+ cmd.y + offsetY,
74
+ cmd.width,
75
+ cmd.height,
76
+ );
77
+ break;
78
+ case "strokeRect":
79
+ this.ctx.strokeRect(
80
+ cmd.x + offsetX,
81
+ cmd.y + offsetY,
82
+ cmd.width,
83
+ cmd.height,
84
+ );
85
+ break;
86
+ case "clearRect":
87
+ this.ctx.clearRect(
88
+ cmd.x + offsetX,
89
+ cmd.y + offsetY,
90
+ cmd.width,
91
+ cmd.height,
92
+ );
93
+ break;
94
+ case "beginPath":
95
+ this.ctx.beginPath();
96
+ break;
97
+ case "closePath":
98
+ this.ctx.closePath();
99
+ break;
100
+ case "moveTo":
101
+ this.ctx.moveTo(cmd.x + offsetX, cmd.y + offsetY);
102
+ break;
103
+ case "lineTo":
104
+ this.ctx.lineTo(cmd.x + offsetX, cmd.y + offsetY);
105
+ break;
106
+ case "arc":
107
+ this.ctx.arc(
108
+ cmd.x + offsetX,
109
+ cmd.y + offsetY,
110
+ cmd.radius,
111
+ cmd.startAngle,
112
+ cmd.endAngle,
113
+ cmd.counterclockwise || false,
114
+ );
115
+ break;
116
+ case "arcTo":
117
+ this.ctx.arcTo(
118
+ cmd.x1 + offsetX,
119
+ cmd.y1 + offsetY,
120
+ cmd.x2 + offsetX,
121
+ cmd.y2 + offsetY,
122
+ cmd.radius,
123
+ );
124
+ break;
125
+ case "quadraticCurveTo":
126
+ this.ctx.quadraticCurveTo(
127
+ cmd.cp1x + offsetX,
128
+ cmd.cp1y + offsetY,
129
+ cmd.x + offsetX,
130
+ cmd.y + offsetY,
131
+ );
132
+ break;
133
+ case "bezierCurveTo":
134
+ this.ctx.bezierCurveTo(
135
+ cmd.cp1x + offsetX,
136
+ cmd.cp1y + offsetY,
137
+ cmd.cp2x + offsetX,
138
+ cmd.cp2y + offsetY,
139
+ cmd.x + offsetX,
140
+ cmd.y + offsetY,
141
+ );
142
+ break;
143
+ case "fill":
144
+ this.ctx.fill();
145
+ break;
146
+ case "stroke":
147
+ this.ctx.stroke();
148
+ break;
149
+ case "fillAndStroke":
150
+ this.ctx.fill();
151
+ this.ctx.stroke();
152
+ break;
153
+ }
154
+ }
155
+
156
+ private computeBounds(): { width: number; height: number } {
157
+ if (this.data.width && this.data.height) {
158
+ return { width: this.data.width, height: this.data.height };
159
+ }
160
+
161
+ if (!this.data.shapes || this.data.shapes.length === 0) {
162
+ return { width: 0, height: 0 };
163
+ }
164
+
165
+ let minX = Infinity;
166
+ let minY = Infinity;
167
+ let maxX = -Infinity;
168
+ let maxY = -Infinity;
169
+
170
+ for (const cmd of this.data.shapes) {
171
+ switch (cmd.type) {
172
+ case "rect":
173
+ case "fillRect":
174
+ case "strokeRect":
175
+ case "clearRect":
176
+ minX = Math.min(minX, cmd.x);
177
+ minY = Math.min(minY, cmd.y);
178
+ maxX = Math.max(maxX, cmd.x + cmd.width);
179
+ maxY = Math.max(maxY, cmd.y + cmd.height);
180
+ break;
181
+ case "moveTo":
182
+ case "lineTo":
183
+ minX = Math.min(minX, cmd.x);
184
+ minY = Math.min(minY, cmd.y);
185
+ maxX = Math.max(maxX, cmd.x);
186
+ maxY = Math.max(maxY, cmd.y);
187
+ break;
188
+ case "arc":
189
+ minX = Math.min(minX, cmd.x - cmd.radius);
190
+ minY = Math.min(minY, cmd.y - cmd.radius);
191
+ maxX = Math.max(maxX, cmd.x + cmd.radius);
192
+ maxY = Math.max(maxY, cmd.y + cmd.radius);
193
+ break;
194
+ case "arcTo":
195
+ minX = Math.min(minX, cmd.x1, cmd.x2);
196
+ minY = Math.min(minY, cmd.y1, cmd.y2);
197
+ maxX = Math.max(maxX, cmd.x1, cmd.x2);
198
+ maxY = Math.max(maxY, cmd.y1, cmd.y2);
199
+ break;
200
+ case "quadraticCurveTo":
201
+ case "bezierCurveTo":
202
+ minX = Math.min(minX, cmd.cp1x, cmd.x);
203
+ minY = Math.min(minY, cmd.cp1y, cmd.y);
204
+ maxX = Math.max(maxX, cmd.cp1x, cmd.x);
205
+ maxY = Math.max(maxY, cmd.cp1y, cmd.y);
206
+ if (cmd.type === "bezierCurveTo") {
207
+ minX = Math.min(minX, cmd.cp2x);
208
+ minY = Math.min(minY, cmd.cp2y);
209
+ maxX = Math.max(maxX, cmd.cp2x);
210
+ maxY = Math.max(maxY, cmd.cp2y);
211
+ }
212
+ break;
213
+ }
214
+ }
215
+
216
+ if (minX === Infinity) {
217
+ return { width: 0, height: 0 };
218
+ }
219
+
220
+ return {
221
+ width: maxX - minX,
222
+ height: maxY - minY,
223
+ };
224
+ }
225
+
226
+ public layout = async () => {
227
+ const bounds = this.computeBounds();
228
+ this.computedWidth = this.data.width ?? bounds.width;
229
+ this.computedHeight = this.data.height ?? bounds.height;
230
+ return this;
231
+ };
232
+
233
+ public draw = async () => {
234
+ const { x, y, rotate } = this.data;
235
+ const offsetX = x;
236
+ const offsetY = y;
237
+
238
+ const drawImpl = () => {
239
+ this.ctx.save();
240
+ this.applyStyle();
241
+ this.applyShadow();
242
+
243
+ for (const cmd of this.data.shapes) {
244
+ this.executeCommand(cmd, offsetX, offsetY);
245
+ }
246
+
247
+ this.ctx.restore();
248
+ };
249
+
250
+ if (!rotate) {
251
+ drawImpl();
252
+ } else {
253
+ const { computedWidth, computedHeight } = this;
254
+ const centerX = x + computedWidth / 2;
255
+ const centerY = y + computedHeight / 2;
256
+
257
+ this.ctx.save();
258
+ this.ctx.translate(centerX, centerY);
259
+ this.ctx.rotate((rotate * Math.PI) / 180);
260
+ this.ctx.translate(-centerX, -centerY);
261
+
262
+ drawImpl();
263
+
264
+ this.ctx.restore();
265
+ }
266
+
267
+ return this;
268
+ };
269
+
270
+ public getContainerCoordinates = () => {
271
+ const { x, y } = this.data;
272
+ return {
273
+ x1: x,
274
+ y1: y,
275
+ x2: x + this.computedWidth,
276
+ y2: y + this.computedHeight,
277
+ };
278
+ };
279
+ }
package/src/index.ts CHANGED
@@ -5,6 +5,7 @@ import { ContainerRenderer } from "./canvas-renderers/container-renderer/index";
5
5
  export type {
6
6
  ContainerRenderData,
7
7
  ImgRenderData,
8
+ ShapeRenderData,
8
9
  TextRenderData,
9
10
  RenderData,
10
11
  } from "./types";
@@ -23,8 +24,8 @@ export class Renderer {
23
24
  }
24
25
 
25
26
  draw = async () => {
26
- if (!this.schema.renderers.length) {
27
- throw new Error("[Renderer] empty canvas with no renderers");
27
+ if (!this.schema.layers.length) {
28
+ throw new Error("[Renderer] empty canvas with no layers");
28
29
  }
29
30
  const container = new ContainerRenderer(this.ctx, {
30
31
  id: this.schema.id,
@@ -33,7 +34,7 @@ export class Renderer {
33
34
  y: 0,
34
35
  width: this.canvas.width,
35
36
  height: this.canvas.height,
36
- renderers: this.schema.renderers,
37
+ layers: this.schema.layers,
37
38
  });
38
39
  await container.layout().then((d) => d.draw());
39
40
  return this.toBuffer();