Skip to content

Controls

PolyCSS ships additive controls that follow the three.js split:

  • PolyOrbitControls: orbit-style drag (pointer drag rotates around the scene center) + wheel zoom + autorotate. This is the default pick for most scenes.
  • PolyMapControls: map/pan-style drag (pointer drag pans the camera across the surface). Use for top-down or flat layouts.
  • PolyFirstPersonControls: pointer-lock mouselook plus WASD / arrow movement, jump, and crouch.
  • PolyTransformControls: translate / rotate gizmo for a selected mesh handle.

Camera controls mutate the wrapping <poly-camera> / PolyCamera state. Transform controls mutate the attached mesh handle via setTransform.

They are available as custom elements, imperative APIs, and React / Vue components.

(React / Vue prop names use camelCase; the <poly-orbit-controls> / <poly-map-controls> custom elements accept the kebab-case form, e.g. animate.speedanimate-speed.)

PropTypeDefaultDescription
dragbooleantruePointer-drag rotation. Drag-right turns the front of the scene rightward, drag-down tilts the top toward the user: the visible object tracks the pointer.
wheelbooleantrueWheel / pinch zoom. Trackpad pinch is delivered as wheel with ctrlKey=true, so this covers desktop scroll + Mac pinch in one path.
invertboolean | numberfalseDrag-direction inversion. true reverses; a number scales sensitivity in the default direction (negative = invert).
minZoomnumber0.1Minimum zoom scale clamp.
maxZoomnumber10Maximum zoom scale clamp.
dollybooleanfalseWhen true, the wheel drives distance (dolly pull-back) instead of zoom scale. See Dolly mode below.
minDistancenumber0Minimum distance clamp when dolly is enabled.
maxDistancenumber5000Maximum distance clamp when dolly is enabled.
animatefalse | { speed?, axis?, pauseOnInteraction? }falseAuto-rotate. Pass false (or omit) to disable. See Animate options below.
FieldTypeDefaultDescription
speednumber0.3Degrees per 60 Hz-equivalent frame. The tick is dt-clamped (max 50 ms per frame), so 0.3 ≈ 18 deg/sec on every refresh rate.
axis"x" | "y""y"Rotation axis. "y" orbits the camera horizontally; "x" tilts vertically.
pauseOnInteractionbooleantrueHalt the animate loop while a pointer drag is in progress; resume on pointer-up.
<script type="module" src="https://esm.sh/@layoutit/polycss/elements"></script>
<poly-camera rot-x="65" rot-y="45">
<poly-scene>
<poly-orbit-controls drag wheel animate-speed="0.3"></poly-orbit-controls>
<poly-torus color="#4ecdc4"></poly-torus>
</poly-scene>
</poly-camera>

The presence of any animate-* attribute (animate-speed, animate-axis, animate-pause-on-interaction) implies animate is enabled. Removing them all turns animate off.

When you mount a scene through the createPolyScene imperative API, pair it with createPolyOrbitControls(scene, options). The controls handle returns a small lifecycle interface for live updates.

import { createPolyCamera, createPolyScene, createPolyOrbitControls, createPolyTorus } from "@layoutit/polycss";
const camera = createPolyCamera({ rotX: 65, rotY: 45 });
const scene = createPolyScene(host, { camera });
scene.add(createPolyTorus({ color: "#4ecdc4" }));
const controls = createPolyOrbitControls(scene, {
drag: true,
wheel: true,
animate: { speed: 0.3, axis: "y", pauseOnInteraction: true },
});
// Later: toggle features live without re-creating:
controls.update({ animate: false }); // stop auto-rotate
controls.update({ drag: false }); // also disable pointer drag
// Pause everything (detaches listeners + cancels rAF): reversible:
controls.pause();
controls.resume();
// Hard teardown:
controls.destroy();
// Three.js OrbitControls-style event subscription:
controls.addEventListener("change", (e) => console.log(e.camera));
controls.addEventListener("start", () => console.log("interaction begin"));
controls.addEventListener("end", () => console.log("interaction end"));

Use <poly-map-controls> (or createPolyMapControls) when you want drag to pan the camera across a flat surface rather than orbit around the scene center.

<poly-camera rot-x="90" rot-y="0">
<poly-scene>
<poly-map-controls drag wheel></poly-map-controls>
<poly-mesh src="/terrain.glb"></poly-mesh>
</poly-scene>
</poly-camera>

Disable input but keep autorotate running:

