@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,344 @@
1
+ /**
2
+ * Subpixel Error Distribution Algorithm
3
+ *
4
+ * This module implements the Largest Remainder Method (LRM) for distributing
5
+ * subpixel rounding errors across child elements within a parent container.
6
+ *
7
+ * ## The Problem (Architect's Decision #2: Spatial Closure)
8
+ *
9
+ * Given a 100px container with 3 children of equal width (33.333...px each):
10
+ *
11
+ * ```
12
+ * Naive rounding:
13
+ * floor(33.333) = 33
14
+ * 33 + 33 + 33 = 99px ← 1px HOLE!
15
+ * ```
16
+ *
17
+ * This violates VS axiom: "Constraint holes cannot be hidden in theoretical blind spots"
18
+ *
19
+ * ## Solution: Largest Remainder Method
20
+ *
21
+ * 1. Compute integer quotients: floor(33.333) = 33 for each
22
+ * 2. Compute remainders: 0.333... for each
23
+ * 3. Total shortfall: 100 - 99 = 1px
24
+ * 4. Distribute 1px to elements with largest remainders
25
+ *
26
+ * Result: [34, 33, 33] or [33, 34, 33] or [33, 33, 34]
27
+ * Sum: 100px exactly ✓
28
+ *
29
+ * ## Mathematical Guarantee
30
+ *
31
+ * For any set of positive rationals r₁, r₂, ..., rₙ where Σrᵢ = T (integer):
32
+ *
33
+ * Σ⌊rᵢ⌋ ≤ T ≤ Σ⌈rᵢ⌉
34
+ *
35
+ * LRM distributes exactly (T - Σ⌊rᵢ⌋) extra pixels to achieve Σ = T.
36
+ */
37
+
38
+ import type { EntityId, Rational } from '../ast/types';
39
+
40
+ // =============================================================================
41
+ // Types
42
+ // =============================================================================
43
+
44
+ /**
45
+ * A sibling group: children that must sum to parent's dimension.
46
+ */
47
+ export interface SiblingGroup {
48
+ /** Parent container entity */
49
+ parentId: EntityId;
50
+
51
+ /** Child entities in layout order */
52
+ childIds: EntityId[];
53
+
54
+ /** Axis being distributed */
55
+ axis: 'horizontal' | 'vertical';
56
+
57
+ /** Parent's exact pixel dimension (must be integer) */
58
+ parentDimension: number;
59
+
60
+ /** Each child's rational dimension (pre-rounding) */
61
+ childDimensions: Map<EntityId, number>;
62
+ }
63
+
64
+ /**
65
+ * Distribution result for a single child.
66
+ */
67
+ export interface DistributedDimension {
68
+ entityId: EntityId;
69
+
70
+ /** Integer pixel dimension after distribution */
71
+ pixels: number;
72
+
73
+ /** Original fractional value */
74
+ original: number;
75
+
76
+ /** Error introduced by rounding (-0.5 to +0.5) */
77
+ error: number;
78
+ }
79
+
80
+ /**
81
+ * Complete distribution result.
82
+ */
83
+ export interface DistributionResult {
84
+ /** Distributed dimensions for each child */
85
+ dimensions: DistributedDimension[];
86
+
87
+ /** Verification: sum of all pixels */
88
+ totalPixels: number;
89
+
90
+ /** Should equal parentDimension exactly */
91
+ isExact: boolean;
92
+
93
+ /** Distribution method used */
94
+ method: 'largest-remainder' | 'first-fit';
95
+ }
96
+
97
+ // =============================================================================
98
+ // Largest Remainder Method
99
+ // =============================================================================
100
+
101
+ /**
102
+ * Distribute subpixel errors using the Largest Remainder Method.
103
+ *
104
+ * ## Algorithm
105
+ *
106
+ * ```
107
+ * INPUT: childDimensions = [33.333, 33.333, 33.333], parentDimension = 100
108
+ *
109
+ * Step 1: Compute floors
110
+ * floors = [33, 33, 33]
111
+ * sum(floors) = 99
112
+ *
113
+ * Step 2: Compute remainders
114
+ * remainders = [0.333, 0.333, 0.333]
115
+ *
116
+ * Step 3: Compute shortfall
117
+ * shortfall = 100 - 99 = 1
118
+ *
119
+ * Step 4: Sort by remainder (descending), distribute shortfall
120
+ * Give 1px to first element (arbitrary tie-break: leftmost)
121
+ *
122
+ * OUTPUT: [34, 33, 33]
123
+ * ```
124
+ *
125
+ * ## Tie-Breaking Strategy
126
+ *
127
+ * When remainders are equal (common for equal-width elements):
128
+ * - Distribute extra pixels left-to-right (reading order)
129
+ * - This is visually predictable and matches user expectation
130
+ */
131
+ export function distributeWithLargestRemainder(group: SiblingGroup): DistributionResult {
132
+ const { childIds, parentDimension, childDimensions } = group;
133
+
134
+ // Edge case: no children
135
+ if (childIds.length === 0) {
136
+ return {
137
+ dimensions: [],
138
+ totalPixels: 0,
139
+ isExact: parentDimension === 0,
140
+ method: 'largest-remainder',
141
+ };
142
+ }
143
+
144
+ // Step 1: Compute floors and remainders
145
+ interface WorkItem {
146
+ entityId: EntityId;
147
+ original: number;
148
+ floor: number;
149
+ remainder: number;
150
+ index: number; // Original order for tie-breaking
151
+ }
152
+
153
+ const items: WorkItem[] = childIds.map((id, index) => {
154
+ const original = childDimensions.get(id) ?? 0;
155
+ const floor = Math.floor(original);
156
+ return {
157
+ entityId: id,
158
+ original,
159
+ floor,
160
+ remainder: original - floor,
161
+ index,
162
+ };
163
+ });
164
+
165
+ // Step 2: Compute shortfall
166
+ const sumOfFloors = items.reduce((sum, item) => sum + item.floor, 0);
167
+ const shortfall = parentDimension - sumOfFloors;
168
+
169
+ // Sanity check: shortfall should be non-negative and <= childCount
170
+ if (shortfall < 0 || shortfall > items.length) {
171
+ // This indicates a constraint violation (children sum > parent)
172
+ // Fall back to proportional scaling
173
+ return distributeProportionally(group);
174
+ }
175
+
176
+ // Step 3: Sort by remainder (descending), then by index (ascending) for tie-break
177
+ const sorted = [...items].sort((a, b) => {
178
+ const remainderDiff = b.remainder - a.remainder;
179
+ if (Math.abs(remainderDiff) > 1e-10) {
180
+ return remainderDiff;
181
+ }
182
+ // Tie-break: leftmost first
183
+ return a.index - b.index;
184
+ });
185
+
186
+ // Step 4: Distribute shortfall to top N elements
187
+ const extraPixels = new Set<EntityId>();
188
+ for (let i = 0; i < shortfall; i++) {
189
+ extraPixels.add(sorted[i].entityId);
190
+ }
191
+
192
+ // Step 5: Build result
193
+ const dimensions: DistributedDimension[] = items.map(item => {
194
+ const pixels = item.floor + (extraPixels.has(item.entityId) ? 1 : 0);
195
+ return {
196
+ entityId: item.entityId,
197
+ pixels,
198
+ original: item.original,
199
+ error: pixels - item.original,
200
+ };
201
+ });
202
+
203
+ const totalPixels = dimensions.reduce((sum, d) => sum + d.pixels, 0);
204
+
205
+ return {
206
+ dimensions,
207
+ totalPixels,
208
+ isExact: totalPixels === parentDimension,
209
+ method: 'largest-remainder',
210
+ };
211
+ }
212
+
213
+ /**
214
+ * Fallback: Proportional scaling when LRM fails.
215
+ *
216
+ * Used when child sum exceeds parent (constraint violation).
217
+ */
218
+ function distributeProportionally(group: SiblingGroup): DistributionResult {
219
+ const { childIds, parentDimension, childDimensions } = group;
220
+
221
+ const totalOriginal = childIds.reduce(
222
+ (sum, id) => sum + (childDimensions.get(id) ?? 0),
223
+ 0
224
+ );
225
+
226
+ if (totalOriginal === 0) {
227
+ return {
228
+ dimensions: childIds.map(id => ({
229
+ entityId: id,
230
+ pixels: 0,
231
+ original: 0,
232
+ error: 0,
233
+ })),
234
+ totalPixels: 0,
235
+ isExact: parentDimension === 0,
236
+ method: 'first-fit',
237
+ };
238
+ }
239
+
240
+ const scale = parentDimension / totalOriginal;
241
+ const scaled = childIds.map(id => {
242
+ const original = childDimensions.get(id) ?? 0;
243
+ return {
244
+ entityId: id,
245
+ original,
246
+ scaled: original * scale,
247
+ };
248
+ });
249
+
250
+ // Apply LRM to scaled values
251
+ const scaledGroup: SiblingGroup = {
252
+ ...group,
253
+ childDimensions: new Map(scaled.map(s => [s.entityId, s.scaled])),
254
+ };
255
+
256
+ const result = distributeWithLargestRemainder(scaledGroup);
257
+ result.method = 'first-fit';
258
+ return result;
259
+ }
260
+
261
+ // =============================================================================
262
+ // Integration with Topology Rounding
263
+ // =============================================================================
264
+
265
+ /**
266
+ * Parent-child containment constraint for error distribution.
267
+ */
268
+ export interface ContainmentConstraint {
269
+ parentId: EntityId;
270
+ childIds: EntityId[];
271
+ axis: 'horizontal' | 'vertical';
272
+ }
273
+
274
+ /**
275
+ * Apply error distribution to a set of sibling groups.
276
+ *
277
+ * This is called AFTER basic topology-preserving rounding,
278
+ * to ensure parent boundaries are exactly satisfied.
279
+ */
280
+ export function applyErrorDistribution(
281
+ roundedBounds: Map<EntityId, { x: number; y: number; width: number; height: number }>,
282
+ containments: ContainmentConstraint[],
283
+ ): Map<EntityId, { x: number; y: number; width: number; height: number }> {
284
+ const result = new Map(roundedBounds);
285
+
286
+ for (const constraint of containments) {
287
+ const parentBounds = result.get(constraint.parentId);
288
+ if (!parentBounds) continue;
289
+
290
+ const parentDimension = constraint.axis === 'horizontal'
291
+ ? parentBounds.width
292
+ : parentBounds.height;
293
+
294
+ // Build sibling group
295
+ const childDimensions = new Map<EntityId, number>();
296
+ for (const childId of constraint.childIds) {
297
+ const childBounds = result.get(childId);
298
+ if (childBounds) {
299
+ const dim = constraint.axis === 'horizontal'
300
+ ? childBounds.width
301
+ : childBounds.height;
302
+ childDimensions.set(childId, dim);
303
+ }
304
+ }
305
+
306
+ const group: SiblingGroup = {
307
+ parentId: constraint.parentId,
308
+ childIds: constraint.childIds,
309
+ axis: constraint.axis,
310
+ parentDimension,
311
+ childDimensions,
312
+ };
313
+
314
+ // Distribute
315
+ const distribution = distributeWithLargestRemainder(group);
316
+
317
+ // Apply distributed dimensions
318
+ let offset = constraint.axis === 'horizontal' ? parentBounds.x : parentBounds.y;
319
+
320
+ for (const dist of distribution.dimensions) {
321
+ const childBounds = result.get(dist.entityId);
322
+ if (childBounds) {
323
+ if (constraint.axis === 'horizontal') {
324
+ childBounds.x = offset;
325
+ childBounds.width = dist.pixels;
326
+ } else {
327
+ childBounds.y = offset;
328
+ childBounds.height = dist.pixels;
329
+ }
330
+ offset += dist.pixels;
331
+ }
332
+ }
333
+ }
334
+
335
+ return result;
336
+ }
337
+
338
+ // =============================================================================
339
+ // Exports for Testing
340
+ // =============================================================================
341
+
342
+ export const _internals = {
343
+ distributeProportionally,
344
+ };