<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>丝滑滚动 & 防抖动演示</title>
<style>
:root {
--bg-color: #f3f4f6;
--chat-bg: #ffffff;
--user-msg-bg: #3b82f6;
--ai-msg-bg: #e5e7eb;
--scroll-btn-bg: rgba(0, 0, 0, 0.6);
}
body,
html {
margin: 0;
padding: 0;
height: 100%;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: var(--bg-color);
}
#app {
display: flex;
flex-direction: column;
height: 100vh;
max-width: 800px;
margin: 0 auto;
background: var(--chat-bg);
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
position: relative;
}
header {
height: 60px;
border-bottom: 1px solid #eee;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(5px);
z-index: 10;
position: absolute;
top: 0;
width: 100%;
box-sizing: border-box;
}
#chat-container {
flex: 1;
overflow-y: auto;
padding: 80px 20px 20px 20px;
scroll-behavior: auto;
/* 禁止 CSS 平滑,完全由 JS 接管以保证跟手性 */
}
.message {
margin-bottom: 20px;
display: flex;
flex-direction: column;
opacity: 0;
animation: fadeIn 0.3s forwards;
}
@keyframes fadeIn {
to {
opacity: 1;
transform: translateY(0);
}
}
.message-content {
max-width: 80%;
padding: 12px 16px;
border-radius: 12px;
line-height: 1.6;
font-size: 15px;
position: relative;
word-wrap: break-word;
}
.user {
align-items: flex-end;
}
.user .message-content {
background: var(--user-msg-bg);
color: white;
border-bottom-right-radius: 2px;
}
.ai {
align-items: flex-start;
}
.ai .message-content {
background: var(--ai-msg-bg);
color: #333;
border-bottom-left-radius: 2px;
}
.cursor::after {
content: '▋';
display: inline-block;
animation: blink 1s infinite;
color: #333;
margin-left: 2px;
}
@keyframes blink {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0;
}
}
footer {
padding: 20px;
border-top: 1px solid #eee;
background: white;
display: flex;
gap: 10px;
}
button#btn-generate {
padding: 10px 20px;
background: #10a37f;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: bold;
width: 100%;
}
button#btn-generate:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.float-btn {
position: absolute;
right: 30px;
width: 40px;
height: 40px;
border-radius: 50%;
background: var(--scroll-btn-bg);
color: white;
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
opacity: 0;
visibility: hidden;
transform: translateY(10px);
z-index: 20;
font-size: 20px;
}
.float-btn.visible {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.float-btn:hover {
background: black;
}
#btn-scroll-top {
bottom: 140px;
}
#btn-scroll-bottom {
bottom: 90px;
}
</style>
</head>
<body>
<div id="app">
<header>
<h3>Chat Interface (No Jitter)</h3>
<span style="font-size: 12px; color: #666;">防抖动版 Demo</span>
</header>
<div id="chat-container">
<div class="message ai">
<div class="message-content">你好!我是模拟 AI。现在你可以放心地在生成过程中尝试向上滚动,页面不会再出现“拉扯”或“抖动”现象了。</div>
</div>
</div>
<button id="btn-scroll-top" class="float-btn" title="回到顶部">↑</button>
<button id="btn-scroll-bottom" class="float-btn" title="回到底部">↓</button>
<footer>
<button id="btn-generate">开始模拟长文本生成 (Stream)</button>
</footer>
</div>
<script>
const container = document.getElementById('chat-container');
const btnGenerate = document.getElementById('btn-generate');
const btnScrollTop = document.getElementById('btn-scroll-top');
const btnScrollBottom = document.getElementById('btn-scroll-bottom');
let isAutoScrolling = true; // 自动吸底开关
let isGenerating = false; // 生成状态
let lastScrollTop = 0; // 记录上次滚动位置,用于判断方向
// ==========================================
// 1. 核心滚动监听 (已修复抖动问题)
// ==========================================
container.addEventListener('scroll', () => {
const currentScrollTop = container.scrollTop;
const scrollHeight = container.scrollHeight;
const clientHeight = container.clientHeight;
// 计算距离底部的距离
const distanceToBottom = scrollHeight - currentScrollTop - clientHeight;
// 判断滚动方向:当前位置 < 上次位置 = 向上滚
// 注意处理 safari 橡皮筋效果导致的负值
const isScrollingUp = currentScrollTop < lastScrollTop && currentScrollTop >= 0;
// 更新记录
lastScrollTop = currentScrollTop <= 0 ? 0 : currentScrollTop;
// --- 核心防抖逻辑 ---
// 1. 只要检测到用户明确向上滚动,哪怕此时距离底部只有 1px,也立即停止吸底。
// 这解决了“代码想往下拉,手想往上推”造成的视觉抖动。
if (isScrollingUp) {
isAutoScrolling = false;
}
// 2. 只有当用户真切地回到了最底部 (误差容忍度 5px),才恢复吸底。
if (distanceToBottom <= 5) {
isAutoScrolling = true;
btnScrollBottom.classList.remove('visible'); // 在底部时隐藏向下按钮
} else {
// 如果不在底部
btnScrollBottom.classList.add('visible');
}
// --- 控制顶部按钮 ---
if (currentScrollTop > 300) {
btnScrollTop.classList.add('visible');
} else {
btnScrollTop.classList.remove('visible');
}
});
// 执行吸底动作 (无动画,瞬时完成,避免生成时的延迟感)
function scrollToBottom() {
container.scrollTop = container.scrollHeight;
}
// 监听 DOM 变化 (AI 生成文字时触发)
const observer = new MutationObserver(() => {
// 只有在开关开启时才执行滚动
if (isAutoScrolling) {
scrollToBottom();
}
});
observer.observe(container, {
childList: true,
subtree: true,
characterData: true
});
// ==========================================
// 2. 丝滑滚动动画 (easeOutCubic)
// ==========================================
function smoothScrollTo(targetPosition, duration = 500) {
const startPosition = container.scrollTop;
const distance = targetPosition - startPosition;
let startTime = null;
function animation(currentTime) {
if (startTime === null) startTime = currentTime;
const timeElapsed = currentTime - startTime;
const run = easeOutCubic(timeElapsed, startPosition, distance, duration);
container.scrollTop = run;
if (timeElapsed < duration) {
requestAnimationFrame(animation);
}
}
// t=time, b=start, c=change, d=duration
function easeOutCubic(t, b, c, d) {
t /= d;
t--;
return c * (t * t * t + 1) + b;
}
requestAnimationFrame(animation);
}
// 回到顶部
btnScrollTop.addEventListener('click', () => {
smoothScrollTo(0, 800);
isAutoScrolling = false; // 用户要去顶部,肯定不想被拉回底部
});
// 回到底部
btnScrollBottom.addEventListener('click', () => {
const target = container.scrollHeight - container.clientHeight;
smoothScrollTo(target, 600);
// 动画完成后,强制恢复吸底状态
setTimeout(() => {
isAutoScrolling = true;
btnScrollBottom.classList.remove('visible');
}, 650);
});
// ==========================================
// 3. 模拟数据生成
// ==========================================
const longText = `这里是一段用于测试的长文本。你可以点击生成,然后在生成的过程中尝试用鼠标滚轮或者手指向上滑动页面。
你会发现:
1. 当你什么都不做时,页面会自动跟随文字滚动到底部。
2. 当你向上滚动一点点时,页面会立即停止跟随,停留在你想看的位置,完全没有抖动。
3. 当你手动滚回底部,或者点击右下角的向下箭头后,自动跟随又会恢复。
下面开始填充一些无意义的文字来增加长度...
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
技术原理总结:
核心在于 scroll 事件中对 isScrollingUp 的判断。旧逻辑是只要在底部附近就强行吸底,新逻辑是只要检测到上滑意图就强行断开吸底。
Ending...`;
btnGenerate.addEventListener('click', async () => {
if (isGenerating) return;
isGenerating = true;
btnGenerate.disabled = true;
btnGenerate.innerText = "生成中...";
// 添加用户消息
addMessage('user', '开始测试防抖动滚动效果。');
// 开始生成前,先强制吸底一次
isAutoScrolling = true;
scrollToBottom();
// 模拟请求延迟
await new Promise(r => setTimeout(r, 500));
// 创建 AI 消息框
const aiMsgDiv = document.createElement('div');
aiMsgDiv.className = 'message ai';
const contentDiv = document.createElement('div');
contentDiv.className = 'message-content cursor';
aiMsgDiv.appendChild(contentDiv);
container.appendChild(aiMsgDiv);
// 打字机逻辑
let index = 0;
const speed = 30;
function typeWriter() {
if (index < longText.length) {
// 每次增加一段随机长度的字符
const chunk = longText.slice(index, index + Math.floor(Math.random() * 5) + 1);
contentDiv.innerText += chunk;
index += chunk.length;
setTimeout(typeWriter, speed);
} else {
isGenerating = false;
contentDiv.classList.remove('cursor');
btnGenerate.disabled = false;
btnGenerate.innerText = "开始模拟长文本生成 (Stream)";
}
}
typeWriter();
});
function addMessage(role, text) {
const div = document.createElement('div');
div.className = `message ${role}`;
div.innerHTML = `<div class="message-content">${text}</div>`;
container.appendChild(div);
}
</script>
<!-- 1. 引入CDN -->
<!-- <script src="https://cdn.jsdelivr.net/particles.js/2.0.0/particles.min.js"></script> -->
<!-- 2. 替换原有particles容器 -->
<!-- <section class="hero" id="hero">
// 替换原<div class="particles" id="particles"></div>
<div id="particles-js"></div>
<div class="hero-content">
</div>
</section> -->
<!-- 3. 配置粒子交互(替换原有粒子生成JS) -->
<!-- <script>
particlesJS('particles-js', {
"particles": {
"number": {
"value": 80, // 粒子数量
"density": { "enable": true, "value_area": 800 }
},
"color": { "value": ["#00f3ff", "#ff00c8", "#9d00ff"] }, // 霓虹色
"shape": { "type": "circle" },
"size": { "value": 3, "random": true },
"opacity": { "value": 0.8, "random": true },
"line_linked": {
"enable": true, // 粒子间连线
"distance": 150, // 连线距离阈值
"color": "#00f3ff", // 连线颜色
"opacity": 0.2,
"width": 1
},
"move": {
"enable": true,
"speed": 2,
"direction": "none",
"random": true,
"straight": false,
"out_mode": "out",
"attract": {
"enable": true, // 开启鼠标吸附
"rotateX": 600,
"rotateY": 1200
}
}
},
"interactivity": {
"detect_on": "canvas",
"events": {
"onhover": {
"enable": true, // 鼠标悬浮触发
"mode": "grab" // 吸附模式(grab=粒子连线吸附,repulse=粒子排斥,bubble=粒子放大)
},
"onclick": {
"enable": true,
"mode": "push" // 点击生成新粒子
},
"resize": true
},
"modes": {
"grab": { "distance": 140, "line_linked": { "opacity": 0.5 } },
"repulse": { "distance": 200 },
"bubble": { "size": 8 }
}
},
"retina_detect": true // 适配视网膜屏
});
</script> -->
<!-- <div id="tsparticles"></div>
<script src="https://cdn.jsdelivr.net/npm/tsparticles@2.11.1/tsparticles.bundle.min.js"></script>
<script>
tsParticles.load("tsparticles", {
fpsLimit: 60,
background: {
color: "#050505", // 深色背景,接近黑色但不死黑
},
interactivity: {
events: {
onClick: {
enable: true,
mode: "push", // 点击添加粒子
},
onHover: {
enable: true,
mode: "grab", // 悬停产生抓取连线效果
},
resize: true,
},
modes: {
push: {
quantity: 4, // 点击产生粒子的数量
},
grab: {
distance: 200, // 抓取连线的距离
links: {
opacity: 1, // 抓取时连线变亮
color: "#00d2ff" // 抓取时的连线颜色(青色高亮)
}
},
},
},
particles: {
// 颜色配置:青色、深蓝、紫色混合
color: {
value: ["#00d2ff", "#3a86ff", "#8338ec", "#ff006e"],
},
links: {
color: "#ffffff",
distance: 150,
enable: true,
opacity: 0.2, // 默认连线透明度低一点,制造景深感
width: 1,
triangles: {
enable: true, // 开启三角形填充
color: "#ffffff", // 填充颜色
opacity: 0.05 // 极淡的填充,营造科技网格感
}
},
collisions: {
enable: true,
},
move: {
direction: "none",
enable: true,
outModes: {
default: "bounce", // 碰到边界反弹
},
random: false,
speed: 2, // 移动速度
straight: false,
attract: {
enable: false,
rotateX: 600,
rotateY: 1200
}
},
number: {
density: {
enable: true,
area: 800,
},
value: 80, // 粒子总数
},
opacity: {
value: 0.5,
random: true, // 随机透明度,产生闪烁感
anim: {
enable: true,
speed: 1,
opacity_min: 0.1,
sync: false
}
},
shape: {
type: ["circle", "triangle"], // 混合形状:圆形和三角形
},
size: {
value: { min: 1, max: 5 },
random: true,
anim: {
enable: true,
speed: 4,
size_min: 0.1,
sync: false
}
},
},
detectRetina: true,
});
</script>
</script> -->
<!-- 1. 引入CDN -->
<script src="https://cdn.jsdelivr.net/npm/write-js@2.0.0/dist/write.min.js"></script>
<!-- 2. HTML结构 -->
<p class="hero-subtitle">We build <span class="typed-text" id="write"></span></p>
<!-- 3. 配置Write.js -->
<script>
const texts = [
'Intelligent Systems',
'Secure Infrastructure',
'Future-Proof Solutions',
'Innovation-Driven Tech'
];
let currentIndex = 0;
// 定义打字函数
function writeText() {
write('#write', {
text: texts[currentIndex],
speed: 80,
callback: () => {
// 打完后延迟2秒删除
setTimeout(() => {
write('#write', {
text: '',
speed: 50,
callback: () => {
// 切换下一个文本,循环
currentIndex = (currentIndex + 1) % texts.length;
writeText();
}
});
}, 2000);
}
});
}
// 启动打字
writeText();
// 手动添加光标样式
const style = document.createElement('style');
style.textContent = `
#write::after {
content: '|';
color: var(--neon-cyan);
animation: blink 1s infinite;
}
`;
document.head.appendChild(style);
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TypeIt 酷炫打字效果集合</title>
<!-- 引入 TypeIt 核心库 -->
<script src="https://unpkg.com/typeit@8.7.1/dist/index.umd.js"></script>
<style>
/* 全局样式设置 */
:root {
--bg-color: #0f172a;
--card-bg: #1e293b;
--text-main: #f8fafc;
--accent-color: #38bdf8;
--code-color: #a5b4fc;
}
body {
background-color: var(--bg-color);
color: var(--text-main);
font-family: 'Consolas', 'Monaco', 'Courier New', monospace; /* 使用等宽字体增强代码感 */
margin: 0;
padding: 2rem;
display: flex;
flex-direction: column;
align-items: center;
min-height: 100vh;
}
h1 {
margin-bottom: 2rem;
font-size: 2rem;
text-shadow: 0 0 10px rgba(56, 189, 248, 0.5);
border-bottom: 2px solid var(--accent-color);
padding-bottom: 0.5rem;
}
/* 网格布局 */
.grid-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
gap: 1.5rem;
width: 100%;
max-width: 1200px;
}
/* 卡片样式 */
.card {
background-color: var(--card-bg);
border-radius: 12px;
padding: 1.5rem;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.05);
display: flex;
flex-direction: column;
justify-content: flex-start;
min-height: 180px;
position: relative;
overflow: hidden;
}
.card-header {
font-size: 0.85rem;
color: #94a3b8;
margin-bottom: 1rem;
text-transform: uppercase;
letter-spacing: 1px;
display: flex;
align-items: center;
}
.card-header::before {
content: '';
display: inline-block;
width: 8px;
height: 8px;
background-color: var(--accent-color);
border-radius: 50%;
margin-right: 8px;
}
/* 打字区域样式 */
.type-output {
font-size: 1.2rem;
line-height: 1.6;
color: var(--text-main);
}
/* 特殊高亮样式类 (TypeIt支持插入HTML) */
.highlight {
color: var(--accent-color);
font-weight: bold;
}
.error {
color: #ef4444;
text-decoration: line-through;
}
.func {
color: #d8b4fe;
}
.str {
color: #86efac;
}
</style>
</head>
<body>
<h1>TypeIt.js 交互特效展示</h1>
<div class="grid-container">
<!-- 效果 1: 经典的自我修正 (打错字 -> 删除 -> 重写) -->
<div class="card">
<div class="card-header">01. 自我修正模式</div>
<div id="type1" class="type-output"></div>
</div>
<!-- 效果 2: 模拟代码编写 (语法高亮与换行) -->
<div class="card">
<div class="card-header">02. 程序员编码视角</div>
<div id="type2" class="type-output"></div>
</div>
<!-- 效果 3: 极速冻结与爆发 (变速控制) -->
<div class="card">
<div class="card-header">03. 变速节奏控制</div>
<div id="type3" class="type-output"></div>
</div>
<!-- 效果 4: 循环滚动标语 (无限循环) -->
<div class="card">
<div class="card-header">04. 无限循环 Slogan</div>
<div id="type4" class="type-output"></div>
</div>
<!-- 效果 5: 智能对话框 (暂停与思考感) -->
<div class="card">
<div class="card-header">05. AI 思考模拟</div>
<div id="type5" class="type-output"></div>
</div>
</div>
<script>
// --- 效果 1: 经典的自我修正 ---
// 模拟打错了字,然后回退修改,非常人性化
new TypeIt("#type1", {
speed: 50,
startDelay: 900
})
.type("我们致力于创造最糟糕...") // 先打错
.pause(500) // 停顿一下,仿佛意识到错误
.move(-3) // 光标左移3格
.delete(2) // 删除2个字 ("糟糕")
.type("<span class='highlight'>完美</span>") // 插入带样式的修正文字
.move(null, { to: "END" }) // 光标移回末尾
.type(" 的用户体验。")
.go();
// --- 效果 2: 模拟代码编写 ---
// 展示如何在打字过程中插入 HTML 标签来实现代码高亮
new TypeIt("#type2", {
speed: 30,
lifeLike: true, // 让打字速度略微随机,像真人
cursorChar: "▋", // 使用块状光标
})
.type("<span class='func'>function</span> <span class='highlight'>initWorld</span>() {")
.break() // 换行
.type(" <span class='func'>return</span> <span class='str'>'Hello Future!'</span>;")
.break()
.type("}")
.go();
// --- 效果 3: 变速节奏控制 ---
// 演示 exec() 方法和 options 修改,实现忽快忽慢的效果
new TypeIt("#type3", {
speed: 50,
waitUntilVisible: true
})
.type("普通的速度输入一段文字,")
.pause(300)
.type("但是突然...")
.pause(500)
// 瞬间改变速度,极速输入
.options({ speed: 0 })
.type("系统检测到大量数据涌入,处理器开始超频运转!所有数据在一瞬间完成加载!")
.options({ speed: 50 }) // 恢复正常速度
.pause(500)
.break()
.type("恢复平静。")
.go();
// --- 效果 4: 无限循环 Slogan ---
// 适合用在 Banner 上,展示多个关键词
new TypeIt("#type4", {
speed: 80,
loop: true, // 开启循环
loopDelay: 1500 // 循环前的等待时间
})
.type("Design.")
.pause(1000)
.delete() // 删除全部
.type("Develop.")
.pause(1000)
.delete()
.type("Deploy.")
.pause(1000)
.delete()
.type("<span class='highlight'>Dominate.</span>") // 最后一个词高亮
.go();
// --- 效果 5: AI 思考模拟 ---
// 利用省略号动画模拟“正在输入”或“正在思考”
new TypeIt("#type5", {
speed: 50,
cursor: true
})
.type("用户:这个方案可行吗?")
.break()
.break()
.type("AI:正在分析数据")
// 模拟思考的三个点
.type(".", { delay: 400 }).type(".", { delay: 400 }).type(".", { delay: 400 })
.delete(3) // 删掉点
.type(".", { delay: 400 }).type(".", { delay: 400 }).type(".", { delay: 400 }) // 再来一次,增加紧张感
.delete(9) // 删除 "正在分析数据..."
.type("经计算,成功率为 <span class='highlight'>99.9%</span>。")
.go();
</script>
</body>
</html>