Making interact.js resizable pixel perfect

2020-04-19 06:19发布

问题:

In this jsFiddle I have an SVG rect that is resizable using interact.js. There's also a 10px by 10px grid and the .resizable function has included a 10px by 10 px snap. The objective is to resize the rect and have the edges snap exactly on the grid.

In most cases it works fine, but many times it is not, as you can see in the picture below. Maybe an adjustment needs to be done manually on resizeend ? How to fix this problem?

回答1:

As Erik said:

With this target.setAttribute(attr/a, Math.round(v/10)*10) it seems to work:

  .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, Math.round(v/10)*10);
    }

    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, Math.round(v/10)*10);
    }

    findLocations(rect, handles);
  });

Full Demo here - https://jsfiddle.net/alexander_L/1mzs36qL/3/ and below:

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");


// draw vertical lines
var gridSize = 10;
for (var i=0;i < 100;i++){
  var line = document.createElementNS("http://www.w3.org/2000/svg", "line");    
  svg.appendChild(line);
  line.setAttribute("x1", (i + 1) * gridSize)
  line.setAttribute("y1", 0)
  line.setAttribute("x2", (i + 1) * gridSize)
  line.setAttribute("y2", 500)
  line.setAttribute("stroke-width", 1)
  line.setAttribute("stroke", 'gray');
}
        
// draw horizontal lines
for (var i=0;i < 100;i++){
  var line = document.createElementNS("http://www.w3.org/2000/svg", "line");    
  svg.appendChild(line);
  line.setAttribute("x1", 0)
  line.setAttribute("y1", (i + 1) * gridSize)
  line.setAttribute("x2", 2000)
  line.setAttribute("y2", (i + 1) * gridSize)
  line.setAttribute("stroke-width", 1)
  line.setAttribute("stroke", 'gray');
}


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', 1);
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'
    },
    modifiers: [
      interact.modifiers.snap({
          targets: [
            interact.snappers.grid({
              x: 10,
              y: 10,
            })
          ]
        })
     ]
  })
  .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, Math.round(v/10)*10);
    }

    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, Math.round(v/10)*10);
    }

    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>