import Bind from "@web-atoms/core/dist/core/Bind"; import { BindableProperty } from "@web-atoms/core/dist/core/BindableProperty"; import XNode, { isTemplateSymbol } from "@web-atoms/core/dist/core/XNode"; import { AtomControl } from "@web-atoms/core/dist/web/controls/AtomControl"; import "./styles/pinch-zoom-view.global.css"; const center = (ev: TouchEvent) => { const touch = ev.touches[0]; if (touch) { return { x: touch.clientX, y: touch.clientY } } return { x: 0, y: 0 }; } const distance = (first: Touch, second: Touch) => { return Math.hypot(first.pageX - second.pageX, first.pageY - second.pageY); }; export interface IZoom { anchorX: number; anchorY: number; x: number; y: number; scale: number; } export default class PinchZoomView extends AtomControl { @BindableProperty public zoom: IZoom; @BindableProperty public source: string; @BindableProperty private loading: boolean; private image: HTMLImageElement; private imageContainer: HTMLDivElement; protected preCreate() { this.element.title = "Use mouse wheel to zoom"; this.loading = false; this.zoom = { scale: 0, anchorX: 0, anchorY: 0, x: 0, y: 0 }; this.element.dataset.pinchZoom = "true"; this.element.draggable = false; this.render(
this.getSource(this.source))} style-opacity={Bind.oneWay(() => this.loading ? "0.3" : "1")} event-load={() => { this.loading = false; this.updateZoom(this.zoom); }} />
this.loading ? "spinner fa-duotone fa-spinner fa-spin" : "hide")}/> this.updateZoom()} class={Bind.oneWay(() => this.zoom.scale ? "scale" : "hide")} title="Display entire image"/>
); this.imageContainer = this.element.firstElementChild as HTMLDivElement; this.image = this.imageContainer.firstElementChild as HTMLImageElement; const scrollView = this.element; let previous: {x: number, y: number}; let touchMoveDisposable; let touchEndDisposable; this.bindEvent(scrollView, "touchstart", (evs: TouchEvent) => { previous = center(evs); // evs.preventDefault(); // evs.stopImmediatePropagation?.(); // const start = this.zoom.scale; let previousDistance = undefined; touchMoveDisposable ??= this.bindEvent(scrollView, "touchmove", (ev: TouchEvent) => { let { x, y, anchorX, anchorY, scale } = this.zoom; if (ev.touches.length === 2) { ev.preventDefault(); ev.stopImmediatePropagation(); const rect = this.element.getBoundingClientRect(); const first = ev.touches[0]; const second = ev.touches[1]; anchorX = ((first.clientX + second.clientX) / 2) - rect.left; anchorY = ((first.clientY + second.clientY) / 2) - rect.top; const newScale = distance(first, second); if (previousDistance === void 0) { previousDistance = newScale; return; } if (previousDistance === newScale) { return; } scale += newScale - previousDistance; previousDistance = newScale; this.updateZoom({ anchorX, anchorY, x, y, scale }); return; } if (!previous) { return; } // enable panning... const cp = center(ev); x += (cp.x - previous.x); y += (cp.y - previous.y); previous = cp; this.updateZoom({ anchorX, anchorY, x, y, scale }); }); touchEndDisposable ??= this.bindEvent(scrollView, "touchend", (ev: TouchEvent) => { // ev.preventDefault(); // ev.stopImmediatePropagation?.(); touchMoveDisposable?.dispose(); touchEndDisposable?.dispose(); touchMoveDisposable = undefined; touchEndDisposable = undefined; previousDistance = undefined; }); }); let mouseMoveDisposable; let mouseUpDisposable; this.bindEvent(scrollView, "dragstart", (ev: DragEvent) => { ev.preventDefault(); ev.stopImmediatePropagation(); }); this.bindEvent(scrollView, "mousedown", (ev: MouseEvent) => { this.element.dataset.state = "grabbing"; previous = { x: ev.clientX, y: ev.clientY }; mouseMoveDisposable ??= this.bindEvent(scrollView, "mousemove", (e: MouseEvent) => { e.preventDefault(); e.stopImmediatePropagation?.(); const { anchorX, anchorY, scale } = this.zoom; let {x , y } = this.zoom; const cp = { x: e.clientX, y: e.clientY }; x += (cp.x - previous.x); y += (cp.y - previous.y); previous = cp; this.updateZoom({ anchorX, anchorY, x, y, scale }); }); mouseUpDisposable ??= this.bindEvent(scrollView, "mouseup", (e: MouseEvent) => { e.preventDefault(); e.stopImmediatePropagation?.(); this.element.dataset.state = ""; previous = null; mouseMoveDisposable.dispose(); mouseUpDisposable.dispose(); mouseMoveDisposable = undefined; mouseUpDisposable = undefined; }); }); this.bindEvent(scrollView, "wheel", (ev: WheelEvent) => { const target = ev.currentTarget; ev.preventDefault(); ev.stopImmediatePropagation?.(); const newScale = this.zoom.scale - (ev.deltaY < 0 ? -50 : 50); const anchorX = ev.offsetX; const anchorY = ev.offsetY; const { x, y } = this.zoom; this.updateZoom({ anchorX, anchorY, x, y, scale: newScale < 0 ? 0 : newScale }); }, undefined, { passive: false }); } private getSource(text: string) { if (text) { this.loading = true; } return text; } private updateZoom(zoom: IZoom = { x: 0, y: 0, anchorX: 0, anchorY: 0, scale: 0 }) { const { anchorX, anchorY, x, y } = zoom; let { scale } = zoom; // console.log(zoom); this.zoom = zoom; const image = this.image; if (!image.naturalHeight) { return; } const maxHeight = this.element.clientWidth > this.element.clientHeight; const s = maxHeight ? this.element.clientWidth / image.naturalWidth : this.element.clientHeight / image.naturalHeight ; if (scale <= 0) { scale = 0; } const newWidth = (this.element.clientWidth + scale) + "px"; const newHeight = (this.element.clientHeight + scale) + "px"; this.image.style.maxWidth = this.element.clientWidth + "px"; this.image.style.maxHeight = this.element.clientHeight + "px"; if (scale <= 0) { this.imageContainer.style.transform = ""; return; } const clientWidth = this.element.clientWidth; const scaleFactor = (clientWidth + scale) / clientWidth; this.imageContainer.style.transformOrigin = `${anchorX}px ${anchorY}`; this.imageContainer.style.transform = `translate(${x}px, ${y}px) scale(${scaleFactor})`; } }