使用 React 计算 SVG 边界框? [英] Calculating SVG bounding boxes with React?

查看:25
本文介绍了使用 React 计算 SVG 边界框?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在编写一个使用 React 生成 SVG 的可视化应用程序.我需要的部分之一是标签 - 即文本,由一个封闭框包围,带有可变文本,可能会旋转并设置样式.

所以我有一个用于 NodeLabel 的组件,目前具有固定尺寸:

render() {返回<g><rect className="label" x={this.props.x} y={this.props.y-10} width={20} height={40}></rect><text className="labelText" x={this.props.x} y={this.props.y}>{this.props.children}</text>}

我在 DOM 中找到了一些关于这样做的信息,这里:

这是我新的最小示例:

//MyLabel 应该以 x,y 为中心,按角度旋转,//并在其周围放置一个边界框,距离文本 2px.class MyLabel 扩展了 React.Component {使成为() {const label = <text x={this.props.x} y={this.props.y} textAnchor="middle"alignmentBaseline="central">{this.props.children}</text>;//label 不是 DOM 元素,所以不能调用 label.getBoundingClientRect() 或 getBBox()//(魔术在这里找到标签的bbox..)//暂时创建一个静态的让 bb = {x: this.props.x-20, y: this.props.y-6, width: 40, height: 12};//添加边距常量边距 = 2;bb.width += margin * 2;bb.height += margin * 2;bb.x -= 保证金;bb.y -= 保证金;//rect 使用 bbox 来决定它的大小和位置const outline = <rect x={bb.x} y={bb.y} width={bb.width} height={bb.height} className="labeloutline"></rect>;const rot = `rotate(${this.props.angle} ${this.props.x} ${this.props.y})`;//构建最终标签(现在加上一个 x,y 点)return <g transform={rot}>{outline}{label}<circle cx={this.props.x} cy={this.props.y} r="2" fill="red"/></g>}}类应用程序扩展了 React.Component {使成为() {返回 <MyLabel x={100} y={100} angle={0}>Dalmation</MyLabel><MyLabel x={200} y={100} 角度={45}>可卡犬</MyLabel><MyLabel x={100} y={200} angle={145}>Pug</MyLabel><MyLabel x={200} y={200} 角度={315}>博美犬</MyLabel></svg>;}}/** 将上述组件渲染到div#app中*/ReactDOM.render(, document.getElementById('app'));

body { 背景:灰色;}svg {背景:浅灰色;}.labeloutline { 填充:白色;笔画:黑色;}

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script><div id="app"></div>

解决方案

您要么预先计算/测量字体几何形状,然后根据输入字符串对文本尺寸进行合理估计(这是最简单的解决方案,但显然会破坏如果字体发生变化),或执行两阶段渲染:

也就是说,您通过 ref 获取 dom 元素,在装载时获取框,最后通过更新状态重新渲染,例如:

class MyLabel 扩展 React.Component {构造函数(道具){超级(道具);this.state = {text_extents:null};}componentDidMount() {const box = this.text.getBBox();this.setState({text_extents:[box.width,box.height]});}使成为() {常量边距 = 2;const 范围 = this.state.text_extents;const label = { this.text = t;}} textAnchor="middle" dy={extents?(extents[1]/4):0} >{this.props.children}</text>;const 大纲 = 范围?<rect x={-extents[0]/2-margin} y={-extents[1]/2-margin} 宽度={extents[0]+2*margin} 高度={extents[1]+2*margin} className="labeloutline"></rect>: 空值;return <g transform={`translate(${this.props.x},${this.props.y}) rotate(${this.props.angle})`}>{outline}{label}</g>;}}

请注意,根据最新的 react 文档,这不应导致任何用户可见的闪烁:

<块引用>

componentDidMount(): 在这个方法中调用 setState() 会触发额外的渲染,但它会在浏览器更新屏幕之前发生.这保证即使在这种情况下 render() 将被调用两次,用户也不会看到中间状态.请谨慎使用此模式,因为它通常会导致性能问题.但是,当您需要在渲染取决于其大小或位置的内容之前测量 DOM 节点时,对于模态和工具提示等情况可能是必要的.

最后,请注意,如果标签字符串发生变化(通过 props 或其他方式),您需要相应地更新范围(通过 componentDidUpdate()).

I am writing a visualisation application with React generating SVG. One of the parts I need is a label - that is, text, surrounded by an enclosing box, with variable text, possibly rotated and styled.

So I have a component for the NodeLabel, currently with fixed dimensions:

