How to zoom in and center on an object with fabricjs? - javascript

I would like to be able to click on an object, and have it zoomed to its boundingbox in the canvas viewport. How do I accomplish that? See http://jsfiddle.net/tinodb/qv989nzs/8/ for what I would like to get working.
Fabricjs' canvas has the zoomToPoint method (about which the docs say: Sets zoom level of this canvas instance, zoom centered around point), but that does not center to the given point, but it does work for zooming with scrolling. See http://jsfiddle.net/qv989nzs/
I tried several other approaches, like using canvas.setViewportTransform:
// centers a circle positioned at (200, 150)??
canvas.setViewportTransform([2, 0, 0, 2, -250, -150])
But I can't find the relation between the last two parameters to setViewportTransform and the position of the object.
(Btw, another problem is with the first example fiddle, that the zooming only works on the first click. Why is that?)

I found a way to do this, which is composed of:
canvas.setZoom(1) // reset zoom so pan actions work as expected
vpw = canvas.width / zoom
vph = canvas.height / zoom
x = (object.left - vpw / 2) // x is the location where the top left of the viewport should be
y = (object.top - vph / 2) // y idem
canvas.absolutePan({x:x, y:y})
canvas.setZoom(zoom)
See http://jsfiddle.net/tinodb/4Le8n5xd/ for a working example.
I was unable to get it to work with zoomToPoint and setViewportTransform (the latter of which does strange things, see for example http://jsfiddle.net/qv989nzs/9/ and click the blue circle; it is supposed to put the top left viewport at (25, 25), but it does not)

Here's an example of how to do it with setViewportTransform:
// first set the zoom, x, and y coordinates
var zoomLevel = 2;
var objectLeft = 250;
var objectTop = 150;
// then calculate the offset based on canvas size
var newLeft = (-objectLeft * zoomLevel) + canvas.width / 2;
var newTop = (-objectTop * zoomLevel) + canvas.height / 2;
// update the canvas viewport
canvas.setViewportTransform([zoomLevel, 0, 0, zoomLevel, newLeft, newTop]);

Related

How would I go about implementing a moving origin on zoom on a canvas?

I'm having some issues with implementing a custom zoom.
I have most of the zoom functionality working, however, it zooms "around" the origin, in this case, the top-left corner of the grid. Instead, I want it to zoom in around the cursor.
The canvas is as big as the window (I'll be adding UI on top of the canvas, kinda like photoshop), and I'm drawing a grid to the canvas that is WIDTH x HEIGHT pixels large, "scaled up" by a factor of PIXEL_SIZE.
The important part of this, this event handler right here
const WIDTH = 32;
const HEIGHT = 24;
const PIXEL_SIZE = 10;
...
// controls how big the grid is
let scale = 1.5;
// controls where the grid is drawn,
// starts exactly in the middle of the screen
let originX = canvas.width / 2 - (WIDTH * PIXEL_SIZE * scale) / 2;
let originY = canvas.height / 2 - (HEIGHT * PIXEL_SIZE * scale) / 2;
function zoom(event) {
event.preventDefault();
const wheel = event.deltaY < 0 ? 1 : -1;
const z = Math.exp(wheel * 0.05);
scale *= z;
// this adjusts the scale just fine-ish
// however, the origin needs to move in relation
// to the cursor position
}
window.addEventListener("wheel", zoom, { passive: false });
I've tried a few things, but for some reason, my brain can't do the math at the moment. I've tried using ctx.translate and ctx.scale, but neither of them seems to do what I want to do (though I could be using them wrong).
Here's a JSFiddle with what I've got so far. If someone could take a look, that would be awesome.
Pretty cool code. Zoooming in on a specific point can be tricky and could be done a number of ways. The main problem is where exactly do you want to zoom and at what scale/how fast when nearing the max zoom.
I would suggest starting off with defining an offset
...
// starts exactly in the middle of the screen
let originX = canvas.width / 2 - (WIDTH * PIXEL_SIZE * scale) / 2;
let originY = canvas.height / 2 - (HEIGHT * PIXEL_SIZE * scale) / 2;
let offsetX = originX;
let offsetY = originY;
...
And then computing the offset using event.x and event.y of the MouseEvent in the zoom callback, similar to:
function zoom(event) {
...
// event.x and event.y returns the position relative to ...something
// To properly scale the values, you could get the relative position
// and size of the canvas and find your offset, for example
// (viwportsizeX - canvasSizeX) + event.X / (scaleX) and the same for Y
// when the position is realtive to the viewport
offsetX = event.x;
offsetY = event.y;
}
And of course update the offset in the animation mainloop:
function animate() {
requestAnimationFrame(animate);
ctx.clearRect(0, 0, canvas.width, canvas.height);
bufferCtx.putImageData(data, 0, 0);
ctx.drawImage(
bufferCanvas,
originX - offsetX,
originY - offsetY,
WIDTH * PIXEL_SIZE * scale,
HEIGHT * PIXEL_SIZE * scale
);
}
Zooming in exactly on the point of the cursor is possible by computing the offsets relative to the canvas element being zoomed on.
JSFiddle

Accounting for Canvas Size Differences when Drawing on Image with Stored Coordinates

I'm struggling to find a method/strategy to handle drawing with stored coordinates and the variation in canvas dimensions across various devices and screen sizes for my web app.
Basically I want to display an image on the canvas. The user will mark two points on an area of image and the app records where these markers were placed. The idea is that the user will use the app every odd day, able to see where X amount of previous points were drawn and able to add two new ones to the area mentioned in places not already marked by previous markers. The canvas is currently set up for height = window.innerHeight and width = window.innerWidth/2.
My initial thought was recording the coordinates of each pair of points and retrieving them as required so they can be redrawn. But these coordinates don't match up if the canvas changes size, as discovered when I tested the web page on different devices. How can I record the previous coordinates and use them to mark the same area of the image regardless of canvas dimensions?
Use percentages! Example:
So lets say on Device 1 the canvas size is 150x200,
User puts marker on pixel 25x30. You can do some math to get the percentage.
And then you SAVE that percentage, not the location,
Example:
let userX = 25; //where the user placed a marker
let canvasWidth = 150;
//Use a calculator to verify :D
let percent = 100 / (canvasWidth / userX); //16.666%
And now that you have the percent you can set the marker's location based on that percent.
Example:
let markerX = (canvasWidth * percent) / 100; //24.999
canvasWidth = 400; //Lets change the canvas size!
markerX = (canvasWidth * percent) / 100; //66.664;
And voila :D just grab the canvas size and you can determine marker's location every time.
Virtual Canvas
You must define a virtual canvas. This is the ideal canvas with a predefined size, all coordinates are relative to this canvas. The center of this virtual canvas is coordinate 0,0
When a coordinate is entered it is converted to the virtual coordinates and stored. When rendered they are converted to the device screen coordinates.
Different devices have various aspect ratios, even a single device can be tilted which changes the aspect. That means that the virtual canvas will not exactly fit on all devices. The best you can do is ensure that the whole virtual canvas is visible without stretching it in x, or y directions. this is called scale to fit.
Scale to fit
To render to the device canvas you need to scale the coordinates so that the whole virtual canvas can fit. You use the canvas transform to apply the scaling.
To create the device scale matrix
const vWidth = 1920; // virtual canvas size
const vHeight = 1080;
function scaleToFitMatrix(dWidth, dHeight) {
const scale = Math.min(dWidth / vWidth, dHeight / vHeight);
return [scale, 0, 0, scale, dWidth / 2, dHeight / 2];
}
const scaleMatrix = scaleToFitMatrix(innerWidth, innerHeight);
Scale position not pixels
Point is defined as a position on the virtual canvas. However the transform will also scale the line widths, and feature sizes which you would not want on very low or high res devices.
To keep the same pixels size but still render in features in pixel sizes you use the inverse scale, and reset the transform just before you stroke as follows (4 pixel box centered over point)
const point = {x : 0, y : 0}; // center of virtual canvas
const point1 = {x : -vWidth / 2, y : -vHeight / 2}; // top left of virtual canvas
const point2 = {x : vWidth / 2, y : vHeight / 2}; // bottom right of virtual canvas
function drawPoint(ctx, matrix, vX, vY, pW, pH) { // vX, vY virtual coordinate
const invScale = 1 / matrix[0]; // to scale to pixel size
ctx.setTransform(...matrix);
ctx.lineWidth = 1; // width of line
ctx.beginPath();
ctx.rect(vX - pW * 0.5 * invScale, vY - pH * 0.5 * invScale, pW * invScale, pH * invScale);
ctx.setTransform(1,0,0,1,0,0); // reset transform for line width to be correct
ctx.fill();
ctx.stroke();
}
const ctx = canvas.getContext("2d");
drawPoint(ctx, scaleMatrix, point.x, point.y, 4, 4);
Transforming via CPU
To convert a point from the device coordinates to the virtual coordinates you need to apply the inverse matrix to that point. For example you get the pageX, pageY coordinates from a mouse, you convert using the scale matrix as follows
function pointToVirtual(matrix, point) {
point.x = (point.x - matrix[4]) / matrix[0];
point.y = (point.y - matrix[5]) / matrix[3];
return point;
}
To convert from virtual to device
function virtualToPoint(matrix, point) {
point.x = (point.x * matrix[0]) + matrix[4];
point.y = (point.y * matrix[3]) + matrix[5];
return point;
}
Check bounds
There may be an area above/below or left/right of the canvas that is outside the virtual canvas coordinates. To check if inside the virtual canvas call the following
function isInVritual(vPoint) {
return ! (vPoint.x < -vWidth / 2 ||
vPoint.y < -vHeight / 2 ||
vPoint.x >= vWidth / 2 ||
vPoint.y >= vHeight / 2);
}
const dPoint = {x: page.x, y: page.y}; // coordinate in device coords
if (isInVirtual(pointToVirtual(scaleMatrix,dPoint))) {
console.log("Point inside");
} else {
console.log("Point out of bounds.");
}
Extra points
The above assumes that the canvas is aligned to the screen.
Some devices will be zoomed (pinch scaled). You will need to check the device pixel scale for the best results.
It is best to set the virtual canvas size to the max screen resolution you expect.
Always work in virtual coordinates, only convert to device coordinates when you need to render.

How can I scale the size of a sprite in Phaser/PixiJS without changing its position?

I'm making a game in Phaser using some large images that I want to scale down in the actual game:
create() {
//Create the sprite group and scale it down to 30%
this.pieces = this.add.group(undefined, "pieces", true);
this.pieces.scale.x = 0.3;
this.pieces.scale.y = 0.3;
//Add the players to the middle of the stage and add them to the 'pieces' group
var middle = new Phaser.Point( game.stage.width/2, game.stage.height/2);
var player_two = this.add.sprite(middle.x - 50, middle.y, 'image1', undefined, this.pieces);
var player_one = this.add.sprite(middle.x, middle.y-50, 'image2', undefined, this.pieces);
}
However because the sprites are scaled in size, their starting location is also scaled, so instead appearing in the middle of the stage, they appear only 30% of the distance to the middle.
How do I scale the sprite image without it affecting their location?
(The code is incidentally in Typescript but I think this particular sample is also javascript so that's probably irrelevant)
Set Sprite.anchor to 0.5 so the physics body is centered on the Sprite, scale the sprite image without it affecting their location.
this.image.anchor.setTo(0.5, 0.5);
Doc Phaser.Image
Anchor example
You can scale your sprite like so:
var scaleX = 2;
var scaleY = 2;
sprite.scale.set(scaleX, scaleY);
then calculate position of sprite:
var positionX = 100;
var positionY = 100;
sprite.x = positionX / scaleX;
sprite.y = positionY / scaleY;
Now your sprite is at position (100, 100).
The problem is that sprite.x got multiplied by with scaleX.
Regarding Phaser, I'd like to add that in the specific case of weapon.bullets or other groups you create yourself you're going to have to do it this way instead:
weapon.bullets.setAll('scale.x', 0.5);
weapon.bullets.setAll('scale.y', 0.5);
I got stuck on this and ended up in this thread, which is closed but in my case just not what I needed. Others will hopefully have some use out of this :)

mouse coordinates to isometric coordinates

I'm trying to get the right tile in the isometric world by mouse position.
I've read some theads about this, but it doesn't seem to work for me.
The basic Idea is to recalculate the normal mouse coordinates to the isometric tile-coordinates.
As you can see the mouse cursor is at the tile 5/4 and the recalculation is wrong (the tile selected is 4/5). This is my Code:
var params.tx = 100, params.ty=54,
PI = 3.14152,
x1 = x_mouse - params.tx*5,
y1 = y_mouse * -2,
xr = Math.cos(PI/4)*x1 - Math.sin(PI/4)*y1,
yr = Math.sin(PI/4)*x1 + Math.cos(PI/4)*y1,
diag = (params.ty) * Math.sqrt(2),
x2 = parseInt(xr / diag)+1,
y2 = parseInt(yr * -1 / diag)+1;
The original height of a tile is 54px, but as you can see, only the border tiles show their full height. The rest of the tiles are cut by 4 pixels.
Please help me - may be my whole formula is wrong.

Move HTML5 Canvas with a background image

I want to visualize a huge diagram that is drawn in a HTML5 canvas. As depicted below, let’s imagine the world map, it’s impossible to visualize it all at the same time with a “decent” detail. Therefore, in my canvas I would like to be able to pan over it using the mouse to see the other countries that are not visible.
Does anyone know how to implement this sort of panning in a HTML5 canvas? Another feature would be the zoom in and out.
I've seen a few examples but I couldn't get them working nor they seam to address my question.
Thanks in advance!
To achieve a panning functionality with a peep-hole it's simply a matter of two draw operations, one full and one clipped.
To get this result you can do the following (see full code here):
Setup variables:
var ctx = canvas.getContext('2d'),
ix = 0, iy = 0, /// image position
offsetX = 0, offsetY = 0, /// current offsets
deltaX, deltaY, /// deltas from mouse down
mouseDown = false, /// in mouse drag
img = null, /// background
rect, /// rect position
rectW = 200, rectH = 150; /// size of highlight area
Set up the main functions that you use to set size according to window size (including on resize):
/// calc canvas w/h in relation to window as well as
/// setting rectangle in center with the pre-defined
/// width and height
function setSize() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
rect = [canvas.width * 0.5 - rectW * 0.5,
canvas.height * 0.5 - rectH * 0.5,
rectW, rectH]
update();
}
/// window resize so recalc canvas and rect
window.onresize = setSize;
The main function in this is the draw function. Here we draw the image on the position calculated by mouse moving (see next section).
First step to get that washed-out look is to set alpha down to about 0.2 (you could also draw a transparent rectangle on top but this is more efficient).
Then draw the complete image.
Reset alpha
Draw the peep-hole using clipping with corrected offsets for the source.
-
/// main draw
function update() {
if (img === null) return;
/// limit x/y as drawImage cannot draw with negative
/// offsets for clipping
if (ix + offsetX > rect[0]) ix = rect[0] - offsetX;
if (iy + offsetY > rect[1]) iy = rect[1] - offsetY;
/// clear background to clear off garbage
ctx.clearRect(0, 0, canvas.width, canvas.height);
/// make everything transparent
ctx.globalAlpha = 0.2;
/// draw complete background
ctx.drawImage(img, ix + offsetX, iy + offsetY);
/// reset alpha as we need opacity for next draw
ctx.globalAlpha = 1;
/// draw a clipped version of the background and
/// adjust for offset and image position
ctx.drawImage(img, -ix - offsetX + rect[0], /// sx
-iy - offsetY + rect[1], /// sy
rect[2], rect[3], /// sw/h
/// destination
rect[0], rect[1], rect[2], rect[3]);
/// make a nice sharp border by offsetting it half pixel
ctx.strokeRect(rect[0] + 0.5, rect[1] + 0.5, rect[2], rect[3]);
}
Now it's a matter of handling mouse down, move and up and calculate the offsets -
In the mouse down we store current mouse positions that we'll use for calculating deltas on mouse move:
canvas.onmousedown = function(e) {
/// don't do anything until we have an image
if (img === null) return;
/// correct mouse pos
var coords = getPos(e),
x = coords[0],
y = coords[1];
/// store current position to calc deltas
deltaX = x;
deltaY = y;
/// here we go..
mouseDown = true;
}
Here we use the deltas to avoid image jumping setting the corner to mouse position. The deltas are transferred as offsets to the update function:
canvas.onmousemove = function(e) {
/// in a drag?
if (mouseDown === true) {
var coords = getPos(e),
x = coords[0],
y = coords[1];
/// offset = current - original position
offsetX = x - deltaX;
offsetY = y - deltaY;
/// redraw what we have so far
update();
}
}
And finally on mouse up we make the offsets a permanent part of the image position:
document.onmouseup = function(e) {
/// was in a drag?
if (mouseDown === true) {
/// not any more!!!
mouseDown = false;
/// make image pos. permanent
ix += offsetX;
iy += offsetY;
/// so we need to reset offsets as well
offsetX = offsetY = 0;
}
}
For zooming the canvas I believe this is already answered in this post - you should be able to merge this with the answer given here:
Zoom Canvas to Mouse Cursor
To do something like you have requested, it is just a case of having 2 canvases, each with different z-index. one canvas smaller than the other and position set to the x and y of the mouse.
Then you just display on the small canvas the correct image based on the position of the x and y on the small canvas in relation to the larger canvas.
However your question is asking for a specific solution, which unless someone has done and they are willing to just dump their code, you're going to find it hard to get a complete answer. I hope it goes well though.

Categories

Resources