@vectojs/core 1.12.0 → 1.14.0
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/index.js +209 -123
- package/dist/index.mjs +123 -37
- package/dist/tree/Scene.d.ts +74 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -667,16 +667,29 @@ var Scene = (_class2 = class _Scene {
|
|
|
667
667
|
__init10() {this.dirty = true}
|
|
668
668
|
/** Whether to throttle rendering to 2 FPS when the scene is static to save power. */
|
|
669
669
|
__init11() {this.autoThrottle = true}
|
|
670
|
+
// --- Frame telemetry (read via `frameStats`) ---------------------------
|
|
671
|
+
/** Wall-clock ms spent inside the last `render()` call. */
|
|
672
|
+
__init12() {this._lastFrameMs = 0}
|
|
673
|
+
/** Rolling exponential average of rendered-frame intervals, in ms. */
|
|
674
|
+
__init13() {this._avgFrameIntervalMs = 0}
|
|
675
|
+
/** dt (ms) handed to the last rendered frame. */
|
|
676
|
+
__init14() {this._lastDt = 0}
|
|
677
|
+
/** Count of frames actually rendered since the loop started. */
|
|
678
|
+
__init15() {this._renderedFrames = 0}
|
|
679
|
+
/** Count of rAF ticks skipped (idle / capped) since the loop started. */
|
|
680
|
+
__init16() {this._skippedFrames = 0}
|
|
681
|
+
/** `time` of the previous *rendered* frame, for interval measurement. */
|
|
682
|
+
__init17() {this._lastRenderTick = 0}
|
|
670
683
|
/**
|
|
671
684
|
* Frame-rate cap (power saving). `0` = uncapped (native refresh). When set,
|
|
672
685
|
* the loop renders at most `maxFPS` times per second; animations still run,
|
|
673
686
|
* just less often. See {@link SceneOptions.maxFPS}.
|
|
674
687
|
*/
|
|
675
|
-
|
|
688
|
+
__init18() {this.maxFPS = 60}
|
|
676
689
|
/** Whether the OS prefers-reduced-motion setting auto-caps the loop. */
|
|
677
|
-
|
|
690
|
+
__init19() {this.respectReducedMotion = true}
|
|
678
691
|
/** Cached media-query list; `.matches` is read live each frame. */
|
|
679
|
-
|
|
692
|
+
__init20() {this.reducedMotionQuery = null}
|
|
680
693
|
/** True when the OS asks for reduced motion and we respect it. Read by the animation drivers. */
|
|
681
694
|
get prefersReducedMotion() {
|
|
682
695
|
return this.respectReducedMotion && !!_optionalChain([this, 'access', _21 => _21.reducedMotionQuery, 'optionalAccess', _22 => _22.matches]);
|
|
@@ -685,95 +698,98 @@ var Scene = (_class2 = class _Scene {
|
|
|
685
698
|
* Throttle interval (ms) for the a11y/automation shadow sync. `0` = every
|
|
686
699
|
* frame. See {@link SceneOptions.a11ySyncInterval}.
|
|
687
700
|
*/
|
|
688
|
-
|
|
701
|
+
__init21() {this.a11ySyncInterval = 0}
|
|
689
702
|
/** Timestamp of the last a11y sync, for throttling. */
|
|
690
|
-
|
|
703
|
+
__init22() {this.lastA11ySync = -Infinity}
|
|
691
704
|
/** True if we skipped an a11y sync during animation and need to sync when at rest. */
|
|
692
|
-
|
|
705
|
+
__init23() {this.a11yPendingSyncAfterAnimation = false}
|
|
693
706
|
// A11y / Automation Layer. `null` in non-DOM (SSR/Node) environments — the
|
|
694
707
|
// whole projection degrades to a no-op so the engine's logic stays usable
|
|
695
708
|
// server-side (e.g. headless layout / vector export) without jsdom.
|
|
696
709
|
|
|
697
|
-
|
|
710
|
+
__init24() {this.a11yElements = /* @__PURE__ */ new Map()}
|
|
698
711
|
/** DOM nodes mirroring static text content, keyed by entity id. */
|
|
699
|
-
|
|
712
|
+
__init25() {this.contentElements = /* @__PURE__ */ new Map()}
|
|
700
713
|
/** Pending cold font-calibration frame per projected grid entity. */
|
|
701
|
-
|
|
714
|
+
__init26() {this.contentGridCalibrationFrames = /* @__PURE__ */ new Map()}
|
|
702
715
|
/** Detached, untransformed font probes used by the cold calibration pass. */
|
|
703
|
-
|
|
716
|
+
__init27() {this.contentGridCalibrationProbes = /* @__PURE__ */ new Map()}
|
|
704
717
|
/** Invalidates grid font calibration after browser font availability changes. */
|
|
705
|
-
|
|
718
|
+
__init28() {this.contentFontEpoch = 0}
|
|
706
719
|
/** Cached Canvas-to-client scale for the current font/viewport epoch. */
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
720
|
+
__init29() {this.contentMetricScaleEpoch = -1}
|
|
721
|
+
__init30() {this.contentMetricScaleX = 1}
|
|
722
|
+
__init31() {this.contentProjectionEnabled = true}
|
|
723
|
+
// Virtualization margin (px) for content projection; `undefined` → one
|
|
724
|
+
// viewport height, resolved at sync time. `Infinity` = materialize everything.
|
|
725
|
+
__init32() {this.contentProjectionMargin = void 0}
|
|
710
726
|
/**
|
|
711
727
|
* True while a text-selection drag that started on a projection's blank
|
|
712
728
|
* region (no text node under the press) is being driven manually — the
|
|
713
729
|
* browser has no native anchor for it, so mousemove extends the Selection
|
|
714
730
|
* from the position we resolved ourselves.
|
|
715
731
|
*/
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
732
|
+
__init33() {this.blankRegionSelectionDrag = false}
|
|
733
|
+
__init34() {this.contentSelectionAnchor = null}
|
|
734
|
+
__init35() {this.contentSelectionEndListener = null}
|
|
719
735
|
// Animation/interactive flags collected during the render walk (tree-walk
|
|
720
736
|
// fusion): the loop reads last frame's answers instead of re-walking the
|
|
721
737
|
// tree up to 4× per tick. Start true so the first tick stays conservative.
|
|
722
|
-
|
|
723
|
-
|
|
738
|
+
__init36() {this.frameHadAnimation = true}
|
|
739
|
+
__init37() {this.frameHadInteractive = true}
|
|
724
740
|
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
741
|
+
__init38() {this.focusedA11yElement = null}
|
|
742
|
+
__init39() {this.caretBlinkTimer = null}
|
|
743
|
+
__init40() {this.a11yNeedsReorder = true}
|
|
744
|
+
__init41() {this.portalRoot = null}
|
|
745
|
+
__init42() {this.fullViewportElements = []}
|
|
746
|
+
__init43() {this.normalElements = []}
|
|
747
|
+
__init44() {this.activeIds = /* @__PURE__ */ new Set()}
|
|
748
|
+
__init45() {this.activePortalsThisFrame = /* @__PURE__ */ new Set()}
|
|
749
|
+
__init46() {this.activePortalsPrevFrame = /* @__PURE__ */ new Set()}
|
|
750
|
+
__init47() {this.portalEntities = /* @__PURE__ */ new Map()}
|
|
751
|
+
__init48() {this.renderOrderCounter = 0}
|
|
736
752
|
/**
|
|
737
753
|
* Authoritative paint order for semantic nodes discovered during the main
|
|
738
754
|
* render. A node may not have a DOM projection until the following a11y
|
|
739
755
|
* sync, so retaining the order prevents a newly opened overlay from spending
|
|
740
756
|
* its first frame below previously projected controls.
|
|
741
757
|
*/
|
|
742
|
-
|
|
758
|
+
__init49() {this.a11yRenderOrders = /* @__PURE__ */ new Map()}
|
|
743
759
|
// Optional WebGL point-cloud layer (see SceneOptions.pointBackend).
|
|
744
|
-
|
|
745
|
-
|
|
760
|
+
__init50() {this.pointRenderer = null}
|
|
761
|
+
__init51() {this.glCanvas = null}
|
|
746
762
|
|
|
747
763
|
|
|
748
764
|
|
|
749
|
-
|
|
765
|
+
__init52() {this.disableWindowResize = false}
|
|
750
766
|
/** See {@link SceneOptions.maxDPR}. `undefined` = uncapped (real DPR). */
|
|
751
767
|
|
|
752
768
|
// WebGPU properties
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
769
|
+
__init53() {this.destroyed = false}
|
|
770
|
+
__init54() {this.device = null}
|
|
771
|
+
__init55() {this.deviceLost = false}
|
|
772
|
+
__init56() {this.particleBackend = "auto"}
|
|
773
|
+
__init57() {this._webgpuDisabled = false}
|
|
758
774
|
get webgpuDisabled() {
|
|
759
775
|
return this._webgpuDisabled || this.particleBackend === "cpu";
|
|
760
776
|
}
|
|
761
777
|
set webgpuDisabled(value) {
|
|
762
778
|
this._webgpuDisabled = value;
|
|
763
779
|
}
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
780
|
+
__init58() {this.recoveryTimerId = null}
|
|
781
|
+
__init59() {this.manager = null}
|
|
782
|
+
__init60() {this.initializingWebGPU = false}
|
|
783
|
+
__init61() {this.gpuCanvas = null}
|
|
784
|
+
__init62() {this.gpuContext = null}
|
|
769
785
|
/** True while the GPU canvas holds a presented particle frame (needs clearing when they leave). */
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
786
|
+
__init63() {this.gpuHasContent = false}
|
|
787
|
+
__init64() {this.mouseX = -9999}
|
|
788
|
+
__init65() {this.mouseY = -9999}
|
|
789
|
+
__init66() {this.pointerMoveListener = null}
|
|
790
|
+
__init67() {this.pointerLeaveListener = null}
|
|
791
|
+
__init68() {this.hasWarnedZeroSize = false}
|
|
792
|
+
__init69() {this.fontLoadHandler = null}
|
|
777
793
|
// ── Dev-mode warning infrastructure ──────────────────────────────
|
|
778
794
|
//
|
|
779
795
|
// Enable with `Scene.devMode = true` or by setting `globalThis.__DEV__`.
|
|
@@ -791,7 +807,7 @@ var Scene = (_class2 = class _Scene {
|
|
|
791
807
|
return false;
|
|
792
808
|
}
|
|
793
809
|
|
|
794
|
-
|
|
810
|
+
__init70() {this._devFrameCount = 0}
|
|
795
811
|
_devWarn(message) {
|
|
796
812
|
if (!this._devActive) return;
|
|
797
813
|
console.warn(`[vectojs/dev] ${message}`);
|
|
@@ -835,7 +851,7 @@ var Scene = (_class2 = class _Scene {
|
|
|
835
851
|
};
|
|
836
852
|
walkProjections(this.root);
|
|
837
853
|
}
|
|
838
|
-
constructor(canvas, options = {}) {;_class2.prototype.__init7.call(this);_class2.prototype.__init8.call(this);_class2.prototype.__init9.call(this);_class2.prototype.__init10.call(this);_class2.prototype.__init11.call(this);_class2.prototype.__init12.call(this);_class2.prototype.__init13.call(this);_class2.prototype.__init14.call(this);_class2.prototype.__init15.call(this);_class2.prototype.__init16.call(this);_class2.prototype.__init17.call(this);_class2.prototype.__init18.call(this);_class2.prototype.__init19.call(this);_class2.prototype.__init20.call(this);_class2.prototype.__init21.call(this);_class2.prototype.__init22.call(this);_class2.prototype.__init23.call(this);_class2.prototype.__init24.call(this);_class2.prototype.__init25.call(this);_class2.prototype.__init26.call(this);_class2.prototype.__init27.call(this);_class2.prototype.__init28.call(this);_class2.prototype.__init29.call(this);_class2.prototype.__init30.call(this);_class2.prototype.__init31.call(this);_class2.prototype.__init32.call(this);_class2.prototype.__init33.call(this);_class2.prototype.__init34.call(this);_class2.prototype.__init35.call(this);_class2.prototype.__init36.call(this);_class2.prototype.__init37.call(this);_class2.prototype.__init38.call(this);_class2.prototype.__init39.call(this);_class2.prototype.__init40.call(this);_class2.prototype.__init41.call(this);_class2.prototype.__init42.call(this);_class2.prototype.__init43.call(this);_class2.prototype.__init44.call(this);_class2.prototype.__init45.call(this);_class2.prototype.__init46.call(this);_class2.prototype.__init47.call(this);_class2.prototype.__init48.call(this);_class2.prototype.__init49.call(this);_class2.prototype.__init50.call(this);_class2.prototype.__init51.call(this);_class2.prototype.__init52.call(this);_class2.prototype.__init53.call(this);_class2.prototype.__init54.call(this);_class2.prototype.__init55.call(this);_class2.prototype.__init56.call(this);_class2.prototype.__init57.call(this);_class2.prototype.__init58.call(this);_class2.prototype.__init59.call(this);_class2.prototype.__init60.call(this);_class2.prototype.__init61.call(this);_class2.prototype.__init62.call(this);_class2.prototype.__init63.call(this);
|
|
854
|
+
constructor(canvas, options = {}) {;_class2.prototype.__init7.call(this);_class2.prototype.__init8.call(this);_class2.prototype.__init9.call(this);_class2.prototype.__init10.call(this);_class2.prototype.__init11.call(this);_class2.prototype.__init12.call(this);_class2.prototype.__init13.call(this);_class2.prototype.__init14.call(this);_class2.prototype.__init15.call(this);_class2.prototype.__init16.call(this);_class2.prototype.__init17.call(this);_class2.prototype.__init18.call(this);_class2.prototype.__init19.call(this);_class2.prototype.__init20.call(this);_class2.prototype.__init21.call(this);_class2.prototype.__init22.call(this);_class2.prototype.__init23.call(this);_class2.prototype.__init24.call(this);_class2.prototype.__init25.call(this);_class2.prototype.__init26.call(this);_class2.prototype.__init27.call(this);_class2.prototype.__init28.call(this);_class2.prototype.__init29.call(this);_class2.prototype.__init30.call(this);_class2.prototype.__init31.call(this);_class2.prototype.__init32.call(this);_class2.prototype.__init33.call(this);_class2.prototype.__init34.call(this);_class2.prototype.__init35.call(this);_class2.prototype.__init36.call(this);_class2.prototype.__init37.call(this);_class2.prototype.__init38.call(this);_class2.prototype.__init39.call(this);_class2.prototype.__init40.call(this);_class2.prototype.__init41.call(this);_class2.prototype.__init42.call(this);_class2.prototype.__init43.call(this);_class2.prototype.__init44.call(this);_class2.prototype.__init45.call(this);_class2.prototype.__init46.call(this);_class2.prototype.__init47.call(this);_class2.prototype.__init48.call(this);_class2.prototype.__init49.call(this);_class2.prototype.__init50.call(this);_class2.prototype.__init51.call(this);_class2.prototype.__init52.call(this);_class2.prototype.__init53.call(this);_class2.prototype.__init54.call(this);_class2.prototype.__init55.call(this);_class2.prototype.__init56.call(this);_class2.prototype.__init57.call(this);_class2.prototype.__init58.call(this);_class2.prototype.__init59.call(this);_class2.prototype.__init60.call(this);_class2.prototype.__init61.call(this);_class2.prototype.__init62.call(this);_class2.prototype.__init63.call(this);_class2.prototype.__init64.call(this);_class2.prototype.__init65.call(this);_class2.prototype.__init66.call(this);_class2.prototype.__init67.call(this);_class2.prototype.__init68.call(this);_class2.prototype.__init69.call(this);_class2.prototype.__init70.call(this);
|
|
839
855
|
this.canvas = canvas;
|
|
840
856
|
this.debugA11y = _nullishCoalesce(options.debugA11y, () => ( false));
|
|
841
857
|
this.disableWindowResize = _nullishCoalesce(options.disableWindowResize, () => ( false));
|
|
@@ -857,6 +873,7 @@ var Scene = (_class2 = class _Scene {
|
|
|
857
873
|
this.particleBackend = _nullishCoalesce(options.particleBackend, () => ( "auto"));
|
|
858
874
|
this.a11ySyncInterval = _nullishCoalesce(options.a11ySyncInterval, () => ( 0));
|
|
859
875
|
this.contentProjectionEnabled = _nullishCoalesce(options.contentProjection, () => ( true));
|
|
876
|
+
this.contentProjectionMargin = options.contentProjectionMargin;
|
|
860
877
|
this._devActive = _Scene._devModeDetected();
|
|
861
878
|
this.reducedMotionQuery = typeof window !== "undefined" && typeof window.matchMedia === "function" ? window.matchMedia("(prefers-reduced-motion: reduce)") : null;
|
|
862
879
|
this.root = new class RootEntity extends _chunk2Z23LTH3js.Entity {
|
|
@@ -1318,6 +1335,36 @@ var Scene = (_class2 = class _Scene {
|
|
|
1318
1335
|
markDirty() {
|
|
1319
1336
|
this.dirty = true;
|
|
1320
1337
|
}
|
|
1338
|
+
/**
|
|
1339
|
+
* Live frame telemetry for profilers and devtools overlays. All timings are
|
|
1340
|
+
* measured on the `requestAnimationFrame` loop; a scene driven only by
|
|
1341
|
+
* {@link step} (e.g. deterministic video export) leaves these at their zero
|
|
1342
|
+
* defaults.
|
|
1343
|
+
*
|
|
1344
|
+
* `fps` is derived from the interval between *rendered* frames, so idle
|
|
1345
|
+
* `onDemand` scenes and frames skipped by the {@link maxFPS} cap or the
|
|
1346
|
+
* static auto-throttle do not deflate it — it reports the cadence of actual
|
|
1347
|
+
* redraws, not the raw rAF rate. `frameTimeMs` is the wall-clock cost of the
|
|
1348
|
+
* last `render()` pass alone (excludes a11y/content-projection sync).
|
|
1349
|
+
*
|
|
1350
|
+
* The renderer always repaints the full canvas, so there is no partial
|
|
1351
|
+
* dirty-rectangle to expose; `dirty` is the boolean redraw-pending flag and
|
|
1352
|
+
* `pendingRedraw` reflects whether the next `onDemand` tick will actually
|
|
1353
|
+
* render.
|
|
1354
|
+
*/
|
|
1355
|
+
get frameStats() {
|
|
1356
|
+
const interval = this._avgFrameIntervalMs;
|
|
1357
|
+
return {
|
|
1358
|
+
fps: interval > 0 ? Math.min(1e3 / interval, this.maxFPS > 0 ? this.maxFPS : 1e3 / interval) : 0,
|
|
1359
|
+
frameTimeMs: this._lastFrameMs,
|
|
1360
|
+
frameIntervalMs: interval,
|
|
1361
|
+
dt: this._lastDt,
|
|
1362
|
+
renderedFrames: this._renderedFrames,
|
|
1363
|
+
skippedFrames: this._skippedFrames,
|
|
1364
|
+
renderMode: this.renderMode,
|
|
1365
|
+
dirty: this.dirty
|
|
1366
|
+
};
|
|
1367
|
+
}
|
|
1321
1368
|
/** True when any node in the subtree has a pending animation. */
|
|
1322
1369
|
/** True when any node in the subtree is interactive (drives a11y sync). */
|
|
1323
1370
|
syncOptionalAttribute(element, name, value) {
|
|
@@ -1618,6 +1665,56 @@ var Scene = (_class2 = class _Scene {
|
|
|
1618
1665
|
* hidden (`display: none`) so text-heavy scenes only materialize what is
|
|
1619
1666
|
* visible to the browser's text machinery anyway.
|
|
1620
1667
|
*/
|
|
1668
|
+
/**
|
|
1669
|
+
* Whether `node`'s world-space box, expanded by `margin` px on every side,
|
|
1670
|
+
* overlaps the scene viewport AND every `clipChildren` ancestor's box. Used
|
|
1671
|
+
* both to virtualize content projection (materialize only near-viewport text,
|
|
1672
|
+
* at `margin = contentProjectionMargin`) and for the exact `display:none`
|
|
1673
|
+
* visibility test (`margin = 0`). Boundless nodes (width/height 0) opt out of
|
|
1674
|
+
* culling and always count as visible, matching the legacy behavior.
|
|
1675
|
+
*/
|
|
1676
|
+
projectionBoxVisible(node, tf, margin) {
|
|
1677
|
+
if (!(node.width > 0 && node.height > 0)) return true;
|
|
1678
|
+
const { a, b, c, d, e, f } = tf;
|
|
1679
|
+
const worldCorners = [];
|
|
1680
|
+
let minX = Infinity;
|
|
1681
|
+
let minY = Infinity;
|
|
1682
|
+
let maxX = -Infinity;
|
|
1683
|
+
let maxY = -Infinity;
|
|
1684
|
+
for (let i = 0; i < 4; i++) {
|
|
1685
|
+
const lx = i & 1 ? node.width : 0;
|
|
1686
|
+
const ly = i & 2 ? node.height : 0;
|
|
1687
|
+
const wx = a * lx + c * ly + e;
|
|
1688
|
+
const wy = b * lx + d * ly + f;
|
|
1689
|
+
worldCorners.push({ x: wx, y: wy });
|
|
1690
|
+
if (wx < minX) minX = wx;
|
|
1691
|
+
if (wx > maxX) maxX = wx;
|
|
1692
|
+
if (wy < minY) minY = wy;
|
|
1693
|
+
if (wy > maxY) maxY = wy;
|
|
1694
|
+
}
|
|
1695
|
+
if (!(maxX >= -margin && minX <= this.width + margin && maxY >= -margin && minY <= this.height + margin)) {
|
|
1696
|
+
return false;
|
|
1697
|
+
}
|
|
1698
|
+
for (let ancestor = node.parent; ancestor; ancestor = ancestor.parent) {
|
|
1699
|
+
if (!ancestor.clipChildren || ancestor.width <= 0 || ancestor.height <= 0) continue;
|
|
1700
|
+
let localMinX = Infinity;
|
|
1701
|
+
let localMinY = Infinity;
|
|
1702
|
+
let localMaxX = -Infinity;
|
|
1703
|
+
let localMaxY = -Infinity;
|
|
1704
|
+
for (const corner of worldCorners) {
|
|
1705
|
+
const local = ancestor.worldToLocal(corner.x, corner.y);
|
|
1706
|
+
if (!local) continue;
|
|
1707
|
+
localMinX = Math.min(localMinX, local.x);
|
|
1708
|
+
localMinY = Math.min(localMinY, local.y);
|
|
1709
|
+
localMaxX = Math.max(localMaxX, local.x);
|
|
1710
|
+
localMaxY = Math.max(localMaxY, local.y);
|
|
1711
|
+
}
|
|
1712
|
+
if (!(localMaxX >= -margin && localMinX <= ancestor.width + margin && localMaxY >= -margin && localMinY <= ancestor.height + margin)) {
|
|
1713
|
+
return false;
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
return true;
|
|
1717
|
+
}
|
|
1621
1718
|
syncContentProjection(node) {
|
|
1622
1719
|
if (!this.contentProjectionEnabled || !this.a11yRoot) return;
|
|
1623
1720
|
const projection = node.getContentProjection();
|
|
@@ -1631,6 +1728,17 @@ var Scene = (_class2 = class _Scene {
|
|
|
1631
1728
|
}
|
|
1632
1729
|
return;
|
|
1633
1730
|
}
|
|
1731
|
+
const worldTf = node.getWorldTransform();
|
|
1732
|
+
const margin = _nullishCoalesce(this.contentProjectionMargin, () => ( this.height));
|
|
1733
|
+
if (Number.isFinite(margin) && !this.projectionBoxVisible(node, worldTf, margin)) {
|
|
1734
|
+
if (el) {
|
|
1735
|
+
this.clearContentGridState(node.id, el);
|
|
1736
|
+
el.remove();
|
|
1737
|
+
this.contentElements.delete(node.id);
|
|
1738
|
+
this.a11yNeedsReorder = true;
|
|
1739
|
+
}
|
|
1740
|
+
return;
|
|
1741
|
+
}
|
|
1634
1742
|
if (!el) {
|
|
1635
1743
|
el = document.createElement("div");
|
|
1636
1744
|
el.setAttribute("data-vecto-content", node.id);
|
|
@@ -1727,7 +1835,7 @@ var Scene = (_class2 = class _Scene {
|
|
|
1727
1835
|
el.style.userSelect = selectable ? "text" : "none";
|
|
1728
1836
|
el.style.cursor = selectable ? "text" : "";
|
|
1729
1837
|
}
|
|
1730
|
-
const { a, b, c, d, e, f } =
|
|
1838
|
+
const { a, b, c, d, e, f } = worldTf;
|
|
1731
1839
|
const contentX = _nullishCoalesce(projection.contentX, () => ( 0));
|
|
1732
1840
|
const contentY = _nullishCoalesce(projection.contentY, () => ( 0));
|
|
1733
1841
|
const baselineOffset = lines && lines.length > 0 ? 0 : projection.baseline === void 0 ? 0 : projection.baseline - _chunk2Z23LTH3js.cssLineBoxBaseline.call(void 0, font, _nullishCoalesce(projection.lineHeight, () => ( 16)));
|
|
@@ -1737,42 +1845,7 @@ var Scene = (_class2 = class _Scene {
|
|
|
1737
1845
|
if (node.width > 0) el.style.width = `${node.width}px`;
|
|
1738
1846
|
if (node.height > 0) el.style.height = `${node.height}px`;
|
|
1739
1847
|
el.style.transform = `matrix(${a}, ${b}, ${c}, ${d}, 0, 0)`;
|
|
1740
|
-
|
|
1741
|
-
if (node.width > 0 && node.height > 0) {
|
|
1742
|
-
const worldCorners = [];
|
|
1743
|
-
let minX = Infinity;
|
|
1744
|
-
let minY = Infinity;
|
|
1745
|
-
let maxX = -Infinity;
|
|
1746
|
-
let maxY = -Infinity;
|
|
1747
|
-
for (let i = 0; i < 4; i++) {
|
|
1748
|
-
const lx = i & 1 ? node.width : 0;
|
|
1749
|
-
const ly = i & 2 ? node.height : 0;
|
|
1750
|
-
const wx = a * lx + c * ly + e;
|
|
1751
|
-
const wy = b * lx + d * ly + f;
|
|
1752
|
-
worldCorners.push({ x: wx, y: wy });
|
|
1753
|
-
if (wx < minX) minX = wx;
|
|
1754
|
-
if (wx > maxX) maxX = wx;
|
|
1755
|
-
if (wy < minY) minY = wy;
|
|
1756
|
-
if (wy > maxY) maxY = wy;
|
|
1757
|
-
}
|
|
1758
|
-
visible = maxX >= 0 && minX <= this.width && maxY >= 0 && minY <= this.height;
|
|
1759
|
-
for (let ancestor = node.parent; visible && ancestor; ancestor = ancestor.parent) {
|
|
1760
|
-
if (!ancestor.clipChildren || ancestor.width <= 0 || ancestor.height <= 0) continue;
|
|
1761
|
-
let localMinX = Infinity;
|
|
1762
|
-
let localMinY = Infinity;
|
|
1763
|
-
let localMaxX = -Infinity;
|
|
1764
|
-
let localMaxY = -Infinity;
|
|
1765
|
-
for (const corner of worldCorners) {
|
|
1766
|
-
const local = ancestor.worldToLocal(corner.x, corner.y);
|
|
1767
|
-
if (!local) continue;
|
|
1768
|
-
localMinX = Math.min(localMinX, local.x);
|
|
1769
|
-
localMinY = Math.min(localMinY, local.y);
|
|
1770
|
-
localMaxX = Math.max(localMaxX, local.x);
|
|
1771
|
-
localMaxY = Math.max(localMaxY, local.y);
|
|
1772
|
-
}
|
|
1773
|
-
visible = localMaxX >= 0 && localMinX <= ancestor.width && localMaxY >= 0 && localMinY <= ancestor.height;
|
|
1774
|
-
}
|
|
1775
|
-
}
|
|
1848
|
+
const visible = this.projectionBoxVisible(node, worldTf, 0);
|
|
1776
1849
|
const display = visible ? "" : "none";
|
|
1777
1850
|
if (el.style.display !== display) el.style.display = display;
|
|
1778
1851
|
}
|
|
@@ -2221,6 +2294,7 @@ var Scene = (_class2 = class _Scene {
|
|
|
2221
2294
|
cap = Math.min(cap, 2);
|
|
2222
2295
|
}
|
|
2223
2296
|
if (cap > 0 && time - this.lastTime < 1e3 / cap - 1) {
|
|
2297
|
+
this._skippedFrames++;
|
|
2224
2298
|
this.scheduleFrame();
|
|
2225
2299
|
return;
|
|
2226
2300
|
}
|
|
@@ -2231,11 +2305,23 @@ var Scene = (_class2 = class _Scene {
|
|
|
2231
2305
|
}
|
|
2232
2306
|
this.lastTime = time;
|
|
2233
2307
|
if (this.renderMode === "onDemand" && isIdle) {
|
|
2308
|
+
this._skippedFrames++;
|
|
2234
2309
|
this.scheduleFrame();
|
|
2235
2310
|
return;
|
|
2236
2311
|
}
|
|
2237
2312
|
this.dirty = false;
|
|
2313
|
+
const now = typeof performance !== "undefined" ? performance.now() : time;
|
|
2314
|
+
if (this._lastRenderTick > 0) {
|
|
2315
|
+
const interval = time - this._lastRenderTick;
|
|
2316
|
+
if (interval > 0) {
|
|
2317
|
+
this._avgFrameIntervalMs = this._avgFrameIntervalMs === 0 ? interval : this._avgFrameIntervalMs * 0.9 + interval * 0.1;
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
this._lastRenderTick = time;
|
|
2321
|
+
this._lastDt = dt;
|
|
2238
2322
|
this.render(this.renderer, dt, time);
|
|
2323
|
+
this._lastFrameMs = (typeof performance !== "undefined" ? performance.now() : time) - now;
|
|
2324
|
+
this._renderedFrames++;
|
|
2239
2325
|
const hasActiveAnimation = this.frameHadAnimation;
|
|
2240
2326
|
const hasInteractive = this.frameHadInteractive;
|
|
2241
2327
|
const wantsContentSync = this.contentProjectionEnabled;
|
|
@@ -2762,15 +2848,15 @@ var TextEntity = (_class3 = class extends _chunk2Z23LTH3js.Entity {
|
|
|
2762
2848
|
|
|
2763
2849
|
|
|
2764
2850
|
|
|
2765
|
-
|
|
2851
|
+
__init71() {this.nodes = []}
|
|
2766
2852
|
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2853
|
+
__init72() {this.fillStyle = "#94a3b8"}
|
|
2854
|
+
__init73() {this.strokeStyle = null}
|
|
2855
|
+
__init74() {this.hoveredFillStyle = "#ffffff"}
|
|
2856
|
+
__init75() {this.lineWidth = 1}
|
|
2857
|
+
__init76() {this.isHovered = false}
|
|
2772
2858
|
constructor(text, atlas, maxWidth, fontSize = 24) {
|
|
2773
|
-
super();_class3.prototype.
|
|
2859
|
+
super();_class3.prototype.__init71.call(this);_class3.prototype.__init72.call(this);_class3.prototype.__init73.call(this);_class3.prototype.__init74.call(this);_class3.prototype.__init75.call(this);_class3.prototype.__init76.call(this);;
|
|
2774
2860
|
this.text = text;
|
|
2775
2861
|
this.atlas = atlas;
|
|
2776
2862
|
this.fontSize = fontSize;
|
|
@@ -2885,15 +2971,15 @@ var TextEntity = (_class3 = class extends _chunk2Z23LTH3js.Entity {
|
|
|
2885
2971
|
// src/components/GridTextEntity.ts
|
|
2886
2972
|
var GridTextEntity = (_class4 = class extends _chunk2Z23LTH3js.Entity {
|
|
2887
2973
|
|
|
2888
|
-
|
|
2889
|
-
|
|
2974
|
+
__init77() {this.fillStyle = "#ffffff"}
|
|
2975
|
+
__init78() {this.grid = []}
|
|
2890
2976
|
// Array of rows
|
|
2891
|
-
|
|
2892
|
-
|
|
2977
|
+
__init79() {this.cols = 0}
|
|
2978
|
+
__init80() {this.rows = 0}
|
|
2893
2979
|
|
|
2894
2980
|
|
|
2895
2981
|
constructor(_atlas, fontSize = 10) {
|
|
2896
|
-
super();_class4.prototype.
|
|
2982
|
+
super();_class4.prototype.__init77.call(this);_class4.prototype.__init78.call(this);_class4.prototype.__init79.call(this);_class4.prototype.__init80.call(this);;
|
|
2897
2983
|
this.fontSize = fontSize;
|
|
2898
2984
|
this.charWidth = fontSize * 1;
|
|
2899
2985
|
this.charHeight = fontSize * 1.1;
|
|
@@ -2985,23 +3071,23 @@ var SplineEntity = (_class5 = class extends _chunk2Z23LTH3js.Entity {
|
|
|
2985
3071
|
|
|
2986
3072
|
|
|
2987
3073
|
|
|
2988
|
-
|
|
2989
|
-
|
|
3074
|
+
__init81() {this.offscreen = null}
|
|
3075
|
+
__init82() {this.baked = false}
|
|
2990
3076
|
/** Logical (CSS-pixel) size of the baked bitmap — the blit destination size. */
|
|
2991
|
-
|
|
2992
|
-
|
|
3077
|
+
__init83() {this.bakedWidth = 0}
|
|
3078
|
+
__init84() {this.bakedHeight = 0}
|
|
2993
3079
|
/** Gradient strokes can't be baked to a solid-color bitmap; they render per-frame. */
|
|
2994
3080
|
|
|
2995
3081
|
/** Lazily-flattened polylines (one Float32Array of [x,y,...] per segment) for hit-testing. */
|
|
2996
|
-
|
|
3082
|
+
__init85() {this.polylines = null}
|
|
2997
3083
|
/**
|
|
2998
3084
|
* When `true`, the renderer draws a rounded-rect outline of the entity's
|
|
2999
3085
|
* local bounds after painting the curves. Useful for drag feedback and
|
|
3000
3086
|
* debugging hit areas. Defaults to `false`.
|
|
3001
3087
|
*/
|
|
3002
|
-
|
|
3088
|
+
__init86() {this.showBounds = false}
|
|
3003
3089
|
constructor(doc, opts = {}) {
|
|
3004
|
-
super();_class5.prototype.
|
|
3090
|
+
super();_class5.prototype.__init81.call(this);_class5.prototype.__init82.call(this);_class5.prototype.__init83.call(this);_class5.prototype.__init84.call(this);_class5.prototype.__init85.call(this);_class5.prototype.__init86.call(this);;
|
|
3005
3091
|
this.doc = doc;
|
|
3006
3092
|
this.lineWidth = _nullishCoalesce(opts.lineWidth, () => ( 2));
|
|
3007
3093
|
this.cache = _nullishCoalesce(opts.cache, () => ( true));
|
|
@@ -3376,9 +3462,9 @@ var Group = class extends _chunk2Z23LTH3js.Entity {
|
|
|
3376
3462
|
// src/math/SpatialHashGrid.ts
|
|
3377
3463
|
var SpatialHashGrid = (_class6 = class {
|
|
3378
3464
|
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
constructor(cellSize = 64) {;_class6.prototype.
|
|
3465
|
+
__init87() {this.grid = /* @__PURE__ */ new Map()}
|
|
3466
|
+
__init88() {this.entityCells = /* @__PURE__ */ new Map()}
|
|
3467
|
+
constructor(cellSize = 64) {;_class6.prototype.__init87.call(this);_class6.prototype.__init88.call(this);
|
|
3382
3468
|
this.cellSize = cellSize;
|
|
3383
3469
|
}
|
|
3384
3470
|
hash(cx, cy) {
|
|
@@ -3470,18 +3556,18 @@ var SpatialHashGrid = (_class6 = class {
|
|
|
3470
3556
|
// src/tree/DOMPortalEntity.ts
|
|
3471
3557
|
var DOMPortalEntity = (_class7 = class extends _chunk2Z23LTH3js.Entity {
|
|
3472
3558
|
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3559
|
+
__init89() {this.isDOMPortal = true}
|
|
3560
|
+
__init90() {this.domListeners = []}
|
|
3561
|
+
__init91() {this.resizeObserver = null}
|
|
3562
|
+
__init92() {this.cachedWidth = 100}
|
|
3563
|
+
__init93() {this.cachedHeight = 100}
|
|
3564
|
+
__init94() {this.lastWidth = ""}
|
|
3565
|
+
__init95() {this.lastHeight = ""}
|
|
3566
|
+
__init96() {this.lastTransform = ""}
|
|
3567
|
+
__init97() {this.lastZIndex = ""}
|
|
3568
|
+
__init98() {this.lastOpacity = ""}
|
|
3483
3569
|
constructor(domElement, width, height, id) {
|
|
3484
|
-
super(id);_class7.prototype.
|
|
3570
|
+
super(id);_class7.prototype.__init89.call(this);_class7.prototype.__init90.call(this);_class7.prototype.__init91.call(this);_class7.prototype.__init92.call(this);_class7.prototype.__init93.call(this);_class7.prototype.__init94.call(this);_class7.prototype.__init95.call(this);_class7.prototype.__init96.call(this);_class7.prototype.__init97.call(this);_class7.prototype.__init98.call(this);;
|
|
3485
3571
|
this.domElement = domElement;
|
|
3486
3572
|
this.width = _nullishCoalesce(width, () => ( 0));
|
|
3487
3573
|
this.height = _nullishCoalesce(height, () => ( 0));
|
package/dist/index.mjs
CHANGED
|
@@ -667,6 +667,19 @@ var Scene = class _Scene {
|
|
|
667
667
|
dirty = true;
|
|
668
668
|
/** Whether to throttle rendering to 2 FPS when the scene is static to save power. */
|
|
669
669
|
autoThrottle = true;
|
|
670
|
+
// --- Frame telemetry (read via `frameStats`) ---------------------------
|
|
671
|
+
/** Wall-clock ms spent inside the last `render()` call. */
|
|
672
|
+
_lastFrameMs = 0;
|
|
673
|
+
/** Rolling exponential average of rendered-frame intervals, in ms. */
|
|
674
|
+
_avgFrameIntervalMs = 0;
|
|
675
|
+
/** dt (ms) handed to the last rendered frame. */
|
|
676
|
+
_lastDt = 0;
|
|
677
|
+
/** Count of frames actually rendered since the loop started. */
|
|
678
|
+
_renderedFrames = 0;
|
|
679
|
+
/** Count of rAF ticks skipped (idle / capped) since the loop started. */
|
|
680
|
+
_skippedFrames = 0;
|
|
681
|
+
/** `time` of the previous *rendered* frame, for interval measurement. */
|
|
682
|
+
_lastRenderTick = 0;
|
|
670
683
|
/**
|
|
671
684
|
* Frame-rate cap (power saving). `0` = uncapped (native refresh). When set,
|
|
672
685
|
* the loop renders at most `maxFPS` times per second; animations still run,
|
|
@@ -707,6 +720,9 @@ var Scene = class _Scene {
|
|
|
707
720
|
contentMetricScaleEpoch = -1;
|
|
708
721
|
contentMetricScaleX = 1;
|
|
709
722
|
contentProjectionEnabled = true;
|
|
723
|
+
// Virtualization margin (px) for content projection; `undefined` → one
|
|
724
|
+
// viewport height, resolved at sync time. `Infinity` = materialize everything.
|
|
725
|
+
contentProjectionMargin = void 0;
|
|
710
726
|
/**
|
|
711
727
|
* True while a text-selection drag that started on a projection's blank
|
|
712
728
|
* region (no text node under the press) is being driven manually — the
|
|
@@ -857,6 +873,7 @@ var Scene = class _Scene {
|
|
|
857
873
|
this.particleBackend = options.particleBackend ?? "auto";
|
|
858
874
|
this.a11ySyncInterval = options.a11ySyncInterval ?? 0;
|
|
859
875
|
this.contentProjectionEnabled = options.contentProjection ?? true;
|
|
876
|
+
this.contentProjectionMargin = options.contentProjectionMargin;
|
|
860
877
|
this._devActive = _Scene._devModeDetected();
|
|
861
878
|
this.reducedMotionQuery = typeof window !== "undefined" && typeof window.matchMedia === "function" ? window.matchMedia("(prefers-reduced-motion: reduce)") : null;
|
|
862
879
|
this.root = new class RootEntity extends Entity {
|
|
@@ -1318,6 +1335,36 @@ var Scene = class _Scene {
|
|
|
1318
1335
|
markDirty() {
|
|
1319
1336
|
this.dirty = true;
|
|
1320
1337
|
}
|
|
1338
|
+
/**
|
|
1339
|
+
* Live frame telemetry for profilers and devtools overlays. All timings are
|
|
1340
|
+
* measured on the `requestAnimationFrame` loop; a scene driven only by
|
|
1341
|
+
* {@link step} (e.g. deterministic video export) leaves these at their zero
|
|
1342
|
+
* defaults.
|
|
1343
|
+
*
|
|
1344
|
+
* `fps` is derived from the interval between *rendered* frames, so idle
|
|
1345
|
+
* `onDemand` scenes and frames skipped by the {@link maxFPS} cap or the
|
|
1346
|
+
* static auto-throttle do not deflate it — it reports the cadence of actual
|
|
1347
|
+
* redraws, not the raw rAF rate. `frameTimeMs` is the wall-clock cost of the
|
|
1348
|
+
* last `render()` pass alone (excludes a11y/content-projection sync).
|
|
1349
|
+
*
|
|
1350
|
+
* The renderer always repaints the full canvas, so there is no partial
|
|
1351
|
+
* dirty-rectangle to expose; `dirty` is the boolean redraw-pending flag and
|
|
1352
|
+
* `pendingRedraw` reflects whether the next `onDemand` tick will actually
|
|
1353
|
+
* render.
|
|
1354
|
+
*/
|
|
1355
|
+
get frameStats() {
|
|
1356
|
+
const interval = this._avgFrameIntervalMs;
|
|
1357
|
+
return {
|
|
1358
|
+
fps: interval > 0 ? Math.min(1e3 / interval, this.maxFPS > 0 ? this.maxFPS : 1e3 / interval) : 0,
|
|
1359
|
+
frameTimeMs: this._lastFrameMs,
|
|
1360
|
+
frameIntervalMs: interval,
|
|
1361
|
+
dt: this._lastDt,
|
|
1362
|
+
renderedFrames: this._renderedFrames,
|
|
1363
|
+
skippedFrames: this._skippedFrames,
|
|
1364
|
+
renderMode: this.renderMode,
|
|
1365
|
+
dirty: this.dirty
|
|
1366
|
+
};
|
|
1367
|
+
}
|
|
1321
1368
|
/** True when any node in the subtree has a pending animation. */
|
|
1322
1369
|
/** True when any node in the subtree is interactive (drives a11y sync). */
|
|
1323
1370
|
syncOptionalAttribute(element, name, value) {
|
|
@@ -1618,6 +1665,56 @@ var Scene = class _Scene {
|
|
|
1618
1665
|
* hidden (`display: none`) so text-heavy scenes only materialize what is
|
|
1619
1666
|
* visible to the browser's text machinery anyway.
|
|
1620
1667
|
*/
|
|
1668
|
+
/**
|
|
1669
|
+
* Whether `node`'s world-space box, expanded by `margin` px on every side,
|
|
1670
|
+
* overlaps the scene viewport AND every `clipChildren` ancestor's box. Used
|
|
1671
|
+
* both to virtualize content projection (materialize only near-viewport text,
|
|
1672
|
+
* at `margin = contentProjectionMargin`) and for the exact `display:none`
|
|
1673
|
+
* visibility test (`margin = 0`). Boundless nodes (width/height 0) opt out of
|
|
1674
|
+
* culling and always count as visible, matching the legacy behavior.
|
|
1675
|
+
*/
|
|
1676
|
+
projectionBoxVisible(node, tf, margin) {
|
|
1677
|
+
if (!(node.width > 0 && node.height > 0)) return true;
|
|
1678
|
+
const { a, b, c, d, e, f } = tf;
|
|
1679
|
+
const worldCorners = [];
|
|
1680
|
+
let minX = Infinity;
|
|
1681
|
+
let minY = Infinity;
|
|
1682
|
+
let maxX = -Infinity;
|
|
1683
|
+
let maxY = -Infinity;
|
|
1684
|
+
for (let i = 0; i < 4; i++) {
|
|
1685
|
+
const lx = i & 1 ? node.width : 0;
|
|
1686
|
+
const ly = i & 2 ? node.height : 0;
|
|
1687
|
+
const wx = a * lx + c * ly + e;
|
|
1688
|
+
const wy = b * lx + d * ly + f;
|
|
1689
|
+
worldCorners.push({ x: wx, y: wy });
|
|
1690
|
+
if (wx < minX) minX = wx;
|
|
1691
|
+
if (wx > maxX) maxX = wx;
|
|
1692
|
+
if (wy < minY) minY = wy;
|
|
1693
|
+
if (wy > maxY) maxY = wy;
|
|
1694
|
+
}
|
|
1695
|
+
if (!(maxX >= -margin && minX <= this.width + margin && maxY >= -margin && minY <= this.height + margin)) {
|
|
1696
|
+
return false;
|
|
1697
|
+
}
|
|
1698
|
+
for (let ancestor = node.parent; ancestor; ancestor = ancestor.parent) {
|
|
1699
|
+
if (!ancestor.clipChildren || ancestor.width <= 0 || ancestor.height <= 0) continue;
|
|
1700
|
+
let localMinX = Infinity;
|
|
1701
|
+
let localMinY = Infinity;
|
|
1702
|
+
let localMaxX = -Infinity;
|
|
1703
|
+
let localMaxY = -Infinity;
|
|
1704
|
+
for (const corner of worldCorners) {
|
|
1705
|
+
const local = ancestor.worldToLocal(corner.x, corner.y);
|
|
1706
|
+
if (!local) continue;
|
|
1707
|
+
localMinX = Math.min(localMinX, local.x);
|
|
1708
|
+
localMinY = Math.min(localMinY, local.y);
|
|
1709
|
+
localMaxX = Math.max(localMaxX, local.x);
|
|
1710
|
+
localMaxY = Math.max(localMaxY, local.y);
|
|
1711
|
+
}
|
|
1712
|
+
if (!(localMaxX >= -margin && localMinX <= ancestor.width + margin && localMaxY >= -margin && localMinY <= ancestor.height + margin)) {
|
|
1713
|
+
return false;
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
return true;
|
|
1717
|
+
}
|
|
1621
1718
|
syncContentProjection(node) {
|
|
1622
1719
|
if (!this.contentProjectionEnabled || !this.a11yRoot) return;
|
|
1623
1720
|
const projection = node.getContentProjection();
|
|
@@ -1631,6 +1728,17 @@ var Scene = class _Scene {
|
|
|
1631
1728
|
}
|
|
1632
1729
|
return;
|
|
1633
1730
|
}
|
|
1731
|
+
const worldTf = node.getWorldTransform();
|
|
1732
|
+
const margin = this.contentProjectionMargin ?? this.height;
|
|
1733
|
+
if (Number.isFinite(margin) && !this.projectionBoxVisible(node, worldTf, margin)) {
|
|
1734
|
+
if (el) {
|
|
1735
|
+
this.clearContentGridState(node.id, el);
|
|
1736
|
+
el.remove();
|
|
1737
|
+
this.contentElements.delete(node.id);
|
|
1738
|
+
this.a11yNeedsReorder = true;
|
|
1739
|
+
}
|
|
1740
|
+
return;
|
|
1741
|
+
}
|
|
1634
1742
|
if (!el) {
|
|
1635
1743
|
el = document.createElement("div");
|
|
1636
1744
|
el.setAttribute("data-vecto-content", node.id);
|
|
@@ -1727,7 +1835,7 @@ var Scene = class _Scene {
|
|
|
1727
1835
|
el.style.userSelect = selectable ? "text" : "none";
|
|
1728
1836
|
el.style.cursor = selectable ? "text" : "";
|
|
1729
1837
|
}
|
|
1730
|
-
const { a, b, c, d, e, f } =
|
|
1838
|
+
const { a, b, c, d, e, f } = worldTf;
|
|
1731
1839
|
const contentX = projection.contentX ?? 0;
|
|
1732
1840
|
const contentY = projection.contentY ?? 0;
|
|
1733
1841
|
const baselineOffset = lines && lines.length > 0 ? 0 : projection.baseline === void 0 ? 0 : projection.baseline - cssLineBoxBaseline(font, projection.lineHeight ?? 16);
|
|
@@ -1737,42 +1845,7 @@ var Scene = class _Scene {
|
|
|
1737
1845
|
if (node.width > 0) el.style.width = `${node.width}px`;
|
|
1738
1846
|
if (node.height > 0) el.style.height = `${node.height}px`;
|
|
1739
1847
|
el.style.transform = `matrix(${a}, ${b}, ${c}, ${d}, 0, 0)`;
|
|
1740
|
-
|
|
1741
|
-
if (node.width > 0 && node.height > 0) {
|
|
1742
|
-
const worldCorners = [];
|
|
1743
|
-
let minX = Infinity;
|
|
1744
|
-
let minY = Infinity;
|
|
1745
|
-
let maxX = -Infinity;
|
|
1746
|
-
let maxY = -Infinity;
|
|
1747
|
-
for (let i = 0; i < 4; i++) {
|
|
1748
|
-
const lx = i & 1 ? node.width : 0;
|
|
1749
|
-
const ly = i & 2 ? node.height : 0;
|
|
1750
|
-
const wx = a * lx + c * ly + e;
|
|
1751
|
-
const wy = b * lx + d * ly + f;
|
|
1752
|
-
worldCorners.push({ x: wx, y: wy });
|
|
1753
|
-
if (wx < minX) minX = wx;
|
|
1754
|
-
if (wx > maxX) maxX = wx;
|
|
1755
|
-
if (wy < minY) minY = wy;
|
|
1756
|
-
if (wy > maxY) maxY = wy;
|
|
1757
|
-
}
|
|
1758
|
-
visible = maxX >= 0 && minX <= this.width && maxY >= 0 && minY <= this.height;
|
|
1759
|
-
for (let ancestor = node.parent; visible && ancestor; ancestor = ancestor.parent) {
|
|
1760
|
-
if (!ancestor.clipChildren || ancestor.width <= 0 || ancestor.height <= 0) continue;
|
|
1761
|
-
let localMinX = Infinity;
|
|
1762
|
-
let localMinY = Infinity;
|
|
1763
|
-
let localMaxX = -Infinity;
|
|
1764
|
-
let localMaxY = -Infinity;
|
|
1765
|
-
for (const corner of worldCorners) {
|
|
1766
|
-
const local = ancestor.worldToLocal(corner.x, corner.y);
|
|
1767
|
-
if (!local) continue;
|
|
1768
|
-
localMinX = Math.min(localMinX, local.x);
|
|
1769
|
-
localMinY = Math.min(localMinY, local.y);
|
|
1770
|
-
localMaxX = Math.max(localMaxX, local.x);
|
|
1771
|
-
localMaxY = Math.max(localMaxY, local.y);
|
|
1772
|
-
}
|
|
1773
|
-
visible = localMaxX >= 0 && localMinX <= ancestor.width && localMaxY >= 0 && localMinY <= ancestor.height;
|
|
1774
|
-
}
|
|
1775
|
-
}
|
|
1848
|
+
const visible = this.projectionBoxVisible(node, worldTf, 0);
|
|
1776
1849
|
const display = visible ? "" : "none";
|
|
1777
1850
|
if (el.style.display !== display) el.style.display = display;
|
|
1778
1851
|
}
|
|
@@ -2221,6 +2294,7 @@ var Scene = class _Scene {
|
|
|
2221
2294
|
cap = Math.min(cap, 2);
|
|
2222
2295
|
}
|
|
2223
2296
|
if (cap > 0 && time - this.lastTime < 1e3 / cap - 1) {
|
|
2297
|
+
this._skippedFrames++;
|
|
2224
2298
|
this.scheduleFrame();
|
|
2225
2299
|
return;
|
|
2226
2300
|
}
|
|
@@ -2231,11 +2305,23 @@ var Scene = class _Scene {
|
|
|
2231
2305
|
}
|
|
2232
2306
|
this.lastTime = time;
|
|
2233
2307
|
if (this.renderMode === "onDemand" && isIdle) {
|
|
2308
|
+
this._skippedFrames++;
|
|
2234
2309
|
this.scheduleFrame();
|
|
2235
2310
|
return;
|
|
2236
2311
|
}
|
|
2237
2312
|
this.dirty = false;
|
|
2313
|
+
const now = typeof performance !== "undefined" ? performance.now() : time;
|
|
2314
|
+
if (this._lastRenderTick > 0) {
|
|
2315
|
+
const interval = time - this._lastRenderTick;
|
|
2316
|
+
if (interval > 0) {
|
|
2317
|
+
this._avgFrameIntervalMs = this._avgFrameIntervalMs === 0 ? interval : this._avgFrameIntervalMs * 0.9 + interval * 0.1;
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
this._lastRenderTick = time;
|
|
2321
|
+
this._lastDt = dt;
|
|
2238
2322
|
this.render(this.renderer, dt, time);
|
|
2323
|
+
this._lastFrameMs = (typeof performance !== "undefined" ? performance.now() : time) - now;
|
|
2324
|
+
this._renderedFrames++;
|
|
2239
2325
|
const hasActiveAnimation = this.frameHadAnimation;
|
|
2240
2326
|
const hasInteractive = this.frameHadInteractive;
|
|
2241
2327
|
const wantsContentSync = this.contentProjectionEnabled;
|
package/dist/tree/Scene.d.ts
CHANGED
|
@@ -98,9 +98,43 @@ export interface SceneOptions {
|
|
|
98
98
|
* scenes to skip the sync walk.
|
|
99
99
|
*/
|
|
100
100
|
contentProjection?: boolean;
|
|
101
|
+
/**
|
|
102
|
+
* How far outside the viewport (in CSS px, each side) content projections are
|
|
103
|
+
* materialized as DOM. Projections whose box is farther than this are not
|
|
104
|
+
* created — and are removed when they scroll past it — so a document taller
|
|
105
|
+
* than the viewport keeps only a bounded, near-viewport set of DOM nodes
|
|
106
|
+
* instead of one element (plus a `<span>` per line) per block for the whole
|
|
107
|
+
* document. A larger margin keeps more off-screen text ready for native
|
|
108
|
+
* find-in-page / selection at the cost of more DOM; `Infinity` restores the
|
|
109
|
+
* legacy "materialize the entire document" behavior. Default: one viewport
|
|
110
|
+
* height (`undefined` → resolved to `Scene.height` at sync time).
|
|
111
|
+
*/
|
|
112
|
+
contentProjectionMargin?: number;
|
|
101
113
|
}
|
|
102
114
|
/** Frame-rate the loop is capped to when the OS requests reduced motion. */
|
|
103
115
|
export declare const REDUCED_MOTION_FPS = 30;
|
|
116
|
+
/**
|
|
117
|
+
* Live render-loop telemetry, read from {@link Scene.frameStats}. See that
|
|
118
|
+
* getter for how each field is measured.
|
|
119
|
+
*/
|
|
120
|
+
export interface FrameStats {
|
|
121
|
+
/** Rendered-frame cadence (Hz), clamped to `maxFPS`. `0` before the first pair of rendered frames. */
|
|
122
|
+
fps: number;
|
|
123
|
+
/** Wall-clock ms of the last `render()` pass (excludes a11y/content sync). */
|
|
124
|
+
frameTimeMs: number;
|
|
125
|
+
/** Smoothed interval between rendered frames, in ms (EMA). */
|
|
126
|
+
frameIntervalMs: number;
|
|
127
|
+
/** dt (ms) handed to the last rendered frame. */
|
|
128
|
+
dt: number;
|
|
129
|
+
/** Total frames rendered since `start()`. */
|
|
130
|
+
renderedFrames: number;
|
|
131
|
+
/** Total rAF ticks skipped (idle/onDemand/capped) since `start()`. */
|
|
132
|
+
skippedFrames: number;
|
|
133
|
+
/** The scene's current render mode. */
|
|
134
|
+
renderMode: 'always' | 'onDemand';
|
|
135
|
+
/** Whether a redraw is currently pending (the boolean dirty flag). */
|
|
136
|
+
dirty: boolean;
|
|
137
|
+
}
|
|
104
138
|
export interface A11yTreeNode {
|
|
105
139
|
id: string;
|
|
106
140
|
tag: string;
|
|
@@ -147,6 +181,18 @@ export declare class Scene {
|
|
|
147
181
|
private dirty;
|
|
148
182
|
/** Whether to throttle rendering to 2 FPS when the scene is static to save power. */
|
|
149
183
|
autoThrottle: boolean;
|
|
184
|
+
/** Wall-clock ms spent inside the last `render()` call. */
|
|
185
|
+
private _lastFrameMs;
|
|
186
|
+
/** Rolling exponential average of rendered-frame intervals, in ms. */
|
|
187
|
+
private _avgFrameIntervalMs;
|
|
188
|
+
/** dt (ms) handed to the last rendered frame. */
|
|
189
|
+
private _lastDt;
|
|
190
|
+
/** Count of frames actually rendered since the loop started. */
|
|
191
|
+
private _renderedFrames;
|
|
192
|
+
/** Count of rAF ticks skipped (idle / capped) since the loop started. */
|
|
193
|
+
private _skippedFrames;
|
|
194
|
+
/** `time` of the previous *rendered* frame, for interval measurement. */
|
|
195
|
+
private _lastRenderTick;
|
|
150
196
|
/**
|
|
151
197
|
* Frame-rate cap (power saving). `0` = uncapped (native refresh). When set,
|
|
152
198
|
* the loop renders at most `maxFPS` times per second; animations still run,
|
|
@@ -182,6 +228,7 @@ export declare class Scene {
|
|
|
182
228
|
private contentMetricScaleEpoch;
|
|
183
229
|
private contentMetricScaleX;
|
|
184
230
|
private contentProjectionEnabled;
|
|
231
|
+
private contentProjectionMargin;
|
|
185
232
|
/**
|
|
186
233
|
* True while a text-selection drag that started on a projection's blank
|
|
187
234
|
* region (no text node under the press) is being driven manually — the
|
|
@@ -339,6 +386,24 @@ export declare class Scene {
|
|
|
339
386
|
* entity state outside of {@link Entity.animate} so the change is rendered.
|
|
340
387
|
*/
|
|
341
388
|
markDirty(): void;
|
|
389
|
+
/**
|
|
390
|
+
* Live frame telemetry for profilers and devtools overlays. All timings are
|
|
391
|
+
* measured on the `requestAnimationFrame` loop; a scene driven only by
|
|
392
|
+
* {@link step} (e.g. deterministic video export) leaves these at their zero
|
|
393
|
+
* defaults.
|
|
394
|
+
*
|
|
395
|
+
* `fps` is derived from the interval between *rendered* frames, so idle
|
|
396
|
+
* `onDemand` scenes and frames skipped by the {@link maxFPS} cap or the
|
|
397
|
+
* static auto-throttle do not deflate it — it reports the cadence of actual
|
|
398
|
+
* redraws, not the raw rAF rate. `frameTimeMs` is the wall-clock cost of the
|
|
399
|
+
* last `render()` pass alone (excludes a11y/content-projection sync).
|
|
400
|
+
*
|
|
401
|
+
* The renderer always repaints the full canvas, so there is no partial
|
|
402
|
+
* dirty-rectangle to expose; `dirty` is the boolean redraw-pending flag and
|
|
403
|
+
* `pendingRedraw` reflects whether the next `onDemand` tick will actually
|
|
404
|
+
* render.
|
|
405
|
+
*/
|
|
406
|
+
get frameStats(): FrameStats;
|
|
342
407
|
/** True when any node in the subtree has a pending animation. */
|
|
343
408
|
/** True when any node in the subtree is interactive (drives a11y sync). */
|
|
344
409
|
private syncOptionalAttribute;
|
|
@@ -350,6 +415,15 @@ export declare class Scene {
|
|
|
350
415
|
* hidden (`display: none`) so text-heavy scenes only materialize what is
|
|
351
416
|
* visible to the browser's text machinery anyway.
|
|
352
417
|
*/
|
|
418
|
+
/**
|
|
419
|
+
* Whether `node`'s world-space box, expanded by `margin` px on every side,
|
|
420
|
+
* overlaps the scene viewport AND every `clipChildren` ancestor's box. Used
|
|
421
|
+
* both to virtualize content projection (materialize only near-viewport text,
|
|
422
|
+
* at `margin = contentProjectionMargin`) and for the exact `display:none`
|
|
423
|
+
* visibility test (`margin = 0`). Boundless nodes (width/height 0) opt out of
|
|
424
|
+
* culling and always count as visible, matching the legacy behavior.
|
|
425
|
+
*/
|
|
426
|
+
private projectionBoxVisible;
|
|
353
427
|
private syncContentProjection;
|
|
354
428
|
/**
|
|
355
429
|
* Materialize a prepared grid in logical source order while positioning each
|