Hello I need some help with applying an answer on another question to my code. I'm currently trying to recreate aseprite's zoom functionality but, with my current code, when I zoom on different sides of the canvas it shifts a couple of pixels.
Previously I was told that my question was a duplicate of: Zoom in on a point (using scale and translate)
And I have looked at: https://stackoverflow.com/a/3151987/24209432, but I can't seem to get their answer to work with my code. Please help me with this.
Here is a gif displaying what currently happens: Zoom shift problem
And here is how it should work: Aseprite zoom
Here is my relavant code:
<html>
<body>
<main>
<section Id="canvas-panel">
<section
id="canvas"
style="width: 256px; height: 256px; background-size: 12.5%"
>
<canvas width="256px" height="256px"></canvas>
</section>
</section>
</main>
</body>
</html>
<script>
let canvasPanel = document.getElementById("canvas-panel");
let canvas = document.getElementById("canvas");
// Function to calculate the closest point on the border of the canvas to the click position.
function calculateClosestBorderPoint(event) {
const canvasRect = canvas.getBoundingClientRect();
const clickX = event.clientX;
const clickY = event.clientY;
let closestX = canvasRect.left;
let closestY = canvasRect.top;
// Determine if the click is closer to the left or right side of the canvas.
if (clickX < canvasRect.left) {
closestX = canvasRect.left;
} else if (clickX > canvasRect.right) {
closestX = canvasRect.right;
} else {
closestX = clickX;
}
// Determine if the click is closer to the top or bottom of the canvas.
if (clickY < canvasRect.top) {
closestY = canvasRect.top;
} else if (clickY > canvasRect.bottom) {
closestY = canvasRect.bottom;
} else {
closestY = clickY;
}
return { x: closestX, y: closestY };
}
// Initialize scale.
let scale = 1;
// Define zoom limits
const minScale = 0.5; // Minimum zoom limit.
const maxScale = 4.0; // Maximum zoom limit.
// Function to update the canvas transformation.
function updateCanvasTransform() {
canvas.style.transform = `scale(${scale})`;
}
canvasPanel.addEventListener("wheel", function (e) {
e.preventDefault();
const zoomDirection = e.deltaY < 0 ? 1 : -1;
if (scale == minScale && zoomDirection == -1) return;
if (scale == maxScale && zoomDirection == 1) return;
const zoomFactor = 1.1;
const closestPoint = calculateClosestBorderPoint(event);
scale *= zoomFactor ** zoomDirection; // Apply zoom factor based on direction
// Clamp scale to limits
scale = Math.min(scale, maxScale);
scale = Math.max(scale, minScale);
// Update canvas position to keep the closest point centered relative to the viewport
const canvasRect = canvas.getBoundingClientRect();
const viewportCenterX = canvasRect.width / 2;
const viewportCenterY = canvasRect.height / 2;
// Convert closest point to relative position within the viewport (0-1 scale)
let relativeX =
((closestPoint.x - canvasRect.left) / canvasRect.width) * 100;
let relativeY =
((closestPoint.y - canvasRect.top) / canvasRect.height) * 100;
canvas.style.transformOrigin = `${relativeX}% ${relativeY}%`;
// Update canvas transformation using only scale
updateCanvasTransform();
});
</script>
<style>
#canvas {
position: fixed;
overflow: hidden;
padding: 0;
margin: 0;
transform: scale(1) translate(0px, 0px);
background-color: #1d1d1d;
}
#canvas-panel {
position: relative;
width: 100%;
height: 100%;
background-color: #303030;
}
html,
body {
background-color: #161616;
margin: 0;
width: 100%;
height: 100%;
}
</style>
This worked, good luck people that find this later! :)
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<div id="canvas-panel" onclick="calculateClosestBorderPoint(event)">
<div id="canvas"></div>
</div>
<style>
body {
background-color: orange;
}
#canvas-panel {
margin: 0;
width: 800px;
height: 800px;
background-color: white;
position: relative;
overflow: hidden;
}
#canvas {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
margin: auto;
background-color: red;
width: 600px;
height: 600px;
}
</style>
<script>
const MIN_SCALE = 0.2;
const MAX_SCALE = 10;
const zoomFactor = 1.2;
var scale = 1;
var offsetX = 0;
var offsetY = 0;
var $canvas = $("#canvas");
var $canvasPanel = $("#canvas-panel");
var canvasPanelWidth = $canvasPanel.width();
var canvasPanelHeight = $canvasPanel.height();
var canvasWidth = $canvas.width();
var canvasHeight = $canvas.height();
$(document).ready(setMargin);
// Set the margin of the canvas for when the canvas is bigger than the canvasPanel.
// This is all because margin: auto; doesn't work when the canvas is bigger than the panel.
function setMargin() {
var marginX = (canvasPanelWidth - canvasWidth) / 2;
var marginY = (canvasPanelHeight - canvasHeight) / 2;
$canvas.css('margin', `${marginY} ${marginX}`);
}
function calculateClosestBorderPoint(rect, x, y) {
const clickX = x;
const clickY = y;
let closestX = rect.left;
let closestY = rect.top;
// Determine if the click is closer to the left or right side of the canvas.
if (clickX < rect.left) {
closestX = rect.left;
} else if (clickX > rect.right) {
closestX = rect.right;
} else {
closestX = clickX;
}
// Determine if the click is closer to the top or bottom of the canvas.
if (clickY < rect.top) {
closestY = rect.top;
} else if (clickY > rect.bottom) {
closestY = rect.bottom;
} else {
closestY = clickY;
}
return { x: closestX, y: closestY };
}
$canvasPanel.on("wheel", function (event) {
event.preventDefault();
var closestPoint = calculateClosestBorderPoint(
canvas.getBoundingClientRect(),
event.originalEvent.pageX,
event.originalEvent.pageY
);
var borderX = closestPoint.x - $canvasPanel.offset().left;
var borderY = closestPoint.y - $canvasPanel.offset().top;
var zoomDirection = event.originalEvent.deltaY < 0 ? 1 : -1;
if (scale == MIN_SCALE && zoomDirection == -1) return;
if (scale == MAX_SCALE && zoomDirection == 1) return;
var nextScale = scale * zoomFactor ** zoomDirection;
nextScale = Math.min(nextScale, MAX_SCALE);
nextScale = Math.max(nextScale, MIN_SCALE);
var percentXInCurrentBox = borderX / canvasPanelWidth;
var percentYInCurrentBox = borderY / canvasPanelHeight;
var currentBoxWidth = canvasPanelWidth / scale;
var currentBoxHeight = canvasPanelHeight / scale;
var nextBoxWidth = canvasPanelWidth / nextScale;
var nextBoxHeight = canvasPanelHeight / nextScale;
var deltaX =
(nextBoxWidth - currentBoxWidth) * (percentXInCurrentBox - 0.5);
var deltaY =
(nextBoxHeight - currentBoxHeight) * (percentYInCurrentBox - 0.5);
var nextOffsetX = offsetX - deltaX;
var nextOffsetY = offsetY - deltaY;
$canvas.css({
transform: "scale(" + nextScale + ")",
left: -1 * nextOffsetX * nextScale,
right: nextOffsetX * nextScale,
top: -1 * nextOffsetY * nextScale,
bottom: nextOffsetY * nextScale,
});
offsetX = nextOffsetX;
offsetY = nextOffsetY;
scale = nextScale;
});
</script>