@nexart/ui-renderer 0.8.5 → 0.8.7

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.5
3
+ Version: 0.8.7
4
4
 
5
5
  **Lightweight Preview Runtime for NexArt Protocol**
6
6
 
@@ -19,31 +19,35 @@ Version: 0.8.5
19
19
 
20
20
  ---
21
21
 
22
- ## v0.8.5Correct Animation Loop Semantics
22
+ ## v0.8.7Live Runtime Binding Fix
23
23
 
24
- Fixed animation loop to use proper soft-gating without resets.
24
+ Fixed critical bug where time-varying properties were frozen in loop mode.
25
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
26
+ - **Live binding**: `frameCount`, `t`, `time`, `tGlobal`, `totalFrames` now update every frame
27
+ - **Proxy + with pattern**: Uses a Proxy scope with `with()` for live property access
28
+ - **Animations work**: Sketches see correct values on every frame
30
29
 
31
- **Correct Pattern:**
30
+ **Animation Semantics:**
31
+ In loop mode, all time variables update every frame:
32
+ - `frameCount`: Increments each frame
33
+ - `t`: Normalized time `(frameCount % totalFrames) / totalFrames` (range [0,1))
34
+ - `time`: Alias for `t`
35
+ - `tGlobal`: Alias for `t`
36
+ - `totalFrames`: Total frames in the loop (default 120)
37
+
38
+ **Test Sketch:**
32
39
  ```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
+ function draw() {
41
+ background(0);
42
+ fill(255);
43
+ circle(
44
+ width / 2 + sin(frameCount * 0.1) * 300,
45
+ height / 2,
46
+ 120
47
+ );
40
48
  }
41
49
  ```
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
50
+ Expected: Circle moves horizontally, motion is continuous.
47
51
 
48
52
  ---
49
53
 
@@ -269,7 +273,7 @@ import { getCapabilities } from '@nexart/ui-renderer';
269
273
 
270
274
  const caps = getCapabilities();
271
275
  // {
272
- // version: '0.8.5',
276
+ // version: '0.8.7',
273
277
  // isCanonical: false,
274
278
  // isArchival: false,
275
279
  // 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.5
3
+ * Version: 0.8.6
4
4
  *
5
5
  * Lightweight Preview Runtime for NexArt Protocol
6
6
  *
@@ -40,13 +40,14 @@ export { compilePrimitive } from './presets/primitives';
40
40
  export { wrapSketch, validateSketchSafety } from './presets/sketch-wrapper';
41
41
  export { getCapabilities, getPrimitiveTypes, getPrimitivesByCategory, getPrimitiveInfo, isPrimitiveValid, getMotionSources, getBackgroundTextures, } from './capabilities';
42
42
  export { createPreviewEngine, renderStaticPreview, stopActivePreview, } from './preview/preview-engine';
43
- export { PREVIEW_BUDGET, CANVAS_LIMITS, type RuntimeProfile, type PreviewMode, type PreviewEngineConfig, type PreviewRenderResult, type PreviewRenderer, type FrameBudgetState, } from './preview/preview-types';
44
- export { createFrameBudget, canRenderFrame, recordFrame, resetBudget, shouldSkipFrame, } from './preview/frame-budget';
43
+ export { PREVIEW_FPS, CANVAS_LIMITS, type RuntimeProfile, type PreviewMode, type PreviewEngineConfig, type PreviewRenderResult, type PreviewRenderer, type FpsThrottleState, } from './preview/preview-types';
44
+ export { type FrameBudgetState } from './preview/frame-budget';
45
+ export { createFpsThrottle, shouldRenderFrame, recordFrame, resetThrottle, createFrameBudget, canRenderFrame, resetBudget, shouldSkipFrame, getElapsedMs, } from './preview/frame-budget';
45
46
  export { calculateScaledDimensions, applyScaledDimensions, type ScaledDimensions, } from './preview/canvas-scaler';
46
47
  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
48
  export { AESTHETIC_DEFAULTS, SDK_VERSION as TYPE_SDK_VERSION } from './types';
48
49
  export type { Capabilities, PrimitiveCapability, ParameterSpec, } from './capabilities';
49
- export declare const SDK_VERSION = "0.8.5";
50
+ export declare const SDK_VERSION = "0.8.6";
50
51
  export declare const PROTOCOL_VERSION = "0.8";
51
52
  export declare const IS_CANONICAL = false;
