移动数字范围滑块组件(SVG + Svelte)时鼠标滞后



我正在尝试为 Svelte 编写基于 SVG 的垂直数字范围滑块组件。

我的问题是关于转换的,尤其是SVG getScreenCTM()矩阵。

该组件设计为简单且可通过 props 自定义,如下所示:

<!-- user markup file .svelte -->
<SVGVerticalSlider 
title="100 units range"
value={50}
units=""
min={0}
max={100}
step={1}
major={10}
minor={2.5}
/>

它的工作原理是显示从 {min} 到 {max} 的范围,带有带有 {minor } 和 {major} 行的渐变轨道,并标记每条主要行,进度为 {step},并显示带有 {value + " + units} 的标记可拖动箭头。

SVG 中使用的坐标系是 {YSTEP} 从 {YMIN} 到 {YMAX},因此我使用线性插值函数来计算值:

// linear interpolation function
const lerp = (Xa, Ya, Xb, Yb, x) => Ya + (x - Xa) * (Yb - Ya) / (Xb - Xa) 

箭头函数在 SVG 内部的组中绘制:

<g 
class="slider" 
transform={ 'translate(0,' + Y_VALUE + ')' } 
on:mousedown={DND.start}
on:mousemove={DND.move}
on:mouseup={DND.end}
on:mouseleave={DND.end}>
<!-- path_point  = simple fat arrow draw pointing on Y_VALUE at (0, 0) -->
<path d={path_points} style={ 'fill:' + bg + ';' }/>
<text x="10" y="3" class="fat-arrow">{ formatted_value } </text>
</g>

在 DND.move() 事件列表器中,如果单击位置,执行矩阵 CTM 操作,然后计算两个位置的差异,因此我可以计算 Y_COUNT = offset.y/YSTEP 将鼠标拖放到内联 SVG 上的步数。

所以反应式绑定是:

let formatted_value = 0  
$: formatted_value = units ? value.toFixed(2) + ' ' + units : value
$: if (init) {
Y_VALUE = lerp (min, YMIN, max, YMAX, value)   
init = false
} else {
// value += Y_COUNT * step  ...   
if (Y_COUNT > 0)
value += Math.floor (Y_COUNT) * step
else
value += Math.ceil(Y_COUNT) * step
// new value of the cursor in SVG coordinates:
Y_VALUE = lerp (min, YMIN, max, YMAX, value)
// range guards (max) 
if (value >= max) {
value = max
Y_VALUE = YMAX    
}
// range guards (min)
if (value <= min) {
value = min
Y_VALUE = YMIN
} 
}

这里做了什么?

  • 每次触发 move() 事件 ID 时记录鼠标位置
  • 两个连续的位置不同给了我偏移量。
  • 计算Y_COUNT = offset.y/YSTEP 为我们提供了移动箭头的步骤量
  • 计算组件的新用户值,以Y_COUNT的整数部分计数(Math.floor 或 Math.ceil)
  • 计算箭头位置的新Y_VALUE,并在 SVG 标记上绑定 IIT

问题:

我可以观察到鼠标指针位置和箭头的有效计算位置之间存在滞后,因此鼠标指针和箭头位置不会粘在一起!

我尝试过各种数字刻度

scale         min      max      step
----------------------------------------
normalized    1        100      1
big_money     0        350000   2500
percentage    0        5        0.1

滞后效应并不明显依赖于规模分布和

如何解决?

组件的 HTML + JS + Svelte 代码(200+ 行代码)

<!-- this is an attempt to build a nice SVG vertical numeric range slider for Svelte -->
<script>
import { onMount } from 'svelte'
export let title = 'default-title'
export let min = 0
export let max = 100
export let step = 1
export let major = 10
export let minor = 5
export let value = 50
export let units = '' // whatever: €, %, ...
// markers list
let markers = []
let majors = []
let minors = []
/**
COordinate Systèm
user (component) level      min      max      step     value    offset      count
(tranform lerp)
internal 0..100 range       YMIN     YMAX     YSTEP    Y_VALUE   Y_OFFSET  Y_COUNT    
SVG coordinates                                                    
(transfo-rm ScreenCTM)
client cords (mouse)                          client.step event clientY offset.y                    
*/  
// linear interpolation function
const lerp = (Xa, Ya, Xb, Yb, x) => Ya + (x - Xa) * (Yb - Ya) / (Xb - Xa) 
const XMIN = 0
const YMIN = 0
const XMAX = 50
const YMAX = 100
// initialisation phase flag
let init = true
// internal variable representing Y
let Y_VALUE = 0
// internam step
let YSTEP0 = lerp (min, YMIN, max, YMAX, step)
let YSTEP = 1.0 * (YMAX - YMIN) / ((max - min) /  step)
//  console.log (YSTEP)
//  console.log (YSTEP0)

// internal step count
let Y_COUNT = 0
// user scale steps count
let y_count = 0
// Y_OFFSET inside SVG coords
let Y_OFFSET = 0
let bg = 'yellow'
let yaxis = {
x1: 0, y1: 0,
x2: 0, y2: 95
}

let formatted_value = 0 
$: formatted_value = units ? value.toFixed(2) + ' ' + units : value
$: if (init) {
Y_VALUE = lerp (min, YMIN, max, YMAX, value)   
init = false
} else {
let yamount = 0    
// without Math.floor or Math.ceil, it simply would be:
// value += Y_COUNT * step   
// but in fact we're doing step-by-step:
if (Y_COUNT > 0) {
// if (Y_COUNT < 1) Y_COUNT = 1
yamount = Math.ceil (Y_COUNT) * step
} else {
// if (Y_COUNT > -1) Y_COUNT = -1
yamount = Math.floor (Y_COUNT) * step
}
value +=  yamount
Y_VALUE = lerp (min, YMIN, max, YMAX, value)
if (value >= max) {
value = max
Y_VALUE = YMAX    
}

if (value <= min) {
value = min
Y_VALUE = YMIN
} 
}
//  example path: 
let path_points = 'M0,0 L10,-8 L50,-8 L50,8 L10,8 Z'
// Drag'n DRop is kept inside a closure (IIFE)
// so internal vriables are kept outside  (theoretically) of Svelte's binding mechansim
const DND = (() => {
let svg = null
let CTM = null
let ICTM = null
let stepSVGPoint = null
let selected = null
let offset = { x:0,   y: 0}
let position = { x:0, y: 0}
// adapted from http://www.petercollingridge.co.uk/tutorials/svg/interactive/dragging/
const getMousePosition = (event) => {
if (event.touches) { event = event.touches[0]; }
let transformed = {
x: (event.clientX - CTM.e) / CTM.a,
y: (event.clientY - CTM.f) / CTM.d 
}
return transformed
}
const findSVGRoot = (element) => {
const root = element.parentElement.parentElement
return root
} 
const start = (event) => {
if (selected === null) {
selected = event.target
svg = findSVGRoot (selected)
CTM = svg.getScreenCTM( )
ICTM = CTM.inverse()
event.preventDefault ()
position = getMousePosition (event)
offset = { x:0, y: 0 }
bg = 'green'

}
}
const move = (event) => {
// console.log (svg)
if (selected) {
event.preventDefault ()
const npos = getMousePosition (event)
offset.x = (npos.x - position.x)
offset.y = (npos.y - position.y)
Y_COUNT = (offset.y / YSTEP) 
// hold ancient position
position = npos
bg = 'red' 
console.clear ()
console.log ('offset.y', offset.y)
console.log ('Y_COUNT', Y_COUNT)
//     console.log ('offset.y', offset.y)
}
}
const end = (event) => {
event.preventDefault ()
selected = null
offset = null
position = null
bg = 'yellow'
}
const click = (event) => {    }
return { init, start, move, end, click }
}) ()

onMount (() => {
let major_step = min
let minor_step = min
while (minor_step <= max) {
const ym = lerp (min, YMIN, max, YMAX, minor_step)
minors [ minors.length] = {
x1: 0,   y1: ym,
x1: 2.5, y2: ym, 
}
minor_step += minor
}
while (major_step <= max) {
const ym = lerp (min, YMIN, max, YMAX, major_step) 
majors [ majors.length] = {
x1: -5,   y1: ym,
x1: 7.5,  y2: ym, 
}
markers [ markers.length] = {
x: 10,
y: ym,
label: major_step
}
major_step += major
}
})

</script>
<h1> #{ title }</h1>
<svg 
xmlns="http://www.w3.org/2000/svg" 
viewBox="-5 -15 55 125" 
width="120" height="280"
on:click={DND.click}>
<g class="ruler">
<line 
x1="{ yaxis.x1 }" y1="{ yaxis.y1 }" 
x2="{ yaxis.x2 }" y2="{ yaxis.y2 }" 
/>
{#each majors as maj }
<line x1={maj.x1} y1={maj.y1} x2={maj.x2} y2={maj.y2} />
{/each}
{#each markers as mm_text }
<text x="{ mm_text.x }" y="{ mm_text.y }"> { mm_text.label} </text>
{/each}
{#each minors as min }
<line x1={min.x1} y1={min.y1} x2={min.x2} y2={min.y2} />
{/each}
</g>
<g 
class="slider" 
transform={ 'translate(0,' + (Y_VALUE) + ')'} 
on:mousedown={DND.start}
on:mousemove={DND.move}
on:mouseup={DND.end}
on:mouseleave={DND.end}>
<path d={path_points} style={ 'fill:' + bg + ';' }/>
<text x="10" y="3" class="fat-arrow">{ formatted_value } </text>
</g>
</svg>
<p> Y_COUNT (internal): { Y_COUNT.toFixed(3)} </p>
<p> YSTEP (internal): { YSTEP.toFixed(3)} </p>
<p>Y_VALUE (internal): { Y_VALUE.toFixed(3)} </p>
<p>value: { value.toFixed(3)} </p>
<style>
svg {
background-color: lightgrey;
}
line {
stroke-width: 0.5;
stroke: rgb(0, 0, 255);
}
text {
font-size: 0.35em;
color: rgb(0, 0, 255);
}
text.fat-arrow {
font-size: 0.55em;
color: rgb(192, 48, 16);
font-weight: bold;
cursor: pointer;
}
path {
fill: rgb(255, 255, 64 );
stroke: black;
stroke-width: 0.5;
cursor: pointer;
}
</style>

最后我自己发现:我误会了数学...

说,我有用户输入的边界和步骤,并且同样转换为 SVG 坐标:

user input      SVG
------------------------
min: 0          YMIN: 0
max: 10000      YMAX: 100
step: 100       YSTEP: ?

计算YSTEP的正确方法不是:

// NO !!!
YSTEP = lerp (min, YMIN, max, YMAX, step)

。因为我之前决定上下翻转滑块刻度,而是:

// YES !!!
YSTEP = step * (YMAX - YMIN) / (max - min)

这使我能够!!有积极(或消极)的步骤

所以我可以关闭问题。对我来说一切都很好,里加德;-)

最新更新