javascript 拖放
Recently, I wrote an article about an implementation of drag and drop functionality using vanilla JavaScript. This time, I want to apply linear interpolation for the dragging logic, such that the draggable object smoothly “catches up” with the user’s cursor/touchpoint as opposed to immediately following it:
最近,我写了一篇有关使用香草JavaScript实现拖放功能的文章。 这次,我想对拖动逻辑应用线性插值,以使可拖动对象与用户的光标/接触点平滑地“捕捉”,而不是紧随其后:
Without linear Interpolation. 没有线性插值。 With linear interpolation. 带线性插补。Linear interpolation is a way to project data between known states. In the case of drag and drop functionality, it can be used to create the coordinates between the current and future position of a draggable element so that we can create a smooth transition between these two states. The basic function that we are going to use is:
线性插值是在已知状态之间投影数据的一种方式。 在拖放功能的情况下,它可用于在可拖动元素的当前位置和将来位置之间创建坐标,以便我们可以在这两个状态之间创建平滑过渡。 我们将使用的基本功能是:
function lerp(start, end, amt) { return (1-amt)*start+amt*end};start — represents the starting state (number)
start —代表开始状态(数字)
end — the end state (number)
end —结束状态(数字)
amt — amount to interpolate between the two (0.0 to 1.0)
amt两者之间要插补的数量(0.0到1.0)
So if our starting coordinate is 100, the end state coordinate is 200, and the amount we want to interpolate for each cycle of redraw (~frame) is 0.1, then:
因此,如果我们的起始坐标为100,结束状态坐标为200,并且每个重绘周期(〜帧)要插值的数量为0.1,则:
Frame 1 = (1-0.1)*100 + 0.1*200 = 110;Frame 2 = (1-0.1)*110 + 0.1*200 = 119;Frame 2 = (1-0.1)*119 + 0.1*200 = 127.1;...Frame N = (1-0.1)*199 + 0.1*200 = 199.1;Now if we think of a drag and drop use case, the function above creates a succession of coordinates that the dragged element would need to transition through.
现在,如果我们想到一个拖放用例,上面的函数将创建一连串的坐标,被拖动的元素将需要过渡。
Here is the initial code that attaches the listeners we need for basic drag and drop functionality:
以下是附加了基本拖放功能所需的侦听器的初始代码:
<div class="container"> <div style="top: 100px; left: 100px" class="box"></div> <div style="top: 200px; left: 200px" class="box"></div> <div style="top: 300px; left: 300px" class="box"></div> <div style="top: 400px; left: 400px" class="box"></div> </div> .container { width: 600px; height: 600px; background-color: darkgrey; touch-action: none; user-select: none; } .box { position: absolute; width: 100px; height: 100px; background-color: black; }The above renders the container that will have event listeners attached to it and the boxes that will become draggable later on. Now we will add the JavaScript that works drag and drop magic without actually moving anything yet:
上面的代码呈现了将附加事件侦听器的容器以及以后将变得可拖动的框。 现在,我们将添加可以拖放魔术而实际上并未移动任何东西JavaScript:
const container = document.querySelector('.container'); container.addEventListener('pointerdown', userPressed, { passive: true }); var element, bbox, inputX, inputY, boxCenterX, boxCenterY, raf; function userPressed(event) { element = event.target; if (element.classList.contains('box')) { console.log("picked up!") container.addEventListener('pointermove', userMoved, { passive: true }); container.addEventListener('pointerup', userReleased, { passive: true }); container.addEventListener('pointercancel', userReleased, { passive: true }); }; }; function userMoved(event) { console.log("dragging!") }; function userReleased(event) { console.log("dropped!") container.removeEventListener('pointermove', userMoved); container.removeEventListener('pointerup', userReleased); container.removeEventListener('pointercancel', userReleased); };We are using Pointer Events, which provide a blend of touch and mouse input to support modern devices and cross-input.
我们正在使用Pointer Events ,它提供了触摸和鼠标输入的混合,以支持现代设备和交叉输入。
We are attaching passive listeners and doing so dynamically to prevent event pollution, so only required listeners will be active at a time. 我们将附加被动侦听器,并动态地这样做以防止事件污染,因此一次仅需要的侦听器将处于活动状态。Our CSS features touch-action and user-select rules to make sure we do not trigger any default browser behaviors (scroll, native drag and drop, etc).
我们CSS具有touch-action和user-select规则,以确保我们不会触发任何默认浏览器行为(滚动,本机拖放等)。
We also take care of disrupted touch events using the pointercancel listener and treating it the same as pointerup.
我们还使用pointercancel侦听器来处理中断的触摸事件,并将其与pointerup相同。
With the user experience in mind, we need to assume that interpolation should become “active” after the user presses down their pointer and then becomes “inactive” after the user releases their pointer. So something needs to call our LERP (=Linear Interpolation) function continuously between pointerdown and pointerup/pointercancel events.
考虑到用户的体验,我们需要假设插值应该在用户按下指针后变为“活动”,然后在用户释放指针后变为“不活动”。 因此,需要在pointerdown和pointerdown之间连续调用我们的LERP(=线性插值)函数 pointerup/pointercancel事件。
We could leverage setInterval of course, but the better candidate would be RequestAnimationFrame API, as it gives us a precise 60fps (1000ms/60=~16.7ms) rendering frame timer.
当然,我们可以利用setInterval ,但是更好的选择是RequestAnimationFrame API ,因为它为我们提供了精确的60fps(1000ms / 60 =〜16.7ms)渲染帧计时器。
Let’s implement this part:
让我们实现这一部分:
const container = document.querySelector('.container'); container.addEventListener('pointerdown', userPressed, { passive: true }); var element, raf; function userPressed(event) { element = event.target; if (element.classList.contains('box')) { console.log("picked up!") container.addEventListener('pointermove', userMoved, { passive: true }); container.addEventListener('pointerup', userReleased, { passive: true }); container.addEventListener('pointercancel', userReleased, { passive: true }); raf = requestAnimationFrame(userMovedRaf); }; }; function userMoved(event) { console.log("dragging!") }; function userMovedRaf() { console.log("calling LERP") raf = requestAnimationFrame(userMovedRaf) }; function userReleased(event) { console.log("dropped!") container.removeEventListener('pointermove', userMoved); container.removeEventListener('pointerup', userReleased); container.removeEventListener('pointercancel', userReleased); if (raf) { cancelAnimationFrame(raf); raf = null; }; }; function lerp (start, end, amt) { return (1-amt)*start+amt*end };Now all our methods get called properly and we can implement logic that allows us to redraw the box using interpolated coordinates:
现在,我们所有的方法都被正确调用,并且我们可以实现允许使用插值坐标重绘框的逻辑:
First, we need to make sure on pointerdown and pointermove events that we start getting input coordinates (inputX, inputY) that will be used at each animation frame by our LERP function.
首先,我们需要确保 pointerdown 和 pointermove 我们开始获取输入坐标( inputX , inputY )的事件,这些事件将由我们的LERP函数在每个动画帧处使用。
Next, we need to make sure we also capture a client bounding rectangle that we will use to calculate the center point of our draggable box, ensuring it is the box’s center that is “catching up” with the user’s pointer and not just the top left corner.
接下来,我们需要确保还捕获了一个客户端边界矩形,该矩形将用于计算可拖动框的中心点,确保它是框的中心与用户的指针一起“捕捉”,而不仅仅是左上角角。
The full implementation is below:
完整的实现如下:
const container = document.querySelector('.container'); container.addEventListener('pointerdown', userPressed, { passive: true }); var element, bbox, inputX, inputY, boxCenterX, boxCenterY, raf; function userPressed(event) { element = event.target; if (element.classList.contains('box')) { inputX = event.clientX; inputY = event.clientY; bbox = element.getBoundingClientRect(); boxCenterX = bbox.left + bbox.width/2; boxCenterY = bbox.top + bbox.height/2; container.addEventListener('pointermove', userMoved, { passive: true }); container.addEventListener('pointerup', userReleased, { passive: true }); container.addEventListener('pointercancel', userReleased, { passive: true }); raf = requestAnimationFrame(userMovedRaf); }; }; function userMoved(event) { inputX = event.clientX; inputY = event.clientY; }; function userMovedRaf() { let x = lerp(boxCenterX, inputX, 0.05); let y = lerp(boxCenterY, inputY, 0.05); element.style.left = x - bbox.width/2 + "px"; element.style.top = y - bbox.width/2 + "px"; boxCenterX = x; boxCenterY = y; raf = requestAnimationFrame(userMovedRaf) }; function userReleased(event) { container.removeEventListener('pointermove', userMoved); container.removeEventListener('pointerup', userReleased); container.removeEventListener('pointercancel', userReleased); if (raf) { cancelAnimationFrame(raf); raf = null; }; }; function lerp (start, end, amt) { return (1-amt)*start+amt*end };We have just implemented a different kind of drag and drop experience in pure vanilla JavaScript by leveraging basic linear interpolation:
通过利用基本的线性插值,我们刚刚在纯香草JavaScript中实现了另一种拖放体验:
We used the modern Pointer Events API. 我们使用了现代的Pointer Events API。Instead of setInterval, we leveraged the RequestAnimationFrame API.
代替setInterval ,我们利用了RequestAnimationFrame API。
Linear interpolation is a great way to create a smooth dragging experience for your users — especially in cases where you need to make sure the user’s cursor/touch input point is not always blocked by the draggable element.
线性插值是一种为用户提供流畅的拖动体验的好方法,尤其是在需要确保用户的光标/触摸输入点不总是被可拖动元素阻挡的情况下。
The full demo is available via codepen.io:
完整的演示可通过codepen.io获得:
演示地址
Thank you for reading!
感谢您的阅读!
翻译自: https://medium.com/better-programming/drag-and-drop-with-linear-interpolation-in-javascript-9e5dc779bc23
javascript 拖放