@nexart/ui-renderer 0.8.2 → 0.8.5

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.2
3
+ Version: 0.8.5
4
4
 
5
5
  **Lightweight Preview Runtime for NexArt Protocol**
6
6
 
@@ -19,6 +19,50 @@ Version: 0.8.2
19
19
 
20
20
  ---
21
21
 
22
+ ## v0.8.5 — Correct Animation Loop Semantics
23
+
24
+ Fixed animation loop to use proper soft-gating without resets.
25
+
26
+ - **No resets inside loop**: Budget and frameCount are never reset mid-animation
27
+ - **Soft budget gating**: Budget exhaustion skips draw(), preserving last frame
28
+ - **Modulo-based looping**: `t = (frameCount % totalFrames) / totalFrames` for natural animation cycles
29
+ - **Canvas preservation**: Skipped frames keep current pixels, no black flashes
30
+
31
+ **Correct Pattern:**
32
+ ```javascript
33
+ function loop() {
34
+ requestAnimationFrame(loop);
35
+
36
+ if (!budget.canRenderFrame()) return; // Preserve pixels
37
+
38
+ runtime.draw(); // Only changes canvas here
39
+ budget.recordFrame();
40
+ }
41
+ ```
42
+
43
+ **Behavior:**
44
+ - Renders up to 30 frames (budget limit)
45
+ - After budget exhausted: RAF continues, draw() skipped, last frame stays visible
46
+ - Restart via `stop()` → `start()` to reset and continue
47
+
48
+ ---
49
+
50
+ ## v0.8.3 — Animation Loop Fix
51
+
52
+ Fixed critical bug where preview only rendered a single frame.
53
+
54
+ - **RAF unconditionally scheduled**: `requestAnimationFrame(loop)` is now called on every tick
55
+ - **Budget gates draw only**: Frame budget controls whether `draw()` runs, not whether the loop continues
56
+ - **Continuous animation**: Loop runs forever until explicitly stopped
57
+
58
+ **Animation Loop Invariant (locked for v0.x):**
59
+ ```
60
+ RAF schedules → budget checks → draw executes (or skips) → repeat
61
+ Never: budget check → stop loop
62
+ ```
63
+
64
+ ---
65
+
22
66
  ## v0.8.2 — Runtime Dimensions Fix
23
67
 
24
68
  Fixed critical bug where preview scaling affected `width`/`height` inside sketches.
@@ -225,7 +269,7 @@ import { getCapabilities } from '@nexart/ui-renderer';
225
269
 
226
270
  const caps = getCapabilities();
227
271
  // {
228
- // version: '0.8.2',
272
+ // version: '0.8.5',
229
273
  // isCanonical: false,
230
274
  // isArchival: false,
231
275
  // 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.2
3
+ * Version: 0.8.5
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.2";
49
+ export declare const SDK_VERSION = "0.8.5";
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.2
3
+ * Version: 0.8.5
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.2';
47
+ export const SDK_VERSION = '0.8.5';
48
48
  export const PROTOCOL_VERSION = '0.8';
49
49
  export const IS_CANONICAL = false;
50
50
  export const IS_ARCHIVAL = false;
@@ -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,CA0Pd"}
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,CAyQd"}
@@ -173,34 +173,48 @@ export function renderCodeModeSystem(system, canvas, options = {}) {
173
173
  }
174
174
  resetBudget(budget);
