The best animatable JS + CSS Rubik's cube on Codepen.
A Pen by Romain Coeur on CodePen.
- var faces = ['left', 'right', 'top', 'bottom', 'back', 'front'], pieces = 56 // n^3 - (n-2)^3 | |
#scene.scene | |
#pivot.pivot.centered(style = 'transform: rotateX(-35deg) rotateY(-135deg);') | |
#cube.cube | |
while pieces-- | |
.piece | |
each face in faces | |
div(class = 'element ' + face) | |
.credits | |
.text(style = 'position: initial') Wanna make solver, but I'm too lazy to translate 100 lines of my Cpp's solver to JS... | |
#guide | |
- var n = 4 | |
while n-- | |
div(id = 'anchor' + n class = 'anchor' style = 'transform: translateZ(3px) translateY(-33.33%) rotate(' + (-90 * n) + 'deg) translateY(66.67%)') |
The best animatable JS + CSS Rubik's cube on Codepen.
A Pen by Romain Coeur on CodePen.
var colors = ['blue', 'green', 'black', 'yellow', 'orange', 'red'], | |
pieces = document.getElementsByClassName('piece'); | |
// console.log('pieces', pieces) | |
// Returns j-th adjacent face of i-th face | |
function mx(i, j) { | |
return ([2, 4, 3, 5][j % 4 |0] + i % 2 * ((j|0) % 4 * 2 + 3) + 2 * (i / 2 |0)) % 6; | |
} | |
function getAxis(face) { | |
return String.fromCharCode('X'.charCodeAt(0) + face / 2); // X, Y or Z | |
} | |
// Moves each of 26 pieces to their places, assigns IDs and attaches stickers | |
function assembleCube() { | |
function moveto(face) { | |
id = id + (1 << face); | |
pieces[i].children[face].appendChild(document.createElement('div')) | |
.setAttribute('class', 'sticker ' + colors[face]); | |
return 'translate' + getAxis(face) + '(' + (face % 2 * 4 - 2) + 'em)'; | |
} | |
for (var id, x, i = 0; id = 0, i < 26; i++) { | |
x = mx(i, i % 18); | |
pieces[i].style.transform = 'rotateX(0deg)' + moveto(i % 6) + | |
(i > 5 ? moveto(x) + (i > 17 ? moveto(mx(x, x + 2)) : '') : ''); | |
pieces[i].setAttribute('id', 'piece' + id); | |
} | |
} | |
function getPieceBy(face, index, corner) { | |
return document.getElementById('piece' + | |
((1 << face) + (1 << mx(face, index)) + (1 << mx(face, index + 1)) * corner)); | |
} | |
// Swaps stickers of the face (by clockwise) stated times, thereby rotates the face | |
function swapPieces(face, times) { | |
for (var i = 0; i < 6 * times; i++) { | |
var piece1 = getPieceBy(face, i / 2, i % 2), | |
piece2 = getPieceBy(face, i / 2 + 1, i % 2); | |
for (var j = 0; j < 5; j++) { | |
var sticker1 = piece1.children[j < 4 ? mx(face, j) : face].firstChild, | |
sticker2 = piece2.children[j < 4 ? mx(face, j + 1) : face].firstChild, | |
className = sticker1 ? sticker1.className : ''; | |
if (className) | |
sticker1.className = sticker2.className, | |
sticker2.className = className; | |
} | |
} | |
} | |
// Animates rotation of the face (by clockwise if cw), and then swaps stickers | |
function animateRotation(face, cw, currentTime) { | |
var k = .3 * (face % 2 * 2 - 1) * (2 * cw - 1), | |
qubes = Array(9).fill(pieces[face]).map(function (value, index) { | |
return index ? getPieceBy(face, index / 2, index % 2) : value; | |
}); | |
(function rotatePieces() { | |
var passed = Date.now() - currentTime, | |
style = 'rotate' + getAxis(face) + '(' + k * passed * (passed < 300) + 'deg)'; | |
qubes.forEach(function (piece) { | |
piece.style.transform = piece.style.transform.replace(/rotate.\(\S+\)/, style); | |
}); | |
if (passed >= 300) | |
return swapPieces(face, 3 - 2 * cw); | |
requestAnimationFrame(rotatePieces); | |
})(); | |
} | |
// Events | |
function mousedown(md_e) { | |
var startXY = pivot.style.transform.match(/-?\d+\.?\d*/g).map(Number), | |
element = md_e.target.closest('.element'), | |
face = [].indexOf.call((element || cube).parentNode.children, element); | |
function mousemove(mm_e) { | |
if (element) { | |
var gid = /\d/.exec(document.elementFromPoint(mm_e.pageX, mm_e.pageY).id); | |
if (gid && gid.input.includes('anchor')) { | |
mouseup(); | |
var e = element.parentNode.children[mx(face, Number(gid) + 3)].hasChildNodes(); | |
animateRotation(mx(face, Number(gid) + 1 + 2 * e), e, Date.now()); | |
} | |
} else pivot.style.transform = | |
'rotateX(' + (startXY[0] - (mm_e.pageY - md_e.pageY) / 2) + 'deg)' + | |
'rotateY(' + (startXY[1] + (mm_e.pageX - md_e.pageX) / 2) + 'deg)'; | |
} | |
function mouseup() { | |
document.body.appendChild(guide); | |
scene.removeEventListener('mousemove', mousemove); | |
document.removeEventListener('mouseup', mouseup); | |
scene.addEventListener('mousedown', mousedown); | |
} | |
(element || document.body).appendChild(guide); | |
scene.addEventListener('mousemove', mousemove); | |
document.addEventListener('mouseup', mouseup); | |
scene.removeEventListener('mousedown', mousedown); | |
} | |
document.ondragstart = function() { return false; } | |
window.addEventListener('load', assembleCube); | |
scene.addEventListener('mousedown', mousedown); |
$base-color: #0A0A0A; | |
$ground-color: #2F2F31; | |
$element-size: 2em; | |
$sticker-size: 95%; | |
$rounded: 10%; | |
$cube-scale: 1.9; | |
$faces: (left: (0, -90, 180), right: (0, 90, 90), back: (0, 180, -90), front: (0, 0, 0), bottom: (-90, 0, -90), top: (90, 0, 180)); | |
$colors: (blue: #001ca8, green: #006E16, white: #DDD, yellow: #E0AE00, orange: #FF5000, red: #DF0500); | |
html, body { | |
height: 100%; | |
overflow: hidden; | |
background: radial-gradient(circle, white, rgba(black, .5)) { | |
color: $ground-color; | |
blend-mode: overlay; } | |
} | |
.credits { | |
width: 100%; | |
top: 90%; | |
} | |
.text { | |
text-align: center; | |
font: { family: Helvetica; size: .8rem; } | |
color: grey; | |
pointer-events: none; | |
} | |
.centered { | |
position: absolute; | |
top: 0; bottom: 0; left: 0; right: 0; | |
margin: auto; | |
} | |
.scene { | |
width: 100%; | |
height: 100%; | |
perspective: 1200px; | |
transform-style: preserve-3d; | |
> .pivot { | |
width: 0; | |
height: 0; | |
transition: .18s; | |
} | |
.anchor { | |
width: $element-size; | |
height: $element-size * 3; | |
} | |
div { | |
position: absolute; | |
transform-style: inherit; | |
} | |
} | |
#piece4 > .element.top > .sticker { | |
background-image: URL('http://i63.tinypic.com/25hh1xu.png'); | |
background-size: cover; | |
} | |
.cube { | |
font-size: $cube-scale * 100%; | |
margin-left: -$element-size / 2; | |
margin-top: -$element-size / 2; | |
> .piece { | |
width: $element-size - .1em; | |
height: $element-size - .1em; | |
> .element { | |
width: 100%; | |
height: 100%; | |
background: $base-color; | |
outline: 1px solid transparent; // firefox aliasing | |
border: .05em solid $base-color { radius: $rounded } | |
@each $face, $angles in $faces { | |
&.#{$face} { | |
transform: rotateX + '(' + nth($angles, 1) + 'deg)' | |
rotateY + '(' + nth($angles, 2) + 'deg)' | |
rotateZ + '(' + nth($angles, 3) + 'deg)' translateZ($element-size / 2); | |
} | |
} | |
> .sticker { | |
@extend .centered; | |
transform: translateZ(2px); | |
width: $sticker-size; | |
height: $sticker-size; | |
border-radius: $rounded; | |
outline: 1px solid transparent; // firefox aliasing | |
box-shadow: inset .05em .05em .2rem 0 rgba(white, .25), | |
inset -.05em -.05em .2rem 0 rgba(black, .25); | |
@each $color, $value in $colors { | |
&.#{$color} { background-color: $value } | |
} | |
} | |
} | |
} | |
} |