body {
margin: 0;
}
canvas {
cursor: grab;
display: block;
}
canvas:active {
cursor: grabbing;
}
.hovered {
cursor: help;
}
.label {
color: #fff;
font-family: sans-serif;
padding: 2px;
background: rgba(0, 0, 0, 0.6);
}
<script type="module">
import {
WebGLRenderer,
PerspectiveCamera,
Scene,
BoxGeometry,
MeshNormalMaterial,
Mesh,
Vector3,
Box3,
Raycaster,
Vector2,
} from 'https://unpkg.com/<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="ccb8a4bea9a98cfce2fdfef9e2fe">[email protected]</a>/build/three.module.js';
import { OrbitControls } from 'https://unpkg.com/<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="196d716b7c7c592937282b2c">[email protected]</a>/examples/jsm/controls/OrbitControls.js';
import {
CSS2DRenderer,
CSS2DObject,
} from 'https://unpkg.com/<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="acd8c4dec9c9ec9c829d9e99829e">[email protected]</a>/examples/jsm/renderers/CSS2DRenderer.js';
// Boilerplate
const { innerWidth, innerHeight } = window;
const renderer = new WebGLRenderer({ alpha: true, antialias: true });
renderer.setSize(innerWidth, innerHeight);
renderer.setPixelRatio(2);
document.body.appendChild(renderer.domElement);
const camera = new PerspectiveCamera(70, innerWidth / innerHeight, 0.1, 10);
camera.position.z = 1;
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
const scene = new Scene();
const geometry = new BoxGeometry(0.2, 0.2, 0.2);
const material = new MeshNormalMaterial();
const mesh = new Mesh(geometry, material);
mesh.name = 'mesh';
scene.add(mesh);
// Setting up labels
const labelRenderer = new CSS2DRenderer();
labelRenderer.setSize(innerWidth, innerHeight);
labelRenderer.domElement.style.position = 'absolute';
labelRenderer.domElement.style.top = '0px';
labelRenderer.domElement.style.pointerEvents = 'none';
document.body.appendChild(labelRenderer.domElement);
const labelDiv = document.createElement('div');
labelDiv.className = 'label';
labelDiv.style.marginTop = '-1em';
const label = new CSS2DObject(labelDiv);
label.visible = false;
scene.add(label);
// Tracking mouse movement for object selection
const raycaster = new Raycaster();
const mouse = new Vector2();
window.addEventListener('mousemove', ({ clientX, clientY }) => {
const { innerWidth, innerHeight } = window;
mouse.x = (clientX / innerWidth) * 2 - 1;
mouse.y = -(clientY / innerHeight) * 2 + 1;
});
// Handling window resize
window.addEventListener('resize', () => {
const { innerWidth, innerHeight } = window;
renderer.setSize(innerWidth, innerHeight);
camera.aspect = innerWidth / innerHeight;
camera.updateProjectionMatrix();
});
renderer.setAnimationLoop(() => {
controls.update();
// Selecting objects with normalized mouse coordinates
raycaster.setFromCamera(mouse, camera);
const [hovered] = raycaster.intersectObjects(scene.children);
if (hovered) {
// Configuring label
renderer.domElement.className = 'hovered';
label.visible = true;
labelDiv.textContent = hovered.object.name;
// Determining offset from object dimensions
const offset = new Vector3();
new Box3().setFromObject(hovered.object).getSize(offset);
// Moving label over hovered element
label.position.set(
hovered.object.position.x,
offset.y / 2,
hovered.object.position.z
);
} else {
// Resetting label
renderer.domElement.className = '';
label.visible = false;
labelDiv.textContent = '';
}
// Rendering scene
renderer.render(scene, camera);
// Rendering labels
labelRenderer.render(scene, camera);
});
</script>