├── README.md ├── colorize.js ├── colors.svg ├── cubes.js ├── index.css ├── index.html ├── og-image.png ├── perspective.js └── rotation.js /README.md: -------------------------------------------------------------------------------- 1 | 3D CSS cubes 2 | ===== 3 | 4 | A very simple interactive grid in 3d space. Click to drag and rotate the view, add cubes of different colors, and play around. 5 | 6 | Nothing to set up -- just open up `index.html` and you're set! 7 | 8 | 3D effects are pure CSS. 9 | 10 | ---- 11 | -------------------------------------------------------------------------------- /colorize.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christinecha/3d-css-cubes/5a896fdfff7ca5225199bbdac8124254c46ee075/colorize.js -------------------------------------------------------------------------------- /colors.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /cubes.js: -------------------------------------------------------------------------------- 1 | var vw = window.innerWidth 2 | 3 | var $plane = document.getElementById('plane') 4 | var $grid = document.getElementById('grid') 5 | var $cubes = document.getElementById('cubes') 6 | var $cubeModel = document.querySelector('.cube') 7 | var $colorize = document.getElementById('colorize') 8 | var $colorPreview = document.getElementById('color-preview') 9 | 10 | var color = '#ffaa22' 11 | var widthInSpaces = 10 12 | var heightInSpaces = 10 13 | var spaceSize = $plane.clientWidth / widthInSpaces 14 | var totalSpaces = widthInSpaces * heightInSpaces 15 | 16 | /** METHODS **/ 17 | function addSpace(index) { 18 | var $space = document.createElement('div') 19 | $space.classList.add('space') 20 | 21 | var x = index === 0 ? 0 : (index % widthInSpaces) * spaceSize 22 | var y = index === 0 ? 0 : Math.floor(index / widthInSpaces) * spaceSize 23 | 24 | $space.style.left = x + 'px' 25 | $space.style.top = y + 'px' 26 | 27 | $grid.appendChild($space) 28 | 29 | $space.addEventListener('mouseenter', function(e) { e.target.style.backgroundColor = color }) 30 | $space.addEventListener('mouseleave', function(e) { e.target.style.backgroundColor = 'transparent' }) 31 | $space.addEventListener('click', addCube) 32 | } 33 | 34 | function addCube(e) { 35 | e.preventDefault() 36 | var target = e.target 37 | var layer = 0 38 | var cubeTranslation = 'translate3d(0, 0, 0)' 39 | 40 | if (e.target.classList.contains('face')) { 41 | target = e.target.parentNode 42 | layer = parseInt(target.getAttribute('data-layer')) + 1 43 | cubeTranslation = `translate3d(0, 0, ${layer * spaceSize}px)` 44 | } 45 | 46 | var $cube = $cubeModel.cloneNode(true) 47 | var $face1 = $cube.querySelector('.face-1') 48 | 49 | $cube.classList.remove('is--hidden') 50 | $cube.setAttribute('data-layer', layer) 51 | $cube.style.left = target.style.left 52 | $cube.style.top = target.style.top 53 | $cube.style.transform = cubeTranslation 54 | 55 | $face1.addEventListener('click', addCube) 56 | 57 | colorizeCube($cube) 58 | 59 | $cubes.appendChild($cube) 60 | } 61 | 62 | function colorizeCube($cube) { 63 | var $faces = Array.from($cube.querySelectorAll('.face')) 64 | $faces.forEach(function($face, i) { 65 | $face.style.backgroundColor = getColorNeighbor(color, i) 66 | }) 67 | } 68 | 69 | function getRandomColor() { 70 | var chars = '0123456789abcdef'.split('') 71 | var hex = '#' 72 | 73 | for (var n = 0; n < 6; n++) { 74 | var rand = Math.round(Math.random() * (chars.length - 1)) 75 | hex += chars[rand] 76 | } 77 | 78 | return hex 79 | } 80 | 81 | function getColorNeighbor(hex, n) { 82 | var chars = '0123456789abcdef'.split('') 83 | var hexArr = hex.replace('#', '').split('') 84 | 85 | var big = (n % 3) * 2 86 | var small = (n % 2) === 0 ? big + 1 : big - 1 87 | if (small === -1) small = 5 88 | 89 | var neighborBig = hexArr[big] === 'f' ? 'e' : chars[chars.indexOf(hexArr[big]) + 1] 90 | var neighborSmall = hexArr[big] === 'f' ? 'e' : chars[chars.indexOf(hexArr[big]) + 1] 91 | 92 | hexArr[big] = neighborBig 93 | hexArr[small] = neighborSmall 94 | 95 | return '#' + hexArr.join('') 96 | } 97 | 98 | function isMobileAt(vw) { 99 | return vw < 500 100 | } 101 | 102 | /** EVENT LISTENERS **/ 103 | window.addEventListener('resize', function() { 104 | if (window.innerWidth === vw) return 105 | if (isMobileAt(vw) === isMobileAt(window.innerWidth)) return 106 | 107 | vw = window.innerWidth 108 | var conversion = isMobileAt(vw) ? 3 / 5 : 5 / 3 109 | 110 | var $cubes = Array.from(document.querySelectorAll('.cube')) 111 | var $spaces = Array.from(document.querySelectorAll('.space')) 112 | var $nodes = $cubes.concat($spaces) 113 | 114 | $nodes.forEach(function($node) { 115 | $node.style.left = parseInt($node.style.left) * conversion + 'px' 116 | $node.style.top = parseInt($node.style.top) * conversion + 'px' 117 | }) 118 | }) 119 | 120 | $colorize.addEventListener('click', function() { 121 | color = getRandomColor() 122 | $colorPreview.style.backgroundColor = color 123 | }) 124 | 125 | 126 | /** RUN **/ 127 | var spaceIndex = 0 128 | 129 | while (spaceIndex < totalSpaces) { 130 | addSpace(spaceIndex) 131 | spaceIndex ++ 132 | } 133 | -------------------------------------------------------------------------------- /index.css: -------------------------------------------------------------------------------- 1 | /** RESET **/ 2 | * { 3 | margin: 0 0; 4 | padding: 0 0; 5 | font-size: 0; 6 | font-family: 'Arial', sans-serif; 7 | user-select: none; 8 | -webkit-user-select: none; 9 | -moz-user-select: none; 10 | border: none; 11 | outline: none; 12 | } 13 | 14 | /** SHARED **/ 15 | .spacer { 16 | height: 100%; 17 | width: 0; 18 | vertical-align: middle; 19 | display: inline-block; 20 | } 21 | 22 | .is--hidden { 23 | visibility: hidden; 24 | } 25 | 26 | /** TOOLBAR **/ 27 | #toolbar { 28 | position: fixed; 29 | left: 20px; 30 | top: 20px; 31 | z-index: 99; 32 | } 33 | 34 | #toolbar h3 { 35 | font-size: 9px; 36 | letter-spacing: 0.08em; 37 | margin-bottom: 15px; 38 | } 39 | 40 | #toolbar h5 { 41 | font-size: 8px; 42 | letter-spacing: 0.08em; 43 | margin-bottom: 5px; 44 | color: #aaa; 45 | } 46 | 47 | #colorize { 48 | width: 20px; 49 | height: 20px; 50 | background-image: url('./colors.svg'); 51 | background-size: cover; 52 | cursor: pointer; 53 | opacity: 0.6; 54 | display: inline-block; 55 | } 56 | 57 | #colorize:hover { 58 | opacity: 1; 59 | } 60 | 61 | #color-preview { 62 | margin: 0 10px; 63 | display: inline-block; 64 | width: 20px; 65 | height: 20px; 66 | background-color: #ffaa22; 67 | } 68 | 69 | #perspective { 70 | width: 150px; 71 | margin-bottom: 20px; 72 | position: relative; 73 | cursor: ew-resize; 74 | } 75 | 76 | #perspective .slider { 77 | height: 5px; 78 | width: 100%; 79 | background-color: #eee; 80 | } 81 | 82 | #perspective .knob { 83 | border-radius: 100%; 84 | height: 10px; 85 | width: 10px; 86 | background-color: #ccc; 87 | position: absolute; 88 | top: -2.5px; 89 | } 90 | 91 | .github { 92 | position: fixed; 93 | top: 5px; 94 | right: 5px; 95 | width: 20px; 96 | cursor: pointer; 97 | z-index: 99; 98 | } 99 | 100 | .github:hover path { 101 | fill: #00aeef; 102 | } 103 | 104 | 105 | /** PLANE **/ 106 | #plane-wrapper { 107 | position: fixed; 108 | min-width: 100%; 109 | height: 100vh; 110 | perspective-origin: 50% 50%; 111 | perspective: 600px; 112 | cursor: move; 113 | text-align: center; 114 | overflow: hidden; 115 | } 116 | 117 | #plane { 118 | display: inline-block; 119 | background-color: #eee; 120 | width: 500px; 121 | height: 500px; 122 | margin: 0 auto; 123 | vertical-align: middle; 124 | transform-style: preserve-3d; 125 | transform-origin: 50% 50%; 126 | } 127 | 128 | /** GRID **/ 129 | #grid { 130 | position: absolute; 131 | } 132 | 133 | .space { 134 | position: absolute; 135 | width: 50px; 136 | height: 50px; 137 | box-sizing: border-box; 138 | border: 1px solid #ddd; 139 | cursor: pointer; 140 | } 141 | 142 | /** CUBE **/ 143 | .cube { 144 | position: relative; 145 | transform-style: preserve-3d; 146 | } 147 | 148 | .face { 149 | position: absolute; 150 | width: 50px; 151 | height: 50px; 152 | background-color: #ffaa22; 153 | transform-origin: 50% 50%; 154 | opacity: 0.5; 155 | z-index: 2; 156 | } 157 | 158 | .face-1 { 159 | background-color: #ffaa11; 160 | transform: translate3d(0, 0, 50px); 161 | } 162 | 163 | .face-1:hover { 164 | opacity: 1; 165 | } 166 | 167 | .face-2 { 168 | background-color: #ffaa33; 169 | transform-origin: 0% 50%; 170 | transform: rotateX(90deg) translate3d(0, 25px, 25px); 171 | } 172 | 173 | .face-3 { 174 | background-color: #ffbb22; 175 | transform: rotateX(90deg) translate3d(0, 25px, -25px); 176 | } 177 | 178 | .face-4 { 179 | background-color: #ff9922; 180 | transform-origin: 50% 0%; 181 | transform: rotateY(90deg) translate3d(-25px, 0px, -25px); 182 | } 183 | 184 | .face-5 { 185 | background-color: #eeaa22; 186 | transform-origin: 50% 0%; 187 | transform: rotateY(90deg) translate3d(-25px, 0px, 25px); 188 | } 189 | 190 | 191 | /** MEDIA QUERIES **/ 192 | @media (max-width: 500px) { 193 | #plane { 194 | width: 300px; 195 | height: 300px; 196 | } 197 | 198 | .space, .face { width: 30px; height: 30px; } 199 | .face-1 { transform: translate3d(0, 0, 30px); } 200 | .face-2 { transform: rotateX(90deg) translate3d(0, 15px, 15px); } 201 | .face-3 { transform: rotateX(90deg) translate3d(0, 15px, -15px); } 202 | .face-4 { transform: rotateY(90deg) translate3d(-15px, 0px, -15px); } 203 | .face-5 { transform: rotateY(90deg) translate3d(-15px, 0px, 15px); } 204 | } 205 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Interactive CSS Cubes 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |

