parametrize the bounceOut ease effect in D3 - d3.js

In the image below, the black line describes the bounceOut function, as shown in the D3-ease documentation. Is there any way to make the bounces smaller, as in the red line?

There is no native way to change that specific easing method.
However, if you look at the source code, you'll see that the bounce steps are hardcoded:
var b1 = 4 / 11,
b2 = 6 / 11,
b3 = 8 / 11,
b4 = 3 / 4,
b5 = 9 / 11,
b6 = 10 / 11,
b7 = 15 / 16,
b8 = 21 / 22,
b9 = 63 / 64,
b0 = 1 / b1 / b1;
So, what you can easily do is creating a custom easing with a different set of values.
Just for comparison, here is the D3 easing:
d3.select("svg")
.append("circle")
.attr("r", 20)
.attr("cx", 20)
.attr("cy", 50)
.transition()
.ease(d3.easeBounceOut)
.duration(2000)
.attr("cx", 280);
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg></svg>
And here the same code, with our custom easing:
d3.select("svg")
.append("circle")
.attr("r", 20)
.attr("cx", 20)
.attr("cy", 50)
.transition()
.ease(customBounce)
.duration(2000)
.attr("cx", 280);
function customBounce(t) {
var b1 = 50 / 64,
b2 = 54 / 64,
b3 = 58 / 64,
b4 = 60 / 64,
b5 = 62 / 64,
b6 = 63 / 64,
b0 = 1 / b1 / b1;
return (t = +t) < b1 ? b0 * t * t : t < b3 ? b0 * (t -= b2) * t + b4 : b0 * (t -= b5) * t + b6;
}
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg></svg>
I didn't choose the best math according to your desired outcome, that's just a general explanation, so you can do it by yourself.

The following code, that I've found on a Custom Easing page by Mike Bostock on bl.ocks.org, makes exactly what I want, describing a parameterized bounce:
It defines the function:
function bounce(h) {
if (!arguments.length) h = 0.25;
var b0 = 1 - h,
b1 = b0 * (1 - b0) + b0,
b2 = b0 * (1 - b1) + b1,
x0 = 2 * Math.sqrt(h),
x1 = x0 * Math.sqrt(h),
x2 = x1 * Math.sqrt(h),
t0 = 1 / (1 + x0 + x1 + x2),
t1 = t0 + t0 * x0,
t2 = t1 + t0 * x1,
m0 = t0 + t0 * x0 / 2,
m1 = t1 + t0 * x1 / 2,
m2 = t2 + t0 * x2 / 2,
a = 1 / (t0 * t0);
return function(t) {
return t >= 1 ? 1
: t < t0 ? a * t * t
: t < t1 ? a * (t -= m0) * t + b0
: t < t2 ? a * (t -= m1) * t + b1
: a * (t -= m2) * t + b2;
};
}
and then specify it as ease argument in the transition:
d3element.transition()
.duration(1500)
.ease(bounce(0.2))
...omissis...

Related

how to draw a line with arrow with varying stroke width values

