Skip to content

Instantly share code, notes, and snippets.

@jdmichaud
Last active October 26, 2018 15:25
Show Gist options
  • Save jdmichaud/e5575453debb48980c9ef92e934d7f0e to your computer and use it in GitHub Desktop.
Save jdmichaud/e5575453debb48980c9ef92e934d7f0e to your computer and use it in GitHub Desktop.
CSS 3d rotation of a cube
<!DOCTYPE html>
<html>
<title>Cube</title>
<!-- Trackball is used to test the rotation with the mouse, but you can rotate -->
<!-- the cube from the command line this way: -->
<!-- > rotateCube('cube1', [1, -1, 0], -Math.PI / 5); -->
<script src="trackball.js"></script>
<style>
* { box-sizing: border-box; }
body {
font-family: sans-serif;
padding: 100px;
background-color: black;
}
/* The scene contains the cube */
/* Comment perspective for an orthographic projection */
.scene {
width: 200px;
height: 200px;
margin: 0px;
padding: 0px;
/*perspective: 400px;*/
}
/* The cube will contains the face. It will be rotated using the trandform property */
.cube {
width: 100%;
height: 100%;
position: relative;
transform-style: preserve-3d;
}
/* Each face is a simple div */
.cube__face {
position: absolute;
width: 100%;
height: 100%;
border: 2px solid black;
outline: 1px solid white;
text-align: center;
user-select: none;
-moz-user-select: none;
}
.cube__face--front { background: rgba(0, 0, 0, 100%); }
.cube__face--right { background: rgba(0, 0, 0, 100%); }
.cube__face--back { background: rgba(0, 0, 0, 100%); }
.cube__face--left { background: rgba(0, 0, 0, 100%); }
.cube__face--top { background: rgba(0, 0, 0, 100%); }
.cube__face--bottom { background: rgba(0, 0, 0, 100%); }
/* Apply a constant rotation to each div to make a cube */
/* The Z axis is relative to the div so translateZ always works whatever the div orientation */
.cube__face--front { transform: rotateY( 0deg) translateZ(100px); }
.cube__face--right { transform: rotateY( 90deg) translateZ(100px); }
.cube__face--back { transform: rotateY(180deg) translateZ(100px); }
.cube__face--left { transform: rotateY(-90deg) translateZ(100px); }
.cube__face--top { transform: rotateX( 90deg) translateZ(100px); }
.cube__face--bottom { transform: rotateX(-90deg) translateZ(100px); }
.cube__face span {
display: block;
position: relative;
top: 50%;
transform: translateY(-50%);
font-size: 90px;
font-weight: bold;
color: white;
text-shadow: -1px 0 black, 0 1px black, 1px 0 black, 0 -1px black;
}
</style>
<script>
// Apply the CSS 3D rotation from the axis and the angle in radian
function rotateCube(id, axis, angle) {
const transform = `rotate3d(${axis[0]}, ${axis[1]}, ${axis[2]}, ${angle * 180 / Math.PI}deg)`;
document.getElementById(id).style.transform = transform;
}
window.onload = () => track('scene1', 'cube1');
</script>
<!-- Here are the set of divs that represents the cube -->
<div id="scene1" class="scene">
<div id="cube1" class="cube">
<div class="cube__face cube__face--front"><span>A</span></div>
<div class="cube__face cube__face--back"><span>P</span></div>
<div class="cube__face cube__face--right"><span>R</span></div>
<div class="cube__face cube__face--left"><span>L</span></div>
<div class="cube__face cube__face--top"><span>S</span></div>
<div class="cube__face cube__face--bottom"><span>I</span></div>
</div>
</div>
</html>
function loadScript(path) {
var script = document.createElement('script');
script.type = 'text/javascript';
script.setAttribute('async', false);
script.src = path;
document.head.appendChild(script);
}
function normalize(v) {
return math.divide(v, math.norm(v));
}
function computeTrackball(ballCenter, radius, point) {
const radius_squared = radius * radius;
const vector_from_center = sub(point.slice(0, 2), ballCenter.slice(0, 2));
const norm_squared = dot(vector_from_center, vector_from_center);
let z = 0;
if (norm_squared < radius_squared) {
// On the sphere
z = Math.sqrt(radius_squared - norm_squared);
}
// if point outside of sphere, the point is on the plane z = 0
return [point[0], point[1], z];
}
// Returns the rotation axis and its angle from two vectors
function computeRotation(center, previous, current) {
previous = sub(previous, center);
current = sub(current, center);
const axis = cross(current, previous);
const angle = Math.acos(dot(normalize(current), normalize(previous)));
return { axis, angle };
}
function projectPointOnTrackball(center, radius, previous, current) {
// Compute the current position on the trackball we are point it to
const previousSphere = computeTrackball(center, radius, [...previous, 0]);
const currentSphere = computeTrackball(center, radius, [...current, 0]);
return [previousSphere, currentSphere];
}
// Find the basis of plane when given a normal unit vector of that plane
// and an origin
function findPlaneBasis(origin, normal) {
// First, find two points on the plane for which the axis is the normal vector
let v1, v2;
if (normal[1] === 0 && normal[2] === 0) {
if (normal[0] < 0) {
v1 = [0, 1, 0];
v2 = [0, 0, 1];
} else {
v1 = [0, 0, 1];
v2 = [0, 1, 0];
}
} else if (normal[0] === 0 && normal[2] === 0) {
if (normal[1] < 0) {
v1 = [0, 0, 1];
v2 = [1, 0, 0];
} else {
v1 = [1, 0, 0];
v2 = [0, 0, 1];
}
} else if (normal[0] === 0 && normal[1] === 0) {
if (normal[2] < 0) {
v1 = [1, 0, 0];
v2 = [0, 1, 0];
} else {
v1 = [0, 1, 0];
v2 = [1, 0, 0];
}
} else {
v1 = normal[2] === 0 ? [1, 0, 0] :
[1, 0, (normal[0] * origin[0] + normal[1] * origin[1] + normal[2] * origin[2] -
normal[0] * (origin[0] + 1) + normal[1] * origin[1]) / normal[2]];
v1 = divide(v1, norm(v1));
v2 = cross(v1, normal);
}
return [v1, v2];
}
function rotate(center, axis, angle, vertices) {
axis = normalize(axis);
// Create a new basis, with rotation axis as the z axis
const planeBasis = transpose(findPlaneBasis(center, axis));
const newBasis = [
[...planeBasis[0], axis[0], center[0]],
[...planeBasis[1], axis[1], center[1]],
[...planeBasis[2], axis[2], center[2]],
[ 0, 0, 0, 1]
];
// Create the rotation matrix
const rotMat = [
[Math.cos(angle), -Math.sin(angle), 0, 0],
[Math.sin(angle), Math.cos(angle), 0, 0],
[ 0, 0, 1, 0],
[ 0, 0, 0, 1],
];
const transformMatrix = mul(newBasis, mul(rotMat, inv(newBasis)));
// Rotate !
return vertices.map(vertice => {
return mul(transformMatrix, [...vertice, 1]).slice(0, 3);
});
}
// Retrieve the axis and angle from a rotation matrix M
function getParametersFromMatrix(M) {
// see https://en.wikipedia.org/wiki/Rotation_matrix#Determining_the_axis
const axis = normalize([
M[2][1] - M[1][2],
M[0][2] - M[2][0],
M[1][0] - M[0][1],
]);
// see https://en.wikipedia.org/wiki/Rotation_matrix#Determining_the_angle
const angle = Math.acos((M[0][0] + M[1][1] + M[2][2] - 1) / 2);
return { axis, angle };
}
const cache = {};
function getOffsetCoordinates(target, event) {
const boundingRect = cache[target.id] !== undefined ? cache[target] : target.getBoundingClientRect();
return [event.clientX - boundingRect.left, event.clientY - boundingRect.top];
}
// The camera which is only use as an accumulator to the small rotations performed.
let camera = [
[1, 0, 0],
[0, 1, 0],
[0, 0, 1],
]
loadScript('https://cdnjs.cloudflare.com/ajax/libs/mathjs/5.1.0/math.js');
function track(scene, cube) {
window.sub = math.subtract;
window.dot = math.dot;
window.mul = math.multiply;
window.divide = math.divide;
window.cross = math.cross;
window.transpose = math.transpose;
window.norm = math.norm;
window.inv = math.inv;
const scene1 = document.getElementById(scene);
const center = [scene1.offsetWidth / 2, scene1.offsetHeight / 2, 0];
const radius = scene1.offsetWidth;
// Core function. Takes previous and current, compute the rotation and apply it.
window.drag = function drag(previous, current) {
const increment =
computeRotation(center, ...projectPointOnTrackball(center, radius, previous, current));
camera = rotate([0, 0, 0], increment.axis, increment.angle, camera);
const { axis, angle } = getParametersFromMatrix(transpose(camera));
rotateCube(cube, axis, angle);
}
scene1.addEventListener('mousedown', downEvent => {
let previous = getOffsetCoordinates(scene1, downEvent);
const mousemoveHandler = moveEvent => {
const current = getOffsetCoordinates(scene1, moveEvent);
if (JSON.stringify(previous) === JSON.stringify(current)) return;
drag(previous, current);
previous = current;
};
document.addEventListener('mousemove', mousemoveHandler);
// Stop listening to mousemove on mouseup
document.addEventListener('mouseup', _ => document.removeEventListener('mousemove', mousemoveHandler));
});
}
// Rotate 90deg to the right (facing SL)
// Rotate 90deg down (facing AT) like:
// #
// ###
// #
// #
// #
// # #
// #
// #
// drag([100, 100], [200, 100])
// drag([100, 100], [100, 200])
// This should give the following rotation:
// rotateCube('cube1', [-0.5, 0.5, -0.5], 2.10)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment