Home > Blockchain >  SVG: arrow head marker with rounded joins/caps
SVG: arrow head marker with rounded joins/caps

Time:06-30

I am working on a setup, where it is possible for a user to draw an arrow at a certain canvas_size on top of e.g. a picture with a fixed aspect ratio. If watching the picture on a different screen_size the arrow should still have the same thickness, and still be pointing at the same thing (if e.g. resizing the screen the arrow_marker_size is recomputed (not included in code-snippet)).

Currently I have set this up by having a transparent line with an arrow head marker with markerUnits="strokeWidth" and then I have a line with the wanted color with vectorEffect="non-scaling-stroke" (I have been able to create a more simple solution with a fixed arrow_marker_size and a single line, but that doesn't work in Safari, where the arrow head e.g. gets very small when lowering the screen_size). This seems to work, but I am open for better solutions - but my actual problem is that I can't figure out how to make sure that the arrow head has rounded corners???

class SvgWithArrow extends React.Component {
  
  constructor(props) {
    super(props);

    this.state = {
      arrow_marker_size: 5
    };
  }
  
  render = () => {
  
     const { x1, y1, x2, y2, color } = this.props.arrow; 

     const canvasPositionStyle = {
      width: this.props.screen_size.width   'px',
      height: this.props.screen_size.height   'px',
      background: "blue"
    };
    
    return (
      <div style={canvasPositionStyle}>
        <svg viewBox={'0 0 '   this.props.canvas_size.width   ' '   this.props.canvas_size.height}>
         <line
          x1={x1}
          y1={y1}
          x2={x2}
          y2={y2}
          markerEnd="url(#arrow-1)"
          stroke="transparent"
          strokeWidth="3px"
        />
        <marker
          id="arrow-1"
          markerWidth={this.state.arrow_marker_size}
          markerHeight={this.state.arrow_marker_size}
          refX={this.state.arrow_marker_size}
          refY={this.state.arrow_marker_size / 2}
          orient="auto"
          markerUnits="strokeWidth"
        >
          <path
            fill="none"
            stroke={color}
            vectorEffect="non-scaling-stroke"
            // TODO rounded corners?!! This isn't working...
            strokeLinejoin="round"
            strokeLinecap="round"
            d={'M0, '   this.state.arrow_marker_size
                        ' L'   this.state.arrow_marker_size   ','
                        (this.state.arrow_marker_size / 2)   ' 0, 0'}
          />
        </marker>
        <line
          x1={x1}
          y1={y1}
          x2={x2}
          y2={y2}
          stroke={color}
          strokeWidth="3px"
          strokeLinecap="round"
          vectorEffect="non-scaling-stroke"
        />
        </svg>
      </div>
    );
  }
}

// Render it
ReactDOM.render(
    <SvgWithArrow
      screen_size={{ width: 100*(16/9), height: 100 }}
      canvas_size={{ width: 150*(16/9), height: 150 }}
      arrow={{x1: 100, y1: 110, x2: 50, y2: 40, color: "red" }} />,
    document.getElementById("root")
);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>

CodePudding user response:

The path line of the marker is cut off, so the marker "box" needs to be larger than the line – or the line needs to be smaller. So, the linecaps are there, but they are outside the marker element. I changed the path so that it has that margin stating with – something like: d="M 1 5 L 5 3 L 1 1" and adding 1 to this.state.arrow_marker_size some places.

class SvgWithArrow extends React.Component {
  
  constructor(props) {
    super(props);

    this.state = {
      arrow_marker_size: 5
    };
  }
  
  render = () => {
  
     const { x1, y1, x2, y2, color } = this.props.arrow; 

     const canvasPositionStyle = {
      width: this.props.screen_size.width   'px',
      height: this.props.screen_size.height   'px',
      background: "blue"
    };
    
    return (
      <div style={canvasPositionStyle}>
        <svg viewBox={'0 0 '   this.props.canvas_size.width   ' '   this.props.canvas_size.height}>
         <line
          x1={x1}
          y1={y1}
          x2={x2}
          y2={y2}
          markerEnd="url(#arrow-1)"
          stroke="transparent"
          strokeWidth="3"
        />
        <marker
          id="arrow-1"
          markerWidth={this.state.arrow_marker_size 1}
          markerHeight={this.state.arrow_marker_size 1}
          refX={this.state.arrow_marker_size}
          refY={(this.state.arrow_marker_size 1) / 2}
          orient="auto"
          markerUnits="strokeWidth"
        >
          <path
            fill="none"
            stroke={color}
            vectorEffect="non-scaling-stroke"
            // TODO rounded corners?!! This isn't working...
            strokeLinejoin="round"
            strokeLinecap="round"
            d={`M 1 ${this.state.arrow_marker_size}
                L ${this.state.arrow_marker_size} ${((this.state.arrow_marker_size 1) / 2)}
                L 1 1`}
          />
        </marker>
        <line
          x1={x1}
          y1={y1}
          x2={x2}
          y2={y2}
          stroke={color}
          strokeWidth="3px"
          strokeLinecap="round"
          vectorEffect="non-scaling-stroke"
        />
        </svg>
      </div>
    );
  }
}

// Render it
ReactDOM.render(
    <SvgWithArrow
      screen_size={{ width: 100*(16/9), height: 100 }}
      canvas_size={{ width: 150*(16/9), height: 150 }}
      arrow={{x1: 100, y1: 110, x2: 50, y2: 40, color: "red" }} />,
    document.getElementById("root")
);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>

  •  Tags:  
  • svg
  • Related