@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,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
|
+
};
|