175
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 loop continuation. ║
181
+ // ║ Never reset frameCount or budget inside the loop. ║
182
+ // ║ Canvas only changes when draw() runs — skipped frames keep pixels. ║
183
+ // ║ Looping uses modulo math (t = frame % total), not counter resets. ║
184
+ // ╚═══════════════════════════════════════════════════════════════════════╝
176
185
  const loop = () => {
186
+ // ALWAYS schedule next frame first — loop never terminates on its own
187
+ animationId = requestAnimationFrame(loop);
188
+ // Early exit checks (after RAF is scheduled)
177
189
  if (!isRunning || isDestroyed)
178
190
  return;
191
+ // Budget check — if exhausted, skip draw but keep last frame visible
179
192
  if (!canRenderFrame(budget)) {
180
- console.log(`[UIRenderer] Budget exhausted: ${budget.exhaustionReason}`);
181
- isRunning = false;
182
- return;
193
+ return; // Preserve current canvas, no clear, no reset
183
194
  }
184
195
  frameCount++;
185
- if (!shouldSkipFrame(frameCount)) {
186
- if (runtime) {
187
- runtime.frameCount = frameCount;
188
- runtime.t = (frameCount % totalFrames) / totalFrames;
189
- runtime.time = runtime.t;
190
- runtime.tGlobal = runtime.t;
191
- }
192
- try {
193
- clearCanvasIgnoringTransform(ctx, canvas);
194
- if (drawFn)
195
- drawFn();
196
- drawBadge();
197
- recordFrame(budget);
198
- }
199
- catch (error) {
200
- console.warn('[UIRenderer] Draw error:', error);
201
- }
196
+ // Frame stride check — skip some frames for performance
197
+ if (shouldSkipFrame(frameCount)) {
198
+ return; // Skip but don't clear — preserve last drawn frame
199
+ }
200
+ // Update runtime timing using modulo for natural looping
201
+ if (runtime) {
202
+ runtime.frameCount = frameCount;
203
+ runtime.t = (frameCount % totalFrames) / totalFrames;
204
+ runtime.time = runtime.t;
205
+ runtime.tGlobal = runtime.t;
206
+ }
207
+ try {
208
+ // Clear canvas and draw (only when actually rendering)
209
+ clearCanvasIgnoringTransform(ctx, canvas);
210
+ if (drawFn)
211
+ drawFn();
212
+ drawBadge();
213
+ recordFrame(budget);
214
+ }
215
+ catch (error) {
216
+ console.warn('[UIRenderer] Draw error:', error);
202
217
  }
203
- animationId = requestAnimationFrame(loop);
204
218
  };
205
219
  animationId = requestAnimationFrame(loop);
206
220
  }
@@ -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;AA8MzB;;;;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;AA2NzB;;;;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"}
@@ -132,36 +132,48 @@ class PreviewEngine {
132
132
  }
133
133
  this.scheduleNextFrame();