52
53
  export declare const IS_ARCHIVAL = false;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAC;AACpH,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAC5D,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAC/D,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AACjE,OAAO,EAAE,uBAAuB,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAClF,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AACxD,OAAO,EAAE,UAAU,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAC5E,OAAO,EACL,eAAe,EACf,iBAAiB,EACjB,uBAAuB,EACvB,gBAAgB,EAChB,gBAAgB,EAChB,gBAAgB,EAChB,qBAAqB,GACtB,MAAM,gBAAgB,CAAC;AAExB,OAAO,EACL,mBAAmB,EACnB,mBAAmB,EACnB,iBAAiB,GAClB,MAAM,0BAA0B,CAAC;AAElC,OAAO,EACL,cAAc,EACd,aAAa,EACb,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,KAAK,mBAAmB,EACxB,KAAK,mBAAmB,EACxB,KAAK,eAAe,EACpB,KAAK,gBAAgB,GACtB,MAAM,yBAAyB,CAAC;AAEjC,OAAO,EACL,iBAAiB,EACjB,cAAc,EACd,WAAW,EACX,WAAW,EACX,eAAe,GAChB,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EACL,yBAAyB,EACzB,qBAAqB,EACrB,KAAK,gBAAgB,GACtB,MAAM,yBAAyB,CAAC;AAEjC,YAAY,EACV,iBAAiB,EACjB,YAAY,EACZ,sBAAsB,EACtB,iBAAiB,EACjB,UAAU,EACV,gBAAgB,EAChB,kBAAkB,EAClB,aAAa,EACb,cAAc,EACd,iBAAiB,EACjB,gBAAgB,EAChB,aAAa,EACb,gBAAgB,EAChB,aAAa,EACb,YAAY,EACZ,WAAW,EACX,gBAAgB,EAChB,UAAU,EACV,kBAAkB,EAClB,aAAa,EACb,WAAW,EACX,YAAY,EACZ,YAAY,EACZ,WAAW,EACX,gBAAgB,EAChB,aAAa,EACb,gBAAgB,EAChB,YAAY,EACZ,cAAc,EACd,gBAAgB,GACjB,MAAM,SAAS,CAAC;AAEjB,OAAO,EAAE,kBAAkB,EAAE,WAAW,IAAI,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAE9E,YAAY,EACV,YAAY,EACZ,mBAAmB,EACnB,aAAa,GACd,MAAM,gBAAgB,CAAC;AAExB,eAAO,MAAM,WAAW,UAAU,CAAC;AACnC,eAAO,MAAM,gBAAgB,QAAQ,CAAC;AACtC,eAAO,MAAM,YAAY,QAAQ,CAAC;AAClC,eAAO,MAAM,WAAW,QAAQ,CAAC;AACjC,eAAO,MAAM,QAAQ,wBAAwB,CAAC;AAC9C,eAAO,MAAM,wBAAwB,QAAQ,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAC;AACpH,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAC5D,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAC/D,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AACjE,OAAO,EAAE,uBAAuB,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAClF,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AACxD,OAAO,EAAE,UAAU,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAC5E,OAAO,EACL,eAAe,EACf,iBAAiB,EACjB,uBAAuB,EACvB,gBAAgB,EAChB,gBAAgB,EAChB,gBAAgB,EAChB,qBAAqB,GACtB,MAAM,gBAAgB,CAAC;AAExB,OAAO,EACL,mBAAmB,EACnB,mBAAmB,EACnB,iBAAiB,GAClB,MAAM,0BAA0B,CAAC;AAElC,OAAO,EACL,WAAW,EACX,aAAa,EACb,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,KAAK,mBAAmB,EACxB,KAAK,mBAAmB,EACxB,KAAK,eAAe,EACpB,KAAK,gBAAgB,GACtB,MAAM,yBAAyB,CAAC;AAGjC,OAAO,EAAE,KAAK,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAE/D,OAAO,EACL,iBAAiB,EACjB,iBAAiB,EACjB,WAAW,EACX,aAAa,EAEb,iBAAiB,EACjB,cAAc,EACd,WAAW,EACX,eAAe,EACf,YAAY,GACb,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EACL,yBAAyB,EACzB,qBAAqB,EACrB,KAAK,gBAAgB,GACtB,MAAM,yBAAyB,CAAC;AAEjC,YAAY,EACV,iBAAiB,EACjB,YAAY,EACZ,sBAAsB,EACtB,iBAAiB,EACjB,UAAU,EACV,gBAAgB,EAChB,kBAAkB,EAClB,aAAa,EACb,cAAc,EACd,iBAAiB,EACjB,gBAAgB,EAChB,aAAa,EACb,gBAAgB,EAChB,aAAa,EACb,YAAY,EACZ,WAAW,EACX,gBAAgB,EAChB,UAAU,EACV,kBAAkB,EAClB,aAAa,EACb,WAAW,EACX,YAAY,EACZ,YAAY,EACZ,WAAW,EACX,gBAAgB,EAChB,aAAa,EACb,gBAAgB,EAChB,YAAY,EACZ,cAAc,EACd,gBAAgB,GACjB,MAAM,SAAS,CAAC;AAEjB,OAAO,EAAE,kBAAkB,EAAE,WAAW,IAAI,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAE9E,YAAY,EACV,YAAY,EACZ,mBAAmB,EACnB,aAAa,GACd,MAAM,gBAAgB,CAAC;AAExB,eAAO,MAAM,WAAW,UAAU,CAAC;AACnC,eAAO,MAAM,gBAAgB,QAAQ,CAAC;AACtC,eAAO,MAAM,YAAY,QAAQ,CAAC;AAClC,eAAO,MAAM,WAAW,QAAQ,CAAC;AACjC,eAAO,MAAM,QAAQ,wBAAwB,CAAC;AAC9C,eAAO,MAAM,wBAAwB,QAAQ,CAAC"}
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @nexart/ui-renderer
3
- * Version: 0.8.5
3
+ * Version: 0.8.6
4
4
  *
5
5
  * Lightweight Preview Runtime for NexArt Protocol
6
6
  *
@@ -40,11 +40,13 @@ export { compilePrimitive } from './presets/primitives';
40
40
  export { wrapSketch, validateSketchSafety } from './presets/sketch-wrapper';
41
41
  export { getCapabilities, getPrimitiveTypes, getPrimitivesByCategory, getPrimitiveInfo, isPrimitiveValid, getMotionSources, getBackgroundTextures, } from './capabilities';
42
42
  export { createPreviewEngine, renderStaticPreview, stopActivePreview, } from './preview/preview-engine';
43
- export { PREVIEW_BUDGET, CANVAS_LIMITS, } from './preview/preview-types';
44
- export { createFrameBudget, canRenderFrame, recordFrame, resetBudget, shouldSkipFrame, } from './preview/frame-budget';
43
+ export { PREVIEW_FPS, CANVAS_LIMITS, } from './preview/preview-types';
44
+ export { createFpsThrottle, shouldRenderFrame, recordFrame, resetThrottle,
45
+ // Deprecated exports for backward compatibility (will be removed in v0.9.0)
46
+ createFrameBudget, canRenderFrame, resetBudget, shouldSkipFrame, getElapsedMs, } from './preview/frame-budget';
45
47
  export { calculateScaledDimensions, applyScaledDimensions, } from './preview/canvas-scaler';
46
48
  export { AESTHETIC_DEFAULTS, SDK_VERSION as TYPE_SDK_VERSION } from './types';
47
- export const SDK_VERSION = '0.8.5';
49
+ export const SDK_VERSION = '0.8.6';
48
50
  export const PROTOCOL_VERSION = '0.8';
49
51
  export const IS_CANONICAL = false;
50
52
  export const IS_ARCHIVAL = false;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @nexart/ui-renderer v0.8.0 - Code Mode Renderer
2
+ * @nexart/ui-renderer v0.8.7 - Code Mode Renderer
3
3
  *
4
4
  * ╔══════════════════════════════════════════════════════════════════════════╗
5
5
  * ║ PREVIEW RENDERER — LIGHTWEIGHT, NON-AUTHORITATIVE ║
@@ -7,11 +7,8 @@
7
7
  * ║ This renderer is a preview-only runtime. ║
8
8
  * ║ It does not guarantee determinism or protocol compliance. ║
9
9
  * ║ ║
10
- * ║ Performance Limits (MANDATORY):
11
- * ║ - Max frames: 30
12
- * ║ - Max total time: 500ms ║
13
- * ║ - Max canvas dimension: 900px ║
14
- * ║ - Frame stride: render every 3rd frame ║
10
+ * ║ Performance: FPS-capped at 8 FPS, canvas max 900px
11
+ * ║ Animation: Runs continuously until stop() is called
15
12
  * ║ ║
16
13
  * ║ For minting, export, or validation: use @nexart/codemode-sdk ║
17
14
  * ╚══════════════════════════════════════════════════════════════════════════╝
