Координаты курсора при масштабировании canvas js
При написании функции масштабирования внутри холста возникла проблема с расчётом координат. Если курсор находится в центре холста, масштабирование происходит равномерно во все стороны но если курсор в любой другой точке то изображение масштабируется некорректно и уходит в сторону. Как правильнее переопределить координаты таким образом, чтобы при любом положении курсора изображение масштабировалось от него равномерно во все стороны?
function scale() {
var canvas = document.getElementById('canvas');
var context = canvas.getContext('2d');
const canvas_width = document.documentElement.scrollWidth;
const canvas_height = document.documentElement.scrollHeight;
canvas.addEventListener("wheel", onmousewheel, false);
canvas.addEventListener("DOMMouseScroll", onmousewheel, false);
var position = {x: 0, y: 0};
function onmousewheel(event) {
var x = event.clientX - position.x + canvas_width/2;
var y = event.clientY - position.y + canvas_height/2;
const delta = event.type === "wheel" ? event.wheelDelta : -event.detail;
const increment = delta > 0 ? 1.1 : 0.9;
const previous_scale = scale;
scale *= increment;
position.x -= x * (scale / previous_scale - 1);
position.y -= y * (scale / previous_scale - 1);
context.clearRect(0, 0, canvas.width, canvas.height);
context.setTransform(scale, 0, 0, scale, position.x, position.y);
CANVAS.draw(); // отдельная функция отрисовки всех элементов на холсте
event.preventDefault();
}
}
Также интересует, как можно сделать анимацию холста в момент масштабирования более плавной и без резких рывков?
Заранее благодарю за ответ.
Ответы (1 шт):
Вероятно, это то, что вам нужно ( при условии, что scale() вызывается один раз для подписки на события wheel ):
function scale() {
const canvasWidth = canvas.width;
const canvasHeight = canvas.height;
canvas.addEventListener("wheel", onmousewheel, false);
canvas.addEventListener("DOMMouseScroll", onmousewheel, false);
// Прямоугольник видимости или окно, в котором мы работаем
const currentArea = {x1: 0, y1: 0, x2: canvasWidth, y2: canvasHeight};
let scale = 1.0;
function onmousewheel(event) {
const mouseX = event.clientX;
const mouseY = event.clientY;
const delta = event.type === "wheel" ? event.wheelDelta : -event.detail;
const increment = delta > 0 ? 1.1 : 0.9;
scale *= increment;
currentArea.x1 -= mouseX;
currentArea.y1 -= mouseY;
currentArea.x2 -= mouseX;
currentArea.y2 -= mouseY;
currentArea.x1 *= increment;
currentArea.y1 *= increment;
currentArea.x2 *= increment;
currentArea.y2 *= increment;
currentArea.x1 += mouseX;
currentArea.y1 += mouseY;
currentArea.x2 += mouseX;
currentArea.y2 += mouseY;
// canvasScale == scale, поэтому их можно использовать равнозначно
const canvasScale = (currentArea.x2 - currentArea.x1) / canvasWidth;
context.setTransform(1, 0, 0, 1, 0, 0); // Чтобы не оставалось следов от масштабирования
context.clearRect(0, 0, canvasWidth, canvasHeight);
context.setTransform(canvasScale, 0, 0, canvasScale, currentArea.x1, currentArea.y1);
CANVAS.draw(); // отдельная функция отрисовки всех элементов на холсте
event.preventDefault();
}
}
Для того, чтобы сделать увеличение плавным, можно завести функцию, которая каждый кадр будет рисовать на холсте изображение. Тогда, при событии wheel мы можем рассчитать "желаемое" увеличение, а уже в рисующей функции плавно подходить к нему. Плавный подход уже можно сделать через интерполяцию:
// Вот это и есть интерполирующая функция
function lerp(a, b, x) {
return a + (b - a) * x;
}
const canvasWidth = canvas.width;
const canvasHeight = canvas.height;
// Вот сюда будут записываться "желаемые" значения
const desiredArea = {x1: 0, y1: 0, x2: canvasWidth, y2: canvasHeight};
// А здесь будет храниться текущее окно ( увеличение )
const currentArea = {x1: 0, y1: 0, x2: canvasWidth, y2: canvasHeight};
function scale() {
canvas.addEventListener("wheel", onmousewheel, false);
canvas.addEventListener("DOMMouseScroll", onmousewheel, false);
function onmousewheel(event) {
const mouseX = event.clientX;
const mouseY = event.clientY;
const delta = event.type === "wheel" ? event.wheelDelta : -event.detail;
const increment = delta > 0 ? 1.1 : 0.9;
desiredArea.x1 -= mouseX;
desiredArea.y1 -= mouseY;
desiredArea.x2 -= mouseX;
desiredArea.y2 -= mouseY;
desiredArea.x1 *= increment;
desiredArea.y1 *= increment;
desiredArea.x2 *= increment;
desiredArea.y2 *= increment;
desiredArea.x1 += mouseX;
desiredArea.y1 += mouseY;
desiredArea.x2 += mouseX;
desiredArea.y2 += mouseY;
event.preventDefault();
}
}
function draw() {
// Вот тут можно поиграться с плавностью приближения, только значения должны быть
// от 0 и до 1
const lerpCoefficient = 0.5; // 0.5 даёт хорошее отношение между отзывчивостью и плавностью
currentArea.x1 = lerp(currentArea.x1, desiredArea.x1, lerpCoefficient);
currentArea.y1 = lerp(currentArea.y1, desiredArea.y1, lerpCoefficient);
currentArea.x2 = lerp(currentArea.x2, desiredArea.x2, lerpCoefficient);
currentArea.y2 = lerp(currentArea.y2, desiredArea.y2, lerpCoefficient);
const canvasScale = (currentArea.x2 - currentArea.x1) / canvasWidth;
context.setTransform(1, 0, 0, 1, 0, 0); // Чтобы не оставалось следов от масштабирования
context.clearRect(0, 0, canvasWidth, canvasHeight);
context.setTransform(canvasScale, 0, 0, canvasScale, currentArea.x1, currentArea.y1);
CANVAS.draw(); // отдельная функция отрисовки всех элементов на холсте
requestAnimationFrame(() => draw());
}
draw();
Это всё примеры реализаций, и при желании, я думаю, их можно будет оформить конкрентно под ваш случай ( например: вынести функционал увеличения и глобальные переменные currentArea и desiredArea куда-нибудь в отдельный класс, который будет заниматься отрисовкой ).
Посмотреть пример вживую можно ниже:
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d");
canvas.width = window.screen.width;
canvas.height = window.screen.height;
document.body.append(canvas);
const testImg = new Image();
testImg.src = "https://i.imgur.com/6yHmlwT.jpeg";
// Позиция курсора
let x = 0;
let y = 0;
const CANVAS = {
draw: () => {
if(context === null) {
return;
}
context.fillStyle = "#000";
context.fillRect(0, 0, 1920, 1080);
const canvasScale = (currentArea.x2 - currentArea.x1) / canvasWidth;
context.drawImage(testImg, 0, 0);
context.fillStyle = "red";
context.beginPath();
// Преобразовываем позицию курсора
context.arc((x - currentArea.x1) / canvasScale, (y - currentArea.y1) / canvasScale, 5, 0, 2*Math.PI);
context.fill();
context.closePath();
context.strokeWidth = 2;
context.strokeStyle = "#00f";
context.strokeRect((100 - currentArea.x1) / canvasScale, (100 - currentArea.y1) / canvasScale, 100 / canvasScale, 50 / canvasScale);
}
}
// Вот это и есть интерполирующая функция
function lerp(a, b, x) {
return a + (b - a) * x;
}
const canvasWidth = canvas.width;
const canvasHeight = canvas.height;
// Вот сюда будут записываться "желаемые" значения
const desiredArea = {x1: 0, y1: 0, x2: canvasWidth, y2: canvasHeight};
// А здесь будет храниться текущее окно ( увеличение )
const currentArea = {x1: 0, y1: 0, x2: canvasWidth, y2: canvasHeight};
function scale() {
canvas.addEventListener("wheel", onmousewheel, false);
canvas.addEventListener("DOMMouseScroll", onmousewheel, false);
function onmousewheel(event) {
const mouseX = event.clientX;
const mouseY = event.clientY;
const delta = event.type === "wheel" ? event.wheelDelta : -event.detail;
const increment = delta > 0 ? 1.1 : 0.9;
x = mouseX;
y = mouseY;
desiredArea.x1 -= mouseX;
desiredArea.y1 -= mouseY;
desiredArea.x2 -= mouseX;
desiredArea.y2 -= mouseY;
desiredArea.x1 *= increment;
desiredArea.y1 *= increment;
desiredArea.x2 *= increment;
desiredArea.y2 *= increment;
desiredArea.x1 += mouseX;
desiredArea.y1 += mouseY;
desiredArea.x2 += mouseX;
desiredArea.y2 += mouseY;
event.preventDefault();
}
}
function draw() {
// Вот тут можно поиграться с плавностью приближения, только значения должны быть
// от 0 и до 1
const lerpCoefficient = 0.5; // 0.5 даёт хорошее отношение между отзывчивостью и плавностью
currentArea.x1 = lerp(currentArea.x1, desiredArea.x1, lerpCoefficient);
currentArea.y1 = lerp(currentArea.y1, desiredArea.y1, lerpCoefficient);
currentArea.x2 = lerp(currentArea.x2, desiredArea.x2, lerpCoefficient);
currentArea.y2 = lerp(currentArea.y2, desiredArea.y2, lerpCoefficient);
const canvasScale = (currentArea.x2 - currentArea.x1) / canvasWidth;
context.setTransform(1, 0, 0, 1, 0, 0); // Чтобы не оставалось следов от масштабирования
context.clearRect(0, 0, canvasWidth, canvasHeight);
context.setTransform(canvasScale, 0, 0, canvasScale, currentArea.x1, currentArea.y1);
CANVAS.draw(); // отдельная функция отрисовки всех элементов на холсте
requestAnimationFrame(() => draw());
}
document.addEventListener("mousemove", e => {
x = e.clientX;
y = e.clientY;
})
draw();
scale();
html, body {
margin: 0;
padding: 0;
overflow: hidden;
}