| | |
| | | this.initModal(); |
| | | this.initListeners(); |
| | | this.initSubscribers(); |
| | | |
| | | console.log('Gallery loaded...'); |
| | | } |
| | | /********************************************************************* |
| | | ELEMENTS |
| | |
| | | closeMessage: 'Closed Gallery', |
| | | } |
| | | ); |
| | | this.modal.subscribe((event, data) => { |
| | | this.modal.subscribe((event) => { |
| | | if (event === 'modal-close') { |
| | | this.toggleGallery(false); |
| | | } |
| | |
| | | // PINCH (two pointers) |
| | | if (this.activePointers.size === 2) { |
| | | const pts = [...this.activePointers.values()]; |
| | | const dist = Math.hypot( |
| | | pts[0].x - pts[1].x, |
| | | pts[0].y - pts[1].y |
| | | ); |
| | | const dist = Math.hypot(pts[0].x - pts[1].x, pts[0].y - pts[1].y); |
| | | const increment = this.pinchStartScale * (dist / this.pinchStartDist) - this.zoom.scale; |
| | | |
| | | const newScale = this.pinchStartScale * (dist / this.pinchStartDist); |
| | | const increment = newScale - this.zoom.scale; |
| | | |
| | | // Zoom centered (NO midpoint) |
| | | this.handleZoom(increment); |
| | | |
| | | const midX = (pts[0].x + pts[1].x) / 2; // ← anchor to finger midpoint |
| | | const midY = (pts[0].y + pts[1].y) / 2; |
| | | this.handleZoom(increment, midX, midY); |
| | | return; |
| | | } |
| | | |
| | |
| | | } |
| | | |
| | | onPointerUp(e) { |
| | | const last = this.activePointers.get(e.pointerId); // grab before delete |
| | | this.activePointers.delete(e.pointerId); |
| | | |
| | | if (this.activePointers.size < 2) { |
| | | this.pinchStartDist = 0; |
| | | } |
| | | |
| | | // Only check for swipe if we weren't panning and no more active pointers |
| | | if (!this.zoom.panning && this.activePointers.size === 0) { |
| | | // End of tap or swipe - detect swipe |
| | | this.swipe.endX = e.clientX; |
| | | this.swipe.endY = e.clientY; |
| | | const dx = this.swipe.endX - this.swipe.startX; |
| | | const dy = this.swipe.endY - this.swipe.startY; |
| | | if (this.zoom.scale <= 1 && !this.zoom.panning && this.activePointers.size === 0) { |
| | | const endX = last?.x ?? e.clientX; // use tracked position, fall back to event |
| | | const endY = last?.y ?? e.clientY; |
| | | const dx = endX - this.swipe.startX; |
| | | const dy = endY - this.swipe.startY; |
| | | |
| | | if (Math.abs(dx) > this.zoom.threshold) { |
| | | if (dx > 0) { |
| | | console.log('Swipe right'); |
| | | this.prevElement(); |
| | | } else { |
| | | console.log('Swipe left'); |
| | | this.nextElement(); |
| | | } |
| | | if (Math.abs(dx) > this.zoom.threshold && Math.abs(dx) > Math.abs(dy)) { |
| | | dx > 0 ? this.prevElement() : this.nextElement(); |
| | | } |
| | | } |
| | | |
| | | // Reset panning state when all pointers are released |
| | | if (this.activePointers.size === 0) { |
| | | this.zoom.panning = false; |
| | | // Reset cursor based on zoom state |
| | | this.ui.gallery.image.style.cursor = this.zoom.scale > 1 ? 'grab' : 'default'; |
| | | } |
| | | } |
| | |
| | | this.handleZoom(increment, e.clientX, e.clientY); |
| | | } |
| | | |
| | | clampPan() { |
| | | const BORDER = 32; // 2rem |
| | | const wrap = this.ui.gallery.wrap; |
| | | if (!wrap) return; |
| | | |
| | | const wrapRect = wrap.getBoundingClientRect(); |
| | | |
| | | // MUST use natural dimensions |
| | | const naturalW = 1920; |
| | | const naturalH = 1920; |
| | | |
| | | // But they must be constrained by how the CSS fits them (max-width: 90vw, max-height: 85vh) |
| | | // So compute the base display size: |
| | | const displayRatio = Math.min( |
| | | wrapRect.width / naturalW, |
| | | wrapRect.height / naturalH |
| | | ); |
| | | |
| | | const baseWidth = naturalW * displayRatio; |
| | | const baseHeight = naturalH * displayRatio; |
| | | |
| | | const scaledWidth = baseWidth * this.zoom.scale; |
| | | const scaledHeight = baseHeight * this.zoom.scale; |
| | | |
| | | // Allowed pan range |
| | | const minX = wrapRect.width - scaledWidth - BORDER; |
| | | const maxX = BORDER; |
| | | |
| | | const minY = wrapRect.height - scaledHeight - BORDER; |
| | | const maxY = BORDER; |
| | | |
| | | // clamp |
| | | this.zoom.x = Math.min(maxX, Math.max(minX, this.zoom.x)); |
| | | this.zoom.y = Math.min(maxY, Math.max(minY, this.zoom.y)); |
| | | } |
| | | |
| | | |
| | | |
| | | |
| | | handleZoom(increment, clientX=null, clientY=null) { |
| | | const oldScale = this.zoom.scale; |
| | | let newScale = oldScale + increment; |
| | |
| | | } |
| | | |
| | | applyTransform() { |
| | | // this.clampPan(); |
| | | this.clampPan(); |
| | | const img = this.ui.gallery.image; |
| | | img.style.transform = `translate(${this.zoom.x}px, ${this.zoom.y}px) scale(${this.zoom.scale})`; |
| | | // Update cursor based on zoom level |
| | | img.style.cursor = this.zoom.scale > 1 ? 'grab' : 'default'; |
| | | } |
| | | clampPan() { |
| | | const img = this.ui.gallery.image; |
| | | const excessX = Math.max(0, (img.offsetWidth * this.zoom.scale - window.innerWidth) / 2); |
| | | const excessY = Math.max(0, (img.offsetHeight * this.zoom.scale - window.innerHeight) / 2); |
| | | this.zoom.x = Math.max(-excessX, Math.min(excessX, this.zoom.x)); |
| | | this.zoom.y = Math.max(-excessY, Math.min(excessY, this.zoom.y)); |
| | | } |
| | | resetZoom() { |
| | | this.zoom.scale = 1; |
| | | this.zoom.x = 0; |
| | |
| | | /** |
| | | * |
| | | * @param {boolean} open |
| | | * @param {null|number} index |
| | | */ |
| | | toggleGallery(open, index= null) { |
| | | toggleGallery(open) { |
| | | if (open) { |
| | | // Disable native image dragging |
| | | this.ui.gallery.image.draggable = false; |