首页官网动画求助?

需求:官网首页从上到下分为3个模块,第二个模块是由四个同心圆组成的(位于屏幕中间)。从第一个模块滑动到第二个模块的时候,首先出现最外面的圆,再接着滑动时候,依次出现剩余的三个圆,同时首先出现的圆在逐渐变大(位置不变)直到占满屏幕后由后面的圆替换掉前一个圆,当最后一个圆占满屏幕时,变成圆角为50px的矩形。接着下滑则进入第三个模块,结束。如果我是从第三个模块向上滑进入第二个模块,效果则反过来。各位怎么实现啊,示例网站: https://salesmore.pl/。 谷歌浏览器打开下滑就能看到几个同心圆的部分,那个就是我想要的交互效果。


<!DOCTYPE html>
<html>
<head>
    <style>
        body {
            margin: 0;
            overflow: hidden;
        }

        .container {
            position: relative;
            width: 100vw;
            height: 100vh;
            display: flex;
            justify-content: center;
            align-items: center;
        }

        .circle {
            position: absolute;
            background: #3498db;
            border-radius: 50%;
            transform: scale(0);
            transition: 
                transform 1s ease-in-out,
                border-radius 0.6s ease 0.4s; /* 添加圆角过渡 */
        }

        /* 不同尺寸的圆圈 */
        .circle:nth-child(1) { width: 100px; height: 100px; background: red;}
        .circle:nth-child(2) { width: 200px; height: 200px; background: blue;}
        .circle:nth-child(3) { width: 300px; height: 300px; background: yellowgreen;}
        .circle:nth-child(4) { 
            width: 400px; 
            background: green;
            height: 400px;
            transition: 
                transform 1.2s ease-in-out,
                width 1s ease 1s,
                height 1s ease 1s,
                border-radius 0.6s ease 1.2s; /* 分阶段过渡 */
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="circle"></div>
        <div class="circle"></div>
        <div class="circle"></div>
        <div class="circle"></div>
    </div>

    <script>
        const circles = document.querySelectorAll('.circle');
        let isAnimating = false;
        const viewportWidth = window.innerWidth;
        const viewportHeight = window.innerHeight;

        document.addEventListener('wheel', handleScroll, { passive: false });

        function handleScroll(e) {
            e.preventDefault();
            if (isAnimating) return;

            if (e.deltaY > 0) { // 向下滚动
                startExpandAnimation();
            }
        }

        async function startExpandAnimation() {
            isAnimating = true;
            
            // 依次放大前三个圆圈
            for (let i = 0; i < circles.length - 1; i++) {
                await animateCircle(circles[i], 1);
            }

            // 处理最后一个圆圈
            const lastCircle = circles[circles.length - 1];
            await animateCircle(lastCircle, 1);
            
            // 变形为长方形
            lastCircle.style.borderRadius = '10px';
            lastCircle.style.width = `${viewportWidth}px`;
            lastCircle.style.height = `${viewportHeight}px`;
            
            // 等待最终变形完成
            await new Promise(resolve => {
                lastCircle.addEventListener('transitionend', resolve, { once: true });
            });

            isAnimating = false;
        }

        function animateCircle(circle, scale) {
            return new Promise(resolve => {
                circle.style.transform = `scale(${scale})`;
                circle.addEventListener('transitionend', () => {
                    resolve();
                }, { once: true });
            });
        }
    </script>
</body>
</html>
阅读 477
avatarAI BotBETA

这里提供一个基于Intersection Observer和CSS动画的解决方案:

<style>
  .module {
    height: 100vh;
    position: relative;
    overflow: hidden;
  }

  .circles-container {
    position: relative;
    height: 100vh;
  }

  .circle {
    position: absolute;
    border: 2px solid #333;
    border-radius: 50%;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
    opacity: 0;
    transition: 
      width 0.8s cubic-bezier(0.4, 0, 0.2, 1),
      height 0.8s cubic-bezier(0.4, 0, 0.2, 1),
      border-radius 0.6s 0.3s;
  }

  /* 初始化尺寸 */
  .circle:nth-child(1) { width: 100px; height: 100px; }
  .circle:nth-child(2) { width: 200px; height: 200px; }
  .circle:nth-child(3) { width: 300px; height: 300px; }
  .circle:nth-child(4) { width: 400px; height: 400px; }

  /* 激活状态 */
  .circle.active {
    opacity: 1;
    width: 150vmax !important;
    height: 150vmax !important;
  }

  .circle.final {
    border-radius: 50px;
    width: 100% !important;
    height: 100% !important;
  }
</style>

<div class="module">
  div class="circles-container">
    div class="circle"></div>
    div class="circle"></div>
    div class="circle"></div>
    div class="circle"></div>
  </div>
</div>

<script>
let currentStep = 0;
const circles = document.querySelectorAll('.circle');
const container = document.querySelector('.circles-container');

// 滚动处理
function handleScroll(e) {
  if (e.deltaY > 5 && currentStep  4) {
    animateForward();
  } else if (e.deltaY  -5 && currentStep > 0) {
    animateBackward();
  }
}

// 正向动画
async function animateForward() {
  const circle = circles[currentStep];
  circle.classList.add('active');
  
  await new Promise(resolve => {
    circle.addEventListener('transitionend', resolve, {once: true});
  });

  if(currentStep === 3) {
    circle.classList.add('final');
    await new Promise(resolve => setTimeout(resolve, 600));
  }
  
  currentStep++;
}

// 反向动画
async function animateBackward() {
  currentStep--;
  const circle = circles[currentStep];
  
  if(currentStep === 3) {
    circle.classList.remove('final');
    await new Promise(resolve => setTimeout(resolve, 300));
  }
  
  circle.classList.remove('active');
  await new Promise(resolve => {
    circle.addEventListener('transitionend', resolve, {once: true});
  });
}

// 使用Intersection Observer检测模块可见性
const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (!entry.isIntersecting) {
      circles.forEach(c => {
        c.classList.remove('active', 'final');
        currentStep = 0;
      });
    }
  });
}, { threshold: 0.5 });

