@nexart/ui-renderer 0.8.1 → 0.8.3

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @nexart/ui-renderer
2
2
 
3
- Version: 0.8.1
3
+ Version: 0.8.3
4
4
 
5
5
  **Lightweight Preview Runtime for NexArt Protocol**
6
6
 
@@ -19,6 +19,34 @@ Version: 0.8.1
19
19
 
20
20
  ---
21
21
 
22
+ ## v0.8.3 — Animation Loop Fix
23
+
24
+ Fixed critical bug where preview only rendered a single frame.
25
+
26
+ - **RAF unconditionally scheduled**: `requestAnimationFrame(loop)` is now called on every tick
27
+ - **Budget gates draw only**: Frame budget controls whether `draw()` runs, not whether the loop continues
28
+ - **Continuous animation**: Loop runs forever until explicitly stopped
29
+
30
+ **Animation Loop Invariant (locked for v0.x):**
31
+ ```
32
+ RAF schedules → budget checks → draw executes (or skips) → repeat
33
+ Never: budget check → stop loop
34
+ ```
35
+
36
+ ---
37
+
38
+ ## v0.8.2 — Runtime Dimensions Fix
39
+
40
+ Fixed critical bug where preview scaling affected `width`/`height` inside sketches.
41
+
42
+ - **Runtime uses original dimensions**: `width` and `height` now match Code Mode exactly
43
+ - **Canvas buffer still scaled for performance**: Rendering is fast, semantics are correct
44
+ - **Loop animations work correctly**: Geometry math and timing no longer break
45
+
46
+ **Key rule enforced:** Preview scaling is a rendering concern, not a semantic one.
47
+
48
+ ---
49
+
22
50
  ## v0.8.1 — Canvas Scaling Fix
23
51
 
24
52
  Fixed canvas zoom/cropping bug caused by resolution downscaling in v0.8.0.
@@ -213,7 +241,7 @@ import { getCapabilities } from '@nexart/ui-renderer';
213
241
 
214
242
  const caps = getCapabilities();
215
243
  // {
216
- // version: '0.8.1',
244
+ // version: '0.8.3',
217
245
  // isCanonical: false,
218
246
  // isArchival: false,
219
247
  // previewBudget: { MAX_FRAMES: 30, MAX_TOTAL_TIME_MS: 500, FRAME_STRIDE: 3 },
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @nexart/ui-renderer
3
- * Version: 0.8.1
3
+ * Version: 0.8.3
4
4
  *
5
5
  * Lightweight Preview Runtime for NexArt Protocol
6
6
  *
