@brytdesigns/web-component-parallax 1.2.17 → 1.2.19

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/esm/index.js CHANGED
@@ -2,7 +2,9 @@ import { createWithElementContext, customShadowlessElement, correctElementType,
2
2
  import { splitProps, createEffect, batch, onCleanup, mergeProps, createMemo } from 'solid-js';
3
3
  import { createStore } from 'solid-js/store';
4
4
  import { createContext, consume, provide } from 'component-register';
5
- import { motionValue, scroll, frame, animate, transform, transformValue, styleEffect } from 'motion';
5
+ import { motionValue, scroll, transformValue, transform, frame, animate, styleEffect } from 'motion';
6
+ import { awaitAllAnimations } from '@brytdesigns/web-component-core/animation';
7
+ import { abortablePromise } from '@brytdesigns/web-component-core/promise';
6
8
 
7
9
  var __defProp = Object.defineProperty;
8
10
  var __export = (target, all) => {
@@ -118,48 +120,29 @@ function getLastTruthyIndex(values) {
118
120
  }
119
121
 
120
122
  // src/hooks/useParallaxStickyLayer.ts
121
- var HANDOFF_EASE = "easeOut";
122
- var ENTER_HYSTERESIS = 1e-3;
123
- var EXIT_HYSTERESIS = 5e-3;
124
- var HANDOFF_MIN_DELTA_PX = 0.75;
125
- var EXIT_WAIT_TIMEOUT_MS = 500;
126
- var PREDICT_LOOKAHEAD_MS = 28;
127
- var PREDICT_ENTER_BAND = 6e-3;
128
- var PREDICT_EXIT_BAND = 0.01;
129
- var PREDICT_MIN_VELOCITY = 4e-4;
130
- var LOW_VELOCITY_NO_TWEEN = 1e-3;
131
- var HIGH_VELOCITY_SKIP_HANDOFF = 0.01;
132
- var MAX_PREDICT_LOOKAHEAD_MS = 60;
133
- var MAX_HANDOFF_DELTA_PX = 56;
134
- function supportsStickyPosition() {
135
- if (typeof CSS === "undefined" || typeof CSS.supports !== "function") {
136
- return false;
137
- }
138
- return CSS.supports("position", "sticky") || CSS.supports("position", "-webkit-sticky");
139
- }
140
123
  function initializeParallaxStickyLayerContext(props) {
141
124
  const [_internal, _state] = splitProps(props, ["root", "start", "end"]);
142
125
  const parallax = useParallax(_internal.root);
143
- const stickySupported = supportsStickyPosition();
144
126
  const progress = motionValue(-1);
145
- const rawScrollProgress = motionValue(-1);
146
127
  const inRange = motionValue(false);
147
128
  const belowRange = motionValue(false);
148
129
  const aboveRange = motionValue(false);
149
- const normStartValue = motionValue(0);
150
- const normEndValue = motionValue(1);
151
- const mode = motionValue("absolute-below");
152
- const handoffOffsetY = motionValue(0);
153
- const activeContentAnimations = /* @__PURE__ */ new Set();
154
- let handoffAnimation = null;
155
- let exitWaitRaf = null;
156
- let exitWaitTimeout = null;
157
- let pendingExitVersion = 0;
158
- let pendingExitMode = null;
159
- let lastScrollSampleValue = 0;
160
- let lastScrollSampleTime = 0;
161
- let hasScrollSample = false;
162
- let latestScrollVelocity = 0;
130
+ const position = transformValue(
131
+ () => inRange.get() ? "sticky" : "absolute"
132
+ );
133
+ const transform$1 = transformValue(() => {
134
+ const isInRange2 = inRange.get();
135
+ const isBelowRange = belowRange.get();
136
+ const isAboveRange = aboveRange.get();
137
+ if (isInRange2) return "none";
138
+ if (isBelowRange) return `translate(0px, ${(_internal.start - 1) * 100}vh)`;
139
+ if (isAboveRange) return `translate(0px, ${(_internal.end - 1) * 100}vh)`;
140
+ });
141
+ const animations = motionValue([]);
142
+ const styles = transformValue(() => ({
143
+ position: position.get(),
144
+ transform: transform$1.get()
145
+ }));
163
146
  _internal.root.style.top = "0px";
164
147
  _internal.root.style.right = "0px";
165
148
  _internal.root.style.bottom = "0px";
@@ -170,315 +153,51 @@ function initializeParallaxStickyLayerContext(props) {
170
153
  if (window.getComputedStyle(_internal.root).display === "inline") {
171
154
  _internal.root.style.display = "block";
172
155
  }
173
- function updateStyles() {
174
- if (!stickySupported) {
175
- const normStart = normStartValue.get();
176
- const normEnd = normEndValue.get();
177
- const raw = rawScrollProgress.get();
178
- const span = Math.max(normEnd - normStart, Number.EPSILON);
179
- const t = Math.min(Math.max((raw - normStart) / span, 0), 1);
180
- const pageY = _internal.start - 1 + t * (_internal.end - _internal.start);
181
- _internal.root.style.position = "absolute";
182
- _internal.root.style.transform = `translate(0px, ${pageY * 100}vh)`;
183
- return;
184
- }
185
- const currentMode = mode.get();
186
- const offset = handoffOffsetY.get();
187
- const hasOffset = Math.abs(offset) >= 0.1;
188
- if (currentMode === "sticky") {
189
- _internal.root.style.position = "sticky";
190
- _internal.root.style.transform = hasOffset ? `translate(0px, ${offset}px)` : "none";
191
- return;
192
- }
193
- const baseTransform = currentMode === "absolute-below" ? `translate(0px, ${(_internal.start - 1) * 100}vh)` : `translate(0px, ${(_internal.end - 1) * 100}vh)`;
194
- _internal.root.style.position = "absolute";
195
- _internal.root.style.transform = hasOffset ? `${baseTransform} translate(0px, ${offset}px)` : baseTransform;
196
- }
197
- function renderStyles() {
198
- frame.render(updateStyles);
199
- }
200
- function stopHandoffAnimation() {
201
- handoffAnimation?.stop();
202
- handoffAnimation = null;
203
- handoffOffsetY.set(0);
204
- }
205
- function clearExitWaitHandles() {
206
- if (exitWaitRaf !== null) {
207
- window.cancelAnimationFrame(exitWaitRaf);
208
- exitWaitRaf = null;
209
- }
210
- if (exitWaitTimeout !== null) {
211
- window.clearTimeout(exitWaitTimeout);
212
- exitWaitTimeout = null;
213
- }
214
- }
215
- function pruneFinishedAnimations() {
216
- for (const animation of activeContentAnimations) {
217
- if (animation.state !== "running") {
218
- activeContentAnimations.delete(animation);
219
- }
220
- }
221
- }
222
- function hasActiveContentAnimations() {
223
- pruneFinishedAnimations();
224
- return activeContentAnimations.size > 0;
225
- }
226
- function registerContentAnimation(animation) {
227
- activeContentAnimations.add(animation);
228
- animation.then(
229
- () => {
230
- activeContentAnimations.delete(animation);
231
- },
232
- () => {
233
- activeContentAnimations.delete(animation);
234
- }
235
- );
236
- }
237
- function getHandoffDuration(deltaY, velocity) {
238
- const speed = Math.abs(velocity);
239
- if (speed <= LOW_VELOCITY_NO_TWEEN) return 0;
240
- const byDistance = Math.min(0.12, Math.max(0.05, Math.abs(deltaY) / 1400));
241
- const speedScale = Math.min(speed / 0.01, 1);
242
- return Math.max(0.04, byDistance - speedScale * 0.02);
243
- }
244
- function settleHandoffOverFrames(deltaY, frameCount) {
245
- handoffOffsetY.set(deltaY);
246
- updateStyles();
247
- let remainingFrames = Math.max(frameCount, 1);
248
- const settle = () => {
249
- remainingFrames -= 1;
250
- if (remainingFrames <= 0) {
251
- handoffOffsetY.set(0);
252
- updateStyles();
253
- return;
254
- }
255
- frame.render(settle);
256
- };
257
- frame.render(settle);
258
- }
259
- function cancelPendingExit() {
260
- pendingExitVersion += 1;
261
- pendingExitMode = null;
262
- clearExitWaitHandles();
263
- }
264
- function commitModeImmediately(nextMode) {
265
- stopHandoffAnimation();
266
- mode.set(nextMode);
267
- updateStyles();
268
- }
269
- function waitForAnimations(nextMode) {
270
- if (pendingExitMode === nextMode && (exitWaitRaf !== null || exitWaitTimeout !== null)) {
271
- return;
272
- }
273
- cancelPendingExit();
274
- pendingExitMode = nextMode;
275
- const version = pendingExitVersion;
276
- const completeExit = () => {
277
- if (version !== pendingExitVersion) return;
278
- clearExitWaitHandles();
279
- pendingExitMode = null;
280
- const latest = Math.min(Math.max(rawScrollProgress.get(), 0), 1);
281
- const stableMode = getTargetMode(
282
- latest,
283
- normStartValue.get(),
284
- normEndValue.get()
285
- );
286
- if (stableMode !== nextMode) return;
287
- commitModeImmediately(nextMode);
288
- };
289
- if (!hasActiveContentAnimations()) {
290
- completeExit();
291
- return;
292
- }
293
- const tick = () => {
294
- if (version !== pendingExitVersion) return;
295
- if (!hasActiveContentAnimations()) {
296
- completeExit();
297
- return;
298
- }
299
- exitWaitRaf = window.requestAnimationFrame(tick);
300
- };
301
- exitWaitTimeout = window.setTimeout(() => {
302
- completeExit();
303
- }, EXIT_WAIT_TIMEOUT_MS);
304
- exitWaitRaf = window.requestAnimationFrame(tick);
305
- }
306
- function getTargetMode(value, normStart, normEnd) {
307
- if (!stickySupported) {
308
- if (value <= normStart) return "absolute-below";
309
- if (value >= normEnd) return "absolute-above";
310
- return "absolute-below";
311
- }
312
- const current = mode.get();
313
- if (current === "sticky") {
314
- if (value < normStart - EXIT_HYSTERESIS) return "absolute-below";
315
- if (value > normEnd + EXIT_HYSTERESIS) return "absolute-above";
316
- return "sticky";
317
- }
318
- if (current === "absolute-below") {
319
- if (value >= normStart + ENTER_HYSTERESIS) return "sticky";
320
- return "absolute-below";
321
- }
322
- if (value <= normEnd - ENTER_HYSTERESIS) return "sticky";
323
- return "absolute-above";
324
- }
325
- function getModeDecisionValue(value, normStart, normEnd, allowPrediction) {
326
- const now = performance.now();
327
- let velocity = 0;
328
- if (hasScrollSample) {
329
- const elapsed = Math.max(now - lastScrollSampleTime, 1);
330
- velocity = (value - lastScrollSampleValue) / elapsed;
331
- }
332
- lastScrollSampleValue = value;
333
- lastScrollSampleTime = now;
334
- hasScrollSample = true;
335
- latestScrollVelocity = velocity;
336
- if (!allowPrediction || Math.abs(velocity) < PREDICT_MIN_VELOCITY) {
337
- return value;
338
- }
339
- const speed = Math.abs(velocity);
340
- const speedScale = Math.min(speed / HIGH_VELOCITY_SKIP_HANDOFF, 1);
341
- const lookaheadMs = PREDICT_LOOKAHEAD_MS + speedScale * (MAX_PREDICT_LOOKAHEAD_MS - PREDICT_LOOKAHEAD_MS);
342
- const predicted = Math.min(Math.max(value + velocity * lookaheadMs, 0), 1);
343
- const currentMode = mode.get();
344
- if (currentMode === "absolute-below") {
345
- const entryEdge = normStart + ENTER_HYSTERESIS;
346
- const nearStart = Math.abs(value - entryEdge) <= PREDICT_ENTER_BAND;
347
- if (velocity > 0 && nearStart && value < entryEdge && predicted >= entryEdge) {
348
- return entryEdge;
349
- }
350
- }
351
- if (currentMode === "absolute-above") {
352
- const entryEdge = normEnd - ENTER_HYSTERESIS;
353
- const nearEnd = Math.abs(value - entryEdge) <= PREDICT_ENTER_BAND;
354
- if (velocity < 0 && nearEnd && value > entryEdge && predicted <= entryEdge) {
355
- return entryEdge;
356
- }
357
- }
358
- if (currentMode === "sticky") {
359
- const exitTopEdge = normStart - EXIT_HYSTERESIS;
360
- const exitBottomEdge = normEnd + EXIT_HYSTERESIS;
361
- const nearTop = Math.abs(value - exitTopEdge) <= PREDICT_EXIT_BAND;
362
- if (velocity < 0 && nearTop && value > exitTopEdge && predicted <= exitTopEdge) {
363
- return exitTopEdge;
364
- }
365
- const nearBottom = Math.abs(value - exitBottomEdge) <= PREDICT_EXIT_BAND;
366
- if (velocity > 0 && nearBottom && value < exitBottomEdge && predicted >= exitBottomEdge) {
367
- return exitBottomEdge;
368
- }
369
- }
370
- return value;
371
- }
372
- function applyModeWithHandoff(nextMode) {
373
- if (!stickySupported) {
374
- stopHandoffAnimation();
375
- mode.set(nextMode);
376
- renderStyles();
377
- return;
378
- }
379
- stopHandoffAnimation();
380
- const prevRect = _internal.root.getBoundingClientRect();
381
- mode.set(nextMode);
382
- updateStyles();
383
- const nextRect = _internal.root.getBoundingClientRect();
384
- const deltaY = prevRect.top - nextRect.top;
385
- if (Math.abs(deltaY) <= HANDOFF_MIN_DELTA_PX) {
386
- handoffOffsetY.set(0);
387
- renderStyles();
388
- return;
389
- }
390
- if (Math.abs(latestScrollVelocity) >= HIGH_VELOCITY_SKIP_HANDOFF || Math.abs(deltaY) >= MAX_HANDOFF_DELTA_PX) {
391
- settleHandoffOverFrames(deltaY, 2);
392
- return;
393
- }
394
- handoffOffsetY.set(deltaY);
395
- updateStyles();
396
- const duration = getHandoffDuration(deltaY, latestScrollVelocity);
397
- if (duration <= 0) {
398
- settleHandoffOverFrames(deltaY, 1);
399
- return;
400
- }
401
- handoffAnimation = animate([
402
- [handoffOffsetY, 0, { duration, ease: HANDOFF_EASE }]
403
- ]);
404
- handoffAnimation.then(() => {
405
- if (!handoffAnimation) return;
406
- handoffAnimation = null;
407
- handoffOffsetY.set(0);
408
- renderStyles();
409
- });
410
- }
411
- function syncFromScroll(value, allowHandoff) {
412
- const clamped = Math.min(Math.max(value, 0), 1);
413
- rawScrollProgress.set(clamped);
414
- const normStart = normStartValue.get();
415
- const normEnd = normEndValue.get();
416
- const range = [normStart, normEnd];
417
- const span = Math.max(normEnd - normStart, Number.EPSILON);
418
- const normalizedProgress = Math.min(
419
- Math.max((clamped - normStart) / span, 0),
420
- 1
421
- );
422
- inRange.set(isInRange({ range, value: clamped }));
423
- belowRange.set(clamped <= normStart);
424
- aboveRange.set(clamped >= normEnd);
425
- progress.set(normalizedProgress);
426
- const decisionValue = getModeDecisionValue(
427
- clamped,
428
- normStart,
429
- normEnd,
430
- allowHandoff
431
- );
432
- const nextMode = getTargetMode(decisionValue, normStart, normEnd);
433
- const currentMode = mode.get();
434
- if (currentMode === "sticky" && nextMode !== "sticky" && allowHandoff) {
435
- waitForAnimations(nextMode);
436
- updateStyles();
437
- return;
438
- }
439
- cancelPendingExit();
440
- if (nextMode === currentMode) {
441
- if (!allowHandoff) {
442
- stopHandoffAnimation();
443
- updateStyles();
444
- }
445
- return;
446
- }
447
- if (allowHandoff) {
448
- applyModeWithHandoff(nextMode);
449
- return;
450
- }
451
- commitModeImmediately(nextMode);
452
- }
453
156
  createEffect(() => {
454
157
  const range = [_internal.start, _internal.end];
455
158
  const maxPages = parallax.maxPages;
456
159
  const normalizedRange = normalizeTuple(range, maxPages);
457
160
  const [normStart, normEnd] = normalizedRange;
458
- normStartValue.set(normStart);
459
- normEndValue.set(normEnd);
460
- const initialValue = parallax.scrollYProgress.get();
461
- syncFromScroll(initialValue, false);
462
- renderStyles();
161
+ const transformer = transform(normalizedRange, [0, 1]);
463
162
  const unsubscribe = parallax.scrollYProgress.on("change", (value) => {
464
- syncFromScroll(value, true);
163
+ inRange.set(isInRange({ range: normalizedRange, value }));
164
+ belowRange.set(value <= normStart);
165
+ aboveRange.set(value >= normEnd);
166
+ progress.set(transformer(value));
465
167
  });
466
168
  return onCleanup(() => {
467
169
  unsubscribe();
468
170
  });
469
171
  });
470
- const unsubHandoffOffset = handoffOffsetY.on("change", (_) => {
172
+ function updateStyles() {
173
+ const s = styles.get();
174
+ _internal.root.style.position = s.position;
175
+ _internal.root.style.transform = s.transform;
176
+ }
177
+ function renderStyles() {
178
+ frame.render(updateStyles);
179
+ }
180
+ function waitForAnimations() {
181
+ const a = animations.get();
182
+ const controller = new AbortController();
183
+ abortablePromise(awaitAllAnimations(a), controller.signal).then(() => {
184
+ animations.set([]);
185
+ renderStyles();
186
+ });
187
+ }
188
+ const unsubStyles = styles.on("change", (_) => {
189
+ if (progress.get() === 1 || progress.get() === 0)
190
+ return waitForAnimations();
191
+ animations.set([]);
471
192
  renderStyles();
472
193
  });
473
194
  function _animate(...args) {
474
195
  const animation = animate(...args);
475
- registerContentAnimation(animation);
196
+ animations.set([...animations.get(), animation]);
476
197
  return animation;
477
198
  }
478
199
  onCleanup(() => {
479
- cancelPendingExit();
480
- stopHandoffAnimation();
481
- unsubHandoffOffset();
200
+ unsubStyles();
482
201
  });
483
202
  return [progress, { animate: _animate }];
484
203
  }
@@ -681,6 +400,7 @@ ${messages.join("\n")}`
681
400
  const active = activeProps();
682
401
  const { range, ...props2 } = active;
683
402
  const valueSubscribers = [];
403
+ let disposed = false;
684
404
  const progressValue = Object.keys(PROP_DEFAULTS).reduce(
685
405
  (values, key) => {
686
406
  const fallback = Reflect.get(PROP_DEFAULTS, key);
@@ -714,11 +434,16 @@ ${messages.join("\n")}`
714
434
  () => `translate(${animated.x?.get()}, ${animated.y?.get()}) scale(${animated.scale?.get()}) rotate(${animated.rotate?.get()}deg)`
715
435
  );
716
436
  function animateValues() {
437
+ if (disposed) return;
717
438
  const sequence = Object.keys(animated).map((key) => {
718
- let v = progressValue[key]?.get?.();
719
- if (typeof v === "undefined") return null;
439
+ const subject = animated[key];
440
+ const v = progressValue[key]?.get?.();
441
+ if (!subject) return null;
442
+ if (typeof v === "undefined" || v === null) return null;
443
+ if (typeof v !== "string" && typeof v !== "number") return null;
720
444
  return [animated[key], v, { at: "<" }];
721
445
  }).filter((v) => v !== null);
446
+ if (sequence.length === 0) return;
722
447
  animate2(sequence);
723
448
  }
724
449
  function scheduleAnimation() {
@@ -738,9 +463,12 @@ ${messages.join("\n")}`
738
463
  ...styledValues
739
464
  });
740
465
  return onCleanup(() => {
466
+ disposed = true;
741
467
  unstyle();
742
468
  unsub();
743
- valueSubscribers.forEach((unsub2) => unsub2());
469
+ valueSubscribers.forEach((unsub2) => {
470
+ unsub2();
471
+ });
744
472
  Object.values(animated).forEach((value) => {
745
473
  value.destroy();
746
474
  });