CLICK + DRAG TO ROTATE

20 |
21 |
ADJUST PERSPECTIVE
22 |
23 |
24 |
25 |
26 |
CHANGE COLOR
27 | 28 |
29 |
30 | 31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | 40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | 49 | 50 | 51 | 52 | 53 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /og-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christinecha/3d-css-cubes/5a896fdfff7ca5225199bbdac8124254c46ee075/og-image.png -------------------------------------------------------------------------------- /perspective.js: -------------------------------------------------------------------------------- 1 | var $planeWrapper = document.getElementById('plane-wrapper') 2 | var $perspective = document.getElementById('perspective') 3 | var $slider = $perspective.querySelector('.slider') 4 | var $knob = $perspective.querySelector('.knob') 5 | 6 | var perspective = 600 7 | var minPerspective = 200 8 | var maxPerspective = 1200 9 | var lastX = 0 10 | var newX = 0 11 | 12 | function handleMouseDown(e) { 13 | console.log('moo') 14 | document.addEventListener('mousemove', handleMouseMove) 15 | document.addEventListener('touchmove', handleMouseMove) 16 | } 17 | 18 | function handleMouseUp(e) { 19 | document.removeEventListener('mousemove', handleMouseMove) 20 | document.removeEventListener('touchmove', handleMouseMove) 21 | } 22 | 23 | function handleMouseMove(e) { 24 | e.preventDefault() 25 | 26 | // when the browser is ready, draw the next frame 27 | window.requestAnimationFrame( function(){ 28 | var sliderRect = $slider.getBoundingClientRect() 29 | newX = e.pageX - sliderRect.left 30 | 31 | if (newX > sliderRect.width) newX = sliderRect.width 32 | if (newX < 0) newX = 0 33 | 34 | $knob.style.left = newX + 'px' 35 | 36 | perspective = (newX / sliderRect.width) * (maxPerspective - minPerspective) + 200 37 | $planeWrapper.style.perspective = perspective + 'px' 38 | }) 39 | } 40 | 41 | $knob.addEventListener('mousedown', handleMouseDown) 42 | $knob.addEventListener('touchstart', handleMouseDown) 43 | 44 | document.addEventListener('mouseup', handleMouseUp) 45 | document.addEventListener('touchend', handleMouseUp) 46 | 47 | $knob.style.left = (perspective - 200) / (maxPerspective - minPerspective) * $slider.getBoundingClientRect().width + 'px' 48 | -------------------------------------------------------------------------------- /rotation.js: -------------------------------------------------------------------------------- 1 | var vw = window.innerWidth 2 | 3 | var isDragging = false 4 | var dragStart = {} 5 | var dragEnd = {} 6 | 7 | var xRotation = 60 8 | var yRotation = 0 9 | var zRotation = 45 10 | 11 | var $plane = document.getElementById('plane') 12 | var $perspectiveKnob = document.querySelector('#perspective .knob') 13 | 14 | /** METHODS **/ 15 | function setDragStart(e) { 16 | dragStart = { x: e.pageX, y: e.pageY } 17 | } 18 | 19 | function calculateRotations(e) { 20 | dragEnd = { x: e.pageX, y: e.pageY } 21 | 22 | var dragDiffX = dragEnd.x - dragStart.x 23 | var dragDiffY = dragEnd.y - dragStart.y 24 | 25 | xRotation -= dragDiffY / 100 26 | zRotation -= dragDiffX / 100 27 | 28 | if (xRotation < 0) xRotation = 0 29 | if (xRotation > 90) xRotation = 89 30 | } 31 | 32 | function applyRotations(node, x, y, z) { 33 | var rotationString = `rotateX(${x}deg) rotateY(${y}deg) rotateZ(${z}deg)` 34 | node.style.transform = rotationString 35 | } 36 | 37 | function startDrag(e) { 38 | if (e.path && e.path.indexOf($perspectiveKnob) !== -1) return 39 | 40 | var targetTouches = e.targetTouches ? Array.prototype.slice.apply(e.targetTouches) : null 41 | if (targetTouches && targetTouches.indexOf($perspectiveKnob) !== -1) return 42 | 43 | // setup move listeners 44 | document.addEventListener('mousemove', drag) 45 | document.addEventListener('touchmove', drag) 46 | 47 | setDragStart(e) 48 | } 49 | 50 | function endDrag() { 51 | 52 | // tear down move listeners 53 | document.removeEventListener('mousemove', drag) 54 | document.removeEventListener('touchmove', drag) 55 | } 56 | 57 | function drag(e) { 58 | e.preventDefault() 59 | calculateRotations(e) 60 | 61 | // when the browser is ready, apply the new positioning 62 | window.requestAnimationFrame( function(){ 63 | applyRotations($plane, xRotation, yRotation, zRotation) 64 | }) 65 | } 66 | 67 | /** EVENT LISTENERS **/ 68 | document.addEventListener('mousedown', startDrag) 69 | document.addEventListener('touchstart', startDrag) 70 | 71 | document.addEventListener('mouseup', endDrag) 72 | document.addEventListener('touchend', endDrag) 73 | 74 | /** RUN **/ 75 | applyRotations($plane, xRotation, yRotation, zRotation) 76 | --------------------------------------------------------------------------------