819 lines
11 KiB
Plaintext
819 lines
11 KiB
Plaintext
<!DOCTYPE html>
|
||
|
||
<html>
|
||
|
||
<head>
|
||
|
||
<style>
|
||
|
||
:root {
|
||
|
||
--transition-duration: 0.8s;
|
||
|
||
}
|
||
|
||
* {
|
||
|
||
margin: 0;
|
||
|
||
padding: 0;
|
||
|
||
box-sizing: border-box;
|
||
|
||
}
|
||
|
||
html {
|
||
|
||
scroll-behavior: smooth;
|
||
|
||
scroll-snap-type: y mandatory;
|
||
|
||
}
|
||
|
||
body {
|
||
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||
|
||
overflow-y: scroll;
|
||
|
||
scroll-snap-type: y mandatory;
|
||
|
||
}
|
||
|
||
/* 第一屏强制snap */
|
||
|
||
.first-screen-section {
|
||
|
||
height: 100vh;
|
||
|
||
scroll-snap-align: start;
|
||
|
||
scroll-snap-stop: always;
|
||
|
||
position: relative;
|
||
|
||
}
|
||
|
||
/* 第一屏内容 */
|
||
|
||
#hero {
|
||
|
||
background: linear-gradient(135deg, #1a2980 0%, #26d0ce 100%);
|
||
|
||
color: white;
|
||
|
||
display: flex;
|
||
|
||
flex-direction: column;
|
||
|
||
justify-content: center;
|
||
|
||
align-items: center;
|
||
|
||
text-align: center;
|
||
|
||
padding: 20px;
|
||
|
||
}
|
||
|
||
.hero-content {
|
||
|
||
max-width: 800px;
|
||
|
||
}
|
||
|
||
.hero-title {
|
||
|
||
font-size: clamp(2.5rem, 5vw, 4rem);
|
||
|
||
margin-bottom: 1rem;
|
||
|
||
opacity: 0;
|
||
|
||
transform: translateY(30px);
|
||
|
||
animation: fadeInUp 1s ease forwards 0.3s;
|
||
|
||
}
|
||
|
||
.hero-subtitle {
|
||
|
||
font-size: clamp(1rem, 2vw, 1.3rem);
|
||
|
||
opacity: 0.9;
|
||
|
||
margin-bottom: 2rem;
|
||
|
||
opacity: 0;
|
||
|
||
transform: translateY(30px);
|
||
|
||
animation: fadeInUp 1s ease forwards 0.6s;
|
||
|
||
}
|
||
|
||
.scroll-prompt {
|
||
|
||
position: absolute;
|
||
|
||
bottom: 40px;
|
||
|
||
display: flex;
|
||
|
||
flex-direction: column;
|
||
|
||
align-items: center;
|
||
|
||
gap: 12px;
|
||
|
||
opacity: 0;
|
||
|
||
animation: fadeIn 1s ease forwards 1s;
|
||
|
||
}
|
||
|
||
.chevron {
|
||
|
||
width: 24px;
|
||
|
||
height: 24px;
|
||
|
||
border-right: 3px solid rgba(255, 255, 255, 0.7);
|
||
|
||
border-bottom: 3px solid rgba(255, 255, 255, 0.7);
|
||
|
||
transform: rotate(45deg);
|
||
|
||
animation: bounce 2s infinite;
|
||
|
||
}
|
||
|
||
@keyframes fadeInUp {
|
||
|
||
to {
|
||
|
||
opacity: 1;
|
||
|
||
transform: translateY(0);
|
||
|
||
}
|
||
|
||
}
|
||
|
||
@keyframes fadeIn {
|
||
|
||
to {
|
||
|
||
opacity: 1;
|
||
|
||
}
|
||
|
||
}
|
||
|
||
@keyframes bounce {
|
||
|
||
0%, 100% {
|
||
|
||
transform: rotate(45deg) translateY(0);
|
||
|
||
}
|
||
|
||
50% {
|
||
|
||
transform: rotate(45deg) translateY(10px);
|
||
|
||
}
|
||
|
||
}
|
||
|
||
/* 第二屏开始正常滚动 */
|
||
|
||
.content-section {
|
||
|
||
min-height: 100vh;
|
||
|
||
padding: 80px 20px;
|
||
|
||
scroll-snap-align: none; /* 取消snap */
|
||
|
||
}
|
||
|
||
.section-content {
|
||
|
||
max-width: 1200px;
|
||
|
||
margin: 0 auto;
|
||
|
||
}
|
||
|
||
.content-title {
|
||
|
||
font-size: 2.5rem;
|
||
|
||
margin-bottom: 2rem;
|
||
|
||
color: #1a2980;
|
||
|
||
}
|
||
|
||
.content-text {
|
||
|
||
font-size: 1.1rem;
|
||
|
||
line-height: 1.8;
|
||
|
||
color: #333;
|
||
|
||
margin-bottom: 1.5rem;
|
||
|
||
}
|
||
|
||
/* 卡片样式 */
|
||
|
||
.feature-grid {
|
||
|
||
display: grid;
|
||
|
||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||
|
||
gap: 30px;
|
||
|
||
margin-top: 40px;
|
||
|
||
}
|
||
|
||
.feature-card {
|
||
|
||
background: white;
|
||
|
||
padding: 30px;
|
||
|
||
border-radius: 16px;
|
||
|
||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08);
|
||
|
||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||
|
||
}
|
||
|
||
.feature-card:hover {
|
||
|
||
transform: translateY(-5px);
|
||
|
||
box-shadow: 0 15px 40px rgba(0, 0, 0, 0.12);
|
||
|
||
}
|
||
|
||
.feature-icon {
|
||
|
||
font-size: 2.5rem;
|
||
|
||
margin-bottom: 1rem;
|
||
|
||
color: #26d0ce;
|
||
|
||
}
|
||
|
||
.feature-title {
|
||
|
||
font-size: 1.5rem;
|
||
|
||
margin-bottom: 1rem;
|
||
|
||
color: #1a2980;
|
||
|
||
}
|
||
|
||
/* 动态类 */
|
||
|
||
.snap-enabled {
|
||
|
||
scroll-snap-type: y mandatory;
|
||
|
||
}
|
||
|
||
.snap-disabled {
|
||
|
||
scroll-snap-type: none;
|
||
|
||
}
|
||
|
||
/* 进度指示器 */
|
||
|
||
.progress-container {
|
||
|
||
position: fixed;
|
||
|
||
top: 0;
|
||
|
||
left: 0;
|
||
|
||
width: 100%;
|
||
|
||
height: 4px;
|
||
|
||
background: rgba(0, 0, 0, 0.05);
|
||
|
||
z-index: 1000;
|
||
|
||
}
|
||
|
||
.progress-bar {
|
||
|
||
height: 100%;
|
||
|
||
background: linear-gradient(to right, #1a2980, #26d0ce);
|
||
|
||
width: 0%;
|
||
|
||
transition: width 0.3s ease;
|
||
|
||
}
|
||
|
||
</style>
|
||
|
||
</head>
|
||
|
||
<body>
|
||
|
||
<!-- 滚动进度条 -->
|
||
|
||
<div class="progress-container">
|
||
|
||
<div class="progress-bar" id="progressBar"></div>
|
||
|
||
</div>
|
||
|
||
<!-- 第一屏 -->
|
||
|
||
<section id="hero" class="first-screen-section">
|
||
|
||
<div class="hero-content">
|
||
|
||
<h1 class="hero-title">Start Your Journey</h1>
|
||
|
||
<p class="hero-subtitle">Experience seamless scrolling from first screen to second</p>
|
||
|
||
</div>
|
||
|
||
|
||
|
||
<div class="scroll-prompt" id="scrollPrompt">
|
||
|
||
<div class="chevron"></div>
|
||
|
||
<span style="font-size: 0.9rem; opacity: 0.8;">Scroll to Continue</span>
|
||
|
||
</div>
|
||
|
||
</section>
|
||
|
||
<!-- 正常内容区域 -->
|
||
|
||
<main id="content">
|
||
|
||
<section class="content-section">
|
||
|
||
<div class="section-content">
|
||
|
||
<h2 class="content-title">Welcome to Normal Scrolling</h2>
|
||
|
||
<p class="content-text">From here, you can scroll normally. The first screen was a special full-screen scroll, but now you have control.</p>
|
||
|
||
|
||
|
||
<div class="feature-grid">
|
||
|
||
<div class="feature-card">
|
||
|
||
<div class="feature-icon">🎯</div>
|
||
|
||
<h3 class="feature-title">Precision</h3>
|
||
|
||
<p>Accurate and responsive scrolling experience tailored to your needs.</p>
|
||
|
||
</div>
|
||
|
||
|
||
|
||
<div class="feature-card">
|
||
|
||
<div class="feature-icon">⚡</div>
|
||
|
||
<h3 class="feature-title">Performance</h3>
|
||
|
||
<p>Optimized scrolling with smooth transitions and minimal lag.</p>
|
||
|
||
</div>
|
||
|
||
|
||
|
||
<div class="feature-card">
|
||
|
||
<div class="feature-icon">🎨</div>
|
||
|
||
<h3 class="feature-title">Design</h3>
|
||
|
||
<p>Beautiful visual feedback and intuitive user interactions.</p>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
</section>
|
||
|
||
|
||
|
||
<section class="content-section" style="background: #f8fafc;">
|
||
|
||
<div class="section-content">
|
||
|
||
<h2 class="content-title">More Content</h2>
|
||
|
||
<p class="content-text">This section demonstrates normal scrolling behavior. You can scroll freely without any forced snapping.</p>
|
||
|
||
|
||
|
||
<div style="height: 800px; display: flex; flex-direction: column; gap: 20px; padding: 20px; background: white; border-radius: 12px; margin-top: 40px;">
|
||
|
||
<div style="height: 200px; background: linear-gradient(90deg, #f0f0f0, #ffffff); border-radius: 8px;"></div>
|
||
|
||
<div style="height: 200px; background: linear-gradient(90deg, #ffffff, #f0f0f0); border-radius: 8px;"></div>
|
||
|
||
<div style="height: 200px; background: linear-gradient(90deg, #f0f0f0, #ffffff); border-radius: 8px;"></div>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
</section>
|
||
|
||
|
||
|
||
<section class="content-section">
|
||
|
||
<div class="section-content">
|
||
|
||
<h2 class="content-title">Final Section</h2>
|
||
|
||
<p class="content-text">You've reached the end of the example. The transition from first screen to normal scrolling should have been seamless.</p>
|
||
|
||
</div>
|
||
|
||
</section>
|
||
|
||
</main>
|
||
|
||
<script>
|
||
|
||
class SmartScrollController {
|
||
|
||
constructor() {
|
||
|
||
this.firstScreen = document.getElementById('hero');
|
||
|
||
this.scrollPrompt = document.getElementById('scrollPrompt');
|
||
|
||
this.progressBar = document.getElementById('progressBar');
|
||
|
||
this.body = document.body;
|
||
|
||
|
||
|
||
this.isFirstScreenActive = true;
|
||
|
||
this.isAnimating = false;
|
||
|
||
this.lastScrollY = 0;
|
||
|
||
|
||
|
||
this.init();
|
||
|
||
}
|
||
|
||
|
||
|
||
init() {
|
||
|
||
// 初始启用snap
|
||
|
||
this.enableSnap();
|
||
|
||
|
||
|
||
// 监听滚动事件
|
||
|
||
this.setupScrollListeners();
|
||
|
||
|
||
|
||
// 监听滚动提示点击
|
||
|
||
this.setupClickListeners();
|
||
|
||
|
||
|
||
// 监听键盘
|
||
|
||
this.setupKeyboardListeners();
|
||
|
||
|
||
|
||
// 更新进度条
|
||
|
||
this.updateProgressBar();
|
||
|
||
}
|
||
|
||
|
||
|
||
setupScrollListeners() {
|
||
|
||
let ticking = false;
|
||
|
||
|
||
|
||
window.addEventListener('scroll', () => {
|
||
|
||
if (!ticking) {
|
||
|
||
requestAnimationFrame(() => {
|
||
|
||
this.handleScroll();
|
||
|
||
ticking = false;
|
||
|
||
});
|
||
|
||
ticking = true;
|
||
|
||
}
|
||
|
||
|
||
|
||
// 更新进度条
|
||
|
||
this.updateProgressBar();
|
||
|
||
});
|
||
|
||
|
||
|
||
// 检测第一屏是否离开
|
||
|
||
const observer = new IntersectionObserver(
|
||
|
||
(entries) => {
|
||
|
||
entries.forEach(entry => {
|
||
|
||
if (!entry.isIntersecting && this.isFirstScreenActive) {
|
||
|
||
// 第一屏离开视口,禁用snap
|
||
|
||
this.disableSnap();
|
||
|
||
this.isFirstScreenActive = false;
|
||
|
||
|
||
|
||
// 隐藏滚动提示
|
||
|
||
this.scrollPrompt.style.opacity = '0';
|
||
|
||
this.scrollPrompt.style.transition = 'opacity 0.5s ease';
|
||
|
||
}
|
||
|
||
});
|
||
|
||
},
|
||
|
||
{
|
||
|
||
threshold: 0.1,
|
||
|
||
rootMargin: '0px'
|
||
|
||
}
|
||
|
||
);
|
||
|
||
|
||
|
||
observer.observe(this.firstScreen);
|
||
|
||
}
|
||
|
||
|
||
|
||
setupClickListeners() {
|
||
|
||
// 点击滚动提示
|
||
|
||
this.scrollPrompt.addEventListener('click', () => {
|
||
|
||
this.scrollToNextSection();
|
||
|
||
});
|
||
|
||
|
||
|
||
// 点击第一屏其他区域
|
||
|
||
this.firstScreen.addEventListener('click', (e) => {
|
||
|
||
if (e.target === this.firstScreen) {
|
||
|
||
this.scrollToNextSection();
|
||
|
||
}
|
||
|
||
});
|
||
|
||
}
|
||
|
||
|
||
|
||
setupKeyboardListeners() {
|
||
|
||
window.addEventListener('keydown', (e) => {
|
||
|
||
if (this.isAnimating) return;
|
||
|
||
|
||
|
||
if (this.isFirstScreenActive) {
|
||
|
||
if (e.key === 'ArrowDown' || e.key === 'PageDown' || e.key === ' ') {
|
||
|
||
e.preventDefault();
|
||
|
||
this.scrollToNextSection();
|
||
|
||
}
|
||
|
||
}
|
||
|
||
});
|
||
|
||
}
|
||
|
||
|
||
|
||
handleScroll() {
|
||
|
||
const currentScrollY = window.scrollY;
|
||
|
||
const firstScreenHeight = this.firstScreen.offsetHeight;
|
||
|
||
|
||
|
||
if (this.isFirstScreenActive) {
|
||
|
||
// 如果用户尝试手动滚动离开第一屏,帮助他们完成过渡
|
||
|
||
if (currentScrollY > 50 && currentScrollY < firstScreenHeight && currentScrollY > this.lastScrollY) {
|
||
|
||
// 向下滚动,帮助过渡到第二屏
|
||
|
||
this.scrollToNextSection();
|
||
|
||
}
|
||
|
||
}
|
||
|
||
|
||
|
||
this.lastScrollY = currentScrollY;
|
||
|
||
}
|
||
|
||
|
||
|
||
scrollToNextSection() {
|
||
|
||
if (this.isAnimating) return;
|
||
|
||
|
||
|
||
this.isAnimating = true;
|
||
|
||
|
||
|
||
const firstScreenHeight = this.firstScreen.offsetHeight;
|
||
|
||
const targetScroll = firstScreenHeight;
|
||
|
||
|
||
|
||
// 平滑滚动
|
||
|
||
window.scrollTo({
|
||
|
||
top: targetScroll,
|
||
|
||
behavior: 'smooth'
|
||
|
||
});
|
||
|
||
|
||
|
||
// 滚动完成后标记
|
||
|
||
setTimeout(() => {
|
||
|
||
this.isAnimating = false;
|
||
|
||
this.disableSnap();
|
||
|
||
this.isFirstScreenActive = false;
|
||
|
||
this.scrollPrompt.style.opacity = '0';
|
||
|
||
}, 800);
|
||
|
||
}
|
||
|
||
|
||
|
||
enableSnap() {
|
||
|
||
this.body.classList.add('snap-enabled');
|
||
|
||
this.body.classList.remove('snap-disabled');
|
||
|
||
}
|
||
|
||
|
||
|
||
disableSnap() {
|
||
|
||
this.body.classList.add('snap-disabled');
|
||
|
||
this.body.classList.remove('snap-enabled');
|
||
|
||
|
||
|
||
// 移除snap属性
|
||
|
||
document.documentElement.style.scrollSnapType = 'none';
|
||
|
||
document.body.style.scrollSnapType = 'none';
|
||
|
||
}
|
||
|
||
|
||
|
||
updateProgressBar() {
|
||
|
||
const scrollTop = window.scrollY;
|
||
|
||
const scrollHeight = document.documentElement.scrollHeight - window.innerHeight;
|
||
|
||
const progress = (scrollTop / scrollHeight) * 100;
|
||
|
||
|
||
|
||
this.progressBar.style.width = `${progress}%`;
|
||
|
||
}
|
||
|
||
}
|
||
|
||
// 初始化
|
||
|
||
const scrollController = new SmartScrollController();
|
||
|
||
// 处理页面刷新时的位置
|
||
|
||
window.addEventListener('load', () => {
|
||
|
||
const scrollY = window.scrollY;
|
||
|
||
const firstScreenHeight = document.getElementById('hero').offsetHeight;
|
||
|
||
|
||
|
||
if (scrollY > firstScreenHeight * 0.5) {
|
||
|
||
// 如果刷新时已经超过第一屏的一半,直接禁用snap
|
||
|
||
scrollController.disableSnap();
|
||
|
||
scrollController.isFirstScreenActive = false;
|
||
|
||
scrollController.scrollPrompt.style.opacity = '0';
|
||
|
||
}
|
||
|
||
});
|
||
|
||
</script>
|
||
|
||
</body>
|
||
|
||
</html> |