css 模拟一个动画效果,消息堆叠。

灵感来源:

我的效果:

过程

3D 消息堆叠动画

本项目使用 HTML、CSS 和 JavaScript 实现了一个动态且富有视觉吸引力的 3D 消息通知系统。它创建了一个消息卡片堆栈,卡片在 3D 空间中分层排列,并为添加和重新排列消息提供了平滑的动画效果。

核心功能

  • 3D 透视堆叠: 利用 CSS 的 perspectivetransform 属性(translateZ, scale),消息卡片被排列成一个 3D 堆栈,营造出深度感和层次感。
  • 动态消息序列: 应用程序会自动显示一系列预定义的任务消息,每条新消息都会将旧消息向堆栈后方推动。
  • 静态欢迎消息: 界面加载时会首先显示一个静态的"欢迎"消息,它会立即出现,提供了一个稳定的入口点,避免了任何突兀的初始动画。
  • 任务序号列表: 所有动态生成的任务消息都会自动添加序号(例如,"1. Calling Linear MCP"),使任务流程更加清晰。
  • 流畅且高性能的动画: 卡片的移动效果由 transformopacity 属性上的 CSS transition 控制。这确保了动画的流畅性,并通过 GPU 加速来防止卡顿。
  • 一致的视觉间距: 经过精心计算,堆栈中卡片之间的垂直距离补偿了缩放效果带来的视觉差异,确保了卡片之间保持一致且美观的重叠效果,避免了缝隙的产生。
  • 现代化的 UI 设计: 消息卡片采用了带有微妙渐变和阴影的磨砂玻璃效果 (backdrop-filter: blur()),营造出一种现代而精致的外观。
  • 交互式控制: 用户可以通过按 空格键 来手动触发新消息的出现。

工作原理

  1. 初始化 (initializeApp):

    • 页面加载时,JavaScript 首先创建并显示一个静态的"欢迎"消息。该卡片被直接赋予其最终的 layer-0 样式类,以避免任何进入动画。
    • 随后,一个定时器被启动,用以开始动态消息序列。
  2. 创建新消息 (addNewMessage):

    • 从一个预定义的任务字符串数组中创建一个新消息。
    • 该消息元素被添加到 HTML 中消息堆栈 (messageStack) 的最前面。
  3. 重新排列层级 (rearrangeMessageLayers):

    • 添加新消息后,此函数被调用以更新整个堆栈。
    • 它会遍历所有活动的消息,并根据它们在堆栈中的位置分配一个 CSS 类(layer-0, layer-1 等)。
    • 这些层级类对应于 CSS 中特定的 transform 值,这些值定义了每张卡片在 3D 堆栈中的位置(平移)和大小(缩放)。
    • CSS 的 transition 属性会自动为卡片从旧位置到新位置的变化创建平滑的动画。

文件结构

  • index.html: 包含基础的 HTML 结构,包括消息堆栈的主容器 (<div id="messageStack">)。
  • styles.css: 定义了项目的所有视觉样式,包括 3D 透视效果、卡片样式、磨砂玻璃效果以及堆栈中每个层级的具体 transform 值。
  • script.js: 包含了创建、管理和动画化消息的所有应用程序逻辑。
