d3 error with manual zoom - javascript

I'm using d3 v4.
I'm implementing a zoom on an area graph using the following example. My zoom is registered as such:
// Zoom Components
zoom = d3.zoom()
.scaleExtent([1, dayDiff*12])
.translateExtent([[0, 0], [width, height]])
.extent([[0, 0], [width, height]])
.on("zoom", zoomed);
With my zoom method looking like this:
function zoomed(){
t = d3.event.transform;
console.log(t);
...
}
When naturally zooming with the wheel, the console spits out:
{
k:1.0097512975966858
x:-1.9210056265470996
y:-1.004383652458642
}
I'm using a TimeScale and I want to zoom in and translate to a certain period of time. For example, I may only want to show 7 days as my x1 and x2, so i calculate scale factor of k and then calculate tx value to translate to a certain area. I
created a manual zoom method to trigger a manual zoom. With the following code:
function manualZoom(){
var outerRightDay,
thirtyBeforeOuter,
k,
tx;
// Get outer right day
outerRightDay = moment(xScale.domain()[1]);
// Get 30 days before
thirtyBeforeOuter = moment(outerRightDay).subtract(31,'days');
// Get scale k
k = width / (xScale(outerRightDay) - xScale(thirtyBeforeOuter));
// Get transform value
svg.call(zoom.scaleBy, k);
tx = 0 - k * xScale(thirtyBeforeOuter);
svg.call(zoom.translateBy,tx);
}
After running this, the zoomed method spits out:
{
k:1.0097512975966858 //a good number
x:-1.9210056265470996 //a good number
y:NAN //this is an issue!!!!
}
It works with mouse wheel, buy on touch devices. My y is NAN and stops me from zooming in on a touch device. How can I calculate ty to supply it to zoom.translateBy().
I have included a jsFiddle here.

Since you only use transform rescale on x-axis (in zoomed function), you don't care about the value of ty. Just pass a value to translateBy, it won't be NaN and it will work:
svg.call(zoom.translateBy,tx,0);
Why ty is NaN with your code? zoom.translateBy calls transform.translate function, its source code is:
translate: function(x, y) {
return x === 0 & y === 0 ? this : new Transform(this.k, this.x + this.k * x, this.y + this.k * y);
}
So if y is undefined, this.y + this.k * y will be evaluated as NaN.

Related

Initiate d3 map over certain area given latitude and longitude

I am building a map in d3 and basing it off of this codepen by Andy Barefoot: https://codepen.io/nb123456/pen/zLdqvM?editors=0010. I want to modify the initiateZoom() function so that if I set the lat/lon coordinates for a box surrounding say Ohio, the map will initialize its panning to be over Ohio.
function initiateZoom() {
minZoom = Math.max($("#map-holder").width() / w, $("#map-holder").height() / h);
maxZoom = 20 * minZoom;
zoom
.scaleExtent([minZoom, maxZoom])
.translateExtent([[0, 0], [w, h]])
;
midX = ($("#map-holder").width() - minZoom * w) / 2;
midY = ($("#map-holder").height() - minZoom * h) / 2;//These are the original values
var swlat = 32;
var swlon = -82;
var nelat = 42;
var nelon = -72;
var projectCoordinates = projection([(swlat+nelat)/2, (swlon+nelon)/2]);
/*This did not work
var midX = minZoom*(w-(swlat+nelat)/2) - ($("#map-holder").width()-(swlat+nelat)/2);
var midY = minZoom*(h-(swlon+nelon)/2) - ($("#map-holder").height()-(swlon+nelon)/2);*/
/*Neither did this
var midX = minZoom*(w-projectCoordinates[0])-($("#map-holder").width()-projectCoordinates[0]);
var midY = minZoom*(h-projectCoordinates[1])-($("#map-holder").height()-projectCoordinates[1]);*/
svg.call(zoom.transform, d3.zoomIdentity.translate(midX, midY).scale(minZoom));
}
The idea behind the original approach was to:
1: Get the current pixel display of the map
2: Get the new pixel distance from the map corner to the map point after zoom has been applied
3: The pixel distance of the center of the container to the top of the container
4: subtract the values from 2 and 3
The original post was trying to translate the map so that it would initialize the zoom and pan over the center of the map. I tried to modify this approach first by directly substituting the lat/lon values into the above equations. I also tried first transforming the lat/lon values using the projection and then substituting those values in, with little success. What do I need to do in order to get my desired result?
Setting a translateExtent could be a bad idea because it depends on the zoom scale.
The following replacement works.
function initiateZoom() {
// Define a "minzoom" whereby the "Countries" is as small possible without leaving white space at top/bottom or sides
minZoom = Math.max($("#map-holder").width() / w, $("#map-holder").height() / h);
// set max zoom to a suitable factor of this value
maxZoom = 20 * minZoom;
// set extent of zoom to chosen values
// set translate extent so that panning can't cause map to move out of viewport
zoom
.scaleExtent([minZoom, maxZoom])
.translateExtent([[0, 0], [w, h]])
;
var swlat = 32;
var swlon = -82;
var nelat = 42;
var nelon = -72;
var nwXY = projection([swlon, nelat]);
var seXY = projection([nelon, swlat]);
var zoomScale = Math.min($("#map-holder").width()/(seXY[0]-nwXY[0]), $("#map-holder").height()/(seXY[1]-nwXY[1]))
var projectCoordinates = projection([(swlon+nelon)/2, (swlat+nelat)/2]);
svg.call(zoom.transform, d3.zoomIdentity.translate($("#map-holder").width()*0.5-zoomScale*projectCoordinates[0], $("#map-holder").height()*0.5-zoomScale*projectCoordinates[1]).scale(zoomScale));
}

