<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Three.js WebGL + CSS3D (Stack Snippet)</title>
<style>
html, body { margin:0; height:100%; overflow:hidden; background:#000; }
/* Stack the two renderers exactly on top of each other */
canvas, #css3d {
position:absolute; top:0; left:0; width:100%; height:100%;
}
</style>
<!-- Import map to resolve bare specifiers used by addons -->
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/"
}
}
</script>
</head>
<body>
<div id="css3d"></div>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { CSS3DRenderer, CSS3DObject } from 'three/addons/renderers/CSS3DRenderer.js';
let camera, scene, renderer, controls;
let scene2, renderer2;
const frustumSize = 500;
const material = new THREE.MeshBasicMaterial({ color: 0x000000, side: THREE.DoubleSide });
init();
animate();
function init() {
// Camera (orthographic)
const aspect = window.innerWidth / window.innerHeight;
camera = new THREE.OrthographicCamera(
(frustumSize * aspect) / -2,
(frustumSize * aspect) / 2,
frustumSize / 2,
-frustumSize / 2,
1,
1000
);
camera.position.set(-200, 200, 200);
camera.lookAt(0, 0, 0);
// Scenes
scene = new THREE.Scene();
scene.background = new THREE.Color(0x000000);
scene2 = new THREE.Scene();
// WebGL renderer (for the planes)
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.outputColorSpace = THREE.SRGBColorSpace;
document.body.appendChild(renderer.domElement);
// CSS3D renderer (for HTML elements)
renderer2 = new CSS3DRenderer();
renderer2.setSize(window.innerWidth, window.innerHeight);
renderer2.domElement.id = 'css3d';
document.body.appendChild(renderer2.domElement);
// Controls (use CSS3D domElement to capture pointer events)
controls = new OrbitControls(camera, renderer2.domElement);
controls.minZoom = 0.5;
controls.maxZoom = 2;
controls.enableDamping = true;
// Create planes + matching CSS3D elements
createPlane(
100, 100, 'chocolate',
new THREE.Vector3(-50, 0, 0),
new THREE.Euler(0, -90 * THREE.MathUtils.DEG2RAD, 0)
); // left
createPlane(
100, 100, 'saddlebrown',
new THREE.Vector3(0, 0, 50),
new THREE.Euler(0, 0, 0)
); // right
createPlane(
100, 100, 'yellowgreen',
new THREE.Vector3(0, 50, 0),
new THREE.Euler(-90 * THREE.MathUtils.DEG2RAD, 0, 0)
); // top
createPlane(
300, 300, 'seagreen',
new THREE.Vector3(0, 0, 0),
new THREE.Euler(-90 * THREE.MathUtils.DEG2RAD, 0, 0)
); // bottom
window.addEventListener('resize', onWindowResize);
}
function createPlane(width, height, cssColor, pos, rot) {
// CSS3D element
const el = document.createElement('div');
el.style.width = width + 'px';
el.style.height = height + 'px';
el.style.opacity = '0.75';
el.style.background = cssColor;
const cssObj = new CSS3DObject(el);
cssObj.position.copy(pos);
cssObj.rotation.copy(rot);
scene2.add(cssObj);
// Matching WebGL mesh (so you can see it in WebGL renderer too)
const geom = new THREE.PlaneGeometry(width, height);
const mesh = new THREE.Mesh(geom, material);
mesh.position.copy(pos);
mesh.rotation.copy(rot);
scene.add(mesh);
}
function onWindowResize() {
const aspect = window.innerWidth / window.innerHeight;
camera.left = - (frustumSize * aspect) / 2;
camera.right = (frustumSize * aspect) / 2;
camera.top = frustumSize / 2;
camera.bottom = - frustumSize / 2;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
renderer2.setSize(window.innerWidth, window.innerHeight);
}
function animate() {
requestAnimationFrame(animate);
// --- Your frustum tweak block (kept as-is, just safer math) ---
function distanceToOrigin(p) { return Math.hypot(p.x, p.y, p.z); }
camera.position.z = 0; // as in your original code
const r = distanceToOrigin(camera.position) || 1e-6; // avoid divide-by-zero
const angle = Math.asin(THREE.MathUtils.clamp(camera.position.y / r, -1, 1));
const s = 150 * Math.sin(angle) * 1.10;
camera.left = -s;
camera.right = s;
camera.top = s;
camera.bottom = -s;
camera.updateProjectionMatrix();
// -------------------------------------------------------------
controls.update();
renderer.render(scene, camera);
renderer2.render(scene2, camera);
}
</script>
</body>
</html>