feat(image-modal): enhance swipe-to-dismiss functionality with settling animation

This commit is contained in:
2026-09-02 14:45:36 -03:00
parent 7d52e11bca
commit 1d54cb1c48
4 changed files with 61 additions and 3 deletions

View File

@@ -35,10 +35,16 @@ export class ImageModalComponent {
protected readonly offsetX = signal(0);
protected readonly offsetY = signal(0);
protected readonly imageFailed = signal(false);
protected readonly swipeOffsetY = signal(0);
protected readonly swipeSettling = signal(false);
protected readonly transform = computed(
() => `translate3d(${this.offsetX()}px, ${this.offsetY()}px, 0) scale(${this.zoom()})`,
);
protected readonly zoomLabel = computed(() => `${Math.round(this.zoom() * 100)}%`);
protected readonly swipeTransform = computed(() => `translate3d(0, ${this.swipeOffsetY()}px, 0)`);
protected readonly swipeOpacity = computed(() =>
Math.max(0.65, 1 - (this.swipeOffsetY() / 400) * 0.35),
);
private dragStart: Point | null = null;
private dragOffset: Point = { x: 0, y: 0 };
@@ -88,6 +94,7 @@ export class ImageModalComponent {
this.gestureMoved = false;
if (this.pointers.size === 1) {
this.swipeSettling.set(false);
this.dragStart = point;
this.dragOffset = { x: this.offsetX(), y: this.offsetY() };
this.swipeStart =
@@ -95,6 +102,7 @@ export class ImageModalComponent {
} else if (this.pointers.size === 2) {
this.hadMultiplePointers = true;
this.swipeStart = null;
this.swipeOffsetY.set(0);
this.beginPinch();
}
}
@@ -125,6 +133,13 @@ export class ImageModalComponent {
return;
}
if (this.pointers.size === 1 && this.swipeStart) {
const deltaX = Math.abs(point.x - this.swipeStart.x);
const deltaY = point.y - this.swipeStart.y;
this.swipeOffsetY.set(deltaY > 0 && deltaY >= deltaX ? deltaY : 0);
return;
}
if (this.pointers.size === 1 && this.dragStart && this.zoom() > this.data.minZoom) {
this.offsetX.set(this.dragOffset.x + point.x - this.dragStart.x);
this.offsetY.set(this.dragOffset.y + point.y - this.dragStart.y);
@@ -137,7 +152,10 @@ export class ImageModalComponent {
const trackedPoint = this.pointers.get(event.pointerId);
const endPoint = trackedPoint ? { x: event.clientX, y: event.clientY } : null;
const shouldDismiss =
wasTouch && endPoint !== null && this.isSwipeDown(this.swipeStart, endPoint);
event.type === 'pointerup' &&
wasTouch &&
endPoint !== null &&
this.isSwipeDown(this.swipeStart, endPoint);
this.pointers.delete(event.pointerId);
const viewport = this.viewport().nativeElement;
if (viewport.hasPointerCapture?.(event.pointerId)) {
@@ -169,6 +187,10 @@ export class ImageModalComponent {
this.pointerDownAt = null;
this.hadMultiplePointers = false;
this.swipeStart = null;
if (this.swipeOffsetY() > 0) {
this.swipeSettling.set(true);
this.swipeOffsetY.set(0);
}
this.clampOffset();
}
}