Увеличение масштаба (используя масштаб и перевод)

Я хочу, чтобы иметь возможность приблизить точку под мышью в холсте HTML 5, например масштабирование на Картах Google. Как я могу это достичь?

Ответ 1

Наконец, решил:

var zoomIntensity = 0.2;

var canvas = document.getElementById("canvas");
var context = canvas.getContext("2d");
var width = 600;
var height = 200;

var scale = 1;
var originx = 0;
var originy = 0;
var visibleWidth = width;
var visibleHeight = height;


function draw(){
    // Clear screen to white.
    context.fillStyle = "white";
    context.fillRect(originx,originy,800/scale,600/scale);
    // Draw the black square.
    context.fillStyle = "black";
    context.fillRect(50,50,100,100);
}
// Draw loop at 60FPS.
setInterval(draw, 1000/60);

canvas.onmousewheel = function (event){
    event.preventDefault();
    // Get mouse offset.
    var mousex = event.clientX - canvas.offsetLeft;
    var mousey = event.clientY - canvas.offsetTop;
    // Normalize wheel to +1 or -1.
    var wheel = event.wheelDelta/120;

    // Compute zoom factor.
    var zoom = Math.exp(wheel*zoomIntensity);
    
    // Translate so the visible origin is at the context origin.
    context.translate(originx, originy);
  
    // Compute the new visible origin. Originally the mouse is at a
    // distance mouse/scale from the corner, we want the point under
    // the mouse to remain in the same place after the zoom, but this
    // is at mouse/new_scale away from the corner. Therefore we need to
    // shift the origin (coordinates of the corner) to account for this.
    originx -= mousex/(scale*zoom) - mousex/scale;
    originy -= mousey/(scale*zoom) - mousey/scale;
    
    // Scale it (centered around the origin due to the trasnslate above).
    context.scale(zoom, zoom);
    // Offset the visible origin to it proper position.
    context.translate(-originx, -originy);

    // Update scale and others.
    scale *= zoom;
    visibleWidth = width / scale;
    visibleHeight = height / scale;
}
<canvas id="canvas" width="600" height="200"></canvas>

Ответ 2

Лучшим решением является простое перемещение позиции окна просмотра на основе изменения масштаба. Точка масштабирования - это просто точка старого масштабирования и новый масштаб, который вы хотите оставить неизменным. То есть предварительный просмотр в окне просмотра и увеличенное изображение в окне просмотра имеют одинаковое значение по сравнению с видовым экраном. Учитывая, что мы масштабируемся относительно происхождения. Вы можете соответствующим образом отрегулировать положение видового экрана:

scalechange = newscale - oldscale;
offsetX = -(zoomPointX * scalechange);
offsetY = -(zoomPointY * scalechange);

Таким образом, вы можете просто панорамировать вниз и вправо, когда вы увеличиваете масштаб, в зависимости от того, насколько сильно вы увеличили масштаб, относительно точки, в которой вы увеличили масштаб.

введите описание изображения здесь

Ответ 3

На самом деле это очень сложная проблема (математически), и я почти полностью работаю над тем же. Я задал аналогичный вопрос в Stackoverflow, но не получил ответа, но отправлен в DocType (StackOverflow для HTML/CSS) и получил ответ. Проверьте http://doctype.com/javascript-image-zoom-css3-transforms-calculate-origin-example

Я нахожусь в середине создания плагина jQuery, который делает это (масштабирование в стиле Google Maps с использованием CSS3 Transforms). У меня есть стрелка увеличения до мыши, работающая нормально, все еще пытаясь понять, как разрешить пользователю перетаскивать холст, как вы можете делать в Google Maps. Когда я получу его работу, я отправлю код здесь, но ознакомьтесь с ссылкой выше для части "мышь-зум-точка".

Я не понимал, что есть масштаб и перевод методов в контексте Canvas, вы можете добиться того же, используя CSS3, например. с помощью jQuery:

