@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,352 @@
|
|
|
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
|
+
// Core Mapping Functions
|
|
41
|
+
// =============================================================================
|
|
42
|
+
/**
|
|
43
|
+
* Map a P-dimension linear gradient to a GPU shader.
|
|
44
|
+
*
|
|
45
|
+
* @param ck - GPU shader backend instance
|
|
46
|
+
* @param gradient - P-dimension linear gradient definition
|
|
47
|
+
* @param bounds - Rasterized bounds for coordinate transformation
|
|
48
|
+
* @param devicePixelRatio - Device pixel ratio for coordinate scaling
|
|
49
|
+
* @returns GPU shader instance (caller must call delete() when done)
|
|
50
|
+
*/
|
|
51
|
+
export function mapLinearGradientToShader(ck, gradient, bounds, devicePixelRatio) {
|
|
52
|
+
// Convert control points to device coordinates
|
|
53
|
+
const startX = rationalToFloat(gradient.start.x) * devicePixelRatio;
|
|
54
|
+
const startY = rationalToFloat(gradient.start.y) * devicePixelRatio;
|
|
55
|
+
const endX = rationalToFloat(gradient.end.x) * devicePixelRatio;
|
|
56
|
+
const endY = rationalToFloat(gradient.end.y) * devicePixelRatio;
|
|
57
|
+
// Sort stops by position (required by GPU shader backend)
|
|
58
|
+
const sortedStops = [...gradient.stops].sort((a, b) => rationalToFloat(a.position) - rationalToFloat(b.position));
|
|
59
|
+
// Convert colors to Float32Array (RGBA format, each channel 0-1)
|
|
60
|
+
const colors = colorStopsToFloat32Array(sortedStops);
|
|
61
|
+
const positions = positionsToFloat32Array(sortedStops);
|
|
62
|
+
// Map tile mode
|
|
63
|
+
const tileMode = mapTileMode(ck, gradient.tileMode);
|
|
64
|
+
return ck.Shader.MakeLinearGradient(new Float32Array([startX, startY]), new Float32Array([endX, endY]), colors, positions, tileMode);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Map a P-dimension radial gradient to a GPU shader.
|
|
68
|
+
*
|
|
69
|
+
* The GPU backend uses two-point conical gradients which can express:
|
|
70
|
+
* - Circle gradients (same center, different radii)
|
|
71
|
+
* - Focal gradients (different centers)
|
|
72
|
+
*
|
|
73
|
+
* @param ck - GPU shader backend instance
|
|
74
|
+
* @param gradient - P-dimension radial gradient definition
|
|
75
|
+
* @param bounds - Rasterized bounds for coordinate transformation
|
|
76
|
+
* @param devicePixelRatio - Device pixel ratio for coordinate scaling
|
|
77
|
+
* @returns GPU shader instance
|
|
78
|
+
*/
|
|
79
|
+
export function mapRadialGradientToShader(ck, gradient, bounds, devicePixelRatio) {
|
|
80
|
+
// Convert center to device coordinates
|
|
81
|
+
const centerX = rationalToFloat(gradient.center.x) * devicePixelRatio;
|
|
82
|
+
const centerY = rationalToFloat(gradient.center.y) * devicePixelRatio;
|
|
83
|
+
// For elliptical gradients, we use the larger radius and scale
|
|
84
|
+
// For now, use average of radiusX and radiusY as the radius
|
|
85
|
+
const radiusX = rationalToFloat(gradient.radiusX) * devicePixelRatio;
|
|
86
|
+
const radiusY = rationalToFloat(gradient.radiusY) * devicePixelRatio;
|
|
87
|
+
const radius = Math.max(radiusX, radiusY);
|
|
88
|
+
// Handle focal point (if specified)
|
|
89
|
+
let focalX = centerX;
|
|
90
|
+
let focalY = centerY;
|
|
91
|
+
let focalR = 0;
|
|
92
|
+
if (gradient.focalPoint) {
|
|
93
|
+
focalX = rationalToFloat(gradient.focalPoint.x) * devicePixelRatio;
|
|
94
|
+
focalY = rationalToFloat(gradient.focalPoint.y) * devicePixelRatio;
|
|
95
|
+
}
|
|
96
|
+
if (gradient.focalRadius) {
|
|
97
|
+
focalR = rationalToFloat(gradient.focalRadius) * devicePixelRatio;
|
|
98
|
+
}
|
|
99
|
+
// Sort and convert stops
|
|
100
|
+
const sortedStops = [...gradient.stops].sort((a, b) => rationalToFloat(a.position) - rationalToFloat(b.position));
|
|
101
|
+
const colors = colorStopsToFloat32Array(sortedStops);
|
|
102
|
+
const positions = positionsToFloat32Array(sortedStops);
|
|
103
|
+
const tileMode = mapTileMode(ck, gradient.tileMode);
|
|
104
|
+
// Two-point conical: inner circle to outer circle
|
|
105
|
+
return ck.Shader.MakeTwoPointConicalGradient(new Float32Array([focalX, focalY]), focalR, new Float32Array([centerX, centerY]), radius, colors, positions, tileMode);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Map a P-dimension conic (sweep) gradient to a GPU shader.
|
|
109
|
+
*
|
|
110
|
+
* @param ck - GPU shader backend instance
|
|
111
|
+
* @param gradient - P-dimension conic gradient definition
|
|
112
|
+
* @param bounds - Rasterized bounds for coordinate transformation
|
|
113
|
+
* @param devicePixelRatio - Device pixel ratio for coordinate scaling
|
|
114
|
+
* @returns GPU shader instance
|
|
115
|
+
*/
|
|
116
|
+
export function mapConicGradientToShader(ck, gradient, bounds, devicePixelRatio) {
|
|
117
|
+
// Convert center to device coordinates
|
|
118
|
+
const centerX = rationalToFloat(gradient.center.x) * devicePixelRatio;
|
|
119
|
+
const centerY = rationalToFloat(gradient.center.y) * devicePixelRatio;
|
|
120
|
+
// Convert angles (GPU shader expects degrees)
|
|
121
|
+
const startAngle = rationalToFloat(gradient.startAngle) + rationalToFloat(gradient.rotation);
|
|
122
|
+
const endAngle = rationalToFloat(gradient.endAngle) + rationalToFloat(gradient.rotation);
|
|
123
|
+
// Sort and convert stops
|
|
124
|
+
const sortedStops = [...gradient.stops].sort((a, b) => rationalToFloat(a.position) - rationalToFloat(b.position));
|
|
125
|
+
const colors = colorStopsToFloat32Array(sortedStops);
|
|
126
|
+
const positions = positionsToFloat32Array(sortedStops);
|
|
127
|
+
// Sweep gradient always uses Clamp-like behavior
|
|
128
|
+
return ck.Shader.MakeSweepGradient(centerX, centerY, colors, positions, ck.TileMode.Clamp, startAngle, endAngle);
|
|
129
|
+
}
|
|
130
|
+
// =============================================================================
|
|
131
|
+
// Conversion Utilities
|
|
132
|
+
// =============================================================================
|
|
133
|
+
/**
|
|
134
|
+
* Convert a rational number to a floating-point number.
|
|
135
|
+
*
|
|
136
|
+
* This is the critical boundary where exact arithmetic meets GPU floats.
|
|
137
|
+
* The conversion is straightforward division, but precision loss is
|
|
138
|
+
* unavoidable and acceptable at this layer.
|
|
139
|
+
*/
|
|
140
|
+
export function rationalToFloat(r) {
|
|
141
|
+
// Handle bigint to number conversion
|
|
142
|
+
// For very large rationals, this may lose precision
|
|
143
|
+
return Number(r.numerator) / Number(r.denominator);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Convert P-dimension color stops to a Float32Array of RGBA values.
|
|
147
|
+
*
|
|
148
|
+
* GPU shader expects colors in RGBA order, with each channel in [0, 1].
|
|
149
|
+
* P-dimension stores RGB in [0, 255] and Alpha in [0, 1].
|
|
150
|
+
*
|
|
151
|
+
* ## Topology-Preserving Rounding (Clamping)
|
|
152
|
+
*
|
|
153
|
+
* Color values are clamped to [0, 1] to ensure GPU-valid input.
|
|
154
|
+
* This preserves the topological ordering of colors even if the
|
|
155
|
+
* original rational values were slightly out of range.
|
|
156
|
+
*/
|
|
157
|
+
export function colorStopsToFloat32Array(stops) {
|
|
158
|
+
const array = new Float32Array(stops.length * 4);
|
|
159
|
+
for (let i = 0; i < stops.length; i++) {
|
|
160
|
+
const stop = stops[i];
|
|
161
|
+
const offset = i * 4;
|
|
162
|
+
// Convert [0, 255] rational to [0, 1] float with clamping
|
|
163
|
+
array[offset + 0] = clamp01(rationalToFloat(stop.r) / 255);
|
|
164
|
+
array[offset + 1] = clamp01(rationalToFloat(stop.g) / 255);
|
|
165
|
+
array[offset + 2] = clamp01(rationalToFloat(stop.b) / 255);
|
|
166
|
+
// Alpha is already in [0, 1] in P-dimension
|
|
167
|
+
array[offset + 3] = clamp01(rationalToFloat(stop.a));
|
|
168
|
+
}
|
|
169
|
+
return array;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Convert P-dimension color stop positions to a Float32Array.
|
|
173
|
+
*
|
|
174
|
+
* Positions are kept as-is (already in [0, 1] in P-dimension),
|
|
175
|
+
* with clamping for safety.
|
|
176
|
+
*/
|
|
177
|
+
export function positionsToFloat32Array(stops) {
|
|
178
|
+
const array = new Float32Array(stops.length);
|
|
179
|
+
for (let i = 0; i < stops.length; i++) {
|
|
180
|
+
array[i] = clamp01(rationalToFloat(stops[i].position));
|
|
181
|
+
}
|
|
182
|
+
return array;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Clamp a value to [0, 1] range.
|
|
186
|
+
*
|
|
187
|
+
* This is the "topology-preserving rounding" for color values:
|
|
188
|
+
* it ensures the value is valid for GPU while preserving ordering.
|
|
189
|
+
*/
|
|
190
|
+
function clamp01(value) {
|
|
191
|
+
return Math.max(0, Math.min(1, value));
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Map P-dimension tile mode to GPU tile mode constant.
|
|
195
|
+
*/
|
|
196
|
+
function mapTileMode(ck, mode) {
|
|
197
|
+
switch (mode) {
|
|
198
|
+
case 'clamp':
|
|
199
|
+
return ck.TileMode.Clamp;
|
|
200
|
+
case 'repeat':
|
|
201
|
+
return ck.TileMode.Repeat;
|
|
202
|
+
case 'mirror':
|
|
203
|
+
return ck.TileMode.Mirror;
|
|
204
|
+
case 'decal':
|
|
205
|
+
return ck.TileMode.Decal;
|
|
206
|
+
default:
|
|
207
|
+
return ck.TileMode.Clamp;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
// =============================================================================
|
|
211
|
+
// Factory Function for Gradient FillStyle
|
|
212
|
+
// =============================================================================
|
|
213
|
+
/**
|
|
214
|
+
* Create a GPU shader from a FillStyle gradient definition.
|
|
215
|
+
*
|
|
216
|
+
* This is a higher-level factory that integrates with the existing
|
|
217
|
+
* FillStyle type from the AST.
|
|
218
|
+
*/
|
|
219
|
+
export function createGradientShader(ck, fillType, stops, bounds, devicePixelRatio) {
|
|
220
|
+
// Convert simplified FillStyle stops to PColorStop format
|
|
221
|
+
const pStops = stops.map((stop, index) => {
|
|
222
|
+
const rgba = parseColorString(stop.color);
|
|
223
|
+
return {
|
|
224
|
+
id: index,
|
|
225
|
+
r: { numerator: BigInt(rgba.r), denominator: 1n },
|
|
226
|
+
g: { numerator: BigInt(rgba.g), denominator: 1n },
|
|
227
|
+
b: { numerator: BigInt(rgba.b), denominator: 1n },
|
|
228
|
+
a: { numerator: BigInt(Math.round(rgba.a * 1000)), denominator: 1000n },
|
|
229
|
+
position: stop.offset,
|
|
230
|
+
};
|
|
231
|
+
});
|
|
232
|
+
if (fillType === 'linear-gradient') {
|
|
233
|
+
// Default linear gradient: top to bottom
|
|
234
|
+
const linearGradient = {
|
|
235
|
+
id: 0,
|
|
236
|
+
start: {
|
|
237
|
+
id: 0,
|
|
238
|
+
x: { numerator: BigInt(Math.round(bounds.x)), denominator: 1n },
|
|
239
|
+
y: { numerator: BigInt(Math.round(bounds.y)), denominator: 1n },
|
|
240
|
+
},
|
|
241
|
+
end: {
|
|
242
|
+
id: 0,
|
|
243
|
+
x: { numerator: BigInt(Math.round(bounds.x)), denominator: 1n },
|
|
244
|
+
y: { numerator: BigInt(Math.round(bounds.y + bounds.height)), denominator: 1n },
|
|
245
|
+
},
|
|
246
|
+
stops: pStops,
|
|
247
|
+
tileMode: 'clamp',
|
|
248
|
+
};
|
|
249
|
+
return mapLinearGradientToShader(ck, linearGradient, bounds, devicePixelRatio);
|
|
250
|
+
}
|
|
251
|
+
else if (fillType === 'radial-gradient') {
|
|
252
|
+
// Default radial gradient: center of bounds, radius to edge
|
|
253
|
+
const cx = bounds.x + bounds.width / 2;
|
|
254
|
+
const cy = bounds.y + bounds.height / 2;
|
|
255
|
+
const radius = Math.max(bounds.width, bounds.height) / 2;
|
|
256
|
+
const radialGradient = {
|
|
257
|
+
id: 0,
|
|
258
|
+
center: {
|
|
259
|
+
id: 0,
|
|
260
|
+
x: { numerator: BigInt(Math.round(cx * 1000)), denominator: 1000n },
|
|
261
|
+
y: { numerator: BigInt(Math.round(cy * 1000)), denominator: 1000n },
|
|
262
|
+
},
|
|
263
|
+
radiusX: { numerator: BigInt(Math.round(radius * 1000)), denominator: 1000n },
|
|
264
|
+
radiusY: { numerator: BigInt(Math.round(radius * 1000)), denominator: 1000n },
|
|
265
|
+
stops: pStops,
|
|
266
|
+
tileMode: 'clamp',
|
|
267
|
+
};
|
|
268
|
+
return mapRadialGradientToShader(ck, radialGradient, bounds, devicePixelRatio);
|
|
269
|
+
}
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Parse a CSS color string to RGBA values.
|
|
274
|
+
*
|
|
275
|
+
* Supports:
|
|
276
|
+
* - Hex: #RGB, #RGBA, #RRGGBB, #RRGGBBAA
|
|
277
|
+
* - Named colors (basic set)
|
|
278
|
+
*/
|
|
279
|
+
function parseColorString(color) {
|
|
280
|
+
// Named colors (basic set)
|
|
281
|
+
const namedColors = {
|
|
282
|
+
black: { r: 0, g: 0, b: 0 },
|
|
283
|
+
white: { r: 255, g: 255, b: 255 },
|
|
284
|
+
red: { r: 255, g: 0, b: 0 },
|
|
285
|
+
green: { r: 0, g: 128, b: 0 },
|
|
286
|
+
blue: { r: 0, g: 0, b: 255 },
|
|
287
|
+
yellow: { r: 255, g: 255, b: 0 },
|
|
288
|
+
cyan: { r: 0, g: 255, b: 255 },
|
|
289
|
+
magenta: { r: 255, g: 0, b: 255 },
|
|
290
|
+
transparent: { r: 0, g: 0, b: 0 },
|
|
291
|
+
};
|
|
292
|
+
const lower = color.toLowerCase().trim();
|
|
293
|
+
if (lower === 'transparent') {
|
|
294
|
+
return { r: 0, g: 0, b: 0, a: 0 };
|
|
295
|
+
}
|
|
296
|
+
if (namedColors[lower]) {
|
|
297
|
+
return { ...namedColors[lower], a: 1 };
|
|
298
|
+
}
|
|
299
|
+
// Hex parsing
|
|
300
|
+
if (lower.startsWith('#')) {
|
|
301
|
+
const hex = lower.slice(1);
|
|
302
|
+
if (hex.length === 3) {
|
|
303
|
+
// #RGB
|
|
304
|
+
return {
|
|
305
|
+
r: parseInt(hex[0] + hex[0], 16),
|
|
306
|
+
g: parseInt(hex[1] + hex[1], 16),
|
|
307
|
+
b: parseInt(hex[2] + hex[2], 16),
|
|
308
|
+
a: 1,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
else if (hex.length === 4) {
|
|
312
|
+
// #RGBA
|
|
313
|
+
return {
|
|
314
|
+
r: parseInt(hex[0] + hex[0], 16),
|
|
315
|
+
g: parseInt(hex[1] + hex[1], 16),
|
|
316
|
+
b: parseInt(hex[2] + hex[2], 16),
|
|
317
|
+
a: parseInt(hex[3] + hex[3], 16) / 255,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
else if (hex.length === 6) {
|
|
321
|
+
// #RRGGBB
|
|
322
|
+
return {
|
|
323
|
+
r: parseInt(hex.slice(0, 2), 16),
|
|
324
|
+
g: parseInt(hex.slice(2, 4), 16),
|
|
325
|
+
b: parseInt(hex.slice(4, 6), 16),
|
|
326
|
+
a: 1,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
else if (hex.length === 8) {
|
|
330
|
+
// #RRGGBBAA
|
|
331
|
+
return {
|
|
332
|
+
r: parseInt(hex.slice(0, 2), 16),
|
|
333
|
+
g: parseInt(hex.slice(2, 4), 16),
|
|
334
|
+
b: parseInt(hex.slice(4, 6), 16),
|
|
335
|
+
a: parseInt(hex.slice(6, 8), 16) / 255,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
// Fallback: black
|
|
340
|
+
return { r: 0, g: 0, b: 0, a: 1 };
|
|
341
|
+
}
|
|
342
|
+
// =============================================================================
|
|
343
|
+
// Exports for Testing
|
|
344
|
+
// =============================================================================
|
|
345
|
+
export const _internals = {
|
|
346
|
+
rationalToFloat,
|
|
347
|
+
clamp01,
|
|
348
|
+
colorStopsToFloat32Array,
|
|
349
|
+
positionsToFloat32Array,
|
|
350
|
+
mapTileMode,
|
|
351
|
+
parseColorString,
|
|
352
|
+
};
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Topology-Preserving Rounding Algorithm
|
|
3
|
+
*
|
|
4
|
+
* This module implements the rasterization layer that projects P-dimension
|
|
5
|
+
* rational coordinates to discrete pixel coordinates while preserving
|
|
6
|
+
* topological relationships (adjacency, containment, ordering).
|
|
7
|
+
*
|
|
8
|
+
* ## The Problem
|
|
9
|
+
*
|
|
10
|
+
* Given two adjacent surfaces A and B where:
|
|
11
|
+
* A.right = 100.333... (rational)
|
|
12
|
+
* B.left = 100.333... (same rational)
|
|
13
|
+
*
|
|
14
|
+
* Naive rounding may produce:
|
|
15
|
+
* A.right = 100px (floor)
|
|
16
|
+
* B.left = 101px (ceil)
|
|
17
|
+
*
|
|
18
|
+
* This creates a 1px gap that violates the topological constraint
|
|
19
|
+
* that A and B are adjacent (no gap, no overlap).
|
|
20
|
+
*
|
|
21
|
+
* ## Solution: Constraint-Aware Rounding
|
|
22
|
+
*
|
|
23
|
+
* Instead of rounding each coordinate independently, we:
|
|
24
|
+
* 1. Build a graph of topological relationships (adjacency, containment)
|
|
25
|
+
* 2. Partition coordinates into equivalence classes (same rational = same pixel)
|
|
26
|
+
* 3. Round equivalence classes together
|
|
27
|
+
* 4. Propagate rounding decisions through the constraint graph
|
|
28
|
+
*
|
|
29
|
+
* ## Algorithm
|
|
30
|
+
*
|
|
31
|
+
* ```
|
|
32
|
+
* INPUT:
|
|
33
|
+
* - Set of surfaces S with rational bounds
|
|
34
|
+
* - Topological constraints T (adjacency, containment)
|
|
35
|
+
* - Device pixel ratio DPR
|
|
36
|
+
*
|
|
37
|
+
* OUTPUT:
|
|
38
|
+
* - Integer pixel coordinates for all surfaces
|
|
39
|
+
* - Guarantee: topology is preserved
|
|
40
|
+
*
|
|
41
|
+
* ALGORITHM:
|
|
42
|
+
*
|
|
43
|
+
* Phase 1: Build Coordinate Equivalence Classes
|
|
44
|
+
* For each unique rational value r:
|
|
45
|
+
* equiv[r] = { all coordinates that equal r }
|
|
46
|
+
*
|
|
47
|
+
* Phase 2: Compute Rounding Constraints
|
|
48
|
+
* For each adjacency constraint (A.right = B.left):
|
|
49
|
+
* round(A.right) MUST equal round(B.left)
|
|
50
|
+
* For each ordering constraint (A.right < B.left):
|
|
51
|
+
* round(A.right) MUST be < round(B.left)
|
|
52
|
+
*
|
|
53
|
+
* Phase 3: Propagate Rounding Decisions
|
|
54
|
+
* Using constraint propagation:
|
|
55
|
+
* - Start with coordinates that have no constraints (free variables)
|
|
56
|
+
* - Round them to nearest integer
|
|
57
|
+
* - Propagate to constrained coordinates
|
|
58
|
+
* - Resolve conflicts by adjusting adjacent surfaces symmetrically
|
|
59
|
+
*
|
|
60
|
+
* Phase 4: Verify Topology Preservation
|
|
61
|
+
* Assert all topological constraints are satisfied
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
import type { EntityId, Rational, RasterBounds, PVectorBounds } from '../ast/types';
|
|
65
|
+
/**
|
|
66
|
+
* A coordinate in the pre-rasterization space.
|
|
67
|
+
*/
|
|
68
|
+
interface RationalCoord {
|
|
69
|
+
entityId: EntityId;
|
|
70
|
+
edge: 'left' | 'right' | 'top' | 'bottom';
|
|
71
|
+
value: Rational;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Topological constraint between coordinates.
|
|
75
|
+
*/
|
|
76
|
+
type TopoConstraint = {
|
|
77
|
+
type: 'equal';
|
|
78
|
+
a: CoordRef;
|
|
79
|
+
b: CoordRef;
|
|
80
|
+
} | {
|
|
81
|
+
type: 'less-than';
|
|
82
|
+
a: CoordRef;
|
|
83
|
+
b: CoordRef;
|
|
84
|
+
} | {
|
|
85
|
+
type: 'adjacent';
|
|
86
|
+
a: CoordRef;
|
|
87
|
+
b: CoordRef;
|
|
88
|
+
};
|
|
89
|
+
interface CoordRef {
|
|
90
|
+
entityId: EntityId;
|
|
91
|
+
edge: 'left' | 'right' | 'top' | 'bottom';
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Result of the rounding algorithm.
|
|
95
|
+
*/
|
|
96
|
+
export interface RoundingResult {
|
|
97
|
+
/** Rasterized bounds for each entity */
|
|
98
|
+
bounds: Map<EntityId, RasterBounds>;
|
|
99
|
+
/** Any topology violations detected (should be empty if algorithm is correct) */
|
|
100
|
+
violations: TopologyViolation[];
|
|
101
|
+
/** Statistics about the rounding process */
|
|
102
|
+
stats: RoundingStats;
|
|
103
|
+
}
|
|
104
|
+
interface TopologyViolation {
|
|
105
|
+
constraint: TopoConstraint;
|
|
106
|
+
message: string;
|
|
107
|
+
}
|
|
108
|
+
interface RoundingStats {
|
|
109
|
+
totalCoordinates: number;
|
|
110
|
+
equivalenceClasses: number;
|
|
111
|
+
constraintsPropagated: number;
|
|
112
|
+
conflictsResolved: number;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Topology-preserving rounding entry point.
|
|
116
|
+
*/
|
|
117
|
+
export declare function roundWithTopologyPreservation(entities: Map<EntityId, PVectorBounds>, constraints: TopoConstraint[], devicePixelRatio: number): RoundingResult;
|
|
118
|
+
declare function extractCoordinates(entities: Map<EntityId, PVectorBounds>): RationalCoord[];
|
|
119
|
+
/**
|
|
120
|
+
* Build equivalence classes from coordinates and equality constraints.
|
|
121
|
+
*
|
|
122
|
+
* Two coordinates are in the same class if:
|
|
123
|
+
* 1. They have the same rational value, OR
|
|
124
|
+
* 2. They are connected by an 'equal' or 'adjacent' constraint
|
|
125
|
+
*/
|
|
126
|
+
declare function buildEquivalenceClasses(coords: RationalCoord[], constraints: TopoConstraint[]): Map<string, RationalCoord[]>;
|
|
127
|
+
declare function rationalToFloat(r: Rational): number;
|
|
128
|
+
interface PropagationResult {
|
|
129
|
+
adjusted: Map<string, number>;
|
|
130
|
+
conflictsResolved: number;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Propagate rounding decisions through less-than constraints.
|
|
134
|
+
*
|
|
135
|
+
* If A < B in rational space, we must ensure round(A) < round(B) in pixel space.
|
|
136
|
+
* If rounding would violate this, we adjust by:
|
|
137
|
+
* 1. Decreasing A by 1, OR
|
|
138
|
+
* 2. Increasing B by 1
|
|
139
|
+
*
|
|
140
|
+
* We choose the option that minimizes total visual shift.
|
|
141
|
+
*/
|
|
142
|
+
declare function propagateConstraints(initial: Map<string, number>, equivClasses: Map<string, RationalCoord[]>, constraints: TopoConstraint[]): PropagationResult;
|
|
143
|
+
declare function verifyTopology(bounds: Map<EntityId, RasterBounds>, constraints: TopoConstraint[]): TopologyViolation[];
|
|
144
|
+
export declare const _internals: {
|
|
145
|
+
extractCoordinates: typeof extractCoordinates;
|
|
146
|
+
buildEquivalenceClasses: typeof buildEquivalenceClasses;
|
|
147
|
+
propagateConstraints: typeof propagateConstraints;
|
|
148
|
+
verifyTopology: typeof verifyTopology;
|
|
149
|
+
rationalToFloat: typeof rationalToFloat;
|
|
150
|
+
};
|
|
151
|
+
export {};
|