react-easy-panzoom
Version:
Wrapper to enable pan and zoom for any React component
133 lines (113 loc) • 4.12 kB
JavaScript
export type TransformationMatrix = {
a: number,
b: number,
c: number,
d: number,
x: number,
y: number,
}
export type BoundingBox = {
top: number,
left: number,
width: number,
height: number,
}
export type BoundaryRatio = {
vertical: number,
horizontal: number,
}
export type Coordinates = {
x: number,
y: number,
}
export type BoundCoordinates = { boundX: number, boundY: number, offsetX: number, offsetY: number }
export type TransformationParameters = {
angle: number,
scale: number,
offsetX: number,
offsetY: number,
}
export const ZOOM_SPEED_MULTIPLIER = 0.065
// Transform matrix use to rotate, zoom and pan
// Can be written as T(centerX, centerY) * R(theta) * T(-centerX, -centerY) * S(scale, scale) + T(offsetX, offsetY)
// ( a , c, x )
// ( b , d, y )
// ( 0 , 0, 1 )
export const TransformMatrix = (transformationParameters: TransformationParameters, centerCoordinates: Coordinates, ): TransformationMatrix => {
const { angle, scale, offsetX, offsetY } = transformationParameters
const { x: centerX, y: centerY } = centerCoordinates
const theta = angle * Math.PI / 180
const a = Math.cos(theta) * scale
const b = Math.sin(theta) * scale
const c = -b
const d = a
const transformX = - centerX * a + centerY * b + centerX * scale
const transformY = centerX * c - centerY * d + centerY * scale
return { a, b, c, d, x : transformX + offsetX, y: transformY + offsetY }
}
const applyTransformMatrix = (transformationParameters: TransformationParameters, centerCoordinates: Coordinates) => (x, y): [number, number] => {
const { a, b, c, d, x: transformX, y: transformY } = TransformMatrix(transformationParameters, centerCoordinates)
return [
x * a + y * c + transformX,
x * b + y * d + transformY,
]
}
export const getTransformedBoundingBox = (transformationParameters: TransformationParameters, boundingBox: BoundingBox): BoundingBox => {
const { top, left, width, height } = boundingBox
const center = {
x: width / 2,
y: height / 2,
}
const getTransformedCoordinates = applyTransformMatrix(transformationParameters, center)
const [x1, y1] = getTransformedCoordinates(left, top)
const [x2, y2] = getTransformedCoordinates(left + width, top)
const [x3, y3] = getTransformedCoordinates(left + width, top + height)
const [x4, y4] = getTransformedCoordinates(left, top + height)
return {
top: Math.min(y1, y2, y3, y4),
left: Math.min(x1, x2, x3, x4),
width: Math.max(x1, x2, x3, x4) - Math.min(x1, x2, x3, x4),
height: Math.max(y1, y2, y3, y4) - Math.min(y1, y2, y3, y4),
}
}
export const getScaleMultiplier = (delta: number, zoomSpeed: number = 1): number => {
let speed = ZOOM_SPEED_MULTIPLIER * zoomSpeed
let scaleMultiplier = 1
if (delta > 0) { // zoom out
scaleMultiplier = (1 - speed)
} else if (delta < 0) { // zoom in
scaleMultiplier = (1 + speed)
}
return scaleMultiplier
}
export const boundCoordinates = (
x: number, y: number,
boundaryRatio: BoundaryRatio,
boundingBox: BoundingBox,
containerHeight: number, containerWidth: number,
offsetX?: number = 0, offsetY?: number = 0): BoundCoordinates => {
const { top, left, width, height } = boundingBox
// check that computed are inside boundaries otherwise set to the bounding box limits
let boundX = left
let boundY = top
if (boundY < -boundaryRatio.vertical * height) {
boundY = -boundaryRatio.vertical * height
}
else if (boundY > containerHeight - (1 - boundaryRatio.vertical) * height) {
boundY = containerHeight - (1 - boundaryRatio.vertical) * height
}
if (boundX < -boundaryRatio.horizontal * width) {
boundX = -boundaryRatio.horizontal * width
}
else if (boundX > containerWidth - (1 - boundaryRatio.horizontal) * width) {
boundX = containerWidth - (1 - boundaryRatio.horizontal) * width
}
// return new bounds coordinates for the transform matrix
// not the computed x/y coordinates
return {
boundX: x - (left - boundX),
boundY: y - (top - boundY),
offsetX: offsetX - (left - boundX),
offsetY: offsetY - (top - boundY),
}
}