灵感来源:

我的效果:

过程
3D 消息堆叠动画
本项目使用 HTML、CSS 和 JavaScript 实现了一个动态且富有视觉吸引力的 3D 消息通知系统。它创建了一个消息卡片堆栈,卡片在 3D 空间中分层排列,并为添加和重新排列消息提供了平滑的动画效果。
核心功能
- 3D 透视堆叠: 利用 CSS 的
perspective
和transform
属性(translateZ
,scale
),消息卡片被排列成一个 3D 堆栈,营造出深度感和层次感。 - 动态消息序列: 应用程序会自动显示一系列预定义的任务消息,每条新消息都会将旧消息向堆栈后方推动。
- 静态欢迎消息: 界面加载时会首先显示一个静态的"欢迎"消息,它会立即出现,提供了一个稳定的入口点,避免了任何突兀的初始动画。
- 任务序号列表: 所有动态生成的任务消息都会自动添加序号(例如,"1. Calling Linear MCP"),使任务流程更加清晰。
- 流畅且高性能的动画: 卡片的移动效果由
transform
和opacity
属性上的 CSStransition
控制。这确保了动画的流畅性,并通过 GPU 加速来防止卡顿。 - 一致的视觉间距: 经过精心计算,堆栈中卡片之间的垂直距离补偿了缩放效果带来的视觉差异,确保了卡片之间保持一致且美观的重叠效果,避免了缝隙的产生。
- 现代化的 UI 设计: 消息卡片采用了带有微妙渐变和阴影的磨砂玻璃效果 (
backdrop-filter: blur()
),营造出一种现代而精致的外观。 - 交互式控制: 用户可以通过按
空格键
来手动触发新消息的出现。
工作原理
-
初始化 (
initializeApp
):- 页面加载时,JavaScript 首先创建并显示一个静态的"欢迎"消息。该卡片被直接赋予其最终的
layer-0
样式类,以避免任何进入动画。 - 随后,一个定时器被启动,用以开始动态消息序列。
- 页面加载时,JavaScript 首先创建并显示一个静态的"欢迎"消息。该卡片被直接赋予其最终的
-
创建新消息 (
addNewMessage
):- 从一个预定义的任务字符串数组中创建一个新消息。
- 该消息元素被添加到 HTML 中消息堆栈 (
messageStack
) 的最前面。
-
重新排列层级 (
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>