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.
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.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 };
Thanks!
edit: Thinking about it, it makes sense that I get this result, because the points are already created by the line, so rotating and scaling to match the line reflects them visually, but the image was never aligned with the line, so it just rotates to match the line (I don't know how to articulate this very well). I tried rotating to match the angle between the origin and the bottom edge of the reflected piece, but that broke on pieces that stood at about 90 degrees etc.
Okay, I solved it by taking the longest edge and rotating the matrix to the angle of that edge after reflecting:
function getReflectionMatrix() {
const matrix = new DOMMatrix();
const origin = getMidlinePoint(...intersects);
const angle = getAngleFromOrigin(line) * 180 / Math.PI;
const longestEdgeAngle = getLongestEdgeAngle(points) * 180 / Math.PI;
matrix.translateSelf(origin.x, origin.y);
matrix.rotateSelf(angle);
matrix.scaleSelf(1, -1);
matrix.rotateSelf(-angle);
matrix.rotateSelf(longestEdgeAngle);
matrix.translateSelf(-origin.x, -origin.y);
return matrix;
}
function getBoundingBox(points) {
const xs = points.map(p => p.x);
const ys = points.map(p => p.y);
const minX = Math.min(...xs);
const minY = Math.min(...ys);
return { minX, minY };
}
function getLongestEdgeAngle(points) {
let maxLength = 0;
let bestAngle = 0;
for (let i = 0; i < points.length; i++) {
const pt1 = points[i];
const pt2 = points[(i + 1) % points.length];
const dx = pt2.x - pt1.x;
const dy = pt2.y - pt1.y;
const length = Math.hypot(dx, dy);
if (length > maxLength) {
maxLength = length;
bestAngle = Math.atan2(dy, dx);
}
}
return bestAngle;
}
function drawImage() {
ctx.globalAlpha = 0.5;
ctx.save();
ctx.setTransform(drawTransform);
const { minX, minY } = getBoundingBox(reflected);
ctx.drawImage(img, minX, minY);
ctx.restore();
ctx.globalAlpha = 1;
}