Atlaslog
Atlaslog / Examples / three.js / Css2d Label
three.js · Runnable Example

Css2d Label

Runnable three.js example: Css2d Label. Open the live demo, read the complete source, and copy the file into your project. No build step or API key.

Open standalone → Chinese notes All three.js examples
/demos/three-css2d-label.htmlLIVE
single HTML file · CDN dependencies onlyinterface may contain Chinese labels

Source

demos/three-css2d-label.html
<!--
  说明:把 DOM 元素当作三维标注挂在球面上,主要由浏览器排版,画面里没有贴图文字
  核心逻辑:CSS2DObject 绑到经纬度换算的位置,CSS2DRenderer 与 WebGL 共用容器
  依赖:three.js 0.160.0、OrbitControls、CSS2DRenderer · jsDelivr CDN(ESM + importmap)
-->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta name="robots" content="noindex,follow">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>HTML 标注</title>
<style>
  html,body{height:100%;margin:0;background:#060b12}
  body{display:flex;flex-direction:column;overflow:hidden;
       font:13px/1.6 -apple-system,"Segoe UI","PingFang SC",sans-serif;color:#e6e8ec}
  .bar{flex:0 0 auto;padding:8px 12px;background:#161b22;border-bottom:1px solid #252c36;
       display:flex;gap:14px;align-items:center;font-size:12px;color:#8b93a1;flex-wrap:wrap}
  .bar b{color:#58a6ff;font-weight:600}
  .bar button{font:inherit;font-size:12px;padding:4px 10px;border-radius:5px;cursor:pointer;
       border:1px solid #303845;background:#1d2531;color:#c8d2e0}
  .bar button:hover{background:#253040}
  .bar .sp{flex:1 1 auto}
  #stage{flex:1 1 auto;min-height:0;position:relative;overflow:hidden}
  #stage canvas{display:block}
  .label{position:absolute;left:0;top:0;transform:translate(-50%,-50%);
       padding:2px 7px;border-radius:4px;font-size:11px;white-space:nowrap;
       background:rgba(9,16,24,.78);border:1px solid #2c4054;color:#cfe2f5;
       pointer-events:none;will-change:transform}
  .label.major{font-size:12px;color:#9fe6ff;border-color:#3f6a8a;background:rgba(10,24,36,.86)}
  .label .dot{display:inline-block;width:5px;height:5px;margin-right:5px;border-radius:50%;
       background:#5ad1ff;vertical-align:middle}
</style>
</head>
<body>
<div class="bar">
  <span>标注 <b id="cnt">0</b> 个 DOM 节点</span>
  <button id="toggle">隐藏标注</button>
  <span class="sp"></span>
  <span>拖拽旋转地球 · 滚轮缩放</span>
</div>
<div id="stage"></div>

<script type="importmap">
{
  "imports": {
    "three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js",
    "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/"
  }
}
</script>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { CSS2DRenderer, CSS2DObject } from 'three/addons/renderers/CSS2DRenderer.js';

const stage = document.getElementById('stage');
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(stage.clientWidth, stage.clientHeight);
renderer.outputColorSpace = THREE.SRGBColorSpace;
stage.appendChild(renderer.domElement);

const labelRenderer = new CSS2DRenderer();
labelRenderer.setSize(stage.clientWidth, stage.clientHeight);
labelRenderer.domElement.style.position = 'absolute';
labelRenderer.domElement.style.top = '0';
labelRenderer.domElement.style.left = '0';
labelRenderer.domElement.style.pointerEvents = 'none';
stage.appendChild(labelRenderer.domElement);

const scene = new THREE.Scene();
scene.background = new THREE.Color(0x060b12);

const camera = new THREE.PerspectiveCamera(42, stage.clientWidth / stage.clientHeight, 0.1, 2000);
camera.position.set(0, 6, 13);

const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.minDistance = 6.5;
controls.maxDistance = 26;
controls.update();

scene.add(new THREE.HemisphereLight(0xbcd8ff, 0x0a1218, 1.2));
const sun = new THREE.DirectionalLight(0xffffff, 2.2);
sun.position.set(8, 6, 10);
scene.add(sun);

const R = 4;

/* 球体本体 */
const globe = new THREE.Mesh(
  new THREE.SphereGeometry(R, 64, 48),
  new THREE.MeshStandardMaterial({ color: 0x1b3b52, roughness: 0.85, metalness: 0.05 })
);
scene.add(globe);

/* 经纬网:每隔 30° 一条线 */
const gratMat = new THREE.LineBasicMaterial({ color: 0x2f5b78, transparent: true, opacity: 0.55 });
for (let lat = -60; lat <= 60; lat += 30) {
  const pts = [];
  const phi = THREE.MathUtils.degToRad(lat);
  for (let lon = 0; lon <= 360; lon += 4) {
    const th = THREE.MathUtils.degToRad(lon);
    pts.push(new THREE.Vector3(
      R * Math.cos(phi) * Math.cos(th),
      R * Math.sin(phi),
      R * Math.cos(phi) * Math.sin(th)
    ));
  }
  scene.add(new THREE.Line(new THREE.BufferGeometry().setFromPoints(pts), gratMat));
}

for (let lon = 0; lon < 360; lon += 30) {
  const pts = [];
  const th = THREE.MathUtils.degToRad(lon);
  for (let lat = -90; lat <= 90; lat += 4) {
    const phi = THREE.MathUtils.degToRad(lat);
    pts.push(new THREE.Vector3(
      R * Math.cos(phi) * Math.cos(th),
      R * Math.sin(phi),
      R * Math.cos(phi) * Math.sin(th)
    ));
  }
  scene.add(new THREE.Line(new THREE.BufferGeometry().setFromPoints(pts), gratMat));
}

/* 经纬度转球面坐标 */
function latLonToVec(lat, lon, radius) {
  const phi = THREE.MathUtils.degToRad(lat);
  const theta = THREE.MathUtils.degToRad(lon);
  return new THREE.Vector3(
    radius * Math.cos(phi) * Math.cos(theta),
    radius * Math.sin(phi),
    radius * Math.cos(phi) * Math.sin(theta)
  );
}

const CITIES = [
  { name: '北京', lat: 39.9, lon: 116.4, major: true },
  { name: '上海', lat: 31.2, lon: 121.5, major: true },
  { name: '广州', lat: 23.1, lon: 113.3 },
  { name: '乌鲁木齐', lat: 43.8, lon: 87.6 },
  { name: '拉萨', lat: 29.7, lon: 91.1 },
  { name: '哈尔滨', lat: 45.8, lon: 126.6 },
  { name: '成都', lat: 30.7, lon: 104.1 },
  { name: '昆明', lat: 25.0, lon: 102.7 },
  { name: '新加坡', lat: 1.35, lon: 103.8 },
  { name: '悉尼', lat: -33.9, lon: 151.2 },
  { name: '莫斯科', lat: 55.8, lon: 37.6 },
  { name: '开罗', lat: 30.0, lon: 31.2 },
  { name: '内罗毕', lat: -1.3, lon: 36.8 },
  { name: '纽约', lat: 40.7, lon: -74.0, major: true },
  { name: '圣保罗', lat: -23.6, lon: -46.6 },
  { name: '洛杉矶', lat: 34.1, lon: -118.2 }
];

const labels = [];
const labelGroup = new THREE.Group();
scene.add(labelGroup);

CITIES.forEach(function (city) {
  const p = latLonToVec(city.lat, city.lon, R * 1.005);

  const dot = new THREE.Mesh(
    new THREE.SphereGeometry(city.major ? 0.075 : 0.05, 10, 8),
    new THREE.MeshBasicMaterial({ color: city.major ? 0x9fe6ff : 0x5ad1ff })
  );
  dot.position.copy(p);
  labelGroup.add(dot);

  const div = document.createElement('div');
  div.className = 'label' + (city.major ? ' major' : '');
  div.innerHTML = '<span class="dot"></span>' + city.name;

  const obj = new CSS2DObject(div);
  obj.position.copy(p).multiplyScalar(1.02);
  labelGroup.add(obj);
  labels.push(obj);
});

document.getElementById('cnt').textContent = String(labels.length);

let visible = true;
const toggleBtn = document.getElementById('toggle');
toggleBtn.addEventListener('click', function () {
  visible = !visible;
  labelGroup.visible = visible;
  labelRenderer.domElement.style.display = visible ? 'block' : 'none';
  toggleBtn.textContent = visible ? '隐藏标注' : '显示标注';
});

const clock = new THREE.Clock();

renderer.setAnimationLoop(function () {
  globe.rotation.y += 0.0012;
  labelGroup.rotation.y = globe.rotation.y;
  controls.update();
  renderer.render(scene, camera);
  labelRenderer.render(scene, camera);
});

window.addEventListener('resize', function () {
  const w = stage.clientWidth;
  const h = stage.clientHeight;
  camera.aspect = w / h;
  camera.updateProjectionMatrix();
  renderer.setSize(w, h);
  labelRenderer.setSize(w, h);
});
</script>
</body>
</html>
Language: the code and this page are in English; the full tutorial and API notes for this example are in Chinese. The demo itself is the authoritative version — it runs exactly as shown.

More three.js examples

three.js · EXAMPLECube three.js · EXAMPLEGeometry Colors three.js · EXAMPLEGeometry Dynamic