@zseven-w/pen-renderer 0.5.2 → 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 +277 -0
- 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 +228 -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 -71
- 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 +367 -304
- 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,28 +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
|
|
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;
|
|
75
80
|
if (first.type === 'linear_gradient' || first.type === 'radial_gradient') {
|
|
76
|
-
return first.stops[0]?.color ?? DEFAULT_FILL
|
|
81
|
+
return first.stops[0]?.color ?? DEFAULT_FILL;
|
|
77
82
|
}
|
|
78
|
-
return DEFAULT_FILL
|
|
83
|
+
return DEFAULT_FILL;
|
|
79
84
|
}
|
|
80
85
|
|
|
81
86
|
export function resolveStrokeColor(stroke?: PenStroke): string | undefined {
|
|
82
|
-
if (!stroke) return undefined
|
|
83
|
-
if (typeof stroke
|
|
84
|
-
if (
|
|
85
|
-
return
|
|
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;
|
|
86
93
|
}
|
|
87
94
|
|
|
88
95
|
export function resolveStrokeWidth(stroke?: PenStroke): number {
|
|
89
|
-
if (!stroke) return 0
|
|
90
|
-
if (typeof stroke.thickness === 'number') return stroke.thickness
|
|
91
|
-
if (typeof stroke.thickness === 'object' && !Array.isArray(stroke.thickness)) return 0
|
|
92
|
-
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));
|
|
93
113
|
}
|
|
94
114
|
|
|
95
115
|
// ---------------------------------------------------------------------------
|
|
@@ -98,51 +118,59 @@ export function resolveStrokeWidth(stroke?: PenStroke): number {
|
|
|
98
118
|
|
|
99
119
|
/** CJK character range check (for character-level line breaking). */
|
|
100
120
|
function isCJK(ch: string): boolean {
|
|
101
|
-
const c = ch.charCodeAt(0)
|
|
102
|
-
return (
|
|
103
|
-
(c >=
|
|
104
|
-
(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
|
+
);
|
|
105
129
|
}
|
|
106
130
|
|
|
107
131
|
/** Word-wrap a single line of text, appending wrapped lines to `out`. */
|
|
108
132
|
export function wrapLine(ctx: CanvasRenderingContext2D, text: string, maxW: number, out: string[]) {
|
|
109
|
-
if (ctx.measureText(text).width <= maxW) {
|
|
133
|
+
if (ctx.measureText(text).width <= maxW) {
|
|
134
|
+
out.push(text);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
110
137
|
|
|
111
|
-
let current = ''
|
|
112
|
-
let i = 0
|
|
138
|
+
let current = '';
|
|
139
|
+
let i = 0;
|
|
113
140
|
while (i < text.length) {
|
|
114
|
-
const ch = text[i]
|
|
141
|
+
const ch = text[i];
|
|
115
142
|
if (isCJK(ch)) {
|
|
116
|
-
const test = current + ch
|
|
143
|
+
const test = current + ch;
|
|
117
144
|
if (ctx.measureText(test).width > maxW && current) {
|
|
118
|
-
out.push(current)
|
|
119
|
-
current = ch
|
|
145
|
+
out.push(current);
|
|
146
|
+
current = ch;
|
|
120
147
|
} else {
|
|
121
|
-
current = test
|
|
148
|
+
current = test;
|
|
122
149
|
}
|
|
123
|
-
i
|
|
150
|
+
i++;
|
|
124
151
|
} else if (ch === ' ') {
|
|
125
|
-
const test = current + ch
|
|
152
|
+
const test = current + ch;
|
|
126
153
|
if (ctx.measureText(test).width > maxW && current) {
|
|
127
|
-
out.push(current)
|
|
128
|
-
current = ''
|
|
154
|
+
out.push(current);
|
|
155
|
+
current = '';
|
|
129
156
|
} else {
|
|
130
|
-
current = test
|
|
157
|
+
current = test;
|
|
131
158
|
}
|
|
132
|
-
i
|
|
159
|
+
i++;
|
|
133
160
|
} else {
|
|
134
|
-
let word = ''
|
|
161
|
+
let word = '';
|
|
135
162
|
while (i < text.length && text[i] !== ' ' && !isCJK(text[i])) {
|
|
136
|
-
word += text[i];
|
|
163
|
+
word += text[i];
|
|
164
|
+
i++;
|
|
137
165
|
}
|
|
138
|
-
const test = current + word
|
|
166
|
+
const test = current + word;
|
|
139
167
|
if (ctx.measureText(test).width > maxW && current) {
|
|
140
|
-
out.push(current)
|
|
141
|
-
current = word
|
|
168
|
+
out.push(current);
|
|
169
|
+
current = word;
|
|
142
170
|
} else {
|
|
143
|
-
current = test
|
|
171
|
+
current = test;
|
|
144
172
|
}
|
|
145
173
|
}
|
|
146
174
|
}
|
|
147
|
-
if (current) out.push(current)
|
|
175
|
+
if (current) out.push(current);
|
|
148
176
|
}
|