Vissza az animációkhoz
Háttéranimációk

Aranykockák – Térbeli összeállás ✨

Élő előnézet – az animáció teljes méretben.

Élő előnézet
HTML · CSS · JS
HTML
<div class="we-cubes">
    <svg class="we-cubes-lines"
         viewBox="0 0 1440 900"
         preserveAspectRatio="xMidYMid slice"
         aria-hidden="true">
        <path d="M-100,250 C180,70 300,420 550,300 S900,70 1120,240 S1390,430 1540,180"/>
        <path d="M-100,650 C160,490 350,790 600,630 S950,430 1160,620 S1400,760 1540,550"/>
        <path d="M160,-100 C350,140 150,340 360,490 S650,730 510,1000"/>
    </svg>

    <div class="we-cubes-stage"></div>
</div>
CSS
.we-cubes {
    --we-cube-size: 42px;
    --we-cube-gold: #c9a96e;

    position: relative;
    width: 100%;
    min-height: 460px;
    overflow: hidden;
    isolation: isolate;
    perspective: 1200px;
}

.we-cubes-lines {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    pointer-events: none;
    z-index: 0;
}

.we-cubes-lines path {
    fill: none;
    stroke: var(--we-cube-gold);
    stroke-width: .65;
    stroke-dasharray: 1800;
    stroke-dashoffset: 1800;
    opacity: .5;
    animation: weCubesLineFlow 12s ease-in-out infinite alternate;
}

.we-cubes-lines path:nth-child(2) {
    animation-delay: -4s;
    opacity: .32;
}

.we-cubes-lines path:nth-child(3) {
    animation-delay: -7s;
    opacity: .22;
}

@keyframes weCubesLineFlow {
    0% {
        stroke-dashoffset: 1800;
        opacity: .12;
    }
    50% {
        opacity: .5;
    }
    100% {
        stroke-dashoffset: 0;
        opacity: .25;
    }
}

.we-cubes-stage {
    position: absolute;
    left: 50%;
    top: 50%;
    width: 1px;
    height: 1px;
    z-index: 1;
    transform-style: preserve-3d;
    transform: translate(-50%, -50%) rotateX(-13deg) rotateY(-24deg);
    animation: weCubesDrift 8s ease-in-out infinite;
}

@keyframes weCubesDrift {
    0%, 100% {
        margin-top: 0;
        rotate: 0deg;
    }
    50% {
        margin-top: -7px;
        rotate: 1deg;
    }
}

.we-cubes-piece {
    position: absolute;
    left: 0;
    top: 0;
    width: var(--we-cube-size);
    height: var(--we-cube-size);
    transform-style: preserve-3d;
    opacity: 0;
    will-change: transform, opacity;
}

.we-cubes-face {
    position: absolute;
    inset: 0;
    border: 1px solid rgba(224, 190, 123, .76);
    background: linear-gradient(
        145deg,
        rgba(201, 169, 110, .18),
        rgba(23, 31, 51, .48) 58%,
        rgba(201, 169, 110, .09)
    );
    box-shadow:
        inset 0 0 12px rgba(201, 169, 110, .045),
        0 0 7px rgba(201, 169, 110, .035);
}

.we-cubes-face.front {
    transform: translateZ(calc(var(--we-cube-size) / 2));
}
.we-cubes-face.back {
    transform: rotateY(180deg) translateZ(calc(var(--we-cube-size) / 2));
}
.we-cubes-face.right {
    transform: rotateY(90deg) translateZ(calc(var(--we-cube-size) / 2));
}
.we-cubes-face.left {
    transform: rotateY(-90deg) translateZ(calc(var(--we-cube-size) / 2));
}
.we-cubes-face.top {
    transform: rotateX(90deg) translateZ(calc(var(--we-cube-size) / 2));
}
.we-cubes-face.bottom {
    transform: rotateX(-90deg) translateZ(calc(var(--we-cube-size) / 2));
}

