Lain
Lain

Reputation: 3726

Calculate the closest perpendicular point between a static line and a movable line

Description

Given is a blue line, which can be placed as pleased and is movable by mouse. By moving the line a gradient line (here in hotpink) is drawn from the original position. Now this line has the following requirements:

Assuming the left edge of the blue line being point 1 (red circle) and assuming the left edge of the original position of the blue line being point 2 (lime circle) assuming the right side of the blue line being point 3 (green circle) the angle from point 1 needs to be either 90 or -90 degress to point2/point3.

I believe the term for this is: the gradient hotpink line and the blue line are to be perpendicular. The direction of the blue line is not to change, only its position!

Sample

My issue

By moving the blue line I am able to calculate the degree to its original position and to draw the gradient hotpink line. Yet I am unable to calculate the closest point from the blue line which would make the gradient hotpink line perpendicular to both the original blue line and the new position of the blue line. If someone can point me to correct formula or proper term for this problem, I would be grateful.

Visual example (scraped code parts)

Followed is a quick example which I scraped together. One can move the blue line yet I can not force point 1 to keep a certain 90/-90 degree angle to point2/point3.

//REM: Current moving element
var _currentElement = null;

//REM: Drawing for quicker access
var _Drawing = null;

//REM: Starting the drag
function _onDown(event){
  if(event.target && event.target.tagName === 'line'){
    let tMatrix = _getMatrix(event.target);
    
    _currentElement = {
      Element: event.target,
      startX: event.clientX,
      startY: event.clientY,
      startE: tMatrix.e,
      startF: tMatrix.f,
      X1: Number(event.target.getAttribute('x1')),
      Y1: Number(event.target.getAttribute('y1')),
      X2: Number(event.target.getAttribute('x2')),
      Y2: Number(event.target.getAttribute('y2')),
      Ratio: 0.4
    }
  }
}

//REM: Dragging
function _onMove(event){
  if(_currentElement){
    _currentElement.endE = _currentElement.startE + ((event.clientX - _currentElement.startX) * _currentElement.Ratio);
    _currentElement.endF = _currentElement.startF + ((event.clientY - _currentElement.startY) * _currentElement.Ratio);

    console.log(
      'Angle (3)',
      _getAngleBetweenThreePoints(
        {x: _currentElement.X1 + _currentElement.endE, y: _currentElement.Y1 + _currentElement.endF},
        {x: _currentElement.X1, y: _currentElement.Y1},
        {x: _currentElement.X2 + _currentElement.endE, y: _currentElement.Y2 + _currentElement.endF}
      )
    );

    _setMatrix(_currentElement.Element, 1, 0, 0, 1, _currentElement.endE, _currentElement.endF)
  }
}

//REM: Ending the drag
function _onUp(){
  _currentElement = null
}

//REM: Returns the elements matrix
function _getMatrix(element){
  if(element){
    return element.transform.baseVal.numberOfItems ?
    element.transform.baseVal.getItem(0).matrix :
    element.transform.baseVal.appendItem(_Drawing.createSVGTransform()).matrix
  }
}

//REM: Sets the elements matrix
function _setMatrix(element, a, b, c, d, e, f){
  if(element){
    let tMatrix = _getMatrix(element);
    if(tMatrix){
      tMatrix.a = (typeof a === 'number') ? a : tMatrix.a;
      tMatrix.b = (typeof b === 'number') ? b : tMatrix.b;
      tMatrix.c = (typeof c === 'number') ? c : tMatrix.c;
      tMatrix.d = (typeof d === 'number') ? d : tMatrix.d;
      tMatrix.e = (typeof e === 'number') ? e : tMatrix.e;
      tMatrix.f = (typeof f === 'number') ? f : tMatrix.f;

      element.transform.baseVal.getItem(0).setMatrix(tMatrix)
    }
  }
}

//REM: Transforms client-coords to svg-coords
function _getSVGCoords(clientX, clientY){
    var tCTM = _Drawing.getScreenCTM();
    return tCTM ? {x: (clientX - tCTM.e) / tCTM.a, y: (clientY - tCTM.f ) / tCTM.d} : {x: clientX, y: clientY}
}

//REM: Returns angle from p1 to p2 and p3
function _getAngleBetweenThreePoints(p1, p2, p3){
  let tAngle = 0;

  if(p1 && p2 && p3){
    let tTemplate = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
    tTemplate.setAttribute('r', 1);
    
    let tC1 = _Drawing.appendChild(tTemplate.cloneNode(false));
    tC1.setAttribute('fill', 'red');
    tC1.setAttribute('cx', p1.x);
    tC1.setAttribute('cy', p1.y);
    
    let tC2 = _Drawing.appendChild(tTemplate.cloneNode(false));
    tC2.setAttribute('fill', 'lime');
    tC2.setAttribute('cx', p2.x);
    tC2.setAttribute('cy', p2.y);
    
    let tC3 = _Drawing.appendChild(tTemplate.cloneNode(false));
    tC3.setAttribute('fill', 'green');
    tC3.setAttribute('cx', p3.x);
    tC3.setAttribute('cy', p3.y);
    
    let tLine = document.getElementById('line1');
    if(!tLine){
      tLine = _Drawing.appendChild(document.createElementNS('http://www.w3.org/2000/svg', 'line'));
      tLine.id = 'line1';
      tLine.setAttribute('stroke-dasharray', '5,5')
    };
    
    tLine.setAttribute('x1', p1.x);
    tLine.setAttribute('y1', p1.y);
    tLine.setAttribute('x2', p2.x);
    tLine.setAttribute('y2', p2.y);

    tAngle = (Math.atan2(p3.y - p1.y, p3.x - p1.x) - Math.atan2(p2.y - p1.y, p2.x - p1.x)) * 180 / Math.PI
  }

  return tAngle
};

