@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.
- package/dist/ast/types.d.ts +403 -0
- package/dist/ast/types.js +33 -0
- package/dist/compiler/chunk-splitter.d.ts +98 -0
- package/dist/compiler/chunk-splitter.js +361 -0
- package/dist/index.d.ts +55 -0
- package/dist/index.js +17 -0
- package/dist/rasterizer/__tests__/error-distribution.test.d.ts +7 -0
- package/dist/rasterizer/__tests__/error-distribution.test.js +322 -0
- package/dist/rasterizer/canvas-mapper.d.ts +280 -0
- package/dist/rasterizer/canvas-mapper.js +414 -0
- package/dist/rasterizer/error-distribution.d.ts +143 -0
- package/dist/rasterizer/error-distribution.js +231 -0
- package/dist/rasterizer/gradient-mapper.d.ts +223 -0
- package/dist/rasterizer/gradient-mapper.js +352 -0
- package/dist/rasterizer/topology-rounding.d.ts +151 -0
- package/dist/rasterizer/topology-rounding.js +347 -0
- package/dist/runtime/__tests__/event-backpressure.test.d.ts +10 -0
- package/dist/runtime/__tests__/event-backpressure.test.js +190 -0
- package/dist/runtime/event-backpressure.d.ts +393 -0
- package/dist/runtime/event-backpressure.js +458 -0
- package/dist/runtime/render-loop.d.ts +277 -0
- package/dist/runtime/render-loop.js +435 -0
- package/dist/runtime/wasm-resource-manager.d.ts +122 -0
- package/dist/runtime/wasm-resource-manager.js +253 -0
- package/dist/runtime/wgpu-renderer-adapter.d.ts +168 -0
- package/dist/runtime/wgpu-renderer-adapter.js +230 -0
- package/dist/semantic/__tests__/semantic-translator.test.d.ts +4 -0
- package/dist/semantic/__tests__/semantic-translator.test.js +203 -0
- package/dist/semantic/semantic-translator.d.ts +229 -0
- package/dist/semantic/semantic-translator.js +398 -0
- package/package.json +28 -0
- package/playwright-report/data/0bafe4e0863f0e244bba68a838f73241f8f2efaa.md +226 -0
- package/playwright-report/data/9281aca8abfb06c6cecb35d5ddd13d61f8c752d8.md +226 -0
- package/playwright-report/index.html +90 -0
- package/playwright.config.ts +160 -0
- package/screenshot-chrome.png +0 -0
- package/screenshots/visual-demo-verification.png +0 -0
- package/screenshots/visual-demo.png +0 -0
- package/src/ast/types.ts +473 -0
- package/src/compiler/chunk-splitter.ts +534 -0
- package/src/index.ts +62 -0
- package/src/rasterizer/__tests__/error-distribution.test.ts +382 -0
- package/src/rasterizer/canvas-mapper.ts +677 -0
- package/src/rasterizer/error-distribution.ts +344 -0
- package/src/rasterizer/gradient-mapper.ts +563 -0
- package/src/rasterizer/topology-rounding.ts +499 -0
- package/src/runtime/__tests__/event-backpressure.test.ts +254 -0
- package/src/runtime/event-backpressure.ts +622 -0
- package/src/runtime/render-loop.ts +660 -0
- package/src/runtime/wasm-resource-manager.ts +349 -0
- package/src/runtime/wgpu-renderer-adapter.ts +318 -0
- package/src/semantic/__tests__/semantic-translator.test.ts +263 -0
- package/src/semantic/semantic-translator.ts +637 -0
- package/test-results/.last-run.json +4 -0
- package/tests/e2e/async-race.spec.ts +612 -0
- package/tests/e2e/bilayer-sync.spec.ts +405 -0
- package/tests/e2e/failures/.gitkeep +0 -0
- package/tests/e2e/fullstack.spec.ts +681 -0
- package/tests/e2e/g1-continuity.spec.ts +703 -0
- package/tests/e2e/golden/.gitkeep +0 -0
- package/tests/e2e/golden/conic-color-wheel.raw +0 -0
- package/tests/e2e/golden/conic-color-wheel.sha256 +1 -0
- package/tests/e2e/golden/conic-rotated.raw +0 -0
- package/tests/e2e/golden/conic-rotated.sha256 +1 -0
- package/tests/e2e/golden/linear-45deg.raw +0 -0
- package/tests/e2e/golden/linear-45deg.sha256 +1 -0
- package/tests/e2e/golden/linear-horizontal.raw +0 -0
- package/tests/e2e/golden/linear-horizontal.sha256 +1 -0
- package/tests/e2e/golden/linear-multi-stop.raw +0 -0
- package/tests/e2e/golden/linear-multi-stop.sha256 +1 -0
- package/tests/e2e/golden/radial-circle-center.raw +0 -0
- package/tests/e2e/golden/radial-circle-center.sha256 +1 -0
- package/tests/e2e/golden/radial-offset.raw +0 -0
- package/tests/e2e/golden/radial-offset.sha256 +1 -0
- package/tests/e2e/golden/tile-mirror.raw +0 -0
- package/tests/e2e/golden/tile-mirror.sha256 +1 -0
- package/tests/e2e/golden/tile-repeat.raw +0 -0
- package/tests/e2e/golden/tile-repeat.sha256 +1 -0
- package/tests/e2e/gradient-animation.spec.ts +606 -0
- package/tests/e2e/memory-stability.spec.ts +396 -0
- package/tests/e2e/path-topology.spec.ts +674 -0
- package/tests/e2e/performance-profile.spec.ts +501 -0
- package/tests/e2e/screenshot.spec.ts +60 -0
- package/tests/e2e/test-harness.html +1005 -0
- package/tests/e2e/text-layout.spec.ts +451 -0
- package/tests/e2e/visual-demo.html +340 -0
- package/tests/e2e/visual-regression.spec.ts +335 -0
- package/tsconfig.json +12 -0
- 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
|
+
};
|