I have drawn arrow when i try to increase arrow stroke width , it looks wierd.Two lines are overlapping and thickness applies from center. Is there any other way to apply strokewidth outwards.
I have referred below link to draw an arrow,
How do I draw an arrowhead (in Android)?
public class Arrow: View
{
float x0 = 300, y0 = 1000, x1 = 600, y1 = 200;
internal static int DENSITY = -1;
public Arrow(Context con):base(con)
{
DENSITY = (int)con.Resources.DisplayMetrics.Density;
}
protected override void OnDraw(Canvas canvas)
{
Paint paint = new Paint();
paint.StrokeWidth = 10 * Arrow.DENSITY;
float angle, anglerad, radius, lineangle;
radius = 45;
angle = 45;
//calculate line angles
anglerad = (float)(Math.Pi * angle / 180.0f);
lineangle = (float)(Math.Atan2(y1 - y0, x1 - x0));
Path mArrow = new Android.Graphics.Path();
mArrow.MoveTo(x1, y1);
var a1 = (float)(x1 - radius * Math.Cos(lineangle - (anglerad / 2.0)));
var a2 = (float)(y1 - radius * Math.Sin(lineangle - (anglerad / 2.0)));
mArrow.LineTo(a1, a2);
mArrow.MoveTo(a1, a2);
mArrow.MoveTo(x1, y1);
var a3 = (float)(x1 - radius * Math.Cos(lineangle + (anglerad / 2.0)));
var a4 = (float)(y1 - radius * Math.Sin(lineangle + (anglerad / 2.0)));
mArrow.LineTo(a3, a4);
paint.AntiAlias = true;
paint.SetStyle(Android.Graphics.Paint.Style.Stroke);
canvas.DrawPath(mArrow, paint);
canvas.DrawLine(x0, y0, x1, y1, paint);
base.OnDraw(canvas);
}
}
You can use QuadTo to replace the LineTo here, and since this api add a quadratic bezier from the last point, approaching control point (x1,y1), and ending at (x2,y2). Be careful with the start point and the last point of the lines.
So you can replace your code:
mArrow.MoveTo(x1, y1);
var a1 = (float)(x1 - radius * Math.Cos(lineangle - (anglerad / 2.0)));
var a2 = (float)(y1 - radius * Math.Sin(lineangle - (anglerad / 2.0)));
mArrow.LineTo(a1, a2);
mArrow.MoveTo(a1, a2);
mArrow.MoveTo(x1, y1);
var a3 = (float)(x1 - radius * Math.Cos(lineangle + (anglerad / 2.0)));
var a4 = (float)(y1 - radius * Math.Sin(lineangle + (anglerad / 2.0)));
mArrow.LineTo(a3, a4);
To:
var a1 = (float)(x1 - radius * Java.Lang.Math.Cos(lineangle - (anglerad / 2.0)));
var a2 = (float)(y1 - radius * Java.Lang.Math.Sin(lineangle - (anglerad / 2.0)));
mArrow.MoveTo(a1, a2);
mArrow.QuadTo(a1, a2, x1, y1);
var a3 = (float)(x1 - radius * Java.Lang.Math.Cos(lineangle + (anglerad / 2.0)));
var a4 = (float)(y1 - radius * Java.Lang.Math.Sin(lineangle + (anglerad / 2.0)));
mArrow.QuadTo(x1, y1, a3, a4);
I changed the paint.Color to make it clear by my side:

Existing Triangles Cannot be Removed

I bind triangles with data in D3.js. But the triganle cannot be removed with the data. The rectangles are okay. Complete code is attached!
var svg = d3.select("body").append("svg")
.attr("width", 250)
.attr("height", 250);
function render(data){
var tris = svg.selectAll("tri").data(data);
tris.enter().append("path");
tris.attr("d", function(d) {
var x1 = (0.4 - 0.2 * (d - 1)) * 250, y1 = 0.3 * 250;
var x2 = (0.5 - 0.2 * (d - 1)) * 250, y2 = 0.1 * 250;
var x3 = (0.6 - 0.2 * (d - 1)) * 250, y3 = 0.3 * 250;
return "M" + x1 + " " + y1 + " L" + x2 + " " + y2 + " L" + x3 + " " + y3 + "Z";
});
tris.exit().remove();
var rects = svg.selectAll("rect").data(data);
rects.enter().append("rect");
rects.attr("y", 50)
.attr("width", 20)
.attr("height", 20)
.attr("x", function(d) { return d * 40; });
rects.exit().remove();
}
render([1, 2, 3]);
render([1, 2]);
render([1]);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
There is no SVG element called <tri>. Your enter selection works because you can select anything in your data binding function. As there is nothing named "tri" in the whole DOM (be it a tag, a class, an ID, whatever...), your enter selection is never empty and your exit selection is never populated.
That being said, an easy solution is selecting by class...
var tris = svg.selectAll(".tri").data(data);
And setting this class in your enter selection:
tris.enter().append("path").attr("class", "tri");
Here is your code with the changes:
var svg = d3.select("body").append("svg")
.attr("width", 250)
.attr("height", 250);
function render(data){
var tris = svg.selectAll(".tri").data(data);
tris.enter().append("path").attr("class","tri");
tris.attr("d", function(d) {
var x1 = (0.4 - 0.2 * (d - 1)) * 250, y1 = 0.3 * 250;
var x2 = (0.5 - 0.2 * (d - 1)) * 250, y2 = 0.1 * 250;
var x3 = (0.6 - 0.2 * (d - 1)) * 250, y3 = 0.3 * 250;
return "M" + x1 + " " + y1 + " L" + x2 + " " + y2 + " L" + x3 + " " + y3 + "Z";
});
tris.exit().remove();
var rects = svg.selectAll("rect").data(data);
rects.enter().append("rect");
rects.attr("y", 50)
.attr("width", 20)
.attr("height", 20)
.attr("x", function(d) { return d * 40; });
rects.exit().remove();
}
render([1, 2, 3]);
setTimeout(() => render([1, 2]), 1000);
setTimeout(() => render([1]), 2000)
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>

