custom zoom pan pinch implementation
This commit is contained in:
parent
2e469e39a9
commit
d2c9d6eed9
|
@ -15018,7 +15018,15 @@
|
|||
},
|
||||
"packages/lib": {
|
||||
"name": "@sc07-canvas/lib",
|
||||
"version": "1.0.0"
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"eventemitter3": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"packages/lib/node_modules/eventemitter3": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
|
||||
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="
|
||||
},
|
||||
"packages/server": {
|
||||
"name": "@sc07-canvas/server",
|
||||
|
|
|
@ -20,7 +20,15 @@ gulp.task("js", function () {
|
|||
gulp.task(
|
||||
"watch",
|
||||
gulp.series("js", "css", function () {
|
||||
gulp.watch(["src/**/*.ts", "src/**/*.tsx"], gulp.series("js"));
|
||||
gulp.watch(
|
||||
[
|
||||
"src/**/*.ts",
|
||||
"src/**/*.tsx",
|
||||
"../lib/src/**/*.ts",
|
||||
"../lib/src/**/*.tsx",
|
||||
],
|
||||
gulp.series("js")
|
||||
);
|
||||
gulp.watch("src/**/*.scss", gulp.series("css"));
|
||||
})
|
||||
);
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
.board-wrapper {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.debug-point {
|
||||
position: fixed;
|
||||
z-index: 9999;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 5px;
|
||||
background-color: #ff0000;
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
touch-action: none;
|
||||
}
|
|
@ -8,33 +8,15 @@ import {
|
|||
import { Canvas } from "../lib/canvas";
|
||||
import { useAppContext } from "../contexts/AppContext";
|
||||
import throttle from "lodash.throttle";
|
||||
import { PanZoomWrapper } from "@sc07-canvas/lib/src/renderer";
|
||||
|
||||
export const CanvasWrapper = () => {
|
||||
// to prevent safari from blurring things, use the zoom css property
|
||||
return (
|
||||
<main>
|
||||
<TransformWrapper
|
||||
centerOnInit
|
||||
limitToBounds={false}
|
||||
centerZoomedOut={false}
|
||||
minScale={0.5}
|
||||
maxScale={50}
|
||||
wheel={{
|
||||
step: 0.05,
|
||||
smoothStep: 0.05,
|
||||
}}
|
||||
initialScale={5}
|
||||
panning={{
|
||||
velocityDisabled: true,
|
||||
}}
|
||||
doubleClick={{
|
||||
disabled: true,
|
||||
}}
|
||||
>
|
||||
<TransformComponent wrapperStyle={{ width: "100%", height: "100%" }}>
|
||||
<PanZoomWrapper>
|
||||
<CanvasInner />
|
||||
</TransformComponent>
|
||||
</TransformWrapper>
|
||||
</PanZoomWrapper>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
@ -42,28 +24,28 @@ export const CanvasWrapper = () => {
|
|||
const CanvasInner = () => {
|
||||
const canvasRef = createRef<HTMLCanvasElement>();
|
||||
const { config } = useAppContext();
|
||||
const { centerView } = useControls();
|
||||
// const { centerView } = useControls();
|
||||
|
||||
useTransformEffect(
|
||||
throttle(({ state, instance }) => {
|
||||
const params = new URLSearchParams();
|
||||
params.set("x", state.positionX + "");
|
||||
params.set("y", state.positionY + "");
|
||||
params.set("zoom", state.scale + "");
|
||||
window.location.hash = params.toString();
|
||||
}, 1000)
|
||||
);
|
||||
// useTransformEffect(
|
||||
// throttle(({ state, instance }) => {
|
||||
// const params = new URLSearchParams();
|
||||
// params.set("x", state.positionX + "");
|
||||
// params.set("y", state.positionY + "");
|
||||
// params.set("zoom", state.scale + "");
|
||||
// window.location.hash = params.toString();
|
||||
// }, 1000)
|
||||
// );
|
||||
|
||||
useEffect(() => {
|
||||
if (!config.canvas || !canvasRef.current) return;
|
||||
const canvas = canvasRef.current!;
|
||||
const canvasInstance = new Canvas(config, canvas);
|
||||
centerView();
|
||||
// centerView();
|
||||
|
||||
return () => {
|
||||
canvasInstance.destroy();
|
||||
};
|
||||
}, [canvasRef, centerView, config]);
|
||||
}, [canvasRef, config]);
|
||||
|
||||
return (
|
||||
<canvas
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
import React from "react";
|
||||
|
||||
export const PanZoomWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
return <div className="test-wrapper">{children}</div>;
|
||||
};
|
|
@ -116,3 +116,4 @@ main {
|
|||
}
|
||||
|
||||
@import "./components/Pallete.scss";
|
||||
@import "./board.scss";
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
{
|
||||
"name": "@sc07-canvas/lib",
|
||||
"version": "1.0.0",
|
||||
"main": "./src/index.ts"
|
||||
"main": "./src/index.ts",
|
||||
"dependencies": {
|
||||
"eventemitter3": "^5.0.1"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,379 @@
|
|||
import EventEmitter from "eventemitter3";
|
||||
import {
|
||||
calculatePinchZoom,
|
||||
calculateTouchMidPoint,
|
||||
getTouchDistance,
|
||||
} from "./lib/pinch.utils";
|
||||
import {
|
||||
checkZoomBounds,
|
||||
handleCalculateZoomPositions,
|
||||
} from "./lib/zoom.utils";
|
||||
import { Panning } from "./lib/panning.utils";
|
||||
|
||||
interface TransformState {
|
||||
scale: number;
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
interface Flags {
|
||||
useZoom: boolean;
|
||||
}
|
||||
|
||||
interface TouchState {
|
||||
lastTouch: number | null;
|
||||
pinchStartDistance: number | null;
|
||||
lastDistance: number | null;
|
||||
pinchStartScale: number | null;
|
||||
pinchMidpoint: { x: number; y: number } | null;
|
||||
}
|
||||
|
||||
interface MouseState {}
|
||||
|
||||
interface ISetup {
|
||||
scale: [number, number];
|
||||
}
|
||||
|
||||
interface PanZoomEvents {
|
||||
doubleTap: (e: TouchEvent) => void;
|
||||
}
|
||||
|
||||
export class PanZoom extends EventEmitter<PanZoomEvents> {
|
||||
public $wrapper: HTMLDivElement = null as any;
|
||||
public $zoom: HTMLDivElement = null as any;
|
||||
public $move: HTMLDivElement = null as any;
|
||||
|
||||
public transform: TransformState;
|
||||
public touch: TouchState;
|
||||
public mouse: MouseState;
|
||||
public setup: ISetup;
|
||||
public flags: Flags;
|
||||
|
||||
public panning: Panning;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.transform = {
|
||||
scale: 1,
|
||||
x: 0,
|
||||
y: 0,
|
||||
};
|
||||
|
||||
this.touch = {
|
||||
lastTouch: null,
|
||||
pinchStartDistance: null,
|
||||
lastDistance: null,
|
||||
pinchStartScale: null,
|
||||
pinchMidpoint: null,
|
||||
};
|
||||
|
||||
this.mouse = {};
|
||||
|
||||
this.panning = new Panning(this);
|
||||
|
||||
this.setup = {
|
||||
scale: [1, 50],
|
||||
};
|
||||
|
||||
this.flags = {
|
||||
useZoom: false,
|
||||
};
|
||||
}
|
||||
|
||||
initialize(
|
||||
$wrapper: HTMLDivElement,
|
||||
$zoom: HTMLDivElement,
|
||||
$move: HTMLDivElement
|
||||
) {
|
||||
this.$wrapper = $wrapper;
|
||||
this.$zoom = $zoom;
|
||||
this.$move = $move;
|
||||
|
||||
this.detectFlags();
|
||||
this.registerMouseEvents();
|
||||
this.registerTouchEvents();
|
||||
}
|
||||
|
||||
detectFlags() {
|
||||
// Pxls/resources/public/include/helpers.js
|
||||
let haveZoomRendering = false;
|
||||
let haveImageRendering = false;
|
||||
const webkitBased = navigator.userAgent.match(/AppleWebKit/i);
|
||||
const iOSSafari =
|
||||
navigator.userAgent.match(/(iPod|iPhone|iPad)/i) && webkitBased;
|
||||
const desktopSafari =
|
||||
navigator.userAgent.match(/safari/i) &&
|
||||
!navigator.userAgent.match(/chrome/i);
|
||||
const msEdge = navigator.userAgent.indexOf("Edge") > -1;
|
||||
const possiblyMobile =
|
||||
window.innerWidth < 768 && navigator.userAgent.includes("Mobile");
|
||||
if (iOSSafari) {
|
||||
const iOS =
|
||||
parseFloat(
|
||||
(
|
||||
"" +
|
||||
(/CPU.*OS ([0-9_]{1,5})|(CPU like).*AppleWebKit.*Mobile/i.exec(
|
||||
navigator.userAgent
|
||||
) || [0, ""])[1]
|
||||
)
|
||||
.replace("undefined", "3_2")
|
||||
.replace("_", ".")
|
||||
.replace("_", "")
|
||||
) || false;
|
||||
haveImageRendering = false;
|
||||
if (iOS && iOS >= 11) {
|
||||
haveZoomRendering = true;
|
||||
}
|
||||
} else if (desktopSafari) {
|
||||
haveImageRendering = false;
|
||||
haveZoomRendering = true;
|
||||
}
|
||||
if (msEdge) {
|
||||
haveImageRendering = false;
|
||||
}
|
||||
|
||||
this.flags.useZoom = haveZoomRendering;
|
||||
}
|
||||
|
||||
registerTouchEvents() {
|
||||
this.$wrapper.addEventListener(
|
||||
"touchstart",
|
||||
(event) => {
|
||||
const isDoubleTap =
|
||||
this.touch.lastTouch && +new Date() - this.touch.lastTouch < 200;
|
||||
|
||||
if (isDoubleTap && event.touches.length === 1) {
|
||||
this.emit("doubleTap", event);
|
||||
} else {
|
||||
this.touch.lastTouch = +new Date();
|
||||
|
||||
const { touches } = event;
|
||||
|
||||
const isPanningAction = touches.length === 1;
|
||||
const isPinchAction = touches.length === 2;
|
||||
|
||||
if (isPanningAction) {
|
||||
this.panning.start(touches[0].clientX, touches[0].clientY);
|
||||
}
|
||||
if (isPinchAction) {
|
||||
this.onPinchStart(event);
|
||||
}
|
||||
}
|
||||
},
|
||||
{ passive: false }
|
||||
);
|
||||
|
||||
this.$wrapper.addEventListener("touchmove", (event) => {
|
||||
if (this.panning.enabled && event.touches.length === 1) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const touch = event.touches[0];
|
||||
|
||||
this.panning.move(touch.clientX, touch.clientY);
|
||||
} else if (event.touches.length > 1) {
|
||||
this.onPinch(event);
|
||||
}
|
||||
});
|
||||
|
||||
this.$wrapper.addEventListener("touchend", (event) => {
|
||||
if (this.panning.enabled) {
|
||||
this.panning.enabled = false;
|
||||
|
||||
const touch = event.changedTouches[0];
|
||||
|
||||
this.panning.end(touch.clientX, touch.clientY);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// /////
|
||||
// pinch
|
||||
/// /////
|
||||
|
||||
onPinchStart(event: TouchEvent) {
|
||||
const distance = getTouchDistance(event);
|
||||
|
||||
this.touch.pinchStartDistance = distance;
|
||||
this.touch.lastDistance = distance;
|
||||
this.touch.pinchStartScale = this.transform.scale;
|
||||
this.panning.enabled = false;
|
||||
}
|
||||
|
||||
onPinch(event: TouchEvent) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const { scale } = this.transform;
|
||||
|
||||
// one finger started from outside the wrapper
|
||||
if (this.touch.pinchStartDistance === null) return;
|
||||
|
||||
let el: HTMLElement = document.body;
|
||||
// switch (
|
||||
// (document.getElementById("test-flag")! as HTMLSelectElement).value
|
||||
// ) {
|
||||
// case "body":
|
||||
// el = document.body;
|
||||
// break;
|
||||
// case "wrapper":
|
||||
// el = this.$wrapper;
|
||||
// break;
|
||||
// case "move":
|
||||
// el = this.$move;
|
||||
// break;
|
||||
// default:
|
||||
// case "zoom":
|
||||
// el = this.$zoom;
|
||||
// break;
|
||||
// }
|
||||
|
||||
const midPoint = calculateTouchMidPoint(this, event, scale, el);
|
||||
|
||||
if (!Number.isFinite(midPoint.x) || !Number.isFinite(midPoint.y)) return;
|
||||
|
||||
const currentDistance = getTouchDistance(event);
|
||||
const newScale = calculatePinchZoom(this, currentDistance);
|
||||
|
||||
if (newScale === scale) return;
|
||||
|
||||
const { x, y } = handleCalculateZoomPositions(
|
||||
this,
|
||||
midPoint.x,
|
||||
midPoint.y,
|
||||
newScale
|
||||
);
|
||||
|
||||
this.touch.pinchMidpoint = midPoint;
|
||||
this.touch.lastDistance = currentDistance;
|
||||
|
||||
this.debug(midPoint.x, midPoint.y, "midpoint");
|
||||
|
||||
this.transform.x = midPoint.x / newScale - midPoint.x / scale;
|
||||
this.transform.y = midPoint.y / newScale - midPoint.x / scale;
|
||||
this.transform.scale = newScale;
|
||||
this.update();
|
||||
}
|
||||
|
||||
debug(x: number, y: number, id?: string) {
|
||||
// if (document.getElementById("debug-" + id)) {
|
||||
// document.getElementById("debug-" + id)!.style.top = y + "px";
|
||||
// document.getElementById("debug-" + id)!.style.left = x + "px";
|
||||
// return;
|
||||
// }
|
||||
// let el = document.createElement("div");
|
||||
// if (id) el.id = "debug-" + id;
|
||||
// el.classList.add("debug-point");
|
||||
// el.style.setProperty("top", y + "px");
|
||||
// el.style.setProperty("left", x + "px");
|
||||
// document.body.appendChild(el);
|
||||
}
|
||||
|
||||
registerMouseEvents() {
|
||||
// zoom
|
||||
this.$wrapper.addEventListener(
|
||||
"wheel",
|
||||
(e) => {
|
||||
// if (!self.allowDrag) return;
|
||||
const oldScale = this.transform.scale;
|
||||
|
||||
let delta = -e.deltaY;
|
||||
|
||||
switch (e.deltaMode) {
|
||||
case WheelEvent.DOM_DELTA_PIXEL:
|
||||
// 53 pixels is the default chrome gives for a wheel scroll.
|
||||
delta /= 53;
|
||||
break;
|
||||
case WheelEvent.DOM_DELTA_LINE:
|
||||
// default case on Firefox, three lines is default number.
|
||||
delta /= 3;
|
||||
break;
|
||||
case WheelEvent.DOM_DELTA_PAGE:
|
||||
delta = Math.sign(delta);
|
||||
break;
|
||||
}
|
||||
|
||||
this.nudgeScale(delta);
|
||||
|
||||
const scale = this.transform.scale;
|
||||
if (oldScale !== scale) {
|
||||
const dx = e.clientX - this.$wrapper.clientWidth / 2;
|
||||
const dy = e.clientY - this.$wrapper.clientHeight / 2;
|
||||
this.transform.x -= dx / oldScale;
|
||||
this.transform.x += dx / scale;
|
||||
this.transform.y -= dy / oldScale;
|
||||
this.transform.y += dy / scale;
|
||||
this.update();
|
||||
// place.update();
|
||||
}
|
||||
},
|
||||
{ passive: true }
|
||||
);
|
||||
|
||||
this.$wrapper.addEventListener(
|
||||
"mousedown",
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
this.panning.start(e.clientX, e.clientY);
|
||||
},
|
||||
{ passive: false }
|
||||
);
|
||||
|
||||
this.$wrapper.addEventListener(
|
||||
"mousemove",
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (!this.panning.enabled) return;
|
||||
|
||||
this.panning.move(e.clientX, e.clientY);
|
||||
},
|
||||
{ passive: false }
|
||||
);
|
||||
|
||||
this.$wrapper.addEventListener(
|
||||
"mouseup",
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
this.panning.end(e.clientX, e.clientY);
|
||||
},
|
||||
{ passive: false }
|
||||
);
|
||||
}
|
||||
|
||||
update() {
|
||||
if (this.flags.useZoom) {
|
||||
this.$zoom.style.setProperty("zoom", this.transform.scale * 100 + "%");
|
||||
} else {
|
||||
this.$zoom.style.setProperty(
|
||||
"transform",
|
||||
`scale(${this.transform.scale})`
|
||||
);
|
||||
}
|
||||
|
||||
this.$move.style.setProperty(
|
||||
"transform",
|
||||
`translate(${this.transform.x}px, ${this.transform.y}px)`
|
||||
);
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
// remove event handlers
|
||||
}
|
||||
|
||||
// utilities
|
||||
|
||||
nudgeScale(adj: number) {
|
||||
this.transform.scale = checkZoomBounds(
|
||||
this.transform.scale * 1.5 ** adj,
|
||||
this.setup.scale[0],
|
||||
this.setup.scale[1]
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
import React, { useRef, useState, useEffect } from "react";
|
||||
import { RendererContext } from "./RendererContext";
|
||||
import { PanZoom } from "./PanZoom";
|
||||
|
||||
export const PanZoomWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
const wrapper = useRef<HTMLDivElement>(null);
|
||||
const zoom = useRef<HTMLDivElement>(null);
|
||||
const move = useRef<HTMLDivElement>(null);
|
||||
|
||||
const instance = useRef(new PanZoom()).current;
|
||||
|
||||
useEffect(() => {
|
||||
const $wrapper = wrapper.current;
|
||||
const $zoom = zoom.current;
|
||||
const $move = move.current;
|
||||
|
||||
if ($wrapper && $zoom && $move) {
|
||||
instance.initialize($wrapper, $zoom, $move);
|
||||
}
|
||||
|
||||
return () => {
|
||||
instance.cleanup();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<RendererContext.Provider value={instance}>
|
||||
<div
|
||||
ref={wrapper}
|
||||
className="board-wrapper"
|
||||
style={{ touchAction: "none" }}
|
||||
>
|
||||
<div ref={zoom} className="board-zoom">
|
||||
<div ref={move} className="board-move">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</RendererContext.Provider>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,10 @@
|
|||
This is a merge of two projects
|
||||
|
||||
- https://github.com/BetterTyped/react-zoom-pan-pinch
|
||||
- https://github.com/pxlsspace/Pxls
|
||||
|
||||
react-zoom-pan-pinch handles most cases, but doesn't support css `zoom` property, which Pxls found out(?) that on some browsers, css `zoom` can be used to make canvas elements not blurry on some browsers
|
||||
|
||||
react-zoom-pan-pinch would've required a lot of modifications to how it calculates touch points as css `zoom` changes coordinates returned by events
|
||||
|
||||
Both projects are MIT licensed
|
|
@ -0,0 +1,4 @@
|
|||
import { createContext } from "react";
|
||||
import { PanZoom } from "./PanZoom";
|
||||
|
||||
export const RendererContext = createContext<PanZoom>(null as any);
|
|
@ -0,0 +1 @@
|
|||
export { PanZoomWrapper } from "./PanZoomWrapper";
|
|
@ -0,0 +1,60 @@
|
|||
import { PanZoom } from "../PanZoom";
|
||||
|
||||
export class Panning {
|
||||
private instance: PanZoom;
|
||||
|
||||
public enabled: boolean = false;
|
||||
public x: number = 0;
|
||||
public y: number = 0;
|
||||
|
||||
constructor(instance: PanZoom) {
|
||||
this.instance = instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* trigger panning start
|
||||
* @param x clientX
|
||||
* @param y clientY
|
||||
*/
|
||||
public start(x: number, y: number) {
|
||||
this.enabled = true;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
/**
|
||||
* handle moving
|
||||
* @param x clientX
|
||||
* @param y clientY
|
||||
*/
|
||||
public move(x: number, y: number) {
|
||||
const deltaX = (x - this.x) / this.instance.transform.scale;
|
||||
const deltaY = (y - this.y) / this.instance.transform.scale;
|
||||
const newX = this.instance.transform.x + deltaX;
|
||||
const newY = this.instance.transform.y + deltaY;
|
||||
|
||||
this.instance.$move.style.setProperty(
|
||||
"transform",
|
||||
`translate(${newX}px, ${newY}px)`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* save the final change
|
||||
* @param x clientX
|
||||
* @param y clientY
|
||||
*/
|
||||
public end(x: number, y: number) {
|
||||
this.enabled = false;
|
||||
|
||||
const deltaX = (x - this.x) / this.instance.transform.scale;
|
||||
const deltaY = (y - this.y) / this.instance.transform.scale;
|
||||
const newX = this.instance.transform.x + deltaX;
|
||||
const newY = this.instance.transform.y + deltaY;
|
||||
|
||||
this.instance.transform.x = newX;
|
||||
this.instance.transform.y = newY;
|
||||
|
||||
this.instance.update();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
import { PanZoom } from "../PanZoom";
|
||||
import { checkZoomBounds } from "./zoom.utils";
|
||||
|
||||
export const getTouchDistance = (event: TouchEvent): number => {
|
||||
return Math.sqrt(
|
||||
(event.touches[0].pageX - event.touches[1].pageX) ** 2 +
|
||||
(event.touches[0].pageY - event.touches[1].pageY) ** 2
|
||||
);
|
||||
};
|
||||
|
||||
export const roundNumber = (num: number, decimal: number) => {
|
||||
return Number(num.toFixed(decimal));
|
||||
};
|
||||
|
||||
export const calculatePinchZoom = (
|
||||
contextInstance: PanZoom,
|
||||
currentDistance: number
|
||||
): number => {
|
||||
const { touch, setup } = contextInstance;
|
||||
const { scale } = setup;
|
||||
// const { maxScale, minScale, zoomAnimation, disablePadding } = setup;
|
||||
// const { size, disabled } = zoomAnimation;
|
||||
|
||||
const [minScale, maxScale] = scale;
|
||||
|
||||
if (
|
||||
!touch.pinchStartScale ||
|
||||
touch.pinchStartDistance === null ||
|
||||
!currentDistance
|
||||
) {
|
||||
throw new Error("Pinch touches distance was not provided");
|
||||
}
|
||||
|
||||
if (currentDistance < 0) {
|
||||
return contextInstance.transform.scale;
|
||||
}
|
||||
|
||||
const touchProportion = currentDistance / touch.pinchStartDistance;
|
||||
const scaleDifference = touchProportion * touch.pinchStartScale;
|
||||
|
||||
return checkZoomBounds(roundNumber(scaleDifference, 2), minScale, maxScale);
|
||||
};
|
||||
|
||||
export const calculateTouchMidPoint = (
|
||||
instance: PanZoom,
|
||||
event: TouchEvent,
|
||||
scale: number,
|
||||
contentComponent: HTMLElement
|
||||
): { x: number; y: number } => {
|
||||
const contentRect = contentComponent.getBoundingClientRect();
|
||||
const { touches } = event;
|
||||
const firstPointX = roundNumber(touches[0].clientX - contentRect.left, 5);
|
||||
const firstPointY = roundNumber(touches[0].clientY - contentRect.top, 5);
|
||||
const secondPointX = roundNumber(touches[1].clientX - contentRect.left, 5);
|
||||
const secondPointY = roundNumber(touches[1].clientY - contentRect.top, 5);
|
||||
|
||||
return {
|
||||
x: (firstPointX + secondPointX) / 2,
|
||||
y: (firstPointY + secondPointY) / 2,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,49 @@
|
|||
import { PanZoom } from "../PanZoom";
|
||||
|
||||
export function handleCalculateZoomPositions(
|
||||
contextInstance: PanZoom,
|
||||
mouseX: number,
|
||||
mouseY: number,
|
||||
newScale: number
|
||||
): { x: number; y: number } {
|
||||
const { scale, x, y } = contextInstance.transform;
|
||||
|
||||
const scaleDifference = newScale - scale;
|
||||
|
||||
if (typeof mouseX !== "number" || typeof mouseY !== "number") {
|
||||
console.error("Mouse X and Y position were not provided!");
|
||||
return { x, y };
|
||||
}
|
||||
|
||||
const calculatedPositionX = x - mouseX * scaleDifference;
|
||||
const calculatedPositionY = y - mouseY * scaleDifference;
|
||||
contextInstance.debug(calculatedPositionX, calculatedPositionY, "zoom");
|
||||
|
||||
// do not limit to bounds when there is padding animation,
|
||||
// it causes animation strange behaviour
|
||||
|
||||
// const newPositions = getMouseBoundedPosition(
|
||||
// calculatedPositionX,
|
||||
// calculatedPositionY,
|
||||
// bounds,
|
||||
// limitToBounds,
|
||||
// 0,
|
||||
// 0,
|
||||
// null,
|
||||
// );
|
||||
|
||||
return {
|
||||
x: calculatedPositionX,
|
||||
y: calculatedPositionY,
|
||||
};
|
||||
}
|
||||
|
||||
export function checkZoomBounds(
|
||||
zoom: number,
|
||||
minScale: number,
|
||||
maxScale: number
|
||||
): number {
|
||||
if (!Number.isNaN(maxScale) && zoom >= maxScale) return maxScale;
|
||||
if (!Number.isNaN(minScale) && zoom <= minScale) return minScale;
|
||||
return zoom;
|
||||
}
|
Loading…
Reference in New Issue