Rotate any 2D vector 90deg
Perpendicular lines are simple to calculate if you remember the rotate 90deg rule for a 2D vector.
A vector {x,y}
can be rotated 90 deg clockwise {-y,x}
or anti clockwise {y,-x}
. Swap x and y and negate the y for clockwise or x of anti clock wise
So for a line segment x1
,y1
to x2
,y2
convert to a vector, normalise that vector and rotate 90 deg as follows
function getPerpOfLine(x1,y1,x2,y2){ // the two points can not be the same
var nx = x2 - x1; // as vector
var ny = y2 - y1;
const len = Math.sqrt(nx * nx + ny * ny); // length of line
nx /= len; // make one unit long
ny /= len; // which we call normalising a vector
return [-ny, nx]; // return the normal rotated 90 deg
}
Then say you want to draw a 10 pixel line at the ends of the line segment
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
document.body.append(canvas);
ctx.strokeStyle = "black";
ctx.lineJoin = ctx.lineCap = "round";
ctx.lineWidth = 3;
// the line segment
const x1 = 40, y1 = 40, x2 = 260, y2 = 110;
const endLen = 10; // length of end lines
var px = y1 - y2; // as vector at 90 deg to the line
var py = x2 - x1;
const len = endLen / Math.hypot(px, py);
px *= len; // make leng 10 pixels
py *= len;
// draw line the start cap and end cap.
ctx.beginPath();
ctx.lineTo(x1, y1); // the line start
ctx.lineTo(x2, y2);
ctx.moveTo(x1 + px, y1 + py); // the start perp line
ctx.lineTo(x1 - px, y1 - py);
ctx.moveTo(x2 + px, y2 + py); // the end perp line
ctx.lineTo(x2 - px, y2 - py);
ctx.stroke();
Update
Simple solution to rendering content along a line, Uses the same 90deg rule.
There is an alternative method of rendering using the same vector rotation, but rather than set the perpendicular axis via vector multiplication, you set the y axis of the transform to 90deg from the x axis which is along the line. Set the origin to the start of the line and you simply render relative to the line.
setTransformToLine(x1, y1, x2, y2)
The following function will set the canvas transform to be along the line
// Set 2D context current transform along the line x1,y1,x2,y2 and origin to
// start of line. y Axis is rotated clockwise 90 from the line.
// Returns the line length as that is most frequently required when
// using the method saving some time.
function setTransformToLine(x1, y1, x2, y2) {
const vx = x2 - x1; // get the line as vector
const vy = y2 - y1;
const len = Math.hypot(vx, vy); // For <= IE11 use Math.sqrt(vx * vx + vy * vy)
const nx = vx / len; // Normalise the line vector. Making it one
const ny = vy / len; // pixel long. This sets the scale
// The transform is the normalised line vector for x axis, y at 90 deg
// and origin at line start
ctx.setTransform(nx, ny, -ny, nx, x1, y1); // set transform
return len;
}
How to use
This example show how to use the transform to do the same line but add annotated length as well.
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
document.body.append(canvas);
ctx.strokeStyle = "black";
ctx.lineJoin = ctx.lineCap = "round";
ctx.lineWidth = 3;
ctx.font = "16px arial";
ctx.textBaseline = "middle";
ctx.textAlign = "center";
const x1 = 40, y1 = 40, x2 = 260, y2 = 110;
const endLen = 10;
function setTransformToLine(x1, y1, x2, y2) {
const vx = x2 - x1;
const vy = y2 - y1;
const len = Math.hypot(vx, vy);
const nx = vx / len;
const ny = vy / len;
ctx.setTransform(nx, ny, -ny, nx, x1, y1);
return len;
}
// Set the transform along the line. Keep the line length
// line len is need to get the x coord of the end of the line
const lineLen = setTransformToLine(x1, y1, x2, y2);
const lineLenStr = Math.round(lineLen) + "px";
const textWidth = ctx.measureText(lineLenStr).width;
const rlen = lineLen - textWidth - 16; // find the remaining line len after removing space for text
// Rendering is done in line local coordinates
// line is from (0,0) to (lineLen,0)
// Now draw the line the ends first and then along the line leaving gap for text
ctx.beginPath();
ctx.lineTo(0, -endLen); // start perp line
ctx.lineTo(0, endLen);
ctx.moveTo(lineLen, -endLen); // end of line is at lineLen
ctx.lineTo(lineLen, endLen);
ctx.moveTo(0,0); // line start segment
ctx.lineTo(rlen / 2, 0);
ctx.moveTo(lineLen - rlen / 2,0); // line end segment
ctx.lineTo(lineLen, 0);
ctx.stroke(); // render it.
// now add text at the line center
ctx.fillText(lineLenStr, lineLen / 2, 0);
// To restore the transform to its default use identity matrix
ctx.setTransform(1, 0, 0, 1, 0, 0);