Add inertia issue #27 to versor rotation of the globe.
Includes a modified versor.js, allowing the multiplication of the final rotation by a delta to the power alpha.
Moving average for velocity (exponential decay)
Trying to plug it in as cleanly as possible into d3.drag
Original research by Philippe Rivière for visionscarto.net.
Try it on mobile!
forked from Fil's block: Inertia dragging with versor
xxxxxxxxxx
<meta charset="utf-8">
<body>
<canvas width="960" height="500"></canvas>
<script src="https://unpkg.com/d3"></script>
<script src="https://d3js.org/topojson.v2.min.js"></script>
<script src="versor.js"></script>
<script>
function inertiaDrag(projection, render, started, moved, ended) {
const limit = 1.0001;
const A = 5000; // reference time in ms
const B = -Math.log(1 - 1 / limit);
const inertia = {
start() {
inertia.velocity = [0, 0];
if (inertia.timer)
inertia.timer.stop();
started.call(this, inertia.position = d3.mouse(this));
},
move() {
const position = d3.mouse(this);
const time = performance.now();
let timeGap = time - inertia.time;
const decay = 1 - Math.exp(-timeGap / 1000);
inertia.velocity = inertia.velocity.map(function(d, i) {
const posGap = position[i] - inertia.position[i];
const decay1 = 1 - decay;
const decayD = d * decay;
timeGap = time - inertia.time;
return 1000 * decay1 * posGap / timeGap + decayD;
});
inertia.time = time;
moved.call(this, inertia.position = position);
},
end() {
const v = inertia.velocity;
if (v[0] * v[0] + v[1] * v[1] < 100)
return;
// versor velocity
const 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) {
const t = inertia.t = limit * (1 - Math.exp(-B * e / A)),
q1 = versor.multiply(q0, versor.delta(v0, v1, 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.call(this, inertia.position);
},
position: [0, 0],
velocity: [0, 0], // in pixels/s
timer: null,
time: 0,
}
return inertia;
};
const canvas = d3.select("canvas"),
width = 960,
height = 500,
projection = d3.geoOrthographic(),
context = canvas.node().getContext("2d"),
path = d3.geoPath().projection(projection).context(context);
let land,
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.
const render = function() {
context.clearRect(0, 0, width, height);
context.beginPath();
path(land);
context.fill();
};
function dragstarted(position) {
v0 = versor.cartesian(projection.invert(position));
r0 = projection.rotate();
q0 = versor(r0);
}
function dragged(position) {
const inv = projection.rotate(r0).invert(position);
if (isNaN(inv[0])) return;
const v1 = versor.cartesian(inv),
q1 = versor.multiply(q0, versor.delta(v0, v1)),
r1 = versor.rotation(q1);
projection.rotate(r1);
render();
}
function dragended() {
// code...
}
const {start, move, end} = inertiaDrag(projection, render, dragstarted, dragged, dragended);
canvas.call(d3.drag()
.on("start", start)
.on("drag", move)
.on("end", end)
);
d3.json("https://unpkg.com/world-atlas@1/world/110m.json", function(error, world) {
land = topojson.feature(world, world.objects.land);
render();
});
d3.select(self.frameElement).style("height", height + "px");
</script>
https://unpkg.com/d3
https://d3js.org/topojson.v2.min.js