Fire zoom event (or simulate zoom event) - javascript

When I zoom with the mouse, the following function attached to myZoom will be executed:
myZoom.on('zoom', function() {
someElement.attr('transform', 'translate(' + d3.event.translate[0] + ',' + d3.event.translate[1] + ') scale(' + d3.event.scale + ')');
....
// redraw axes, which should stay where they are at.
....
}
To simulate zoom without mouse or some other pointing device, I can just change the value of the attribute 'transform' above. Easy.
But problem is in this function I actually redraw axes, whose scale is automatically recalculated. Refer to this official documentation from d3:
zoom.y([y])
Specifies an y-scale whose domain should be automatically adjusted
when zooming. If not specified, returns the current y-scale, which
defaults to null. If the scale's domain is modified programmatically,
it should be reassigned to the zoom behaviour.
I need to zoom programmatically (maybe with zoom button). How can I fire zoom event, so that scale of my axes is automatically recalculated?

Programmatic zoom seems to be a daunting task in the D3 library because the D3 zooming is closely tied to the mouse events. A common instance of programmatic zooming is zooming in or out with a slider control. Surprisingly, I couldn't find a single working example of how to make D3 zooming work with a slider control. After investing some time and effort I developed this working demo which can be found here D3SliderZoom. The key point is to change the transform attribute of a "<g>" SVGElement embedded in an "<svg>" element using the scale value thrown by the slider.
function zoomWithSlider(scale) {
var svg = d3.select("body").select("svg");
var container = svg.select("g");
var h = svg.attr("height"), w = svg.attr("width");
// Note: works only on the <g> element and not on the <svg> element
// which is a common mistake
container.attr("transform",
"translate(" + w/2 + ", " + h/2 + ") " +
"scale(" + scale + ") " +
"translate(" + (-w/2) + ", " + (-h/2) + ")");
}
This method then has to be invoked from the change event of the slider as shown below.
$(function() {
$( "#slider-vertical" ).slider({
orientation: "vertical",
range: "min",
min: 1000,
max: 10000,
value: 1000,
slide: function( event, ui ) {
zoomWithSlider(ui.value/1000);
}
});
});
This solution is much more elegant than generating pseudo-mouse scroll event.

I ended up calculating new domain for a new zoom level by myself. With this new domain I could redraw two y-axes. For someone, who has same problem, I post my code. It's very specific to my project, so it might be hard to understand. Just for your interest.
wr.zoomSim = function(sNew) {
var s = wr.zoom.scale(),
tx = wr.zoom.translate()[0],
ty = wr.zoom.translate()[1],
sReal = sNew / s,
dtx = wr.width / 2 * (1 - sReal),
dty = wr.height / 2 * (1 - sReal),
txNew = sReal * tx + dtx,
tyNew = sReal * ty + dty,
a = wr.scaleYBZoom.domain()[0],
b = wr.scaleYBZoom.domain()[1],
c = wr.scaleYBZoom.range()[0],
d = wr.scaleYBZoom.range()[1],
r = (b-a)/(d-c);
wr.scaleYBZoom.domain([a + r * ( (c - dty) / sReal - c), a + r * ( (d - dty) / sReal - c)]);
wr.zoom.scale(sNew);
wr.zoom.translate([txNew, tyNew]);
wr.svg2.select('g#bar')
.attr('transform', 'translate(' + txNew + ',' + tyNew + ') scale(' + sNew + ')');
wr.svg2.select('g#axisl')
.call(d3.svg.axis().scale(wr.scaleYBZoom).orient('left'))
.selectAll('line.tick')
.attr('x2', wr.width - wr.bar.left - wr.bar.right + 2 * wr.padding);
wr.svg2.select('g#axisr')
.call(d3.svg.axis().scale(wr.scaleYBZoom).orient('right'))
.selectAll('line')
.remove();
};

Unfortunately #Debasis's answer didn't work for me, because I wanted to achieve this with a zoom behavior which I was already using with my force layout. After two days of desperation I finally found the solution in this thread:
https://groups.google.com/forum/#!msg/d3-js/qu4lX5mpvWY/MnnRMLz_cnUJ
function programmaticZoom ($svg, $zoomContainer, zoomBehavior, factor) {
var width = $svg.attr('width');
var height = $svg.attr('height');
var newScale = zoomBehavior.scale() * factor;
var newX = (zoomBehavior.translate()[0] - width / 2) * factor + width / 2;
var newY = (zoomBehavior.translate()[1] - height / 2) * factor + height / 2;
zoomBehavior
.scale(newScale)
.translate([newX,newY])
.event($zoomContainer);
}

I did this by making use of zoomListener. Worked well in simple steps for me:
Define zoomListener:
var zoomListener = d3.behavior.zoom();
Call the zoom listener
d3.select(the-element-that-you-need-zoom-on).call(zoomListener);
Decide your zoom step. I took steps of 0.1. (Use 1.1 for zoom-in and 0.9 for zoom-out)
Multiply with the current zoom scale
var newScale = zoomListener.scale() * step;
Set the new scale value
zoomListener.scale(newScale);

Related

Resize multiple objects with JS considering rotate

I'm working on visual editor with objects and user interactions around like move, resize, rotate, etc...
I have resize and rotate functionality in place. Now I have implemented multi-select functionality when user select multiple objects and resize objects keeping the original proportion.
That functionality works very well, however not for rotated objects. I've created a simplified codepen example. Basically the question is - how to adjust resize() function to make sure it works well for rotated objects. To reproduce an issue just click on "Rotate" and then "Increase width & height" once or multiple times.
function resize(incrementX, incrementY, offsetX, offsetY) {
...
}
I'm not sure if this is a valid solution for your problem, but you can undo the rotation before resizing, and reset the rotation afterwards. Like this.
function resize(incrementX, incrementY, offsetX, offsetY) {
var old_r = objmultiple.r
rotate(-objmultiple.r)
var ratioX = (objmultiple.w + incrementX) / objmultiple.w;
var ratioY = (objmultiple.h + incrementY) / objmultiple.h;
objmultiple.x += offsetX;
objmultiple.y += offsetY;
objmultiple.w = objmultiple.w + incrementX;
objmultiple.h = objmultiple.h + incrementY;
[obj1, obj2].forEach(function(obj) {
obj.x = (obj.x - objmultiple.x + offsetX) * ratioX + objmultiple.x;
obj.y = (obj.y - objmultiple.y + offsetY) * ratioY + objmultiple.y;
obj.w *= ratioX;
obj.h *= ratioY;
});
rotate(old_r)
}
Codepen here

Accurate pan and zoom to svg node

I am trying to pan and zoom to a svg node using d3js. But I cannot get my head around the math here.
If I force the desired zoom level to be 1, then I seem to get it right.
Here's an example:
let svg = d3.select('svg'),
svgW = svg.node().getBoundingClientRect().width,
svgH = svg.node().getBoundingClientRect().height,
svgCentroid = {
x : svgW / 2,
y : svgH / 2
};
// zoom functionality has been applied to this one
let selector = d3.select('#container');
let elem = d3.select('[id="6"]'),
elemBounds = elem.node().getBBox(),
elemCentroid = {
x : elemBounds.x + (elemBounds.width / 2),
y : elemBounds.y + (elemBounds.height / 2)
};
let position = {
x : svgCentroid.x - elemCentroid.x,
y : svgCentroid.y - elemCentroid.y
};
selector.transition()
.duration(750)
.call(this.zoom.transform, d3.zoomIdentity
.translate(position.x, position.y)
// set scale to 1
.scale(1)
);
My first naive thought was "piece of cake". I will just multiply the calculated positions with desired zoom level. But, surprise surprise, that got me terribly wrong.
// failed miserably
selector.transition()
.duration(750)
.call(this.zoom.transform, d3.zoomIdentity
.translate(position.x * 5, position.y * 5)
.scale(5)
);
I've been trying to play around with this example:
https://bl.ocks.org/smithant/664d6cf86e53442d09687b154a9a411d
It pretty much sums up my intentions, but even though it's right there I don't fully understand it and thus it does not work properly with the rest of my code. I guess what confuses me most about this particular example are how the variables have their names declared.
I'd be grateful if someone could point me in the right direction here. How can I achieve this? What is the appropriate math to correctly zoom and pan within an SVG?
Thanks :)
I think that what you're looking for is:
function () {
var t = d3.transform(d3.select(this).attr("transform")),
x = t.translate[0],
y = t.translate[1];
var scale = 10;
svg.transition().duration(3000)
.call(zoom.translate([((x * -scale) + (svgWidth / 2)), ((y * -scale) + svgHeight / 2)])
.scale(scale).event);
}
Where this represents the element. Have a look here for a working example. In the example you'll be able to zoom to element after pressing on it. Also if panning and zooming an svg is all you need to do check out this library. It just works, no maths required :).

