forked from Mirrors/gomuks
web/lightbox: add support for touch panning/zooming
This commit is contained in:
parent
e11c398a57
commit
23fb7db2b9
1 changed files with 107 additions and 13 deletions
|
@ -75,6 +75,11 @@ export interface LightboxProps extends LightboxParams {
|
|||
onClose: () => void
|
||||
}
|
||||
|
||||
interface Point {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
export class Lightbox extends Component<LightboxProps> {
|
||||
translate = { x: 0, y: 0 }
|
||||
zoom = 1
|
||||
|
@ -82,6 +87,9 @@ export class Lightbox extends Component<LightboxProps> {
|
|||
maybePanning = false
|
||||
readonly ref = createRef<HTMLImageElement>()
|
||||
readonly wrapperRef = createRef<HTMLDivElement>()
|
||||
prevTouch1: Point | null = null
|
||||
prevTouch2: Point | null = null
|
||||
prevTouchDist: number | null = null
|
||||
|
||||
get style() {
|
||||
return {
|
||||
|
@ -123,24 +131,55 @@ export class Lightbox extends Component<LightboxProps> {
|
|||
return
|
||||
}
|
||||
evt.preventDefault()
|
||||
const oldZoom = this.zoom
|
||||
const delta = -evt.deltaY / 1000
|
||||
const newDelta = this.zoom + delta * this.zoom
|
||||
this.zoom = Math.min(Math.max(newDelta, 0.01), 10)
|
||||
const zoomDelta = this.zoom - oldZoom
|
||||
const orientation = this.orientation
|
||||
const negateX = (orientation === 2 || orientation == 3) ? -1 : 1
|
||||
const negateY = (orientation === 2 || orientation == 1) ? -1 : 1
|
||||
const deltaX = zoomDelta * (this.ref.current.clientWidth / 2 - evt.nativeEvent.offsetX) * negateX
|
||||
const deltaY = zoomDelta * (this.ref.current.clientHeight / 2 - evt.nativeEvent.offsetY) * negateY
|
||||
const flipXY = orientation === 1 || orientation === 3
|
||||
this.translate.x += flipXY ? deltaY : deltaX
|
||||
this.translate.y += flipXY ? deltaX : deltaY
|
||||
this.#doZoom(-evt.deltaY / 1000, evt.nativeEvent.offsetX, evt.nativeEvent.offsetY, false)
|
||||
const style = this.style
|
||||
this.ref.current.style.translate = style.translate
|
||||
this.ref.current.style.scale = style.scale
|
||||
}
|
||||
|
||||
#getTouchDistance(p1: Point, p2: Point): number {
|
||||
return Math.hypot(p1.x - p2.x, p1.y - p2.y)
|
||||
}
|
||||
|
||||
#getTouchMidpoint(p1: Point, p2: Point): Point {
|
||||
const contentRect = this.ref.current!.getBoundingClientRect()
|
||||
const p1X = p1.x - contentRect.left
|
||||
const p1Y = p1.y - contentRect.top
|
||||
const p2X = p2.x - contentRect.left
|
||||
const p2Y = p2.y - contentRect.top
|
||||
const point = {
|
||||
x: (p1X + p2X) / 2 / this.zoom,
|
||||
y: (p1Y + p2Y) / 2 / this.zoom,
|
||||
}
|
||||
const orientation = this.orientation
|
||||
if (orientation === 1 || orientation === 3) {
|
||||
// This is slightly weird because doZoom will flip the x and y values again,
|
||||
// but maybe the flipped subtraction from clientWidth/Height is important.
|
||||
return { x: point.y, y: point.x }
|
||||
}
|
||||
return point
|
||||
}
|
||||
|
||||
#doZoom(delta: number, offsetX: number, offsetY: number, touch: boolean) {
|
||||
if (!this.ref.current) {
|
||||
return
|
||||
}
|
||||
const oldZoom = this.zoom
|
||||
const newDelta = oldZoom + delta * this.zoom
|
||||
this.zoom = Math.min(Math.max(newDelta, 0.01), 10)
|
||||
const zoomDelta = this.zoom - oldZoom
|
||||
|
||||
const orientation = this.orientation
|
||||
const negateX = !touch && (orientation === 2 || orientation == 3) ? -1 : 1
|
||||
const negateY = !touch && (orientation === 2 || orientation == 1) ? -1 : 1
|
||||
const flipXY = orientation === 1 || orientation === 3
|
||||
|
||||
const deltaX = zoomDelta * (this.ref.current.clientWidth / 2 - offsetX) * negateX
|
||||
const deltaY = zoomDelta * (this.ref.current.clientHeight / 2 - offsetY) * negateY
|
||||
this.translate.x += flipXY ? deltaY : deltaX
|
||||
this.translate.y += flipXY ? deltaX : deltaY
|
||||
}
|
||||
|
||||
onMouseDown = (evt: React.MouseEvent) => {
|
||||
if (evt.buttons === 1) {
|
||||
evt.preventDefault()
|
||||
|
@ -164,6 +203,57 @@ export class Lightbox extends Component<LightboxProps> {
|
|||
this.ref.current.style.cursor = "grabbing"
|
||||
}
|
||||
|
||||
onTouchStart = (evt: React.TouchEvent) => {
|
||||
if (evt.touches.length === 1) {
|
||||
this.maybePanning = true
|
||||
this.prevTouch1 = { x: evt.touches[0].pageX, y: evt.touches[0].pageY }
|
||||
this.prevTouch2 = null
|
||||
} else if (evt.touches.length === 2) {
|
||||
this.prevTouch1 = { x: evt.touches[0].pageX, y: evt.touches[0].pageY }
|
||||
this.prevTouch2 = { x: evt.touches[1].pageX, y: evt.touches[1].pageY }
|
||||
this.prevTouchDist = this.#getTouchDistance(this.prevTouch1, this.prevTouch2)
|
||||
} else {
|
||||
return
|
||||
}
|
||||
evt.preventDefault()
|
||||
evt.stopPropagation()
|
||||
}
|
||||
|
||||
onTouchEnd = () => {
|
||||
this.prevTouch1 = null
|
||||
this.prevTouch2 = null
|
||||
this.prevTouchDist = null
|
||||
}
|
||||
|
||||
onTouchMove = (evt: React.TouchEvent) => {
|
||||
if (!this.ref.current) {
|
||||
return
|
||||
}
|
||||
if (evt.touches.length > 0 && this.prevTouch1) {
|
||||
this.translate.x += evt.touches[0].pageX - this.prevTouch1.x
|
||||
this.translate.y += evt.touches[0].pageY - this.prevTouch1.y
|
||||
this.prevTouch1 = { x: evt.touches[0].pageX, y: evt.touches[0].pageY }
|
||||
if (evt.touches.length === 1) {
|
||||
this.ref.current.style.translate = this.style.translate
|
||||
this.ref.current.style.cursor = "grabbing"
|
||||
}
|
||||
}
|
||||
if (evt.touches.length > 1 && this.prevTouch1 && this.prevTouch2 && this.prevTouchDist) {
|
||||
this.prevTouch2 = { x: evt.touches[1].pageX, y: evt.touches[1].pageY }
|
||||
const newDist = this.#getTouchDistance(this.prevTouch1, this.prevTouch2)
|
||||
const midpoint = this.#getTouchMidpoint(
|
||||
{ x: evt.touches[0].clientX, y: evt.touches[0].clientY },
|
||||
{ x: evt.touches[1].clientX, y: evt.touches[1].clientY },
|
||||
)
|
||||
this.#doZoom((newDist - this.prevTouchDist) / 100, midpoint.x, midpoint.y, true)
|
||||
this.prevTouchDist = newDist
|
||||
const style = this.style
|
||||
this.ref.current.style.translate = style.translate
|
||||
this.ref.current.style.scale = style.scale
|
||||
}
|
||||
evt.preventDefault()
|
||||
}
|
||||
|
||||
onKeyDown = (evt: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
const key = keyToString(evt)
|
||||
if (key === "Escape") {
|
||||
|
@ -203,6 +293,10 @@ export class Lightbox extends Component<LightboxProps> {
|
|||
className="overlay dimmed lightbox"
|
||||
onClick={this.onClick}
|
||||
onMouseMove={isTouchDevice ? undefined : this.onMouseMove}
|
||||
onTouchStart={isTouchDevice ? this.onTouchStart : undefined}
|
||||
onTouchMove={isTouchDevice ? this.onTouchMove : undefined}
|
||||
onTouchEnd={isTouchDevice ? this.onTouchEnd : undefined}
|
||||
onTouchCancel={isTouchDevice ? this.onTouchEnd : undefined}
|
||||
tabIndex={-1}
|
||||
onKeyDown={this.onKeyDown}
|
||||
ref={this.wrapperRef}
|
||||
|
|
Loading…
Add table
Reference in a new issue