Skip to content

Rotation #6

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 53 additions & 5 deletions example/example.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
const canvasDimensions = { width: 700 * 2, height: 1200 * 2 };

function getReferenceContext2d(element, transform) {
const context = element.getContext("2d");
context.scale(transform.scale, transform.scale);
context.translate(transform.x, transform.y);

context.translate(transform.rotation.x, transform.rotation.y);
context.rotate(transform.rotation.angle);
context.translate(-transform.rotation.x, -transform.rotation.y);

return context;
}

Expand All @@ -16,17 +22,21 @@ const referenceCanvas = document.getElementById("reference");
const farCanvas = document.getElementById("far");

const image = { data: document.createElement("img"), width: 320, height: 164 };
const canvasDimensions = { width: 700, height: 1200 };

referenceCanvas.width = canvasDimensions.width;
referenceCanvas.height = canvasDimensions.height;
farCanvas.width = canvasDimensions.width;
farCanvas.height = canvasDimensions.height;

const scale = canvasDimensions.width / image.width;
const focus = 10000; // 500000000 // breaks down in vanilla canvas
const scale = (canvasDimensions.width / image.width) * 0.5;
const focus = -100; // 500000000 // breaks down in vanilla canvas
const rotation = {
x: image.width / 2,
y: image.height / 2,
angle: Math.PI,
};

const diff = -image.height * 0;
const diff = -image.height * 1;

