Использование холста для масштабируемого контента
Масштабирование и панорамирование элементов очень проблематично c. Это можно сделать, но список вопросов очень длинный. Я бы никогда не реализовал такой интерфейс.
Подумайте об использовании canvas, через 2D или WebGL для отображения такого контента, чтобы спасти себя от множества проблем.
Первая часть ответа реализована с использованием холст. Тот же интерфейс view
используется во втором примере, который перемещает и масштабирует элемент.
Простой 2D-вид.
Поскольку вы выполняете только панорамирование и масштабирование, тогда очень простой метод может быть used.
В приведенном ниже примере реализован объект с именем view. Он содержит текущий масштаб и положение (панорамирование)
. Он обеспечивает две функции для взаимодействия с пользователем.
- Панорамирование функции
view.pan(amount)
будет панорамировать просмотр по расстоянию в пикселях, удерживаемому amount.x
, amount.y
- Масштабирование функции
view.scaleAt(at, amount)
будет масштабироваться (уменьшать масштаб ) вид на amount
(число, представляющее изменение масштаба), в позиции, занимаемой at.x
, at.y
в пикселях.
В этом примере вид применяется к холсту контекст рендеринга с использованием view.apply()
и набор случайных блоков отображаются при изменении представления. Панорамирование и масштабирование осуществляются с помощью событий мыши
Пример использования контекста 2D-холста
Использование перетаскивания кнопки мыши для панорамирования, колесика для масштабирования
const ctx = canvas.getContext("2d");
canvas.width = 500;
canvas.height = 500;
const rand = (m = 255, M = m + (m = 0)) => (Math.random() * (M - m) + m) | 0;
const objects = [];
for (let i = 0; i < 100; i++) {
objects.push({x: rand(canvas.width), y: rand(canvas.height),w: rand(40),h: rand(40), col: `rgb(${rand()},${rand()},${rand()})`});
}
requestAnimationFrame(drawCanvas);
const view = (() => {
const matrix = [1, 0, 0, 1, 0, 0]; // current view transform
var m = matrix; // alias
var scale = 1; // current scale
var ctx; // reference to the 2D context
const pos = { x: 0, y: 0 }; // current position of origin
var dirty = true;
const API = {
set context(_ctx) { ctx = _ctx; dirty = true },
apply() {
if (dirty) { this.update() }
ctx.setTransform(m[0], m[1], m[2], m[3], m[4], m[5])
},
get scale() { return scale },
get position() { return pos },
isDirty() { return dirty },
update() {
dirty = false;
m[3] = m[0] = scale;
m[2] = m[1] = 0;
m[4] = pos.x;
m[5] = pos.y;
},
pan(amount) {
if (dirty) { this.update() }
pos.x += amount.x;
pos.y += amount.y;
dirty = true;
},
scaleAt(at, amount) { // at in screen coords
if (dirty) { this.update() }
scale *= amount;
pos.x = at.x - (at.x - pos.x) * amount;
pos.y = at.y - (at.y - pos.y) * amount;
dirty = true;
},
};
return API;
})();
view.context = ctx;
function drawCanvas() {
if (view.isDirty()) {
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0, 0, canvas.width, canvas.height);
view.apply(); // set the 2D context transform to the view
for (i = 0; i < objects.length; i++) {
var obj = objects[i];
ctx.fillStyle = obj.col;
ctx.fillRect(obj.x, obj.y, obj.h, obj.h);
}
}
requestAnimationFrame(drawCanvas);
}
canvas.addEventListener("mousemove", mouseEvent, {passive: true});
canvas.addEventListener("mousedown", mouseEvent, {passive: true});
canvas.addEventListener("mouseup", mouseEvent, {passive: true});
canvas.addEventListener("mouseout", mouseEvent, {passive: true});
canvas.addEventListener("wheel", mouseWheelEvent, {passive: false});
const mouse = {x: 0, y: 0, oldX: 0, oldY: 0, button: false};
function mouseEvent(event) {
if (event.type === "mousedown") { mouse.button = true }
if (event.type === "mouseup" || event.type === "mouseout") { mouse.button = false }
mouse.oldX = mouse.x;
mouse.oldY = mouse.y;
mouse.x = event.offsetX;
mouse.y = event.offsetY
if(mouse.button) { // pan
view.pan({x: mouse.x - mouse.oldX, y: mouse.y - mouse.oldY});
}
}
function mouseWheelEvent(event) {
var x = event.offsetX;
var y = event.offsetY;
if (event.deltaY < 0) { view.scaleAt({x, y}, 1.1) }
else { view.scaleAt({x, y}, 1 / 1.1) }
event.preventDefault();
}
body {
background: gainsboro;
margin: 0;
}
canvas {
background: white;
box-shadow: 1px 1px 1px rgba(0, 0, 0, .2);
}
<canvas id="canvas"></canvas>
Пример использования element.style.transform
В этом примере используется свойство преобразования стиля элемента для масштабирования и панорамирования.
Обратите внимание , что я использую 2D-матрицу, а не 3d-матрицу, поскольку это может создать много проблем, несовместимых с простым масштабированием и панорамированием, которые используются ниже.
Обратите внимание , что преобразования CSS не применяются к верхнему левому углу элемента во всех случаях. В приведенном ниже примере начало координат находится в центре элемента. Таким образом, при масштабировании масштаб в точке должен быть скорректирован путем вычитания половины размера элементов. Преобразование не влияет на размер элемента.
Примечание Границы, отступы и поля также изменят местоположение источника. Для работы с view.scaleAt(at, amount)
at
должен быть указан левый верхний пиксель элемента
Примечание есть еще много проблем и предостережений, которые необходимо при масштабировании и панорамировании учитывайте слишком много элементов, чтобы уместиться в одном ответе. Вот почему этот ответ начинается с примера работы с холстом, так как он является гораздо более безопасным методом управления визуальным контентом с возможностью масштабирования.
Используйте перетаскивание кнопкой мыши для перемещения, колесо для увеличения. Если вы потеряете свою позицию (увеличьте масштаб или переместитесь на страницу, перезапустите фрагмент)
const view = (() => {
const matrix = [1, 0, 0, 1, 0, 0]; // current view transform
var m = matrix; // alias
var scale = 1; // current scale
const pos = { x: 0, y: 0 }; // current position of origin
var dirty = true;
const API = {
applyTo(el) {
if (dirty) { this.update() }
el.style.transform = `matrix(${m[0]},${m[1]},${m[2]},${m[3]},${m[4]},${m[5]})`;
},
update() {
dirty = false;
m[3] = m[0] = scale;
m[2] = m[1] = 0;
m[4] = pos.x;
m[5] = pos.y;
},
pan(amount) {
if (dirty) { this.update() }
pos.x += amount.x;
pos.y += amount.y;
dirty = true;
},
scaleAt(at, amount) { // at in screen coords
if (dirty) { this.update() }
scale *= amount;
pos.x = at.x - (at.x - pos.x) * amount;
pos.y = at.y - (at.y - pos.y) * amount;
dirty = true;
},
};
return API;
})();
document.addEventListener("mousemove", mouseEvent, {passive: false});
document.addEventListener("mousedown", mouseEvent, {passive: false});
document.addEventListener("mouseup", mouseEvent, {passive: false});
document.addEventListener("mouseout", mouseEvent, {passive: false});
document.addEventListener("wheel", mouseWheelEvent, {passive: false});
const mouse = {x: 0, y: 0, oldX: 0, oldY: 0, button: false};
function mouseEvent(event) {
if (event.type === "mousedown") { mouse.button = true }
if (event.type === "mouseup" || event.type === "mouseout") { mouse.button = false }
mouse.oldX = mouse.x;
mouse.oldY = mouse.y;
mouse.x = event.pageX;
mouse.y = event.pageY;
if(mouse.button) { // pan
view.pan({x: mouse.x - mouse.oldX, y: mouse.y - mouse.oldY});
view.applyTo(zoomMe);
}
event.preventDefault();
}
function mouseWheelEvent(event) {
const x = event.pageX - (zoomMe.width / 2);
const y = event.pageY - (zoomMe.height / 2);
if (event.deltaY < 0) {
view.scaleAt({x, y}, 1.1);
view.applyTo(zoomMe);
} else {
view.scaleAt({x, y}, 1 / 1.1);
view.applyTo(zoomMe);
}
event.preventDefault();
}
body {
user-select: none;
-moz-user-select: none;
}
.zoomables {
pointer-events: none;
border: 1px solid black;
}
#zoomMe {
position: absolute;
top: 0px;
left: 0px;
}
<img id="zoomMe" class="zoomables" src="https://i.stack.imgur.com/C7qq2.png?s=328&g=1">