Googleマップでズームするように、HTML 5キャンバスでマウスの下のポイントをズームできるようにしたい。どうすればそれを達成できますか?
Googleマップでズームするように、HTML 5キャンバスでマウスの下のポイントをズームできるようにしたい。どうすればそれを達成できますか?
回答:
より良い解決策は、ズームの変化に基づいてビューポートの位置を単に移動することです。ズームポイントは、古いズームと同じままにしたい新しいズームのポイントです。つまり、ズーム前のビューポートとズーム後のビューポートは、ビューポートに対して同じズームポイントを持っています。原点を基準にスケーリングしていると仮定します。それに応じてビューポートの位置を調整できます。
scalechange = newscale - oldscale;
offsetX = -(zoomPointX * scalechange);
offsetY = -(zoomPointY * scalechange);
したがって、実際には、ズームインしたときのポイントを基準にして、ズームしたときの大きさの係数によって、ズームインしたときに右下にパンすることができます。
最後にそれを解決しました:
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
残りのコードは、スケーリングを適用して描画コンテキストに変換し、原点がキャンバスのコーナーと一致するようにする必要があります。
これは実際には(数学的に)非常に難しい問題であり、ほぼ同じことを行っています。Stackoverflowでも同様の質問をしましたが、応答はありませんでしたが、DocType(HTML / CSSのStackOverflow)に投稿して応答を得ました。http://doctype.com/javascript-image-zoom-css3-transforms-calculate-origin-exampleを確認してください。
これを行うjQueryプラグイン(CSS3変換を使用したGoogleマップスタイルのズーム)を構築しています。マウスカーソルへのズームのビットは正常に機能していますが、Googleマップの場合と同じように、ユーザーがキャンバスをドラッグできるようにする方法を理解しようとしています。動作するようになったら、ここにコードを投稿しますが、マウスズームからポイントまでの部分については、上記のリンクを確認してください。
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変換を使用するか、キャンバス自体のスケールおよび変換メソッドを使用するかは、あなた次第ですが、計算については上記のリンクを確認してください。
更新:ええ!リンクをたどるのではなく、ここにコードを投稿します。
$(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変換を使用していますが、キャンバスに対して同じ計算を使用できるはずです。
私はおそらくc ++を使用してこの問題に遭遇しましたが、おそらく最初からOpenGLマトリックスを使用しただけではありませんでした...とにかく、原点が左上隅であるコントロールを使用していて、パン/ズームしたい場合グーグルマップのように、これがレイアウトです(私のイベントハンドラとしてアレグロを使用しています):
// 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;
}
私はTatarizeの答えが好きですが、代わりを提供します。これは自明の線形代数問題であり、私が提示する方法は、パン、ズーム、スキューなどでうまく機能します。つまり、画像がすでに変換されている場合は、うまく機能します。
行列がスケーリングされるとき、スケールはポイント(0、0)にあります。したがって、画像があり、それを2倍に拡大縮小すると、右下の点がx方向とy方向の両方で2倍になります([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
これが中心指向の画像に対する私の解決策です:
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>
これは、scale()とtranslate()の代わりにsetTransform()を使用する別の方法です。すべてが同じオブジェクトに格納されます。キャンバスはページ上の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
ここで、絵を描いたり、動かしたり、ズームしたりする人のために、ちょっと情報を載せておきます。
これは、ズームとビューポートの位置を保存する場合に役立ちます。
ここに引き出しがあります:
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();
}
})
これは、PIXI.jsを使用した@tatarizeの回答のコード実装です。非常に大きな画像(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));
ズームの前後にワールドスペース(スクリーンスペースではなく)でポイントを取得し、デルタで変換する必要があります。
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
scrollto(x、y)関数を使用して、ズーム後に表示する必要があるポイントまでのスクロールバーの位置を処理できます。マウスの位置を見つけるには、event.clientXとevent.clientYを使用します。 これはあなたを助けます
重要なことの1つ...次のようなものがある場合:
body {
zoom: 0.9;
}
あなたはキャンバスで同等のものを作る必要があります:
canvas {
zoom: 1.1;
}