JS randomly distribute child elements in their parent without overlap

I am trying to make something where a bunch of circles (divs with border-radius) can be dynamically generated and laid out in their container without overlapping.
Here is my progress so far - https://jsbin.com/domogivuse/2/edit?html,css,js,output
var sizes = [200, 120, 500, 80, 145];
var max = sizes.reduce(function(a, b) {
return Math.max(a, b);
});
var min = sizes.reduce(function(a, b) {
return Math.min(a, b);
});
var percentages = sizes.map(function(x) {
return ((x - min) * 100) / (max - min);
});
percentages.sort(function(a, b) {
return b-a;
})
var container = document.getElementById('container');
var width = container.clientWidth;
var height = container.clientHeight;
var area = width * height;
var maxCircleArea = (area / sizes.length);
var pi = Math.PI;
var maxRadius = Math.sqrt(maxCircleArea / pi);
var minRadius = maxRadius * 0.50;
var range = maxRadius - minRadius;
var radii = percentages.map(function(x) {
return ((x / 100) * range) + minRadius;
});
function getRandomArbitrary(min, max) {
return Math.random() * (max - min) + min;
}
var coords = [];
radii.forEach(function(e, i) {
var circle = document.createElement('div');
var randomTop = getRandomArbitrary(0, height);
var randomLeft = getRandomArbitrary(0, width);
var top = randomTop + (e * 2) < height ?
randomTop :
randomTop - (e * 2) >= 0 ?
randomTop - (e * 2) :
randomTop - e;
var left = randomLeft + (e * 2) < width ?
randomLeft :
randomLeft - (e * 2) >= 0 ?
randomLeft - (e * 2) :
randomLeft - e;
var x = left + e;
var y = top + e;
coords.push({x: x, y: y, radius: e});
circle.className = 'bubble';
circle.style.width = e * 2 + 'px';
circle.style.height = e * 2 + 'px';
circle.style.top = top + 'px';
circle.style.left = left + 'px';
circle.innerText = i
container.appendChild(circle);
});
I have got them being added to the parent container but as you can see they overlap and I don't really know how to solve this. I tried implementing a formula like (x1 - x2)^2 + (y1 - y2)^2 < (radius1 + radius2)^2 but I have no idea about this.
Any help appreciated.
What you're trying to do is called "Packing" and is actually a pretty hard problem. There are a couple potential approaches you can take here.
First, you can randomly distribute them (like you are currently doing), but including a "retry" test, in which if a circle overlaps another, you try a new location. Since it's possible to end up in an impossible situation, you would also want a retry limit at which point it gives up, goes back to the beginning, and tries randomly placing them again. This method is relatively easy, but has the down-side that you can't pack them very densely, because the chances of overlap become very very high. If maybe 1/3 of the total area is covered by circle, this could work.
Second, you can adjust the position of previously placed circles as you add more. This is more equivalent to how this would be accomplished physically -- as you add more you start having to shove the nearby ones out of the way in order to fit the new one. This will require not just finding the things that your current circle hits, but also the ones that would be hit if that one was to move. I would suggest something akin to a "springy" algorithm, where you randomly place all the circles (without thinking about if they fit), and then have a loop where you calculate overlap, and then exert a force on each circle based on that overlap (They push each other apart). This will push the circles away from each other until they stop overlapping. It will also support one circle pushing a second one into a third, and so on. This will be more complex to write, but will support much more dense configurations (since they can end up touching in the end). You still probably need a "this is impossible" check though, to keep it from getting stuck and looping forever.

