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.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" });
}
}