This example tracks the coordinates of a point in three-dimensional space using an SVG overlay.
Ideally, the callout line would be shortened by the radius of the circle plus some margin.
license: mit |
<html> | |
<head> | |
<title>Tracking</title> | |
<style> | |
body { margin: 0; } | |
canvas { width: 100%; height: 100% } | |
</style> | |
</head> | |
<body> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/84/three.min.js"></script> | |
<script src="https://d3js.org/d3.v4.min.js"></script> | |
<script> | |
var scene = new THREE.Scene(); | |
var camera = new THREE.PerspectiveCamera( 75, window.innerWidth/window.innerHeight, 0.1, 1000 ); | |
camera.position.z = 2; | |
var renderer = new THREE.WebGLRenderer(); | |
renderer.setClearColor( 'white' ) | |
renderer.setSize( window.innerWidth, window.innerHeight ); | |
const appDiv = document.createElement('div'); | |
document.body.appendChild(appDiv); | |
appDiv.appendChild( renderer.domElement ); | |
const canvas = document.querySelector('canvas'); | |
const ndc_to_canvas = { | |
x: d3.scaleLinear().domain([-1, 1]).range([0, canvas.width]), | |
y: d3.scaleLinear().domain([-1, 1]).range([canvas.height, 0]) | |
} | |
const svg_ns = "http://www.w3.org/2000/svg"; | |
var svg = document.createElementNS(svg_ns, "svg"); | |
svg.style.position = "absolute"; | |
svg.style.left = 0; | |
svg.style.top = 0; | |
svg.style.width = canvas.width; | |
svg.style.height = canvas.height; | |
svg.height = canvas.height; | |
appDiv.appendChild(svg); | |
const CIRCLE_RADIUS = 10; | |
const FONT_SIZE = 10; | |
const TEXT_POSITION = { | |
x: 500, | |
y: 100 | |
} | |
const circleG = document.createElementNS(svg_ns, "g"); | |
const circle = document.createElementNS(svg_ns, "circle") | |
circle.setAttribute('r', CIRCLE_RADIUS) | |
circle.style.fill = "none"; | |
circle.style.stroke = 'black' | |
circleG.appendChild(circle) | |
svg.appendChild(circleG) | |
const line = document.createElementNS(svg_ns, "line") | |
line.setAttribute('x1', TEXT_POSITION.x) | |
line.setAttribute('y1', TEXT_POSITION.y) | |
line.setAttribute('x2', 300) | |
line.setAttribute('y2', 300) | |
line.style.stroke = "black"; | |
svg.appendChild(line) | |
const textG = document.createElementNS(svg_ns, "g") | |
const text = document.createElementNS(svg_ns, "text"); | |
textG.appendChild(text) | |
textG.setAttribute("transform", `translate(${TEXT_POSITION.x}, ${TEXT_POSITION.y})`); | |
text.textContent = "Something We Care About" | |
text.style.fontSize = `${FONT_SIZE}px` | |
text.style.lineHeight = `${FONT_SIZE}px` | |
text.style.textTransform = "uppercase" | |
text.setAttribute('text-anchor', 'end') | |
text.setAttribute('dx', '-5px') | |
text.setAttribute('dy', `${FONT_SIZE/4}px`) | |
svg.appendChild(textG) | |
var geometry = new THREE.BoxGeometry( 1, 1, 1 ); | |
var material = new THREE.MeshBasicMaterial( { color: 0xbbbbbb, wireframe: true } ); | |
var cube = new THREE.Mesh( geometry, material ); | |
function makePoint() { | |
var geometry = new THREE.SphereGeometry( 0.02, 30, 30 ); | |
var material = new THREE.MeshBasicMaterial( {color: 0x000000} ); | |
var sphere = new THREE.Mesh( geometry, material ); | |
sphere.position.y = 0.5 | |
sphere.position.x = 0.4 | |
return sphere; | |
} | |
const point = makePoint() | |
cube.add(point) | |
scene.add( cube ); | |
const velocity = 0.02; | |
const ticks = Stream(next => { | |
function tick() { | |
next() | |
requestAnimationFrame(tick) | |
} | |
tick() | |
}) | |
const koob = Stream(out => { | |
const rotation = { x: 0, y: 0 }; | |
ticks.listen(tick => { | |
rotation.x += 1; | |
rotation.y += 1; | |
out(rotation) | |
}) | |
}) | |
const raycaster = new THREE.Raycaster(); | |
ticks.listen(function () { | |
cube.rotation.x += velocity; | |
cube.rotation.y += velocity; | |
renderer.render(scene, camera); | |
const world = point.getWorldPosition(); | |
// this is in Normalized Device Coords; | |
const camera_ndc = world.project(camera); | |
const screen_x = ndc_to_canvas.x(camera_ndc.x); | |
const screen_y = ndc_to_canvas.y(camera_ndc.y); | |
const screen = new THREE.Vector2(screen_x, screen_y); | |
circleG.setAttribute('transform', `translate(${screen.x}, ${screen.y})`) | |
line.setAttribute('x2', screen.x) | |
line.setAttribute('y2', screen.y) | |
}) | |
function Stream(start) { | |
return { | |
listen: start | |
} | |
} | |
</script> | |
</body> | |
</html> |