//REM: Assiging events
window.onload = function(){
  _Drawing = document.querySelector('svg');

  document.body.addEventListener('mousedown', _onDown, false);
  document.body.addEventListener('mousemove', _onMove, false);
  document.body.addEventListener('mouseup', _onUp, false);
  document.body.addEventListener('mouseleave', _onUp, false)
};
line,
circle{
  pointer-events: none;
  stroke-width: 1
}

#movable{
  pointer-events: all;
  stroke-width: 10
}

#line1{
  stroke: hotpink
}
<svg xmlns = 'http://www.w3.org/2000/svg' viewBox = '0 0 300 300'>
  <line x1 = '50' y1 = '50' x2 = '160' y2 = '110' stroke = 'blue' id = 'movable'></line>
</svg>

Expected result

By moving the blue line I draw a gradient hotpink line. That gradient line has to be perpendicular to the original position of the blue line as well as the new position of the blue line. The direction of the blue line itself may not change, just its position. If the line would be horizontal, merely straight up and straight down would be allowed. If the line was vertical, merely straight left and right would be allowed. If the line is diagonal, merely movements which are straight in the alignment of that line would be allowed (like a railway). So for example by having my diagonal blue line and moving straight upwards, I want to calculate the closest position from the left edge which would make the gradient hotpink line perpendicular to both the original position of the blue line and the new position of the blue line.

It is a behaviour very similar to Adobe PDF Measuring Tool.

Upvotes: 0

Views: 293

Answers (2)

John
John

Reputation: 3785

I've made a class that gives you the x,y of where the blue line needs to be placed.

class cTracker {
constructor(x1, y1, x2, y2) {
    x1 = parseInt(x1);
    y1 = parseInt(y1);
    x2 = parseInt(x2);
    y2 = parseInt(y2);
    
    const extendLine = 10000;
    
    let blueLineRadian = Math.atan2(x1 - x2, y1 - y2);
    this.m_blueLineRadian = blueLineRadian;
    
    
    let magentaLineRadian = blueLineRadian + (Math.PI / 2);
    
    this.m_blueLineCos = Math.cos(blueLineRadian) * extendLine;
    this.m_blueLineSin = Math.sin(blueLineRadian) * extendLine;
    
    this.m_magentaLineX1 = x1 + Math.sin(magentaLineRadian) * extendLine;
    this.m_magentaLineY1 = y1 + Math.cos(magentaLineRadian) * extendLine;
    
    this.m_magentaLineX2 = x1 + Math.sin(magentaLineRadian + Math.PI) * extendLine;
    this.m_magentaLineY2 = y1 + Math.cos(magentaLineRadian + Math.PI) * extendLine;
    
}
// -------------------------------------------------------------------------
//
// -------------------------------------------------------------------------
getCursorPosition(x, y) {
    this.m_x1 = x - this.m_blueLineSin;
    this.m_y1 = y - this.m_blueLineCos;
    this.m_x2 = x + this.m_blueLineSin;
    this.m_y2 = y + this.m_blueLineCos;

        
    return this.intersect(this.m_magentaLineX1, this.m_magentaLineY1, this.m_magentaLineX2, this.m_magentaLineY2, this.m_x1, this.m_y1, this.m_x2, this.m_y2);
}

                                              
  // -------------------------------------------------------------------------
  //
  // -------------------------------------------------------------------------
  intersect(x1, y1, x2, y2, x3, y3, x4, y4) {
      
      var denominator = ((y4 - y3) * (x2 - x1)) - ((x4 - x3) * (y2 - y1));
      
      var a                                   = y1 - y3;
      var b                                   = x1 - x3;
      var numerator1                          = ((x4 - x3) * a) - ((y4 - y3) * b);
      var numerator2                          = ((x2 - x1) * a) - ((y2 - y1) * b);
      a                                       = numerator1 / denominator;
      
      return {m_x : x1 + (a * (x2 - x1)), m_y : y1 + (a * (y2 - y1))};
  }
 }

To use it, create an instant with x1, y1, x2, y2 of the blue line

let movableLine = document.getElementById('movable');
    
let x1 = parseInt(movableLine.getAttribute("x1"));
let y1 = parseInt(movableLine.getAttribute("y1"));
let x2 = parseInt(movableLine.getAttribute("x2"));
let y2 = parseInt(movableLine.getAttribute("y2"));

this.m_tracker = new cTracker(x1, y1, x2, y2);

To get the X,Y on where to place the blue line you simply do the following...

let xySVG = this._getSVGCoords(event.clientX, event.clientY);         
let xy = this.m_tracker.getCursorPosition(xySVG.x, xySVG.y);

console.log(xy); // This is where the blue line needs to be placed...

Here is a working fiddle: https://jsfiddle.net/syxfv63z/2/

Upvotes: 2

Josef Wittmann
Josef Wittmann

Reputation: 1349

To get the movement perpendicular to the blue line, do the following:

  1. Get the vector of the blue lines orientation.
  2. Rotate it 90 deg (its perpendicular to its surface).
  3. Normalize it, e.g. make its length equal 1 (call it n).
  4. Now get the movement vector of the mouse (current - start, call it m).
  5. Now calculate n * dot(n,m). That is your movement vector in n direction.

Upvotes: 1

Related Questions