Get normal of bezier curve in 2D

I'm trying to wrap text in bezier curve and following tutorial from this link, http://www.planetclegg.com/projects/WarpingTextToSplines.html
I get Derivative by this code:
function Derivative(x0,x1,x2,t)
{
var mt = 1-t;
var a = mt * mt;
var b = mt * t * 2;
var c = t * t;
var result=a * x0 + b * x1 + c * x2;
return result;
}
So i calcu Normal with this code:
function Normal(x0,x1,x2,y0,y1,y2,t)
{
var dx = Derivative(x0,x1,x2,t);
var dy = Derivative(y0,y1,y2,t);
var q = Math.sqrt(dx*dx + dy*dy)
return { x: -dy/q, y: dx/q };
};
So this is result: somthing wrong but i don't know where.
Thanks you all!
The "what I want" image looks a lot like my bezierjs documentation, so: you have the right idea (take the derivative to get the tangent vector, then rotate to get the normal), but make sure to get those derivatives right.
If you're using quadratic Bezier curves, consisting of three 2d points P1, P2 and P3, then the Bezier function is:
P1 * (1-t)² + P2 * 2 * (1-t)t + P3 * t²
and the derivative (written in but one of many ways) is:
P1 * (2t-2) + (2*P3-4*P2) * t + 2 * P2
The code you show as derivative computation is actually the regular quadratic Bezier function, so that's going to give you rather wrong results. Update the code to the correct derivative, and you should be fine.
Pomax's answer is all you need, but if you care for a bit of code here are some util methods implemented in Javascript:
// these methods are only for quadratic curves
// p1: {x,y} start point
// pc: {x,y} control point
// p2: {x,y} end point
// t: (float between 0 and 1) time in the curve
getPointAt(t, p1, pc, p2) {
const x = (1 - t) * (1 - t) * p1.x + 2 * (1 - t) * t * pc.x + t * t * p2.x
const y = (1 - t) * (1 - t) * p1.y + 2 * (1 - t) * t * pc.y + t * t * p2.y
return { x, y };
}
getDerivativeAt(t, p1, pc, p2) {
const d1 = { x: 2 * (pc.x - p1.x), y: 2 * (pc.y - p1.y) };
const d2 = { x: 2 * (p2.x - pc.x), y: 2 * (p2.y - pc.y) };
const x = (1 - t) * d1.x + t * d2.x;
const y = (1 - t) * d1.y + t * d2.y;
return { x, y };
}
getNormalAt(t, p1, pc, p2) {
const d = getDerivativeAt(t, p1, pc, p2);
const q = sqrt(d.x * d.x + d.y * d.y);
const x = -d.y / q;
const y = d.x / q;
return { x, y };
}
https://jsfiddle.net/Lupq8ejm/1/

Webgl: Rotating object becomes distorted along axes