html 复制代码
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>3D Message Stack</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            background: #252020;
            height: 100vh;
            display: flex;
            justify-content: center;
            align-items: center;
            overflow: hidden;
        }

        .container {
            perspective: 1500px;
            perspective-origin: center center;
        }

        .message-stack {
            position: relative;
            transform-style: preserve-d;
            width: 700px;
            height: 450px;
        }

        .message-card {
            position: absolute;
            width: 100%;
            height: 60px;
            /* background: rgba(184, 134, 11, 0.9); */
            background: linear-gradient(to right,
                    rgba(170, 150, 145, 0.25),
                    rgba(150, 155, 145, 0.3));


            backdrop-filter: blur(15px) saturate(180%);
            -webkit-backdrop-filter: blur(15px) saturate(180%);
            border: 1px solid rgba(255, 255, 255, 0.2);
            border-radius: 16px;
            box-shadow:
                0 25px 50px rgba(0, 0, 0, 0.6),
                0 15px 30px rgba(0, 0, 0, 0.4),
                0 8px 16px rgba(0, 0, 0, 0.3),
                inset 0 1px 0 rgba(255, 255, 255, 0.3);

            display: flex;
            align-items: center;
            padding: 0 24px;
            gap: 16px;

            transition: transform 0.6s cubic-bezier(0.65, 0, 0.35, 1),
                opacity 0.6s cubic-bezier(0.65, 0, 0.35, 1);
            transform-origin: center center;

            /* 初始状态 */
            transform: translateZ(0px) scale(1);
            opacity: 1;
        }

        /* 层级定位 - 关键部分:真正的3D堆叠,新消息覆盖旧消息底部5% */
        .message-card.layer-0 {
            height: 350px;
            align-items: flex-start;
            padding-top: 18px;
            transform: translateZ(0px) translateY(0px) scale(1);
            opacity: 1;
            z-index: 10;
        }

        .message-card.layer-1 {
            transform: translateZ(-40px) translateY(-55px) scale(0.9);
            opacity: 1;
            z-index: 9;
        }

        .message-card.layer-2 {
            transform: translateZ(-80px) translateY(-105px) scale(0.8);
            opacity: 1;
            z-index: 8;
        }

        .message-card.layer-3 {
            transform: translateZ(-120px) translateY(-150px) scale(0.7);
            opacity: 1;
            z-index: 7;
        }

        .message-card.layer-4 {
            transform: translateZ(-160px) translateY(-190px) scale(0.6);
            opacity: 1;
            z-index: 6;
        }

        .status-icon {
            width: 12px;
            height: 12px;
            border-radius: 50%;
            display: flex;
            align-items: center;
            justify-content: center;
            flex-shrink: 0;
            font-size: 12px;
            font-weight: bold;
        }

        .status-icon.loading {
            background: #5ee363;
            animation: pulse 1.5s infinite;
        }

        .status-icon.completed {
            background: transparent;
            color: #52dc57;
            font-size: 16px;
        }

        .status-icon.completed::after {
            content: "✓";
        }

        @keyframes pulse {

            0%,
            100% {
                opacity: 1;
            }

            50% {
                opacity: 0.7;
            }
        }

        .message-text {
            flex: 1;
            font-size: 18px;
            font-weight: 500;
            color: rgba(255, 255, 255, 0.8);
            line-height: 1.3;
        }
    </style>
</head>

