Skip to content

Instantly share code, notes, and snippets.

@tomatouiui
Created May 2, 2018 14:52
Show Gist options
  • Select an option

  • Save tomatouiui/d5b4d3738c87f8d2056289c8c2fad86e to your computer and use it in GitHub Desktop.

Select an option

Save tomatouiui/d5b4d3738c87f8d2056289c8c2fad86e to your computer and use it in GitHub Desktop.

Revisions

  1. tomatouiui created this gist May 2, 2018.
    16 changes: 16 additions & 0 deletions index.html
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,16 @@
    <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>
    321 changes: 321 additions & 0 deletions script.babel
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,321 @@
    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();
    }
    7 changes: 7 additions & 0 deletions stripe-cubes-animation.markdown
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,7 @@
    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](https://codepen.io/tomatouiui/pen/mLmvov) by [toma](https://codepen.io/tomatouiui) on [CodePen](https://codepen.io).

    [License](https://codepen.io/tomatouiui/pen/mLmvov/license).
    72 changes: 72 additions & 0 deletions style.scss
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,72 @@
    .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);
    }
    }
    }
    }