$('div.canvasContainer > canvas')
    .css('-moz-transform', 'scale(1) translate(0px, 0px)')
    .css('-webkit-transform', 'scale(1) translate(0px, 0px)')
    .css('-o-transform', 'scale(1) translate(0px, 0px)')
    .css('transform', 'scale(1) translate(0px, 0px)');

Убедитесь, что для начала генерации CSS3 установлено значение 0, 0 (-moz-transform-origin: 0 0). Использование преобразования CSS3 позволяет вам увеличить масштаб, просто убедитесь, что для контейнера DIV установлено значение переполнения: скрыто, чтобы остановить увеличенные края, выпадающие из сторон.

Используете ли вы преобразования CSS3, или собственный масштаб и методы перевода могут выполняться до вас, но проверьте приведенную выше ссылку для вычислений.


Обновление: Мех! Я просто отправлю код здесь, вместо того, чтобы вы могли перейти по ссылке:

$(document).ready(function()
{
    var scale = 1;  // scale of the image
    var xLast = 0;  // last x location on the screen
    var yLast = 0;  // last y location on the screen
    var xImage = 0; // last x location on the image
    var yImage = 0; // last y location on the image

    // if mousewheel is moved
    $("#mosaicContainer").mousewheel(function(e, delta)
    {
        // find current location on screen 
        var xScreen = e.pageX - $(this).offset().left;
        var yScreen = e.pageY - $(this).offset().top;

        // find current location on the image at the current scale
        xImage = xImage + ((xScreen - xLast) / scale);
        yImage = yImage + ((yScreen - yLast) / scale);

        // determine the new scale
        if (delta > 0)
        {
            scale *= 2;
        }
        else
        {
            scale /= 2;
        }
        scale = scale < 1 ? 1 : (scale > 64 ? 64 : scale);

        // determine the location on the screen at the new scale
        var xNew = (xScreen - xImage) / scale;
        var yNew = (yScreen - yImage) / scale;

        // save the current screen location
        xLast = xScreen;
        yLast = yScreen;

        // redraw
        $(this).find('div').css('-moz-transform', 'scale(' + scale + ')' + 'translate(' + xNew + 'px, ' + yNew + 'px' + ')')
                           .css('-moz-transform-origin', xImage + 'px ' + yImage + 'px')
        return false;
    });
});

Конечно, вам нужно будет адаптировать его, чтобы использовать масштаб холста и переводить методы.


Обновление 2: Просто заметили, что я использую transform-origin вместе с переводом. Мне удалось реализовать версию, которая просто использует масштаб и переводить самостоятельно, здесь проверьте http://www.dominicpettifer.co.uk/Files/Mosaic/MosaicTest.html Дождитесь загрузки изображений затем используйте колесико мыши для увеличения, также поддерживает панорамирование, перетаскивая изображение вокруг. Он использует CSS3 Transforms, но вы можете использовать те же вычисления для своего Canvas.

Ответ 4

Я столкнулся с этой проблемой, используя С++, и я, вероятно, не должен был использовать только макеты OpenGL для начала... в любом случае, если вы используете элемент управления, происхождение которого находится в верхнем левом углу, и вы хотите панорамирование/масштабирование, например, карты Google, здесь макет (с использованием allegro как моего обработчика событий):

// initialize
double originx = 0; // or whatever its base offset is
double originy = 0; // or whatever its base offset is
double zoom = 1;

.
.
.

main(){

    // ...set up your window with whatever
    //  tool you want, load resources, etc

    .
    .
    .
    while (running){
        /* Pan */
        /* Left button scrolls. */
        if (mouse == 1) {
            // get the translation (in window coordinates)
            double scroll_x = event.mouse.dx; // (x2-x1) 
            double scroll_y = event.mouse.dy; // (y2-y1) 

            // Translate the origin of the element (in window coordinates)      
            originx += scroll_x;
            originy += scroll_y;
        }

        /* Zoom */ 
        /* Mouse wheel zooms */
        if (event.mouse.dz!=0){    
            // Get the position of the mouse with respect to 
            //  the origin of the map (or image or whatever).
            // Let us call these the map coordinates
            double mouse_x = event.mouse.x - originx;
            double mouse_y = event.mouse.y - originy;

            lastzoom = zoom;

            // your zoom function 
            zoom += event.mouse.dz * 0.3 * zoom;

            // Get the position of the mouse
            // in map coordinates after scaling
            double newx = mouse_x * (zoom/lastzoom);
            double newy = mouse_y * (zoom/lastzoom);

            // reverse the translation caused by scaling
            originx += mouse_x - newx;
            originy += mouse_y - newy;
        }
    }
}  

