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})`;
}
}