See also https://bl.ocks.org/Fil/42ee99b9ee5ac85d5412a0724a4a3828
forked from Fil's block: Inertia dragging with versor
license: gpl-3.0 |
See also https://bl.ocks.org/Fil/42ee99b9ee5ac85d5412a0724a4a3828
forked from Fil's block: Inertia dragging with versor
<!DOCTYPE html> | |
<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> |
// Version 0.0.0. Copyright 2017 Mike Bostock. | |
(function(global, factory) { | |
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : | |
typeof define === 'function' && define.amd ? define(factory) : | |
(global.versor = factory()); | |
}(this, (function() {'use strict'; | |
var acos = Math.acos, | |
asin = Math.asin, | |
atan2 = Math.atan2, | |
cos = Math.cos, | |
max = Math.max, | |
min = Math.min, | |
PI = Math.PI, | |
sin = Math.sin, | |
sqrt = Math.sqrt, | |
radians = PI / 180, | |
degrees = 180 / PI; | |
// Returns the unit quaternion for the given Euler rotation angles [λ, φ, γ]. | |
function versor(e) { | |
var l = e[0] / 2 * radians, sl = sin(l), cl = cos(l), // λ / 2 | |
p = e[1] / 2 * radians, sp = sin(p), cp = cos(p), // φ / 2 | |
g = e[2] / 2 * radians, sg = sin(g), cg = cos(g); // γ / 2 | |
return [ | |
cl * cp * cg + sl * sp * sg, | |
sl * cp * cg - cl * sp * sg, | |
cl * sp * cg + sl * cp * sg, | |
cl * cp * sg - sl * sp * cg | |
]; | |
} | |
// Returns Cartesian coordinates [x, y, z] given spherical coordinates [λ, φ]. | |
versor.cartesian = function(e) { | |
var l = e[0] * radians, p = e[1] * radians, cp = cos(p); | |
return [cp * cos(l), cp * sin(l), sin(p)]; | |
}; | |
// Returns the Euler rotation angles [λ, φ, γ] for the given quaternion. | |
versor.rotation = function(q) { | |
return [ | |
atan2(2 * (q[0] * q[1] + q[2] * q[3]), 1 - 2 * (q[1] * q[1] + q[2] * q[2])) * degrees, | |
asin(max(-1, min(1, 2 * (q[0] * q[2] - q[3] * q[1])))) * degrees, | |
atan2(2 * (q[0] * q[3] + q[1] * q[2]), 1 - 2 * (q[2] * q[2] + q[3] * q[3])) * degrees | |
]; | |
}; | |
// Returns the quaternion to rotate between two cartesian points on the sphere. | |
versor.delta = function(v0, v1, alpha = 1) { | |
var w = cross(v0, v1), l = sqrt(dot(w, w)); | |
if (!l) return [1, 0, 0, 0]; | |
var t = alpha * acos(max(-1, min(1, dot(v0, v1)))) / 2, s = sin(t); // t = θ / 2 | |
return [cos(t), w[2] / l * s, -w[1] / l * s, w[0] / l * s]; | |
}; | |
// Returns the quaternion that represents q0 * q1. | |
versor.multiply = function(q0, q1) { | |
return [ | |
q0[0] * q1[0] - q0[1] * q1[1] - q0[2] * q1[2] - q0[3] * q1[3], | |
q0[1] * q1[0] + q0[0] * q1[1] + q0[2] * q1[3] - q0[3] * q1[2], | |
q0[0] * q1[2] - q0[1] * q1[3] + q0[2] * q1[0] + q0[3] * q1[1], | |
q0[0] * q1[3] + q0[1] * q1[2] - q0[2] * q1[1] + q0[3] * q1[0] | |
]; | |
}; | |
function cross(v0, v1) { | |
return [ | |
v0[1] * v1[2] - v0[2] * v1[1], | |
v0[2] * v1[0] - v0[0] * v1[2], | |
v0[0] * v1[1] - v0[1] * v1[0] | |
]; | |
} | |
function dot(v0, v1) { | |
return v0[0] * v1[0] + v0[1] * v1[1] + v0[2] * v1[2]; | |
} | |
return versor; | |
}))); |