@pixodesk/svg-animator-web 1.0.27 → 1.0.28

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.cjs CHANGED
@@ -1316,6 +1316,12 @@ var PxScrollSchema = implementsInterface()(px.object({
1316
1316
  kind: px.enum(["view", "scroll"]).optional(),
1317
1317
  axis: px.enum(["block", "inline", "x", "y"]).optional(),
1318
1318
  source: px.enum(["nearest", "root"]).optional(),
1319
+ // Free-form: the two keywords `parent`/`scroller` plus any CSS selector.
1320
+ subject: px.string().optional(),
1321
+ smoothing: px.number().optional(),
1322
+ pin: px.boolean().optional(),
1323
+ pinTop: px.number().optional(),
1324
+ pinDistance: px.number().optional(),
1319
1325
  range: PxScrollRangeSchema.optional()
1320
1326
  }));
1321
1327
  var PxAnimatorConfigSchema = implementsInterface()(px.object({
@@ -6540,6 +6546,10 @@ function createNativeScrollTimeline(subject, config) {
6540
6546
  if (!config || !isScrollTimeline(config)) return null;
6541
6547
  const scroll = config.scroll || {};
6542
6548
  const kind = (_a = scroll.kind) != null ? _a : "view";
6549
+ if (scroll.smoothing) {
6550
+ console.warn('scroll timeline: `smoothing` needs the built-in driver \u2014 ignoring `driver: "native"`');
6551
+ return null;
6552
+ }
6543
6553
  const g = globalThis;
6544
6554
  const view = kind === "view";
6545
6555
  const Ctor = view ? g.ViewTimeline : g.ScrollTimeline;
@@ -6548,7 +6558,7 @@ function createNativeScrollTimeline(subject, config) {
6548
6558
  let timeline;
6549
6559
  try {
6550
6560
  if (view) {
6551
- timeline = new Ctor({ subject, axis });
6561
+ timeline = new Ctor({ subject: resolveScrollSubject(subject, scroll.subject), axis });
6552
6562
  } else {
6553
6563
  const source = scroll.source === "root" ? documentScroller() : findNearestScroller(subject, "y") || findNearestScroller(subject, "x") || documentScroller();
6554
6564
  timeline = new Ctor({ source, axis });
@@ -6564,7 +6574,10 @@ function createNativeScrollTimeline(subject, config) {
6564
6574
  };
6565
6575
  }
6566
6576
  function findNearestScroller(el, axis) {
6577
+ const body = document.body;
6578
+ const root = document.documentElement;
6567
6579
  for (let p = el.parentElement; p; p = p.parentElement) {
6580
+ if (p === body || p === root) return null;
6568
6581
  const style = getComputedStyle(p);
6569
6582
  const overflow = axis === "y" ? style.overflowY : style.overflowX;
6570
6583
  if (overflow === "auto" || overflow === "scroll" || overflow === "hidden" || overflow === "overlay") {
@@ -6576,11 +6589,42 @@ function findNearestScroller(el, axis) {
6576
6589
  function documentScroller() {
6577
6590
  return document.scrollingElement || document.documentElement;
6578
6591
  }
6592
+ var SUBJECT_PARENT = "parent";
6593
+ var SUBJECT_SCROLLER = "scroller";
6594
+ function resolveScrollSubject(svgRoot, subject) {
6595
+ var _a, _b;
6596
+ const spec = subject == null ? void 0 : subject.trim();
6597
+ if (!spec) return svgRoot;
6598
+ if (spec === SUBJECT_PARENT) {
6599
+ let outermostPinned = null;
6600
+ for (let p = svgRoot.parentElement; p && p !== document.body; p = p.parentElement) {
6601
+ const position = getComputedStyle(p).position;
6602
+ if (position === "sticky" || position === "fixed") outermostPinned = p;
6603
+ }
6604
+ return (_b = (_a = outermostPinned == null ? void 0 : outermostPinned.parentElement) != null ? _a : svgRoot.parentElement) != null ? _b : svgRoot;
6605
+ }
6606
+ if (spec === SUBJECT_SCROLLER) {
6607
+ return findNearestScroller(svgRoot, "y") || findNearestScroller(svgRoot, "x") || documentScroller();
6608
+ }
6609
+ let found = null;
6610
+ try {
6611
+ found = document.querySelector(spec);
6612
+ } catch (e) {
6613
+ console.warn('scroll timeline: subject "' + spec + '" is not a valid selector \u2014 measuring the SVG itself');
6614
+ return svgRoot;
6615
+ }
6616
+ if (!found) {
6617
+ console.warn('scroll timeline: subject "' + spec + '" matched no element \u2014 measuring the SVG itself');
6618
+ return svgRoot;
6619
+ }
6620
+ return found;
6621
+ }
6579
6622
  function createScrollDriver(subject, config, onProgress) {
6580
- var _a;
6623
+ var _a, _b;
6581
6624
  if (!config || !isScrollTimeline(config)) return null;
6582
6625
  const scroll = config.scroll || {};
6583
6626
  const kind = (_a = scroll.kind) != null ? _a : "view";
6627
+ const measured = resolveScrollSubject(subject, scroll.subject);
6584
6628
  const nearest = findNearestScroller(subject, "y") || findNearestScroller(subject, "x");
6585
6629
  const scroller = kind === "scroll" && scroll.source === "root" ? documentScroller() : nearest || documentScroller();
6586
6630
  const isRootScroller = scroller === documentScroller();
@@ -6591,7 +6635,7 @@ function createScrollDriver(subject, config, onProgress) {
6591
6635
  const maxOffset = axis === "y" ? scroller.scrollHeight - scroller.clientHeight : scroller.scrollWidth - scroller.clientWidth;
6592
6636
  return scrollOffsetProgress(offset, maxOffset, scroll.range);
6593
6637
  }
6594
- const subjectRect = subject.getBoundingClientRect();
6638
+ const subjectRect = measured.getBoundingClientRect();
6595
6639
  let portStart, portSize;
6596
6640
  if (isRootScroller) {
6597
6641
  portStart = 0;
@@ -6605,12 +6649,43 @@ function createScrollDriver(subject, config, onProgress) {
6605
6649
  const subjectSize = axis === "y" ? subjectRect.height : subjectRect.width;
6606
6650
  return scrollViewProgress(subjectStart, subjectSize, portSize, scroll.range);
6607
6651
  };
6608
- let rafId = null;
6652
+ const smoothingSec = Math.max(0, (_b = scroll.smoothing) != null ? _b : 0) / 1e3;
6653
+ const SETTLE_EPSILON = 1e-3;
6609
6654
  let destroyed = false;
6655
+ let smoothed = null;
6656
+ let smoothRaf = null;
6657
+ let lastFrameMs = 0;
6658
+ const emit = (target) => {
6659
+ if (!smoothingSec) {
6660
+ onProgress(target);
6661
+ return;
6662
+ }
6663
+ if (smoothed === null) {
6664
+ smoothed = target;
6665
+ onProgress(target);
6666
+ return;
6667
+ }
6668
+ if (smoothRaf !== null) return;
6669
+ lastFrameMs = 0;
6670
+ const step = (nowMs) => {
6671
+ smoothRaf = null;
6672
+ if (destroyed) return;
6673
+ const dtSec = lastFrameMs ? Math.min(0.1, (nowMs - lastFrameMs) / 1e3) : 1 / 60;
6674
+ lastFrameMs = nowMs;
6675
+ const goal = compute();
6676
+ const k = 1 - Math.exp(-dtSec / smoothingSec);
6677
+ smoothed = smoothed + (goal - smoothed) * k;
6678
+ if (Math.abs(goal - smoothed) < SETTLE_EPSILON) smoothed = goal;
6679
+ onProgress(smoothed);
6680
+ if (smoothed !== goal) smoothRaf = requestAnimationFrame(step);
6681
+ };
6682
+ smoothRaf = requestAnimationFrame(step);
6683
+ };
6684
+ let rafId = null;
6610
6685
  const tick = () => {
6611
6686
  rafId = null;
6612
6687
  if (destroyed) return;
6613
- onProgress(compute());
6688
+ emit(compute());
6614
6689
  };
6615
6690
  const schedule = () => {
6616
6691
  if (destroyed || rafId !== null) return;
@@ -6622,7 +6697,8 @@ function createScrollDriver(subject, config, onProgress) {
6622
6697
  let resizeObserver;
6623
6698
  if (typeof ResizeObserver !== "undefined") {
6624
6699
  resizeObserver = new ResizeObserver(schedule);
6625
- resizeObserver.observe(subject);
6700
+ resizeObserver.observe(measured);
6701
+ if (measured !== subject) resizeObserver.observe(subject);
6626
6702
  if (!isRootScroller) resizeObserver.observe(scroller);
6627
6703
  }
6628
6704
  const driver = {
@@ -6636,14 +6712,50 @@ function createScrollDriver(subject, config, onProgress) {
6636
6712
  cancelAnimationFrame(rafId);
6637
6713
  rafId = null;
6638
6714
  }
6715
+ if (smoothRaf !== null) {
6716
+ cancelAnimationFrame(smoothRaf);
6717
+ smoothRaf = null;
6718
+ }
6639
6719
  },
6720
+ // `refresh` is a deliberate JUMP (attach, host relayout) — never eased.
6640
6721
  refresh: () => {
6641
- if (!destroyed) onProgress(compute());
6722
+ if (!destroyed) {
6723
+ smoothed = compute();
6724
+ onProgress(smoothed);
6725
+ }
6642
6726
  }
6643
6727
  };
6644
6728
  driver.refresh();
6645
6729
  return driver;
6646
6730
  }
6731
+ function applyScrollPin(svgRoot, scroll) {
6732
+ var _a;
6733
+ const styled = svgRoot;
6734
+ if (!(scroll == null ? void 0 : scroll.pin) || !styled.style) return () => {
6735
+ };
6736
+ const style = styled.style;
6737
+ const prevPosition = style.position;
6738
+ const prevTop = style.top;
6739
+ style.position = "sticky";
6740
+ style.top = ((_a = scroll.pinTop) != null ? _a : 0) + "px";
6741
+ let wrapper = null;
6742
+ const parent = svgRoot.parentElement;
6743
+ if (scroll.pinDistance && scroll.pinDistance > 0 && parent) {
6744
+ wrapper = document.createElement("div");
6745
+ wrapper.setAttribute("data-px-pin", "");
6746
+ wrapper.style.height = scroll.pinDistance * 100 + "vh";
6747
+ parent.insertBefore(wrapper, svgRoot);
6748
+ wrapper.appendChild(svgRoot);
6749
+ }
6750
+ return () => {
6751
+ style.position = prevPosition;
6752
+ style.top = prevTop;
6753
+ if (wrapper == null ? void 0 : wrapper.parentElement) {
6754
+ wrapper.parentElement.insertBefore(svgRoot, wrapper);
6755
+ wrapper.remove();
6756
+ }
6757
+ };
6758
+ }
6647
6759
 
6648
6760
  // src/PxAnimatorBind.ts
6649
6761
  function finaliseAnimator(animatorConfig, callbacks, make) {
@@ -6670,7 +6782,10 @@ function bindWithEngineChoice(doc, adapter, callbacks, rootElement) {
6670
6782
  if (isScrollTimeline(animatorConfig)) {
6671
6783
  return finaliseAnimator(animatorConfig, callbacks, (cb) => {
6672
6784
  var _a, _b;
6785
+ let unpin = () => {
6786
+ };
6673
6787
  if (animatorConfig.mode !== PxAnimatorMode.frames && ((_a = animatorConfig.scroll) == null ? void 0 : _a.driver) === "native" && rootElement) {
6788
+ unpin = applyScrollPin(rootElement, animatorConfig.scroll);
6674
6789
  const native = createNativeScrollTimeline(rootElement, animatorConfig);
6675
6790
  if (native) {
6676
6791
  const api2 = createWebApiAnimator(
@@ -6680,12 +6795,23 @@ function bindWithEngineChoice(doc, adapter, callbacks, rootElement) {
6680
6795
  animatorConfig.mode === PxAnimatorMode.waapi,
6681
6796
  native
6682
6797
  );
6683
- if (api2) return api2;
6798
+ if (api2) {
6799
+ const destroyNative = api2.destroy.bind(api2);
6800
+ api2.destroy = () => {
6801
+ unpin();
6802
+ destroyNative();
6803
+ };
6804
+ return api2;
6805
+ }
6684
6806
  }
6807
+ unpin();
6808
+ unpin = () => {
6809
+ };
6685
6810
  }
6686
6811
  const api = (animatorConfig.mode !== PxAnimatorMode.frames ? createWebApiAnimator(doc, cb, rootElement, animatorConfig.mode === PxAnimatorMode.waapi) : null) || createFrameLoopAnimator(doc, adapter, cb, rootElement);
6687
6812
  const subject = ((_b = api.getRootElement) == null ? void 0 : _b.call(api)) || rootElement;
6688
6813
  if (subject) {
6814
+ unpin = applyScrollPin(subject, animatorConfig.scroll);
6689
6815
  const totalMs = scrollTotalDurationMs(animatorConfig);
6690
6816
  const driver = createScrollDriver(
6691
6817
  subject,
@@ -6696,6 +6822,7 @@ function bindWithEngineChoice(doc, adapter, callbacks, rootElement) {
6696
6822
  const destroy = api.destroy.bind(api);
6697
6823
  api.destroy = () => {
6698
6824
  driver.destroy();
6825
+ unpin();
6699
6826
  destroy();
6700
6827
  };
6701
6828
  }