My vanilla JS program has two canvases. The user draws arbitrary straight lines on an image on the first canvas, and pieces taken from splitting the canvas by the line are drawn on the second canvas, some of them reflected around the line (using reflectPiece). I draw the reflected points of the piece directly on the second canvas, then apply a reflection matrix transform (derived with getReflectionMatrix) so that I can rotate/scale the image appropriately, then draw the image clipped to the outline.
This works perfectly for most combinations that I have tried, but I have found a line that it will not work for and cannot work out why. It is as if the reflection is not applying (or doubling) or something, so the image is rotated to the wrong angle and its angle doesn’t align with the unmodified outline of the reflected points.
- In my example code, the unmodified outline after reflection is drawn
in black and blue using two approaches – point for point
from reflectPiece, and using DOMPoint.matrixTransform to
transform the original points with the reflection matrix in order to show the
matrix doesn’t work the same both times – and filled in with both
yellow and blue (green). These are the intended points.
- Then I draw
the reflected points with the reflection matrix applied as a
transform in red, to show the result of doubling the reflection.
- Then I draw the image with the transform applied like I do in my
main code, and the angle aligns with the double-rotated red piece, when it should
align with the one that’s coloured in green.
I’m sure it’s something really basic and I feel silly asking, but I’ve been stuck for days somehow 🙁 Geometry is not my forte.
Broken:

Working:

Here is my debug code:
<canvas id="canvas" width="800" height="800"></canvas>
<img src="debug.png" id="image" style="display: none">
<script>
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
const img = document.getElementById("image");
const intersects = [new DOMPoint(143.07549812764975, 58.35350167526056),
new DOMPoint(198.3825597334491, 8.86823602796649)];
const points = [
new DOMPoint(198.38255973344914, 8.868236027966528),
new DOMPoint(162.65825355141538, 359.09787638799855),
new DOMPoint(112.9163540869709, 354.0240772523315),
new DOMPoint(143.0754981276499, 58.3535016752607),
];
const line = { startX: 250.4616879421679, startY: -37.7288786850976, endX: 103.53831205783209, endY: 93.7288786850976 };
const reflected = reflectPiece(points, line);
const transform = getReflectionMatrix(line);
const mapped = mapTransform(points, transform);
const canvasTransform = new DOMMatrix().translateSelf(canvas.width/2, canvas.height/2);
const drawTransform = canvasTransform.multiply(transform);
ctx.setTransform(canvasTransform);
img.onload = () => {
debug();
}
function debug() {
drawMatrixTransformedPoints();
drawReflectedWithTransform();
drawReflectedPoints();
drawImage();
}
function drawMatrixTransformedPoints() {
ctx.strokeStyle = "black";
tracePiecePath(mapped);
ctx.stroke();
ctx.fillStyle = "yellow";
ctx.fill();
}
function drawReflectedPoints() {
ctx.strokeStyle = "blue";
tracePiecePath(reflected);
ctx.stroke();
ctx.globalAlpha = 0.25;
ctx.fillStyle = "cyan";
ctx.fill();
ctx.globalAlpha = 1;
}
function drawReflectedWithTransform() {
ctx.save();
ctx.strokeStyle = "red";
ctx.setTransform(drawTransform);
tracePiecePath(reflected);
ctx.stroke();
ctx.restore();
}
function drawImage() {
ctx.globalAlpha = 0.5;
ctx.save();
ctx.setTransform(drawTransform);
ctx.drawImage(img, 0, 0);
ctx.restore();
ctx.globalAlpha = 1;
}
function tracePiecePath(points) {
ctx.beginPath();
const firstPoint = points[0];
ctx.moveTo(firstPoint.x, firstPoint.y);
points.slice(1).forEach(point => {
ctx.lineTo(point.x, point.y);
});
ctx.closePath();
}
function getReflectionMatrix(line) {
const matrix = new DOMMatrix();
const origin = getMidlinePoint(...intersects);
const angle = getAngleFromOrigin(line);
const angleInDegrees = angle * 180 / Math.PI;
matrix.translateSelf(origin.x, origin.y);
matrix.rotateSelf(angleInDegrees);
matrix.scaleSelf(1, -1);
matrix.rotateSelf(-angleInDegrees);
matrix.translateSelf(-origin.x, -origin.y);
return matrix;
}
function getMidlinePoint(pt1, pt2) {
const x = (pt1.x + pt2.x)/2;
const y = (pt1.y + pt2.y)/2;
return new DOMPoint(x, y);
}
function getAngleFromOrigin(line) {
const { startX, endX, startY, endY } = line;
const dx = endX - startX;
const dy = endY - startY;
return Math.atan2(dy, dx);
}
function reflectPiece(points, line) {
const normal = this.getNormalVector(line);
const newPoints = [];
for (let i = 0; i < points.length; i++) {
newPoints.push(this.reflectPoint(line, points[i], normal));
};
return newPoints;
}
function getNormalVector(line) {
const { startX, endX, startY, endY } = line;
const dx = endX - startX;
const dy = endY - startY;
const len = Math.hypot(dx, dy);
const nx = -dy / len;
const ny = dx / len;
return { nx, ny };
}
function reflectPoint(line, point, normal) {
const { x, y } = point;
const { startX, startY } = line;
const { nx, ny } = normal;
const vx = x - startX;
const vy = y - startY;
const dot = vx * nx + vy * ny;
const rx = startX + vx - 2 * dot * nx;
const ry = startY + vy - 2 * dot * ny;
return new DOMPoint(rx, ry);
}
function mapTransform(points, m) {
return points.map(point => point.matrixTransform(m));
}
</script>
Here is an example of a working shape (aligns with the green shape, not the red one):
const intersects = [new DOMPoint(256.8378378378378, 50),
new DOMPoint(258.18918918918916)];
const points = [new DOMPoint(369.29268292682923, 50),
new DOMPoint(256.83783783783787, 50),
new DOMPoint(258.1891891891892, 0),
new DOMPoint(316.8536585365853, 0)];
const line = { startX: 267.0952908723996, startY: -329.5257622787839, endX: 247.90470912760043, endY: 380.5257622787839 };
debug.png: 
Thanks!