How to do picking (i.e. country selection on mouseclick)?
I came across this project by Steve Hall. In it he makes a 3d map using Three.js and D3 that is responsive to mouseover and click events. He also has a great writeup that shares how he tackled the problem.
At the time, I didn't really understand what the article was saying. But two things I did takeaway were,
- the use of Three.js’s built-in raycasting to figure out the mouse’s coordinates in 3d space
- the use of an algorithm to determine if a point is inside a bounded region
2D Picking
A bit of searching later, I came across the related Wikipedia article (Point in Polygon). Even luckier, I came across this Stack Overflow answer which mentions that HTML Canvas already has a built-in, optimized algorithm (isPointInPath()
) for doing this detection. Yay!
With this in mind, I reworked my map drawing code a bit. Specifically, I saved each continent’s path as a Path2D object. And on mousemove, simply called isPointInPath( continentPath )
.
Here is a demo. (Mouseover the continents to see their names.)
And here’s the code,
// Initialize
for( var j = 0; j < rawishCoords.length; j++ ){
// Use Path2D() to store path data for later access, stackoverflow.com/a/28913470
var country = new Path2D( rawishCoords[j].d );
// draw the path
ctx.strokeStyle = "blue";
ctx.stroke(country);
rawishCoords[j].canvasPath = country;
}
// Listen for mousemove
canvas.addEventListener('mousemove', function(evt) {
var mousePos = getMousePos(c, evt);
whereIsThisPoint( mousePos.x, mousePos.y );
}, false);
// Check if point in path
var whereIsThisPoint = function( px, py ){
// Use canvas's built-in isPointInPath() method
// developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/isPointInPath
for( var i = 0; i < rawishCoords.length; i++ ){
var pointInPath = ctx.isPointInPath( rawishCoords[i].canvasPath, px, py);
if ( pointInPath ){
console.log( rawishCoords[i].id );
document.getElementById("textDiv").textContent = rawishCoords[i].id;
}
}
};
3D Picking
As for the 3D raycasting, I used one of Lee Stemkoski’s excellent Three.js demos, in combination with the official docs to get it working.
The code for this is as shown in the docs,
var raycaster = new THREE.Raycaster();
var mouse = new THREE.Vector2();
function onMouseMove( event ) {
// calculate mouse position in normalized device coordinates
// (-1 to +1) for both components
mouse.x = ( event.clientX / window.innerWidth ) * 2 - 1;
mouse.y = - ( event.clientY / window.innerHeight ) * 2 + 1;
}
function render() {
// update the picking ray with the camera and mouse position
raycaster.setFromCamera( mouse, camera );
// calculate objects intersecting the picking ray
var intersects = raycaster.intersectObjects( scene.children );
for ( var i = 0; i < intersects.length; i++ ) {
intersects[ i ].object.material.color.set( 0xff0000 );
}
renderer.render( scene, camera );
}
window.addEventListener( 'mousemove', onMouseMove, false );
window.requestAnimationFrame(render);
Play with it here. (Mouseover makes a face turn red.)
Combining the two
Coming soon.
The idea, would be to convert the 3D mouse coordinates to 2D coordinates, and use those with the isPointInPath()
method.