const mkImage = ({ x, y, image }) => ({
x,
Expand All @@ -37,6 +47,7 @@ const mkImage = ({ x, y, image }) => ({
});

const images = [
mkImage({ x: 0, y: focus - 2 * image.height, image }),
mkImage({ x: 0, y: focus - 1 * image.height, image }),
mkImage({ x: 0, y: focus + 0 * image.height, image }),
mkImage({ x: 0, y: focus + 1 * image.height, image }),
Expand All @@ -46,6 +57,7 @@ const images = [
];

const rectangles = [
{ x: 10, y: focus - 200, width: 200, height: 30 },
{ x: 10, y: focus + 20, width: 200, height: 30 },
{ x: 100, y: focus + 250, width: 200, height: 30 },
{ x: -10, y: focus - 10, width: 200, height: 30 },
Expand All @@ -60,12 +72,13 @@ const rectangles = [

const contextReference = getReferenceContext2d(
document.getElementById("reference"),
{ x: 0, y: -focus - diff, scale: scale }
{ x: 0, y: -focus - diff, scale: scale, rotation: rotation }
);
const contextFar = getFarContext2d(document.getElementById("far"), {
x: 0,
y: -focus - diff,
scale: scale,
rotation: rotation,
});

image.data.onload = function () {
Expand Down Expand Up @@ -123,6 +136,41 @@ image.data.onload = function () {

ctx.restore();
});
// focus y
ctx.save();

ctx.beginPath();
ctx.lineWidth = 8;
ctx.strokeStyle = "#0ac";
ctx.moveTo(-2 * image.width, focus);
ctx.lineTo(2 * image.width, focus);

ctx.stroke();
ctx.restore();

// origo
ctx.save();

ctx.beginPath();
ctx.lineWidth = 4;
ctx.strokeStyle = "#f00";
const size = 16;
ctx.arc(0, 0, size, 0, 2 * Math.PI);
ctx.stroke();

ctx.beginPath();
ctx.strokeStyle = "#0f0";
ctx.moveTo(0, 0);
ctx.lineTo(2 * size, 0);
ctx.stroke();

ctx.beginPath();
ctx.strokeStyle = "#00f";
ctx.moveTo(0, 0);
ctx.lineTo(0, 2 * size);
ctx.stroke();

ctx.restore();
}

render(contextReference);
Expand Down
2 changes: 1 addition & 1 deletion example/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,6 @@ <h3 style="text-align: center">reference</h3>
<h3 style="text-align: center">far</h3>
</div>
<script type="application/javascript" src="../lib.web/index.js"></script>
<script type="application/javascript" src="./example.js"></script>
<script type="module" src="./example.js"></script>
</body>
</html>
198 changes: 161 additions & 37 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,67 @@
const isDefined = (o) => ![null, undefined].includes(o);

const getFarContext2d = (canvas, { x = 0, y = 0, scale = 1 } = {}) => {
const d = { x, y, scale };
const getFarContext2d = (
canvas,
{ x = 0, y = 0, scale = 1, rotation = { x: 0, y: 0, angle: 0 } } = {}
) => {
if (![0, Math.PI].includes(rotation.angle)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How come this is the case?

throw new Error("Only 0 and PI rotation angles are supported");
}

const _context = canvas.getContext("2d");

const d = { x, y: y * Math.cos(rotation.angle), scale, rotation };

const s = {
x: (x) => d.scale * (x + d.x),
y: (y) => d.scale * (y + d.y),
distance: (distance) => distance * d.scale,
x: (x) => {
// First, translate, then rotate
const translatedX = x + d.x;
const translatedY = d.y; // y-coordinate remains the same for calculating x
// Apply rotation
const rotatedX =
Math.cos(d.rotation.angle) * (translatedX - d.rotation.x) -
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Math.cos/sin is kinda slow (I'm guessing they don't have a shortcut for PI). But I think it's ok as long as we have much less then 30k objects to render

Math.sin(d.rotation.angle) * (translatedY - d.rotation.y) +
d.rotation.x;
// Finally, apply scaling
return d.scale * rotatedX;
},
y: (y) => {
// First, translate, then rotate
const translatedX = d.x; // x-coordinate remains the same for calculating y
const translatedY = y + d.y;
// Apply rotation
const rotatedY =
Math.sin(d.rotation.angle) * (translatedX - d.rotation.x) +
Math.cos(d.rotation.angle) * (translatedY - d.rotation.y) +
d.rotation.y;
// Finally, apply scaling
return d.scale * rotatedY;
},
distance: (distance) => distance * d.scale, // Scale distances
inv: {
x: (x) => x / d.scale - d.x,
y: (y) => y / d.scale - d.y,
distance: (distance) => distance / d.scale,
x: (x) => {
// First, undo scaling
let unscaledX = x / d.scale;
// Then, undo rotation
const rotatedX =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unrotated?

Math.cos(-d.rotation.angle) * (unscaledX - d.rotation.x) -
Math.sin(-d.rotation.angle) * -d.rotation.y +
d.rotation.x;
// Finally, undo translation
return rotatedX - d.x;
},
y: (y) => {
// First, undo scaling
let unscaledY = y / d.scale;
// Then, undo rotation
const rotatedY =
Math.sin(-d.rotation.angle) * -d.rotation.x +
Math.cos(-d.rotation.angle) * (unscaledY - d.rotation.y) +
d.rotation.y;
// Finally, undo translation
return rotatedY - d.y;
},
distance: (distance) => distance / d.scale, // Undo scaling for distances
},
};

Expand Down Expand Up @@ -295,25 +345,40 @@ const getFarContext2d = (canvas, { x = 0, y = 0, scale = 1 } = {}) => {
drawImage(image, ...args) {
if (args.length === 2) {
const [dx, dy] = args;
return _context.drawImage(

// Save the current context state
_context.save();

// Move to where the image will be drawn and apply rotation
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here _context.translate + _context.rotate is more then enough

_context.translate(s.x(dx), s.y(dy));
_context.rotate(d.rotation.angle);

// Draw the image with its top-left corner at the origin
_context.drawImage(
image,
s.x(dx),
s.y(dy),
0,
0,
s.distance(image.width),
s.distance(image.height)
);

// Restore the context to its original state
_context.restore();
} else if (args.length === 4) {
const [dx, dy, dWidth, dHeight] = args;
return _context.drawImage(
// Similar steps as above, adapted for specified width and height
_context.save();
_context.translate(s.x(dx), s.y(dy));
_context.rotate(d.rotation.angle);
_context.drawImage(
image,
s.x(dx),
s.y(dy),
0,
0,
s.distance(dWidth),
s.distance(dHeight)
);
_context.restore();
} else if (args.length === 8) {
// NOTE see getImageData
const [sx, sy, sWidth, sHeight, dx, dy] = args;
notImplementedYet("drawImage(sx, sy, sWidth, sHeight, dx, dy)");
}
},
Expand Down Expand Up @@ -350,20 +415,46 @@ const getFarContext2d = (canvas, { x = 0, y = 0, scale = 1 } = {}) => {
}
},
fillRect(x, y, width, height) {
return _context.fillRect(
s.x(x),
s.y(y),
s.distance(width),
s.distance(height)
);
// Save the current context state
_context.save();

// Calculate the center of the rectangle
let centerX = x + width / 2;
let centerY = y + height / 2;

// Move to the center of the rectangle
_context.translate(s.x(centerX), s.y(centerY));

// Rotate the context
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the comment is actually wrong, chatgpt ... :P

_context.rotate(d.rotation.angle);

// Move back from the center to the top-left corner of the rectangle
_context.translate(-s.distance(width) / 2, -s.distance(height) / 2);

// Draw the rectangle
_context.fillRect(0, 0, s.distance(width), s.distance(height));

// Restore the context to its original state
_context.restore();
},
fillText(text, x, y, maxWidth = undefined) {
return _context.fillText(
// Save the current context state
_context.save();

// Apply translation and rotation
_context.translate(s.x(x), s.y(y));
_context.rotate(d.rotation.angle);

// Render the text
_context.fillText(
text,
s.x(x),
s.y(y),
0,
0,
isDefined(maxWidth) ? s.distance(maxWidth) : undefined
);

// Restore the context to its original state
_context.restore();
},
getContextAttributes() {
return _context.getContextAttributes();
Expand Down Expand Up @@ -404,12 +495,29 @@ const getFarContext2d = (canvas, { x = 0, y = 0, scale = 1 } = {}) => {
return _context.quadraticCurveTo(s.x(cpx), s.y(cpy), s.x(x), s.y(y));
},
rect(x, y, width, height) {
return _context.rect(
s.x(x),
s.y(y),
s.distance(width),
s.distance(height)
);
// Save the current context state
_context.save();

// Calculate the center of the rectangle
let centerX = x + width / 2;
let centerY = y + height / 2;

// Move to the center of the rectangle
_context.translate(s.x(centerX), s.y(centerY));

// Rotate the context
_context.rotate(d.rotation.angle);

// Move back from the center to the top-left corner of the rectangle
_context.translate(-s.distance(width) / 2, -s.distance(height) / 2);

// Create the rectangle path
_context.beginPath();
_context.rect(0, 0, s.distance(width), s.distance(height));
_context.closePath();

// Restore the context to its original state
_context.restore();
},
resetTransform() {
notSupported("resetTransform");
Expand Down Expand Up @@ -453,12 +561,25 @@ const getFarContext2d = (canvas, { x = 0, y = 0, scale = 1 } = {}) => {
);
},
strokeText(text, x, y, maxWidth = undefined) {
return _context.strokeText(
// Save the current context state
_context.save();

// Translate to the baseline starting point of the text
_context.translate(s.x(x), s.y(y));

// Rotate the context
_context.rotate(d.rotation.angle);

// Draw the text
_context.strokeText(
text,
s.x(x),
s.y(y),
isDefined(maxWidth) ? s.distance(maxWidth) : undefined
0,
0,
maxWidth !== undefined ? s.distance(maxWidth) : undefined
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

was this on purpose or just chatgpt?

);

// Restore the context to its original state
_context.restore();
},
transform(a, b, c, d, e, f) {
notSupported("transform");
Expand All @@ -471,10 +592,13 @@ const getFarContext2d = (canvas, { x = 0, y = 0, scale = 1 } = {}) => {
};
};

export const far = (canvas, { x = 0, y = 0, scale = 1 } = {}) => ({
export const far = (
canvas,
{ x = 0, y = 0, scale = 1, rotation = { x: 0, y: 0, angle: 0 } } = {}
) => ({
getContext: (contextType, contextAttribute) => {
if (contextType == "2d" && !isDefined(contextAttribute)) {
return getFarContext2d(canvas, { x, y, scale });
return getFarContext2d(canvas, { x, y, scale, rotation });
} else {
throw new Error('getContext(contextType != "2d") not implemented');
}
Expand Down