Skip to content

Commit

Permalink
feat(zoom): pinch zooming with pointer events!
Browse files Browse the repository at this point in the history
  • Loading branch information
timmywil committed Aug 8, 2019
1 parent 9c8efb4 commit 5ddbd30
Show file tree
Hide file tree
Showing 3 changed files with 54 additions and 27 deletions.
68 changes: 46 additions & 22 deletions src/panzoom.ts
Expand Up @@ -9,7 +9,7 @@
import { getBorder, getMargin, getPadding, setStyle, setTransform } from './css'
import isAttached from './isAttached'
import isSVGElement from './isSVGElement'
import { addEvent, removeEvent } from './pointers'
import { addEvent, getDistance, getMiddle, removeEvent } from './pointers'
import './polyfills'
import { PanOptions, PanzoomObject, PanzoomOptions, ZoomOptions } from './types'

Expand All @@ -30,7 +30,7 @@ const defaultOptions: PanzoomOptions = {
startX: 0,
startY: 0,
startScale: 1,
step: 0.1
step: 0.3
}

function Panzoom(elem: HTMLElement | SVGElement, options?: PanzoomOptions): PanzoomObject {
Expand Down Expand Up @@ -62,7 +62,7 @@ function Panzoom(elem: HTMLElement | SVGElement, options?: PanzoomOptions): Panz
// Handle option side-effects
if (opts.hasOwnProperty('cursor')) {
elem.style.cursor = opts.cursor
}
}
}

// Set overflow on the parent
Expand All @@ -76,6 +76,7 @@ function Panzoom(elem: HTMLElement | SVGElement, options?: PanzoomOptions): Panz
// Set some default styles on the panzoom element
elem.style.cursor = options.cursor
elem.style.userSelect = 'none'
elem.style.touchAction = 'none'
// The default for HTML is '50% 50%'
// The default for SVG is '0 0'
// SVG can't be changed in IE
Expand Down Expand Up @@ -218,7 +219,9 @@ function Panzoom(elem: HTMLElement | SVGElement, options?: PanzoomOptions): Panz
x = result.x
y = result.y

opts.setTransform(elem, { x, y, scale }, opts)
const values = { x, y, scale }
opts.setTransform(elem, values, opts)
return values
}

function zoom(toScale: number, zoomOptions?: ZoomOptions) {
Expand All @@ -242,7 +245,9 @@ function Panzoom(elem: HTMLElement | SVGElement, options?: PanzoomOptions): Panz
}

scale = toScale
opts.setTransform(elem, { x, y, scale }, opts)
const values = { x, y, scale }
opts.setTransform(elem, values, opts)
return values
}

function zoomInOut(isIn: boolean, zoomOptions?: ZoomOptions) {
Expand All @@ -258,7 +263,11 @@ function Panzoom(elem: HTMLElement | SVGElement, options?: PanzoomOptions): Panz
zoomInOut(false, zoomOptions)
}

function zoomToMousePoint(toScale: number, point: { clientX: number; clientY: number }) {
function zoomToMousePoint(
toScale: number,
point: { clientX: number; clientY: number },
zoomOptions?: ZoomOptions
) {
const dims = getDimensions()

// Instead of thinking of operating on the panzoom element,
Expand Down Expand Up @@ -310,20 +319,22 @@ function Panzoom(elem: HTMLElement | SVGElement, options?: PanzoomOptions): Panz
y: (clientY / effectiveArea.height) * (effectiveArea.height * toScale)
}

zoom(toScale, { focal, animate: false })
return zoom(toScale, { ...zoomOptions, focal, animate: false })
}

