skip to Main Content

I was working on an application to track movement in an SVG viewbox relative to the size of the SVG element itself. The sample code below has been reduced to remove any extra functions, variables, calculations, etc. as much as possible to demonstrate just the problem.

When moving the mouse cursor across the SVG, the green (purple & yellow edits added per comments) pointer tracker its x & y positions by 10px each when moving through the approximate zones marked by the red bands. Also moving left to right vs. right to left has slightly different trigger points.

const svg = document.getElementById('svg1');
svg.addEventListener('pointermove', event => {
    // console.log(event.offsetX);
    const id1 = 'pointer1';
    const id2 = 'pointer1';
    const id3 = 'pointer3';

    const pointerOld1 = document.getElementById(id1);
    if (pointerOld1) pointerOld1.remove();
    const pointerOld2 = document.getElementById(id2);
    if (pointerOld2) pointerOld2.remove();

    const x = event.offsetX;
    const y = event.offsetY;

    const pointerNew1 = document.createElementNS(svg.namespaceURI, 'circle');
    pointerNew1.setAttributeNS(null, 'id', id1);
    pointerNew1.setAttributeNS(null, 'cx', x * 0.25);
    pointerNew1.setAttributeNS(null, 'cy', y * 0.5);
    pointerNew1.setAttributeNS(null, 'r', '15px');
    pointerNew1.setAttributeNS(null, 'stroke', 'green');
    pointerNew1.setAttributeNS(null, 'stroke-width', '5px');
    pointerNew1.setAttributeNS(null, 'fill', 'none');
    svg.append(pointerNew1);

    const point = new DOMPoint(x, y).matrixTransform(svg.getCTM().inverse());
    const pointerNew2 = document.createElementNS(svg.namespaceURI, 'circle');
    pointerNew2.setAttributeNS(null, 'id', id1);
    pointerNew2.setAttributeNS(null, 'cx', point.x);
    pointerNew2.setAttributeNS(null, 'cy', point.y);
    pointerNew2.setAttributeNS(null, 'r', '15px');
    pointerNew2.setAttributeNS(null, 'stroke', 'purple');
    pointerNew2.setAttributeNS(null, 'stroke-width', '5px');
    pointerNew2.setAttributeNS(null, 'fill', 'none');
    svg.append(pointerNew2);

    const pointer3 = document.getElementById(id3);
    if (!pointer3) {
        const pointer3 = document.createElementNS(svg.namespaceURI, 'circle');
        pointer3.setAttributeNS(null, 'id', id3);
        pointer3.setAttributeNS(null, 'r', '10px');
        pointer3.setAttributeNS(null, 'stroke', 'yellow');
        pointer3.setAttributeNS(null, 'stroke-width', '5px');
        pointer3.setAttributeNS(null, 'fill', 'none');
        svg.append(pointer3);
    }
    else {
        pointer3.setAttributeNS(null, 'cx', x * 0.25);
        pointer3.setAttributeNS(null, 'cy', y * 0.5);
    }

});
<svg id="svg1" xmlns="http://www.w3.org/2000/svg" viewbox="0 0 100 100" width="400" height="200"
    style="border: 2px solid;">
    <line x1="0" y1="0" x2="100px" y2="100" stroke="blue" />
    <line x1="100" y1="0" x2="0" y2="100" stroke="blue" />
    <rect x="14" y="0" width="1" height="100" fill="red"></rect>
    <rect x="16" y="0" width="1" height="100" fill="red"></rect>
    <rect x="18" y="0" width="1" height="100" fill="red"></rect>
    <rect x="20" y="0" width="1" height="100" fill="red"></rect>
    <rect x="22" y="0" width="12" height="100" fill="red"></rect>
    <rect x="75" y="0" width="1" height="100" fill="red"></rect>
    <rect x="77" y="0" width="12" height="100" fill="red"></rect>
</svg>

I found some "work-arounds" including:

  1. If I uncomment the console.log(event.offsetX); line, it performs fine.
  2. If I move the x & y constant declarations before the if (pointerOld) pointerOld.remove(); line, it also works fine.

However, I am trying to understand why it is behaving like this in the first place? Am I doing something wrong? Or is this a bug (in HTML/JavaScript/SVG/web browser/OS)?

2

Answers


  1. First of all, like herrstrietzel write in the comment, the coordinate system for the SVG is different from the HTML page. Use the screenToSVG() function mentioned here.

    Second, you don’t need to create a new circle element on each event call back. Reuse the one that is already there.

    const pointerMove = event => {
      let svg = event.target.closest('svg');
      let pointer = svg.querySelector('.pointer');
    
      let x = event.clientX;
      let y = event.clientY;
      let coordinates = screenToSVG(svg, x, y);
      
      pointer.setAttribute('cx', coordinates.x);
      pointer.setAttribute('cy',coordinates.y);
    }
    
    let svg1 = document.getElementById('svg1');
    svg1.addEventListener('pointermove', pointerMove);
    
    function screenToSVG(svg, screenX, screenY) {
       var p = svg.createSVGPoint();
        p.x = screenX;
        p.y = screenY;
        return p.matrixTransform(svg.getScreenCTM().inverse());
    }
    <svg id="svg1" xmlns="http://www.w3.org/2000/svg" viewbox="0 0 100 100" width="400" height="200" style="border: 2pt solid;">
        <line x1="0" y1="0" x2="100px" y2="100" stroke="blue" />
        <line x1="100" y1="0" x2="0" y2="100" stroke="blue" />
        <rect x="14" y="0" width="1" height="100" fill="red"></rect>
        <rect x="16" y="0" width="1" height="100" fill="red"></rect>
        <rect x="18" y="0" width="1" height="100" fill="red"></rect>
        <rect x="20" y="0" width="1" height="100" fill="red"></rect>
        <rect x="22" y="0" width="12" height="100" fill="red"></rect>
        <rect x="75" y="0" width="1" height="100" fill="red"></rect>
        <rect x="77" y="0" width="12" height="100" fill="red"></rect>
        <circle class="pointer" r="15" stroke="green" stroke-width="5" fill="none"/>
    </svg>
    Login or Signup to reply.
  2. your viewbox is for 100x100 (position), and your svg size is 400x200(px)
    so there is -50 and +50 to use in coordinates.

    let circle = document.querySelector('svg circle');
    
    document.querySelector('svg').addEventListener('pointermove', e =>
      {
      circle.setAttribute( 'cx', e.offsetX / 2 );
      circle.setAttribute( 'cy', e.offsetY / 2 );
      });
    svg {
      margin : 100px;
      width  : 400px;
      height : 200px;
      border : 2px solid black;
      }
    svg line {
      stroke : blue;
      }
    svg rect {
      fill : red;
      }
    svg circle {
      fill         : none;
      stroke       : green;
      stroke-width : 5;
    }
    <!-- 
      according viewBox to used css width / height ratio value on 1/2 
      which move to x positions to +50. 
    -->
    
    <svg viewbox="0 0 200 100"  >  
      <line x1="50"   y1="0" x2="150" y2="100"    />
      <line x1="150"  y1="0" x2="50"  y2="100"    />
      <rect x="64"  y="0" width="1"  height="100" />
      <rect x="66"  y="0" width="1"  height="100" />
      <rect x="68"  y="0" width="1"  height="100" />
      <rect x="70"  y="0" width="1"  height="100" />
      <rect x="72"  y="0" width="12" height="100" />
      <rect x="125" y="0" width="1"  height="100" />
      <rect x="127" y="0" width="12" height="100" />
      <circle cx="100"  cy="50"  r="15"/>
    </svg>
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search