.
.
.

draw(originx,originy,zoom){
    // NOTE:The following is pseudocode
    //          the point is that this method applies so long as
    //          your object scales around its top-left corner
    //          when you multiply it by zoom without applying a translation.

    // draw your object by first scaling...
    object.width = object.width * zoom;
    object.height = object.height * zoom;

    //  then translating...
    object.X = originx;
    object.Y = originy; 
}

Ответ 5

Здесь мое решение для центра-ориентированного изображения:

var MIN_SCALE = 1;
var MAX_SCALE = 5;
var scale = MIN_SCALE;

var offsetX = 0;
var offsetY = 0;

var $image     = $('#myImage');
var $container = $('#container');

var areaWidth  = $container.width();
var areaHeight = $container.height();

$container.on('wheel', function(event) {
    event.preventDefault();
    var clientX = event.originalEvent.pageX - $container.offset().left;
    var clientY = event.originalEvent.pageY - $container.offset().top;

    var nextScale = Math.min(MAX_SCALE, Math.max(MIN_SCALE, scale - event.originalEvent.deltaY / 100));

    var percentXInCurrentBox = clientX / areaWidth;
    var percentYInCurrentBox = clientY / areaHeight;

    var currentBoxWidth  = areaWidth / scale;
    var currentBoxHeight = areaHeight / scale;

    var nextBoxWidth  = areaWidth / nextScale;
    var nextBoxHeight = areaHeight / nextScale;

    var deltaX = (nextBoxWidth - currentBoxWidth) * (percentXInCurrentBox - 0.5);
    var deltaY = (nextBoxHeight - currentBoxHeight) * (percentYInCurrentBox - 0.5);

    var nextOffsetX = offsetX - deltaX;
    var nextOffsetY = offsetY - deltaY;

    $image.css({
        transform : 'scale(' + nextScale + ')',
        left      : -1 * nextOffsetX * nextScale,
        right     : nextOffsetX * nextScale,
        top       : -1 * nextOffsetY * nextScale,
        bottom    : nextOffsetY * nextScale
    });

    offsetX = nextOffsetX;
    offsetY = nextOffsetY;
    scale   = nextScale;
});
body {
    background-color: orange;
}
#container {
    margin: 30px;
    width: 500px;
    height: 500px;
    background-color: white;
    position: relative;
    overflow: hidden;
}
img {
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
    max-width: 100%;
    max-height: 100%;
    margin: auto;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>

<div id="container">
    <img id="myImage" src="http://s18.postimg.org/eplac6dbd/mountain.jpg">
</div>

Ответ 6

Мне нравится ответ Татариза, но я предоставлю альтернативу. Это тривиальная проблема линейной алгебры, и предлагаемый мной метод хорошо работает с панорамированием, масштабированием, перекосом и т.д. То есть он хорошо работает, если ваш образ уже преобразован.

Когда матрица масштабируется, масштаб находится в точке (0, 0). Итак, если у вас есть изображение и масштабируйте его в 2 раза, нижняя правая точка будет удвоена как в направлениях x, так и в направлении y (используя соглашение, которое [0, 0] является левым верхним краем изображения).

Если вместо этого вы хотели бы увеличить изображение относительно центра, то решение будет следующим: (1) перевести изображение таким образом, чтобы его центр находился в (0, 0); (2) масштабировать изображение по х и у факторам; (3) перевести изображение назад. т.е.

myMatrix
  .translate(image.width / 2, image.height / 2)    // 3
  .scale(xFactor, yFactor)                         // 2
  .translate(-image.width / 2, -image.height / 2); // 1

Более абстрактно, одна и та же стратегия работает для любой точки. Если, например, вы хотите масштабировать изображение в точке P:

myMatrix
  .translate(P.x, P.y)
  .scale(xFactor, yFactor)
  .translate(-P.x, -P.y);

И, наконец, если изображение уже каким-то образом преобразовано (например, если оно повернуто, перекошено, переведено или масштабировано), то текущее преобразование необходимо сохранить. В частности, преобразование, определенное выше, должно быть после умножено (или умножено справа) на текущее преобразование.

myMatrix
  .translate(P.x, P.y)
  .scale(xFactor, yFactor)
  .translate(-P.x, -P.y)
  .multiply(myMatrix);

Там у вас это есть. Здесь плунжер, который показывает это в действии. Прокрутите колесико мыши по точкам, и вы увидите, что они постоянно остаются на месте. (Протестировано только в Chrome.) Http://plnkr.co/edit/3aqsWHPLlSXJ9JCcJzgH?p=preview

Ответ 7

Я хочу разместить здесь некоторую информацию для тех, кто делает отдельное рисование изображения и перемещает -изменяет его.

Это может быть полезно, если вы хотите сохранить масштабирование и положение окна просмотра.

Вот ящик:

function redraw_ctx(){
   self.ctx.clearRect(0,0,canvas_width, canvas_height)
   self.ctx.save()
   self.ctx.scale(self.data.zoom, self.data.zoom) // 
   self.ctx.translate(self.data.position.left, self.data.position.top) // position second
   // Here We draw useful scene My task - image:
   self.ctx.drawImage(self.img ,0,0) // position 0,0 - we already prepared
   self.ctx.restore(); // Restore!!!
}

Уведомление шкала ДОЛЖНА быть первой.

И вот масштабирование:

function zoom(zf, px, py){
    // zf - is a zoom factor, which in my case was one of (0.1, -0.1)
    // px, py coordinates - is point within canvas 
    // eg. px = evt.clientX - canvas.offset().left
    // py = evt.clientY - canvas.offset().top
    var z = self.data.zoom;
    var x = self.data.position.left;
    var y = self.data.position.top;

    var nz = z + zf; // getting new zoom
    var K = (z*z + z*zf) // putting some magic

    var nx = x - ( (px*zf) / K ); 
    var ny = y - ( (py*zf) / K);

    self.data.position.left = nx; // renew positions
    self.data.position.top = ny;   
    self.data.zoom = nz; // ... and zoom
    self.redraw_ctx(); // redraw context
    }

и, конечно, нам понадобится драгер:

this.my_cont.mousemove(function(evt){
    if (is_drag){
        var cur_pos = {x: evt.clientX - off.left,
                       y: evt.clientY - off.top}
        var diff = {x: cur_pos.x - old_pos.x,
                    y: cur_pos.y - old_pos.y}

        self.data.position.left += (diff.x / self.data.zoom);  // we want to move the point of cursor strictly
        self.data.position.top += (diff.y / self.data.zoom);

        old_pos = cur_pos;
        self.redraw_ctx();

    }


})

Ответ 8

Здесь альтернативный способ сделать это, который использует setTransform() вместо scale() и translate(). Все хранится в одном объекте. Предполагается, что холст находится на уровне 0,0 на странице, иначе вам нужно будет вычесть его позицию из кодовых страниц.

this.zoomIn = function (pageX, pageY) {
    var zoomFactor = 1.1;
    this.scale = this.scale * zoomFactor;
    this.lastTranslation = {
        x: pageX - (pageX - this.lastTranslation.x) * zoomFactor,
        y: pageY - (pageY - this.lastTranslation.y) * zoomFactor
    };
    this.canvasContext.setTransform(this.scale, 0, 0, this.scale,
                                    this.lastTranslation.x,
                                    this.lastTranslation.y);
};
this.zoomOut = function (pageX, pageY) {
    var zoomFactor = 1.1;
    this.scale = this.scale / zoomFactor;
    this.lastTranslation = {
        x: pageX - (pageX - this.lastTranslation.x) / zoomFactor,
        y: pageY - (pageY - this.lastTranslation.y) / zoomFactor
    };
    this.canvasContext.setTransform(this.scale, 0, 0, this.scale,
                                    this.lastTranslation.x,
                                    this.lastTranslation.y);
};

Сопровождающий код для обработки панорамирования:

this.startPan = function (pageX, pageY) {
    this.startTranslation = {
        x: pageX - this.lastTranslation.x,
        y: pageY - this.lastTranslation.y
    };
};
this.continuePan = function (pageX, pageY) {
    var newTranslation = {x: pageX - this.startTranslation.x,
                          y: pageY - this.startTranslation.y};
    this.canvasContext.setTransform(this.scale, 0, 0, this.scale,
                                    newTranslation.x, newTranslation.y);
};
this.endPan = function (pageX, pageY) {
    this.lastTranslation = {
        x: pageX - this.startTranslation.x,
        y: pageY - this.startTranslation.y
    };
};

Чтобы получить ответ самостоятельно, считайте, что одни и те же координаты страницы должны совпадать с теми же координатами холста до и после масштабирования. Затем вы можете сделать некоторую алгебру, начиная с этого уравнения:

(pageCoords - translation)/scale = canvasCoords

Ответ 9

if(wheel > 0) {
    this.scale *= 1.1; 
    this.offsetX -= (mouseX - this.offsetX) * (1.1 - 1);
    this.offsetY -= (mouseY - this.offsetY) * (1.1 - 1);
}
else {
    this.scale *= 1/1.1; 
    this.offsetX -= (mouseX - this.offsetX) * (1/1.1 - 1);
    this.offsetY -= (mouseY - this.offsetY) * (1/1.1 - 1);
}

Ответ 10

Здесь выполняется реализация кода ответа @tatarize с использованием PIXI.js. У меня есть окно просмотра, которое смотрит на часть очень большого изображения (например, стиль карт Google).

$canvasContainer.on('wheel', function (ev) {

    var scaleDelta = 0.02;
    var currentScale = imageContainer.scale.x;
    var nextScale = currentScale + scaleDelta;

    var offsetX = -(mousePosOnImage.x * scaleDelta);
    var offsetY = -(mousePosOnImage.y * scaleDelta);

    imageContainer.position.x += offsetX;
    imageContainer.position.y += offsetY;

    imageContainer.scale.set(nextScale);

    renderer.render(stage);
});
  • $canvasContainer - мой html-контейнер.
  • imageContainer - это мой контейнер PIXI, в котором есть изображение.
  • mousePosOnImage - это положение мыши относительно всего изображения (а не только порт представления).

Вот как я получил положение мыши:

  imageContainer.on('mousemove', _.bind(function(ev) {
    mousePosOnImage = ev.data.getLocalPosition(imageContainer);
    mousePosOnViewport.x = ev.data.originalEvent.offsetX;
    mousePosOnViewport.y = ev.data.originalEvent.offsetY;
  },self));

Ответ 11

Вам нужно получить точку в мировом пространстве (напротив экрана) до и после масштабирования, а затем перевести по дельта.

mouse_world_position = to_world_position(mouse_screen_position);
zoom();
mouse_world_position_new = to_world_position(mouse_screen_position);
translation += mouse_world_position_new - mouse_world_position;

Положение мыши находится в пространстве экрана, поэтому вам нужно преобразовать его в мировое пространство. Простое преобразование должно быть похоже на следующее:

world_position = screen_position / scale - translation

Ответ 12

вы можете использовать функцию scrollto (x, y) для обработки положения полосы прокрутки вправо до точки, которую необходимо отобразить после масштабирования. Для нахождения позиции мыши используйте event.clientX и event.clientY. это поможет вам