function zoomWithWheel(event: WheelEvent) {
function zoomWithWheel(event: WheelEvent, zoomOptions?: ZoomOptions) {
// Need to prevent the default here
// or it conflicts with regular page scroll
event.preventDefault()

const opts = { ...options, ...zoomOptions }

// Normalize to deltaX in case shift modifier is used on Mac
const delta = event.deltaY === 0 && event.deltaX ? event.deltaX : event.deltaY
const wheel = delta < 0 ? 1 : -1
const toScale = constrainScale(scale * Math.exp(wheel * options.step)).scale
const toScale = constrainScale(scale * Math.exp((wheel * opts.step) / 3), opts).scale

zoomToMousePoint(toScale, event)
zoomToMousePoint(toScale, event, opts)
}

function reset(resetOptions?: PanzoomOptions) {
Expand All @@ -339,26 +350,32 @@ function Panzoom(elem: HTMLElement | SVGElement, options?: PanzoomOptions): Panz
let origY: number
let startX: number
let startY: number
let startScale: number
let startDistance: number
const pointers: PointerEvent[] = []

function handleDown(event: PointerEvent) {
addEvent(pointers, event)
if (event.pointerId) {
elem.setPointerCapture(event.pointerId)
}
if (
isPanning ||
(event.target && (event.target as Element).classList.contains(options.clickableClass))
) {
// Don't handle this event if the target is a clickable
if (event.target && (event.target as Element).classList.contains(options.clickableClass)) {
return
}
isPanning = true
event.preventDefault()
event.stopPropagation()
origX = x
origY = y
startX = event.clientX
startY = event.clientY

// This works whether there are multiple
// pointers or not
const point = getMiddle(pointers)
startX = point.clientX
startY = point.clientY
startScale = scale
startDistance = getDistance(pointers)
}

function move(event: PointerEvent) {
Expand All @@ -372,21 +389,28 @@ function Panzoom(elem: HTMLElement | SVGElement, options?: PanzoomOptions): Panz
) {
return
}
pan(origX + (event.clientX - startX) / scale, origY + (event.clientY - startY) / scale, {
addEvent(pointers, event)
const current = getMiddle(pointers)
if (pointers.length > 1) {
// Use the distance between the first 2 pointers
// to determine the current scale
const diff = getDistance(pointers) - startDistance
const toScale = constrainScale((diff * options.step) / 80 + startScale).scale
zoomToMousePoint(toScale, current)
}

pan(origX + (current.clientX - startX) / scale, origY + (current.clientY - startY) / scale, {
animate: false
})
}

function handleUp(event: PointerEvent) {
// Note: don't remove all pointers
// Can restart without having to reinitiate all of them
removeEvent(pointers, event)
if (event.pointerId) {
elem.releasePointerCapture(event.pointerId)
}
// If there are still pointers active,
// don't stop panning
if (pointers.length > 0) {
return
}
isPanning = false
origX = origY = startX = startY = undefined
}
Expand Down
9 changes: 6 additions & 3 deletions src/pointers.ts
Expand Up @@ -13,9 +13,12 @@ function findEventIndex(pointers: PointerEvent[], event: PointerEvent) {
}

export function addEvent(pointers: PointerEvent[], event: PointerEvent) {
if (findEventIndex(pointers, event) === -1) {
pointers.push(event)
const i = findEventIndex(pointers, event)
// Update if already present
if (i > -1) {
pointers.splice(i, 1)
}
pointers.push(event)
}

export function removeEvent(pointers: PointerEvent[], event: PointerEvent) {
Expand All @@ -30,7 +33,7 @@ export function removeEvent(pointers: PointerEvent[], event: PointerEvent) {
* the given pointer events, for panning
* with multiple pointers.
*/
export function getCenter(pointers: PointerEvent[]) {
export function getMiddle(pointers: PointerEvent[]) {
// Copy to avoid changing by reference
pointers = pointers.slice(0)
let event1: Pick<PointerEvent, 'clientX' | 'clientY'> = pointers.pop()
Expand Down
4 changes: 2 additions & 2 deletions test/demo/examples/Standard.tsx
Expand Up @@ -6,7 +6,7 @@ import Demo from '../Demo'
const code = (
<Code>
{`\
const panzoom = Panzoom(elem, { step: 0.3 })
const panzoom = Panzoom(elem)
zoomInButton.addEventListener('click', panzoom.zoomIn)
zoomOutButton.addEventListener('click', panzoom.zoomOut)
resetButton.addEventListener('click', panzoom.reset)
Expand All @@ -22,7 +22,7 @@ export default function Buttons() {
const panzoomRef = useRef<Panzoom>(null)
let panzoom = panzoomRef.current
useEffect(() => {
panzoom = panzoomRef.current = Panzoom(elem.current, { step: 0.3 })
panzoom = panzoomRef.current = Panzoom(elem.current)
}, [])
return (
<Demo title="Panning and zooming" code={code}>
Expand Down

0 comments on commit 5ddbd30

Please sign in to comment.