@sebastianwessel/isostate 0.1.0 → 0.2.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.
@@ -0,0 +1,5 @@
1
+ import { mountScene } from "./runtime/mount-scene.ts";
2
+ import { projectToScreen as projectGridToScreen } from "./utils/projection.ts";
3
+ export { mountScene };
4
+ export declare function projectToScreen(x: number, y: number, cellSize: number, offsetX?: number, offsetY?: number, paddingX?: number, paddingY?: number): ReturnType<typeof projectGridToScreen>;
5
+ //# sourceMappingURL=browser-runtime.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser-runtime.d.ts","sourceRoot":"","sources":["../src/browser-runtime.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AACtD,OAAO,EAAE,eAAe,IAAI,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAE/E,OAAO,EAAE,UAAU,EAAE,CAAC;AAEtB,wBAAgB,eAAe,CAC9B,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,EACT,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,MAAM,EAChB,QAAQ,CAAC,EAAE,MAAM,EACjB,QAAQ,CAAC,EAAE,MAAM,GACf,UAAU,CAAC,OAAO,mBAAmB,CAAC,CAExC"}
@@ -258,7 +258,7 @@ function interpolateConnector(id, prevStop, nextStop, t) {
258
258
  route: interpolateRoute(prev.route, next.route, t),
259
259
  layer: t < 1 ? prev.layer : next.layer,
260
260
  lifecycle,
261
- style: cloneConnectorStyle(next.style),
261
+ style: cloneConnectorStyle(t < 1 ? prev.style : next.style),
262
262
  start: next.start,
263
263
  end: next.end,
264
264
  direction: next.direction,
@@ -908,6 +908,7 @@ function buildSceneDOM(container, bundle, config) {
908
908
  presence: "removed",
909
909
  };
910
910
  const instance = createElementInstance(initial, layout, assetResolver);
911
+ instance.anchor = assetAnchorForBounds(bundle, initial);
911
912
  instance.node.classList.add(`iso-layer-${def.layer}`);
912
913
  instance.node.setAttribute("data-layer", def.layer);
913
914
  if (initial.presence === "removed") {
@@ -936,6 +937,7 @@ function updateElementTransforms(svg, elements, connectors = []) {
936
937
  const state = map.get(def.id);
937
938
  if (!state)
938
939
  continue;
940
+ state.current = def;
939
941
  updateGeneratedElementContent(state.node, def, layout);
940
942
  applyElementTransform(state.node, def, layout);
941
943
  applyAmbientClasses(state, def.ambient ?? []);
@@ -962,6 +964,32 @@ function getConnectorState(svg, id) {
962
964
  function getResolvedViewBox(bundle) {
963
965
  return resolveSceneLayout(bundle).viewBox;
964
966
  }
967
+ function getCurrentElementBounds(svg, id) {
968
+ const layout = svg._layout;
969
+ const state = svg._elementMap?.get(id);
970
+ if (!layout || !state || state.current.presence === "removed")
971
+ return undefined;
972
+ return boundsToViewBox(calculateElementBounds(state.current, layout, state.anchor));
973
+ }
974
+ function getGridAreaBounds(bundle, area) {
975
+ const layout = resolveSceneLayout(bundle);
976
+ const [x, y] = area.at;
977
+ const [width, height] = area.size;
978
+ let bounds = emptyBounds();
979
+ for (const point of [
980
+ [x, y],
981
+ [x + width, y],
982
+ [x, y + height],
983
+ [x + width, y + height],
984
+ ]) {
985
+ const screen = projectToScreen(point[0], point[1], layout.cellSize, layout.selectedBounds.minX, layout.selectedBounds.minY, layout.padding.x, layout.padding.y);
986
+ bounds = includePoint(bounds, screen.screenX, screen.screenY);
987
+ }
988
+ return boundsToViewBox(bounds);
989
+ }
990
+ function applySceneViewBox(svg, viewBox) {
991
+ svg.setAttribute("viewBox", `${viewBox.minX} ${viewBox.minY} ${viewBox.width} ${viewBox.height}`);
992
+ }
965
993
  // ── Lifecycle helpers ─────────────────────────────────────────────────────
966
994
  /** Hide an element after its exit animation completes. */
967
995
  function hideElementAfterExit(node) {
@@ -1003,15 +1031,7 @@ function calculateContentBounds(bundle, cellSize) {
1003
1031
  for (const element of stop.elements ?? []) {
1004
1032
  if (element.presence === "removed")
1005
1033
  continue;
1006
- const { rawX, rawY } = projectToRaw(element.pos[0] + element.size, element.pos[1] + element.size, cellSize);
1007
- const visualSize = calculateVisualSize(element.size, cellSize);
1008
- const [anchorX, anchorY] = assetAnchorForBounds(bundle, element);
1009
- bounds = includeBounds(bounds, {
1010
- minX: rawX - visualSize * anchorX,
1011
- minY: rawY - visualSize * anchorY,
1012
- maxX: rawX + visualSize * (1 - anchorX),
1013
- maxY: rawY + visualSize * (1 - anchorY),
1014
- });
1034
+ bounds = includeBounds(bounds, calculateRawElementBounds(bundle, element, cellSize));
1015
1035
  }
1016
1036
  for (const connector of stop.connectors ?? []) {
1017
1037
  if (connector.presence === "removed")
@@ -1021,6 +1041,27 @@ function calculateContentBounds(bundle, cellSize) {
1021
1041
  }
1022
1042
  return normalizeBounds(bounds, cellSize);
1023
1043
  }
1044
+ function calculateRawElementBounds(bundle, element, cellSize) {
1045
+ const { rawX, rawY } = projectToRaw(element.pos[0] + element.size, element.pos[1] + element.size, cellSize);
1046
+ const visualSize = calculateVisualSize(element.size, cellSize);
1047
+ const [anchorX, anchorY] = assetAnchorForBounds(bundle, element);
1048
+ return {
1049
+ minX: rawX - visualSize * anchorX,
1050
+ minY: rawY - visualSize * anchorY,
1051
+ maxX: rawX + visualSize * (1 - anchorX),
1052
+ maxY: rawY + visualSize * (1 - anchorY),
1053
+ };
1054
+ }
1055
+ function calculateElementBounds(element, layout, anchor) {
1056
+ const screen = projectToScreen(element.pos[0] + element.size, element.pos[1] + element.size, layout.cellSize, layout.selectedBounds.minX, layout.selectedBounds.minY, layout.padding.x, layout.padding.y);
1057
+ const visualSize = calculateVisualSize(element.size, layout.cellSize);
1058
+ return {
1059
+ minX: screen.screenX - visualSize * anchor[0],
1060
+ minY: screen.screenY - visualSize * anchor[1],
1061
+ maxX: screen.screenX + visualSize * (1 - anchor[0]),
1062
+ maxY: screen.screenY + visualSize * (1 - anchor[1]),
1063
+ };
1064
+ }
1024
1065
  function assetAnchorForBounds(bundle, element) {
1025
1066
  return bundle.assets?.[element.asset]?.anchor ?? [0.5, 1];
1026
1067
  }
@@ -1076,6 +1117,18 @@ function normalizeBounds(bounds, cellSize) {
1076
1117
  return bounds;
1077
1118
  return { minX: 0, minY: 0, maxX: cellSize, maxY: cellSize };
1078
1119
  }
1120
+ function boundsToViewBox(bounds) {
1121
+ const centerX = (bounds.minX + bounds.maxX) / 2;
1122
+ const centerY = (bounds.minY + bounds.maxY) / 2;
1123
+ const width = Math.max(1, bounds.maxX - bounds.minX);
1124
+ const height = Math.max(1, bounds.maxY - bounds.minY);
1125
+ return {
1126
+ minX: roundDimension(centerX - width / 2),
1127
+ minY: roundDimension(centerY - height / 2),
1128
+ width: roundDimension(width),
1129
+ height: roundDimension(height),
1130
+ };
1131
+ }
1079
1132
  function includePoint(bounds, x, y) {
1080
1133
  return {
1081
1134
  minX: Math.min(bounds.minX, x),
@@ -1242,9 +1295,9 @@ function createElementInstance(def, layout, resolveAsset) {
1242
1295
  node.addEventListener("animationend", () => {
1243
1296
  node.style.animation = "";
1244
1297
  }, { once: true });
1245
- return { node, isHidden: false, entryKey: entryAnim, ambient: new Set() };
1298
+ return { node, current: def, anchor: [0.5, 1], isHidden: false, entryKey: entryAnim, ambient: new Set() };
1246
1299
  }
1247
- return { node, isHidden: false, ambient: new Set() };
1300
+ return { node, current: def, anchor: [0.5, 1], isHidden: false, ambient: new Set() };
1248
1301
  }
1249
1302
  function isTextAsset(assetId) {
1250
1303
  return assetId === BUILT_IN_TEXT_ASSET_ID;
@@ -1502,8 +1555,7 @@ function applyConnectorAmbientClasses(state, ambient) {
1502
1555
  state.shaft.classList.remove(`iso-ambient-${name}`);
1503
1556
  }
1504
1557
  for (const name of next) {
1505
- if (!state.ambient.has(name))
1506
- state.shaft.classList.add(`iso-ambient-${name}`);
1558
+ state.shaft.classList.add(`iso-ambient-${name}`);
1507
1559
  }
1508
1560
  state.ambient = next;
1509
1561
  }
@@ -1587,6 +1639,8 @@ class AnimationController {
1587
1639
  _pendingProgress = null;
1588
1640
  _destroyed = false;
1589
1641
  _listeners = new Map();
1642
+ _cameraAnim = null;
1643
+ _cameraState = null;
1590
1644
  // Scroll tracking state
1591
1645
  _minScroll = 0;
1592
1646
  _maxScroll = 0;
@@ -1638,6 +1692,11 @@ class AnimationController {
1638
1692
  this._paused = false;
1639
1693
  this._config = { ...DEFAULT_CONFIG, ...config };
1640
1694
  this._sceneElement = runtime.sceneElement ?? config.sceneElement ?? null;
1695
+ this._cameraState = {
1696
+ viewBox: getResolvedViewBox(bundle),
1697
+ target: { type: "reset" },
1698
+ isZoomed: false,
1699
+ };
1641
1700
  this._engine.init(bundle);
1642
1701
  this._bindScroll();
1643
1702
  }
@@ -1654,6 +1713,7 @@ class AnimationController {
1654
1713
  return;
1655
1714
  }
1656
1715
  this._progress = clamped;
1716
+ this._cancelCameraAnimation();
1657
1717
  if (this._paused)
1658
1718
  return;
1659
1719
  this._scheduleProgressForward(clamped);
@@ -1721,6 +1781,47 @@ class AnimationController {
1721
1781
  this._assertNotDestroyed();
1722
1782
  return this._paused;
1723
1783
  }
1784
+ zoomToElement(id, options = {}) {
1785
+ this._assertNotDestroyed();
1786
+ const svg = this._getSceneSvg();
1787
+ if (!this._bundle || !svg) {
1788
+ throw new ControllerError("CAMERA_NOT_INITIALIZED", "zoomToElement() requires an initialized scene SVG");
1789
+ }
1790
+ const elementBounds = getCurrentElementBounds(svg, id);
1791
+ if (!elementBounds) {
1792
+ throw new ControllerError("CAMERA_TARGET_NOT_FOUND", `Camera target element "${id}" was not found`, {
1793
+ elementId: id,
1794
+ });
1795
+ }
1796
+ this._applyCameraDestination(expandViewBox(elementBounds, this._resolveCameraPadding(options)), { type: "element", id }, options);
1797
+ }
1798
+ zoomToArea(area, options = {}) {
1799
+ this._assertNotDestroyed();
1800
+ if (!this._bundle || !this._getSceneSvg()) {
1801
+ throw new ControllerError("CAMERA_NOT_INITIALIZED", "zoomToArea() requires an initialized scene SVG");
1802
+ }
1803
+ this._assertValidCameraArea(area);
1804
+ const bounds = getGridAreaBounds(this._bundle, area);
1805
+ this._applyCameraDestination(expandViewBox(bounds, this._resolveCameraPadding(options)), {
1806
+ type: "area",
1807
+ at: [...area.at],
1808
+ size: [...area.size],
1809
+ }, options);
1810
+ }
1811
+ resetZoom(options = {}) {
1812
+ this._assertNotDestroyed();
1813
+ if (!this._bundle || !this._getSceneSvg()) {
1814
+ throw new ControllerError("CAMERA_NOT_INITIALIZED", "resetZoom() requires an initialized scene SVG");
1815
+ }
1816
+ this._applyCameraDestination(getResolvedViewBox(this._bundle), { type: "reset" }, options);
1817
+ }
1818
+ getCameraState() {
1819
+ this._assertNotDestroyed();
1820
+ if (!this._cameraState) {
1821
+ throw new ControllerError("CAMERA_NOT_INITIALIZED", "Camera state is not initialized");
1822
+ }
1823
+ return copyCameraState(this._cameraState);
1824
+ }
1724
1825
  /**
1725
1826
  * Destroy controller and clean up all listeners and resources.
1726
1827
  */
@@ -1729,12 +1830,14 @@ class AnimationController {
1729
1830
  this._unbindScroll();
1730
1831
  this._cancelFrame();
1731
1832
  this._cancelTransition();
1833
+ this._cancelCameraAnimation();
1732
1834
  if (this._ownsEngine)
1733
1835
  this._engine.destroy();
1734
1836
  this._listeners.clear();
1735
1837
  this._bundle = null;
1736
1838
  this._container = null;
1737
1839
  this._pendingProgress = null;
1840
+ this._cameraState = null;
1738
1841
  this._destroyed = true;
1739
1842
  }
1740
1843
  // ── Event system ───────────────────────────────────────────────────────
@@ -1824,6 +1927,121 @@ class AnimationController {
1824
1927
  updateElementTransforms(svg, updates, connectors);
1825
1928
  this._applyLifecycleChanges(updates);
1826
1929
  this._applyConnectorLifecycleChanges(connectors);
1930
+ this._applyCameraForProgress(this._progress);
1931
+ }
1932
+ _applyCameraForProgress(progress) {
1933
+ const svg = this._getSceneSvg();
1934
+ if (!this._bundle || !svg)
1935
+ return;
1936
+ const sceneStops = this.scenes;
1937
+ if (sceneStops.length === 0)
1938
+ return;
1939
+ const { previous, next } = surroundingStops(sceneStops, progress);
1940
+ const previousCamera = this._effectiveCameraAt(previous.index);
1941
+ const nextCamera = this._effectiveCameraAt(next.index);
1942
+ const previousViewBox = this._resolveCameraViewBox(previousCamera);
1943
+ const nextViewBox = this._resolveCameraViewBox(nextCamera);
1944
+ const range = next.scene.progress - previous.scene.progress;
1945
+ const rawT = range <= 0 ? 0 : (progress - previous.scene.progress) / range;
1946
+ const t = resolveCameraEasing(nextCamera.easing)(Math.max(0, Math.min(1, rawT)));
1947
+ const viewBox = lerpViewBox(previousViewBox, nextViewBox, t);
1948
+ const target = t < 1 ? previousCamera.target : nextCamera.target;
1949
+ this._setCameraViewBox(svg, viewBox, target);
1950
+ }
1951
+ _effectiveCameraAt(index) {
1952
+ for (let i = index; i >= 0; i--) {
1953
+ const camera = this.scenes[i]?.camera;
1954
+ if (camera) {
1955
+ return {
1956
+ target: camera.target,
1957
+ padding: camera.padding,
1958
+ duration: camera.duration,
1959
+ easing: camera.easing,
1960
+ };
1961
+ }
1962
+ }
1963
+ return { target: { type: "reset" } };
1964
+ }
1965
+ _resolveCameraViewBox(camera) {
1966
+ if (!this._bundle)
1967
+ throw new ControllerError("CAMERA_NOT_INITIALIZED", "Camera requires an initialized bundle");
1968
+ if (camera.target.type === "reset")
1969
+ return getResolvedViewBox(this._bundle);
1970
+ if (camera.target.type === "area") {
1971
+ return expandViewBox(getGridAreaBounds(this._bundle, { at: camera.target.at, size: camera.target.size }), camera.padding ?? 32);
1972
+ }
1973
+ const svg = this._getSceneSvg();
1974
+ if (!svg)
1975
+ throw new ControllerError("CAMERA_NOT_INITIALIZED", "Camera requires an initialized scene SVG");
1976
+ const bounds = getCurrentElementBounds(svg, camera.target.id);
1977
+ if (!bounds) {
1978
+ throw new ControllerError("CAMERA_TARGET_NOT_FOUND", `Camera target element "${camera.target.id}" was not found`, {
1979
+ elementId: camera.target.id,
1980
+ });
1981
+ }
1982
+ return expandViewBox(bounds, camera.padding ?? 32);
1983
+ }
1984
+ _applyCameraDestination(viewBox, target, options) {
1985
+ const svg = this._getSceneSvg();
1986
+ if (!svg)
1987
+ throw new ControllerError("CAMERA_NOT_INITIALIZED", "Camera requires an initialized scene SVG");
1988
+ this._cancelCameraAnimation();
1989
+ const duration = this._resolveCameraDuration(options);
1990
+ if (duration === 0) {
1991
+ this._setCameraViewBox(svg, viewBox, target);
1992
+ return;
1993
+ }
1994
+ const from = currentSvgViewBox(svg) ?? this._cameraState?.viewBox ?? viewBox;
1995
+ const easing = resolveCameraEasing(options.easing ?? this._config.transitionEasing);
1996
+ const start = performance.now();
1997
+ const step = (now) => {
1998
+ const t = Math.min((now - start) / duration, 1);
1999
+ this._setCameraViewBox(svg, lerpViewBox(from, viewBox, easing(t)), target);
2000
+ if (t < 1) {
2001
+ this._cameraAnim = requestAnimationFrame(step);
2002
+ }
2003
+ else {
2004
+ this._cameraAnim = null;
2005
+ }
2006
+ };
2007
+ this._cameraAnim = requestAnimationFrame(step);
2008
+ }
2009
+ _setCameraViewBox(svg, viewBox, target) {
2010
+ applySceneViewBox(svg, viewBox);
2011
+ const full = this._bundle ? getResolvedViewBox(this._bundle) : viewBox;
2012
+ this._cameraState = {
2013
+ viewBox,
2014
+ target,
2015
+ isZoomed: !sameViewBox(viewBox, full),
2016
+ };
2017
+ this._emit("camera-change", this.getCameraState());
2018
+ }
2019
+ _getSceneSvg() {
2020
+ return (this._sceneElement ?? this._container?.querySelector("svg") ?? null);
2021
+ }
2022
+ _resolveCameraPadding(options) {
2023
+ const padding = options.padding ?? 32;
2024
+ if (!Number.isFinite(padding) || padding < 0 || padding > 2048) {
2025
+ throw new ControllerError("INVALID_CAMERA_OPTIONS", "Camera padding must be between 0 and 2048");
2026
+ }
2027
+ return padding;
2028
+ }
2029
+ _resolveCameraDuration(options) {
2030
+ const duration = options.duration ?? this._config.transitionDuration;
2031
+ if (!Number.isInteger(duration) || duration < 0 || duration > 10000) {
2032
+ throw new ControllerError("INVALID_CAMERA_OPTIONS", "Camera duration must be an integer from 0 to 10000");
2033
+ }
2034
+ return duration;
2035
+ }
2036
+ _assertValidCameraArea(area) {
2037
+ if (!Array.isArray(area.at) ||
2038
+ area.at.length !== 2 ||
2039
+ !area.at.every((value) => Number.isFinite(value) && value >= 0) ||
2040
+ !Array.isArray(area.size) ||
2041
+ area.size.length !== 2 ||
2042
+ !area.size.every((value) => Number.isFinite(value) && value > 0)) {
2043
+ throw new ControllerError("INVALID_CAMERA_OPTIONS", "Camera area must use non-negative at and positive size");
2044
+ }
1827
2045
  }
1828
2046
  _applyLifecycleChanges(elements) {
1829
2047
  for (const elDef of elements) {
@@ -1910,8 +2128,11 @@ class AnimationController {
1910
2128
  if (entryAnim === "none")
1911
2129
  return;
1912
2130
  animateElement(state.node, `iso-anim-${entryAnim}`, "enter");
2131
+ const expectedAnimation = state.node.style.animation;
1913
2132
  state.node.addEventListener("animationend", () => {
1914
- state.node.style.animation = "";
2133
+ if (state.node.style.animation === expectedAnimation) {
2134
+ state.node.style.animation = "";
2135
+ }
1915
2136
  }, { once: true });
1916
2137
  }
1917
2138
  _applyExitAnimation(elDef, state) {
@@ -1921,8 +2142,11 @@ class AnimationController {
1921
2142
  return;
1922
2143
  }
1923
2144
  animateElement(state.node, `iso-anim-${exitAnim}`, "exit");
2145
+ const expectedAnimation = state.node.style.animation;
1924
2146
  state.node.addEventListener("animationend", () => {
1925
- hideElementAfterExit(state.node);
2147
+ if (state.node.style.animation === expectedAnimation) {
2148
+ hideElementAfterExit(state.node);
2149
+ }
1926
2150
  }, { once: true });
1927
2151
  }
1928
2152
  _applyConnectorEntryAnimation(connectorDef, state) {
@@ -1930,8 +2154,11 @@ class AnimationController {
1930
2154
  if (entryAnim === "none")
1931
2155
  return;
1932
2156
  animateElement(state.node, `iso-anim-${entryAnim}`, "enter");
2157
+ const expectedAnimation = state.node.style.animation;
1933
2158
  state.node.addEventListener("animationend", () => {
1934
- state.node.style.animation = "";
2159
+ if (state.node.style.animation === expectedAnimation) {
2160
+ state.node.style.animation = "";
2161
+ }
1935
2162
  }, { once: true });
1936
2163
  }
1937
2164
  _applyConnectorExitAnimation(connectorDef, state) {
@@ -1941,8 +2168,11 @@ class AnimationController {
1941
2168
  return;
1942
2169
  }
1943
2170
  animateElement(state.node, `iso-anim-${exitAnim}`, "exit");
2171
+ const expectedAnimation = state.node.style.animation;
1944
2172
  state.node.addEventListener("animationend", () => {
1945
- hideElementAfterExit(state.node);
2173
+ if (state.node.style.animation === expectedAnimation) {
2174
+ hideElementAfterExit(state.node);
2175
+ }
1946
2176
  }, { once: true });
1947
2177
  }
1948
2178
  // ── Scene transitions ──────────────────────────────────────────────────
@@ -1991,6 +2221,12 @@ class AnimationController {
1991
2221
  this._transitionAnim = null;
1992
2222
  }
1993
2223
  }
2224
+ _cancelCameraAnimation() {
2225
+ if (this._cameraAnim !== null) {
2226
+ cancelAnimationFrame(this._cameraAnim);
2227
+ this._cameraAnim = null;
2228
+ }
2229
+ }
1994
2230
  // ── Scroll binding ─────────────────────────────────────────────────────
1995
2231
  _bindScroll() {
1996
2232
  const container = this._config.container;
@@ -2175,9 +2411,70 @@ function oppositeEntryAnimation(exit) {
2175
2411
  return "none";
2176
2412
  }
2177
2413
  }
2414
+ function surroundingStops(scenes, progress) {
2415
+ let previousIndex = 0;
2416
+ let nextIndex = scenes.length - 1;
2417
+ for (let i = 0; i < scenes.length; i++) {
2418
+ if (scenes[i].progress <= progress)
2419
+ previousIndex = i;
2420
+ if (scenes[i].progress >= progress) {
2421
+ nextIndex = i;
2422
+ break;
2423
+ }
2424
+ }
2425
+ return {
2426
+ previous: { scene: scenes[previousIndex], index: previousIndex },
2427
+ next: { scene: scenes[nextIndex], index: nextIndex },
2428
+ };
2429
+ }
2430
+ function expandViewBox(viewBox, padding) {
2431
+ return {
2432
+ minX: roundCameraNumber(viewBox.minX - padding),
2433
+ minY: roundCameraNumber(viewBox.minY - padding),
2434
+ width: roundCameraNumber(Math.max(1, viewBox.width + padding * 2)),
2435
+ height: roundCameraNumber(Math.max(1, viewBox.height + padding * 2)),
2436
+ };
2437
+ }
2438
+ function lerpViewBox(from, to, t) {
2439
+ return {
2440
+ minX: roundCameraNumber(from.minX + (to.minX - from.minX) * t),
2441
+ minY: roundCameraNumber(from.minY + (to.minY - from.minY) * t),
2442
+ width: roundCameraNumber(from.width + (to.width - from.width) * t),
2443
+ height: roundCameraNumber(from.height + (to.height - from.height) * t),
2444
+ };
2445
+ }
2446
+ function sameViewBox(a, b) {
2447
+ return a.minX === b.minX && a.minY === b.minY && a.width === b.width && a.height === b.height;
2448
+ }
2449
+ function currentSvgViewBox(svg) {
2450
+ const raw = svg.getAttribute("viewBox");
2451
+ if (!raw)
2452
+ return undefined;
2453
+ const parts = raw.trim().split(/\s+/).map(Number);
2454
+ if (parts.length !== 4 || parts.some((part) => !Number.isFinite(part)))
2455
+ return undefined;
2456
+ return { minX: parts[0], minY: parts[1], width: parts[2], height: parts[3] };
2457
+ }
2458
+ function resolveCameraEasing(easing) {
2459
+ return resolveEasing(easing === "ease-in-out" ? "easeInOutCubic" : easing === "ease-out" ? "easeOutCubic" : "linear");
2460
+ }
2461
+ function copyCameraState(state) {
2462
+ return {
2463
+ viewBox: { ...state.viewBox },
2464
+ target: state.target?.type === "area"
2465
+ ? { type: "area", at: [...state.target.at], size: [...state.target.size] }
2466
+ : state.target
2467
+ ? { ...state.target }
2468
+ : undefined,
2469
+ isZoomed: state.isZoomed,
2470
+ };
2471
+ }
2472
+ function roundCameraNumber(value) {
2473
+ return Math.round(value * 1000) / 1000;
2474
+ }
2178
2475
 
2179
2476
  const RUNTIME_BUNDLE_FORMAT = "isostate-runtime-bundle";
2180
- const RUNTIME_VERSION = "0.1.0";
2477
+ const RUNTIME_VERSION = "0.2.0";
2181
2478
  const HEX_DIGEST_PATTERN = /^[a-f0-9]{64}$/;
2182
2479
  /** Mount a compiled runtime bundle into an HTML element. */
2183
2480
  function mountScene(target, bundle, options = {}) {
@@ -2267,6 +2564,10 @@ function getResolvedConfig(bundle, options = {}) {
2267
2564
  align: [...bundle.layout.align],
2268
2565
  },
2269
2566
  viewBox: getResolvedViewBox(bundle),
2567
+ camera: {
2568
+ viewBox: getResolvedViewBox(bundle),
2569
+ isZoomed: false,
2570
+ },
2270
2571
  theme: bundle.theme,
2271
2572
  themeVars: getResolvedThemeVars(bundle, options.themeVars),
2272
2573
  scenes: bundle.scenes.map((scene) => ({
@@ -2377,4 +2678,4 @@ function rotr(value, bits) {
2377
2678
  }
2378
2679
 
2379
2680
  export { AnimationController as A, DEFAULT_CELL_SIZE as D, AnimationEngine as a, AssetRegistryImpl as b, applyThemeToElement as c, buildSceneDOM as d, calculateTransform as e, calculateVisualSize as f, composeTheme as g, createAssetRegistry as h, createDefaultRegistry as i, easeInCubic as j, easeInOutCubic as k, easeOutCubic as l, linear as m, mountScene as n, resolveTheme as o, projectToScreen as p, resolveEasing as r };
2380
- //# sourceMappingURL=index-CDQt8CfR.js.map
2681
+ //# sourceMappingURL=index-DoyWxJDm.js.map