render() {
        return <g>
            <rect className="label" x={this.props.x} y={this.props.y-10} width={20} height={40}></rect>
            <text className="labelText" x={this.props.x} y={this.props.y}>{this.props.children}</text>
        </g>
    }

And I've found some info about doing this in the DOM, here: Rectangle border around SVG text

But I don't quite see how to translate this into a React component - inside the render() method, there's no DOM elements to look at. Can I just use document.createElement() instead and expect an SVG element's dimensions to behave properly (and honour CSS)? Also, is there a way to avoid having essentially two copies of the creation code, one in JSX and one just before that to figure out dimensions? (like, for example, evaluating a snippet of JSX to DOM elements for this temporary off-screen copy)

Update: Jan 2018 and I'm back at this again :-) The actual application is an open source network diagramming tool, currently using GD and PHP, but moving to JS, React and SVG, I hope.

The bandwidth labels here are what I'm trying to reproduce, although the node labels use the same function in the current non-SVG version.

Here is my new minimal example:

// MyLabel should be centred at x,y, rotated by angle, 
// and have a bounding box around it, 2px from the text.
class MyLabel extends React.Component {
  render() {
    const label = <text x={this.props.x} y={this.props.y} textAnchor="middle" alignmentBaseline="central">{this.props.children}</text>;
        
    // label isn't a DOM element, so you can't call label.getBoundingClientRect() or getBBox()

    // (Magic happens here to find bbox of label..)        
    // make up a static one for now
    let bb = {x: this.props.x-20, y: this.props.y-6, width: 40, height: 12};
    
    // add margin
    const margin = 2;
    bb.width += margin * 2;
    bb.height += margin * 2;
    bb.x -= margin;
    bb.y -= margin;
    
    // rect uses bbox to decide its size and position
    const outline = <rect x={bb.x} y={bb.y} width={bb.width} height={bb.height} className="labeloutline"></rect>;
    
    const rot = `rotate(${this.props.angle} ${this.props.x} ${this.props.y})`;
    // build the final label (plus an x,y spot for now)
    return <g transform={rot}>{outline}{label}<circle cx={this.props.x} cy={this.props.y} r="2" fill="red" /></g>;
  }
}

class Application extends React.Component {
  render() {
    return <svg width={300} height={300}>
      <MyLabel x={100} y={100} angle={0}>Dalmation</MyLabel>
      <MyLabel x={200} y={100} angle={45}>Cocker Spaniel</MyLabel>
      <MyLabel x={100} y={200} angle={145}>Pug</MyLabel>
      <MyLabel x={200} y={200} angle={315}>Pomeranian</MyLabel>
    </svg>;
  }
}

/*
 * Render the above component into the div#app
 */
ReactDOM.render(<Application />, document.getElementById('app'));

body { background: gray; }
svg {background: lightgray;}
.labeloutline { fill: white; stroke: black;}

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>

<div id="app"></div>

解决方案

You either precompute/measure your typeface geometry and get a reasonable estimate of the text dimensions based on the input string (this is the simplest solution, but will obviously break if the typeface changes), or perform a two-stage rendering:

that is, you obtain the dom element via the ref, fetch the box on mount and finally re-render by updating state, something like:

class MyLabel extends React.Component {
  constructor(props){
    super(props);
    this.state = {text_extents:null};
  }
  componentDidMount() {
   const box = this.text.getBBox();

   this.setState({text_extents:[box.width,box.height]});
  }
 render() {
   const margin = 2;
   const extents = this.state.text_extents;
   const label = <text ref={(t) => { this.text = t; }} textAnchor="middle" dy={extents?(extents[1]/4):0} >{this.props.children}</text>;
   const outline = extents ?
         <rect x={-extents[0]/2-margin} y={-extents[1]/2-margin} width={extents[0]+2*margin} height={extents[1]+2*margin} className="labeloutline"></rect>
         : null;

   return <g transform={`translate(${this.props.x},${this.props.y}) rotate(${this.props.angle})`}>{outline}{label}</g>;
 }
}

Note that, as per latest react docs, this should not incur in any user visible flickering:

componentDidMount(): Calling setState() in this method will trigger an extra rendering, but it will happen before the browser updates the screen. This guarantees that even though the render() will be called twice in this case, the user won’t see the intermediate state. Use this pattern with caution because it often causes performance issues. It can, however, be necessary for cases like modals and tooltips when you need to measure a DOM node before rendering something that depends on its size or position.

Finally, note that if the label string changes (via props or whatever) you'll need to update extents accordingly (via componentDidUpdate()).

这篇关于使用 React 计算 SVG 边界框?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