@@ -46,7 +46,7 @@ export { calculateScaledDimensions, applyScaledDimensions, type ScaledDimensions
46
46
  export type { NexArtSystemInput, NexArtSystem, DeclarativeSystemInput, DeclarativeSystem, CodeSystem, NexArtCodeSystem, UnifiedSystemInput, UnifiedSystem, UnifiedElement, BackgroundElement, PrimitiveElement, SketchElement, BackgroundPreset, PrimitiveName, ColorPalette, MotionSpeed, StrokeWeightAuto, LoopConfig, DeclarativeElement, SystemElement, DotsElement, LinesElement, WavesElement, GridElement, FlowFieldElement, OrbitsElement, BackgroundConfig, MotionConfig, PreviewOptions, ValidationResult, } from './types';
47
47
  export { AESTHETIC_DEFAULTS, SDK_VERSION as TYPE_SDK_VERSION } from './types';
48
48
  export type { Capabilities, PrimitiveCapability, ParameterSpec, } from './capabilities';
49
- export declare const SDK_VERSION = "0.8.1";
49
+ export declare const SDK_VERSION = "0.8.3";
50
50
  export declare const PROTOCOL_VERSION = "0.8";
51
51
  export declare const IS_CANONICAL = false;
52
52
  export declare const IS_ARCHIVAL = false;
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @nexart/ui-renderer
3
- * Version: 0.8.1
3
+ * Version: 0.8.3
4
4
  *
5
5
  * Lightweight Preview Runtime for NexArt Protocol
6
6
  *
@@ -44,7 +44,7 @@ export { PREVIEW_BUDGET, CANVAS_LIMITS, } from './preview/preview-types';
44
44
  export { createFrameBudget, canRenderFrame, recordFrame, resetBudget, shouldSkipFrame, } from './preview/frame-budget';
45
45
  export { calculateScaledDimensions, applyScaledDimensions, } from './preview/canvas-scaler';
46
46
  export { AESTHETIC_DEFAULTS, SDK_VERSION as TYPE_SDK_VERSION } from './types';
47
- export const SDK_VERSION = '0.8.1';
47
+ export const SDK_VERSION = '0.8.3';
48
48
  export const PROTOCOL_VERSION = '0.8';
49
49
  export const IS_CANONICAL = false;
50
50
  export const IS_ARCHIVAL = false;
@@ -10,6 +10,16 @@
10
10
  * ║ Max dimension: 900px ║
11
11
  * ║ Preserves aspect ratio ║
12
12
  * ║ Uses CSS scaling for display ║
13
+ * ╠══════════════════════════════════════════════════════════════════════════╣
14
+ * ║ ARCHITECTURAL INVARIANT (v0.8.2+): ║
15
+ * ║ ║
16
+ * ║ Scaling is a RENDERING concern, NOT a SEMANTIC one. ║
17
+ * ║ ║
18
+ * ║ - Canvas buffer: scaled (renderWidth × renderHeight) ║
19
+ * ║ - Runtime width/height: ALWAYS original protocol dimensions ║
20
+ * ║ - Context transform: ctx.scale() maps original → render space ║
21
+ * ║ ║
22
+ * ║ This ensures sketch math works identically in preview and Code Mode. ║
13
23
  * ╚══════════════════════════════════════════════════════════════════════════╝
14
24
  */
15
25
  export interface ScaledDimensions {
@@ -1 +1 @@
1
- {"version":3,"file":"canvas-scaler.d.ts","sourceRoot":"","sources":["../../src/preview/canvas-scaler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH,MAAM,WAAW,gBAAgB;IAC/B,iCAAiC;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,kCAAkC;IAClC,YAAY,EAAE,MAAM,CAAC;IACrB,qBAAqB;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,sBAAsB;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,2BAA2B;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,kCAAkC;IAClC,SAAS,EAAE,OAAO,CAAC;CACpB;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,GACb,gBAAgB,CA0BlB;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,iBAAiB,EACzB,UAAU,EAAE,gBAAgB,GAC3B,IAAI,CAWN;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,MAAM,GAClB,MAAM,CAER;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,wBAAwB,EAC7B,WAAW,EAAE,MAAM,GAClB,IAAI,CAIN;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,iBAAiB,EACzB,UAAU,EAAE,gBAAgB,GAC3B,IAAI,CAaN;AAED;;;;;;GAMG;AACH,wBAAgB,4BAA4B,CAC1C,GAAG,EAAE,wBAAwB,EAC7B,MAAM,EAAE,iBAAiB,GACxB,IAAI,CAKN"}
1
+ {"version":3,"file":"canvas-scaler.d.ts","sourceRoot":"","sources":["../../src/preview/canvas-scaler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAIH,MAAM,WAAW,gBAAgB;IAC/B,iCAAiC;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,kCAAkC;IAClC,YAAY,EAAE,MAAM,CAAC;IACrB,qBAAqB;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,sBAAsB;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,2BAA2B;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,kCAAkC;IAClC,SAAS,EAAE,OAAO,CAAC;CACpB;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,GACb,gBAAgB,CA0BlB;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,iBAAiB,EACzB,UAAU,EAAE,gBAAgB,GAC3B,IAAI,CAWN;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,MAAM,GAClB,MAAM,CAER;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,wBAAwB,EAC7B,WAAW,EAAE,MAAM,GAClB,IAAI,CAIN;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,iBAAiB,EACzB,UAAU,EAAE,gBAAgB,GAC3B,IAAI,CAaN;AAED;;;;;;GAMG;AACH,wBAAgB,4BAA4B,CAC1C,GAAG,EAAE,wBAAwB,EAC7B,MAAM,EAAE,iBAAiB,GACxB,IAAI,CAKN"}
@@ -10,6 +10,16 @@
10
10
  * ║ Max dimension: 900px ║
11
11
  * ║ Preserves aspect ratio ║
12
12
  * ║ Uses CSS scaling for display ║
13
+ * ╠══════════════════════════════════════════════════════════════════════════╣
14
+ * ║ ARCHITECTURAL INVARIANT (v0.8.2+): ║
15
+ * ║ ║
16
+ * ║ Scaling is a RENDERING concern, NOT a SEMANTIC one. ║
17
+ * ║ ║
18
+ * ║ - Canvas buffer: scaled (renderWidth × renderHeight) ║
19
+ * ║ - Runtime width/height: ALWAYS original protocol dimensions ║
20
+ * ║ - Context transform: ctx.scale() maps original → render space ║
21
+ * ║ ║
22
+ * ║ This ensures sketch math works identically in preview and Code Mode. ║
13
23
  * ╚══════════════════════════════════════════════════════════════════════════╝
14
24
  */
15
25
  import { CANVAS_LIMITS } from './preview-types';
@@ -1 +1 @@
1
- {"version":3,"file":"code-renderer.d.ts","sourceRoot":"","sources":["../../src/preview/code-renderer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AA0BjE,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,IAAI,EAAE,MAAM,IAAI,CAAC;IACjB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,WAAW,EAAE,KAAK,CAAC;IACnB,UAAU,EAAE,KAAK,CAAC;CACnB;AAwBD,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,gBAAgB,EACxB,MAAM,EAAE,iBAAiB,EACzB,OAAO,GAAE,cAAmB,GAC3B,YAAY,CA8Od"}
1
+ {"version":3,"file":"code-renderer.d.ts","sourceRoot":"","sources":["../../src/preview/code-renderer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AA0BjE,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,IAAI,EAAE,MAAM,IAAI,CAAC;IACjB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,WAAW,EAAE,KAAK,CAAC;IACnB,UAAU,EAAE,KAAK,CAAC;CACnB;AAwBD,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,gBAAgB,EACxB,MAAM,EAAE,iBAAiB,EACzB,OAAO,GAAE,cAAmB,GAC3B,YAAY,CAkQd"}
@@ -70,7 +70,21 @@ export function renderCodeModeSystem(system, canvas, options = {}) {
70
70
  let setupFn = null;
71
71
  let drawFn = null;
72
72
  const compileSource = () => {
73
- runtime = createPreviewRuntime(canvas, scaled.renderWidth, scaled.renderHeight, system.seed ?? 12345, normalizedVars);
73
+ // ╔═══════════════════════════════════════════════════════════════════════╗
74
+ // ║ ARCHITECTURAL INVARIANT — DO NOT CHANGE ║
75
+ // ║ ║
76
+ // ║ Runtime width/height MUST always equal protocol dimensions. ║
77
+ // ║ DO NOT pass scaled values (renderWidth/renderHeight) here. ║
78
+ // ║ ║
79
+ // ║ Scaling is a RENDERING concern handled by ctx.scale(). ║
80
+ // ║ width/height are SEMANTIC values used by sketch math. ║
81
+ // ║ ║
82
+ // ║ Passing scaled dimensions breaks loop animations and geometry. ║
83
+ // ║ This invariant is locked for v0.x — see CHANGELOG v0.8.2. ║
84
+ // ╚═══════════════════════════════════════════════════════════════════════╝
85
+ runtime = createPreviewRuntime(canvas, scaled.originalWidth, // ← Protocol dimension (e.g. 1950)
86
+ scaled.originalHeight, // ← Protocol dimension (e.g. 2400)
87
+ system.seed ?? 12345, normalizedVars);
74
88
  const totalFrames = system.totalFrames ?? 120;
75
89
  runtime.totalFrames = totalFrames;
76
90
  try {
@@ -159,13 +173,22 @@ export function renderCodeModeSystem(system, canvas, options = {}) {
159
173
  }
160
174
  resetBudget(budget);
161
175
  isRunning = true;
176
+ // ╔═══════════════════════════════════════════════════════════════════════╗
177
+ // ║ ANIMATION LOOP INVARIANT — DO NOT CHANGE ║
178
+ // ║ ║
179
+ // ║ requestAnimationFrame(loop) MUST be called on EVERY tick. ║
180
+ // ║ Budget gates ONLY whether draw() executes, NOT whether loop runs. ║
181
+ // ║ Never return before scheduling RAF. Never conditionally schedule. ║
182
+ // ╚═══════════════════════════════════════════════════════════════════════╝
162
183
  const loop = () => {
184
+ // ALWAYS schedule next frame first — loop never terminates on its own
185
+ animationId = requestAnimationFrame(loop);
186
+ // Early exit checks (after RAF is scheduled)
163
187
  if (!isRunning || isDestroyed)
164
188
  return;
189
+ // Budget check — skip draw but keep loop alive
165
190
  if (!canRenderFrame(budget)) {
166
- console.log(`[UIRenderer] Budget exhausted: ${budget.exhaustionReason}`);
167
- isRunning = false;
168
- return;
191
+ return; // Loop continues, just don't draw
169
192
  }
170
193
  frameCount++;
171
194
  if (!shouldSkipFrame(frameCount)) {
@@ -186,7 +209,6 @@ export function renderCodeModeSystem(system, canvas, options = {}) {
186
209
  console.warn('[UIRenderer] Draw error:', error);
187
210
  }
188
211
  }
189
- animationId = requestAnimationFrame(loop);
190
212
  };
191
213
  animationId = requestAnimationFrame(loop);
192
214
  }
@@ -1 +1 @@
1
- {"version":3,"file":"preview-engine.d.ts","sourceRoot":"","sources":["../../src/preview/preview-engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,EACL,KAAK,mBAAmB,EACxB,KAAK,mBAAmB,EACxB,KAAK,eAAe,EAErB,MAAM,iBAAiB,CAAC;AAwMzB;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,mBAAmB,GAAG,eAAe,CAEhF;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,mBAAmB,GAAG,mBAAmB,CAKpF;AAED;;GAEG;AACH,wBAAgB,iBAAiB,IAAI,IAAI,CAKxC"}
1
+ {"version":3,"file":"preview-engine.d.ts","sourceRoot":"","sources":["../../src/preview/preview-engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,EACL,KAAK,mBAAmB,EACxB,KAAK,mBAAmB,EACxB,KAAK,eAAe,EAErB,MAAM,iBAAiB,CAAC;AAqNzB;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,mBAAmB,GAAG,eAAe,CAEhF;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,mBAAmB,GAAG,mBAAmB,CAKpF;AAED;;GAEG;AACH,wBAAgB,iBAAiB,IAAI,IAAI,CAKxC"}
@@ -49,7 +49,15 @@ class PreviewEngine {
49
49
  // NOTE: Canvas resizing resets the 2D context transform.
50
50
  // Reapply scale factor once after resize for correct rendering.
51
51
  reapplyContextScale(this.canvas, scaled);
52
- this.runtime = createPreviewRuntime(this.canvas, scaled.renderWidth, scaled.renderHeight, this.config.seed ?? 12345, this.config.vars ?? []);
52
+ // ╔═══════════════════════════════════════════════════════════════════════╗
53
+ // ║ ARCHITECTURAL INVARIANT — DO NOT CHANGE ║
54
+ // ║ Runtime width/height MUST equal protocol dimensions. ║
55
+ // ║ DO NOT pass renderWidth/renderHeight — breaks loop animations. ║
56
+ // ║ Scaling is handled by ctx.scale(), not by changing width/height. ║
57
+ // ╚═══════════════════════════════════════════════════════════════════════╝
58
+ this.runtime = createPreviewRuntime(this.canvas, scaled.originalWidth, // ← Protocol dimension
59
+ scaled.originalHeight, // ← Protocol dimension
60
+ this.config.seed ?? 12345, this.config.vars ?? []);
53
61
  const totalFrames = this.config.totalFrames ?? 120;
54
62
  this.runtime.totalFrames = totalFrames;
55
63
  try {
@@ -124,15 +132,23 @@ class PreviewEngine {
124
132
  }
125
133
  this.scheduleNextFrame();
126
134
  }
135
+ // ╔═══════════════════════════════════════════════════════════════════════╗
136
+ // ║ ANIMATION LOOP INVARIANT — DO NOT CHANGE ║
137
+ // ║ ║
138
+ // ║ requestAnimationFrame MUST be called on EVERY tick. ║
139
+ // ║ Budget gates ONLY whether draw() executes, NOT whether loop runs. ║
140
+ // ║ Never return before scheduling RAF. Never conditionally schedule. ║
141
+ // ╚═══════════════════════════════════════════════════════════════════════╝
127
142
  scheduleNextFrame() {
128
- if (!this.running)
129
- return;
143
+ // ALWAYS schedule next frame first — loop never terminates on its own
130
144
  this.animationFrameId = requestAnimationFrame(() => {
145
+ this.scheduleNextFrame();
146
+ // Early exit checks (after RAF is scheduled)
131
147
  if (!this.running)
132
148
  return;
149
+ // Budget check — skip draw but keep loop alive
133
150
  if (!canRenderFrame(this.budget)) {
134
- this.running = false;
135
- return;
151
+ return; // Loop continues, just don't draw
136
152
  }
137
153
  this.internalFrameCount++;
138
154
  if (!shouldSkipFrame(this.internalFrameCount)) {
@@ -153,7 +169,6 @@ class PreviewEngine {
153
169
  console.warn('[PreviewEngine] Draw error:', error);
154
170
  }
155
171
  }
156
- this.scheduleNextFrame();
157
172
  });
158
173
  }
159
174
  stopLoop() {
@@ -1 +1 @@
1
- {"version":3,"file":"unified-renderer.d.ts","sourceRoot":"","sources":["../../src/preview/unified-renderer.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAkB,cAAc,EAAoE,MAAM,UAAU,CAAC;AAmBhJ,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,IAAI,EAAE,MAAM,IAAI,CAAC;IACjB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,WAAW,EAAE,KAAK,CAAC;IACnB,UAAU,EAAE,KAAK,CAAC;CACnB;AA6ED,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,aAAa,EACrB,MAAM,EAAE,iBAAiB,EACzB,OAAO,GAAE,cAAmB,GAC3B,eAAe,CA0PjB"}
1
+ {"version":3,"file":"unified-renderer.d.ts","sourceRoot":"","sources":["../../src/preview/unified-renderer.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAkB,cAAc,EAAoE,MAAM,UAAU,CAAC;AAmBhJ,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,IAAI,EAAE,MAAM,IAAI,CAAC;IACjB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,WAAW,EAAE,KAAK,CAAC;IACnB,UAAU,EAAE,KAAK,CAAC;CACnB;AA6ED,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,aAAa,EACrB,MAAM,EAAE,iBAAiB,EACzB,OAAO,GAAE,cAAmB,GAC3B,eAAe,CA6QjB"}
@@ -225,17 +225,34 @@ export function renderUnifiedSystem(system, canvas, options = {}) {
225
225
  return;
226
226
  try {
227
227
  let frameCount = 0;
228
- const p = createPreviewRuntime(canvas, scaled.renderWidth, scaled.renderHeight, system.seed);
228
+ // ╔═══════════════════════════════════════════════════════════════════════╗
229
+ // ║ ARCHITECTURAL INVARIANT — DO NOT CHANGE ║
230
+ // ║ Runtime width/height MUST equal protocol dimensions. ║
231
+ // ║ DO NOT pass renderWidth/renderHeight — breaks loop animations. ║
232
+ // ║ Scaling is handled by ctx.scale(), not by changing width/height. ║
233
+ // ╚═══════════════════════════════════════════════════════════════════════╝
234
+ const p = createPreviewRuntime(canvas, scaled.originalWidth, // ← Protocol dimension
235
+ scaled.originalHeight, // ← Protocol dimension
236
+ system.seed);
229
237
  runSetup(p);
230
238
  resetBudget(budget);
231
239
  isRunning = true;
240
+ // ╔═══════════════════════════════════════════════════════════════════════╗
241
+ // ║ ANIMATION LOOP INVARIANT — DO NOT CHANGE ║
242
+ // ║ ║
243
+ // ║ requestAnimationFrame(loop) MUST be called on EVERY tick. ║
244
+ // ║ Budget gates ONLY whether draw() executes, NOT whether loop runs. ║
245
+ // ║ Never return before scheduling RAF. Never conditionally schedule. ║
246
+ // ╚═══════════════════════════════════════════════════════════════════════╝
232
247
  const loop = () => {
248
+ // ALWAYS schedule next frame first — loop never terminates on its own
249
+ animationId = requestAnimationFrame(loop);
250
+ // Early exit checks (after RAF is scheduled)
233
251
  if (!isRunning || isDestroyed)
234
252
  return;
253
+ // Budget check — skip draw but keep loop alive
235
254
  if (!canRenderFrame(budget)) {
236
- console.log(`[UIRenderer] Budget exhausted: ${budget.exhaustionReason}`);
237
- isRunning = false;
238
- return;
255
+ return; // Loop continues, just don't draw
239
256
  }
240
257
  frameCount++;
241
258
  if (!shouldSkipFrame(frameCount)) {
@@ -251,7 +268,6 @@ export function renderUnifiedSystem(system, canvas, options = {}) {
251
268
  console.warn('[UIRenderer] Draw error:', error);
252
269
  }
253
270
  }
254
- animationId = requestAnimationFrame(loop);
255
271
  };
256
272
  animationId = requestAnimationFrame(loop);
257
273
  }
package/dist/types.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @nexart/ui-renderer v0.8.1 - Type Definitions
2
+ * @nexart/ui-renderer v0.8.3 - Type Definitions
3
3
  *
4
4
  * Lightweight Preview Runtime for NexArt Protocol.
5
5
  * This SDK is non-canonical and for preview only.
@@ -9,7 +9,7 @@
9
9
  * - Max total time: 500ms
10
10
  * - Max canvas dimension: 900px
11
11
  */
12
- export declare const SDK_VERSION = "0.8.1";
12
+ export declare const SDK_VERSION = "0.8.3";
13
13
  export declare const AESTHETIC_DEFAULTS: {
14
14
  readonly background: {
15
15
  readonly r: 246;
package/dist/types.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @nexart/ui-renderer v0.8.1 - Type Definitions
2
+ * @nexart/ui-renderer v0.8.3 - Type Definitions
3
3
  *
4
4
  * Lightweight Preview Runtime for NexArt Protocol.
5
5
  * This SDK is non-canonical and for preview only.
@@ -9,7 +9,7 @@
9
9
  * - Max total time: 500ms
10
10
  * - Max canvas dimension: 900px
11
11
  */
12
- export const SDK_VERSION = '0.8.1';
12
+ export const SDK_VERSION = '0.8.3';
13
13
  export const AESTHETIC_DEFAULTS = {
14
14
  background: { r: 246, g: 245, b: 242 },
15
15
  foreground: { r: 45, g: 45, b: 45 },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nexart/ui-renderer",
3
- "version": "0.8.1",
3
+ "version": "0.8.3",
4
4
  "description": "Lightweight Preview Runtime for NexArt Protocol. Non-canonical, performance-optimized with budget limits (max 30 frames, 500ms, 900px canvas).",
5
5
  "license": "MIT",
6
6
  "type": "module",