This example shows a failure case. The layers of D3.js and Three.js are not synchronized together. The rotation is calculated by using quaternion, which is better than Euler angles to provide more intuitive interaction. The Euler angles case can be found here.
xxxxxxxxxx
<html>
<head>
<meta charset='utf-8'>
<script src='https://d3js.org/d3.v4.min.js'></script>
<script src='https://cdnjs.cloudflare.com/ajax/libs/three.js/r83/three.min.js'></script>
<style>
#map-layer {
position: absolute;
left: 0;
top: 0;
z-index: 2;
}
#shade-layer {
position: absolute;
left: 0;
top: 0;
z-index: 1;
}
</style>
</head>
<body>
<script>
// Set parameters.
const WIDTH = 512, HEIGHT = 512
const RADIUS = 120
const INITIAL_ROTATE = [0, 0]
const SCALE = RADIUS
const BACKGROUND_COLOR = 'white'
const TO_RADIAN = Math.PI / 180
const TO_DEGREE = 180 / Math.PI
const ROTATION_SCALE = 0.25
// create projection object.
var projection = d3.geoOrthographic()
.translate([WIDTH / 2, HEIGHT / 2])
.scale(SCALE)
.clipAngle(90)
.rotate(INITIAL_ROTATE)
// Create map layers at multiple resolutions.
var mapLayer = d3.select('body').append('canvas').attr('id', 'map-layer')
.attr('width', WIDTH).attr('height', HEIGHT)
var mapContext = mapLayer.node().getContext('2d')
mapContext.strokeStyle = 'black'
var mapPath = d3.geoPath().projection(projection).context(mapContext)
var mapMarks = {
type: 'FeatureCollection',
features: [{
type: 'Feature',
geometry: {
type: 'LineString',
coordinates: [
[-5, 0],
[ 5, 0]
]
}
}, {
type: 'Feature',
geometry: {
type: 'LineString',
coordinates: [
[0, -5],
[0, 5]
]
}
}]
}
function drawMapLayer() {
mapContext.setTransform(1, 0, 0, 1, 0, 0)
mapContext.clearRect(0, 0, WIDTH, HEIGHT)
mapContext.beginPath()
mapPath(mapMarks)
mapContext.lineWidth = 2
mapContext.stroke()
}
drawMapLayer()
// Define help functions.
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) {
for (var i = 0, sum = 0; v0.length > i; ++i) sum += v0[i] * v1[i]
return sum
}
function sphericalToCartesian(x) {
var lon = x[0] * TO_RADIAN
var lat = x[1] * TO_RADIAN
return [Math.cos(lat) * Math.cos(lon), Math.cos(lat) * Math.sin(lon), Math.sin(lat)]
}
function quaternionFromVectors(v0, v1) {
var w = cross(v0, v1)
var l = Math.sqrt(dot(w, w))
if (l == 0) return
var theta = 0.5 * Math.acos(Math.max(-1, Math.min(1, dot(v0, v1))))
var qi = w[2] * Math.sin(theta) / l
var qj = - w[1] * Math.sin(theta) / l
var qk = w[0] * Math.sin(theta) / l
var qr = Math.cos(theta)
return theta && [qr, qi, qj, qk]
}
function eulerAnglesToQuaternion(e) {
if(!e) return
var roll = 0.5 * e[0] * TO_RADIAN
var pitch = 0.5 * e[1] * TO_RADIAN
var yaw = 0.5 * e[2] * TO_RADIAN
var sr = Math.sin(roll)
var cr = Math.cos(roll)
var sp = Math.sin(pitch)
var cp = Math.cos(pitch)
var sy = Math.sin(yaw)
var cy = Math.cos(yaw)
qi = sr*cp*cy - cr*sp*sy
qj = cr*sp*cy + sr*cp*sy
qk = cr*cp*sy - sr*sp*cy
qr = cr*cp*cy + sr*sp*sy
return [qr, qi, qj, qk]
}
function multiplyQuaternions(q0, q1) {
if(!q0 || !q1) return;
var a = q0[0]
var b = q0[1]
var c = q0[2]
var d = q0[3]
var e = q1[0]
var f = q1[1]
var g = q1[2]
var h = q1[3]
return [
a*e - b*f - c*g - d*h,
b*e + a*f + c*h - d*g,
a*g - b*h + c*e + d*f,
a*h + b*g - c*f + d*e]
}
function quaternionToEulerAngles(q) {
if (!q) return
return [
Math.atan2(2 * (q[0] * q[1] + q[2] * q[3]), 1 - 2 * (q[1] * q[1] + q[2] * q[2])) * TO_DEGREE,
Math.asin(Math.max(-1, Math.min(1, 2 * (q[0] * q[2] - q[3] * q[1])))) * TO_DEGREE,
Math.atan2(2 * (q[0] * q[3] + q[1] * q[2]), 1 - 2 * (q[2] * q[2] + q[3] * q[3])) * TO_DEGREE
]
}
// Handle drag event.
var m0, sens = 0.25
var drag = d3.drag().on('start', function() {
m0 = projection.invert(d3.mouse(this))
}).on('drag', function() {
// Skip spurious drag event.
if (d3.event.dx === 0 && d3.event.dy === 0) return
var m1 = projection.invert(d3.mouse(this))
var r0 = projection.rotate()
var x0 = sphericalToCartesian(m0)
var x1 = sphericalToCartesian(m1)
var q0 = eulerAnglesToQuaternion(r0)
var q1 = quaternionFromVectors(x0, x1)
var q1 = multiplyQuaternions(q0, q1)
var r1 = quaternionToEulerAngles(q1)
projection.rotate(r1)
drawMapLayer()
// Rotate Three.js objects.
var q = new THREE.Quaternion(q1[1], q1[2], q1[3], q1[0])
q.normalize()
sphereObject.setRotationFromQuaternion(q)
drawShadeLayer()
})
mapLayer.call(drag)
// Create 3D scene and camera objects.
var scene = new THREE.Scene()
var camera = new THREE.OrthographicCamera(-WIDTH / 2, WIDTH / 2, HEIGHT / 2, -HEIGHT / 2, 0.1, 10000)
camera.position.z = 500
// Create renderer object.
var renderer = new THREE.WebGLRenderer({
antialias: true
})
renderer.domElement.id = 'shade-layer'
renderer.setClearColor(BACKGROUND_COLOR, 1)
renderer.setSize(WIDTH, HEIGHT)
document.body.appendChild(renderer.domElement)
// Add light to scene.
var light = new THREE.HemisphereLight('#fff', '#666', 1.5)
light.position.set(0, 500, 0)
scene.add(light)
// Create sphere.
var sphere = new THREE.SphereGeometry(120, 100, 100)
var sphereMaterial = new THREE.MeshNormalMaterial({
wireframe: false
})
var sphereMesh = new THREE.Mesh(sphere, sphereMaterial)
// For debug ...
var dot1 = new THREE.SphereGeometry(5, 10, 10)
dot1.translate(0, 0, 120)
var dot1Material = new THREE.MeshBasicMaterial({
color: 'blue'
})
var dot1Mesh = new THREE.Mesh(dot1, dot1Material)
var dot2 = new THREE.SphereGeometry(5, 10, 10)
dot2.translate(120, 0, 0)
var dot2Material = new THREE.MeshBasicMaterial({
color: 'red'
})
var dot2Mesh = new THREE.Mesh(dot2, dot2Material)
var dot3 = new THREE.SphereGeometry(5, 10, 10)
dot3.translate(0, -120, 0)
var dot3Material = new THREE.MeshBasicMaterial({
color: 'green'
})
var dot3Mesh = new THREE.Mesh(dot3, dot3Material)
var sphereObject = new THREE.Object3D()
sphereObject.add(sphereMesh, dot1Mesh, dot2Mesh, dot3Mesh)
scene.add(sphereObject)
function drawShadeLayer() {
renderer.render(scene, camera)
}
drawShadeLayer()
</script>
</body>
</html>
https://d3js.org/d3.v4.min.js
https://cdnjs.cloudflare.com/ajax/libs/three.js/r83/three.min.js