react-image-zoom-in-place
Advanced tools
Comparing version
222
lib/index.js
@@ -8,75 +8,35 @@ "use strict"; | ||
const utils_1 = require("./utils"); | ||
var InputMethod; | ||
(function (InputMethod) { | ||
InputMethod["Mouse"] = "Mouse"; | ||
InputMethod["Touch"] = "Touch"; | ||
})(InputMethod || (InputMethod = {})); | ||
const INPUTMETHODLIFESPAN = 200; //ms | ||
const RESOLUTIONTOZOOMMULTIPLIER = 100; | ||
function ZoomableImage(props) { | ||
const canvasRef = (0, react_1.useRef)(null); | ||
const [image, setImage] = (0, react_1.useState)(null); | ||
const [zoomPoint, setZoomPoint] = (0, react_1.useState)({ | ||
const [imageOffset, setImageOffset] = (0, react_1.useState)({ | ||
x: 0, | ||
y: 0, | ||
}); | ||
const [firstPoint, setFirstPoint] = (0, react_1.useState)({ | ||
x: 0, | ||
y: 0, | ||
}); | ||
const [secondPoint, setSecondPoint] = (0, react_1.useState)({ | ||
x: 0, | ||
y: 0, | ||
}); | ||
const [mouseLocation, setMouseLocation] = (0, react_1.useState)({ | ||
x: 0, | ||
y: 0, | ||
}); | ||
const [previousPinchDistance, setPreviousPinchDistance] = (0, react_1.useState)(0); | ||
const [zoomLevel, setZoomLevel] = (0, react_1.useState)(1); | ||
const [inputMethod, setInputMethod] = (0, react_1.useState)(null); | ||
const [inputMethodTimestamp, setInputMethodTimestamp] = (0, react_1.useState)(0); | ||
// Function to handle mouse move | ||
const handleMouseMove = (event) => { | ||
if (inputMethod != InputMethod.Mouse && | ||
inputMethod != null && | ||
Date.now() - inputMethodTimestamp < INPUTMETHODLIFESPAN) { | ||
//touch screen state vars | ||
const [previousPosition, setPerviousPosition] = (0, react_1.useState)(null); | ||
const [previousDistance, setPreviousDistance] = (0, react_1.useState)(null); | ||
if (image) { | ||
(0, utils_1.renderAtPointAndZoom)(imageOffset.x, imageOffset.y, zoomLevel, canvasRef.current, image); | ||
} | ||
const updateLocation = (x, y, zoom) => { | ||
const canvas = canvasRef.current; | ||
if (!canvas) | ||
return; | ||
if (previousPosition) { | ||
const zoomRatio = zoom / zoomLevel; | ||
const deltaX = x - previousPosition.x; | ||
const deltaY = y - previousPosition.y; | ||
const newOffset = { | ||
x: imageOffset.x - (x - imageOffset.x) * (zoomRatio - 1) + deltaX, | ||
y: imageOffset.y - (y - imageOffset.y) * (zoomRatio - 1) + deltaY, | ||
}; | ||
newOffset.x = Math.min(0, Math.max(newOffset.x, canvas.width - canvas.width * zoom)); | ||
newOffset.y = Math.min(0, Math.max(newOffset.y, canvas.height - canvas.height * zoom)); | ||
setImageOffset(newOffset); | ||
} | ||
const eventX = event.clientX; | ||
const eventY = event.clientY; | ||
setMouseLocation({ | ||
x: eventX, | ||
y: eventY, | ||
}); | ||
(0, utils_1.handleHover)(eventX, eventY, canvasRef.current, image, zoomLevel, props.width, props.height); | ||
setPerviousPosition({ x: x, y: y }); | ||
setZoomLevel(zoom); | ||
}; | ||
const handleMouseEnter = () => { | ||
if (inputMethod != InputMethod.Mouse && | ||
inputMethod != null && | ||
Date.now() - inputMethodTimestamp < INPUTMETHODLIFESPAN) { | ||
return; | ||
} | ||
setZoomLevel(props.zoom); | ||
}; | ||
const handleMouseLeave = () => { | ||
if (inputMethod != InputMethod.Mouse && | ||
inputMethod != null && | ||
Date.now() - inputMethodTimestamp < INPUTMETHODLIFESPAN) { | ||
return; | ||
} | ||
(0, utils_1.resetZoom)(canvasRef.current, image, props.width, props.height); | ||
setZoomLevel(props.zoom); | ||
}; | ||
const handleMouseScroll = (event) => { | ||
let newZoom = 1; | ||
if (event.deltaY < 0) { | ||
newZoom = Math.min(Math.max(1, zoomLevel + props.step), props.maxZoom); | ||
} | ||
else { | ||
newZoom = Math.min(Math.max(1, zoomLevel - props.step), props.maxZoom); | ||
} | ||
setZoomLevel(newZoom); | ||
(0, utils_1.handleHover)(mouseLocation.x, mouseLocation.y, canvasRef.current, image, newZoom, props.width, props.height); | ||
}; | ||
(0, react_1.useEffect)(() => { | ||
@@ -108,70 +68,94 @@ if (props.src == null) { | ||
(0, react_1.useEffect)(() => { | ||
if (!canvasRef.current) | ||
return; | ||
(0, utils_1.setCanvasDimensions)(canvasRef.current, image, props.width, props.height); | ||
//image onload is not triggered for base64 images | ||
if (image != null) { | ||
(0, utils_1.resetZoom)(canvasRef.current, image, props.width, props.height); | ||
setImageOffset({ x: 0, y: 0 }); | ||
setZoomLevel(1); | ||
} | ||
}, [image, props.width, props.height]); | ||
const handleDragStart = (event) => { | ||
if (event.touches.length === 1) { | ||
setFirstPoint({ | ||
x: event.touches[0].clientX, | ||
y: event.touches[0].clientY, | ||
}); | ||
//mouse events | ||
const handleMouseEnter = () => { | ||
setZoomLevel(props.zoom); | ||
}; | ||
// Function to handle mouse move | ||
const handleMouseMove = (event) => { | ||
const eventX = event.clientX; | ||
const eventY = event.clientY; | ||
const [x, y] = (0, utils_1.mouseLocationToImageOffset)(eventX, eventY, zoomLevel, canvasRef.current); | ||
setImageOffset({ | ||
x: x, | ||
y: y, | ||
}); | ||
setPerviousPosition(null); | ||
}; | ||
const handleMouseLeave = () => { | ||
setImageOffset({ | ||
x: 0, | ||
y: 0, | ||
}); | ||
setZoomLevel(1); | ||
setPerviousPosition(null); | ||
}; | ||
const handleMouseScroll = (event) => { | ||
let newZoom = 1; | ||
if (event.deltaY < 0) { | ||
newZoom = Math.min(Math.max(1, zoomLevel + props.step), props.maxZoom); | ||
} | ||
else if (event.touches.length === 2) { | ||
setPreviousPinchDistance(Math.abs(event.touches[0].clientX - event.touches[1].clientX) + | ||
Math.abs(event.touches[0].clientY - event.touches[1].clientY)); | ||
setSecondPoint({ | ||
x: event.touches[1].clientX, | ||
y: event.touches[1].clientY, | ||
}); | ||
else { | ||
newZoom = Math.min(Math.max(1, zoomLevel - props.step), props.maxZoom); | ||
} | ||
const [x, y] = (0, utils_1.mouseLocationToImageOffset)(event.clientX, event.clientY, newZoom, canvasRef.current); | ||
setImageOffset({ | ||
x: x, | ||
y: y, | ||
}); | ||
setZoomLevel(newZoom); | ||
setPerviousPosition(null); | ||
}; | ||
//touch screen events | ||
const handleSingleTouchMove = (e) => { | ||
if (!canvasRef.current || e.touches.length !== 1) | ||
return; | ||
const touch = e.touches[0]; | ||
const rect = canvasRef.current.getBoundingClientRect(); | ||
const x = Math.min(400, Math.max(0, touch.clientX - rect.left)); | ||
const y = Math.min(400, Math.max(0, touch.clientY - rect.top)); | ||
updateLocation(x, y, zoomLevel); | ||
}; | ||
const handlePinchZoomMove = (e) => { | ||
if (!canvasRef.current || e.touches.length !== 2) | ||
return; | ||
const touch1 = e.touches[0]; | ||
const touch2 = e.touches[1]; | ||
const currentDistance = Math.hypot(touch2.clientX - touch1.clientX, touch2.clientY - touch1.clientY); | ||
const midpointX = (touch1.clientX + touch2.clientX) / 2; | ||
const midpointY = (touch1.clientY + touch2.clientY) / 2; | ||
const rect = canvasRef.current.getBoundingClientRect(); | ||
const x = Math.min(400, Math.max(0, midpointX - rect.left)); | ||
const y = Math.min(400, Math.max(0, midpointY - rect.top)); | ||
let newZoomLevel = zoomLevel; | ||
if (previousDistance) { | ||
newZoomLevel = | ||
zoomLevel + | ||
(currentDistance - previousDistance) / | ||
Math.max(rect.width, rect.height); | ||
newZoomLevel = Math.max(1, Math.min(props.maxZoom, newZoomLevel)); | ||
} | ||
updateLocation(x, y, newZoomLevel); | ||
setPreviousDistance(currentDistance); | ||
}; | ||
const handleDragMove = (event) => { | ||
event.preventDefault(); | ||
if (event.touches.length >= 1) { | ||
const canvas = canvasRef.current; | ||
if (!canvas) | ||
return; | ||
const rect = canvas.getBoundingClientRect(); | ||
const changeX = firstPoint.x - event.touches[0].clientX; | ||
let newX = zoomPoint.x + changeX / props.zoom; | ||
newX = Math.max(0, Math.min(newX, rect.width)); | ||
const changeY = firstPoint.y - event.touches[0].clientY; | ||
let newY = zoomPoint.y + changeY / props.zoom; | ||
newY = Math.max(0, Math.min(newY, rect.height)); | ||
setZoomPoint({ | ||
x: newX, | ||
y: newY, | ||
}); | ||
setFirstPoint({ | ||
x: event.touches[0].clientX, | ||
y: event.touches[0].clientY, | ||
}); | ||
setInputMethod(InputMethod.Touch); | ||
setInputMethodTimestamp(Date.now()); | ||
(0, utils_1.handleHover)(newX, newY, canvasRef.current, image, zoomLevel, props.width, props.height); //props.zoom | ||
} | ||
if (event.touches.length >= 2) { | ||
const dist = Math.abs(firstPoint.x - event.touches[1].clientX) + | ||
Math.abs(firstPoint.y - event.touches[1].clientY); | ||
const newZoom = zoomLevel + (dist - previousPinchDistance) / RESOLUTIONTOZOOMMULTIPLIER; | ||
setPreviousPinchDistance(dist); | ||
setSecondPoint({ | ||
x: event.touches[1].clientX, | ||
y: event.touches[1].clientY, | ||
}); | ||
if (newZoom > 1 && newZoom < props.maxZoom) { | ||
setZoomLevel(newZoom); | ||
(0, utils_1.handleHover)(zoomPoint.x, zoomPoint.y, canvasRef.current, image, newZoom, props.width, props.height); | ||
} | ||
} | ||
if (event.touches.length === 1) | ||
handleSingleTouchMove(event); | ||
if (event.touches.length === 2) | ||
handlePinchZoomMove(event); | ||
}; | ||
const handleDragEnd = (event) => { | ||
setFirstPoint(secondPoint); | ||
setInputMethod(InputMethod.Touch); | ||
setInputMethodTimestamp(Date.now()); | ||
const resetDrag = (event) => { | ||
setPerviousPosition(null); | ||
setPreviousDistance(null); | ||
}; | ||
return ((0, jsx_runtime_1.jsx)("div", { style: { height: "100%", width: "100%" }, children: (0, jsx_runtime_1.jsx)("a", { children: (0, jsx_runtime_1.jsx)("canvas", { style: { touchAction: "none" }, ref: canvasRef, width: props.width ? props.width : 1, height: props.height ? props.height : 1, onWheel: handleMouseScroll, onMouseEnter: handleMouseEnter, onMouseMove: handleMouseMove, onMouseLeave: handleMouseLeave, onTouchStart: handleDragStart, onTouchMove: handleDragMove, onTouchEnd: handleDragEnd, "aria-label": props.alt ? props.alt : "" }) }) })); | ||
return ((0, jsx_runtime_1.jsx)("div", { style: { height: "100%", width: "100%" }, children: (0, jsx_runtime_1.jsx)("canvas", { style: { touchAction: "none" }, ref: canvasRef, width: props.width ? props.width : 1, height: props.height ? props.height : 1, onTouchStart: resetDrag, onTouchMove: handleDragMove, onTouchEnd: resetDrag, onMouseEnter: handleMouseEnter, onMouseMove: handleMouseMove, onMouseLeave: handleMouseLeave, onWheel: handleMouseScroll, "aria-label": props.alt ? props.alt : "" }) })); | ||
} |
export declare const setCanvasDimensions: (canvas: HTMLCanvasElement | null, image: HTMLImageElement | null, w?: number, h?: number) => void; | ||
export declare const handleHover: (eventX: number, eventY: number, canvas: HTMLCanvasElement | null, image: HTMLImageElement | null, zoom: number, w?: number, h?: number) => void; | ||
export declare const resetZoom: (canvas: HTMLCanvasElement | null, image: HTMLImageElement | null, w?: number, h?: number) => void; | ||
export declare const renderAtPointAndZoom: (x: number, y: number, zoom: number, canvas: HTMLCanvasElement | null, image: HTMLImageElement | null) => void; | ||
export declare function mouseLocationToImageOffset(eventX: number, eventY: number, zoom: number, canvas: HTMLCanvasElement | null): [number, number]; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.resetZoom = exports.handleHover = exports.setCanvasDimensions = void 0; | ||
exports.renderAtPointAndZoom = exports.setCanvasDimensions = void 0; | ||
exports.mouseLocationToImageOffset = mouseLocationToImageOffset; | ||
const setCanvasDimensions = (canvas, image, w, h) => { | ||
@@ -27,3 +28,3 @@ if (!canvas) | ||
exports.setCanvasDimensions = setCanvasDimensions; | ||
const handleHover = (eventX, eventY, canvas, image, zoom, w, h) => { | ||
const renderAtPointAndZoom = (x, y, zoom, canvas, image) => { | ||
if (!canvas) | ||
@@ -35,26 +36,17 @@ return; | ||
if (image != null) { | ||
// Get the canvas bounding rectangle for accurate calculations | ||
const rect = canvas.getBoundingClientRect(); | ||
// Calculate mouse X and Y relative to the canvas | ||
const mouseX = eventX - rect.left; | ||
const mouseY = eventY - rect.top; | ||
const x = -(mouseX / rect.width) * (zoom * rect.width - rect.width); | ||
const y = -(mouseY / rect.height) * (zoom * rect.height - rect.height); | ||
ctx.clearRect(0, 0, rect.width, rect.height); | ||
ctx.drawImage(image, x, y, rect.width * zoom, rect.height * zoom); | ||
ctx.clearRect(0, 0, canvas.width, canvas.height); | ||
ctx.drawImage(image, x, y, canvas.width * zoom, canvas.height * zoom); | ||
} | ||
}; | ||
exports.handleHover = handleHover; | ||
const resetZoom = (canvas, image, w, h) => { | ||
exports.renderAtPointAndZoom = renderAtPointAndZoom; | ||
function mouseLocationToImageOffset(eventX, eventY, zoom, canvas) { | ||
if (!canvas) | ||
return; | ||
const ctx = canvas.getContext("2d"); | ||
if (!ctx) | ||
return; | ||
if (image != null) { | ||
const rect = canvas.getBoundingClientRect(); | ||
ctx.clearRect(0, 0, rect.width, rect.height); | ||
ctx.drawImage(image, 0, 0, rect.width, rect.height); | ||
} | ||
}; | ||
exports.resetZoom = resetZoom; | ||
return [0, 0]; | ||
const rect = canvas.getBoundingClientRect(); | ||
// Calculate mouse X and Y relative to the canvas | ||
const mouseX = eventX - rect.left; | ||
const mouseY = eventY - rect.top; | ||
const x = -(mouseX / rect.width) * (zoom * rect.width - rect.width); | ||
const y = -(mouseY / rect.height) * (zoom * rect.height - rect.height); | ||
return [x, y]; | ||
} |
{ | ||
"name": "react-image-zoom-in-place", | ||
"version": "0.0.5", | ||
"version": "0.0.6", | ||
"description": "", | ||
@@ -5,0 +5,0 @@ "main": "lib/index.js", |
@@ -9,3 +9,4 @@ # react-image-zoom-in-place | ||
* Fix some glitches in the pinch zoom and dragging. | ||
* Code cleanup | ||
* Tests to ensure functioning in edge cases | ||
@@ -12,0 +13,0 @@ ## Behavior |
73
1.39%17909
-5.51%366
-6.15%