@media (max-width: 600px) {
    .we-cubes {
        --we-cube-size: 30px;
        min-height: 380px;
    }

    .we-cubes-stage {
        transform: translate(-50%, -50%) scale(.78) rotateX(-13deg) rotateY(-24deg);
    }
}
JavaScript
(() => {
    document.querySelectorAll(".we-cubes").forEach(root => {
        if (root.dataset.initialized === "true") return;
        root.dataset.initialized = "true";

        const stage = root.querySelector(".we-cubes-stage");

        const ASSEMBLE_TIME = 3800;
        const HOLD_TIME = 2400;
        const DISASSEMBLE_TIME = 3100;
        const SCATTER_HOLD = 1000;

        let cubes = [];
        let frameId = 0;
        let timerId = 0;
        let destroyed = false;

        const rand = (min, max) =>
            Math.random() * (max - min) + min;

        const ease = t =>
            t < 0.5
                ? 4 * t * t * t
                : 1 - Math.pow(-2 * t + 2, 3) / 2;

        function inside(x, y, z, cx, cy, cz, rx, ry, rz) {
            return (
                ((x - cx) / rx) ** 2 +
                ((y - cy) / ry) ** 2 +
                ((z - cz) / rz) ** 2 <= 1
            );
        }

        function makeShape() {
            const points = [];

            for (let y = -3; y <= 3; y++) {
                for (let z = -2; z <= 2; z++) {
                    for (let x = -5; x <= 5; x++) {
                        const included =
                            inside(x, y, z, 0, 0, 0, 4.8, 2.65, 1.8) ||
                            inside(x, y, z, 2.2, 1.8, 0, 2.5, 1.6, 1.4) ||
                            inside(x, y, z, -2.7, -1.8, 0, 2.1, 1.5, 1.3) ||
                            inside(x, y, z, -4.2, .1, .1, 1.5, 1.25, 1.25);

                        if (!included) continue;

                        const hash = Math.abs(
                            (x * 73856093) ^
                            (y * 19349663) ^
                            (z * 83492791)
                        ) % 10;

                        const core = Math.abs(x) <= 1 && Math.abs(y) <= 1;

                        if (!core && hash < 4) continue;

                        points.push({ x, y, z });
                    }
                }
            }

            return points;
        }

        function createCube(point) {
            const element = document.createElement("div");
            element.className = "we-cubes-piece";

            ["front", "back", "right", "left", "top", "bottom"]
                .forEach(name => {
                    const face = document.createElement("div");
                    face.className = `we-cubes-face ${name}`;
                    element.appendChild(face);
                });

            stage.appendChild(element);

            const angle = rand(0, Math.PI * 2);
            const radius = rand(100, 280);
            const size = parseFloat(
                getComputedStyle(root).getPropertyValue("--we-cube-size")
            ) || 42;

            return {
                element,
                target: {
                    x: point.x * size * .91,
                    y: -point.y * size * .91,
                    z: point.z * size * .91
                },
                scatter: {
                    x: Math.cos(angle) * radius + rand(-80, 80),
                    y: Math.sin(angle) * radius * .72 + rand(-70, 70),
                    z: rand(-150, 150),
                    rx: rand(-180, 180),
                    ry: rand(-180, 180),
                    rz: rand(-180, 180)
                },
                delay: rand(0, 420)
            };
        }

        function position(c, x, y, z, rx, ry, rz, opacity) {
            c.element.style.opacity = opacity;
            c.element.style.transform =
                `translate3d(${x}px, ${y}px, ${z}px) ` +
                `rotateX(${rx}deg) rotateY(${ry}deg) rotateZ(${rz}deg)`;
        }

        function assemble(start) {
            cancelAnimationFrame(frameId);

            function tick(now) {
                if (destroyed) return;

                const elapsed = now - start;
                let done = true;

                cubes.forEach(c => {
                    const progress = Math.min(
                        1,
                        Math.max(0, elapsed - c.delay) / ASSEMBLE_TIME
                    );
                    const t = ease(progress);

                    if (progress < 1) done = false;

                    const a = c.scatter;
                    const b = c.target;

                    position(
                        c,
                        a.x * (1 - t) + b.x * t,
                        a.y * (1 - t) + b.y * t,
                        a.z * (1 - t) + b.z * t,
                        a.rx * (1 - t),
                        a.ry * (1 - t),
                        a.rz * (1 - t),
                        Math.min(1, progress * 2.5)
                    );
                });

                if (!done) {
                    frameId = requestAnimationFrame(tick);
                } else {
                    timerId = setTimeout(
                        () => disassemble(performance.now()),
                        HOLD_TIME
                    );
                }
            }

            frameId = requestAnimationFrame(tick);
        }

        function disassemble(start) {
            cancelAnimationFrame(frameId);

            function tick(now) {
                if (destroyed) return;

                const elapsed = now - start;
                let done = true;

                cubes.forEach(c => {
                    const progress = Math.min(
                        1,
                        Math.max(0, elapsed - c.delay * .45) /
                            DISASSEMBLE_TIME
                    );
                    const t = ease(progress);

                    if (progress < 1) done = false;

                    const a = c.target;
                    const b = c.scatter;

                    position(
                        c,
                        a.x * (1 - t) + b.x * t,
                        a.y * (1 - t) + b.y * t,
                        a.z * (1 - t) + b.z * t,
                        b.rx * t,
                        b.ry * t,
                        b.rz * t,
                        1 - Math.max(0, (progress - .72) / .28)
                    );
                });

                if (!done) {
                    frameId = requestAnimationFrame(tick);
                } else {
                    timerId = setTimeout(
                        () => assemble(performance.now()),
                        SCATTER_HOLD
                    );
                }
            }

            frameId = requestAnimationFrame(tick);
        }

        function start() {
            cancelAnimationFrame(frameId);
            clearTimeout(timerId);
            stage.replaceChildren();

            cubes = makeShape().map(createCube);
            assemble(performance.now());
        }

        start();

        if ("ResizeObserver" in window) {
            let lastWidth = root.clientWidth;

            const observer = new ResizeObserver(() => {
                if (root.clientWidth === lastWidth) return;
                lastWidth = root.clientWidth;
                start();
            });

            observer.observe(root);
        }
    });
})();