Search code examples
javascriptsvginteract.js

Resizing with handles in interact.js + SVG


In this jsFiddle I have an SVG rect that is resized with interact.js. This works fine, however I need to add resize handles n/ne/e/se/s/sw/w/nw, 8x8 pixel squares at each point. These handles should be used to resize the rect (instead of dragging the rect sides).

I found examples in HTML, not SVG, for example here, but I couldn't figure out how to make this work in SVG instead of HTML. Any help will be greatly appreciated.

var svg = document.getElementById('mysvg');
var rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
svg.appendChild(rect);
rect.setAttribute('x', 100);
rect.setAttribute('y', 100);
rect.setAttribute('width', 100);
rect.setAttribute('height', 100);
rect.setAttribute('class', 'resize-me');
rect.setAttribute('stroke-width', 2);
rect.setAttribute('stroke', 'white');
rect.setAttribute('fill', 'grey')


interact('.resize-me')
    .resizable({
        edges: { left: true, right: true, bottom: true, top: true }
    })
    .on('resizemove', function(event) {
        var target = event.target;
        var x = (parseFloat(target.getAttribute('endx')) || 0)
        var y = (parseFloat(target.getAttribute('endy')) || 0)

        target.setAttribute('width', event.rect.width);
        target.setAttribute('height', event.rect.height);

        x += event.deltaRect.left
        y += event.deltaRect.top
        target.setAttribute('transform', 'translate(' + x + ', ' + y + ')')

        target.setAttribute('endx', x)
        target.setAttribute('endy', y)
    });

Solution

  • Okay, done. Take a look:

    const svg = document.getElementById('mysvg');
    const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
    const group = document.createElementNS("http://www.w3.org/2000/svg", "g");
    
    svg.appendChild(group);
    group.appendChild(rect);
    group.setAttribute('class', 'resize-me');
    
    rect.setAttribute('x', 100);
    rect.setAttribute('y', 100);
    rect.setAttribute('width', 100);
    rect.setAttribute('height', 100);
    rect.setAttribute('stroke-width', 2);
    rect.setAttribute('stroke', 'white');
    rect.setAttribute('fill', 'grey');
    
    // Create the handles
    const handles = [];
    for (let i = 0; i < 8; i++) {
      const handle = document.createElementNS("http://www.w3.org/2000/svg", "rect");
    
      handle.setAttribute('width', 8);
      handle.setAttribute('height', 8);
      handle.setAttribute('stroke-width', 1);
      handle.setAttribute('stroke', 'white');
      handle.setAttribute('fill', 'black');
    
      handles.push(handle);
      group.appendChild(handle);
    }
    
    // Manually assign them their resize duties (R->L, T->B)
    handles[0].classList.add('resize-top', 'resize-left');
    handles[1].classList.add('resize-top');
    handles[2].classList.add('resize-top', 'resize-right');
    handles[3].classList.add('resize-left');
    handles[4].classList.add('resize-right');
    handles[5].classList.add('resize-bottom', 'resize-left');
    handles[6].classList.add('resize-bottom');
    handles[7].classList.add('resize-bottom', 'resize-right');
    
    // This function takes the rect and the list of handles and positions
    // the handles accordingly
    const findLocations = (r, h) => {
      const x = Number(r.getAttribute('x'));
      const y = Number(r.getAttribute('y'));
      const width = Number(r.getAttribute('width'));
      const height = Number(r.getAttribute('height'));
    
      // Important these are in the same order as the classes above
      let locations = [
        [0, 0],
        [width / 2, 0],
        [width, 0],
        [0, height / 2],
        [width, height / 2],
        [0, height],
        [width / 2, height],
        [width, height]
      ];
    
      // Move each location such that it's relative to the (x,y) of the rect,
      // and also subtract half the width of the handles to make up for their
      // own size.
      locations = locations.map(subarr => [
        subarr[0] + x - 4,
        subarr[1] + y - 4
      ]);
    
      for (let i = 0; i < locations.length; i++) {
        h[i].setAttribute('x', locations[i][0]);
        h[i].setAttribute('y', locations[i][1]);
      }
    }
    
    interact('.resize-me')
      .resizable({
        edges: {
          left: '.resize-left',
          right: '.resize-right',
          bottom: '.resize-bottom',
          top: '.resize-top'
        }
      })
      .on('resizemove', function(event) {
        // Resize the rect, not the group, it will resize automatically
        const target = event.target.querySelector('rect');
    
        for (const attr of ['width', 'height']) {
          let v = Number(target.getAttribute(attr));
          v += event.deltaRect[attr];
          target.setAttribute(attr, v);
        }
    
        for (const attr of ['top', 'left']) {
          const a = attr == 'left' ? 'x' : 'y';
          let v = Number(target.getAttribute(a));
          v += event.deltaRect[attr];
          target.setAttribute(a, v);
        }
    
        findLocations(rect, handles);
      });
    
    findLocations(rect, handles);
    svg {
      width: 100%;
      height: 240px;
      background-color: #2e9;
      -ms-touch-action: none;
      touch-action: none;
    }
    
    body {
      margin: 0;
    }
    <script src="https://cdn.jsdelivr.net/npm/interactjs@latest/dist/interact.min.js"></script>
    <svg id="mysvg"></svg>


    This works by taking advantage of Interact.js's support for using separate elements as handles, as mentioned in their docs here, as well as the nifty fact that you're using SVG instead of HTML. This means that you don't have to worry about using a CSS transform. Instead, you can just move the rect's x and y, and the calculations become quite a bit simpler.

    The only easy-to-miss change I had to make was that I had to put the rectangle and the handles into a group. This is because Interact.js will only let you use a child element as a handle for resizing. This means the listener is on the group, which resizes the rectangle within itself (and then, of course, causes the group to grow, since they match the size of their children).

    Let me know if I've missed anything.