【发布时间】:2019-08-25 11:39:38
【问题描述】:
我正在尝试为 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
- compute Y_COUNT = offset.y / YSTEP 为我们提供了移动箭头的步数
- 计算组件的新用户值,以 Y_COUNT 的整数部分计数(Math.floor 或 Math.ceil)
- 计算箭头位置的新 Y_VALUE 并将 iit 绑定到 SVG 标记上
问题:
我可以观察到鼠标指针位置和箭头的有效计算位置之间的滞后,所以鼠标指针和箭头位置不会粘在一起!
我尝试过各种数字刻度
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>
【问题讨论】:
-
请附上minimal reproducible example,以便我们查看问题所在。
-
@Paul :只需在您的应用程序中导入上面的完整代码并使用顶部导出的参数调用它(帖子中的示例),我认为这并不难。 Svelte 3 运行时是否有一种小提琴/REPL?我可以把代码粘贴进去……
-
获得答案的最佳方法是不要让人们不得不做额外的工作来让您的示例运行。
标签: javascript svg slider range svelte