Код: Выделить всё
pointsLeft
const radians = getAngleFromOrigin(line);
const angle = getDegreesFromRadians(radians);
matrix.rotateSelf(angle);
matrix.scaleSelf(1, -1);
matrix.rotateSelf(-angle);
< /code>
const longestEdgeAngle = getLongestEdgeAngle(originalPoints);
const degrees = getDegreesFromRadians(longestEdgeAngle);
matrix.rotateSelf(degrees);
< /code>
I feel like I should be able to look at the signs of a and d and know, but they don't seem to correspond to the actual drawing direction? I got better results by comparing the absolute values of a/b and c/d and then checking whether the value with the greater magnitude was < 0 (implemented in my snippet), but it still failed in one case. I also tried various things involving the net rotation (or taking atan2 of b/a) and trying to see if that changed which one I should check the sign of for each of a/b and c/d, but I couldn't work it out.
Technically speaking, I don't need to determine it from the matrix directly, as I have access to the transformations applied to the matrix. It just seemed safest to me. If context on my function is necessary, see my answer to my previous question here.
I tried looking at the values of the matrixes of my different test cases, but I cannot work out why matrixes with the same signs on a/b/c/d orient differently (in my example, the first test's green piece - the long horizontal one - and the last test's orange piece - the short one - have identical signs, but different drawing orientations).
Code below with some test cases embedded, the goal is to have the image perfectly overlap the outline when clipped (which it will do if I can get this working):
"use strict";
const canvasWidth = 600;
const canvasHeight = 830;
const pieces = [
[{
points: [new DOMPoint(140, 50), new DOMPoint(140.00000000000003, -90), new DOMPoint(90.00000000000001, -90), new DOMPoint(90, 0), ],
line: {
start: new DOMPoint(283.6663192636163, 193.66631926361632),
end: new DOMPoint(-52.666319263616316, -142.66631926361632)
},
original: [new DOMPoint(140, 50), new DOMPoint(0, 50), new DOMPoint(0, 0), new DOMPoint(90, 0), ],
intersects: [new DOMPoint(90, 0), new DOMPoint(140, 50), ],
origTopLeft: new DOMPoint(0, 0),
width: 50.00000000000003,
height: 50.00000000000003
}, {
points: [new DOMPoint(158.36517719568567, 44.67326250912334), new DOMPoint(163.97896049896048, -53.783451143451146), new DOMPoint(213.82095634095634, -49.58802494802492), new DOMPoint(211.1386748844376, -2.5451301597599514), ],
line: {
start: new DOMPoint(252.24682141160773, -39.3261033682806),
end: new DOMPoint(101.75317858839227, 95.3261033682806)
},
original: [new DOMPoint(158.36517719568567, 44.67326250912335), new DOMPoint(256.8378378378378, 50), new DOMPoint(258.18918918918916, -1.6317320576126618e-15), new DOMPoint(211.1386748844376, -2.5451301597599563), ],
intersects: [new DOMPoint(211.1386748844376, -2.5451301597599514), new DOMPoint(158.36517719568567, 44.67326250912334), ],
origTopLeft: new DOMPoint(158.36517719568567, -2.5451301597599563),
width: 55.45577914527067,
height: 55.45577914527067
}, {
points: [new DOMPoint(198.38255973344914, 8.868236027966603), new DOMPoint(-153.64897521683866, 5.578032470538176), new DOMPoint(-154.11627140114496, 55.57584876561373), new DOMPoint(143.07549812764987, 58.3535016752606), ],
line: {
start: new DOMPoint(436.3443301443184, -204.04492697123226),
end: new DOMPoint(-82.3443301443184, 260.04492697123226)
},
original: [new DOMPoint(198.3825597334491, 8.868236027966553), new DOMPoint(162.65825355141538, 359.09787638799855), new DOMPoint(112.9163540869709, 354.0240772523315), new DOMPoint(143.0754981276499, 58.353501675260645), ],
intersects: [new DOMPoint(143.07549812764987, 58.3535016752606), new DOMPoint(198.38255973344914, 8.868236027966603), ],
origTopLeft: new DOMPoint(112.9163540869709, 8.868236027966553),
width: 352.49883113459407,
height: 352.49883113459407
}, ],
[{
points: [new DOMPoint(183, 0), new DOMPoint(-115.80000000000018, -398.4000000000001), new DOMPoint(-155.80000000000018, -368.4000000000001), new DOMPoint(158, 50), ],
line: {
start: new DOMPoint(466.81944546997806, -567.6388909399561),
end: new DOMPoint(-126.81944546997806, 619.6388909399561)
},
original: [new DOMPoint(183.00000000000003, 0), new DOMPoint(681, 0), new DOMPoint(681, 50), new DOMPoint(158, 50), ],
intersects: [new DOMPoint(158, 50), new DOMPoint(183, 0), ],
originalTopLeft: new DOMPoint(158, 0),
width: 338.8000000000002,
height: 338.8000000000002
}, ],
[{
points: [new DOMPoint(157.50666666666666, 24.98461538461538), new DOMPoint(232.01174895512395, 458.84515237596656), new DOMPoint(182.7330781575854, 467.307575458501), new DOMPoint(121.1733333333333, 108.830769230769), ],
line: {
start: new DOMPoint(358.8607804360353, -439.6787240831585),
end: new DOMPoint(-43.86078043603533, 489.6787240831585)
},
original: [new DOMPoint(157.50666666666666, 24.9846153846154), new DOMPoint(-210.00917431192647, 267.30275229357795), new DOMPoint(-182.48623853211006, 309.045871559633), new DOMPoint(121.17333333333352, 108.83076923076914), ],
intersects: [new DOMPoint(121.1733333333333, 108.830769230769), new DOMPoint(157.50666666666666, 24.98461538461538), ],
originalTopLeft: new DOMPoint(-210.00917431192647, 24.9846153846154),
width: 110.83841562179065,
height: 110.83841562179065
}, {
points: [new DOMPoint(118.49999999999997, 49.99999999999999), new DOMPoint(207.78082191780817, 127.91780821917807), new DOMPoint(240.6575342465753, 90.24657534246575), new DOMPoint(137.25, -4.9897642155143516e-15), ],
line: {
start: new DOMPoint(199.2848941516392, -165.42638440437122),
end: new DOMPoint(55.71510584836079, 217.42638440437122)
},
original: [new DOMPoint(118.5, 50), new DOMPoint(0, 50), new DOMPoint(0, 0), new DOMPoint(137.25, 0), ],
intersects: [new DOMPoint(137.25, -4.9897642155143516e-15), new DOMPoint(118.49999999999997, 49.99999999999999), ],
originalTopLeft: new DOMPoint(0, 0),
width: 122.15753424657532,
height: 122.15753424657532
}]
];
// reflect, rotate by angle of the longest edge of the pre-reflected shape so that the image renders at the right angle on the page
function getReflectionMatrix(piece, ctx) {
const {
line,
original,
points,
intersects
} = piece;
const anchor = intersects[0]; // point where the line and the other edges meet, used as an origin for reflection
const display = new DOMMatrix();
reflectMatrix(display, line, anchor);
rotateMatrix(display, original, anchor); // i do this so the image shows up at the right angle on the canvas
translateMatrix(display, points, ctx);
return display;
}
function reflectMatrix(matrix, line, anchor) {
const radians = getAngleFromOrigin(line);
const angle = getDegreesFromRadians(radians);
matrix.translateSelf(anchor.x, anchor.y);
matrix.rotateSelf(angle);
matrix.scaleSelf(1, -1);
matrix.rotateSelf(-angle);
}
function rotateMatrix(matrix, originalPoints, anchor) {
const longestEdgeAngle = getLongestEdgeAngle(originalPoints);
const degrees = getDegreesFromRadians(longestEdgeAngle);
matrix.rotateSelf(degrees);
matrix.translateSelf(-anchor.x, -anchor.y);
}
// snap the image to the appropriate point on the axis-aligned bounding box
function translateMatrix(matrix, newPoints, ctx) {
const pt0T = new DOMPoint(0, 0).matrixTransform(matrix);
const { pointsUp, pointsLeft } = getMatrixDirection(matrix);
const corners = getRotatedBoundingBox(newPoints);
let d = "topLeft";
if (pointsUp && pointsLeft) d = "bottomRight";
if (pointsUp && !pointsLeft) d = "bottomLeft";
if (pointsLeft && !pointsUp) d = "topRight";
const target = corners[d];
const dx = target.x - pt0T.x;
const dy = target.y - pt0T.y;
const translated = new DOMMatrix().translateSelf(dx, dy);
matrix.preMultiplySelf(translated);
drawDebugMarker(target.x, target.y, "purple", ctx); // visualises the origin
}
// extracts the up/down and left/right orientation of the matrix as applied to a canvas
function getMatrixDirection(matrix) {
const { a, b, c, d } = matrix;
let pointsLeft = Math.abs(a) >= Math.abs(b) ? a < 0 : b < 0;
let pointsUp = Math.abs(c) >= Math.abs(d) ? c < 0 : d < 0;
return { pointsLeft, pointsUp };
}
// rotated bounding box helpers - everything below is pretty irrelevant to the question afaict
function getRotatedBoundingBox(points) {
const { angle, corners } = getBestBox(points);
const cos = Math.cos(-angle);
const sin = Math.sin(-angle);
const unrotated = corners.map(point => rotatePoint(point, sin, cos));
return sortCorners(unrotated);
}
function sortCorners(points) {
const sorted = points.toSorted((a, b) => a.y == b.y ? a.x - b.x : a.y - b.y);
const [pt1, pt2, pt3, pt4] = sorted;
const [topLeft, topRight] = pt1.x < pt2.x ? [pt1, pt2] : [pt2, pt1];
const [bottomLeft, bottomRight] = pt3.x < pt4.x ? [pt3, pt4] : [pt4, pt3];
return { topLeft, topRight, bottomRight, bottomLeft };
}
function getBestBox(points) {
let bestArea = Infinity;
let bestBox;
for (let i = 0; i < points.length; i++) {
const a = points;
const b = points[(i + 1) % points.length];
const angle = -Math.atan2(b.y - a.y, b.x - a.x);
const cos = Math.cos(angle);
const sin = Math.sin(angle);
const rotated = points.map(point => rotatePoint(point, sin, cos));
const { width, height } = getDimensions(rotated);
const area = width * height;
if (area < bestArea) {
bestArea = area;
bestBox = makeBoundingBox(rotated, angle);
}
}
return bestBox;
}
function rotatePoint(point, sin, cos) {
const { x, y } = point;
return new DOMPoint(rotateX(x, y, sin, cos), rotateY(x, y, sin, cos));
}
function rotateX(x, y, sin, cos) {
return x * cos - y * sin;
}
function rotateY(x, y, sin, cos) {
return x * sin + y * cos;
}
function makeBoundingBox(points, angle) {
const { minX, maxX, minY, maxY } = getBoundingBox(points);
return {
corners: [
new DOMPoint(minX, minY),
new DOMPoint(maxX, minY),
new DOMPoint(maxX, maxY),
new DOMPoint(minX, maxY),
],
angle
};
}
// helpers for getting shape dimensions etc.
function getAngleFromOrigin(line) {
const { start, end } = line;
const dx = end.x - start.x;
const dy = end.y - start.y;
return Math.atan2(dy, dx);
}
function getLongestEdgeAngle(points) {
let maxLength = 0;
let bestAngle = 0;
for (let i = 0; i < points.length; i++) {
const a = points;
const b = points[(i + 1) % points.length];
const dx = b.x - a.x;
const dy = b.y - a.y;
const length = Math.hypot(dx, dy);
if (length > maxLength) {
maxLength = length;
bestAngle = Math.atan2(dy, dx);
}
}
return bestAngle;
}
function getDegreesFromRadians(angle) {
const degrees = angle * 180 / Math.PI;
return ((degrees % 360) + 360) % 360;
}
function getTopLeft(points) {
const { minX, maxX, minY, maxY } = getBoundingBox(points);
return new DOMPoint(minX, minY);
}
function getBoundingBox(points) {
const coordsX = points.map(point => point.x);
const minX = Math.min(...coordsX);
const maxX = Math.max(...coordsX);
const coordsY = points.map(point => point.y);
const minY = Math.min(...coordsY);
const maxY = Math.max(...coordsY);
return {
minX,
maxX,
minY,
maxY
};
}
function getDimensions(points) {
const { minX, maxX, minY, maxY } = getBoundingBox(points);
const width = maxX - minX;
const height = maxY - minY;
return {
width,
height
};
}
// drawing
function loopThroughPieces(test, ctx, testNum) {
for (let i = 0; i < test.length; i++) {
ctx.setTransform(canvasTransform);
const piece = test;
const colour = getColour(i);
const display = getReflectionMatrix(piece, ctx);
drawPiece(piece, colour, display, ctx);
}
}
function getColour(i) {
// red comes first
const hue = (i * 45) % 360;
const lightness = 100 - (40 + 10);
const alpha = 0.5;
return `hsla(${hue}, 90%, ${lightness}%, ${alpha})`;
}
function drawPiece(piece, colour, display, ctx) {
ctx.save();
tracePiecePath(piece.points, ctx);
ctx.globalAlpha = 0.65;
//ctx.clip(); // it's supposed to be clipped, but i unclipped for visualisation, since sometimes the image floats outside of the outline
ctx.setTransform(canvasTransform.multiply(display));
ctx.drawImage(image, 0, 0, image.width, image.height);
ctx.strokeStyle = colour;
ctx.lineWidth = 3;
ctx.globalAlpha = 1;
ctx.stroke();
ctx.restore();
ctx.save();
}
function tracePiecePath(points, ctx) {
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 drawDebugMarker(x, y, colour, ctx) {
ctx.beginPath();
ctx.arc(x, y, 5, 0, 2 * Math.PI);
ctx.fillStyle = colour;
ctx.fill();
}
// everything below is just assembling test cases etc. and rendering them
function makeCanvasTransform() {
canvasTransform.scaleSelf(0.6, 0.6);
canvasTransform.translateSelf(canvasWidth / 2, canvasHeight / 2);
}
function drawDebugImage() {
const imgCtx = image.getContext("2d");
imgCtx.fillStyle = "white";
imgCtx.fillRect(0, 0, image.width, image.height);
imgCtx.font = "20px arial";
imgCtx.textAlign = "center";
imgCtx.fillStyle = "black";
const segmentWidth = image.width / 12;
let offsetX = 0;
for (let i = 0; i < Math.ceil(image.width / segmentWidth); i++) {
imgCtx.strokeRect(offsetX, 0, segmentWidth, image.height);
imgCtx.fillText(i + 1, offsetX + segmentWidth / 2, image.height / 2);
offsetX += segmentWidth;
}
}
function gatherCtxs() {
const ctxs = [];
for (let i = 0; i < pieces.length; i++) {
const canvas = document.createElement("canvas");
canvas.width = canvasWidth;
canvas.height = canvasHeight;
canvases.appendChild(canvas);
if (i % 2 == 1) {
const br = document.createElement("br");
canvases.appendChild(br);
}
ctxs.push(canvas.getContext("2d"));
}
return ctxs;
}
const image = document.getElementById("image");
const canvases = document.getElementById("canvases");
const canvasTransform = new DOMMatrix();
drawDebugImage();
makeCanvasTransform();
const ctxs = gatherCtxs();
for (let i = 0; i < pieces.length; i++) {
loopThroughPieces(pieces, ctxs, i);
}< /code>
canvas {
border: 1px solid grey;
margin: 2px;
}< /code>
< /code>
Подробнее здесь: https://stackoverflow.com/questions/796 ... d-to-a-can