@viewscript/renderer 0.1.0-202605140639

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.
Files changed (89) hide show
  1. package/dist/ast/types.d.ts +403 -0
  2. package/dist/ast/types.js +33 -0
  3. package/dist/compiler/chunk-splitter.d.ts +98 -0
  4. package/dist/compiler/chunk-splitter.js +361 -0
  5. package/dist/index.d.ts +55 -0
  6. package/dist/index.js +17 -0
  7. package/dist/rasterizer/__tests__/error-distribution.test.d.ts +7 -0
  8. package/dist/rasterizer/__tests__/error-distribution.test.js +322 -0
  9. package/dist/rasterizer/canvas-mapper.d.ts +280 -0
  10. package/dist/rasterizer/canvas-mapper.js +414 -0
  11. package/dist/rasterizer/error-distribution.d.ts +143 -0
  12. package/dist/rasterizer/error-distribution.js +231 -0
  13. package/dist/rasterizer/gradient-mapper.d.ts +223 -0
  14. package/dist/rasterizer/gradient-mapper.js +352 -0
  15. package/dist/rasterizer/topology-rounding.d.ts +151 -0
  16. package/dist/rasterizer/topology-rounding.js +347 -0
  17. package/dist/runtime/__tests__/event-backpressure.test.d.ts +10 -0
  18. package/dist/runtime/__tests__/event-backpressure.test.js +190 -0
  19. package/dist/runtime/event-backpressure.d.ts +393 -0
  20. package/dist/runtime/event-backpressure.js +458 -0
  21. package/dist/runtime/render-loop.d.ts +277 -0
  22. package/dist/runtime/render-loop.js +435 -0
  23. package/dist/runtime/wasm-resource-manager.d.ts +122 -0
  24. package/dist/runtime/wasm-resource-manager.js +253 -0
  25. package/dist/runtime/wgpu-renderer-adapter.d.ts +168 -0
  26. package/dist/runtime/wgpu-renderer-adapter.js +230 -0
  27. package/dist/semantic/__tests__/semantic-translator.test.d.ts +4 -0
  28. package/dist/semantic/__tests__/semantic-translator.test.js +203 -0
  29. package/dist/semantic/semantic-translator.d.ts +229 -0
  30. package/dist/semantic/semantic-translator.js +398 -0
  31. package/package.json +28 -0
  32. package/playwright-report/data/0bafe4e0863f0e244bba68a838f73241f8f2efaa.md +226 -0
  33. package/playwright-report/data/9281aca8abfb06c6cecb35d5ddd13d61f8c752d8.md +226 -0
  34. package/playwright-report/index.html +90 -0
  35. package/playwright.config.ts +160 -0
  36. package/screenshot-chrome.png +0 -0
  37. package/screenshots/visual-demo-verification.png +0 -0
  38. package/screenshots/visual-demo.png +0 -0
  39. package/src/ast/types.ts +473 -0
  40. package/src/compiler/chunk-splitter.ts +534 -0
  41. package/src/index.ts +62 -0
  42. package/src/rasterizer/__tests__/error-distribution.test.ts +382 -0
  43. package/src/rasterizer/canvas-mapper.ts +677 -0
  44. package/src/rasterizer/error-distribution.ts +344 -0
  45. package/src/rasterizer/gradient-mapper.ts +563 -0
  46. package/src/rasterizer/topology-rounding.ts +499 -0
  47. package/src/runtime/__tests__/event-backpressure.test.ts +254 -0
  48. package/src/runtime/event-backpressure.ts +622 -0
  49. package/src/runtime/render-loop.ts +660 -0
  50. package/src/runtime/wasm-resource-manager.ts +349 -0
  51. package/src/runtime/wgpu-renderer-adapter.ts +318 -0
  52. package/src/semantic/__tests__/semantic-translator.test.ts +263 -0
  53. package/src/semantic/semantic-translator.ts +637 -0
  54. package/test-results/.last-run.json +4 -0
  55. package/tests/e2e/async-race.spec.ts +612 -0
  56. package/tests/e2e/bilayer-sync.spec.ts +405 -0
  57. package/tests/e2e/failures/.gitkeep +0 -0
  58. package/tests/e2e/fullstack.spec.ts +681 -0
  59. package/tests/e2e/g1-continuity.spec.ts +703 -0
  60. package/tests/e2e/golden/.gitkeep +0 -0
  61. package/tests/e2e/golden/conic-color-wheel.raw +0 -0
  62. package/tests/e2e/golden/conic-color-wheel.sha256 +1 -0
  63. package/tests/e2e/golden/conic-rotated.raw +0 -0
  64. package/tests/e2e/golden/conic-rotated.sha256 +1 -0
  65. package/tests/e2e/golden/linear-45deg.raw +0 -0
  66. package/tests/e2e/golden/linear-45deg.sha256 +1 -0
  67. package/tests/e2e/golden/linear-horizontal.raw +0 -0
  68. package/tests/e2e/golden/linear-horizontal.sha256 +1 -0
  69. package/tests/e2e/golden/linear-multi-stop.raw +0 -0
  70. package/tests/e2e/golden/linear-multi-stop.sha256 +1 -0
  71. package/tests/e2e/golden/radial-circle-center.raw +0 -0
  72. package/tests/e2e/golden/radial-circle-center.sha256 +1 -0
  73. package/tests/e2e/golden/radial-offset.raw +0 -0
  74. package/tests/e2e/golden/radial-offset.sha256 +1 -0
  75. package/tests/e2e/golden/tile-mirror.raw +0 -0
  76. package/tests/e2e/golden/tile-mirror.sha256 +1 -0
  77. package/tests/e2e/golden/tile-repeat.raw +0 -0
  78. package/tests/e2e/golden/tile-repeat.sha256 +1 -0
  79. package/tests/e2e/gradient-animation.spec.ts +606 -0
  80. package/tests/e2e/memory-stability.spec.ts +396 -0
  81. package/tests/e2e/path-topology.spec.ts +674 -0
  82. package/tests/e2e/performance-profile.spec.ts +501 -0
  83. package/tests/e2e/screenshot.spec.ts +60 -0
  84. package/tests/e2e/test-harness.html +1005 -0
  85. package/tests/e2e/text-layout.spec.ts +451 -0
  86. package/tests/e2e/visual-demo.html +340 -0
  87. package/tests/e2e/visual-regression.spec.ts +335 -0
  88. package/tsconfig.json +12 -0
  89. package/vitest.config.ts +8 -0
