diff --git a/demo_drag/index.html b/demo_drag/index.html
index d0c28e4..c4dbd07 100644
--- a/demo_drag/index.html
+++ b/demo_drag/index.html
@@ -26,10 +26,9 @@
// === generic scheduler & its debugger
let scheduledRender = false
-function scheduleRender(debugForceRender) {
+function scheduleRender() {
if (scheduledRender) return;
scheduledRender = true
-
requestAnimationFrame(function renderAndMaybeScheduleAnotherRender(now) { // eye-grabbing name. No "(anonymous)" function in the debugger & profiler
scheduledRender = false
if (render(now)) scheduleRender()
@@ -75,11 +74,10 @@
let animatedUntilTime = null
let dragged = null
let lastDragged = null
-let inputs = {
- /** @type 'down' | 'up' | 'firstDown' */
- pointerState: 'up',
- pointer: [{x: 0, y: 0, time: 0}] // circular buffer. Btw, on page load, there's no way to render a first cursor state =(
-}
+/** @type 'down' | 'up' | 'firstDown' */
+let pointerState = 'up'
+let pointer = [{x: 0, y: 0, time: 0}] // circular buffer. Btw, on page load, there's no way to render a first cursor state =(
+let events = { mouseup: null, touchend: null, mousemove: null, touchmove: null, pointerdown: null }
let data = []; {
const windowSizeX = document.documentElement.clientWidth // excludes scroll bar & invariant under safari pinch zoom
for (let i = 0; i < 5; i++) {
@@ -112,30 +110,11 @@
// === events
// pointermove doesn't work on android, pointerdown isn't fired on Safari on the first left click after dismissing context menus, mousedown doesn't trigger properly on mobile, pointerup isn't triggered when pointer panned (at least on iOS), don't forget contextmenu event. Tldr there's no pointer event that works cross-browser that can replace mouse & touch events.
window.addEventListener('resize', () => scheduleRender())
-window.addEventListener("mouseup", (e) => {
- inputs.pointerState = 'up'
- scheduleRender()
-})
-window.addEventListener("touchend", (e) => {
- inputs.pointerState = 'up'
- scheduleRender()
-})
-window.addEventListener("mousemove", (e) => {
- // when scrolling (which might schedule a render), a container's pointermove doesn't trigger, so the pointer's local coordinates are stale
- // this means we should only use pointer's global coordinates, which is always right
- inputs.pointer.push({x: e.pageX, y: e.pageY, time: performance.now()})
- // btw, pointer can exceed document bounds, e.g. dragging reports back out-of-bound, legal negative values
- scheduleRender()
-})
-window.addEventListener("touchmove", (e) => {
- inputs.pointer.push({x: e.touches[0].pageX, y: e.touches[0].pageY, time: performance.now()})
- scheduleRender()
-})
-window.addEventListener('pointerdown', (e) => {
- inputs.pointerState = 'firstDown'
- inputs.pointer.push({x: e.pageX, y: e.pageY, time: performance.now()})
- scheduleRender()
-})
+window.addEventListener("mouseup", (e) => {events.mouseup = e; scheduleRender()})
+window.addEventListener("touchend", (e) => {events.touchend = e; scheduleRender()})
+window.addEventListener("mousemove", (e) => {events.mousemove = e; scheduleRender()})
+window.addEventListener("touchmove", (e) => {events.touchmove = e; scheduleRender()})
+window.addEventListener('pointerdown', (e) => {events.pointerdown = e; scheduleRender()})
// === hit testing logic. Boxes' hit area should be static and not follow their current animated state usually (but we can do either). Use the dynamic area here for once
function hitTest(data, pointer) {
@@ -146,16 +125,29 @@
}
function render(now) {
+ // === step 0: process events
+ // mouseup/touchend
+ if (events.mouseup || events.touchend) pointerState = 'up'
+ // move
+ // when scrolling (which might schedule a render), a container's pointermove doesn't trigger, so the pointer's local coordinates are stale
+ // this means we should only use pointer's global coordinates, which is always right
+ if (events.mousemove) pointer.push({x: events.mousemove.pageX, y: events.mousemove.pageY, time: performance.now()})
+ if (events.touchmove) pointer.push({x: events.touchmove.touches[0].pageX, y: events.touchmove.touches[0].pageY, time: performance.now()})
+ // down
+ if (events.pointerdown) {
+ pointerState = 'firstDown'
+ pointer.push({x: events.pointerdown.pageX, y: events.pointerdown.pageY, time: performance.now()})
+ }
+
// === step 1: batched DOM reads (to avoid accidental DOM read & write interleaving)
const windowSizeX = document.documentElement.clientWidth // excludes scroll bar & invariant under safari pinch zoom
- let {pointer} = inputs
const pointerLast = pointer.at(-1) // guaranteed non-null since pointer.length >= 1
// === step 2: handle inputs-related state change
let newDragged
let releaseVelocity = null
- if (inputs.pointerState === 'down') newDragged = dragged
- else if (inputs.pointerState === 'up') {
+ if (pointerState === 'down') newDragged = dragged
+ else if (pointerState === 'up') {
if (dragged != null) {
let dragIdx = data.findIndex(d => d.id === dragged.id)
let i = pointer.length - 1; while (i >= 0 && now - pointer[i].time <= 100) i-- // only consider last ~100ms of movements
@@ -236,12 +228,13 @@
document.body.style.cursor = cursor
// === step 6: update state & prepare for next frame
- if (inputs.pointerState === 'firstDown') inputs.pointerState = 'down'
+ if (pointerState === 'firstDown') pointerState = 'down'
if (dragged && newDragged == null) lastDragged = dragged
dragged = newDragged
animatedUntilTime = stillAnimating ? newAnimatedUntilTime : null
- if (inputs.pointerState === 'up') inputs.pointer = [{x: 0, y: 0, time: 0}]
- if (inputs.pointer.length > 20) inputs.pointer.shift() // keep last ~20
+ if (pointerState === 'up') pointer = [{x: 0, y: 0, time: 0}]
+ if (pointer.length > 20) pointer.shift() // keep last ~20
+ events.mouseup = events.touchend = events.mousemove = events.touchmove = events.pointerdown = null
return stillAnimating
}