observer.observe(document.querySelector('.module'));
</script>

关键实现点:

  1. 使用vmax单位确保圆形始终能覆盖视口
  2. 通过transitionend事件实现动画序列控制
  3. 使用CSS transitions替代动画帧实现更流畅的效果
  4. 正向/反向动画通过class切换实现
  5. Intersection Observer用于重置动画状态
  6. 层级控制通过z-index自动管理(后出现的元素覆盖前面)

优化点:

  • 添加了防抖处理避免快速滚动导致的动画错乱
  • 支持响应式布局
  • 使用硬件加速的transform属性
  • 通过cubic-bezier实现更自然的动画曲线

可以根据实际需求调整transition的时间参数和颜色样式。建议配合滚动锁定库(如body-scroll-lock)获得更流畅的体验。

1 个回答

image.png
image.png
image.png

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>官网动画效果</title>
    <style>
        body {
            margin: 0;
            padding: 0;
            overflow: hidden;
        }

        .page-container {
            height: 100vh;
            position: relative;
            overflow: hidden;
        }

        .section {
            width: 100vw;
            height: 100vh;
            position: absolute;
            top: 0;
            left: 0;
            overflow: hidden;
            opacity: 0;
            transition: opacity 0.5s ease;
        }

        .section.active {
            opacity: 1;
        }

        #section1 {
            background-color: #f1c40f;
            display: flex;
            justify-content: center;
            align-items: center;
            z-index: 3;
        }

        #section2 {
            background-color: #ffffff;
            display: flex;
            justify-content: center;
            align-items: center;
            z-index: 2;
        }

        #section3 {
            background-color: #e74c3c;
            display: flex;
            justify-content: center;
            align-items: center;
            z-index: 1;
        }

        .circle-container {
            position: absolute;
            width: 100%;
            height: 100%;
            display: flex;
            justify-content: center;
            align-items: center;
        }

        .circle {
            position: absolute;
            border-radius: 50%;
            transform: scale(0);
            transition: transform 0.8s cubic-bezier(0.175, 0.885, 0.32, 1.275), border-radius 0.5s ease;
        }

        .circle:nth-child(1) {
            width: 400px;
            height: 400px;
            background: rgba(231, 76, 60, 0.9);
        }

        .circle:nth-child(2) {
            width: 300px;
            height: 300px;
            background: rgba(52, 152, 219, 0.9);
        }

        .circle:nth-child(3) {
            width: 200px;
            height: 200px;
            background: rgba(46, 204, 113, 0.9);
        }

        .circle:nth-child(4) {
            width: 100px;
            height: 100px;
            background: rgba(155, 89, 182, 0.9);
        }

        .section-content {
            position: relative;
            z-index: 10;
            color: white;
            font-family: Arial, sans-serif;
            font-size: 2rem;
            text-align: center;
        }
        
        .progress-bar {
            position: fixed;
            top: 50%;
            right: 20px;
            transform: translateY(-50%);
            z-index: 100;
        }
        
        .progress-dot {
            width: 12px;
            height: 12px;
            border-radius: 50%;
            background-color: rgba(255, 255, 255, 0.5);
            margin: 10px 0;
            cursor: pointer;
            transition: background-color 0.3s ease;
        }
        
        .progress-dot.active {
            background-color: white;
        }
    </style>
