wip
parent
423d98cf39
commit
ea1ef3df13
|
|
@ -21,6 +21,11 @@ export interface SwipeFeedbackOptions {
|
|||
imageElement?: HTMLImageElement | HTMLVideoElement | null;
|
||||
}
|
||||
|
||||
interface SwipeAnimations {
|
||||
currentImageAnimation: Animation;
|
||||
previewAnimation: Animation | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Action that provides visual feedback for horizontal swipe gestures.
|
||||
* Allows the user to drag an element left or right (horizontal only),
|
||||
|
|
@ -28,6 +33,15 @@ export interface SwipeFeedbackOptions {
|
|||
* Optionally shows preview images on the left/right during swipe.
|
||||
*/
|
||||
export const swipeFeedback = (node: HTMLElement, options?: SwipeFeedbackOptions) => {
|
||||
// Animation configuration
|
||||
const ANIMATION_DURATION_MS = 300;
|
||||
// Drag sensitivity: pixels needed to reach 100% animation progress
|
||||
// Higher value = less sensitive (need to drag further)
|
||||
// Lower value = more sensitive (animation advances quickly)
|
||||
const DRAG_DISTANCE_FOR_FULL_ANIMATION = 400;
|
||||
// Enable/disable scaling effect during animation
|
||||
const ENABLE_SCALE_ANIMATION = false;
|
||||
|
||||
// Find the image element to apply custom transforms
|
||||
let imgElement: HTMLImageElement | HTMLVideoElement | null =
|
||||
options?.imageElement ?? node.querySelector('img') ?? node.querySelector('video');
|
||||
|
|
@ -40,10 +54,22 @@ export const swipeFeedback = (node: HTMLElement, options?: SwipeFeedbackOptions)
|
|||
let dragStartTime: Date | null = null;
|
||||
let swipeAmount = 0;
|
||||
|
||||
// Web Animations API - bidirectional animations
|
||||
let leftAnimations: SwipeAnimations | null = null;
|
||||
let rightAnimations: SwipeAnimations | null = null;
|
||||
|
||||
// Set initial cursor
|
||||
node.style.cursor = 'grab';
|
||||
|
||||
const resetPreviewContainers = () => {
|
||||
// Cancel any active animations
|
||||
leftAnimations?.currentImageAnimation?.cancel();
|
||||
leftAnimations?.previewAnimation?.cancel();
|
||||
rightAnimations?.currentImageAnimation?.cancel();
|
||||
rightAnimations?.previewAnimation?.cancel();
|
||||
leftAnimations = null;
|
||||
rightAnimations = null;
|
||||
|
||||
// Reset transforms and opacity
|
||||
if (leftPreviewContainer) {
|
||||
leftPreviewContainer.style.transform = '';
|
||||
|
|
@ -66,6 +92,94 @@ export const swipeFeedback = (node: HTMLElement, options?: SwipeFeedbackOptions)
|
|||
currentOffsetX = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates Web Animations API animations for swipe transitions.
|
||||
* Matches the keyframes from Month.svelte view transitions.
|
||||
* @param direction - 'left' for next (swipe left), 'right' for previous (swipe right)
|
||||
*/
|
||||
const createSwipeAnimations = (direction: 'left' | 'right'): SwipeAnimations | null => {
|
||||
if (!imgElement) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const duration = ANIMATION_DURATION_MS;
|
||||
const easing = 'cubic-bezier(0.33, 1, 0.68, 1)'; // Match Month.svelte:156
|
||||
|
||||
// Set transform origin to center for proper scaling
|
||||
imgElement.style.transformOrigin = 'center';
|
||||
|
||||
// Helper to build transform string with optional scale
|
||||
const scale = (s: number) => (ENABLE_SCALE_ANIMATION ? ` scale(${s})` : '');
|
||||
|
||||
// Animation for current image flying out
|
||||
// Note: Delayed opacity fade (stays at 1 until 20%, fades 20-80%) for tighter crossfade
|
||||
const currentImageAnimation = imgElement.animate(
|
||||
direction === 'left'
|
||||
? [
|
||||
// flyOutLeft - Month.svelte:280-289
|
||||
{ transform: `translateX(0)${scale(1)}`, opacity: '1', offset: 0 },
|
||||
{ transform: `translateX(-20vw)${scale(0.8)}`, opacity: '1', offset: 0.2 },
|
||||
{ transform: `translateX(-50vw)${scale(0.5)}`, opacity: '0.5', offset: 0.5 },
|
||||
{ transform: `translateX(-80vw)${scale(0.2)}`, opacity: '0', offset: 0.8 },
|
||||
{ transform: `translateX(-100vw)${scale(0)}`, opacity: '0', offset: 1 },
|
||||
]
|
||||
: [
|
||||
// flyOutRight - Month.svelte:303-312
|
||||
{ transform: `translateX(0)${scale(1)}`, opacity: '1', offset: 0 },
|
||||
{ transform: `translateX(20vw)${scale(0.8)}`, opacity: '1', offset: 0.2 },
|
||||
{ transform: `translateX(50vw)${scale(0.5)}`, opacity: '0.5', offset: 0.5 },
|
||||
{ transform: `translateX(80vw)${scale(0.2)}`, opacity: '0', offset: 0.8 },
|
||||
{ transform: `translateX(100vw)${scale(0)}`, opacity: '0', offset: 1 },
|
||||
],
|
||||
{
|
||||
duration,
|
||||
easing,
|
||||
fill: 'both',
|
||||
},
|
||||
);
|
||||
|
||||
// Animation for preview image flying in
|
||||
const previewContainer = direction === 'left' ? rightPreviewContainer : leftPreviewContainer;
|
||||
let previewAnimation: Animation | null = null;
|
||||
|
||||
if (previewContainer) {
|
||||
// Set transform origin to center for proper scaling
|
||||
previewContainer.style.transformOrigin = 'center';
|
||||
|
||||
previewAnimation = previewContainer.animate(
|
||||
direction === 'left'
|
||||
? [
|
||||
// flyInRight - Month.svelte:291-300
|
||||
// Note: Early opacity fade (starts at 0, fades 20-80%, stays at 1 after 80%)
|
||||
{ transform: `translateX(100vw)${scale(0)}`, opacity: '0', offset: 0 },
|
||||
{ transform: `translateX(80vw)${scale(0.2)}`, opacity: '0', offset: 0.2 },
|
||||
{ transform: `translateX(50vw)${scale(0.5)}`, opacity: '0.5', offset: 0.5 },
|
||||
{ transform: `translateX(20vw)${scale(0.8)}`, opacity: '1', offset: 0.8 },
|
||||
{ transform: `translateX(0)${scale(1)}`, opacity: '1', offset: 1 },
|
||||
]
|
||||
: [
|
||||
// flyInLeft - Month.svelte:269-278
|
||||
{ transform: `translateX(-100vw)${scale(0)}`, opacity: '0', offset: 0 },
|
||||
{ transform: `translateX(-80vw)${scale(0.2)}`, opacity: '0', offset: 0.2 },
|
||||
{ transform: `translateX(-50vw)${scale(0.5)}`, opacity: '0.5', offset: 0.5 },
|
||||
{ transform: `translateX(-20vw)${scale(0.8)}`, opacity: '1', offset: 0.8 },
|
||||
{ transform: `translateX(0)${scale(1)}`, opacity: '1', offset: 1 },
|
||||
],
|
||||
{
|
||||
duration,
|
||||
easing,
|
||||
fill: 'both',
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Pause both animations immediately - we'll control them manually via currentTime
|
||||
currentImageAnimation.pause();
|
||||
previewAnimation?.pause();
|
||||
|
||||
return { currentImageAnimation, previewAnimation };
|
||||
};
|
||||
|
||||
// Create preview image containers
|
||||
let leftPreviewContainer: HTMLDivElement | null = null;
|
||||
let rightPreviewContainer: HTMLDivElement | null = null;
|
||||
|
|
@ -121,34 +235,23 @@ export const swipeFeedback = (node: HTMLElement, options?: SwipeFeedbackOptions)
|
|||
const viewportWidth = Number.parseFloat(parentComputedStyle.width);
|
||||
const viewportHeight = Number.parseFloat(parentComputedStyle.height);
|
||||
|
||||
// Preview containers should be full viewport size
|
||||
// Preview containers should be full viewport size, positioned at 0,0
|
||||
// The animations will handle the translateX positioning
|
||||
if (leftPreviewContainer) {
|
||||
leftPreviewContainer.style.width = `${viewportWidth}px`;
|
||||
leftPreviewContainer.style.height = `${viewportHeight}px`;
|
||||
leftPreviewContainer.style.left = `${-viewportWidth}px`;
|
||||
leftPreviewContainer.style.left = `0px`;
|
||||
leftPreviewContainer.style.top = `0px`;
|
||||
}
|
||||
|
||||
if (rightPreviewContainer) {
|
||||
rightPreviewContainer.style.width = `${viewportWidth}px`;
|
||||
rightPreviewContainer.style.height = `${viewportHeight}px`;
|
||||
rightPreviewContainer.style.left = `${viewportWidth}px`;
|
||||
rightPreviewContainer.style.left = `0px`;
|
||||
rightPreviewContainer.style.top = `0px`;
|
||||
}
|
||||
};
|
||||
|
||||
const updatePreviewVisibility = () => {
|
||||
// Show left preview when swiping right (offsetX > 0)
|
||||
if (leftPreviewContainer) {
|
||||
leftPreviewContainer.style.display = currentOffsetX > 0 ? 'block' : 'none';
|
||||
}
|
||||
|
||||
// Show right preview when swiping left (offsetX < 0)
|
||||
if (rightPreviewContainer) {
|
||||
rightPreviewContainer.style.display = currentOffsetX < 0 ? 'block' : 'none';
|
||||
}
|
||||
};
|
||||
|
||||
const pointerDown = (event: PointerEvent) => {
|
||||
if (options?.disabled || !imgElement) {
|
||||
return;
|
||||
|
|
@ -168,8 +271,14 @@ export const swipeFeedback = (node: HTMLElement, options?: SwipeFeedbackOptions)
|
|||
// Also add document listeners as fallback
|
||||
document.addEventListener('pointerup', pointerUp);
|
||||
document.addEventListener('pointercancel', pointerUp);
|
||||
|
||||
// Ensure preview containers are created and positioned
|
||||
ensurePreviewsCreated();
|
||||
updatePreviewPositions();
|
||||
|
||||
// Note: We don't create animations here - they're lazy-created in pointerMove
|
||||
// when we know which direction the user is swiping
|
||||
|
||||
event.preventDefault();
|
||||
}
|
||||
};
|
||||
|
|
@ -181,24 +290,84 @@ export const swipeFeedback = (node: HTMLElement, options?: SwipeFeedbackOptions)
|
|||
|
||||
if (isDragging) {
|
||||
currentOffsetX = event.clientX - startX;
|
||||
swipeAmount = currentOffsetX;
|
||||
|
||||
const xDelta = event.clientX - startX;
|
||||
swipeAmount = xDelta;
|
||||
// Determine which direction we're swiping
|
||||
const isSwipingLeft = currentOffsetX < 0;
|
||||
const isSwipingRight = currentOffsetX > 0;
|
||||
|
||||
// Apply transform directly to the image element
|
||||
// Only translate horizontally (no vertical movement)
|
||||
imgElement.style.transform = `translate(${currentOffsetX}px, 0px)`;
|
||||
|
||||
// Apply same transform to preview containers so they move with the swipe
|
||||
if (leftPreviewContainer) {
|
||||
leftPreviewContainer.style.transform = `translate(${currentOffsetX}px, 0px)`;
|
||||
}
|
||||
if (rightPreviewContainer) {
|
||||
rightPreviewContainer.style.transform = `translate(${currentOffsetX}px, 0px)`;
|
||||
// Lazy create animations when first needed
|
||||
if (isSwipingLeft && !leftAnimations) {
|
||||
leftAnimations = createSwipeAnimations('left');
|
||||
// Ensure the right preview container is visible
|
||||
if (rightPreviewContainer) {
|
||||
rightPreviewContainer.style.display = 'block';
|
||||
rightPreviewContainer.style.zIndex = '1';
|
||||
}
|
||||
} else if (isSwipingRight && !rightAnimations) {
|
||||
rightAnimations = createSwipeAnimations('right');
|
||||
// Ensure the left preview container is visible
|
||||
if (leftPreviewContainer) {
|
||||
leftPreviewContainer.style.display = 'block';
|
||||
leftPreviewContainer.style.zIndex = '1';
|
||||
}
|
||||
}
|
||||
|
||||
// Update preview visibility
|
||||
updatePreviewVisibility();
|
||||
// Calculate progress based on absolute drag distance
|
||||
// Using a threshold distance to map to full animation (0-1)
|
||||
const progress = Math.min(Math.abs(currentOffsetX) / DRAG_DISTANCE_FOR_FULL_ANIMATION, 1);
|
||||
|
||||
// Map progress to animation time
|
||||
const animationTime = progress * ANIMATION_DURATION_MS;
|
||||
console.log(
|
||||
`Animation progress: ${(progress * 100).toFixed(1)}% | Time: ${animationTime.toFixed(1)}ms | Offset: ${currentOffsetX.toFixed(1)}px`,
|
||||
);
|
||||
|
||||
if (isSwipingLeft && leftAnimations) {
|
||||
// Ensure the right preview container is visible
|
||||
if (rightPreviewContainer) {
|
||||
rightPreviewContainer.style.display = 'block';
|
||||
rightPreviewContainer.style.zIndex = '1';
|
||||
}
|
||||
// Scrub left animations forward
|
||||
leftAnimations.currentImageAnimation.currentTime = animationTime;
|
||||
if (leftAnimations.previewAnimation) {
|
||||
leftAnimations.previewAnimation.currentTime = animationTime;
|
||||
}
|
||||
// Cancel and recreate right animations to prevent conflicts on imgElement
|
||||
if (rightAnimations) {
|
||||
rightAnimations.currentImageAnimation.cancel();
|
||||
if (rightAnimations.previewAnimation) {
|
||||
rightAnimations.previewAnimation.cancel();
|
||||
}
|
||||
rightAnimations = null;
|
||||
if (leftPreviewContainer) {
|
||||
leftPreviewContainer.style.display = 'none';
|
||||
}
|
||||
}
|
||||
} else if (isSwipingRight && rightAnimations) {
|
||||
// Ensure the left preview container is visible
|
||||
if (leftPreviewContainer) {
|
||||
leftPreviewContainer.style.display = 'block';
|
||||
leftPreviewContainer.style.zIndex = '1';
|
||||
}
|
||||
// Scrub right animations forward
|
||||
rightAnimations.currentImageAnimation.currentTime = animationTime;
|
||||
if (rightAnimations.previewAnimation) {
|
||||
rightAnimations.previewAnimation.currentTime = animationTime;
|
||||
}
|
||||
// Cancel and recreate left animations to prevent conflicts on imgElement
|
||||
if (leftAnimations) {
|
||||
leftAnimations.currentImageAnimation.cancel();
|
||||
if (leftAnimations.previewAnimation) {
|
||||
leftAnimations.previewAnimation.cancel();
|
||||
}
|
||||
leftAnimations = null;
|
||||
if (rightPreviewContainer) {
|
||||
rightPreviewContainer.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
// Notify about swipe movement
|
||||
options?.onSwipeMove?.(currentOffsetX);
|
||||
event.preventDefault();
|
||||
|
|
@ -210,40 +379,38 @@ export const swipeFeedback = (node: HTMLElement, options?: SwipeFeedbackOptions)
|
|||
return;
|
||||
}
|
||||
|
||||
// Add smooth transition
|
||||
const transitionStyle = 'transform 0.3s ease-out';
|
||||
imgElement.style.transition = transitionStyle;
|
||||
if (leftPreviewContainer) {
|
||||
leftPreviewContainer.style.transition = transitionStyle;
|
||||
}
|
||||
if (rightPreviewContainer) {
|
||||
rightPreviewContainer.style.transition = transitionStyle;
|
||||
}
|
||||
// Determine which animations are active
|
||||
const activeAnimations = currentOffsetX < 0 ? leftAnimations : rightAnimations;
|
||||
const activePreviewContainer = currentOffsetX < 0 ? rightPreviewContainer : leftPreviewContainer;
|
||||
|
||||
// Reset transforms
|
||||
imgElement.style.transform = 'translate(0px, 0px)';
|
||||
if (leftPreviewContainer) {
|
||||
leftPreviewContainer.style.transform = 'translate(0px, 0px)';
|
||||
}
|
||||
if (rightPreviewContainer) {
|
||||
rightPreviewContainer.style.transform = 'translate(0px, 0px)';
|
||||
}
|
||||
if (activeAnimations) {
|
||||
// Reverse the animation back to 0
|
||||
activeAnimations.currentImageAnimation.playbackRate = -1;
|
||||
if (activeAnimations.previewAnimation) {
|
||||
activeAnimations.previewAnimation.playbackRate = -1;
|
||||
}
|
||||
|
||||
// Remove transition after animation completes
|
||||
setTimeout(() => {
|
||||
if (imgElement) {
|
||||
imgElement.style.transition = '';
|
||||
}
|
||||
if (leftPreviewContainer) {
|
||||
leftPreviewContainer.style.transition = '';
|
||||
}
|
||||
if (rightPreviewContainer) {
|
||||
rightPreviewContainer.style.transition = '';
|
||||
}
|
||||
}, 300);
|
||||
// Play from current position back to start
|
||||
activeAnimations.currentImageAnimation.play();
|
||||
activeAnimations.previewAnimation?.play();
|
||||
|
||||
// Listen for finish event to clean up
|
||||
const handleFinish = () => {
|
||||
activeAnimations.currentImageAnimation.removeEventListener('finish', handleFinish);
|
||||
// Reset to original state
|
||||
activeAnimations.currentImageAnimation.cancel();
|
||||
activeAnimations.previewAnimation?.cancel();
|
||||
|
||||
// Hide the preview container after animation completes
|
||||
if (activePreviewContainer) {
|
||||
activePreviewContainer.style.display = 'none';
|
||||
activePreviewContainer.style.zIndex = '-1';
|
||||
}
|
||||
};
|
||||
activeAnimations.currentImageAnimation.addEventListener('finish', handleFinish, { once: true });
|
||||
}
|
||||
|
||||
currentOffsetX = 0;
|
||||
updatePreviewVisibility();
|
||||
};
|
||||
|
||||
const completeTransition = (direction: 'left' | 'right') => {
|
||||
|
|
@ -255,75 +422,72 @@ export const swipeFeedback = (node: HTMLElement, options?: SwipeFeedbackOptions)
|
|||
const activePreviewImg = direction === 'right' ? leftPreviewImg : rightPreviewImg;
|
||||
const naturalWidth = activePreviewImg?.naturalWidth ?? 1;
|
||||
const naturalHeight = activePreviewImg?.naturalHeight ?? 1;
|
||||
console.log('nat', naturalWidth, naturalHeight);
|
||||
|
||||
// Call pre-commit callback BEFORE starting the animation
|
||||
// This allows the parent component to update state with the preview dimensions
|
||||
options?.onPreCommit?.(direction, naturalWidth, naturalHeight);
|
||||
|
||||
// Add smooth transition
|
||||
const transitionStyle = 'transform 0.3s ease-out';
|
||||
imgElement.style.transition = transitionStyle;
|
||||
if (leftPreviewContainer) {
|
||||
leftPreviewContainer.style.transition = transitionStyle;
|
||||
}
|
||||
if (rightPreviewContainer) {
|
||||
rightPreviewContainer.style.transition = transitionStyle;
|
||||
}
|
||||
// Get the active animations
|
||||
const activeAnimations = direction === 'left' ? leftAnimations : rightAnimations;
|
||||
|
||||
// Calculate the final offset to center the preview
|
||||
const parentElement = node.parentElement;
|
||||
if (!parentElement) {
|
||||
return;
|
||||
}
|
||||
const viewportWidth = Number.parseFloat(globalThis.getComputedStyle(parentElement).width);
|
||||
if (activeAnimations) {
|
||||
// Get current time before modifying animation
|
||||
const currentTime = Number(activeAnimations.currentImageAnimation.currentTime) || 0;
|
||||
console.log(`Committing transition from ${currentTime}ms / ${ANIMATION_DURATION_MS}ms`);
|
||||
|
||||
// Slide everything to complete the transition
|
||||
// If swiping right (direction='right'), slide everything right by viewport width
|
||||
// If swiping left (direction='left'), slide everything left by viewport width
|
||||
const finalOffset = direction === 'right' ? viewportWidth : -viewportWidth;
|
||||
// If animation is already at or near the end, skip to finish immediately
|
||||
if (currentTime >= ANIMATION_DURATION_MS - 5) {
|
||||
console.log('Animation already complete, finishing immediately');
|
||||
|
||||
// Listen for transition end
|
||||
const handleTransitionEnd = () => {
|
||||
if (!imgElement) {
|
||||
// Keep the preview visible by hiding the main image but showing the preview
|
||||
imgElement.style.opacity = '0';
|
||||
|
||||
// Show the preview that's now in the center
|
||||
const activePreview = direction === 'right' ? leftPreviewContainer : rightPreviewContainer;
|
||||
|
||||
if (activePreview) {
|
||||
activePreview.style.zIndex = '1'; // Bring to front
|
||||
}
|
||||
|
||||
// Trigger navigation (dimensions were already passed in onPreCommit)
|
||||
options?.onSwipeCommit?.(direction);
|
||||
return;
|
||||
}
|
||||
|
||||
imgElement.removeEventListener('transitionend', handleTransitionEnd);
|
||||
|
||||
// Keep the preview visible by hiding the main image but showing the preview
|
||||
// The preview is now centered, and we want it to stay visible while the new component loads
|
||||
imgElement.style.opacity = '0';
|
||||
|
||||
// Show the preview that's now in the center
|
||||
const activePreview = direction === 'right' ? leftPreviewContainer : rightPreviewContainer;
|
||||
|
||||
if (activePreview) {
|
||||
activePreview.style.zIndex = '1'; // Bring to front
|
||||
// Ensure playback rate is forward (in case it was reversed)
|
||||
activeAnimations.currentImageAnimation.playbackRate = 1;
|
||||
if (activeAnimations.previewAnimation) {
|
||||
activeAnimations.previewAnimation.playbackRate = 1;
|
||||
}
|
||||
|
||||
// Remove transitions
|
||||
imgElement.style.transition = '';
|
||||
if (leftPreviewContainer) {
|
||||
leftPreviewContainer.style.transition = '';
|
||||
}
|
||||
if (rightPreviewContainer) {
|
||||
rightPreviewContainer.style.transition = '';
|
||||
}
|
||||
// Play the animation to completion from current position
|
||||
activeAnimations.currentImageAnimation.play();
|
||||
activeAnimations.previewAnimation?.play();
|
||||
|
||||
// Trigger navigation (dimensions were already passed in onPreCommit)
|
||||
options?.onSwipeCommit?.(direction);
|
||||
};
|
||||
// Listen for animation finish
|
||||
const handleFinish = () => {
|
||||
if (!imgElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
imgElement.addEventListener('transitionend', handleTransitionEnd, { once: true });
|
||||
activeAnimations.currentImageAnimation.removeEventListener('finish', handleFinish);
|
||||
|
||||
// Apply the final transform to trigger animation
|
||||
imgElement.style.transform = `translate(${finalOffset}px, 0px)`;
|
||||
if (leftPreviewContainer) {
|
||||
leftPreviewContainer.style.transform = `translate(${finalOffset}px, 0px)`;
|
||||
}
|
||||
if (rightPreviewContainer) {
|
||||
rightPreviewContainer.style.transform = `translate(${finalOffset}px, 0px)`;
|
||||
// Keep the preview visible by hiding the main image but showing the preview
|
||||
// The preview is now centered, and we want it to stay visible while the new component loads
|
||||
imgElement.style.opacity = '0';
|
||||
|
||||
// Show the preview that's now in the center
|
||||
const activePreview = direction === 'right' ? leftPreviewContainer : rightPreviewContainer;
|
||||
|
||||
if (activePreview) {
|
||||
activePreview.style.zIndex = '1'; // Bring to front
|
||||
}
|
||||
|
||||
// Trigger navigation (dimensions were already passed in onPreCommit)
|
||||
options?.onSwipeCommit?.(direction);
|
||||
};
|
||||
|
||||
activeAnimations.currentImageAnimation.addEventListener('finish', handleFinish, { once: true });
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -346,16 +510,22 @@ export const swipeFeedback = (node: HTMLElement, options?: SwipeFeedbackOptions)
|
|||
const threshold = options?.swipeThreshold ?? 45;
|
||||
|
||||
const timeTaken = Date.now() - (dragStartTime?.getTime() ?? 0);
|
||||
|
||||
const velocity = Math.abs(swipeAmount) / timeTaken;
|
||||
console.log('velocity', velocity, swipeAmount);
|
||||
if (Math.abs(swipeAmount) < threshold || velocity < 0.11) {
|
||||
|
||||
// Calculate animation progress (same calculation as in pointerMove)
|
||||
const progress = Math.min(Math.abs(currentOffsetX) / DRAG_DISTANCE_FOR_FULL_ANIMATION, 1);
|
||||
|
||||
// Commit if EITHER:
|
||||
// 1. High velocity (fast swipe) OR
|
||||
// 2. Animation progress is over 25%
|
||||
const hasEnoughVelocity = velocity >= 0.11;
|
||||
const hasEnoughProgress = progress > 0.25;
|
||||
|
||||
if (Math.abs(swipeAmount) < threshold || (!hasEnoughVelocity && !hasEnoughProgress)) {
|
||||
resetPosition();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if swipe exceeded threshold
|
||||
|
||||
const commitDirection = currentOffsetX > 0 ? 'right' : 'left';
|
||||
|
||||
// Call onSwipeEnd callback
|
||||
|
|
@ -416,6 +586,12 @@ export const swipeFeedback = (node: HTMLElement, options?: SwipeFeedbackOptions)
|
|||
}
|
||||
},
|
||||
destroy() {
|
||||
// Cancel all animations
|
||||
leftAnimations?.currentImageAnimation?.cancel();
|
||||
leftAnimations?.previewAnimation?.cancel();
|
||||
rightAnimations?.currentImageAnimation?.cancel();
|
||||
rightAnimations?.previewAnimation?.cancel();
|
||||
|
||||
node.removeEventListener('pointerdown', pointerDown);
|
||||
node.removeEventListener('pointermove', pointerMove);
|
||||
node.removeEventListener('pointerup', pointerUp);
|
||||
|
|
|
|||
|
|
@ -209,28 +209,28 @@
|
|||
}
|
||||
|
||||
:global(::view-transition-old(next)) {
|
||||
animation: 500ms flyOutLeft forwards;
|
||||
animation: 250ms flyOutLeft forwards;
|
||||
transform-origin: center;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
:global(::view-transition-new(next)) {
|
||||
animation: 500ms flyInRight forwards;
|
||||
animation: 250ms flyInRight forwards;
|
||||
transform-origin: center;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
:global(::view-transition-old(previous)) {
|
||||
animation: 500ms flyOutRight forwards;
|
||||
animation: 250ms flyOutRight forwards;
|
||||
transform-origin: center;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
:global(::view-transition-new(previous)) {
|
||||
animation: 500ms flyInLeft forwards;
|
||||
animation: 250ms flyInLeft forwards;
|
||||
transform-origin: center;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
|
|
@ -250,7 +250,7 @@
|
|||
|
||||
:global(::view-transition-old(previous)),
|
||||
:global(::view-transition-old(next)) {
|
||||
animation: 500ms fadeOut forwards;
|
||||
animation: 250ms fadeOut forwards;
|
||||
transform-origin: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
|
@ -259,7 +259,7 @@
|
|||
|
||||
:global(::view-transition-new(previous)),
|
||||
:global(::view-transition-new(next)) {
|
||||
animation: 500ms fadeIn forwards;
|
||||
animation: 250ms fadeIn forwards;
|
||||
transform-origin: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
|
@ -268,33 +268,33 @@
|
|||
}
|
||||
@keyframes -global-flyInLeft {
|
||||
from {
|
||||
transform: translateX(-100vw) scale(0);
|
||||
transform: translateX(-100vw);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0) scale(1);
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes -global-flyOutLeft {
|
||||
from {
|
||||
transform: translateX(0) scale(1);
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
transform: translateX(-100vw) scale(0);
|
||||
transform: translateX(-100vw);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes -global-flyInRight {
|
||||
from {
|
||||
transform: translateX(100vw) scale(0);
|
||||
transform: translateX(100vw);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0) scale(1);
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -302,11 +302,11 @@
|
|||
/* Fly out to right */
|
||||
@keyframes -global-flyOutRight {
|
||||
from {
|
||||
transform: translateX(0) scale(1);
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
transform: translateX(100vw) scale(0);
|
||||
transform: translateX(100vw);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue