问题描述
下面的 React 组件应该允许平移 SVG。确实如此,但由于某种原因,SVG 的移动会呈指数级加速,因此鼠标指针的几个像素移动会导致 SVG(或者更确切地说,SVG viewBox)的移动越来越大。例如,轻轻拖动鼠标,圆圈就会从屏幕上拉开。
这是一个小提琴:https://jsfiddle.net/bupham/ax473r52/4/
似乎可能发生了一些 React 反馈循环,但我不确定。平移行为代码来自另一个 SO 帖子 here。
我尝试将方法调用移动到容器或 SVG,但它仍然发生。我试过将一个函数传递给 setState,仍然发生。我尝试制作 state.viewBox 的浅拷贝而不是制作浅拷贝——仍然会发生。我做错了什么?
export default class SVGContainer extends React.Component {
constructor(props) {
super(props);
this.state= {
viewBox: {x:0,y:0,w:500,h:500},svgSize: {w: 500,h: 500},scale: 1,isPanning: false,startPoint: {x:0,y:0},endPoint: {x:0,}
}
handleMouseDown = (e) => {
console.log('handleMouseDown e',e)
this.setState({
isPanning: true,startPoint: {x: e.clientX,y: e.clientY},})
}
handleMouseMove = (e) => {
this.setState((prevstate,props) => {
if (prevstate.isPanning) {
console.log('handleMouseMove e',e.clientX,e.clientY)
let startPoint = prevstate.startPoint;
const scale = prevstate.scale;
const viewBox = prevstate.viewBox;
const endPoint = {x: e.clientX,y: e.clientY};
const dx = (startPoint.x - endPoint.x) / scale;
const dy = (startPoint.y - endPoint.y) / scale;
const newViewBox = {x:viewBox.x+dx,y:viewBox.y+dy,w:viewBox.w,h:viewBox.h};
console.log('the view Box',newViewBox)
return {viewBox: newViewBox};
}
})
}
handleMouseUp = (e) => {
if (this.state.isPanning){
let startPoint = this.state.startPoint;
const scale = this.state.scale;
const viewBox = this.state.viewBox;
const endPoint = {x: e.clientX,y: e.clientY};
var dx = (startPoint.x - endPoint.x)/scale;
var dy = (startPoint.y - endPoint.y)/scale;
const endViewBox = {x: viewBox.x+dx,y: viewBox.y+dy,w: viewBox.w,h: viewBox.h};
console.log('viewBox at mouseup',endViewBox)
this.setState({
viewBox: endViewBox,});
}
}
handleMouseLeave = (e) => {
this.setState({
isPanning: false,})
}
render() {
return (
<div className="container" >
<svg width="500" height="500"
onWheel={this.handleWheelZoom}
onMouseDown={this.handleMouseDown}
onMouseMove={this.handleMouseMove}
onmouseup={this.handleMouseUp}
onMouseLeave={this.handleMouseLeave}
viewBox={`${this.state.viewBox.x} ${this.state.viewBox.y} ${this.state.viewBox.w} ${this.state.viewBox.h}`}>
<circle cx="50" cy="50" r="50" />
</svg>
</div>
);
}
}
解决方法
这在技术上不是一个答案,而是一个解决方案:我决定使用 d3 (4.13.0) 代替,因为 zoom 的跨浏览器/触摸复杂性太多了,无法手动书写。
至于为什么它不起作用,它很可能与 React 和 React 状态的异步性质有关。一位朋友建议使用 requestAnimationFrame() 和/或限制鼠标事件。
这是我使用的相关代码的样子。我为我需要使用 D3 操作的两个 DOM 节点添加了两个 React 引用:
export default class SVGContainer extends React.Component {
constructor(props) {
super(props);
this.svgRef = React.createRef();
this.gRef = React.createRef();
this.state= {
container: {width: 1000,height: 1000},}
}
componentDidMount() {
window.addEventListener('resize',this.handleWindowResize);
const width = window.innerWidth;
const height = window.innerHeight;
this.setState({
container: {width,height},})
const svg = d3.select(this.svgRef.current);
// D3 wants you to call zoom on a container and then apply the zoom transformations
// elsewhere...you can read why in the docs.
svg.call(this.handleZoom).call(this.handleZoom.transform,initialZoom);
}
componentWillUnmount() {
window.removeEventListener('resize',this.handleWindowResize);
}
handleWindowResize = (e) => {
const height = this.svgRef.current.clientHeight;
const width = this.svgRef.current.clientWidth;
this.setState({
container: {width,});
}
handleZoom = d3.zoom().on('zoom',e => {
const g = d3.select(this.gRef.current);
g.attr('transform',d3.event.transform)
})
render() {
return (
<div className="SVGContainer">
<svg
width={this.state.container.width} height={this.state.container.height}
ref={this.svgRef}>
<circle cx="50" cy="50" r="50" ref={this.gRef} />
</svg>
</div>
);
}
}