Isometric tilemap using canvas (with click detection)

I am currently developing a game, which requires a map consisting of various tile images. I managed to make them display correctly (see second image) but I am now unsure of how to calculate the clicked tile from the mouse position.
Are there any existing libraries for this purpose?
Please also note, that the tile images aren't drawn perfectly "corner-facing-camera", they are slightly rotated clockwise.
Isometric Transformations
Define a projection
Isometric display is the same as standard display, the only thing that has changed is the direction of the x and y axis. Normally the x axis is defined as (1,0) one unit across and zero down and the y axis is (0,1) zero units across and one down. For isometric (strictly speaking your image is a dimetric projection) you will have something like x axis (0.5,1) and y axis (-1,0.5)
The Matrix
From this you can create a rendering matrix with 6 values Two each for both axes and two for the origin, which I will ignore for now (the origin) and just use the 4 for the axis and assume that the origin is always at 0,0
var dimetricMatrix = [0.5,1.0,-1,0.5]; // x and y axis
Matrix transformation
From that you can get a point on the display that matches a given isometric coordinate. Lets say the blocks are 200 by 200 pixels and that you address each block by the block x and y. Thus the block in the bottom of your image is at x = 2 and y = 1 (the first top block is x = 0, y = 0)
Using the matrix we can get the pixel location of the block
var blockW = 200;
var blockH = 200;
var locX = 2;
var locY = 1;
function getLoc(x,y){
var xx,yy; // intermediate results
var m = dimetricMatrix; // short cut to make code readable
x *= blockW; // scale up
y *= blockH;
// now move along the projection x axis
xx = x * m[0];
yy = x * m[1];
// then add the distance along the y axis
xx += y * m[2];
yy += y * m[3];
return {x : xx, y : yy};
}
Befoer I move on you can see that I have scaled the x and y by the block size. We can simplify the above code and include the scale 200,200 in the matrix
var xAxis = [0.5, 1.0];
var yAxis = [-1, 0.5];
var blockW = 200;
var blockH = 200;
// now create the matrix and scale the x and y axis
var dimetricMatrix = [
xAxis[0] * blockW,
xAxis[1] * blockW,
yAxis[0] * blockH,
yAxis[1] * blockH,
]; // x and y axis
The matrix holds the scale in the x and y axis so that the two numbers for x axis tell us the direction and length of a transformed unit.
Simplify function
And redo the getLoc function for speed and efficiency
function transformPoint(point,matrix,result){
if(result === undefined){
result = {};
}
// now move along the projection x axis
result.x = point.x * matrix[0] + point.y * matrix[2];
result.y = point.x * matrix[1] + point.y * matrix[3];
return result;
}
So pass a point and get a transformed point back. The result argument allows you to pass an existing point and that saves having to allocate a new point if you are doing it often.
var point = {x : 2, y : 1};
var screen = transformPoint(point,dimetricMatrix);
// result is the screen location of the block
// next time
screen = transformPoint(point,dimetricMatrix,screen); // pass the screen obj
// to avoid those too
// GC hits that kill
// game frame rates
Inverting the Matrix
All that is handy but you need the reverse of what we just did. Luckily the way matrices work allows us to reverse the process by inverting the matrix.
function invertMatrix(matrix){
var m = matrix; // shortcut to make code readable
var rm = [0,0,0,0]; // resulting matrix
// get the cross product of the x and y axis. It is the area of the rectangle made by the
// two axis
var cross = m[0] * m[3] - m[1] * m[2]; // I call it the cross but most will call
// it the determinate (I think that cross
// product is more suited to geometry while
// determinate is for maths geeks)
rm[0] = m[3] / cross; // invert both axis and unscale (if cross is 1 then nothing)
rm[1] = -m[1] / cross;
rm[2] = -m[2] / cross;
rm[3] = m[0] / cross;
return rm;
}
Now we can invert our matrix
var dimetricMatrixInv = invertMatrix(dimetricMatrix); // get the invers
And now that we have the inverse matrix we can use the transform function to convert from a screen location to a block location
var screen = {x : 100, y : 200};
var blockLoc = transformPoint(screen, dimetricMatrixInv );
// result is the location of the block
The Matrix for rendering
For a bit of magic the transformation matrix dimetricMatrix can also be used by the 2D canvas, but you need to add the origin.
var m = dimetricMatrix;
ctx.setTransform(m[0], m[1], m[2], m[3], 0, 0); // assume origin at 0,0
Now you can draw a box around the block with
ctx.strokeRect(2,1,1,1); // 3rd by 2nd block 1 by 1 block wide.
The origin
I have left out the origin in all the above, I will leave that up to you to find as there is a trillion pages online about matrices as all 2D and 3D rendering use them and getting a good deep knowledge of them is important if you wish to get into computer visualization.