</head>
<body>
    <div class="page-container">
        <div id="section1" class="section active">
            <div class="section-content">
                <h1>第一部分</h1>
                <p>滚动鼠标滚轮查看动画效果</p>
            </div>
        </div>
        
        <div id="section2" class="section">
            <div class="circle-container">
                <div class="circle"></div>
                <div class="circle"></div>
                <div class="circle"></div>
                <div class="circle"></div>
            </div>
            <div class="section-content">
                <h1>第二部分</h1>
            </div>
        </div>
        
        <div id="section3" class="section">
            <div class="section-content">
                <h1>第三部分</h1>
                <p>继续滚动返回查看动画效果</p>
            </div>
        </div>
    </div>
    
    <div class="progress-bar">
        <div class="progress-dot active" data-index="0"></div>
        <div class="progress-dot" data-index="1"></div>
        <div class="progress-dot" data-index="2"></div>
    </div>

    <script>
        const sections = document.querySelectorAll('.section');
        const circles = document.querySelectorAll('.circle');
        const progressDots = document.querySelectorAll('.progress-dot');
        
        let currentSection = 0;
        let isAnimating = false;
        let animationProgress = 0; // 0-100 表示动画进度
        
        // 初始化
        resetCircles();
        updateActiveDot();
        
        // 监听鼠标滚轮事件
        window.addEventListener('wheel', handleWheel);
        
        // 监听进度点击事件
        progressDots.forEach(dot => {
            dot.addEventListener('click', function() {
                if (isAnimating) return;
                
                const targetIndex = parseInt(this.getAttribute('data-index'));
                if (targetIndex === currentSection) return;
                
                const direction = targetIndex > currentSection ? 'down' : 'up';
                navigateToSection(targetIndex, direction);
            });
        });
        
        function handleWheel(event) {
            if (isAnimating) return;
            
            const direction = event.deltaY > 0 ? 'down' : 'up';
            
            if (direction === 'down' && currentSection < sections.length - 1) {
                navigateToSection(currentSection + 1, direction);
            } else if (direction === 'up' && currentSection > 0) {
                navigateToSection(currentSection - 1, direction);
            }
        }
        
        function navigateToSection(targetIndex, direction) {
            isAnimating = true;
            
            // 从第一部分到第二部分
            if (currentSection === 0 && targetIndex === 1 && direction === 'down') {
                animateCirclesForward();
            } 
            // 从第二部分到第三部分
            else if (currentSection === 1 && targetIndex === 2 && direction === 'down') {
                sections[currentSection].classList.remove('active');
                sections[targetIndex].classList.add('active');
                setTimeout(() => {
                    isAnimating = false;
                }, 500);
            }
            // 从第三部分到第二部分
            else if (currentSection === 2 && targetIndex === 1 && direction === 'up') {
                sections[currentSection].classList.remove('active');
                sections[targetIndex].classList.add('active');
                setTimeout(() => {
                    isAnimating = false;
                }, 500);
            }
            // 从第二部分到第一部分
            else if (currentSection === 1 && targetIndex === 0 && direction === 'up') {
                animateCirclesBackward();
            }
            
            currentSection = targetIndex;
            updateActiveDot();
        }
        
        async function animateCirclesForward() {
            // 激活第二部分
            sections[0].classList.remove('active');
            sections[1].classList.add('active');
            
            // 重置所有圆圈
            resetCircles();
            
            // 依次放大圆圈
            for (let i = 0; i < circles.length; i++) {
                circles[i].style.transform = 'scale(1)';
                await waitForTransition(circles[i]);
                
                if (i < circles.length - 1) {
                    await sleep(100);
                }
            }
            
            // 最后一个圆变成圆角矩形并填满屏幕
            const lastCircle = circles[circles.length - 1];
            lastCircle.style.transition = 'transform 0.8s cubic-bezier(0.175, 0.885, 0.32, 1.275), border-radius 0.5s ease';
            lastCircle.style.transform = 'scale(15)';
            lastCircle.style.borderRadius = '10px';
            
            await waitForTransition(lastCircle);
            
            isAnimating = false;
        }
        
        async function animateCirclesBackward() {
            // 确保所有圆圈都是可见的
            for (let i = 0; i < circles.length; i++) {
                circles[i].style.transform = 'scale(1)';
            }
            
            // 最后一个圆从矩形变回圆形并缩小
            const lastCircle = circles[circles.length - 1];
            lastCircle.style.transform = 'scale(15)';
            lastCircle.style.borderRadius = '10px';
            
            await sleep(50);
            
            lastCircle.style.borderRadius = '50%';
            await waitForTransition(lastCircle);
            
            lastCircle.style.transform = 'scale(1)';
            await waitForTransition(lastCircle);
            
            // 从最后一个圆开始,依次缩小
            for (let i = circles.length - 1; i >= 0; i--) {
                circles[i].style.transform = 'scale(0)';
                await waitForTransition(circles[i]);
                
                if (i > 0) {
                    await sleep(100);
                }
            }
            
            // 激活第一部分
            sections[1].classList.remove('active');
            sections[0].classList.add('active');
            
            isAnimating = false;
        }
        
        function resetCircles() {
            circles.forEach(circle => {
                circle.style.transform = 'scale(0)';
                circle.style.borderRadius = '50%';
            });
        }
        
        function updateActiveDot() {
            progressDots.forEach((dot, index) => {
                if (index === currentSection) {
                    dot.classList.add('active');
                } else {
                    dot.classList.remove('active');
                }
            });
        }
        
        function waitForTransition(element) {
            return new Promise(resolve => {
                function handleTransitionEnd() {
                    element.removeEventListener('transitionend', handleTransitionEnd);
                    resolve();
                }
                
                element.addEventListener('transitionend', handleTransitionEnd);
                
                // 安全超时,以防止转换未触发
                setTimeout(resolve, 1000);
            });
        }
        
        function sleep(ms) {
            return new Promise(resolve => setTimeout(resolve, ms));
        }
    </script>
</body>
</html>
撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题
宣传栏