@@ -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,CAyQd"}
1
+ {"version":3,"file":"code-renderer.d.ts","sourceRoot":"","sources":["../../src/preview/code-renderer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAwBjE,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,CAoTd"}
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @nexart/ui-renderer v0.8.0 - Code Mode Renderer
2
+ * @nexart/ui-renderer v0.8.7 - Code Mode Renderer
3
3
  *
4
4
  * ╔══════════════════════════════════════════════════════════════════════════╗
5
5
  * ║ PREVIEW RENDERER — LIGHTWEIGHT, NON-AUTHORITATIVE ║
@@ -7,17 +7,14 @@
7
7
  * ║ This renderer is a preview-only runtime. ║
8
8
  * ║ It does not guarantee determinism or protocol compliance. ║
9
9
  * ║ ║
10
- * ║ Performance Limits (MANDATORY):
11
- * ║ - Max frames: 30
12
- * ║ - Max total time: 500ms ║
13
- * ║ - Max canvas dimension: 900px ║
14
- * ║ - Frame stride: render every 3rd frame ║
10
+ * ║ Performance: FPS-capped at 8 FPS, canvas max 900px
11
+ * ║ Animation: Runs continuously until stop() is called
15
12
  * ║ ║
16
13
  * ║ For minting, export, or validation: use @nexart/codemode-sdk ║
17
14
  * ╚══════════════════════════════════════════════════════════════════════════╝
18
15
  */
19
- import { PREVIEW_BUDGET, } from './preview-types';
20
- import { createFrameBudget, canRenderFrame, recordFrame, resetBudget, shouldSkipFrame, } from './frame-budget';
16
+ import { PREVIEW_FPS, CANVAS_LIMITS, } from './preview-types';
17
+ import { createFpsThrottle, shouldRenderFrame, recordFrame, resetThrottle, } from './frame-budget';
21
18
  import { calculateScaledDimensions, applyScaledDimensions, reapplyContextScale, clearCanvasIgnoringTransform, } from './canvas-scaler';
22
19
  import { createPreviewRuntime } from './preview-runtime';
23
20
  const PROTOCOL_VERSION = '1.2.0';
@@ -45,8 +42,8 @@ function normalizeVars(vars) {
45
42
  return result;
46
43
  }
47
44
  export function renderCodeModeSystem(system, canvas, options = {}) {
48
- console.log('[UIRenderer] Preview mode → lightweight runtime with budget limits');
49
- console.log(`[UIRenderer] Budget: max ${PREVIEW_BUDGET.MAX_FRAMES} frames, ${PREVIEW_BUDGET.MAX_TOTAL_TIME_MS}ms`);
45
+ console.log('[UIRenderer] Preview mode → FPS-capped continuous animation');
46
+ console.log(`[UIRenderer] Target: ${PREVIEW_FPS.TARGET_FPS} FPS, canvas max ${CANVAS_LIMITS.MAX_DIMENSION}px`);
50
47
  if (activeRendererInstance) {
51
48
  activeRendererInstance.destroy();
52
49
  activeRendererInstance = null;
@@ -64,7 +61,7 @@ export function renderCodeModeSystem(system, canvas, options = {}) {
64
61
  let animationId = null;
65
62
  let isRunning = false;
66
63
  let isDestroyed = false;
67
- const budget = createFrameBudget();
64
+ const throttle = createFpsThrottle();
68
65
  const normalizedVars = normalizeVars(system.vars);
69
66
  let runtime = null;
70
67
  let setupFn = null;
@@ -88,19 +85,68 @@ export function renderCodeModeSystem(system, canvas, options = {}) {
88
85
  const totalFrames = system.totalFrames ?? 120;
89
86
  runtime.totalFrames = totalFrames;
90
87
  try {
91
- const globalVars = Object.keys(runtime);
92
- const globalValues = Object.values(runtime);
93
- const wrappedSource = `
94
- ${system.source}
95
- if (typeof setup === 'function') __registerSetup(setup);
96
- if (typeof draw === 'function') __registerDraw(draw);
97
- `;
88
+ // ╔═══════════════════════════════════════════════════════════════════════╗
89
+ // ║ LIVE RUNTIME BINDING — v0.8.7 FIX ║
90
+ // ║ ║
91
+ // ║ Time-varying properties (frameCount, t, time, tGlobal, totalFrames) ║
92
+ // ║ must be accessed via getters that read from the live runtime object. ║
93
+ // ║ ║
94
+ // ║ We use a Proxy + with() pattern to enable bare-name access to live ║
95
+ // ║ runtime properties. The proxy's get trap reads from runtime for ║
96
+ // ║ time-varying props, and from a static cache for everything else. ║
97
+ // ╚═══════════════════════════════════════════════════════════════════════╝
98
+ // Time-varying properties that need live access
99
+ const liveProps = new Set(['frameCount', 't', 'time', 'tGlobal', 'totalFrames']);
100
+ // Cache static properties (functions, constants)
101
+ const staticCache = {};
102
+ for (const key of Object.keys(runtime)) {
103
+ if (!liveProps.has(key)) {
104
+ staticCache[key] = runtime[key];
105
+ }
106
+ }
98
107
  const registerSetup = (fn) => { setupFn = fn; };
99
108
  const registerDraw = (fn) => { drawFn = fn; };
100
- globalVars.push('__registerSetup', '__registerDraw');
101
- globalValues.push(registerSetup, registerDraw);
102
- const fn = new Function(...globalVars, wrappedSource);
103
- fn(...globalValues);
109
+ // Build the set of all known keys (for has trap)
110
+ const knownKeys = new Set([
111
+ ...liveProps,
112
+ ...Object.keys(staticCache),
113
+ '__registerSetup',
114
+ '__registerDraw'
115
+ ]);
116
+ // Create a proxy that provides live access to time-varying properties
117
+ // IMPORTANT: has() must return true ONLY for known keys
118
+ // Otherwise globals (Math, window, etc.) are masked and become undefined
119
+ const runtimeRef = runtime;
120
+ const scope = new Proxy({}, {
121
+ has: (_, prop) => knownKeys.has(prop),
122
+ get: (_, prop) => {
123
+ // Time-varying properties - read live from runtime
124
+ if (liveProps.has(prop)) {
125
+ return runtimeRef[prop];
126
+ }
127
+ // Special functions
128
+ if (prop === '__registerSetup')
129
+ return registerSetup;
130
+ if (prop === '__registerDraw')
131
+ return registerDraw;
132
+ // Static properties from cache
133
+ if (prop in staticCache)
134
+ return staticCache[prop];
135
+ // Fall through to undefined (should not hit this if has() is correct)
136
+ return undefined;
137
+ }
138
+ });
139
+ // Use with() to make the proxy scope available to bare variable names
140
+ // Note: with() is safe here since we control the scope completely
141
+ const wrappedSource = `
142
+ with (__scope) {
143
+ ${system.source}
144
+ if (typeof setup === 'function') __registerSetup(setup);
145
+ if (typeof draw === 'function') __registerDraw(draw);
146
+ }
147
+ `;
148
+ const fn = new Function('__scope', wrappedSource);
149
+ fn(scope);
104
150
  }
105
151
  catch (error) {
106
152
  console.warn('[UIRenderer] Compile error:', error);
@@ -171,32 +217,27 @@ export function renderCodeModeSystem(system, canvas, options = {}) {
171
217
  if (setupFn) {
172
218
  setupFn();
173
219
  }
174
- resetBudget(budget);
220
+ resetThrottle(throttle);
175
221
  isRunning = true;
176
222
  // ╔═══════════════════════════════════════════════════════════════════════╗
177
- // ║ ANIMATION LOOP INVARIANT DO NOT CHANGE
223
+ // ║ ANIMATION LOOP — CONTINUOUS FPS-CAPPED RENDERING
178
224
  // ║ ║
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. ║
225
+ // ║ requestAnimationFrame runs continuously until stop() is called.
226
+ // ║ FPS throttle (8 FPS) prevents excessive CPU usage.
227
+ // ║ Looping uses modulo math: t = (frame % total) / total
228
+ // ║ Canvas cleared before each draw; skipped frames preserve pixels.
184
229
  // ╚═══════════════════════════════════════════════════════════════════════╝
185
230
  const loop = () => {
186
- // ALWAYS schedule next frame first — loop never terminates on its own
231
+ // Schedule next frame first — loop runs until stop()
187
232
  animationId = requestAnimationFrame(loop);
188
- // Early exit checks (after RAF is scheduled)
233
+ // Exit if stopped or destroyed
189
234
  if (!isRunning || isDestroyed)
190
235
  return;
191
- // Budget checkif exhausted, skip draw but keep last frame visible
192
- if (!canRenderFrame(budget)) {
193
- return; // Preserve current canvas, no clear, no reset
236
+ // FPS throttle — skip if not enough time has passed
237
+ if (!shouldRenderFrame(throttle)) {
238
+ return; // Preserve current canvas
194
239
  }
195
240
  frameCount++;
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
241
  // Update runtime timing using modulo for natural looping
201
242
  if (runtime) {
202
243
  runtime.frameCount = frameCount;
@@ -205,12 +246,12 @@ export function renderCodeModeSystem(system, canvas, options = {}) {
205
246
  runtime.tGlobal = runtime.t;
206
247
  }
207
248
  try {
208
- // Clear canvas and draw (only when actually rendering)
249
+ // Clear and draw
209
250
  clearCanvasIgnoringTransform(ctx, canvas);
210
251
  if (drawFn)
211
252
  drawFn();
212
253
  drawBadge();
213
- recordFrame(budget);
254
+ recordFrame(throttle);
214
255
  }
215
256
  catch (error) {
216
257
  console.warn('[UIRenderer] Draw error:', error);
@@ -1,43 +1,57 @@
1
1
  /**
2
- * @nexart/ui-renderer - Frame Budget Manager
2
+ * @nexart/ui-renderer - FPS Throttle
3
3
  *
4
- * Enforces hard execution limits to prevent browser freezes.
4
+ * Simple FPS cap for continuous preview animation.
5
+ * No frame limits, no time limits — just throttled rendering.
5
6
  *
6
7
  * ╔══════════════════════════════════════════════════════════════════════════╗
7
- * ║ EXECUTION BUDGETMANDATORY LIMITS
8
+ * ║ FPS THROTTLECONTINUOUS ANIMATION
8
9
  * ║ ║
9
- * ║ Max frames: 30
10
- * ║ Max total time: 500ms
11
- * ║ Target frame time: ~16ms
12
- * ║ ║
13
- * ║ If limits exceeded: stop immediately, do NOT throw. ║
10
+ * ║ Target FPS: 8 (125ms per frame)
11
+ * ║ No frame limit — runs until stop() is called
12
+ * ║ No time limit — runs indefinitely
14
13
  * ╚══════════════════════════════════════════════════════════════════════════╝
15
14
  */
16
- import { type FrameBudgetState } from './preview-types';
15
+ import { type FpsThrottleState } from './preview-types';
17
16
  /**
18
- * Create a new frame budget tracker
17
+ * Create a new FPS throttle state
19
18
  */
20
- export declare function createFrameBudget(): FrameBudgetState;
19
+ export declare function createFpsThrottle(): FpsThrottleState;
21
20
  /**
22
- * Check if budget allows another frame.
23
- * Returns true if we can render, false if budget is exhausted.
21
+ * Check if enough time has passed to render the next frame.
22
+ * Returns true if we should render, false if we should skip this RAF tick.
24
23
  */
25
- export declare function canRenderFrame(budget: FrameBudgetState): boolean;
24
+ export declare function shouldRenderFrame(throttle: FpsThrottleState): boolean;
26
25
  /**
27
- * Record that a frame was rendered
26
+ * Record that a frame was rendered (updates last frame time)
28
27
  */
29
- export declare function recordFrame(budget: FrameBudgetState): void;
28
+ export declare function recordFrame(throttle: FpsThrottleState): void;
30
29
  /**
31
- * Get elapsed time in milliseconds
30
+ * Reset the throttle for a new render session
32
31
  */
33
- export declare function getElapsedMs(budget: FrameBudgetState): number;
32
+ export declare function resetThrottle(throttle: FpsThrottleState): void;
34
33
  /**
35
- * Reset the frame budget for a new render cycle
34
+ * @deprecated Legacy type alias for backward compatibility.
35
+ * Use FpsThrottleState instead.
36
36
  */
37
- export declare function resetBudget(budget: FrameBudgetState): void;
37
+ export type FrameBudgetState = FpsThrottleState;
38
+ /** @deprecated Use createFpsThrottle instead */
39
+ export declare function createFrameBudget(): FpsThrottleState;
38
40
  /**
39
- * Check if we should skip this frame for performance.
40
- * Uses frame stride to reduce rendering load while keeping animation smooth.
41
+ * @deprecated Use shouldRenderFrame instead.
42
+ *
43
+ * COMPATIBILITY NOTE: This now delegates to shouldRenderFrame.
44
+ * For legacy code that interprets `false` as "budget exhausted forever",
45
+ * this will work correctly because:
46
+ * - Returns true when enough time has passed (ready to render)
47
+ * - Returns false when throttled (wait for next tick)
48
+ * - Never permanently exhausts like the old budget system
41
49
  */
42
- export declare function shouldSkipFrame(frameCount: number): boolean;
50
+ export declare function canRenderFrame(throttle: FpsThrottleState): boolean;
51
+ /** @deprecated Use resetThrottle instead */
52
+ export declare function resetBudget(throttle: FpsThrottleState): void;
53
+ /** @deprecated No longer used — removed frame stride. Always returns false. */
54
+ export declare function shouldSkipFrame(_frameCount: number): boolean;
55
+ /** @deprecated No longer tracked. Returns 0. */
56
+ export declare function getElapsedMs(_throttle: FpsThrottleState): number;
43
57
  //# sourceMappingURL=frame-budget.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"frame-budget.d.ts","sourceRoot":"","sources":["../../src/preview/frame-budget.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAkB,KAAK,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAExE;;GAEG;AACH,wBAAgB,iBAAiB,IAAI,gBAAgB,CAOpD;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAqBhE;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,gBAAgB,GAAG,IAAI,CAE1D;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,gBAAgB,GAAG,MAAM,CAE7D;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,gBAAgB,GAAG,IAAI,CAK1D;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAE3D"}
1
+ {"version":3,"file":"frame-budget.d.ts","sourceRoot":"","sources":["../../src/preview/frame-budget.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAe,KAAK,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAErE;;GAEG;AACH,wBAAgB,iBAAiB,IAAI,gBAAgB,CAIpD;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,gBAAgB,GAAG,OAAO,CAIrE;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,gBAAgB,GAAG,IAAI,CAE5D;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,gBAAgB,GAAG,IAAI,CAE9D;AAOD;;;GAGG;AACH,MAAM,MAAM,gBAAgB,GAAG,gBAAgB,CAAC;AAEhD,gDAAgD;AAChD,wBAAgB,iBAAiB,IAAI,gBAAgB,CAEpD;AAED;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,gBAAgB,GAAG,OAAO,CAElE;AAED,4CAA4C;AAC5C,wBAAgB,WAAW,CAAC,QAAQ,EAAE,gBAAgB,GAAG,IAAI,CAE5D;AAED,+EAA+E;AAC/E,wBAAgB,eAAe,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAE5D;AAED,gDAAgD;AAChD,wBAAgB,YAAY,CAAC,SAAS,EAAE,gBAAgB,GAAG,MAAM,CAEhE"}
@@ -1,78 +1,73 @@
1
1
  /**
2
- * @nexart/ui-renderer - Frame Budget Manager
2
+ * @nexart/ui-renderer - FPS Throttle
3
3
  *
4
- * Enforces hard execution limits to prevent browser freezes.
4
+ * Simple FPS cap for continuous preview animation.
5
+ * No frame limits, no time limits — just throttled rendering.
5
6
  *
6
7
  * ╔══════════════════════════════════════════════════════════════════════════╗
7
- * ║ EXECUTION BUDGETMANDATORY LIMITS
8
+ * ║ FPS THROTTLECONTINUOUS ANIMATION
8
9
  * ║ ║
9
- * ║ Max frames: 30
10
- * ║ Max total time: 500ms
11
- * ║ Target frame time: ~16ms
12
- * ║ ║
13
- * ║ If limits exceeded: stop immediately, do NOT throw. ║
10
+ * ║ Target FPS: 8 (125ms per frame)
11
+ * ║ No frame limit — runs until stop() is called
12
+ * ║ No time limit — runs indefinitely
14
13
  * ╚══════════════════════════════════════════════════════════════════════════╝
15
14
  */
16
- import { PREVIEW_BUDGET } from './preview-types';
15
+ import { PREVIEW_FPS } from './preview-types';
17
16
  /**
18
- * Create a new frame budget tracker
17
+ * Create a new FPS throttle state
19
18
  */
20
- export function createFrameBudget() {
19
+ export function createFpsThrottle() {
21
20
  return {
22
- framesRendered: 0,
23
- startTimeMs: performance.now(),
24
- exhausted: false,
25
- exhaustionReason: undefined,
21
+ lastFrameTimeMs: 0,
26
22
  };
27
23
  }
28
24
  /**
29
- * Check if budget allows another frame.
30
- * Returns true if we can render, false if budget is exhausted.
25
+ * Check if enough time has passed to render the next frame.
26
+ * Returns true if we should render, false if we should skip this RAF tick.
31
27
  */
32
- export function canRenderFrame(budget) {
33
- if (budget.exhausted) {
34
- return false;
35
- }
36
- // Check frame limit
37
- if (budget.framesRendered >= PREVIEW_BUDGET.MAX_FRAMES) {
38
- budget.exhausted = true;
39
- budget.exhaustionReason = 'frame_limit';
40
- return false;
41
- }
42
- // Check time limit
43
- const elapsedMs = performance.now() - budget.startTimeMs;
44
- if (elapsedMs >= PREVIEW_BUDGET.MAX_TOTAL_TIME_MS) {
45
- budget.exhausted = true;
46
- budget.exhaustionReason = 'time_limit';
47
- return false;
48
- }
49
- return true;
28
+ export function shouldRenderFrame(throttle) {
29
+ const now = performance.now();
30
+ const elapsed = now - throttle.lastFrameTimeMs;
31
+ return elapsed >= PREVIEW_FPS.FRAME_INTERVAL_MS;
50
32
  }
51
33
  /**
52
- * Record that a frame was rendered
34
+ * Record that a frame was rendered (updates last frame time)
53
35
  */
54
- export function recordFrame(budget) {
55
- budget.framesRendered++;
36
+ export function recordFrame(throttle) {
37
+ throttle.lastFrameTimeMs = performance.now();
56
38
  }
57
39
  /**
58
- * Get elapsed time in milliseconds
40
+ * Reset the throttle for a new render session
59
41
  */
60
- export function getElapsedMs(budget) {
61
- return performance.now() - budget.startTimeMs;
42
+ export function resetThrottle(throttle) {
43
+ throttle.lastFrameTimeMs = 0;
62
44
  }
63
- /**
64
- * Reset the frame budget for a new render cycle
65
- */
66
- export function resetBudget(budget) {
67
- budget.framesRendered = 0;
68
- budget.startTimeMs = performance.now();
69
- budget.exhausted = false;
70
- budget.exhaustionReason = undefined;
45
+ /** @deprecated Use createFpsThrottle instead */
46
+ export function createFrameBudget() {
47
+ return createFpsThrottle();
71
48
  }
72
49
  /**
73
- * Check if we should skip this frame for performance.
74
- * Uses frame stride to reduce rendering load while keeping animation smooth.
50
+ * @deprecated Use shouldRenderFrame instead.
51
+ *
52
+ * COMPATIBILITY NOTE: This now delegates to shouldRenderFrame.
53
+ * For legacy code that interprets `false` as "budget exhausted forever",
54
+ * this will work correctly because:
55
+ * - Returns true when enough time has passed (ready to render)
56
+ * - Returns false when throttled (wait for next tick)
57
+ * - Never permanently exhausts like the old budget system
75
58
  */
76
- export function shouldSkipFrame(frameCount) {
77
- return frameCount % PREVIEW_BUDGET.FRAME_STRIDE !== 0;
59
+ export function canRenderFrame(throttle) {
60
+ return shouldRenderFrame(throttle);
61
+ }
62
+ /** @deprecated Use resetThrottle instead */
63
+ export function resetBudget(throttle) {
64
+ resetThrottle(throttle);
65
+ }
66
+ /** @deprecated No longer used — removed frame stride. Always returns false. */
67
+ export function shouldSkipFrame(_frameCount) {
68
+ return false; // Never skip based on frame count
69
+ }
70
+ /** @deprecated No longer tracked. Returns 0. */
71
+ export function getElapsedMs(_throttle) {
72
+ return 0;
78
73
  }
@@ -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;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"}
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;AAiNzB;;;;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"}
@@ -24,7 +24,7 @@
24
24
  * ║ For canonical output: use @nexart/codemode-sdk ║
25
25
  * ╚══════════════════════════════════════════════════════════════════════════╝
26
26
  */
27
- import { createFrameBudget, canRenderFrame, recordFrame, getElapsedMs, resetBudget, shouldSkipFrame, } from './frame-budget';
27
+ import { createFpsThrottle, shouldRenderFrame, recordFrame, resetThrottle, } from './frame-budget';
28
28
  import { calculateScaledDimensions, applyScaledDimensions, reapplyContextScale, } from './canvas-scaler';
29
29
  import { createPreviewRuntime } from './preview-runtime';
30
30
  let activePreviewRenderer = null;
@@ -34,7 +34,7 @@ class PreviewEngine {
34
34
  this.setupFn = null;
35
35
  this.drawFn = null;
36
36
  this.animationFrameId = null;
37
- this.budget = createFrameBudget();
37
+ this.throttle = createFpsThrottle();
38
38
  this.running = false;
39
39
  this.internalFrameCount = 0;
40
40
  this.isCanonical = false;
@@ -87,16 +87,15 @@ class PreviewEngine {
87
87
  return () => fn(...globalValues);
88
88
  }
89
89
  renderStatic() {
90
- resetBudget(this.budget);
90
+ const startTime = performance.now();
91
91
  try {
92
92
  if (this.setupFn) {
93
93
  this.setupFn();
94
94
  }
95
- recordFrame(this.budget);
96
95
  return {
97
96
  success: true,
98
97
  framesRendered: 1,
99
- executionTimeMs: getElapsedMs(this.budget),
98
+ executionTimeMs: performance.now() - startTime,
100
99
  terminatedEarly: false,
101
100
  };
102
101
  }
@@ -105,7 +104,7 @@ class PreviewEngine {
105
104
  return {
106
105
  success: false,
107
106
  framesRendered: 0,
108
- executionTimeMs: getElapsedMs(this.budget),
107
+ executionTimeMs: performance.now() - startTime,
109
108
  terminatedEarly: true,
110
109
  terminationReason: 'error',
111
110
  errorMessage: error instanceof Error ? error.message : String(error),
@@ -120,7 +119,7 @@ class PreviewEngine {
120
119
  }
121
120
  activePreviewRenderer = this;
122
121
  this.running = true;
123
- resetBudget(this.budget);
122
+ resetThrottle(this.throttle);
124
123
  this.internalFrameCount = 0;
125
124
  try {
126
125
  if (this.setupFn) {
@@ -133,30 +132,24 @@ class PreviewEngine {
133
132
  this.scheduleNextFrame();
134
133
  }
135
134
  // ╔═══════════════════════════════════════════════════════════════════════╗
136
- // ║ ANIMATION LOOP INVARIANT DO NOT CHANGE
135
+ // ║ ANIMATION LOOP — CONTINUOUS FPS-CAPPED RENDERING
137
136
  // ║ ║
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. ║
137
+ // ║ requestAnimationFrame runs continuously until stop() is called.
138
+ // ║ FPS throttle (8 FPS) prevents excessive CPU usage.
139
+ // ║ Looping uses modulo math: t = (frame % total) / total
143
140
  // ╚═══════════════════════════════════════════════════════════════════════╝
144
141
  scheduleNextFrame() {
145
- // ALWAYS schedule next frame first — loop never terminates on its own
142
+ // Schedule next frame — loop runs until stopLoop()
146
143
  this.animationFrameId = requestAnimationFrame(() => {
147
144
  this.scheduleNextFrame();
148
- // Early exit checks (after RAF is scheduled)
145
+ // Exit if stopped
149
146
  if (!this.running)
150
147
  return;
151
- // Budget checkif exhausted, skip draw but keep last frame visible
152
- if (!canRenderFrame(this.budget)) {
153
- return; // Preserve current canvas, no clear, no reset
148
+ // FPS throttle — skip if not enough time has passed
149
+ if (!shouldRenderFrame(this.throttle)) {
150
+ return; // Preserve current canvas
154
151
  }
155
152
  this.internalFrameCount++;
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
153
  // Update runtime timing using modulo for natural looping
161
154
  try {
162
155
  if (this.runtime) {
@@ -169,7 +162,7 @@ class PreviewEngine {
169
162
  if (this.drawFn) {
170
163
  this.drawFn();
171
164
  }
172
- recordFrame(this.budget);
165
+ recordFrame(this.throttle);
173
166
  }
174
167
  catch (error) {
175
168
  console.warn('[PreviewEngine] Draw error:', error);
@@ -16,18 +16,14 @@
16
16
  */
17
17
  export type RuntimeProfile = 'preview';
18
18
  /**
19
- * Execution budget limits (MANDATORY).
20
- * These prevent browser freezes and ensure UI responsiveness.
19
+ * FPS cap for preview rendering.
20
+ * Replaced time/frame budget with simple FPS throttling for continuous animation.
21
21
  */
22
- export declare const PREVIEW_BUDGET: {
23
- /** Maximum frames rendered before auto-termination */
24
- readonly MAX_FRAMES: 30;
25
- /** Maximum total execution time in milliseconds */
26
- readonly MAX_TOTAL_TIME_MS: 500;
27
- /** Target frame time (best effort, ~60fps) */
28
- readonly TARGET_FRAME_TIME_MS: 16;
29
- /** Frame stride for performance (render every Nth frame) */
30
- readonly FRAME_STRIDE: 3;
22
+ export declare const PREVIEW_FPS: {
23
+ /** Target FPS for loop animations (lower = better performance) */
24
+ readonly TARGET_FPS: 8;
25
+ /** Minimum milliseconds between frames */
26
+ readonly FRAME_INTERVAL_MS: 125;
31
27
  };
32
28
  /**
33
29
  * Canvas resolution limits.
@@ -101,16 +97,10 @@ export interface PreviewRenderer {
101
97
  readonly isArchival: false;
102
98
  }
103
99
  /**
104
- * Frame budget state for tracking execution limits
100
+ * FPS throttle state for continuous animation
105
101
  */
106
- export interface FrameBudgetState {
107
- /** Frames rendered so far */
108
- framesRendered: number;
109
- /** Start time of execution */
110
- startTimeMs: number;
111
- /** Whether budget is exhausted */
112
- exhausted: boolean;
113
- /** Reason for exhaustion */
114
- exhaustionReason?: 'frame_limit' | 'time_limit';
102
+ export interface FpsThrottleState {
103
+ /** Timestamp of last rendered frame */
104
+ lastFrameTimeMs: number;
115
105
  }
116
106
  //# sourceMappingURL=preview-types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"preview-types.d.ts","sourceRoot":"","sources":["../../src/preview/preview-types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH;;;GAGG;AACH,MAAM,MAAM,cAAc,GAAG,SAAS,CAAC;AAEvC;;;GAGG;AACH,eAAO,MAAM,cAAc;IACzB,sDAAsD;;IAEtD,mDAAmD;;IAEnD,8CAA8C;;IAE9C,4DAA4D;;CAEpD,CAAC;AAEX;;;GAGG;AACH,eAAO,MAAM,aAAa;IACxB,2CAA2C;;IAE3C,wBAAwB;;CAEhB,CAAC;AAEX;;GAEG;AACH,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,MAAM,CAAC;AAE5C;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,kCAAkC;IAClC,MAAM,EAAE,iBAAiB,CAAC;IAC1B,6BAA6B;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,mBAAmB;IACnB,IAAI,EAAE,WAAW,CAAC;IAClB,2CAA2C;IAC3C,KAAK,EAAE,MAAM,CAAC;IACd,4CAA4C;IAC5C,MAAM,EAAE,MAAM,CAAC;IACf,4CAA4C;IAC5C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,6CAA6C;IAC7C,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,qDAAqD;IACrD,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,+CAA+C;IAC/C,OAAO,EAAE,OAAO,CAAC;IACjB,sBAAsB;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,iCAAiC;IACjC,eAAe,EAAE,MAAM,CAAC;IACxB,2DAA2D;IAC3D,eAAe,EAAE,OAAO,CAAC;IACzB,uCAAuC;IACvC,iBAAiB,CAAC,EAAE,aAAa,GAAG,YAAY,GAAG,WAAW,GAAG,OAAO,CAAC;IACzE,0CAA0C;IAC1C,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,0CAA0C;IAC1C,YAAY,EAAE,MAAM,mBAAmB,CAAC;IACxC,2BAA2B;IAC3B,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB,0BAA0B;IAC1B,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,oCAAoC;IACpC,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,mCAAmC;IACnC,WAAW,EAAE,MAAM,OAAO,CAAC;IAC3B,qCAAqC;IACrC,QAAQ,CAAC,WAAW,EAAE,KAAK,CAAC;IAC5B,oCAAoC;IACpC,QAAQ,CAAC,UAAU,EAAE,KAAK,CAAC;CAC5B;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,6BAA6B;IAC7B,cAAc,EAAE,MAAM,CAAC;IACvB,8BAA8B;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB,kCAAkC;IAClC,SAAS,EAAE,OAAO,CAAC;IACnB,4BAA4B;IAC5B,gBAAgB,CAAC,EAAE,aAAa,GAAG,YAAY,CAAC;CACjD"}
1
+ {"version":3,"file":"preview-types.d.ts","sourceRoot":"","sources":["../../src/preview/preview-types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH;;;GAGG;AACH,MAAM,MAAM,cAAc,GAAG,SAAS,CAAC;AAEvC;;;GAGG;AACH,eAAO,MAAM,WAAW;IACtB,kEAAkE;;IAElE,0CAA0C;;CAElC,CAAC;AAEX;;;GAGG;AACH,eAAO,MAAM,aAAa;IACxB,2CAA2C;;IAE3C,wBAAwB;;CAEhB,CAAC;AAEX;;GAEG;AACH,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,MAAM,CAAC;AAE5C;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,kCAAkC;IAClC,MAAM,EAAE,iBAAiB,CAAC;IAC1B,6BAA6B;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,mBAAmB;IACnB,IAAI,EAAE,WAAW,CAAC;IAClB,2CAA2C;IAC3C,KAAK,EAAE,MAAM,CAAC;IACd,4CAA4C;IAC5C,MAAM,EAAE,MAAM,CAAC;IACf,4CAA4C;IAC5C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,6CAA6C;IAC7C,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,qDAAqD;IACrD,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,+CAA+C;IAC/C,OAAO,EAAE,OAAO,CAAC;IACjB,sBAAsB;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,iCAAiC;IACjC,eAAe,EAAE,MAAM,CAAC;IACxB,2DAA2D;IAC3D,eAAe,EAAE,OAAO,CAAC;IACzB,uCAAuC;IACvC,iBAAiB,CAAC,EAAE,aAAa,GAAG,YAAY,GAAG,WAAW,GAAG,OAAO,CAAC;IACzE,0CAA0C;IAC1C,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,0CAA0C;IAC1C,YAAY,EAAE,MAAM,mBAAmB,CAAC;IACxC,2BAA2B;IAC3B,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB,0BAA0B;IAC1B,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,oCAAoC;IACpC,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,mCAAmC;IACnC,WAAW,EAAE,MAAM,OAAO,CAAC;IAC3B,qCAAqC;IACrC,QAAQ,CAAC,WAAW,EAAE,KAAK,CAAC;IAC5B,oCAAoC;IACpC,QAAQ,CAAC,UAAU,EAAE,KAAK,CAAC;CAC5B;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,uCAAuC;IACvC,eAAe,EAAE,MAAM,CAAC;CACzB"}
@@ -11,18 +11,14 @@
11
11
  * ╚══════════════════════════════════════════════════════════════════════════╝
12
12
  */
13
13
  /**
14
- * Execution budget limits (MANDATORY).
15
- * These prevent browser freezes and ensure UI responsiveness.
14
+ * FPS cap for preview rendering.
15
+ * Replaced time/frame budget with simple FPS throttling for continuous animation.
16
16
  */
17
- export const PREVIEW_BUDGET = {
18
- /** Maximum frames rendered before auto-termination */
19
- MAX_FRAMES: 30,
20
- /** Maximum total execution time in milliseconds */
21
- MAX_TOTAL_TIME_MS: 500,
22
- /** Target frame time (best effort, ~60fps) */
23
- TARGET_FRAME_TIME_MS: 16,
24
- /** Frame stride for performance (render every Nth frame) */
25
- FRAME_STRIDE: 3,
17
+ export const PREVIEW_FPS = {
18
+ /** Target FPS for loop animations (lower = better performance) */
19
+ TARGET_FPS: 8,
20
+ /** Minimum milliseconds between frames */
21
+ FRAME_INTERVAL_MS: 125, // 1000 / 8 = 125ms per frame
26
22
  };
27
23
  /**
28
24
  * Canvas resolution limits.
@@ -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,CAmRjB"}
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,CA4QjB"}
@@ -8,8 +8,8 @@ import { compilePrimitive } from '../presets/primitives';
8
8
  import { wrapSketch, validateSketchSafety } from '../presets/sketch-wrapper';
9
9
  import { createPreviewRuntime } from './preview-runtime';
10
10
  import { calculateScaledDimensions, applyScaledDimensions, reapplyContextScale, clearCanvasIgnoringTransform } from './canvas-scaler';
11
- import { createFrameBudget, canRenderFrame, recordFrame, resetBudget, shouldSkipFrame } from './frame-budget';
12
- import { PREVIEW_BUDGET } from './preview-types';
11
+ import { createFpsThrottle, shouldRenderFrame, recordFrame, resetThrottle } from './frame-budget';
12
+ import { PREVIEW_FPS, CANVAS_LIMITS } from './preview-types';
13
13
  function validateSketchElement(element) {
14
14
  const { safe, warnings } = validateSketchSafety(element.code);
15
15
  if (!safe) {
@@ -81,8 +81,8 @@ function compileSketchElement(element) {
81
81
  };
82
82
  }
83
83
  export function renderUnifiedSystem(system, canvas, options = {}) {
84
- console.log('[UIRenderer] Unified preview → lightweight runtime with budget limits');
85
- console.log(`[UIRenderer] Budget: max ${PREVIEW_BUDGET.MAX_FRAMES} frames, ${PREVIEW_BUDGET.MAX_TOTAL_TIME_MS}ms`);
84
+ console.log('[UIRenderer] Unified preview → FPS-capped continuous animation');
85
+ console.log(`[UIRenderer] Target: ${PREVIEW_FPS.TARGET_FPS} FPS, canvas max ${CANVAS_LIMITS.MAX_DIMENSION}px`);
86
86
  const { showBadge = true, onPreview, onComplete, onError } = options;
87
87
  const scaled = calculateScaledDimensions(system.width, system.height);
88
88
  if (scaled.wasScaled) {
@@ -96,7 +96,7 @@ export function renderUnifiedSystem(system, canvas, options = {}) {
96
96
  let animationId = null;
97
97
  let isRunning = false;
98
98
  let isDestroyed = false;
99
- const budget = createFrameBudget();
99
+ const throttle = createFpsThrottle();
100
100
  const palette = inferPalette(system.elements);
101
101
  const colors = getPaletteColors(palette);
102
102
  const totalFrames = system.loop?.duration ?? 120;
@@ -235,32 +235,26 @@ export function renderUnifiedSystem(system, canvas, options = {}) {
235
235
  scaled.originalHeight, // ← Protocol dimension
236
236
  system.seed);
237
237
  runSetup(p);
238
- resetBudget(budget);
238
+ resetThrottle(throttle);
239
239
  isRunning = true;
240
240
  // ╔═══════════════════════════════════════════════════════════════════════╗
241
- // ║ ANIMATION LOOP INVARIANT DO NOT CHANGE
241
+ // ║ ANIMATION LOOP — CONTINUOUS FPS-CAPPED RENDERING
242
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. ║
243
+ // ║ requestAnimationFrame runs continuously until stop() is called.
244
+ // ║ FPS throttle (8 FPS) prevents excessive CPU usage.
245
+ // ║ Looping uses modulo math: t = (frame % total) / total
248
246
  // ╚═══════════════════════════════════════════════════════════════════════╝
249
247
  const loop = () => {
250
- // ALWAYS schedule next frame first — loop never terminates on its own
248
+ // Schedule next frame — loop runs until stop()
251
249
  animationId = requestAnimationFrame(loop);
252
- // Early exit checks (after RAF is scheduled)
250
+ // Exit if stopped or destroyed
253
251
  if (!isRunning || isDestroyed)
254
252
  return;
255
- // Budget checkif exhausted, skip draw but keep last frame visible
256
- if (!canRenderFrame(budget)) {
257
- return; // Preserve current canvas, no clear, no reset
253
+ // FPS throttle — skip if not enough time has passed
254
+ if (!shouldRenderFrame(throttle)) {
255
+ return; // Preserve current canvas
258
256
  }
259
257
  frameCount++;
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
258
  // Calculate t using modulo for natural looping
265
259
  const t = (frameCount % totalFrames) / totalFrames;
266
260
  p.randomSeed(system.seed);
@@ -268,7 +262,7 @@ export function renderUnifiedSystem(system, canvas, options = {}) {
268
262
  try {
269
263
  runDraw(p, frameCount, t);
270
264
  drawBadge();
271
- recordFrame(budget);
265
+ recordFrame(throttle);
272
266
  }
273
267
  catch (error) {
274
268
  console.warn('[UIRenderer] Draw error:', error);
package/dist/types.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @nexart/ui-renderer v0.8.5 - Type Definitions
2
+ * @nexart/ui-renderer v0.8.6 - 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.5";
12
+ export declare const SDK_VERSION = "0.8.6";
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.5 - Type Definitions
2
+ * @nexart/ui-renderer v0.8.6 - 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.5';
12
+ export const SDK_VERSION = '0.8.6';
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,7 +1,7 @@
1
1
  {
2
2
  "name": "@nexart/ui-renderer",
3
- "version": "0.8.5",
4
- "description": "Lightweight Preview Runtime for NexArt Protocol. Non-canonical, performance-optimized with budget limits (max 30 frames, 500ms, 900px canvas).",
3
+ "version": "0.8.7",
4
+ "description": "Lightweight Preview Runtime for NexArt Protocol. Non-canonical, FPS-capped continuous animation (8 FPS, max 900px canvas).",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "main": "./dist/index.js",