Find the Points of Intersection of a Circle with a Line in Javascript

I'm trying to animate a given element to go around a pre-defined radius and I'm having trouble getting the position of the element at a Y point given.
I'm trying to find each point with the circle equation, but I can only get one point out of the two possible ones.
In Javascript, I use Math.sqrt( Math.pow(radius, 2) - Math.pow(y, 2) , 2) to get the point. assuming the center of the of the circle is 0,0.
but then I need to translate it to pixels on the screen since there are no negative pixels in positions on the browser.
All the sizing is relative to the window. so the radius, for example, is 80% of the height of the window in my tests.
Also, I'm trying to calculate what the distance of the element between each frame should be for the duration, but I'm not using it yet because I try to fix the issue above first.
This is what I have(a cleaned up version):
let height = window.innerHeight * 0.8,
radius = height / 2,
circumferance = (radius * 2) * Math.PI,
container = document.getElementById('container'),
rotating = document.querySelector('.rotating'),
centerX = radius - (rotating.offsetWidth / 2),
centerY = radius - (rotating.offsetHeight / 2),
duration = 10,
stepDistance = circumferance / 16;
// Setting the dimensions of the container element.
container.style.height = height + 'px';
container.style.width = height + 'px';
// return positive X of any given Y.
function getXOffset(y) {
return Math.sqrt( Math.pow(radius, 2) - Math.pow(y, 2) , 2);
}
// Setting the position of the rotating element to the start.
rotating.style.top = 0 + 'px';
rotating.style.left = centerX + 'px';
setInterval(() => {
let top = parseInt(rotating.style.top),
y = radius - top;
rotating.style.top = (top + 1) + 'px';
rotating.style.left = (centerX + getXOffset(y)) + 'px';
}, 16);
Here is a fiddle with a bit more code for trying to get the right amount of distance between points for a smoother animation(currently needs fixing, but it doesn't bother me yet.)
https://jsfiddle.net/shock/1qcfvr4y/
Last note: I know that there might be other ways to do this with CSS, but I chose to use javascript for learning purposes.
Math.sqrt would only return the positive root. You'll have to account for the negative value based on the application. In this case, you need the positive x value during the 1st half of the cycle and negative during the 2nd half.
To do that, you should implement a method to track the progress and reverse the sign accordingly.
Here is a sample. I modified upon yours.
edit:
Instead of Math.sqrt( Math.pow(radius, 2) - Math.pow(y, 2) , 2) You can use the full formula to get x if you do not want to assume origin as center, which in this case is Math.sqrt( Math.pow(radius, 2) - Math.pow((actualY - centerY), 2) , 2)
explanation:
The original equation (x-a)² + (y'-b)² = r²
becomes x = √(r² - (y'-b)²) + a
Assuming .rotating box have 0 width and height.
The variable equivalents in your code are centerX = a, centerY = b.
By assuming origin as center you're basically doing a pre-calculation so that your y value becomes the equivalent of (y'-b). Hence x = √(r² - y²) + a is valid.
At initial state top = 0
i.e (y'-b) => height - centerY.
In your code y = radius => height/2.
Now (height - centerY) being equal to (height/2) is a side effect of your circle being bound by a square container whose height determines the y value.
In other words, when you use origin as center, you are taking the center offsets outside of circle equation and handling it separately. You could do the same thing by using the whole formula, that is, x = √(r² - (y'-b)²) + a

Snap.svg Getting rotation from slope in point difference

I am working through an animation where I need to make this plane follow the path given and appear to be "circling" the earth. Here is the codePen, which as you can see is fairly simple.
My problem is with angles, I am trying to see how much should I rotate the plane as it moves through the path by calculating the slope of two points, and turning it into degrees.
Even though I have added an epsilon to safe-check for consistent differences across the points and every other safe-check, I am getting that as it approaches +-90 degrees, it changes signs, instead of passing to the other quadrant 120 degrees, etc.
I can understand that this is caused by the fact that
You can see this happening in the console right in the mid point (displays: slope, arctangent, degrees).
To solve this, I am recurring to Math.atan2(), by using newPoint.x - firstPoint.x, newPoint.y - firstPoint.y as its arguments. It starts off with the right values (CodePen here). But it still does a funky rotation.
Here is the code (I'm not posting the SVG image because it's very large):
JS
var snap = Snap('#Globe_1_');
// Trail 1
var trail1 = snap.path('M354.3,707.9c13.9,14.5,27.9,27.2,41.7,38c13.9,10.9,27.2,19.3,39.3,25.4 c12.6,6.1,24.8,9,35.7,10.3c10.9,1.2,21.1-1.2,30.2-5.4c17-7.8,29-24.8,35.7-48.3c7.2-23.5,9-55,5.4-91.8 c-3.7-36.8-12-77.9-24.8-120.9c-12.6-43-30.2-87.6-51.9-131.7c-21.1-44.1-45.2-85.8-70.7-122.7s-50.8-69.5-77.3-95.5 c-27.2-26-52.5-43.6-75.6-53.2c-22.9-9.7-43.6-10.3-60.4-2.5c-16.3,7.8-27.9,24.2-35.1,47.7c-7.2,23.5-9.7,53.8-6.6,88.8')
.attr({
id: 'trail1',
fill:'none',
stroke: '#C25353',
strokeMiterLimit: 10
});
var len = trail1.getTotalLength();
var plane1 = snap.path('M375.7,708.4c0.1,0.8-0.7,1.8-1.6,1.9l-10.4,0.2l-8.4,15.1l-4,0l4.1-14.6l-7.8-0.2l-2.7,3.2L342,714 l1.6-4.9l-1.7-5.4l3.1,0.1l2.5,3.6l7.8,0.2l-4.3-14.6l4,0l8.3,14.7l10.4-0.2C375.5,706.7,376,707.1,375.7,708.4z') .attr({fill: '#CDCCCC' });
var initPoint = trail1.getPointAtLength( 1 ),
lastPoint,
slope = 0,
lastLen = 0;
Snap.animate(0, len, function( value ) {
movePoint = trail1.getPointAtLength( value );
if (lastPoint && ( Math.abs(lastPoint.y - movePoint.y) > 1 || Math.abs(lastPoint.x - movePoint.x) > 1 )) {
var slope_val = (lastPoint.y - movePoint.y) / (lastPoint.x - movePoint.x),
slope_atan = Math.atan2(movePoint.x - initPoint.x, movePoint.y - initPoint.y),
slope_deg = Snap.deg(slope_atan);
slope = slope_deg;
console.log('Capturing rotation', slope_val, slope_atan, slope_deg);
lastLen = value;
}
plane1.transform( 't' + parseInt(movePoint.x - 350) + ',' + parseInt( movePoint.y - 700) + 'r' + slope);
lastPoint = movePoint;
}, 5000, mina.linear);
Can you please help me out, thank you
I'm not sure of the full effect you are after, if its purely 2d angle, Snap already has this built in (returning angle from point along line), so no need to work too hard...
element.getPointAtLength() returns an angle alpha, so movePoint.alpha can be used...
relevant line below, and other calculation lines removed.
plane1.transform( 't' + parseInt(movePoint.x - 350) + ',' + parseInt( movePoint.y - 700) + 'r' + (180 + movePoint.alpha));
codepen

Categories