Rotating cube
Pure CSSSix faces placed with rotate + translateZ, spun by a single keyframe animation.
Drag to spin it, release to let it coast. JS only tracks two angles and some inertia.
Your edited version
rotateX(var(--rx)) rotateY(var(--ry)).setPointerCapture keeps the drag alive when the pointer leaves the element; touch-action: none stops the page scrolling instead.Pointer Events + setPointerCapturerequestAnimationFrame inertiatouch-action: none<div class="scene">
<div class="cube">
<div></div><div></div><div></div>
<div></div><div></div><div></div>
</div>
</div>
.scene {
display: grid;
place-items: center;
width: 100vw;
height: 100vh;
perspective: 800px;
cursor: grab;
touch-action: none; /* let JS have the drag on touch screens */
user-select: none;
}
.scene:active { cursor: grabbing; }
.cube {
--s: 140px;
position: relative;
width: var(--s);
height: var(--s);
transform-style: preserve-3d;
transform: rotateX(var(--rx, -24deg)) rotateY(var(--ry, 32deg));
}
.cube > * {
position: absolute;
inset: 0;
background: rgb(255 181 71 / 0.3);
border: 1px solid rgb(255 181 71 / 0.85);
}
/* turn each face outward, then push it half a side from the centre */
.cube > :nth-child(1) { transform: rotateY(0deg) translateZ(calc(var(--s) / 2)); }
.cube > :nth-child(2) { transform: rotateY(90deg) translateZ(calc(var(--s) / 2)); }
.cube > :nth-child(3) { transform: rotateY(180deg) translateZ(calc(var(--s) / 2)); }
.cube > :nth-child(4) { transform: rotateY(-90deg) translateZ(calc(var(--s) / 2)); }
.cube > :nth-child(5) { transform: rotateX(90deg) translateZ(calc(var(--s) / 2)); }
.cube > :nth-child(6) { transform: rotateX(-90deg) translateZ(calc(var(--s) / 2)); }
const scene = document.querySelector('.scene');
const cube = document.querySelector('.cube');
let rx = -24, ry = 32; // current angles
let vx = 0, vy = 0; // velocity, for inertia
let dragging = false;
let raf = 0;
function apply() {
rx = Math.max(-89, Math.min(89, rx));
cube.style.setProperty('--rx', rx + 'deg');
cube.style.setProperty('--ry', ry + 'deg');
}
function coast() {
if (dragging) return;
vx *= 0.95;
vy *= 0.95;
rx += vx;
ry += vy;
apply();
if (Math.abs(vx) + Math.abs(vy) > 0.05) raf = requestAnimationFrame(coast);
}
scene.addEventListener('pointerdown', (e) => {
dragging = true;
cancelAnimationFrame(raf);
scene.setPointerCapture(e.pointerId);
});
scene.addEventListener('pointermove', (e) => {
if (!dragging) return;
vy = e.movementX * 0.6;
vx = -e.movementY * 0.6;
rx += vx;
ry += vy;
apply();
});
function release() {
if (!dragging) return;
dragging = false;
raf = requestAnimationFrame(coast);
}
scene.addEventListener('pointerup', release);
scene.addEventListener('pointercancel', release);
@use '../mixins' as *;
.d-drag {
display: grid;
place-items: center;
width: 100%;
height: 100%;
transform-style: preserve-3d;
cursor: grab;
user-select: none;
&:active {
cursor: grabbing;
}
&__cube {
--s: 100px;
position: relative;
width: var(--s);
height: var(--s);
transform-style: preserve-3d;
// The only thing JS touches: two angles.
transform: rotateX(var(--rx, -24deg)) rotateY(var(--ry, 32deg));
@include cube-faces(var(--s));
i {
position: absolute;
inset: 0;
@include face(var(--warm), 30%);
background-image:
linear-gradient(rgb(255 255 255 / 0.25) 1px, transparent 1px),
linear-gradient(90deg, rgb(255 255 255 / 0.25) 1px, transparent 1px);
background-size: 25% 25%;
}
}
small {
position: absolute;
bottom: 10px;
color: var(--muted);
font-size: 12px;
}
}