See also https://bl.ocks.org/Fil/42ee99b9ee5ac85d5412a0724a4a3828
forked from Fil's block: Inertia dragging with versor
xxxxxxxxxx
<meta charset="utf-8">
<body>
<script src="https://unpkg.com/d3@4"></script>
<script src="https://d3js.org/topojson.v2.min.js"></script>
<script src="versor.js"></script>
<script>
// import from math
var cos = Math.cos, sin = Math.sin, acos = Math.acos, asin = Math.asin, sqrt = Math.sqrt, atan2 = Math.atan2;
// import from d3-geo
function azimuthalRaw(scale) {
return function(x, y) {
var cx = cos(x),
cy = cos(y),
k = scale(cx * cy);
return [
k * cy * sin(x),
k * sin(y)
];
}
}
function azimuthalInvert(angle) {
return function(x, y) {
var z = sqrt(x * x + y * y),
c = angle(z),
sc = sin(c),
cc = cos(c);
return [
atan2(x * sc, z * cc),
asin(z && y * sc / z)
];
}
}
var width = 960,
height = 500,
m = 10;
// en alpha = z
// on a x pixels = scale(cx)*sin(x) = sqrt(alpha / (1 + (alpha - 1) * cos(x))) * sin(x)
//
var projectionRaw = function() {
var forward = azimuthalRaw(function(cxcy) {
return Math.pow(2 / (1 + cxcy), alpha / 2);
});
// exact inverse only for alpha = 0 and alpha = 1
forward.invert = azimuthalInvert(function(z) {
return (1 + alpha) * asin(z / (1 + alpha));
});
return forward;
},
alpha = 1,
projection = d3.geoProjection(projectionRaw());
var canvas = d3.select("body").append("canvas")
.attr("width", width)
.attr("height", height);
var context = canvas.node().getContext("2d");
var path = d3.geoPath()
.projection(projection)
.context(context);
var inertia = {
start: function(started) {
return function() {
var position = d3.mouse(this);
if (inertia.timer) inertia.timer.stop();
inertia.velocity = [0,0];
started(inertia.position = position);
};
},
move: function(moved) {
return function() {
var position = d3.mouse(this),
time = performance.now(),
decay = 1 - Math.exp(- (time - inertia.time) / 1000);
inertia.velocity = inertia.velocity.map(function(d,i) {
return 1000 * (1 - decay) * (position[i] - inertia.position[i]) / (time - inertia.time) + d * decay;
});
inertia.time = time;
moved(inertia.position = position);
};
},
end: function(started, moved, ended) {
var A = 5000; // reference time in ms
var limit = 1.0001,
B = -Math.log(1 - 1 / limit);
return function() {
var v = inertia.velocity;
if (v[0]*v[0] + v[1]*v[1] < 100) return;
console.log('ended at position', inertia.position, 'with velocity', inertia.velocity);
started(inertia.position);
// versor velocity
var p0 = inertia.position.map((d,i) => d - inertia.velocity[i] / 1000),
v0 = versor.cartesian(projection.invert(p0)),
r0 = projection.rotate(),
q0 = versor(r0),
p1 = inertia.position,
v1 = versor.cartesian(projection.invert(p1));
inertia.timer = d3.timer(function(e) {
inertia.t = limit * (1 - Math.exp( - B * e / A )); // eased time
// standard position = positionstart + t * velocity
// var position = inertia.position.map((d,i) => d + t * inertia.velocity[i])
// moved(position);
// versor position = quaternionstart * rotation^t
var q1 = versor.multiply(q0, versor.delta(v0, v1, inertia.t * 1000)),
r1 = versor.rotation(q1);
projection.rotate(r1);
render();
if (inertia.t > 1) {
inertia.timer.stop();
inertia.velocity = [0,0];
inertia.t = 1;
}
});
if (ended) ended(inertia.position);
};
},
position: [0,0],
velocity: [0,0], // in pixels/s
time: 0,
timer: null,
};
canvas.call(
d3.drag()
.on("start", inertia.start(dragstarted))
.on("drag", inertia.move(dragged))
.on("end", inertia.end(dragstarted, dragged, dragended))
);
var render = function() {},
v0, // Mouse position in Cartesian coordinates at start of drag gesture.
r0, // Projection rotation as Euler angles at start.
q0; // Projection rotation as versor at start.
function dragstarted(position) {
v0 = versor.cartesian(projection.invert(position));
r0 = projection.rotate();
q0 = versor(r0);
}
function dragged(position) {
var inv = projection.rotate(r0).invert(position);
if (isNaN(inv[0])) return;
var v1 = versor.cartesian(inv),
q1 = versor.multiply(q0, versor.delta(v0, v1)),
r1 = versor.rotation(q1);
projection.rotate(r1);
render();
}
function dragended() {}
d3.json("https://unpkg.com/world-atlas@1/world/110m.json", function(error, world) {
if (error) throw error;
var land = topojson.feature(world, world.objects.land);
render = function() {
context.clearRect(0, 0, width, height);
context.beginPath();
path(land);
context.fill();
context.strokeStyle = 'black';
context.beginPath();
path({type:"Sphere"});
context.lineWidth = 2.5;
context.stroke();
context.beginPath();
context.moveTo(
inertia.position[0] + inertia.velocity[0] / 10,
inertia.position[1] + inertia.velocity[1] / 10
);
context.lineTo(
inertia.position[0] + inertia.velocity[0] * inertia.t / 10,
inertia.position[1] + inertia.velocity[1] * inertia.t / 10
);
context.strokeStyle = "red";
context.stroke();
var p = projection.rotate().map(d => Math.floor(10*d)/10);
context.fillText(`λ = ${p[0]}, φ = ${p[1]}, γ = ${p[2]}`, 10, 10 )
context.fillText(`ɑ = ${Math.floor(100*alpha)/100}, clip=${Math.floor(100 * projection.clipAngle())/100}`, 10, 30 );
};
render();
if (1) var tt = d3.timer(function(e) {
alpha = (1 - Math.cos(e/400)) / 2;
var z = 2 * atan2(alpha, 1-alpha) / Math.PI;
projection
.clipAngle(90 + alpha * alpha * alpha * alpha * 90 - 1e-5)
.fitExtent([[m,m],[width-m,height-m]], {type:"Sphere"})
render();
//tt.stop();
});
});
d3.select(self.frameElement).style("height", height + "px");
</script>
https://unpkg.com/d3@4
https://d3js.org/topojson.v2.min.js