<poly-camera rot-x="65" rot-y="45">
<poly-scene>
<poly-orbit-controls drag="false" wheel="false" animate-speed="0.5"></poly-orbit-controls>
<poly-dodecahedron size="100" color="#a78bfa"></poly-dodecahedron>
</poly-scene>
</poly-camera>
<!-- 2× drag sensitivity -->
<poly-orbit-controls invert="2"></poly-orbit-controls>
<!-- Reverse direction -->
<poly-orbit-controls invert></poly-orbit-controls>

By default, the wheel adjusts zoom: a scale transform on the entire scene. Enable dolly to have the wheel adjust distance (a translateZ pull-back) instead. This mirrors three.js OrbitControls where the wheel changes the spherical radius around the target rather than scaling the projection.

When to use each:

  • Scale-zoom (default): Good for 2D-map-style or isometric scenes where you want the scene to grow/shrink in place.
  • Dolly (dolly={true}): Good for perspective scenes where depth foreshortening should stay consistent: the camera moves back rather than the scene shrinking.
// React: dolly mode with clamped range
<PolyOrbitControls dolly minDistance={100} maxDistance={3000} />
// Vanilla: createPolyOrbitControls
const controls = createPolyOrbitControls(scene, {
dolly: true,
minDistance: 100,
maxDistance: 3000,
});

PolyOrbitControls and PolyMapControls are purely additive: they attach their own pointer/wheel listeners and run their own requestAnimationFrame loop when animate is on. In vanilla, state changes flow through scene.setOptions(...); in React/Vue, they mutate the shared camera context and apply the transform directly.

The animate tick is dt-clamped at 50 ms per frame and normalized to 60 Hz. That makes speed: 0.3 produce the same ~18 deg/sec on every monitor refresh rate (60, 120, 144 Hz) and survives a tab regaining focus without a giant catch-up jump.

Use PolyFirstPersonControls for walkable scenes. Click the scene to acquire pointer lock; Escape releases it. Movement is keyboard-driven (WASD / arrows, Space jump, Ctrl crouch) and mouselook updates camera pitch/yaw.

PropTypeDefaultDescription
enabledbooleantrueMaster switch.
lookEnabledbooleantruePointer-lock mouselook.
moveEnabledbooleantrueWASD / arrow-key planar movement.
jumpEnabledbooleantrueSpace-bar jump arc.
crouchEnabledbooleantrueCtrl crouch.
lookSensitivitynumber0.15Degrees per pointer pixel.
invertYbooleanfalseInvert vertical look.
moveSpeednumber5World units per second.
jumpVelocitynumber7Initial jump velocity.
gravitynumber18Jump gravity.
eyeHeightnumber1.7Standing eye height above groundZ.
crouchHeightnumber1Crouched eye height.
groundZnumber0Walk plane height.
minPitch / maxPitchnumber5 / 175Pitch clamp in degrees.
import {
PolyPerspectiveCamera,
PolyScene,
PolyFirstPersonControls,
PolyMesh,
} from "@layoutit/polycss-react";
<PolyPerspectiveCamera perspective={1200} rotX={90} rotY={0}>
<PolyScene>
<PolyFirstPersonControls moveSpeed={8} eyeHeight={1.7} />
<PolyMesh src="/level.glb" />
</PolyScene>
</PolyPerspectiveCamera>

Imperative handles expose lock(), unlock(), isLocked(), getOrigin(), setOrigin(), pause(), resume(), destroy(), and update(partial).

PolyTransformControls attaches to a PolyMeshHandle and renders a PolyCSS gizmo. Translate mode provides axis arrows and plane handles; rotate mode provides axis rings. Dragging updates the attached mesh directly and emits transform-change callbacks.

import { useState } from "react";
import {
PolyCamera,
PolyScene,
PolyMesh,
PolySelect,
PolyTransformControls,
type PolyMeshHandle,
} from "@layoutit/polycss-react";
function Editor() {
const [selected, setSelected] = useState<PolyMeshHandle | null>(null);
return (
<PolyCamera rotX={65} rotY={45}>
<PolyScene>
<PolySelect onChange={(meshes) => setSelected(meshes[0] ?? null)}>
<PolyMesh id="asset" src="/model.glb" />
</PolySelect>
<PolyTransformControls object={selected} mode="translate" translationSnap={10} />
</PolyScene>
</PolyCamera>
);
}

Key props: object, mode, size, showX, showY, showZ, translationSnap, rotationSnap, enabled, onChange, onObjectChange, onMouseDown, onMouseUp, and onDraggingChanged.