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


156

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


2
Я использовал это для увеличения моего холста, и он прекрасно работает! Единственное, что я должен добавить, это то, что расчет величины увеличения не соответствует ожиданиям. "var zoom = 1 + wheel / 2;" т.е. это приводит к 1,5 для увеличения и 0,5 для уменьшения. Я отредактировал это в моей версии так, чтобы у меня было 1,5 для увеличения и 1 / 1,5 для уменьшения, что делает величину увеличения и уменьшения равной. Так что, если вы увеличите один раз и вернетесь назад, у вас будет то же изображение, что и до увеличения.
Крис

2
Обратите внимание, что это не работает в Firefox, но этот метод может быть легко применен к плагину jQuery mousewheel . Спасибо, что поделился!
Johndodo

2
var zoom = Math.pow (1.5f, колесо); // Используйте это для вычисления увеличения. Преимущество состоит в том, что масштабирование колесом = 2 аналогично двойному масштабированию колесом = 1. Кроме того, увеличение на +2 и уменьшение на +2 восстанавливает исходный масштаб.
Мэтт

Ответы:


126

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

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

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

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


2
Более ценным, чем код «вставь и вставь», является объяснение того, что является лучшим решением и почему оно работает без багажа, особенно если оно длиной в три строки.
Tatarize

2
scalechange = newscale / oldscale?
Теджеш Алимилли

4
Кроме того, я хотел бы добавить для тех, кто стремится получить карту, такую ​​как компонент масштабирования, что мышь X, Y должна быть (mousePosRelativeToContainer - currentTransform) / currentScale, иначе она будет обрабатывать текущую позицию мыши относительно контейнера.
Гилад

1
Да, эта математика предполагает, что масштаб и панорамирование находятся в координатах, относящихся к началу координат. Если они относятся к области просмотра, вы должны настроить их соответствующим образом. Хотя я бы предположил, что правильной математикой является zoomPoint = (mousePosRelativeToContainer + currentTranslation). Эта математика также предполагает, что исходная точка обычно находится в верхнем левом углу поля. Но, приспосабливаясь к слегка нетипичным ситуациям, намного проще, учитывая простоту.
Tatarize

1
@ C.Finke Второй способ сделать это - использовать переводы внутри ctx. Вы рисуете все одинакового размера и в одинаковых позициях. Но вы просто используете умножение матриц внутри холста javascript, чтобы установить панорамирование и масштаб (масштаб) контекста. Так что вместо перерисовки всех фигур в другом месте. Вы рисуете их в том же месте и перемещаете область просмотра в JavaScript. Этот метод также потребует, чтобы вы взяли события мыши и перевели их назад. Таким образом, вы должны вычесть панорамирование, а затем обратить коэффициент на увеличение.
Tatarize

68

Наконец-то решил это:

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.onwheel = 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.deltaY < 0 ? 1 : -1;

    // Compute zoom factor.
    var zoom = Math.exp(wheel*zoomIntensity);
    
    // Translate so the visible origin is at the context's 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's 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>

Ключ, как указал @Tatarize , состоит в том, чтобы вычислить положение оси так, чтобы точка увеличения (указатель мыши) оставалась на том же месте после увеличения.

Первоначально мышь находится на расстоянии mouse/scaleот угла, мы хотим, чтобы точка под мышкой оставалась на том же месте после увеличения, но это mouse/new_scaleдалеко от угла. Поэтому нам нужно сдвинуть origin(координаты угла), чтобы учесть это.

originx -= mousex/(scale*zoom) - mousex/scale;
originy -= mousey/(scale*zoom) - mousey/scale;
scale *= zoom

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


Спасибо, чувак, почти потерял 2 дня, прежде чем найти свой код
Веларо

Эй, я просто искал что-то вроде этого и просто хотел сказать, что ты взломал это!
Крисаллик

26

На самом деле это очень сложная задача (математически), и я почти работаю над тем же. Я задал аналогичный вопрос о 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 transform-origin установлено значение 0, 0 (-moz-transform-origin: 0 0). Использование CSS3-трансформации позволяет вам увеличивать все, что угодно, просто убедитесь, что контейнер DIV настроен на переполнение: скрытый, чтобы остановить увеличение масштабов по краям.

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


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

$(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 вместе с translate. Мне удалось реализовать версию, которая просто использует масштабирование и перевод самостоятельно, проверьте ее здесь http://www.dominicpettifer.co.uk/Files/Mosaic/MosaicTest.html Подождите, пока изображения загрузятся, затем используйте свой колесо мыши для увеличения, также поддерживает панорамирование, перетаскивая изображение вокруг. Он использует CSS3 Transforms, но вы должны иметь возможность использовать те же вычисления для вашего Canvas.


я наконец решил это, заняло у меня 3 минуты сейчас, примерно через две недели, делая что-то еще
csiz

@Synday Ironfoot ссылка на его обновление не работает. Эта ссылка: dominicpettifer.co.uk/Files/Mosaic/MosaicTest.html Я хочу эту реализацию. Вы можете разместить здесь код? спасибо
Богз

2
на сегодняшний день (сентябрь 2014 г.) ссылка на MosaicTest.html не работает.
Крис

демонстрация мозаики исчезла. Я обычно использую vanilla js, а не jQuery. на что ссылается $ (this)? the document.body.offsetTop? Я действительно хочу увидеть демонстрацию мозаики, которую мой проект foreverscape.com мог бы извлечь из этого.
FlavorScape

2
Демонстрационная страница мозаики сохранена на archive.org: web.archive.org/web/20130126152008/http://…
Крис

9

Я столкнулся с этой проблемой, используя c ++, чего, вероятно, не следовало бы использовать, просто для начала я использовал матрицы 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; 
}

9

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

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

Если вместо этого вы хотите увеличить изображение относительно центра, то решение будет следующим: (1) переведите изображение так, чтобы его центр находился в (0, 0); (2) масштабировать изображение по x и y коэффициентам; (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


1
Я должен сказать, что если у вас есть доступная матрица аффинного преобразования, используйте ее с энтузиазмом. Многие матрицы преобразования будут даже иметь функции масштабирования (sx, sy, x, y), которые делают именно это. Это почти стоит готовить один, если вам не дают использовать.
Tatarize

На самом деле, я признаюсь, что в коде, в котором я использовал это решение, с тех пор было заменено его классом матрицы. И я делал это точно несколько раз и готовил матричные классы не менее двух раз. ( github.com/EmbroidePy/pyembroidery/blob/master/pyembroidery/… ), ( github.com/EmbroidePy/EmbroidePy/blob/master/embroidepy/… ). Если вам нужно что-то более сложное, чем именно эти операции, матрица - это, по сути, правильный ответ, и как только вы освоите линейную алгебру, вы поймете, что этот ответ на самом деле лучший ответ.
Tatarize

6

Вот мое решение для центрированного изображения:

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>


4

Вот альтернативный способ сделать это, который использует 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 - перевод) / scale = canvasCoords


3

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

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

Вот ящик:

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();

    }


})

3
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);
}

2

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

$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));

0

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

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

0

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


Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.