@@ -0,0 +1,563 @@
1
+ /**
2
+ * Gradient Shader Mapper: P-Dimension to GPU Shaders
3
+ *
4
+ * This module maps P-dimension gradient entities to GPU shader objects.
5
+ * It handles the critical transition from exact rational arithmetic to GPU-compatible
6
+ * floating-point representation while preserving visual fidelity.
7
+ *
8
+ * ## Architecture
9
+ *
10
+ * ```
11
+ * P-Dimension (Exact) GPU (Float)
12
+ * ─────────────────────────────────────────────────────────────
13
+ *
14
+ * LinearGradient { GpuShaderBackend.Shader
15
+ * start: Rational(1, 3) ────────────▶ MakeLinearGradient(
16
+ * end: Rational(2, 3) [0.333..., 0.666...],
17
+ * stops: [ colors: Float32Array,
18
+ * ColorStop(r=255, ...) positions: Float32Array
19
+ * ] )
20
+ * }
21
+ *
22
+ * ┌─────────────────────────────────────────────────────────────┐
23
+ * │ CRITICAL: Topology-preserving rounding at this boundary │
24
+ * │ │
25
+ * │ - Color channels [0, 255] → [0.0, 1.0] with clamping │
26
+ * │ - Position values [0, 1] stay exact, no interpolation │
27
+ * │ - Control points use same rounding as other coordinates │
28
+ * └─────────────────────────────────────────────────────────────┘
29
+ * ```
30
+ *
31
+ * ## Usage
32
+ *
33
+ * ```typescript
34
+ * const shader = mapLinearGradientToShader(ck, gradient, bounds, dpr);
35
+ * paint.setShader(shader);
36
+ * canvas.drawRect(bounds, paint);
37
+ * ```
38
+ */
39
+
40
+ import type { Rational, RasterBounds } from '../ast/types';
41
+
42
+ // =============================================================================
43
+ // Types
44
+ // =============================================================================
45
+
46
+ /** GPU shader backend interface (minimal subset needed for gradients) */
47
+ export interface GpuShaderBackend {
48
+ Shader: {
49
+ MakeLinearGradient(
50
+ start: Float32Array,
51
+ end: Float32Array,
52
+ colors: Float32Array,
53
+ positions: Float32Array | null,
54
+ mode: number,
55
+ localMatrix?: Float32Array,
56
+ ): ShaderInstance;
57
+
58
+ MakeTwoPointConicalGradient(
59
+ start: Float32Array,
60
+ startRadius: number,
61
+ end: Float32Array,
62
+ endRadius: number,
63
+ colors: Float32Array,
64
+ positions: Float32Array | null,
65
+ mode: number,
66
+ localMatrix?: Float32Array,
67
+ ): ShaderInstance;
68
+
69
+ MakeSweepGradient(
70
+ cx: number,
71
+ cy: number,
72
+ colors: Float32Array,
73
+ positions: Float32Array | null,
74
+ mode: number,
75
+ startAngle: number,
76
+ endAngle: number,
77
+ localMatrix?: Float32Array,
78
+ ): ShaderInstance;
79
+ };
80
+
81
+ TileMode: {
82
+ Clamp: number;
83
+ Repeat: number;
84
+ Mirror: number;
85
+ Decal: number;
86
+ };
87
+ }
88
+
89
+ export interface ShaderInstance {
90
+ delete(): void;
91
+ }
92
+
93
+ /**
94
+ * P-dimension color stop with exact rational values.
95
+ */
96
+ export interface PColorStop {
97
+ /** Entity ID */
98
+ id: number;
99
+ /** Red channel [0, 255] (rational) */
100
+ r: Rational;
101
+ /** Green channel [0, 255] (rational) */
102
+ g: Rational;
103
+ /** Blue channel [0, 255] (rational) */
104
+ b: Rational;
105
+ /** Alpha channel [0, 1] (rational) */
106
+ a: Rational;
107
+ /** Position along gradient [0, 1] (rational) */
108
+ position: Rational;
109
+ }
110
+
111
+ /**
112
+ * P-dimension control point with exact coordinates.
113
+ */
114
+ export interface PControlPoint {
115
+ id: number;
116
+ x: Rational;
117
+ y: Rational;
118
+ }
119
+
120
+ /**
121
+ * P-dimension linear gradient definition.
122
+ */
123
+ export interface PLinearGradient {
124
+ id: number;
125
+ start: PControlPoint;
126
+ end: PControlPoint;
127
+ stops: PColorStop[];
128
+ tileMode: 'clamp' | 'repeat' | 'mirror' | 'decal';
129
+ }
130
+
131
+ /**
132
+ * P-dimension radial gradient definition.
133
+ */
134
+ export interface PRadialGradient {
135
+ id: number;
136
+ center: PControlPoint;
137
+ radiusX: Rational;
138
+ radiusY: Rational;
139
+ focalPoint?: PControlPoint;
140
+ focalRadius?: Rational;
141
+ stops: PColorStop[];
142
+ tileMode: 'clamp' | 'repeat' | 'mirror' | 'decal';
143
+ }
144
+
145
+ /**
146
+ * P-dimension conic (sweep) gradient definition.
147
+ */
148
+ export interface PConicGradient {
149
+ id: number;
150
+ center: PControlPoint;
151
+ /** Rotation offset in degrees (rational) */
152
+ rotation: Rational;
153
+ /** Start angle in degrees (rational) */
154
+ startAngle: Rational;
155
+ /** End angle in degrees (rational) */
156
+ endAngle: Rational;
157
+ stops: PColorStop[];
158
+ }
159
+
160
+ // =============================================================================
161
+ // Core Mapping Functions
162
+ // =============================================================================
163
+
164
+ /**
165
+ * Map a P-dimension linear gradient to a GPU shader.
166
+ *
167
+ * @param ck - GPU shader backend instance
168
+ * @param gradient - P-dimension linear gradient definition
169
+ * @param bounds - Rasterized bounds for coordinate transformation
170
+ * @param devicePixelRatio - Device pixel ratio for coordinate scaling
171
+ * @returns GPU shader instance (caller must call delete() when done)
172
+ */
173
+ export function mapLinearGradientToShader(
174
+ ck: GpuShaderBackend,
175
+ gradient: PLinearGradient,
176
+ bounds: RasterBounds,
177
+ devicePixelRatio: number,
178
+ ): ShaderInstance {
179
+ // Convert control points to device coordinates
180
+ const startX = rationalToFloat(gradient.start.x) * devicePixelRatio;
181
+ const startY = rationalToFloat(gradient.start.y) * devicePixelRatio;
182
+ const endX = rationalToFloat(gradient.end.x) * devicePixelRatio;
183
+ const endY = rationalToFloat(gradient.end.y) * devicePixelRatio;
184
+
185
+ // Sort stops by position (required by GPU shader backend)
186
+ const sortedStops = [...gradient.stops].sort(
187
+ (a, b) => rationalToFloat(a.position) - rationalToFloat(b.position),
188
+ );
189
+
190
+ // Convert colors to Float32Array (RGBA format, each channel 0-1)
191
+ const colors = colorStopsToFloat32Array(sortedStops);
192
+ const positions = positionsToFloat32Array(sortedStops);
193
+
194
+ // Map tile mode
195
+ const tileMode = mapTileMode(ck, gradient.tileMode);
196
+
197
+ return ck.Shader.MakeLinearGradient(
198
+ new Float32Array([startX, startY]),
199
+ new Float32Array([endX, endY]),
200
+ colors,
201
+ positions,
202
+ tileMode,
203
+ );
204
+ }
205
+
206
+ /**
207
+ * Map a P-dimension radial gradient to a GPU shader.
208
+ *
209
+ * The GPU backend uses two-point conical gradients which can express:
210
+ * - Circle gradients (same center, different radii)
211
+ * - Focal gradients (different centers)
212
+ *
213
+ * @param ck - GPU shader backend instance
214
+ * @param gradient - P-dimension radial gradient definition
215
+ * @param bounds - Rasterized bounds for coordinate transformation
216
+ * @param devicePixelRatio - Device pixel ratio for coordinate scaling
217
+ * @returns GPU shader instance
218
+ */
219
+ export function mapRadialGradientToShader(
220
+ ck: GpuShaderBackend,
221
+ gradient: PRadialGradient,
222
+ bounds: RasterBounds,
223
+ devicePixelRatio: number,
224
+ ): ShaderInstance {
225
+ // Convert center to device coordinates
226
+ const centerX = rationalToFloat(gradient.center.x) * devicePixelRatio;
227
+ const centerY = rationalToFloat(gradient.center.y) * devicePixelRatio;
228
+
229
+ // For elliptical gradients, we use the larger radius and scale
230
+ // For now, use average of radiusX and radiusY as the radius
231
+ const radiusX = rationalToFloat(gradient.radiusX) * devicePixelRatio;
232
+ const radiusY = rationalToFloat(gradient.radiusY) * devicePixelRatio;
233
+ const radius = Math.max(radiusX, radiusY);
234
+
235
+ // Handle focal point (if specified)
236
+ let focalX = centerX;
237
+ let focalY = centerY;
238
+ let focalR = 0;
239
+
240
+ if (gradient.focalPoint) {
241
+ focalX = rationalToFloat(gradient.focalPoint.x) * devicePixelRatio;
242
+ focalY = rationalToFloat(gradient.focalPoint.y) * devicePixelRatio;
243
+ }
244
+ if (gradient.focalRadius) {
245
+ focalR = rationalToFloat(gradient.focalRadius) * devicePixelRatio;
246
+ }
247
+
248
+ // Sort and convert stops
249
+ const sortedStops = [...gradient.stops].sort(
250
+ (a, b) => rationalToFloat(a.position) - rationalToFloat(b.position),
251
+ );
252
+ const colors = colorStopsToFloat32Array(sortedStops);
253
+ const positions = positionsToFloat32Array(sortedStops);
254
+
255
+ const tileMode = mapTileMode(ck, gradient.tileMode);
256
+
257
+ // Two-point conical: inner circle to outer circle
258
+ return ck.Shader.MakeTwoPointConicalGradient(
259
+ new Float32Array([focalX, focalY]),
260
+ focalR,
261
+ new Float32Array([centerX, centerY]),
262
+ radius,
263
+ colors,
264
+ positions,
265
+ tileMode,
266
+ );
267
+ }
268
+
269
+ /**
270
+ * Map a P-dimension conic (sweep) gradient to a GPU shader.
271
+ *
272
+ * @param ck - GPU shader backend instance
273
+ * @param gradient - P-dimension conic gradient definition
274
+ * @param bounds - Rasterized bounds for coordinate transformation
275
+ * @param devicePixelRatio - Device pixel ratio for coordinate scaling
276
+ * @returns GPU shader instance
277
+ */
278
+ export function mapConicGradientToShader(
279
+ ck: GpuShaderBackend,
280
+ gradient: PConicGradient,
281
+ bounds: RasterBounds,
282
+ devicePixelRatio: number,
283
+ ): ShaderInstance {
284
+ // Convert center to device coordinates
285
+ const centerX = rationalToFloat(gradient.center.x) * devicePixelRatio;
286
+ const centerY = rationalToFloat(gradient.center.y) * devicePixelRatio;
287
+
288
+ // Convert angles (GPU shader expects degrees)
289
+ const startAngle = rationalToFloat(gradient.startAngle) + rationalToFloat(gradient.rotation);
290
+ const endAngle = rationalToFloat(gradient.endAngle) + rationalToFloat(gradient.rotation);
291
+
292
+ // Sort and convert stops
293
+ const sortedStops = [...gradient.stops].sort(
294
+ (a, b) => rationalToFloat(a.position) - rationalToFloat(b.position),
295
+ );
296
+ const colors = colorStopsToFloat32Array(sortedStops);
297
+ const positions = positionsToFloat32Array(sortedStops);
298
+
299
+ // Sweep gradient always uses Clamp-like behavior
300
+ return ck.Shader.MakeSweepGradient(
301
+ centerX,
302
+ centerY,
303
+ colors,
304
+ positions,
305
+ ck.TileMode.Clamp,
306
+ startAngle,
307
+ endAngle,
308
+ );
309
+ }
310
+
311
+ // =============================================================================
312
+ // Conversion Utilities
313
+ // =============================================================================
314
+
315
+ /**
316
+ * Convert a rational number to a floating-point number.
317
+ *
318
+ * This is the critical boundary where exact arithmetic meets GPU floats.
319
+ * The conversion is straightforward division, but precision loss is
320
+ * unavoidable and acceptable at this layer.
321
+ */
322
+ export function rationalToFloat(r: Rational): number {
323
+ // Handle bigint to number conversion
324
+ // For very large rationals, this may lose precision
325
+ return Number(r.numerator) / Number(r.denominator);
326
+ }
327
+
328
+ /**
329
+ * Convert P-dimension color stops to a Float32Array of RGBA values.
330
+ *
331
+ * GPU shader expects colors in RGBA order, with each channel in [0, 1].
332
+ * P-dimension stores RGB in [0, 255] and Alpha in [0, 1].
333
+ *
334
+ * ## Topology-Preserving Rounding (Clamping)
335
+ *
336
+ * Color values are clamped to [0, 1] to ensure GPU-valid input.
337
+ * This preserves the topological ordering of colors even if the
338
+ * original rational values were slightly out of range.
339
+ */
340
+ export function colorStopsToFloat32Array(stops: PColorStop[]): Float32Array {
341
+ const array = new Float32Array(stops.length * 4);
342
+
343
+ for (let i = 0; i < stops.length; i++) {
344
+ const stop = stops[i];
345
+ const offset = i * 4;
346
+
347
+ // Convert [0, 255] rational to [0, 1] float with clamping
348
+ array[offset + 0] = clamp01(rationalToFloat(stop.r) / 255);
349
+ array[offset + 1] = clamp01(rationalToFloat(stop.g) / 255);
350
+ array[offset + 2] = clamp01(rationalToFloat(stop.b) / 255);
351
+
352
+ // Alpha is already in [0, 1] in P-dimension
353
+ array[offset + 3] = clamp01(rationalToFloat(stop.a));
354
+ }
355
+
356
+ return array;
357
+ }
358
+
359
+ /**
360
+ * Convert P-dimension color stop positions to a Float32Array.
361
+ *
362
+ * Positions are kept as-is (already in [0, 1] in P-dimension),
363
+ * with clamping for safety.
364
+ */
365
+ export function positionsToFloat32Array(stops: PColorStop[]): Float32Array {
366
+ const array = new Float32Array(stops.length);
367
+
368
+ for (let i = 0; i < stops.length; i++) {
369
+ array[i] = clamp01(rationalToFloat(stops[i].position));
370
+ }
371
+
372
+ return array;
373
+ }
374
+
375
+ /**
376
+ * Clamp a value to [0, 1] range.
377
+ *
378
+ * This is the "topology-preserving rounding" for color values:
379
+ * it ensures the value is valid for GPU while preserving ordering.
380
+ */
381
+ function clamp01(value: number): number {
382
+ return Math.max(0, Math.min(1, value));
383
+ }
384
+
385
+ /**
386
+ * Map P-dimension tile mode to GPU tile mode constant.
387
+ */
388
+ function mapTileMode(
389
+ ck: GpuShaderBackend,
390
+ mode: 'clamp' | 'repeat' | 'mirror' | 'decal',
391
+ ): number {
392
+ switch (mode) {
393
+ case 'clamp':
394
+ return ck.TileMode.Clamp;
395
+ case 'repeat':
396
+ return ck.TileMode.Repeat;
397
+ case 'mirror':
398
+ return ck.TileMode.Mirror;
399
+ case 'decal':
400
+ return ck.TileMode.Decal;
401
+ default:
402
+ return ck.TileMode.Clamp;
403
+ }
404
+ }
405
+
406
+ // =============================================================================
407
+ // Factory Function for Gradient FillStyle
408
+ // =============================================================================
409
+
410
+ /**
411
+ * Create a GPU shader from a FillStyle gradient definition.
412
+ *
413
+ * This is a higher-level factory that integrates with the existing
414
+ * FillStyle type from the AST.
415
+ */
416
+ export function createGradientShader(
417
+ ck: GpuShaderBackend,
418
+ fillType: 'linear-gradient' | 'radial-gradient',
419
+ stops: Array<{ offset: Rational; color: string }>,
420
+ bounds: RasterBounds,
421
+ devicePixelRatio: number,
422
+ ): ShaderInstance | null {
423
+ // Convert simplified FillStyle stops to PColorStop format
424
+ const pStops: PColorStop[] = stops.map((stop, index) => {
425
+ const rgba = parseColorString(stop.color);
426
+ return {
427
+ id: index,
428
+ r: { numerator: BigInt(rgba.r), denominator: 1n },
429
+ g: { numerator: BigInt(rgba.g), denominator: 1n },
430
+ b: { numerator: BigInt(rgba.b), denominator: 1n },
431
+ a: { numerator: BigInt(Math.round(rgba.a * 1000)), denominator: 1000n },
432
+ position: stop.offset,
433
+ };
434
+ });
435
+
436
+ if (fillType === 'linear-gradient') {
437
+ // Default linear gradient: top to bottom
438
+ const linearGradient: PLinearGradient = {
439
+ id: 0,
440
+ start: {
441
+ id: 0,
442
+ x: { numerator: BigInt(Math.round(bounds.x)), denominator: 1n },
443
+ y: { numerator: BigInt(Math.round(bounds.y)), denominator: 1n },
444
+ },
445
+ end: {
446
+ id: 0,
447
+ x: { numerator: BigInt(Math.round(bounds.x)), denominator: 1n },
448
+ y: { numerator: BigInt(Math.round(bounds.y + bounds.height)), denominator: 1n },
449
+ },
450
+ stops: pStops,
451
+ tileMode: 'clamp',
452
+ };
453
+ return mapLinearGradientToShader(ck, linearGradient, bounds, devicePixelRatio);
454
+ } else if (fillType === 'radial-gradient') {
455
+ // Default radial gradient: center of bounds, radius to edge
456
+ const cx = bounds.x + bounds.width / 2;
457
+ const cy = bounds.y + bounds.height / 2;
458
+ const radius = Math.max(bounds.width, bounds.height) / 2;
459
+
460
+ const radialGradient: PRadialGradient = {
461
+ id: 0,
462
+ center: {
463
+ id: 0,
464
+ x: { numerator: BigInt(Math.round(cx * 1000)), denominator: 1000n },
465
+ y: { numerator: BigInt(Math.round(cy * 1000)), denominator: 1000n },
466
+ },
467
+ radiusX: { numerator: BigInt(Math.round(radius * 1000)), denominator: 1000n },
468
+ radiusY: { numerator: BigInt(Math.round(radius * 1000)), denominator: 1000n },
469
+ stops: pStops,
470
+ tileMode: 'clamp',
471
+ };
472
+ return mapRadialGradientToShader(ck, radialGradient, bounds, devicePixelRatio);
473
+ }
474
+
475
+ return null;
476
+ }
477
+
478
+ /**
479
+ * Parse a CSS color string to RGBA values.
480
+ *
481
+ * Supports:
482
+ * - Hex: #RGB, #RGBA, #RRGGBB, #RRGGBBAA
483
+ * - Named colors (basic set)
484
+ */
485
+ function parseColorString(color: string): { r: number; g: number; b: number; a: number } {
486
+ // Named colors (basic set)
487
+ const namedColors: Record<string, { r: number; g: number; b: number }> = {
488
+ black: { r: 0, g: 0, b: 0 },
489
+ white: { r: 255, g: 255, b: 255 },
490
+ red: { r: 255, g: 0, b: 0 },
491
+ green: { r: 0, g: 128, b: 0 },
492
+ blue: { r: 0, g: 0, b: 255 },
493
+ yellow: { r: 255, g: 255, b: 0 },
494
+ cyan: { r: 0, g: 255, b: 255 },
495
+ magenta: { r: 255, g: 0, b: 255 },
496
+ transparent: { r: 0, g: 0, b: 0 },
497
+ };
498
+
499
+ const lower = color.toLowerCase().trim();
500
+
501
+ if (lower === 'transparent') {
502
+ return { r: 0, g: 0, b: 0, a: 0 };
503
+ }
504
+
505
+ if (namedColors[lower]) {
506
+ return { ...namedColors[lower], a: 1 };
507
+ }
508
+
509
+ // Hex parsing
510
+ if (lower.startsWith('#')) {
511
+ const hex = lower.slice(1);
512
+
513
+ if (hex.length === 3) {
514
+ // #RGB
515
+ return {
516
+ r: parseInt(hex[0] + hex[0], 16),
517
+ g: parseInt(hex[1] + hex[1], 16),
518
+ b: parseInt(hex[2] + hex[2], 16),
519
+ a: 1,
520
+ };
521
+ } else if (hex.length === 4) {
522
+ // #RGBA
523
+ return {
524
+ r: parseInt(hex[0] + hex[0], 16),
525
+ g: parseInt(hex[1] + hex[1], 16),
526
+ b: parseInt(hex[2] + hex[2], 16),
527
+ a: parseInt(hex[3] + hex[3], 16) / 255,
528
+ };
529
+ } else if (hex.length === 6) {
530
+ // #RRGGBB
531
+ return {
532
+ r: parseInt(hex.slice(0, 2), 16),
533
+ g: parseInt(hex.slice(2, 4), 16),
534
+ b: parseInt(hex.slice(4, 6), 16),
535
+ a: 1,
536
+ };
537
+ } else if (hex.length === 8) {
538
+ // #RRGGBBAA
539
+ return {
540
+ r: parseInt(hex.slice(0, 2), 16),
541
+ g: parseInt(hex.slice(2, 4), 16),
542
+ b: parseInt(hex.slice(4, 6), 16),
543
+ a: parseInt(hex.slice(6, 8), 16) / 255,
544
+ };
545
+ }
546
+ }
547
+
548
+ // Fallback: black
549
+ return { r: 0, g: 0, b: 0, a: 1 };
550
+ }
551
+
552
+ // =============================================================================
553
+ // Exports for Testing
554
+ // =============================================================================
555
+
556
+ export const _internals = {
557
+ rationalToFloat,
558
+ clamp01,
559
+ colorStopsToFloat32Array,
560
+ positionsToFloat32Array,
561
+ mapTileMode,
562
+ parseColorString,
563
+ };