<body>
    <div class="container">
        <div class="message-stack" id="messageStack">
            <!-- Messages will be added here -->
        </div>
    </div>
    <script>
        const MESSAGES = [
            "Calling Linear MCP",
            "Checking codebase context...",
            "Checking Warp Drive and rules...",
            "Analyzing project structure",
            "Loading dependencies",
            "Initializing workspace",
            "Validating configuration"
        ].map((text, i) => `${i + 1}. ${text}`);

        let messageCounter = 0;
        let activeMessages = [];
        let currentMessageIndex = 0;
        let animationTimer = null;

        function createMessage(text) {
            return {
                id: `msg-${++messageCounter}`,
                text: text,
                status: 'loading',
                element: null
            };
        }

        function createMessageElement(message) {
            const messageCard = document.createElement('div');
            messageCard.className = 'message-card';
            messageCard.id = message.id;

            const statusIcon = document.createElement('div');
            statusIcon.className = 'status-icon loading';

            const messageText = document.createElement('div');
            messageText.className = 'message-text';
            messageText.textContent = message.text;

            messageCard.appendChild(statusIcon);
            messageCard.appendChild(messageText);

            message.element = messageCard;
            return messageCard;
        }

        function addMessageToStack(message) {
            const messageStack = document.getElementById('messageStack');
            const messageElement = createMessageElement(message);

            messageStack.insertBefore(messageElement, messageStack.firstChild);

            return messageElement;
        }

        function updateMessageStatus(message, status) {
            message.status = status;
            const statusIcon = message.element.querySelector('.status-icon');

            if (status === 'completed') {
                statusIcon.classList.remove('loading');
                statusIcon.classList.add('completed');
            }
        }

        function rearrangeMessageLayers() {
            activeMessages.forEach((message, index) => {
                const element = message.element;

                // 移除所有层级类
                for (let i = 0; i <= 5; i++) {
                    element.classList.remove(`layer-${i}`);
                }
                element.classList.remove('entering');

                // 添加新的层级类
                if (index <= 4) {
                    element.classList.add(`layer-${index}`);
                } else {
                    // 移除超出显示范围的消息
                    element.style.opacity = '0';
                    element.style.transform = 'translateZ(-500px) scale(0.1)';

                    setTimeout(() => {
                        if (element.parentNode) {
                            element.parentNode.removeChild(element);
                        }
                    }, 1000);
                }
            });

            if (activeMessages.length > 5) {
                activeMessages = activeMessages.slice(0, 5);
            }
        }

        function addNewMessage() {
            if (currentMessageIndex >= MESSAGES.length) {
                console.log('所有消息已添加完成');
                return;
            }

            const messageText = MESSAGES[currentMessageIndex];
            const newMessage = createMessage(messageText);

            activeMessages.unshift(newMessage);
            addMessageToStack(newMessage);

            setTimeout(() => {
                rearrangeMessageLayers();
            }, 100);

            setTimeout(() => {
                updateMessageStatus(newMessage, 'completed');
            }, 3000);

            currentMessageIndex++;
        }

        function startMessageSequence() {
            addNewMessage();

            animationTimer = setInterval(() => {
                if (currentMessageIndex < MESSAGES.length) {
                    addNewMessage();
                } else {
                    clearInterval(animationTimer);
                }
            }, 2500);
        }

        function initializeApp() {
            console.log('3D消息堆叠动画初始化');

            // 1. 创建静态欢迎消息
            const welcomeMessage = createMessage('欢迎');
            welcomeMessage.status = 'completed';

            const messageElement = createMessageElement(welcomeMessage);
            updateMessageStatus(welcomeMessage, 'completed');

            // 2. 直接应用最终样式,避免动画
            messageElement.classList.add('layer-0');

            // 3. 添加到DOM和activeMessages
            const messageStack = document.getElementById('messageStack');
            messageStack.appendChild(messageElement);
            activeMessages.push(welcomeMessage);

            // 4. 延迟启动动态消息序列
            setTimeout(() => {
                startMessageSequence();
            }, 1000);
        }

        // 键盘控制
        document.addEventListener('keydown', (event) => {
            if (event.key === ' ') {
                event.preventDefault();
                addNewMessage();
            }
        });

        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', initializeApp);
        } else {
            initializeApp();
        }
    </script>
</body>

</html>
相关推荐
一条上岸小咸鱼几秒前
Kotlin 基本数据类型(四):String
android·前端·kotlin
我是哈哈hh15 分钟前
【Node.js】ECMAScript标准 以及 npm安装
开发语言·前端·javascript·node.js
张元清38 分钟前
电商 Feeds 流缓存策略:Temu vs 拼多多的技术选择
前端·javascript·面试
一枚前端小能手38 分钟前
🎨 CSS布局从入门到放弃?Grid让你重新爱上布局
前端·css
晴空雨39 分钟前
React 合成事件原理:从事件委托到 React 17 的重大改进
前端·react.js
魏嗣宗41 分钟前
Node.js 网络编程全解析:从 Socket 到 HTTP,再到流式协议
前端·全栈
pepedd86442 分钟前
还在开发vue2老项目吗?本文带你梳理vue版本区别
前端·vue.js·trae
pepedd8641 小时前
浅谈js拷贝问题-解决拷贝数据难题
前端·javascript·trae
@大迁世界1 小时前
useCallback 的陷阱:当 React Hooks 反而拖了后腿
前端·javascript·react.js·前端框架·ecmascript
跟橙姐学代码1 小时前
学Python别死记硬背,这份“编程生活化笔记”让你少走三年弯路
前端·python