Add overscroll
This commit is contained in:
@@ -19,7 +19,9 @@ export function tween(start: number, end: number, duration: number) {
|
|||||||
const te = t - ts
|
const te = t - ts
|
||||||
const next = Math.round(ease(te, start, end - start, duration))
|
const next = Math.round(ease(te, start, end - start, duration))
|
||||||
if (
|
if (
|
||||||
(end > start ? next < end && last <= end : next > end && last >= end) &&
|
(end > start
|
||||||
|
? next < end && last <= end
|
||||||
|
: next > end && last >= end) &&
|
||||||
te <= duration
|
te <= duration
|
||||||
) {
|
) {
|
||||||
frame = tick(next)
|
frame = tick(next)
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ const FLICK_MIN_VELOCITY = 0.1
|
|||||||
const ADVANCE_THRESHOLD = 0.15
|
const ADVANCE_THRESHOLD = 0.15
|
||||||
const FRAME_MS = 1000 / 60
|
const FRAME_MS = 1000 / 60
|
||||||
const SETTLE_DURATION = 600
|
const SETTLE_DURATION = 600
|
||||||
|
const OVERSCROLL_RESISTANCE = 0.3
|
||||||
|
const BOUNCE_DURATION = 400
|
||||||
|
|
||||||
function whichByDistance(
|
function whichByDistance(
|
||||||
itemWidths: Map<number, number>,
|
itemWidths: Map<number, number>,
|
||||||
@@ -70,9 +72,15 @@ export function usePointerHandlers({
|
|||||||
let t = 0
|
let t = 0
|
||||||
let stopTween: (() => void) | null = null
|
let stopTween: (() => void) | null = null
|
||||||
let localIndex = currentIndexRef.current
|
let localIndex = currentIndexRef.current
|
||||||
|
let overscrollX = 0
|
||||||
|
|
||||||
el.style.cursor = 'grab'
|
el.style.cursor = 'grab'
|
||||||
|
|
||||||
|
const clearOverscroll = () => {
|
||||||
|
overscrollX = 0
|
||||||
|
el.style.transform = ''
|
||||||
|
}
|
||||||
|
|
||||||
const onMouseDown = (e: MouseEvent) => {
|
const onMouseDown = (e: MouseEvent) => {
|
||||||
e.preventDefault() // prevent native image drag
|
e.preventDefault() // prevent native image drag
|
||||||
|
|
||||||
@@ -81,6 +89,7 @@ export function usePointerHandlers({
|
|||||||
stopTween()
|
stopTween()
|
||||||
stopTween = null
|
stopTween = null
|
||||||
}
|
}
|
||||||
|
clearOverscroll()
|
||||||
|
|
||||||
isMouseDown = true
|
isMouseDown = true
|
||||||
isDragging = false
|
isDragging = false
|
||||||
@@ -121,19 +130,38 @@ export function usePointerHandlers({
|
|||||||
velo = (delta - prevDelta) / (elapsed * FRAME_MS)
|
velo = (delta - prevDelta) / (elapsed * FRAME_MS)
|
||||||
t = e.timeStamp
|
t = e.timeStamp
|
||||||
|
|
||||||
scrollTo(dragScrollLeft - delta)
|
const desiredScroll = dragScrollLeft - delta
|
||||||
|
const maxScroll = el.scrollWidth - el.clientWidth
|
||||||
|
|
||||||
// Update local index from scroll position
|
if (desiredScroll < 0) {
|
||||||
const offsetX = dragScrollLeft - delta
|
// Overscroll at start — rubber band
|
||||||
let accumulated = 0
|
scrollTo(0)
|
||||||
for (let i = 0; i < imageCount; i++) {
|
overscrollX = desiredScroll * OVERSCROLL_RESISTANCE
|
||||||
const w = (itemWidthsRef.current.get(i) ?? 0) + ITEM_GAP
|
el.style.transform = `translateX(${-overscrollX}px)`
|
||||||
if (offsetX < accumulated + w / 2) {
|
} else if (desiredScroll > maxScroll) {
|
||||||
localIndex = i
|
// Overscroll at end — rubber band
|
||||||
break
|
scrollTo(maxScroll)
|
||||||
|
overscrollX = (desiredScroll - maxScroll) * OVERSCROLL_RESISTANCE
|
||||||
|
el.style.transform = `translateX(${-overscrollX}px)`
|
||||||
|
} else {
|
||||||
|
// Normal scroll range
|
||||||
|
scrollTo(desiredScroll)
|
||||||
|
if (overscrollX !== 0) clearOverscroll()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update local index from scroll position (only in normal range)
|
||||||
|
if (overscrollX === 0) {
|
||||||
|
const offsetX = desiredScroll
|
||||||
|
let accumulated = 0
|
||||||
|
for (let i = 0; i < imageCount; i++) {
|
||||||
|
const w = (itemWidthsRef.current.get(i) ?? 0) + ITEM_GAP
|
||||||
|
if (offsetX < accumulated + w / 2) {
|
||||||
|
localIndex = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
accumulated += w
|
||||||
|
if (i === imageCount - 1) localIndex = i
|
||||||
}
|
}
|
||||||
accumulated += w
|
|
||||||
if (i === imageCount - 1) localIndex = i
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,47 +182,67 @@ export function usePointerHandlers({
|
|||||||
capture: true,
|
capture: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Estimate resting distance from velocity
|
if (overscrollX !== 0) {
|
||||||
let v = Math.abs(velo)
|
// Bounce back from overscroll
|
||||||
let restingDistance = 0
|
const targetIndex = overscrollX > 0 ? imageCount - 1 : 0
|
||||||
while (v > FLICK_MIN_VELOCITY) {
|
const fromOverscroll = overscrollX
|
||||||
v *= FLICK_DECAY
|
|
||||||
restingDistance += v
|
|
||||||
}
|
|
||||||
|
|
||||||
const direction: -1 | 1 = delta < 0 ? -1 : 1
|
stopTween = tween(
|
||||||
const totalDistance = Math.abs(delta) + restingDistance
|
fromOverscroll,
|
||||||
|
0,
|
||||||
|
BOUNCE_DURATION,
|
||||||
|
)(
|
||||||
|
v => {
|
||||||
|
el.style.transform = `translateX(${-v}px)`
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
stopTween = null
|
||||||
|
clearOverscroll()
|
||||||
|
onSettle(targetIndex)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// Normal flick settle
|
||||||
|
let v = Math.abs(velo)
|
||||||
|
let restingDistance = 0
|
||||||
|
while (v > FLICK_MIN_VELOCITY) {
|
||||||
|
v *= FLICK_DECAY
|
||||||
|
restingDistance += v
|
||||||
|
}
|
||||||
|
|
||||||
const targetIndex = whichByDistance(
|
const direction: -1 | 1 = delta < 0 ? -1 : 1
|
||||||
itemWidthsRef.current,
|
const totalDistance = Math.abs(delta) + restingDistance
|
||||||
localIndex,
|
|
||||||
totalDistance,
|
|
||||||
direction,
|
|
||||||
imageCount,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Tween from current scroll position to target
|
const targetIndex = whichByDistance(
|
||||||
const from = el.scrollLeft
|
itemWidthsRef.current,
|
||||||
const to = getOffsetForIndex(itemWidthsRef.current, targetIndex)
|
localIndex,
|
||||||
|
totalDistance,
|
||||||
|
direction,
|
||||||
|
imageCount,
|
||||||
|
)
|
||||||
|
|
||||||
if (from === to) {
|
const from = el.scrollLeft
|
||||||
onSettle(targetIndex)
|
const to = getOffsetForIndex(itemWidthsRef.current, targetIndex)
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
stopTween = tween(
|
if (from === to) {
|
||||||
from,
|
|
||||||
to,
|
|
||||||
SETTLE_DURATION,
|
|
||||||
)(
|
|
||||||
v => {
|
|
||||||
scrollTo(v)
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
stopTween = null
|
|
||||||
onSettle(targetIndex)
|
onSettle(targetIndex)
|
||||||
},
|
return
|
||||||
)
|
}
|
||||||
|
|
||||||
|
stopTween = tween(
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
SETTLE_DURATION,
|
||||||
|
)(
|
||||||
|
v => {
|
||||||
|
scrollTo(v)
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
stopTween = null
|
||||||
|
onSettle(targetIndex)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,6 +255,7 @@ export function usePointerHandlers({
|
|||||||
window.removeEventListener('mousemove', onMouseMove)
|
window.removeEventListener('mousemove', onMouseMove)
|
||||||
window.removeEventListener('mouseup', onMouseUp)
|
window.removeEventListener('mouseup', onMouseUp)
|
||||||
if (stopTween) stopTween()
|
if (stopTween) stopTween()
|
||||||
|
clearOverscroll()
|
||||||
el.style.cursor = ''
|
el.style.cursor = ''
|
||||||
el.style.userSelect = ''
|
el.style.userSelect = ''
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user