javascript – Limit the movement of an object to a specific shape

Question:

I propose to use this question as a kind of competition, then I will allocate 500 rp to the one whose answer gets the most votes in favor.

Condition

There is a code that allows you to move a block by certain boundaries (in this case, it is a square).

let reg = $('.region'), obj = $('.region .object');

reg.on('mousemove', function(e){
  let p = {
        // region size
        rW: reg.width(), rH: reg.height(),
        // object size
        oW: obj.width(), oH: obj.height(),
        // mouse position
        mY: e.pageY - reg.offset().top, mX: e.pageX - reg.offset().left,
        // object position (center)
        oY: obj.width() / 2, oX: obj.height() / 2
      };
   // object moving
   obj.css({
    'top': p.mY <= p.oY ? 0 : p.mY >= p.rW - p.oW ? p.rW - p.oW : p.mY - p.oY,
   'left': p.mX <= p.oX ? 0 : p.mX >= p.rH - p.oH ? p.rH - p.oH : p.mX - p.oX
   });
});
body {
  width: 100wh; height: 100vh;
  margin: 0;
  overflow: hidden;
  position: relative;
}

.region {
  display: block;
  width: 200px; height: 200px;
  box-shadow: 0 0 0 2px blue;
  position: absolute;
  left: 0; top: 0; right: 0; bottom: 0;
  margin: auto;
}

.object {
  display: block;
  width: 20px; height: 20px;
  background: red;
  position: absolute;
  left: 0; top: 0;
  pointer-events: none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>

<div class="region">
  <div class="object"></div>
</div>

It is even a no brainer here that the "mechanics" are too simple, for the form is primitive.

But what to do, for example, if the shape, along which it will be necessary to restrict the movement, will be a figure, say a circle , a triangle, or some kind of complex (relatively) figure.

Task

Make the element move along a limited shape (one or more), in priority a circle , a triangle .

Nuances

  1. Touch html layout as little as possible.
  2. it is desirable to use the "dynamic" size of the figure (ie get it from the figure itself (if it is possible).

Outcomes

The competition will last until 08/12/19

Answer:

I propose this option.

Head-on in O ^ 2n go around the approximations of both paths and look for intersection points ….

let borderPts = getPoints(border),
    figurePts = getPoints(figure),
    drag;

addEventListener('mouseup', e => {
  drag && (drag.element.style.pointerEvents = 'all');
  drag = null;
});

addEventListener('mousedown', e => {
  let tr = e.target.getAttribute('transform');
  if (!tr) return;
  let xy = tr.split(/\(|\)|\,/);
  drag = {x: xy[1] - e.x, y: xy[2] - e.y, element: e.target};
  e.target.style.pointerEvents = 'none'
});

let debugPoint = pt => `<circle r=3 fill=none stroke=blue 
  cx="${pt.x}" cy="${pt.y}"></circle>`;

addEventListener('mousemove', e =>  {
  if (!drag) return;
  let intersectionPts = intersect(e.x, e.y);  
  let canMove = e.target === border && intersectionPts.length === 0;
  drag.element.setAttribute('stroke', canMove ? 'black' : 'red' ); 
  if (mode.checked)
    debug.innerHTML = intersectionPts.map(debugPoint).join('');
  if (canMove || mode.checked) 
    drag.element.setAttribute('transform', 
      `translate(${drag.x + e.x},${drag.y + e.y})`);  
});

function intersect(x,y) {
    let pts = figurePts.map(pt => [pt[0] + x + drag.x, pt[1] + y + drag.y]);
    let points = [];
    forEachPair(pts, (pt1, pt2) => {
        forEachPair(borderPts, (pt3, pt4) => {
             let intersection = findIntersection(...pt1,...pt2,...pt3,...pt4);
             intersection && points.push(intersection);           
        });
    });
    return points;
}

function forEachPair(pts, f) {
    for (let i = 0; i<pts.length; i++) 
        f(pts[(i ? i : pts.length)-1], pts[i])
}

function getPoints(path) {
  let precision = 5
  let count = Math.floor(path.getTotalLength()/precision);
  return Array(count).fill(0).map((e,t) => {
    let p = path.getPointAtLength(t*precision);
    return [p.x, p.y];  
  });
}

function findIntersection(x1, y1, x2, y2, x3, y3, x4, y4) {
    // http://paulbourke.net/geometry/pointlineplane/
    let denom = (y4 - y3)*(x2 - x1) - (x4 - x3)*(y2 - y1);
    if (denom == 0) 
        return null;
    let ua = ((x4 - x3)*(y1 - y3) - (y4 - y3)*(x1 - x3))/denom;
    let ub = ((x2 - x1)*(y1 - y3) - (y2 - y1)*(x1 - x3))/denom;
    if (ua >= 0 && ua <= 1 && ub >= 0 && ub <= 1)
    return {
        x: x1 + ua * (x2 - x1),
        y: y1 + ua * (y2 - y1)
    };
}
<svg width=640 height=155>
<g id="debug"></g>
<path id="border" fill="#0001" d="M27,85C27,85,50,17,80,10C109,2,110,50,156,51C201,51,216,9,281,12C345,14,360,63,438,64C515,64,597,1,620,15C642,28,612,109,538,125C463,140,403,82,295,81C186,79,63,118,63,118A28,28,0,0,1,27,85z"></path>  
<path id="figure" stroke="black" fill="#0001" transform="translate(0,0)" d="M270,54C270,54,312,61,319,59C325,56,318,39,314,36C309,33,293,41,287,39C280,36,274,20,269,19C263,17,254,25,251,30C248,34,246,42,249,46C251,49,270,54,270,54z"></path>
</svg><br><input type="checkbox" id="mode">mode
Scroll to Top