I'm having trouble trying to display three cartesian planes in 3D, when they rotate the planes get stretched in one direction (z-axis, blue) and compressed in another (x-axis, red) like these images showing rotation around the y-axis:
45 degrees:
95 degrees:
135 degrees:
I calculate the perspective matrix, with mat4.perspective from the gl-matrix library:
mat4.perspective(this.pMatrix_, this.vFieldOfView_, this.aspect_, this.near_, this.far_);
with values of:
private near_ = 0.1;
private far_ = 1000.0;
private vFieldOfView_ = 60.0 * Math.PI / 180;
Vertex Shader:
void main(void) {
gl_Position = uProjection * uView * uTransform * vec4(aVertexPosition, 1.0);
}
The view matrix translates the object 2.0 units away from the camera.
let t = new Mat4();
t.array[12] = v.x;
t.array[13] = v.y;
t.array[14] = v.z;
I rotate the planes around the y-axis using the matrix generated from this code:
// identity matrix already set
let rad = angle * Math.PI / 180;
r.array[0] = Math.cos(rad);
r.array[2] = Math.sin(rad);
r.array[8] = -1.0 * Math.sin(rad);
r.array[10] = Math.cos(rad);
And I multiply the three transform matrices of the object in this order:
rotation * translation * scale. Was using quaternions to handle rotations but they were similarly distorted so went back to using rotation matrices and kept the rotation simple, in one axis. It looks like I'm doing some multiplication step in the wrong order or not using the perspective matrix correctly or got a sign wrong.
Update:
Just to clarify on the value of some of the matrices in vertex shader:
uProjection = pMatrix_ = value obtained from mat.perspective(...).
uView = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, -2.0, 0, 0, 0, 1] i.e. matrix translated 2 units away in z-axis.
uTransform should be identity matrix in this example.
Update2:
uView was actually [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, -2.0, 1]
You have transposed your view matrix. You have
1 0 0 0
0 1 0 0
0 0 1 0
0 0 -2 1
You want:
1 0 0 0
0 1 0 0
0 0 1 -2
0 0 0 1
This mistake never would have happened if you just stuck with gl-matrix and used mat4.translate(). This is why I don't use direct array access for creating a matrix, it's too easy to screw up.
Remember that OpenGL matrixes are stored like an array of column vectors. So the indexes go like this:
0 4 8 12
1 5 9 13
2 6 10 14
3 7 11 15
I found out where I was going wrong with my implementation, it was with the matrix multiplication. The correct code is this:
static multiply(a: Mat4, b: Mat4, out: Mat4) {
let a11 = a.array[0], a12 = a.array[1], a13 = a.array[2], a14 = a.array[3],
a21 = a.array[4], a22 = a.array[5], a23 = a.array[6], a24 = a.array[7],
a31 = a.array[8], a32 = a.array[9], a33 = a.array[10], a34 = a.array[11],
a41 = a.array[12], a42 = a.array[13], a43 = a.array[14], a44 = a.array[15];
for (let i = 0; i < 16; i += 4) {
let b1 = b.array[i], b2 = b.array[i + 1], b3 = b.array[i + 2], b4 = b.array[i + 3];
out.array[i] = b1 * a11 + b2 * a21 + b3 * a31 + b4 * a41;
out.array[i + 1] = b1 * a12 + b2 * a22 + b3 * a32 + b4 * a42;
out.array[i + 2] = b1 * a13 + b2 * a23 + b3 * a33 + b4 * a43;
out.array[i + 3] = b1 * a14 + b2 * a24 + b3 * a34 + b4 * a44;
}
};

Specifying a number of nodes in each cluster of clustered force layout in d3js

