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 } | |
| } | |
| } | |
| } | |
| } | |
| } |