Zoom in in a canvas at a certain point

I try to let the user zoom in the canvas with a pinch gesture, it's a Javascript Canvas Game (using Intel XDK)
I got the point coordinates (relativley to the window document, saved in an array) and the scale "strength".
var scale = 1;
function scaleCanvas(sc, point) { //point["x"] == 200
//sc has value like 0.5, 1, 1.5 and so on
x = sc/scale;
scale = sc;
ctx.scale(x, x);
}
I know that I have to translate the canvas to the point coordinates, and then retranslate it again. My problem is, that the canvas is already translated. The translation values are saved in the vars dragOffX and dragOffY. Furthermore, the initial translation may be easy, but when the canvas is already scaled, every coordinate is changed.
This is the translation of the canvas when dragging/shifting the content:
var dragOffX = 0;
var dragOffY = 0;
function dragCanvas(x,y) {
dragOffX = dragOffX + x;
dragOffY = dragOffY + y;
x = x* 1/scale;
y = y* 1/scale;
ctx.translate(x,y);
}
So when the player is dragging the content for e.g. 100px to the right, dragOffX gets the value 100.
How do I translate my canvas to the correct coordinates?
It will probably be easier if you store the transformation matrix and use setTransform each time you change it - that resets the canvas transformation matrix first before applying the new transformation, so that you have easier control over the way that the different transformations accumulate.
var transform = {x: 0, y: 0, scale: 1}
function scaleCanvas(scale, point) {
var oldScale = transform.scale;
transform.scale = scale / transform.scale;
// Re-centre the canvas around the zoom point
// (This may need some adjustment to re-centre correctly)
transform.x += point.x / transform.scale - point.x / oldScale
transform.y += point.y / transform.scale - point.y / oldScale;
setTransform();
}
function dragCanvas(x,y) {
transform.x += x / transform.scale;
transform.y += y / transform.scale;
setTransform();
}
function setTransform() {
ctx.setTransform(transform.scale, 0, 0, transform.scale, transform.x, transform.y);
}
JSFiddle
Simply Use this to scale canvas on pivot point
function scaleCanvasOnPivotPoint(s, p_x , p_y) {
ctx.translate(p_x, p_y);
ctx.scale(s);
ctx.translate( -p_x, -p_y);
}

collision check of a moving circle

