codys-cube
v0.3.1
Published
human-written description forthcoming, this is my cube.
Maintainers
Readme
codys-cube
Draft README (AI-drafted). Human editing pending — see
DESCRIPTION.draft.md. The repo/npm description is intentionally a placeholder for now.
A dependency-free, content-agnostic 3D cube. It handles the feel — drag to orbit with damped momentum, idle zero-gravity drift, scroll/pinch zoom, and press-to-focus — and hands you callbacks. What lives on the six faces is entirely up to you: terminals, articles, images, iframes, canvases. Zero dependencies, no build step, ~9 KB of vanilla ES modules.
Extracted from cmux3d (a rotatable cube of six live terminals) so that project and hergenroeder.me (a portfolio whose homepage is the cube) share one source of truth instead of a fork. Fix a bug here, both get it.
The idea
The controller never touches your content. It only:
- reads pointer/wheel input over a
viewport, - writes
--rx,--ry,--scalecustom properties on arig, - tells you
onFocus(face)/onMove(rotation)/onRelease().
Your CSS turns those custom properties into a transform; your app decides what focusing a face means (open a terminal, flatten into an article, zoom a photo). That single seam is the whole design.
Install
npm install codys-cubeOr, buildless in the browser via a CDN (no bundler, no npm needed at runtime):
import { SpaceController } from 'https://esm.sh/codys-cube';
// or pin: 'https://esm.sh/[email protected]'Or as a git submodule (what hergenroeder.me does, to keep same-origin + buildless):
git submodule add https://github.com/codyhxyz/codys-cube vendor/codys-cube
# then import from '/vendor/codys-cube/src/space.js'Quick start
HTML — a rig of face elements inside a viewport. Each face carries
data-face:
<div class="viewport" id="viewport">
<div class="rig" id="rig">
<section class="panel" data-face="0">…front…</section>
<section class="panel" data-face="1">…right…</section>
<!-- 2 back · 3 left · 4 top · 5 bottom -->
</div>
</div>CSS — the ~20 lines that turn the controller's custom properties into a cube:
.viewport { position: fixed; inset: 0; display: grid; place-items: center;
perspective: 3200px; touch-action: none; }
.rig { position: relative; width: var(--cube, 70vmin); height: var(--cube, 70vmin);
transform: rotateX(var(--rx)) rotateY(var(--ry)) scale(var(--scale, 1));
transform-style: preserve-3d; }
.panel { position: absolute; inset: 0; backface-visibility: hidden; }
.panel[data-face="0"] { transform: translateZ(calc(var(--cube, 70vmin) / 2)); }
.panel[data-face="1"] { transform: rotateY(90deg) translateZ(calc(var(--cube, 70vmin) / 2)); }
.panel[data-face="2"] { transform: rotateY(180deg) translateZ(calc(var(--cube, 70vmin) / 2)); }
.panel[data-face="3"] { transform: rotateY(-90deg) translateZ(calc(var(--cube, 70vmin) / 2)); }
.panel[data-face="4"] { transform: rotateX(90deg) translateZ(calc(var(--cube, 70vmin) / 2)); }
.panel[data-face="5"] { transform: rotateX(-90deg) translateZ(calc(var(--cube, 70vmin) / 2)); }JS:
import { SpaceController } from 'codys-cube';
const space = new SpaceController({
viewport: document.getElementById('viewport'),
rig: document.getElementById('rig'),
// the angle the cube tweens to when each face is focused:
faces: [
{ face: 0, view: { x: -8, y: 0 } },
{ face: 1, view: { x: -8, y: -90 } },
{ face: 2, view: { x: -8, y: 180 } },
{ face: 3, view: { x: -8, y: 90 } },
{ face: 4, view: { x: -96, y: 0 } },
{ face: 5, view: { x: 82, y: 0 } },
],
onFocus: (face) => {/* open / flatten / zoom face */},
onRelease: () => {/* return to the cube */},
onMove: (rotation) => {/* e.g. drive a backdrop */},
});
space.bind();Drag orbits with momentum; releasing throws it. A press without a drag (>4px)
focuses the face under the pointer. Scroll zooms; two-finger pinch zooms on touch.
Call space.focus(n) / space.release() yourself for a nav rail or deep links,
and space.orbitStep(dx, dy) for keyboard arrows.
API
| Member | Purpose |
| --- | --- |
| new SpaceController(options) | see options below |
| .bind() | attach listeners, start the idle-drift loop; returns this |
| .focus(face) | tween to a face's view and fire onFocus |
| .release() | leave focus, reset zoom, resume drift, fire onRelease |
| .orbitStep(dx, dy) | nudge rotation (keyboard), no momentum |
| .setView(face, {x, y}) | (re)register a face's focus angle |
Options: viewport, rig, faces, onFocus, onMove, onRelease,
zoomMin (0.72), zoomMax (1.42), focusZoom (1.08), panelSelector (.panel).
Respects prefers-reduced-motion: no idle drift, no throw-momentum, instant
focus.
Optional backdrop shader
import { startShader } from 'codys-cube/shader';
const bg = startShader(document.getElementById('bg-canvas')); // full-viewport <canvas>
space.onMove = (r) => bg.setOrbit(r);
// bg.setFocus(face) on focus/releaseAn opinionated dark WebGL field (noise mist + receding grid floor + beam + cursor glow) that reacts to orbit and focus. It's a separate export on purpose — great for an instrument/terminal aesthetic, wrong for a light editorial page. Leave it out and you pay nothing for it.
Consumers
- cmux3d — six live terminals on the faces.
- hergenroeder.me — a portfolio; faces flatten into articles on focus.
License
MIT © Cody Hergenroeder
