在SVG中,可拖动的<text>存在问题,光标移动速度比被拖动的文本更快。

huangapple go评论140阅读模式
英文:

Draggable <text> in SVG is buggy, cursor moves faster than the text being dragged

问题

当我们点击拖动文本时,鼠标在每个方向上移动得比文本快大约5%。这导致了在文本拖动时出现了问题。

这个问题似乎与<svg>中的viewBox有关。我们正在寻找一个解决方案,保持<svg>中的viewBoxwidth: 100%不变,以及<D3BarChart> 返回的<div width='60%'>,因为这些是我们的图表在网页应用中创建和显示的关键部分。

也许有一种方法可以在<DraggableText />中调整viewBox的值?

英文:

We have the following code sample:

<!-- begin snippet: js hide: false console: true babel: true -->

<!-- language: lang-js -->

function DraggableText({ x, y, text }) {
    const [position, setPosition] = React.useState({ x: x, y: y });
    const [isDragging, setIsDragging] = React.useState(false);
    const [mouseOffset, setMouseOffset] = React.useState({ x: 0, y: 0 });
  
    const handleMouseDown = (event) =&gt; {
      event.preventDefault();
      setIsDragging(true);
      setMouseOffset({
        x: event.clientX - position.x,
        y: event.clientY - position.y,
      });
    };
  
    const handleMouseUp = (event) =&gt; {
      setIsDragging(false);
    };
  
    const handleMouseMove = (event) =&gt; {
      if (isDragging) {
        setPosition({
          x: event.clientX - mouseOffset.x,
          y: event.clientY - mouseOffset.y,
        });
      }
    };
  
    return (
      &lt;text
        x={position.x}
        y={position.y}
        onMouseDown={handleMouseDown}
        onMouseUp={handleMouseUp}
        onMouseMove={handleMouseMove}
        style={{ cursor: &#39;move&#39; }}
      &gt;
        {text}
      &lt;/text&gt;
    );
  }
  
  
 function D3BarChart({ }) {
   
    // And Finally, Return!
    return (
        &lt;svg
            className=&#39;cbb-box-shadowed&#39;
            width=&#39;100%&#39;
            viewBox={`0 0 700 450`}
            preserveAspectRatio=&#39;xMaxYMax&#39;
            style={{ background: &#39;#F2F2F2&#39; }}
        &gt;
            &lt;DraggableText x={100} y={100} text=&quot;Drag me!&quot; /&gt;
        &lt;/svg&gt;
    );
}

// render both components

ReactDOM.render(
  (&lt;div&gt;
    &lt;div width=&#39;60%&#39;&gt;
      &lt;D3BarChart /&gt;
    &lt;/div&gt;
  &lt;/div&gt;),
  document.querySelector(&#39;#root&#39;));

<!-- language: lang-html -->

&lt;script src=&quot;https://cdnjs.cloudflare.com/ajax/libs/react/16.14.0/umd/react.production.min.js&quot;&gt;&lt;/script&gt;
&lt;script src=&quot;https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.14.0/umd/react-dom.production.min.js&quot;&gt;&lt;/script&gt;


&lt;div id=&quot;root&quot;&gt;React component will be rendered here.&lt;/div&gt;

<!-- end snippet -->

When we click to drag the text, the mouse moves about 5% faster than the text in each direction. The result is a buggy experience using the text-drag.

The issue appears to be related to the viewBox in the &lt;svg&gt;. We are looking for a solution that keeps the viewBox and the width: 100% as is on the SVG, as well as the &lt;div width=&#39;60%&#39;&gt; that the D3BarChart is returned inside of, as these are key parts to how the graphs are created and displayed on our web application.

Perhaps there is a way to offset the viewBox value in the &lt;DraggableText /&gt;?

答案1

得分: 10

基本上我能看到的与我的组件(在面板上拖动手柄)相比的变化是

  • 将mousemove和mouseup添加到窗口(或容器),而不是元素
  • 将事件坐标缩放到父级
  • 对处理程序使用useCallback(),仅在依赖项更改时重建

使mousemove和mouseup具有更广泛的范围可以让文本“追赶”鼠标,如果你移动得比React处理事件更快的话。

否则,鼠标可能会与文本脱节,mousemove不再被处理。

function DraggableText({ x, y, text }) {
  // ...(你的组件代码)
}

function D3BarChart({ }) {
  // ...(你的组件代码)
}

ReactDOM.render(
  (<div>
    <div width='60%'>
      <D3BarChart />
    </div>
  </div>),
  document.querySelector('#root'));

更新

我注意到当在全屏模式下打开片段时,它不再正确跟踪。

这是由于scale在组件挂载时被设置,但如果你执行这个顺序

  • 拖动和鼠标松开

  • 调整窗口大小(在片段中选择“全屏”)

  • 再次拖动

然后第二次拖动时scale是错误的。

现在的代码已更新以处理窗口调整大小。

英文:

Basically the changes I can see compared to my own component (drag handles on a panel) are

  • add mousemove and mouseup to window (or container), not to the element
  • scaling the event coordinates to the parent
  • using useCallback() for handlers, to rebuild only on dependency changes

Giving mousemove and mouseup wider scope allows the text to "catch up" with the mouse if you move it quicker than React can process the events.

Otherwise the mouse can get dissociated from the text and mousemove is no longer processed.

<!-- begin snippet: js hide: false console: false babel: true -->

<!-- language: lang-js -->

function DraggableText({ x, y, text }) {
  const [position, setPosition] = React.useState({ x, y })
  const [isDragging, setIsDragging] = React.useState(false)
  const [mouseOffset, setMouseOffset] = React.useState({ x: 0, y: 0 })

  const textRef = React.useRef()
  const [scale, setScale] = React.useState({ x: 1, y: 1 })

  const calcScale = React.useCallback(() =&gt; {
    if (!textRef.current) return
    const parent = textRef.current.parentElement 
    const viewbox = parent.viewBox.baseVal;
    const rect = parent.getBoundingClientRect();
    const scale = {
      x: viewbox.width / Math.round(rect.width), 
      y: viewbox.height / Math.round(rect.height) 
    }
    return scale
  }, [])

  const handleMouseDown = React.useCallback(event =&gt; {
    event.preventDefault()
    setIsDragging(true)

    const scale = calcScale()
    setScale(scale)

    const newPosition = {
      x: (event.clientX * scale.x) - position.x,
      y: (event.clientY * scale.y) - position.y,
    }
    setMouseOffset(newPosition)
  }, [position, calcScale, scale])

  const handleMouseUp = React.useCallback(() =&gt; {
    if (!isDragging) return
    setIsDragging(false)
  }, [isDragging])

  const handleMouseMove = React.useCallback(event =&gt; {
    if (!isDragging) return
    setPosition({
      x: scale.x * event.clientX - mouseOffset.x,
      y: scale.y * event.clientY - mouseOffset.y,
    })
  }, [isDragging, mouseOffset, scale])

  // External listeners
  React.useEffect(() =&gt; {
    console.log(&#39;isDragging&#39;, isDragging)
    if (!isDragging) return
    window.addEventListener(&#39;mousemove&#39;, handleMouseMove)
    window.addEventListener(&#39;mouseup&#39;, handleMouseUp)
    return () =&gt; {
      window.removeEventListener(&#39;mousemove&#39;, handleMouseMove)
      window.removeEventListener(&#39;mouseup&#39;, handleMouseUp)
    };
  }, [isDragging, handleMouseMove, handleMouseUp])

  return (
    &lt;text
    ref={textRef}
      x={position.x}
      y={position.y}
      onMouseDown={handleMouseDown}
      // onMouseUp={handleMouseUp}
      // onMouseMove={handleMouseMove}
      style={{ cursor: &#39;move&#39; }}
    &gt;
      {text}
    &lt;/text&gt;
  )
} 
 
 function D3BarChart({ }) {
   
    // And Finally, Return!
    return (
        &lt;svg
            className=&#39;cbb-box-shadowed&#39;
            width=&#39;100%&#39;
            viewBox={`0 0 700 450`}
            preserveAspectRatio=&#39;xMaxYMax&#39;
            style={{ background: &#39;#F2F2F2&#39; }}
        &gt;
            &lt;DraggableText x={100} y={100} text=&quot;Drag me!&quot; /&gt;
        &lt;/svg&gt;
    );
}

// render both components

ReactDOM.render(
  (&lt;div&gt;
    &lt;div width=&#39;60%&#39;&gt;
      &lt;D3BarChart /&gt;
    &lt;/div&gt;
  &lt;/div&gt;),
  document.querySelector(&#39;#root&#39;));

<!-- language: lang-html -->

&lt;script src=&quot;https://cdnjs.cloudflare.com/ajax/libs/react/16.14.0/umd/react.production.min.js&quot;&gt;&lt;/script&gt;
&lt;script src=&quot;https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.14.0/umd/react-dom.production.min.js&quot;&gt;&lt;/script&gt;


&lt;div id=&quot;root&quot;&gt;React component will be rendered here.&lt;/div&gt;

<!-- end snippet -->

I also set position on mousedown which does away with mouseOffset state, but haven't made that change here in case there's something else going on that I didn't notice.

It would be a small optimization anyway.


Update

I notice when the snippet is opened in full-page mode, it's no longer tracking properly.

This is due to scale being set on component mount, but if you perform this sequence

  • drag and mouse up

  • resize the window (in snippet, select "full page")

  • drag again

then scale is wrong for the 2nd drag.

The code is now updated to handle window resize.

答案2

得分: 1

试试这段代码。

function DraggableText() {
  const [position, setPosition] = useState({
    x: 50,
    y: 50
  });

  const handleDragStart = (event) => {
    event.preventDefault();
    const startX = event.clientX - position.x;
    const startY = event.clientY - position.y;

    const handleMouseMove = (event) => {
      setPosition({
        x: event.clientX - startX,
        y: event.clientY - startY,
      });
    };

    const handleMouseUp = () => {
      document.removeEventListener("mousemove", handleMouseMove);
      document.removeEventListener("mouseup", handleMouseUp);
    };

    document.addEventListener("mousemove", handleMouseMove);
    document.addEventListener("mouseup", handleMouseUp);
  };

  return (
    <svg>
      <text x={position.x} y={position.y} onMouseDown={handleDragStart} style={{ cursor: "move" }}>
        Drag me!
      </text>
    </svg>
  );
}

export default DraggableText;
英文:

Try this code.

<!-- begin snippet: js hide: false console: true babel: false -->

<!-- language: lang-js -->

function DraggableText() {
  const [position, setPosition] = useState({
    x: 50,
    y: 50
  });

  const handleDragStart = (event) =&gt; {
    event.preventDefault();
    const startX = event.clientX - position.x;
    const startY = event.clientY - position.y;

    const handleMouseMove = (event) =&gt; {
      setPosition({
        x: event.clientX - startX,
        y: event.clientY - startY,
      });
    };

    const handleMouseUp = () =&gt; {
      document.removeEventListener(&quot;mousemove&quot;, handleMouseMove);
      document.removeEventListener(&quot;mouseup&quot;, handleMouseUp);
    };

    document.addEventListener(&quot;mousemove&quot;, handleMouseMove);
    document.addEventListener(&quot;mouseup&quot;, handleMouseUp);
  };

  return ( &lt;
    svg &gt;
    &lt;
    text x = {
      position.x
    }
    y = {
      position.y
    }
    onMouseDown = {
      handleDragStart
    }
    style = {
      {
        cursor: &quot;move&quot;
      }
    } &gt;
    Drag me!
    &lt;
    /text&gt; &lt;
    /svg&gt;
  );
}

export default DraggableText;

<!-- end snippet -->

答案3

得分: 1

你的代码的主要问题,正如你已经提到的,是SVG的宽度和SVG的视口宽度不同。

在你的情况下,SVG的宽度是100%,因此它是动态计算的,但它的视口宽度是700像素。如果你将它们都设置为相同的值,你可以避免这个问题,否则你需要进行一些数学计算。

为了简化起见,假设你有这样的标记:<svg width="300px" viewBox="0 0 600 ...。你的SVG将在屏幕上占据300像素。但由于你的SVG视口宽度为600像素,SVG在计算其内部元素的位置时会“认为”它是两倍大。这意味着如果你想在SVG中按照屏幕几何位置将一个元素定位到(100px, 100px),你需要将这些值乘以2,并将你的SVG元素位置设置为(200px, 200px)。

这个缩放因子(在此示例中为2)可以通过编程方式计算,读取SVG.viewBox属性并使用Element.getBoundingClientRect()获取屏幕上的实际当前图像大小,如下所示:

scale = svg.viewBox.baseVal.width / svg.getBoundingClientRect().width

在你的代码中,你应该将鼠标位置按照SVG的几何位置进行缩放,如下所示:

function DraggableText({ x, y, text }) {
    const [position, setPosition] = React.useState({ x: x, y: y });
    const [isDragging, setIsDragging] = React.useState(false);
    const [mouseOffset, setMouseOffset] = React.useState({ x: 0, y: 0 });
    const [scale, setScale] = React.useState(1);

    const handleMouseDown = (event) => {
        event.preventDefault();
        setIsDragging(true);
        const svg = event.target.parentElement;
        const vbox = svg.viewBox.baseVal;
        const rect = svg.getBoundingClientRect();
        const in_scale = vbox.width / rect.width;
        setScale(in_scale);
        setMouseOffset({
            x: (event.clientX * in_scale) - position.x,
            y: (event.clientY * in_scale) - position.y,
        });
    };

    const handleMouseUp = (event) => {
        setIsDragging(false);
    };

    const handleMouseMove = (event) => {
        if (isDragging) {
            setPosition({
                x: scale * event.clientX - mouseOffset.x,
                y: scale * event.clientY - mouseOffset.y,
            });
        }
    };

    return (
        <text
            x={position.x}
            y={position.y}
            onMouseDown={handleMouseDown}
            onMouseUp={handleMouseUp}
            onMouseMove={handleMouseMove}
            style={{ cursor: 'move' }}
        >
            {text}
        </text>
    );
}

除此之外,正如其他人指出的,你会获得更好的结果,如果你使用SVG的鼠标移动事件而不是文本元素,因为如果你将其快速拖动,你的鼠标可能会离开文本元素,事件将不再触发。

英文:

Then main problem with your code, as you already said, is that the svg width and the svg viewBox width are different.

In your case the svg width is 100%, so it's dinamically calculated, but its viewBox width is 700px. If you set both to the same value you would avoid this issue, otherwise you have to do some math.

For sake of simplicity, let's say you have this markup: &lt;svg width=&quot;300px&quot; viewBox=&quot;0 0 600 …. Your svg would occupy 300px on the screen. But since your svg viewBox width is 600px, the svg would "think" to be twice as big when it comes to calculate the position of its internal elements.
This means that if you want to position an element into the svg at (100px, 100px) according to the screen geometry, you will need to multiply those values by 2 and set your svg element position to (200px, 200px).
This scaling factor (2 in this example) can be calculated programmatically reading the SVG.viewBox attribute and getting the actual current image size on screen with Element.getBoundingClientRect(), like this:

scale = svg.viewBox.baseVal.width / svg.getBoundingClientRect().width

In your code you should scale the mouse position to match the svg geometry, like so:

<!-- begin snippet: js hide: false console: true babel: true -->

<!-- language: lang-js -->

function DraggableText({ x, y, text }) {
const [position, setPosition] = React.useState({ x: x, y: y });
const [isDragging, setIsDragging] = React.useState(false);
const [mouseOffset, setMouseOffset] = React.useState({ x: 0, y: 0 });
const [scale, setScale] = React.useState(1);
const handleMouseDown = (event) =&gt; {
event.preventDefault();
setIsDragging(true);
const svg = event.target.parentElement;
const vbox = svg.viewBox.baseVal;
const rect = svg.getBoundingClientRect();
const in_scale = vbox.width / rect.width;
setScale(in_scale);
setMouseOffset({
x: (event.clientX * in_scale) - position.x,
y: (event.clientY * in_scale) - position.y,
});
};
const handleMouseUp = (event) =&gt; {
setIsDragging(false);
};
const handleMouseMove = (event) =&gt; {
if (isDragging) {
setPosition({
x: scale * event.clientX - mouseOffset.x,
y: scale * event.clientY - mouseOffset.y,
});
}
};
return (
&lt;text
x={position.x}
y={position.y}
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
onMouseMove={handleMouseMove}
style={{ cursor: &#39;move&#39; }}
&gt;
{text}
&lt;/text&gt;
);
}
function D3BarChart({ }) {
// And Finally, Return!
return (
&lt;svg
className=&#39;cbb-box-shadowed&#39;
width=&#39;100%&#39;
viewBox={`0 0 700 450`}
preserveAspectRatio=&#39;xMaxYMax&#39;
style={{ background: &#39;#F2F2F2&#39; }}
&gt;
&lt;DraggableText x={100} y={100} text=&quot;Drag me!&quot; /&gt;
&lt;/svg&gt;
);
}
// render both components
ReactDOM.render(
(&lt;div&gt;
&lt;div width=&#39;60%&#39;&gt;
&lt;D3BarChart /&gt;
&lt;/div&gt;
&lt;/div&gt;),
document.querySelector(&#39;#root&#39;));

<!-- language: lang-html -->

&lt;script src=&quot;https://cdnjs.cloudflare.com/ajax/libs/react/16.14.0/umd/react.production.min.js&quot;&gt;&lt;/script&gt;
&lt;script src=&quot;https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.14.0/umd/react-dom.production.min.js&quot;&gt;&lt;/script&gt;


&lt;div id=&quot;root&quot;&gt;React component will be rendered here.&lt;/div&gt;

<!-- end snippet -->

Apart from this, as others pointed out, you would get a better result using the mouse move event of the svg instead of the text element, since if you drag it around fast enough, your mouse would be able to leave it, and the event will not fire anymore.

huangapple
  • 本文由 发表于 2023年3月10日 01:41:40
  • 转载请务必保留本文链接:https://go.coder-hub.com/75688226.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定