@zseven-w/pen-renderer 0.6.0 → 0.7.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 +6 -6
- package/package.json +9 -9
- package/src/__tests__/document-flattener.test.ts +166 -90
- package/src/__tests__/font-manager.test.ts +65 -0
- package/src/__tests__/image-loader.test.ts +136 -0
- package/src/__tests__/paint-utils.test.ts +61 -0
- package/src/__tests__/render-node-thumbnail.test.ts +312 -0
- package/src/document-flattener.ts +222 -159
- package/src/font-manager.ts +221 -190
- package/src/image-loader.ts +138 -51
- package/src/index.ts +18 -17
- package/src/init.ts +50 -21
- package/src/node-renderer.ts +957 -386
- package/src/paint-utils.ts +99 -74
- package/src/path-utils.ts +235 -115
- package/src/render-node-thumbnail.ts +155 -0
- package/src/renderer.ts +196 -175
- package/src/spatial-index.ts +139 -27
- package/src/text-renderer.ts +360 -302
- package/src/types.ts +18 -22
- package/src/viewport.ts +28 -29
package/src/paint-utils.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type { CanvasKit } from 'canvaskit-wasm'
|
|
2
|
-
import type { PenFill, PenStroke } from '@zseven-w/pen-types'
|
|
3
|
-
import { DEFAULT_FILL, DEFAULT_STROKE_WIDTH } from '@zseven-w/pen-core'
|
|
1
|
+
import type { CanvasKit } from 'canvaskit-wasm';
|
|
2
|
+
import type { PenFill, PenStroke } from '@zseven-w/pen-types';
|
|
3
|
+
import { DEFAULT_FILL, DEFAULT_STROKE_WIDTH } from '@zseven-w/pen-core';
|
|
4
4
|
|
|
5
|
-
export { cssFontFamily } from '@zseven-w/pen-core'
|
|
5
|
+
export { cssFontFamily } from '@zseven-w/pen-core';
|
|
6
6
|
|
|
7
7
|
// ---------------------------------------------------------------------------
|
|
8
8
|
// Color parsing — ck.Color4f takes 0-1 floats for all channels (r, g, b, a)
|
|
@@ -10,57 +10,61 @@ export { cssFontFamily } from '@zseven-w/pen-core'
|
|
|
10
10
|
|
|
11
11
|
export function parseColor(ck: CanvasKit, color: string): Float32Array {
|
|
12
12
|
if (color.startsWith('#')) {
|
|
13
|
-
const hex = color.slice(1)
|
|
13
|
+
const hex = color.slice(1);
|
|
14
14
|
if (hex.length === 8) {
|
|
15
|
-
const r = parseInt(hex.slice(0, 2), 16) / 255
|
|
16
|
-
const g = parseInt(hex.slice(2, 4), 16) / 255
|
|
17
|
-
const b = parseInt(hex.slice(4, 6), 16) / 255
|
|
18
|
-
const a = parseInt(hex.slice(6, 8), 16) / 255
|
|
19
|
-
return ck.Color4f(r, g, b, a)
|
|
15
|
+
const r = parseInt(hex.slice(0, 2), 16) / 255;
|
|
16
|
+
const g = parseInt(hex.slice(2, 4), 16) / 255;
|
|
17
|
+
const b = parseInt(hex.slice(4, 6), 16) / 255;
|
|
18
|
+
const a = parseInt(hex.slice(6, 8), 16) / 255;
|
|
19
|
+
return ck.Color4f(r, g, b, a);
|
|
20
20
|
}
|
|
21
21
|
if (hex.length === 6) {
|
|
22
|
-
const r = parseInt(hex.slice(0, 2), 16) / 255
|
|
23
|
-
const g = parseInt(hex.slice(2, 4), 16) / 255
|
|
24
|
-
const b = parseInt(hex.slice(4, 6), 16) / 255
|
|
25
|
-
return ck.Color4f(r, g, b, 1)
|
|
22
|
+
const r = parseInt(hex.slice(0, 2), 16) / 255;
|
|
23
|
+
const g = parseInt(hex.slice(2, 4), 16) / 255;
|
|
24
|
+
const b = parseInt(hex.slice(4, 6), 16) / 255;
|
|
25
|
+
return ck.Color4f(r, g, b, 1);
|
|
26
26
|
}
|
|
27
27
|
if (hex.length === 3) {
|
|
28
|
-
const r = parseInt(hex[0] + hex[0], 16) / 255
|
|
29
|
-
const g = parseInt(hex[1] + hex[1], 16) / 255
|
|
30
|
-
const b = parseInt(hex[2] + hex[2], 16) / 255
|
|
31
|
-
return ck.Color4f(r, g, b, 1)
|
|
28
|
+
const r = parseInt(hex[0] + hex[0], 16) / 255;
|
|
29
|
+
const g = parseInt(hex[1] + hex[1], 16) / 255;
|
|
30
|
+
const b = parseInt(hex[2] + hex[2], 16) / 255;
|
|
31
|
+
return ck.Color4f(r, g, b, 1);
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
|
-
if (color === 'transparent') return ck.Color4f(0, 0, 0, 0)
|
|
35
|
-
if (color === 'white') return ck.Color4f(1, 1, 1, 1)
|
|
36
|
-
if (color === 'black') return ck.Color4f(0, 0, 0, 1)
|
|
34
|
+
if (color === 'transparent') return ck.Color4f(0, 0, 0, 0);
|
|
35
|
+
if (color === 'white') return ck.Color4f(1, 1, 1, 1);
|
|
36
|
+
if (color === 'black') return ck.Color4f(0, 0, 0, 1);
|
|
37
37
|
// rgba() parsing
|
|
38
|
-
const rgbaMatch = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/)
|
|
38
|
+
const rgbaMatch = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/);
|
|
39
39
|
if (rgbaMatch) {
|
|
40
40
|
return ck.Color4f(
|
|
41
41
|
parseInt(rgbaMatch[1]) / 255,
|
|
42
42
|
parseInt(rgbaMatch[2]) / 255,
|
|
43
43
|
parseInt(rgbaMatch[3]) / 255,
|
|
44
44
|
rgbaMatch[4] !== undefined ? parseFloat(rgbaMatch[4]) : 1,
|
|
45
|
-
)
|
|
45
|
+
);
|
|
46
46
|
}
|
|
47
|
-
return ck.Color4f(0.82, 0.835, 0.858, 1) // fallback #d1d5db
|
|
47
|
+
return ck.Color4f(0.82, 0.835, 0.858, 1); // fallback #d1d5db
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
// ---------------------------------------------------------------------------
|
|
51
51
|
// Corner radius helpers
|
|
52
52
|
// ---------------------------------------------------------------------------
|
|
53
53
|
|
|
54
|
-
export function cornerRadiusValue(
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
return
|
|
54
|
+
export function cornerRadiusValue(
|
|
55
|
+
cr: number | [number, number, number, number] | undefined,
|
|
56
|
+
): number {
|
|
57
|
+
if (cr === undefined) return 0;
|
|
58
|
+
if (typeof cr === 'number') return cr;
|
|
59
|
+
return cr[0];
|
|
58
60
|
}
|
|
59
61
|
|
|
60
|
-
export function cornerRadii(
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
return
|
|
62
|
+
export function cornerRadii(
|
|
63
|
+
cr: number | [number, number, number, number] | undefined,
|
|
64
|
+
): [number, number, number, number] {
|
|
65
|
+
if (cr === undefined) return [0, 0, 0, 0];
|
|
66
|
+
if (typeof cr === 'number') return [cr, cr, cr, cr];
|
|
67
|
+
return cr;
|
|
64
68
|
}
|
|
65
69
|
|
|
66
70
|
// ---------------------------------------------------------------------------
|
|
@@ -68,31 +72,44 @@ export function cornerRadii(cr: number | [number, number, number, number] | unde
|
|
|
68
72
|
// ---------------------------------------------------------------------------
|
|
69
73
|
|
|
70
74
|
export function resolveFillColor(fills?: PenFill[] | string): string {
|
|
71
|
-
if (typeof fills === 'string') return fills
|
|
72
|
-
if (!fills || fills.length === 0) return DEFAULT_FILL
|
|
73
|
-
const first = fills[0]
|
|
74
|
-
if (!first) return DEFAULT_FILL
|
|
75
|
-
if (first.type === 'solid') return first.color
|
|
75
|
+
if (typeof fills === 'string') return fills;
|
|
76
|
+
if (!fills || fills.length === 0) return DEFAULT_FILL;
|
|
77
|
+
const first = fills[0];
|
|
78
|
+
if (!first) return DEFAULT_FILL;
|
|
79
|
+
if (first.type === 'solid') return first.color;
|
|
76
80
|
if (first.type === 'linear_gradient' || first.type === 'radial_gradient') {
|
|
77
|
-
return first.stops[0]?.color ?? DEFAULT_FILL
|
|
81
|
+
return first.stops[0]?.color ?? DEFAULT_FILL;
|
|
78
82
|
}
|
|
79
|
-
return DEFAULT_FILL
|
|
83
|
+
return DEFAULT_FILL;
|
|
80
84
|
}
|
|
81
85
|
|
|
82
86
|
export function resolveStrokeColor(stroke?: PenStroke): string | undefined {
|
|
83
|
-
if (!stroke) return undefined
|
|
84
|
-
if (typeof stroke === 'string') return stroke
|
|
85
|
-
if (typeof stroke.fill === 'string') return stroke.fill
|
|
86
|
-
if (stroke.fill && stroke.fill.length > 0) return resolveFillColor(stroke.fill)
|
|
87
|
-
if ('color' in stroke && typeof (stroke as any).color === 'string') return (stroke as any).color
|
|
88
|
-
return undefined
|
|
87
|
+
if (!stroke) return undefined;
|
|
88
|
+
if (typeof stroke === 'string') return stroke;
|
|
89
|
+
if (typeof stroke.fill === 'string') return stroke.fill;
|
|
90
|
+
if (stroke.fill && stroke.fill.length > 0) return resolveFillColor(stroke.fill);
|
|
91
|
+
if ('color' in stroke && typeof (stroke as any).color === 'string') return (stroke as any).color;
|
|
92
|
+
return undefined;
|
|
89
93
|
}
|
|
90
94
|
|
|
91
95
|
export function resolveStrokeWidth(stroke?: PenStroke): number {
|
|
92
|
-
if (!stroke) return 0
|
|
93
|
-
if (typeof stroke.thickness === 'number') return stroke.thickness
|
|
94
|
-
if (typeof stroke.thickness === 'object' && !Array.isArray(stroke.thickness)) return 0
|
|
95
|
-
return stroke.thickness?.[0] ?? DEFAULT_STROKE_WIDTH
|
|
96
|
+
if (!stroke) return 0;
|
|
97
|
+
if (typeof stroke.thickness === 'number') return stroke.thickness;
|
|
98
|
+
if (typeof stroke.thickness === 'object' && !Array.isArray(stroke.thickness)) return 0;
|
|
99
|
+
return stroke.thickness?.[0] ?? DEFAULT_STROKE_WIDTH;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function hasVisibleStroke(stroke?: PenStroke): boolean {
|
|
103
|
+
return resolveStrokeWidth(stroke) > 0 && !!resolveStrokeColor(stroke);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function shouldUseTransparentFallbackFill(
|
|
107
|
+
fills: PenFill[] | string | undefined,
|
|
108
|
+
stroke?: PenStroke,
|
|
109
|
+
isContainer = false,
|
|
110
|
+
): boolean {
|
|
111
|
+
const hasExplicitFill = typeof fills === 'string' ? fills.length > 0 : !!fills?.length;
|
|
112
|
+
return !hasExplicitFill && (isContainer || hasVisibleStroke(stroke));
|
|
96
113
|
}
|
|
97
114
|
|
|
98
115
|
// ---------------------------------------------------------------------------
|
|
@@ -101,51 +118,59 @@ export function resolveStrokeWidth(stroke?: PenStroke): number {
|
|
|
101
118
|
|
|
102
119
|
/** CJK character range check (for character-level line breaking). */
|
|
103
120
|
function isCJK(ch: string): boolean {
|
|
104
|
-
const c = ch.charCodeAt(0)
|
|
105
|
-
return (
|
|
106
|
-
(c >=
|
|
107
|
-
(c >=
|
|
121
|
+
const c = ch.charCodeAt(0);
|
|
122
|
+
return (
|
|
123
|
+
(c >= 0x4e00 && c <= 0x9fff) ||
|
|
124
|
+
(c >= 0x3400 && c <= 0x4dbf) ||
|
|
125
|
+
(c >= 0x3000 && c <= 0x303f) ||
|
|
126
|
+
(c >= 0xff00 && c <= 0xffef) ||
|
|
127
|
+
(c >= 0x2e80 && c <= 0x2fdf)
|
|
128
|
+
);
|
|
108
129
|
}
|
|
109
130
|
|
|
110
131
|
/** Word-wrap a single line of text, appending wrapped lines to `out`. */
|
|
111
132
|
export function wrapLine(ctx: CanvasRenderingContext2D, text: string, maxW: number, out: string[]) {
|
|
112
|
-
if (ctx.measureText(text).width <= maxW) {
|
|
133
|
+
if (ctx.measureText(text).width <= maxW) {
|
|
134
|
+
out.push(text);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
113
137
|
|
|
114
|
-
let current = ''
|
|
115
|
-
let i = 0
|
|
138
|
+
let current = '';
|
|
139
|
+
let i = 0;
|
|
116
140
|
while (i < text.length) {
|
|
117
|
-
const ch = text[i]
|
|
141
|
+
const ch = text[i];
|
|
118
142
|
if (isCJK(ch)) {
|
|
119
|
-
const test = current + ch
|
|
143
|
+
const test = current + ch;
|
|
120
144
|
if (ctx.measureText(test).width > maxW && current) {
|
|
121
|
-
out.push(current)
|
|
122
|
-
current = ch
|
|
145
|
+
out.push(current);
|
|
146
|
+
current = ch;
|
|
123
147
|
} else {
|
|
124
|
-
current = test
|
|
148
|
+
current = test;
|
|
125
149
|
}
|
|
126
|
-
i
|
|
150
|
+
i++;
|
|
127
151
|
} else if (ch === ' ') {
|
|
128
|
-
const test = current + ch
|
|
152
|
+
const test = current + ch;
|
|
129
153
|
if (ctx.measureText(test).width > maxW && current) {
|
|
130
|
-
out.push(current)
|
|
131
|
-
current = ''
|
|
154
|
+
out.push(current);
|
|
155
|
+
current = '';
|
|
132
156
|
} else {
|
|
133
|
-
current = test
|
|
157
|
+
current = test;
|
|
134
158
|
}
|
|
135
|
-
i
|
|
159
|
+
i++;
|
|
136
160
|
} else {
|
|
137
|
-
let word = ''
|
|
161
|
+
let word = '';
|
|
138
162
|
while (i < text.length && text[i] !== ' ' && !isCJK(text[i])) {
|
|
139
|
-
word += text[i];
|
|
163
|
+
word += text[i];
|
|
164
|
+
i++;
|
|
140
165
|
}
|
|
141
|
-
const test = current + word
|
|
166
|
+
const test = current + word;
|
|
142
167
|
if (ctx.measureText(test).width > maxW && current) {
|
|
143
|
-
out.push(current)
|
|
144
|
-
current = word
|
|
168
|
+
out.push(current);
|
|
169
|
+
current = word;
|
|
145
170
|
} else {
|
|
146
|
-
current = test
|
|
171
|
+
current = test;
|
|
147
172
|
}
|
|
148
173
|
}
|
|
149
174
|
}
|
|
150
|
-
if (current) out.push(current)
|
|
175
|
+
if (current) out.push(current);
|
|
151
176
|
}
|