I am trying to design an infographic using the Clustered Force Layout III example and I have it almost the way I want, but I need to specify a number of nodes per cluster. Cluster 1 - 10 nodes, Cluster 2 - 7 nodes, Cluster 3 - 11 nodes, Cluster 4 - 18 nodes, Cluster 5 - 16 nodes, Cluster 6 - 19 nodes, Cluster 7 - 42 nodes, Cluster 8 - 14 nodes. I have the correct total number of nodes and the correct total number of Clusters and I've made all the nodes the same size. Now I just need to push the circles around to represent by data. My code is below.
var width = 175,
height = 175,
padding = 1.5, // separation between same-color nodes
clusterPadding = 6, // separation between different-color nodes
maxRadius = 4;
var n = 137, // total number of nodes
m = 8; // number of distinct clusters
var color = d3.scale.category10()
.domain(d3.range(m));
// The largest node for each cluster.
var clusters = new Array(m);
var nodes = d3.range(n).map(function() {
var i = Math.floor(Math.random() * m),
r = maxRadius,
d = {
cluster: i,
radius: r,
x: Math.cos(i / m * 2 * Math.PI) * 200 + width / 2 + Math.random(),
y: Math.sin(i / m * 2 * Math.PI) * 200 + height / 2 + Math.random()
};
if (!clusters[i] || (r > clusters[i].radius)) clusters[i] = d;
return d;
});
var force = d3.layout.force()
.nodes(nodes)
.size([width, height])
.gravity(.02)
.charge(0)
.on("tick", tick)
.start();
var svg = d3.select(".wpd3-1042-0").append("svg")
.attr("width", width)
.attr("height", height);
var node = svg.selectAll("circle")
.data(nodes)
.enter().append("circle")
.style("fill", function(d) { return color(d.cluster); })
.call(force.drag);
node.transition()
.duration(750)
.delay(function(d, i) { return i * 5; })
.attrTween("r", function(d) {
var i = d3.interpolate(0, d.radius);
return function(t) { return d.radius = i(t); };
});
function tick(e) {
node
.each(cluster(10 * e.alpha * e.alpha))
.each(collide(.5))
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
}
// Move d to be adjacent to the cluster node.
function cluster(alpha) {
return function(d) {
var cluster = clusters[d.cluster];
if (cluster === d) return;
var x = d.x - cluster.x,
y = d.y - cluster.y,
l = Math.sqrt(x * x + y * y),
r = d.radius + cluster.radius;
if (l != r) {
l = (l - r) / l * alpha;
d.x -= x *= l;
d.y -= y *= l;
cluster.x += x;
cluster.y += y;
}
};
}
// Resolves collisions between d and all other circles.
function collide(alpha) {
var quadtree = d3.geom.quadtree(nodes);
return function(d) {
var r = d.radius + maxRadius + Math.max(padding, clusterPadding),
nx1 = d.x - r,
nx2 = d.x + r,
ny1 = d.y - r,
ny2 = d.y + r;
quadtree.visit(function(quad, x1, y1, x2, y2) {
if (quad.point && (quad.point !== d)) {
var x = d.x - quad.point.x,
y = d.y - quad.point.y,
l = Math.sqrt(x * x + y * y),
r = d.radius + quad.point.radius + (d.cluster === quad.point.cluster ? padding : clusterPadding);
if (l < r) {
l = (l - r) / l * alpha;
d.x -= x *= l;
d.y -= y *= l;
quad.point.x += x;
quad.point.y += y;
}
}
return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
});
};
}
Yes! you can achieve this by doing something like this:
var clusterNumber = [10, 7, 11, 18, 16, 19, 42, 14];//your cluster number of node array
var n = d3.sum(clusterNumber, function (d) {
return d
}); /// total number of nodes
var m = clusterNumber.length;//total number of clusters
var color = d3.scale.category10()
.domain(d3.range(m));
// The largest node for each cluster.
var clusters = new Array(m);
var nodes = [];
clusterNumber.forEach(function (cn, i) {
//this will make a cluster
var r = maxRadius;
for (var j = 0; j < cn; j++) {
//this loop will make all the nodes
var d = {
cluster: i,
radius: r,
x: Math.cos(i / m * 2 * Math.PI) * 200 + width / 2 + Math.random(),
y: Math.sin(i / m * 2 * Math.PI) * 200 + height / 2 + Math.random()
};
if (!clusters[i] || (r > clusters[i].radius)) clusters[i] = d;
nodes.push(d);
}
});
Full working code here
Hope this helps!

Resources