@codehz/draw-call 0.1.1 → 0.2.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.
@@ -101,6 +101,33 @@ interface ComputedLayout {
101
101
  contentWidth: number;
102
102
  contentHeight: number;
103
103
  }
104
+ interface RichTextSpanSegment {
105
+ text: string;
106
+ font: FontProps;
107
+ color: Color;
108
+ background: Color;
109
+ underline: boolean;
110
+ strikethrough: boolean;
111
+ width: number;
112
+ height: number;
113
+ ascent: number;
114
+ descent: number;
115
+ offset: number;
116
+ }
117
+ interface RichTextLine {
118
+ segments: RichTextSpanSegment[];
119
+ width: number;
120
+ height: number;
121
+ baseline: number;
122
+ }
123
+ interface LayoutNode {
124
+ element: Element;
125
+ layout: ComputedLayout;
126
+ children: LayoutNode[];
127
+ lines?: string[];
128
+ lineOffsets?: number[];
129
+ richLines?: RichTextLine[];
130
+ }
104
131
  interface LayoutConstraints {
105
132
  minWidth: number;
106
133
  maxWidth: number;
@@ -109,7 +136,7 @@ interface LayoutConstraints {
109
136
  }
110
137
  //#endregion
111
138
  //#region src/types/components.d.ts
112
- type ElementType = "box" | "text" | "image" | "shape" | "stack";
139
+ type ElementType = "box" | "text" | "richtext" | "image" | "svg" | "stack";
113
140
  interface ElementBase {
114
141
  type: ElementType;
115
142
  }
@@ -140,8 +167,29 @@ interface TextProps extends LayoutProps {
140
167
  interface TextElement extends ElementBase, TextProps {
141
168
  type: "text";
142
169
  }
170
+ interface RichTextStyleProps {
171
+ font?: FontProps;
172
+ color?: Color;
173
+ background?: Color;
174
+ underline?: boolean;
175
+ strikethrough?: boolean;
176
+ }
177
+ interface RichTextSpan extends RichTextStyleProps {
178
+ text: string;
179
+ }
180
+ interface RichTextProps extends LayoutProps, RichTextStyleProps {
181
+ spans: RichTextSpan[];
182
+ lineHeight?: number;
183
+ align?: "left" | "center" | "right";
184
+ verticalAlign?: "top" | "middle" | "bottom";
185
+ maxLines?: number;
186
+ ellipsis?: boolean;
187
+ }
188
+ interface RichTextElement extends ElementBase, RichTextProps {
189
+ type: "richtext";
190
+ }
143
191
  interface ImageProps extends LayoutProps {
144
- src: string | ImageBitmap | CanvasImageSource;
192
+ src: ImageBitmap | CanvasImageSource;
145
193
  fit?: "contain" | "cover" | "fill" | "none" | "scale-down";
146
194
  position?: {
147
195
  x?: "left" | "center" | "right" | number;
@@ -154,50 +202,112 @@ interface ImageProps extends LayoutProps {
154
202
  interface ImageElement extends ElementBase, ImageProps {
155
203
  type: "image";
156
204
  }
157
- type ShapeType = "rect" | "circle" | "ellipse" | "line" | "polygon" | "path";
158
- interface ShapeProps extends LayoutProps {
159
- shape: ShapeType;
160
- fill?: Color;
205
+ interface SvgStyleProps {
206
+ fill?: Color | "none";
161
207
  stroke?: StrokeProps;
208
+ opacity?: number;
209
+ }
210
+ interface SvgTransformProps {
211
+ transform?: {
212
+ translate?: [number, number];
213
+ rotate?: number | [number, number, number];
214
+ scale?: number | [number, number];
215
+ skewX?: number;
216
+ skewY?: number;
217
+ matrix?: [number, number, number, number, number, number];
218
+ };
219
+ }
220
+ interface SvgRectChild extends SvgStyleProps, SvgTransformProps {
221
+ type: "rect";
222
+ x?: number;
223
+ y?: number;
224
+ width: number;
225
+ height: number;
226
+ rx?: number;
227
+ ry?: number;
228
+ }
229
+ interface SvgCircleChild extends SvgStyleProps, SvgTransformProps {
230
+ type: "circle";
231
+ cx: number;
232
+ cy: number;
233
+ r: number;
234
+ }
235
+ interface SvgEllipseChild extends SvgStyleProps, SvgTransformProps {
236
+ type: "ellipse";
237
+ cx: number;
238
+ cy: number;
239
+ rx: number;
240
+ ry: number;
241
+ }
242
+ interface SvgLineChild extends SvgStyleProps, SvgTransformProps {
243
+ type: "line";
244
+ x1: number;
245
+ y1: number;
246
+ x2: number;
247
+ y2: number;
248
+ }
249
+ interface SvgPolylineChild extends SvgStyleProps, SvgTransformProps {
250
+ type: "polyline";
251
+ points: [number, number][];
252
+ }
253
+ interface SvgPolygonChild extends SvgStyleProps, SvgTransformProps {
254
+ type: "polygon";
255
+ points: [number, number][];
256
+ }
257
+ interface SvgPathChild extends SvgStyleProps, SvgTransformProps {
258
+ type: "path";
259
+ d: string;
260
+ }
261
+ interface SvgTextChild extends SvgStyleProps, SvgTransformProps {
262
+ type: "text";
263
+ x?: number;
264
+ y?: number;
265
+ content: string;
266
+ font?: FontProps;
267
+ textAnchor?: "start" | "middle" | "end";
268
+ dominantBaseline?: "auto" | "middle" | "hanging";
269
+ }
270
+ interface SvgGroupChild extends SvgStyleProps, SvgTransformProps {
271
+ type: "g";
272
+ children: SvgChild[];
273
+ }
274
+ type SvgChild = SvgRectChild | SvgCircleChild | SvgEllipseChild | SvgLineChild | SvgPolylineChild | SvgPolygonChild | SvgPathChild | SvgTextChild | SvgGroupChild;
275
+ type SvgAlign = "none" | "xMinYMin" | "xMidYMin" | "xMaxYMin" | "xMinYMid" | "xMidYMid" | "xMaxYMid" | "xMinYMax" | "xMidYMax" | "xMaxYMax";
276
+ interface SvgProps extends LayoutProps {
277
+ viewBox?: {
278
+ x?: number;
279
+ y?: number;
280
+ width: number;
281
+ height: number;
282
+ };
283
+ preserveAspectRatio?: {
284
+ align?: SvgAlign;
285
+ meetOrSlice?: "meet" | "slice";
286
+ };
287
+ children: SvgChild[];
288
+ background?: Color;
162
289
  shadow?: Shadow;
163
- points?: [number, number][];
164
- path?: string;
165
290
  }
166
- interface ShapeElement extends ElementBase, ShapeProps {
167
- type: "shape";
291
+ interface SvgElement extends ElementBase, SvgProps {
292
+ type: "svg";
168
293
  }
169
- interface StackProps extends ContainerLayoutProps {
294
+ type StackAlign = "start" | "end" | "center";
295
+ interface StackProps extends LayoutProps {
170
296
  children: Element[];
171
297
  background?: Color;
172
298
  border?: Border;
173
299
  shadow?: Shadow;
174
300
  opacity?: number;
175
301
  clip?: boolean;
302
+ /** 水平对齐方式(默认 start) */
303
+ align?: StackAlign;
304
+ /** 垂直对齐方式(默认 start) */
305
+ justify?: StackAlign;
176
306
  }
177
307
  interface StackElement extends ElementBase, StackProps {
178
308
  type: "stack";
179
309
  }
180
- type Element = BoxElement | TextElement | ImageElement | ShapeElement | StackElement;
181
- //#endregion
182
- //#region src/layout/measure.d.ts
183
- interface MeasureContext {
184
- measureText(text: string, font: FontProps): {
185
- width: number;
186
- height: number;
187
- offset: number;
188
- };
189
- }
190
- declare function createCanvasMeasureContext(ctx: CanvasRenderingContext2D): MeasureContext;
191
- //#endregion
192
- //#region src/layout/engine.d.ts
193
- interface LayoutNode {
194
- element: Element;
195
- layout: ComputedLayout;
196
- children: LayoutNode[];
197
- lines?: string[];
198
- lineOffsets?: number[];
199
- }
200
- declare function computeLayout(element: Element, ctx: MeasureContext, constraints: LayoutConstraints, x?: number, y?: number): LayoutNode;
310
+ type Element = BoxElement | TextElement | RichTextElement | ImageElement | SvgElement | StackElement;
201
311
  //#endregion
202
312
  //#region src/canvas.d.ts
203
313
  interface CanvasOptions {
@@ -218,11 +328,12 @@ interface DrawCallCanvas {
218
328
  readonly width: number;
219
329
  readonly height: number;
220
330
  readonly pixelRatio: number;
331
+ readonly canvas: HTMLCanvasElement;
221
332
  render(element: Element): LayoutNode;
222
333
  clear(): void;
223
334
  getContext(): CanvasRenderingContext2D;
224
335
  toDataURL(type?: string, quality?: number): string;
225
- toBuffer(type?: "image/png" | "image/jpeg"): Promise<Buffer>;
336
+ toBuffer(type?: "image/png" | "image/jpeg"): Buffer;
226
337
  }
227
338
  /**
228
339
  * 创建适用于浏览器环境的 Canvas
@@ -231,4 +342,4 @@ interface DrawCallCanvas {
231
342
  */
232
343
  declare function createCanvas(options: CanvasOptions): DrawCallCanvas;
233
344
  //#endregion
234
- export { Shadow as A, Bounds as C, GradientDescriptor as D, FontProps as E, radialGradient as F, Spacing as M, StrokeProps as N, LinearGradientDescriptor as O, linearGradient as P, Border as S, ColorStop as T, AlignSelf as _, LayoutNode as a, JustifyContent as b, createCanvasMeasureContext as c, Element as d, StackElement as f, AlignItems as g, TextProps as h, createCanvas as i, Size as j, RadialGradientDescriptor as k, BoxElement as l, TextElement as m, DrawCallCanvas as n, computeLayout as o, StackProps as p, LayoutSize as r, MeasureContext as s, CanvasOptions as t, BoxProps as u, ContainerLayoutProps as v, Color as w, LayoutProps as x, FlexDirection as y };
345
+ export { linearGradient as $, SvgTransformProps as A, LayoutProps as B, SvgPathChild as C, SvgRectChild as D, SvgProps as E, ContainerLayoutProps as F, FontProps as G, Bounds as H, FlexDirection as I, RadialGradientDescriptor as J, GradientDescriptor as K, JustifyContent as L, TextProps as M, AlignItems as N, SvgStyleProps as O, AlignSelf as P, StrokeProps as Q, LayoutConstraints as R, SvgLineChild as S, SvgPolylineChild as T, Color as U, Border as V, ColorStop as W, Size as X, Shadow as Y, Spacing as Z, SvgChild as _, BoxElement as a, SvgEllipseChild as b, ImageElement as c, RichTextProps as d, radialGradient as et, RichTextSpan as f, SvgAlign as g, StackProps as h, createCanvas as i, TextElement as j, SvgTextChild as k, ImageProps as l, StackElement as m, DrawCallCanvas as n, BoxProps as o, StackAlign as p, LinearGradientDescriptor as q, LayoutSize as r, Element as s, CanvasOptions as t, RichTextElement as u, SvgCircleChild as v, SvgPolygonChild as w, SvgGroupChild as x, SvgElement as y, LayoutNode as z };
@@ -0,0 +1,155 @@
1
+ /**
2
+ * 示例:使用 draw-call 绘制一个卡片
3
+ * 运行: bun examples/card.ts
4
+ */
5
+ import { Box, linearGradient, printLayout, Svg, svg, Text } from "@codehz/draw-call";
6
+ import { createNodeCanvas } from "@codehz/draw-call/node";
7
+ import { GlobalFonts } from "@napi-rs/canvas";
8
+ import { fileURLToPath } from "bun";
9
+
10
+ GlobalFonts.registerFromPath(fileURLToPath(import.meta.resolve("@fontpkg/unifont/unifont-15.0.01.ttf")), "unifont");
11
+
12
+ const canvas = createNodeCanvas({
13
+ width: 400,
14
+ height: 320,
15
+ pixelRatio: 2,
16
+ });
17
+
18
+ // 绘制背景
19
+ const layout = canvas.render(
20
+ Box({
21
+ width: "fill",
22
+ height: "fill",
23
+ background: "#f0f2f5",
24
+ padding: 20,
25
+ justify: "center",
26
+ align: "center",
27
+ children: [
28
+ // 卡片
29
+ Box({
30
+ width: 360,
31
+ background: "#ffffff",
32
+ border: { radius: 12 },
33
+ shadow: { offsetY: 4, blur: 16, color: "rgba(0,0,0,0.12)" },
34
+ direction: "column",
35
+ clip: true,
36
+ children: [
37
+ // 卡片头部
38
+ Box({
39
+ height: 100,
40
+ background: linearGradient(135, "#667eea", "#764ba2"),
41
+ padding: 20,
42
+ justify: "space-between",
43
+ align: "end",
44
+ children: [
45
+ // SVG 图标演示
46
+ Svg({
47
+ width: 48,
48
+ height: 48,
49
+ viewBox: { width: 24, height: 24 },
50
+ children: [
51
+ // 绘制一个简单的画笔图标
52
+ svg.circle({ cx: 12, cy: 12, r: 10, fill: "rgba(255,255,255,0.2)" }),
53
+ svg.path({
54
+ d: "M4 20h4l10.5-10.5a1.5 1.5 0 0 0-4-4L4 16v4z",
55
+ fill: "#ffffff",
56
+ }),
57
+ svg.line({
58
+ x1: 13.5,
59
+ y1: 6.5,
60
+ x2: 17.5,
61
+ y2: 10.5,
62
+ stroke: { color: "#ffffff", width: 1.5 },
63
+ }),
64
+ ],
65
+ }),
66
+ Text({
67
+ content: "draw-call",
68
+ font: { size: 28, weight: "bold", family: "unifont" },
69
+ color: "#ffffff",
70
+ shadow: {
71
+ offsetX: 1,
72
+ offsetY: 1,
73
+ blur: 2,
74
+ color: "rgba(0,0,0,0.3)",
75
+ },
76
+ }),
77
+ ],
78
+ }),
79
+ // 卡片内容
80
+ Box({
81
+ padding: 20,
82
+ direction: "column",
83
+ gap: 12,
84
+ children: [
85
+ Text({
86
+ content: "声明式 Canvas 绘图",
87
+ font: { size: 18, weight: "bold", family: "unifont" },
88
+ color: "#333333",
89
+ }),
90
+ Text({
91
+ content: "使用类似 UI 框架的方式来绘制 Canvas 内容,支持 Flexbox 布局、文本自动换行等特性。",
92
+ font: { size: 14, family: "unifont" },
93
+ color: "#666666",
94
+ lineHeight: 1.6,
95
+ wrap: true,
96
+ }),
97
+ // 标签
98
+ Box({
99
+ direction: "row",
100
+ gap: 8,
101
+ children: [
102
+ Box({
103
+ padding: { left: 10, right: 10, top: 4, bottom: 4 },
104
+ background: "#e8f4ff",
105
+ border: { radius: 4 },
106
+ children: [
107
+ Text({
108
+ content: "Canvas",
109
+ font: { size: 12, family: "unifont" },
110
+ color: "#1890ff",
111
+ }),
112
+ ],
113
+ }),
114
+ Box({
115
+ padding: { left: 10, right: 10, top: 4, bottom: 4 },
116
+ background: "#f6ffed",
117
+ border: { radius: 4 },
118
+ children: [
119
+ Text({
120
+ content: "TypeScript",
121
+ font: { size: 12, family: "unifont" },
122
+ color: "#52c41a",
123
+ }),
124
+ ],
125
+ }),
126
+ Box({
127
+ padding: { left: 10, right: 10, top: 4, bottom: 4 },
128
+ background: "#fff7e6",
129
+ border: { radius: 4 },
130
+ children: [
131
+ Text({
132
+ content: "声明式",
133
+ font: { size: 12, family: "unifont" },
134
+ color: "#fa8c16",
135
+ }),
136
+ ],
137
+ }),
138
+ ],
139
+ }),
140
+ ],
141
+ }),
142
+ ],
143
+ }),
144
+ ],
145
+ })
146
+ );
147
+
148
+ // 保存到文件
149
+ const buffer = await canvas.toBuffer("image/png");
150
+ await Bun.write("examples/card.png", buffer);
151
+ console.log("Card saved to examples/card.png");
152
+
153
+ // 美观打印布局树
154
+ console.log("\n=== Layout Tree ===");
155
+ printLayout(layout);