134
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 loop continuation. ║
140
+ // ║ Never reset frameCount or budget inside the loop. ║
141
+ // ║ Canvas only changes when draw() runs — skipped frames keep pixels. ║
142
+ // ║ Looping uses modulo math (t = frame % total), not counter resets. ║
143
+ // ╚═══════════════════════════════════════════════════════════════════════╝
135
144
  scheduleNextFrame() {
136
- if (!this.running)
137
- return;
145
+ // ALWAYS schedule next frame first — loop never terminates on its own
138
146
  this.animationFrameId = requestAnimationFrame(() => {
147
+ this.scheduleNextFrame();
148
+ // Early exit checks (after RAF is scheduled)
139
149
  if (!this.running)
140
150
  return;
151
+ // Budget check — if exhausted, skip draw but keep last frame visible
141
152
  if (!canRenderFrame(this.budget)) {
142
- this.running = false;
143
- return;
153
+ return; // Preserve current canvas, no clear, no reset
144
154
  }
145
155
  this.internalFrameCount++;
146
- if (!shouldSkipFrame(this.internalFrameCount)) {
147
- try {
148
- if (this.runtime) {
149
- this.runtime.frameCount = this.internalFrameCount;
150
- const totalFrames = this.config.totalFrames ?? 120;
151
- this.runtime.t = this.internalFrameCount / totalFrames;
152
- this.runtime.time = this.runtime.t;
153
- this.runtime.tGlobal = this.runtime.t;
154
- }
155
- if (this.drawFn) {
156
- this.drawFn();
157
- }
158
- recordFrame(this.budget);
156
+ // Frame stride check — skip some frames for performance
157
+ if (shouldSkipFrame(this.internalFrameCount)) {
158
+ return; // Skip but don't clear — preserve last drawn frame
159
+ }
160
+ // Update runtime timing using modulo for natural looping
161
+ try {
162
+ if (this.runtime) {
163
+ this.runtime.frameCount = this.internalFrameCount;
164
+ const totalFrames = this.config.totalFrames ?? 120;
165
+ this.runtime.t = (this.internalFrameCount % totalFrames) / totalFrames;
166
+ this.runtime.time = this.runtime.t;
167
+ this.runtime.tGlobal = this.runtime.t;
159
168
  }
160
- catch (error) {
161
- console.warn('[PreviewEngine] Draw error:', error);
169
+ if (this.drawFn) {
170
+ this.drawFn();
162
171
  }
172
+ recordFrame(this.budget);
173
+ }
174
+ catch (error) {
175
+ console.warn('[PreviewEngine] Draw error:', error);
163
176
  }
164
- this.scheduleNextFrame();
165
177
  });
166
178
  }
167
179
  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,CAqQjB"}
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,CAmRjB"}
@@ -237,29 +237,42 @@ export function renderUnifiedSystem(system, canvas, options = {}) {
237
237
  runSetup(p);
238
238
  resetBudget(budget);
239
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 loop continuation. ║
245
+ // ║ Never reset frameCount or budget inside the loop. ║
246
+ // ║ Canvas only changes when draw() runs — skipped frames keep pixels. ║
247
+ // ║ Looping uses modulo math (t = frame % total), not counter resets. ║
248
+ // ╚═══════════════════════════════════════════════════════════════════════╝
240
249
  const loop = () => {
250
+ // ALWAYS schedule next frame first — loop never terminates on its own
251
+ animationId = requestAnimationFrame(loop);
252
+ // Early exit checks (after RAF is scheduled)
241
253
  if (!isRunning || isDestroyed)
242
254
  return;
255
+ // Budget check — if exhausted, skip draw but keep last frame visible
243
256
  if (!canRenderFrame(budget)) {
244
- console.log(`[UIRenderer] Budget exhausted: ${budget.exhaustionReason}`);
245
- isRunning = false;
246
- return;
257
+ return; // Preserve current canvas, no clear, no reset
247
258
  }
248
259
  frameCount++;
249
- if (!shouldSkipFrame(frameCount)) {
250
- const t = (frameCount % totalFrames) / totalFrames;
251
- p.randomSeed(system.seed);
252
- p.noiseSeed(system.seed);
253
- try {
254
- runDraw(p, frameCount, t);
255
- drawBadge();
256
- recordFrame(budget);
257
- }
258
- catch (error) {
259
- console.warn('[UIRenderer] Draw error:', error);
260
- }
260
+ // Frame stride check — skip some frames for performance
261
+ if (shouldSkipFrame(frameCount)) {
262
+ return; // Skip but don't clear — preserve last drawn frame
263
+ }
264
+ // Calculate t using modulo for natural looping
265
+ const t = (frameCount % totalFrames) / totalFrames;
266
+ p.randomSeed(system.seed);
267
+ p.noiseSeed(system.seed);
268
+ try {
269
+ runDraw(p, frameCount, t);
270
+ drawBadge();
271
+ recordFrame(budget);
272
+ }
273
+ catch (error) {
274
+ console.warn('[UIRenderer] Draw error:', error);
261
275
  }
262
- animationId = requestAnimationFrame(loop);
263
276
  };
264
277
  animationId = requestAnimationFrame(loop);
265
278
  }
package/dist/types.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @nexart/ui-renderer v0.8.2 - Type Definitions
2
+ * @nexart/ui-renderer v0.8.5 - 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.2";
12
+ export declare const SDK_VERSION = "0.8.5";
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.2 - Type Definitions
2
+ * @nexart/ui-renderer v0.8.5 - 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.2';
12
+ export const SDK_VERSION = '0.8.5';
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.2",
3
+ "version": "0.8.5",
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",