enableDamping = true adds Inertia's inertia (dampingFactor 0.05) and then needs controls.update(delta) every frame. Distance, polar and azimuth limits, enablePan and enableZoom keep the view flattering, and autoRotate turns a showcase. Without a DOM element the controls even run in Node:
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
const camera = new THREE.PerspectiveCamera();
camera.position.set(0, 1, 4);
const controls = new OrbitControls(camera); // no element: driven by code alone
Object.assign(controls, {
autoRotate: true, autoRotateSpeed: 2, // 2 means one orbit per 30 s
minAzimuthAngle: -Math.PI / 4, maxAzimuthAngle: Math.PI / 4, // stay in front of the shelf
maxPolarAngle: Math.PI / 2, minDistance: 2, maxDistance: 6, // above the floor, near
});
for (let second = 1; second <= 5; second++) {
for (let frame = 0; frame < 60; frame++) controls.update(1 / 60);
const azimuth = THREE.MathUtils.radToDeg(controls.getAzimuthalAngle());
console.log(`${second} s: azimuth ${azimuth.toFixed(1)} degrees`);
}Output
1 s: azimuth -12.0 degrees 2 s: azimuth -24.0 degrees 3 s: azimuth -36.0 degrees 4 s: azimuth -45.0 degrees 5 s: azimuth -45.0 degrees
Given a delta, update() turns at a per-second speed (12 degrees), and the azimuth limit stops it at 45.
<!doctype html>
<style>
body { margin: 0; background: #f5f1e8; font: 13px system-ui, sans-serif; color: #263238; }
canvas { display: block; max-width: 100%; }
.ui { padding: 4px 8px; }
</style>
<script type="importmap">
{ "imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.186.1/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.186.1/examples/jsm/" } }
</script>
<div class="ui"><label><input type="checkbox" id="auto" checked> autoRotate</label>
<label><input type="checkbox" id="damp" checked> enableDamping</label>
<label><input type="checkbox" id="lim" checked> ±45° azimuth limit</label> <span id="az"></span></div>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.setSize(600, 280);
document.body.prepend(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color('#fbf8f2');
const camera = new THREE.PerspectiveCamera(35, 600 / 280, 0.1, 50);
camera.position.set(0, 0.6, 3.6);
scene.add(new THREE.HemisphereLight('#ffffff', '#8d6e63', 1.2));
const sun = new THREE.DirectionalLight('#ffffff', 2.5);
sun.position.set(2, 3, 4);
scene.add(sun);
const floor = new THREE.Mesh(new THREE.CircleGeometry(3, 48), new THREE.MeshStandardMaterial({ color: '#e6ddcc' }));
floor.rotation.x = -Math.PI / 2;
floor.position.y = -0.55;
scene.add(floor);
const wall = new THREE.Mesh(new THREE.PlaneGeometry(4, 2.2), new THREE.MeshStandardMaterial({ color: '#d7cbb6' }));
wall.position.set(0, 0.5, -0.3);
scene.add(wall);
const plank = new THREE.Mesh(new THREE.BoxGeometry(3.2, 0.06, 0.4), new THREE.MeshStandardMaterial({ color: '#8d6e63' }));
plank.position.y = -0.52;
scene.add(plank);
['#1f5f8b', '#5b3f99', '#e09a10', '#3f7d3a', '#b5452f', '#2a9d8f'].forEach((color, i) => {
const book = new THREE.Mesh(new THREE.BoxGeometry(0.42, 0.98, 0.12), new THREE.MeshStandardMaterial({ color }));
book.position.x = (i - 2.5) * 0.5;
scene.add(book);
});
const controls = new OrbitControls(camera, renderer.domElement);
controls.target.set(0, -0.05, 0);
Object.assign(controls, {
enableDamping: true, dampingFactor: 0.05, // inertia after a drag
autoRotate: true, autoRotateSpeed: 4, // 2 would be one orbit per 30 s
maxPolarAngle: Math.PI / 2 - 0.05, // never below the floor
minDistance: 2, maxDistance: 6,
enablePan: false,
});
const $ = (id) => document.getElementById(id);
function applyLimits() {
controls.autoRotate = $('auto').checked;
controls.enableDamping = $('damp').checked;
const limit = $('lim').checked ? Math.PI / 4 : Infinity; // stay in front of the shelf
controls.minAzimuthAngle = -limit;
controls.maxAzimuthAngle = limit;
}
['auto', 'damp', 'lim'].forEach((id) => $(id).addEventListener('change', applyLimits));
applyLimits();
let direction = 1;
const timer = new THREE.Timer();
renderer.setAnimationLoop(() => {
timer.update();
const azimuth = THREE.MathUtils.radToDeg(controls.getAzimuthalAngle());
// At a limit auto-rotation stops, so reverse it to show the showcase swinging between the limits
if ($('lim').checked && Math.abs(azimuth) > 44.5) direction = -Math.sign(azimuth);
controls.autoRotateSpeed = 4 * direction;
controls.update(timer.getDelta()); // needed every frame with damping or autoRotate
$('az').textContent = `azimuth ${azimuth.toFixed(1)}°`;
renderer.render(scene, camera);
});
</script>