Search code examples
javascriptcss

CSS Arrow with any angle but fixed start and end point


I am currently working on a side project, where I want to "draw" diagrams. At the moment you just drag "sticky notes" to a grid, move, resize and label them. I now want to connect these notes with arrows. I got the calculations for length (pythagoras) and the angle of the line down.

calculateAngle(): number {
    return Math.atan2(this._start.y - this._end.y, this._start.x - this._end.x) * 180 / Math.PI + 180;
}

calculateLength(): number {
    return Math.round(Math.sqrt(Math.pow(this._start.y - this._end.y, 2) + Math.pow(this._start.x - this._end.x, 2)));
}

I also found a simple CSS solution for the arrows which work perfectly for horizontal and vertical arrows, but as soon the angle isn't dividable by 90deg the position is of course of.

This is based of a solution I found in SO and just added the transform. If you play around with the deg value, you can see how the base and head move around in the 2D space: https://jsfiddle.net/0r7k6L4c/

Does anyone know a good way to calculate the "new" top and left values if the rotation is taken into account? Or maybe I just offset the position with margins if this calulation is easier.

I used to be good at math in school but that was a few decades ago.


Solution

  • You could use the newly available CSS hypot and atan2 functions. Define the two points A, B using CSS Properties ax, ay and bx, by.
    Make your element position fixed or absolute, set the CSS transform-origin point to be X left Y 50%, and rotate it by atan2 radians:

    .arrow {
      --t: 5px; /* tail size */
    
      position: absolute;
      transform-origin: left 50%;
      left: calc(var(--ax) * 1px);
      top: calc(var(--ay) * 1px);
      height: var(--t);
      width: calc(hypot(calc(var(--by) - var(--ay)), calc(var(--bx) - var(--ax))) * 1px);
      rotate: atan2(calc(var(--by) - var(--ay)), calc(var(--bx) - var(--ax)));
      background: #000;
    }
    <div class="arrow" style="--ax:10; --ay:10; --bx:50; --by:100;"></div>

    and to create the arrow shape, either use the :before pseudo or like the following where I borrowed Temani's cool solution using clip-path:

    .arrow {
      
      --t: 5px;  /* tail size */
      --h: 10px; /* head size */
      
      position: absolute;
      transform-origin: left 50%;
      left: calc(var(--ax) * 1px);
      top: calc(var(--ay) * 1px);
      height: var(--h);
      width: calc(hypot(calc(var(--by) - var(--ay)), calc(var(--bx) - var(--ax))) * 1px);
      rotate: atan2(calc(var(--by) - var(--ay)), calc(var(--bx) - var(--ax)));
      clip-path: polygon(0 calc(50% - var(--t)/2),calc(100% - var(--h)) calc(50% - var(--t)/2),calc(100% - var(--h)) 0,100% 50%,calc(100% - var(--h)) 100%,calc(100% - var(--h)) calc(50% + var(--t)/2),0 calc(50% + var(--t)/2));
      background: #000;
    }
    <div class="arrow" style="--ax:10; --ay:10; --bx:50; --by:100;"></div>