@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.
- package/README.md +374 -6
- package/{canvas-BYrCq8eS.d.mts → canvas.d.cts} +145 -34
- package/{canvas-CkpP3RNK.d.cts → canvas.d.mts} +145 -34
- package/examples/card.ts +155 -0
- package/examples/demo.ts +478 -0
- package/examples/richtext.ts +284 -0
- package/index.cjs +139 -9
- package/index.d.cts +51 -2
- package/index.d.mts +51 -2
- package/index.mjs +127 -3
- package/node.cjs +8 -7
- package/node.d.cts +3 -3
- package/node.d.mts +3 -3
- package/node.mjs +7 -6
- package/package.json +1 -1
- package/render.cjs +1555 -0
- package/render.mjs +1531 -0
- package/engine-C88lMGbX.mjs +0 -606
- package/engine-OyKoV7Gn.cjs +0 -636
|
@@ -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" | "
|
|
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:
|
|
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
|
-
|
|
158
|
-
|
|
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
|
|
167
|
-
type: "
|
|
291
|
+
interface SvgElement extends ElementBase, SvgProps {
|
|
292
|
+
type: "svg";
|
|
168
293
|
}
|
|
169
|
-
|
|
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 |
|
|
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"):
|
|
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 {
|
|
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 };
|
package/examples/card.ts
ADDED
|
@@ -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);
|