@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.
@@ -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(cr: number | [number, number, number, number] | undefined): number {
55
- if (cr === undefined) return 0
56
- if (typeof cr === 'number') return cr
57
- return cr[0]
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(cr: number | [number, number, number, number] | undefined): [number, number, number, number] {
61
- if (cr === undefined) return [0, 0, 0, 0]
62
- if (typeof cr === 'number') return [cr, cr, cr, cr]
63
- return cr
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 (c >= 0x4E00 && c <= 0x9FFF) || (c >= 0x3400 && c <= 0x4DBF) ||
106
- (c >= 0x3000 && c <= 0x303F) || (c >= 0xFF00 && c <= 0xFFEF) ||
107
- (c >= 0x2E80 && c <= 0x2FDF)
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) { out.push(text); return }
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]; 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
  }