im working on a 2d canvas game. i have a player circle and some circles (random in size and position). i made the random circles move around an random x.y point. this means i have to radii. one is the radius from the rotationpoint to the middlepoint of the "bubble" and the other ist die radius of the bubble itself.
what i need is the collision between playercircle und the bubbles. i know how to create circle to circle collisondetction with pythagorean theorem and it works quite well. However there is a problem:
right now the collision works for the random x and y point + the radius (from rotationpoint) but not for the bubble itself.
what i tryed is to store the x and y of the rotationpoint + the radius to the middlepoint of the bubble into a variable to use them in collision. it works quite fine. if i console.log these x and y points they give me the changing x and ys from the middlepoint of the bubble.
my problem now is that if if substract these point from the playercircle x and y i didnt work with the right collision. so obviously im missing somethig and right now i am at a dead end.
i made a fiddle to show you, the function for the collision is on line 170, variablenames BubbleX and BubbleY. The .counter to animate the around the neg. or positiv:
http://jsfiddle.net/CLrPx/1/ (you need to use the console to the if there is a collision or not)
function collideBubbles(c1, c2) {
// moving/rotation xPos and yPos
var bubbleX = c2.xPos + Math.cos(c2.counter / 100) * c2.radius; // actual x and y pos. from bubble!
var bubbleY = c2.yPos + Math.cos(c2.counter / 100) * c2.radius;
//console.log('bubbleX: ' + bubbleX);
//console.log('bubbleY: ' + bubbleY);
var dx = c1.xPos - bubbleX; // change with pos from actual bubble!
var dy = c1.yPos - bubbleY; // change with pos from actual bubble!
var distance = c1.radius + c2.bubbleRadius
// Pytagorean Theorem
return (dx * dx + dy * dy <= distance * distance);
}

SVG zoom in on mouse - mathematical model

Before you think "why is this guy asking for help on this problem, surely this has been implemented 1000x" - while you are mostly correct, I have attempted to solve this problem with several open source libs yet here I am.
I am attempting to implement an SVG based "zoom in on mouse wheel, focusing on the mouse" from scratch.
I know there are many libraries that accomplish this, d3 and svg-pan-zoom to name a couple. Unfortunately, my implementations using those libs are falling short of my expectations. I was hoping that I could get some help from the community with the underlying mathematical model for this type of UI feature.
Basically, the desired behavior is like Google Maps, a user has their mouse hovering over a location, they scroll the mouse wheel (inward), and the scale of the map image increases, while the location being hovered over becomes the horizontal and vertical center of the viewport.
Naturally, I have access to the width / height of the viewport and the x / y of the mouse.
In this example, I will only focus on the x axis, the viewport is 900 units wide, the square is 100 units wide, it's x offset is 400 units, and the scale is 1:1
<g transform="translate(0 0) scale(1)">
Assuming the mouse x position was at or near 450 units, if a user wheels in until scale reached 2:1, I would expect the x offset to reach -450 units, centering the point of focus like so.
<g transform="translate(-450 0) scale(2)">
The x and y offsets need to be recalculated on each increment of wheel scroll as a function of the current scale / mouse offsets.
All of my attempts have fallen utterly short of the desired behavior, any advice is appreciated.
While I appreciate any help, please refrain from answering with suggestions to 3rd party libraries, jQuery plugins and things of that nature. My aim here is to understand the mathematical model behind this problem in a general sense, my use of SVG is primarily illustrative.
What I usually do is I maintain three variable offset x offset y and scale. They will be applied as a transform to a container group, like your element <g transform="translate(0 0) scale(1)">.
If the mouse would be over the origin the new translation would be trivial to calculate. You just multiply the offset x and y by the difference in scale :
offsetX = offsetX * newScale/scale
offsetY = offsetY * newScale/scale
What you could do is translate the offset so that the mouse is at the origin. Then you scale and then you translate every thing back. Have a look at this typescript class that has a scaleRelativeTo method to do just what you want:
export class Point implements Interfaces.IPoint {
x: number;
y: number;
public constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
add(p: Interfaces.IPoint): Point {
return new Point(this.x + p.x, this.y + p.y);
}
snapTo(gridX: number, gridY: number): Point {
var x = Math.round(this.x / gridX) * gridX;
var y = Math.round(this.y / gridY) * gridY;
return new Point(x, y);
}
scale(factor: number): Point {
return new Point(this.x * factor, this.y * factor);
}
scaleRelativeTo(point: Interfaces.IPoint, factor: number): Point {
return this.subtract(point).scale(factor).add(point);
}
subtract(p: Interfaces.IPoint): Point {
return new Point(this.x - p.x, this.y - p.y);
}
}
So if you have given transform given by translate(offsetX,offsetY) scale(scale) and a scroll event took place at (mouseX, mouseY) leading to a new scale newScale you would calculate the new transform by :
offsetX = (offsetX - mouseX) * newScale/scale + mouseX
offsetY = (offsetY - mouseY) * newScale/scale + mouseY

Categories

Resources