Skip to content

Instantly share code, notes, and snippets.

@tomatouiui
Created May 2, 2018 14:52
Show Gist options
  • Save tomatouiui/d5b4d3738c87f8d2056289c8c2fad86e to your computer and use it in GitHub Desktop.
Save tomatouiui/d5b4d3738c87f8d2056289c8c2fad86e to your computer and use it in GitHub Desktop.
Stripe Cubes Animation
<main>
<header><div id="header-hero"></div></header>
<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>
</main>
let Strut = {
random: function (e, t) {
return Math.random() * (t - e) + e;
},
arrayRandom: function (e) {
return e[Math.floor(Math.random() * e.length)];
},
interpolate: function (e, t, n) {
return e * (1 - n) + t * n;
},
rangePosition: function (e, t, n) {
return (n - e) / (t - e);
},
clamp: function (e, t, n) {
return Math.max(Math.min(e, n), t);
},
queryArray: function (e, t) {
return t || (t = document.body), Array.prototype.slice.call(t.querySelectorAll(e));
},
ready: function (e) {
document.readyState == 'complete' ? e() : document.addEventListener('DOMContentLoaded', e);
}
};
const reduceMotion = matchMedia("(prefers-reduced-motion)").matches;
{
// =======
// helpers
// =======
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 headerIsHidden = false;
const template = document.getElementById("cube-template");
const parent = document.getElementById("header-hero");
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 tick = () => {
cubes.forEach(updateSides);
if (reduceMotion) return;
requestAnimationFrame(tick);
};
// ===============
// parallax scroll
// ===============
// give it some extra space to account for the parallax and the shadows of the cubes
const parallaxLimit = document.querySelector("main > header").getBoundingClientRect().height + 80;
window.addEventListener("scroll", () => {
const scroll = window.scrollY;
if (scroll < parallaxLimit) {
headerIsHidden = false;
cubes.forEach(({cube, speed}) =>
cube.style.transform = `translateY(${Math.abs(speed.x * .5) * scroll}px)`);
return;
}
headerIsHidden = true;
});
// ==========
// initialize
// ==========
const container = document.createElement("div");
container.className = "cubes";
cubes.forEach(({fragment}) => container.appendChild(fragment));
const start = () => {
tick();
parent.appendChild(container);
};
'requestIdleCallback' in window ? requestIdleCallback(start) : start();
}

Stripe Cubes Animation

Recreated the https://stripe.com/connect animation for learning purposes. Just trying to learn how it was done so I can create smooth and gorgeous pages that balance between markup, css and javascript .

A Pen by toma on CodePen.

License.

.cubes
{
.cube
{
position: absolute;
height: 100px;
width: 100px;
margin: 0;
animation: cube-fade-in 2s cubic-bezier(.165, .84, .44, 1);
will-change: transform;
@keyframes cube-fade-in
{
0%
{
opacity: 0;
transform: scale(.5)
}
}
*
{
position: absolute;
height: 100%;
width: 100%;
}
.shadow
{
background: #07427a;
top: 40%;
}
.sides
{
transform-style: preserve-3d;
perspective: 600px;
div
{
backface-visibility: hidden;
will-change: transform;
}
.front
{
transform: rotateY(0deg) translateZ(50px);
}
.back
{
transform: rotateY(-180deg) translateZ(50px);
}
.left
{
transform: rotateY(-90deg) translateZ(50px);
}
.right
{
transform: rotateY(90deg) translateZ(50px);
}
.top
{
transform: rotateX(90deg) translateZ(50px);
}
.bottom
{
transform: rotateX(-90deg) translateZ(50px);
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment