Last active
October 17, 2017 15:34
-
-
Save DominikAngerer/3bae45d841de9c25d1cfe0b6853bf988 to your computer and use it in GitHub Desktop.
Animated Cubes ES6
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<html> | |
<head> | |
<title></title> | |
<style> | |
body, html { | |
background: #ccc; | |
} | |
#cube-container .cubes { | |
top: 0 | |
} | |
#cube-container .cube { | |
position: absolute; | |
will-change: transform; | |
animation: cube-fade-in 2s cubic-bezier(.165, .84, .44, 1) | |
} | |
#cube-container .cube:first-child { | |
margin-top: -270px | |
} | |
#cube-container .cube:nth-child(2) { | |
z-index: 1; | |
margin-top: -265px | |
} | |
#cube-container .cube:nth-child(3) { | |
z-index: -1; | |
margin: -355px 0 0 50px | |
} | |
#cube-container .cube:nth-child(3)~.cube { | |
display: none | |
} | |
@media (min-width: 670px) { | |
#cube-container .cubes .cube { | |
margin: 0 | |
} | |
#cube-container .cube:nth-child(3) { | |
z-index: 0 | |
} | |
#cube-container .cube:nth-child(3)~.cube { | |
display: block | |
} | |
} | |
@keyframes cube-fade-in { | |
0% { | |
opacity: 0; | |
transform: scale(.5) | |
} | |
} | |
#cube-container .cube div { | |
position: absolute; | |
width: 100%; | |
height: 100% | |
} | |
#cube-container .cube .shadow { | |
top: 40%; | |
background: #07427a | |
} | |
#cube-container .cube .sides { | |
transform-style: preserve-3d; | |
perspective: 600px | |
} | |
#cube-container .cube .sides div { | |
-webkit-backface-visibility: hidden; | |
backface-visibility: hidden; | |
will-change: transform | |
} | |
</style> | |
</head> | |
<body> | |
<header> | |
<div class="stripes"> | |
<span></span> | |
<span></span> | |
<span></span> | |
<span></span> | |
<span></span> | |
</div> | |
<template id="cube-template"> | |
<div class="cube"> | |
<div class="shadow"></div> | |
<div class="sides"> | |
<div class="back"></div> | |
<div class="top"></div> | |
<div class="left"></div> | |
<div class="front"></div> | |
<div class="right"></div> | |
<div class="bottom"></div> | |
</div> | |
</div> | |
</template> | |
<div id="cube-container"> | |
</div> | |
</header> | |
<script type="text/javascript"> | |
const setState = (state, speed) => | |
directions.forEach(axis => { | |
state[axis] += speed[axis]; | |
if (Math.abs(state[axis]) < 360) return; | |
const max = Math.max(state[axis], 360); | |
const min = max == 360 ? Math.abs(state[axis]) : 360; | |
state[axis] = max - min; | |
}); | |
const cubeIsHidden = left => left > parentWidth + 30; | |
// ================= | |
// shared references | |
// ================= | |
let Strut = { | |
random: function(t, o) { | |
return Math.random() * (o - t) + t | |
}, | |
arrayRandom: function(t) { | |
return t[Math.floor(Math.random() * t.length)] | |
}, | |
interpolate: function(t, o, e) { | |
return t * (1 - e) + o * e | |
}, | |
rangePosition: function(t, o, e) { | |
return (e - t) / (o - t) | |
}, | |
clamp: function(t, o, e) { | |
return Math.max(Math.min(t, e), o) | |
}, | |
queryArray: function(t, o) { | |
return o || (o = document.body), | |
Array.prototype.slice.call(o.querySelectorAll(t)) | |
}, | |
ready: function(t) { | |
"loading" !== document.readyState ? t() : document.addEventListener("DOMContentLoaded", t) | |
} | |
} | |
let headerIsHidden = false; | |
const template = document.getElementById("cube-template"); | |
const parent = document.getElementById("cube-container"); | |
const getParentWidth = () => parent.getBoundingClientRect().width; | |
let parentWidth = getParentWidth(); | |
window.addEventListener("resize", () => parentWidth = getParentWidth()); | |
const directions = ["x", "y"]; | |
const palette = { | |
white: { | |
color: [255, 255, 255], | |
shading: [160, 190, 218] | |
}, | |
orange: { | |
color: [255, 250, 230], | |
shading: [255, 120, 50] | |
}, | |
green: { | |
color: [205, 255, 204], | |
shading: [0, 211, 136] | |
} | |
}; | |
// ============== | |
// cube instances | |
// ============== | |
const setCubeStyles = ({cube, size, left, top}) => { | |
Object.assign(cube.style, { | |
width: `${size}px`, | |
height: `${size}px`, | |
left: `${left}px`, | |
top: `${top}px` | |
}); | |
Object.assign(cube.querySelector(".shadow").style, { | |
filter: `blur(${Math.round(size * .6)}px)`, | |
opacity: Math.min(size / 120, .4) | |
}); | |
}; | |
const createCube = size => { | |
const fragment = document.importNode(template.content, true); | |
const cube = fragment.querySelector(".cube"); | |
const state = { | |
x: 0, | |
y: 0 | |
}; | |
const speed = directions.reduce((object, axis) => { | |
const max = size > sizes.m ? .3 : .6; | |
object[axis] = Strut.random(-max, max); | |
return object; | |
}, {}); | |
const sides = Strut.queryArray(".sides div", cube).reduce((object, side) => { | |
object[side.className] = { | |
side, | |
hidden: false, | |
rotate: { | |
x: 0, | |
y: 0 | |
} | |
}; | |
return object; | |
}, {}); | |
sides.top.rotate.x = 90; | |
sides.bottom.rotate.x = -90; | |
sides.left.rotate.y = -90; | |
sides.right.rotate.y = 90; | |
sides.back.rotate.y = -180 | |
return {fragment, cube, state, speed, sides: Object.values(sides)}; | |
}; | |
const sizes = { | |
xs: 15, | |
s: 25, | |
m: 40, | |
l: 100, | |
xl: 120 | |
}; | |
const cubes = [ | |
{ | |
tint: palette.green, | |
size: sizes.xs, | |
left: 35, | |
top: 465 | |
},{ | |
tint: palette.white, | |
size: sizes.s, | |
left: 55, | |
top: 415 | |
},{ | |
tint: palette.white, | |
size: sizes.xl, | |
left: 140, | |
top: 400 | |
},{ | |
tint: palette.white, | |
size: sizes.m, | |
left: 420, | |
top: 155 | |
},{ | |
tint: palette.green, | |
size: sizes.xs, | |
left: 440, | |
top: 280 | |
},{ | |
tint: palette.orange, | |
size: sizes.s, | |
left: 480, | |
top: 228 | |
},{ | |
tint: palette.white, | |
size: sizes.l, | |
left: 580, | |
top: 255 | |
},{ | |
tint: palette.green, | |
size: sizes.s, | |
left: 780, | |
top: 320 | |
},{ | |
tint: palette.white, | |
size: sizes.xl, | |
left: 780, | |
top: 120 | |
},{ | |
tint: palette.orange, | |
size: sizes.l, | |
left: 900, | |
top: 310 | |
},{ | |
tint: palette.green, | |
size: sizes.m, | |
left: 1030, | |
top: 200 | |
} | |
].map(object => Object.assign(createCube(object.size), object)); | |
cubes.forEach(setCubeStyles); | |
// ======================= | |
// cube rotating animation | |
// ======================= | |
const getDistance = (state, rotate) => | |
directions.reduce((object, axis) => { | |
object[axis] = Math.abs(state[axis] + rotate[axis]); | |
return object; | |
}, {}); | |
const getRotation = (state, size, rotate) => { | |
const axis = rotate.x ? "Z" : "Y"; | |
const direction = rotate.x > 0 ? -1 : 1; | |
return ` | |
rotateX(${state.x + rotate.x}deg) | |
rotate${axis}(${direction * (state.y + rotate.y)}deg) | |
translateZ(${size / 2}px) | |
`; | |
}; | |
const getShading = (tint, rotate, distance) => { | |
const darken = directions.reduce((object, axis) => { | |
const delta = distance[axis]; | |
const ratio = delta / 180; | |
object[axis] = delta > 180 ? Math.abs(2 - ratio) : ratio; | |
return object; | |
}, {}); | |
if (rotate.x) | |
darken.y = 0; | |
else { | |
const {x} = distance; | |
if (x > 90 && x < 270) | |
directions.forEach(axis => darken[axis] = 1 - darken[axis]); | |
} | |
const alpha = (darken.x + darken.y) / 2; | |
const blend = (value, index) => Math.round(Strut.interpolate(value, tint.shading[index], alpha)); | |
const [r, g, b] = tint.color.map(blend); | |
return `rgb(${r}, ${g}, ${b})`; | |
}; | |
const shouldHide = (rotateX, x, y) => { | |
if (rotateX) | |
return x > 90 && x < 270; | |
if (x < 90) | |
return y > 90 && y < 270; | |
if (x < 270) | |
return y < 90; | |
return y > 90 && y < 270; | |
}; | |
const updateSides = ({state, speed, size, tint, sides, left}) => { | |
if (headerIsHidden || cubeIsHidden(left)) return; | |
const animate = object => { | |
const {side, rotate, hidden} = object; | |
const distance = getDistance(state, rotate); | |
// don't animate hidden sides | |
if (shouldHide(rotate.x, distance.x, distance.y)) { | |
if (!hidden) { | |
side.hidden = true; | |
object.hidden = true; | |
} | |
return; | |
} | |
if (hidden) { | |
side.hidden = false; | |
object.hidden = false; | |
} | |
side.style.transform = getRotation(state, size, rotate); | |
side.style.backgroundColor = getShading(tint, rotate, distance); | |
}; | |
setState(state, speed); | |
sides.forEach(animate); | |
}; | |
const reduceMotion = matchMedia("(prefers-reduced-motion)").matches; | |
const tick = () => { | |
cubes.forEach(updateSides); | |
if (reduceMotion) return; | |
requestAnimationFrame(tick); | |
}; | |
const container = document.createElement("div"); | |
container.className = "cubes"; | |
cubes.forEach(({fragment}) => container.appendChild(fragment)); | |
const start = () => { | |
tick(); | |
parent.appendChild(container); | |
}; | |
Strut.ready(() => { | |
start() | |
}); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment