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/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); }; const findLargestDistance = (touches: TouchList) => { let pair = null as { t1: Touch, t2: Touch, distance: number}; for (let i = 0; i < touches.length; i++) { const t1 = touches[i]; for (let j = i + 1; j < touches.length; j++) { const t2 = touches[j]; const distance = Math.hypot(t1.pageX - t2.pageX, t1.pageY - t2.pageY); if (pair === null) { pair = { t1, t2, distance }; continue; } if (pair.distance < distance) { pair = { t1, t2, distance }; } } } return pair; }; export interface IZoom { anchorX: number; anchorY: number; x: number; y: number; scale: number; } export default class ZoomView extends AtomControl { @BindableProperty public zoom: IZoom; @BindableProperty public source: string; @BindableProperty private loading: boolean; private image: HTMLImageElement; private scrollDiv: HTMLDivElement; 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.setAttribute("data-zoom-view", "zoom-view"); 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.scrollDiv = this.element.firstElementChild as HTMLDivElement; this.imageContainer = this.scrollDiv.firstElementChild as HTMLDivElement; this.image = this.imageContainer 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); let previousDistance = undefined; touchMoveDisposable ??= this.bindEvent(scrollView, "touchmove", (ev: TouchEvent) => { let { x, y, anchorX, anchorY, scale } = this.zoom; if (ev.touches.length > 1) { ev.preventDefault(); ev.stopImmediatePropagation(); const rect = this.element.getBoundingClientRect(); const ld = findLargestDistance(ev.touches); const first = ld.t1; const second = ld.t2; const newScale = ld.distance; if (previousDistance === void 0) { previousDistance = newScale; return; } if (previousDistance === newScale) { return; } anchorX = ((first.clientX + second.clientX) / 2) - rect.left; anchorY = ((first.clientY + second.clientY) / 2) - rect.top; scale += newScale - previousDistance; previousDistance = newScale; this.updateZoom({ anchorX, anchorY, x, y, scale }); return; } }); 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 cp = { x: e.clientX, y: e.clientY }; const diffX = previous.x - cp.x; const diffY = previous.y - cp.y; previous = cp; this.scrollDiv.scrollBy(diffX, diffY); }); 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) => { 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 { clientWidth, clientHeight } = this.element; if (scale <= 0) { this.element.scrollTo(0,0); this.imageContainer.style.transform = ``; return; } const scaleFactor = (clientWidth + scale) / clientWidth; this.imageContainer.style.transformOrigin = `${anchorX}px ${anchorY}px`; this.imageContainer.style.transform = `scale(${scaleFactor})`; const e = this.imageContainer.getBoundingClientRect(); // console.log([e.left, e.top]); const left = -e.left; const top = -e.top; this.imageContainer.style.transformOrigin = "0 0"; const de = this.imageContainer.getBoundingClientRect(); // console.log([de.left, de.top]); this.scrollDiv.scrollTo({ left: left + de.left, top: top + de.top , behavior: "instant" }); } }