将滚动驱动的动画从 JavaScript 重构到 CSS

原文信息: 查看原文查看原文

Refactoring a scroll-driven animation from JavaScript to CSS

- Andrico Karoulla

滚动链接动画目前在前端领域非常流行,这在很大程度上得益于原生 CSS 方法将动画链接到滚动进度的日益普及。我甚至在这个网站上使用了它们。屏幕左上角的进度指示器是使用大量尖端 CSS 特性构建的

然而,滚动链接动画并不是什么新鲜事,真正新鲜的是日益增长的支持,可以在不编写一行 JavaScript 的情况下实现这种酷炫的视觉修饰。并非每个人都有幸在一个全新项目中构建有趣和引人入胜的用户界面。如果你已经有了一个由 JS 驱动的滚动链接动画怎么办?在本文中,我将介绍如何将由 JavaScript 驱动的滚动链接动画迁移到使用新的 CSS 属性。

我们将通过将以下动画从一个 JavaScript 实现迁移到使用新的 CSS 属性来进行操作。

home-animation-simple-starter.gif

这个动画是由 JavaScript 驱动的。一般逻辑如下:

  1. 当页面加载时,我向窗口附加了一个“滚动”事件监听器
  2. 每当用户滚动时,事件处理程序会触发,它:
    1. 遍历目标元素列表。
    2. 检查目标元素是否在可见窗口内
    3. 如果是,它确定当前动画状态
    4. 它将进度值设置为 CSS 变量,该变量用于设置目标元素的样式。

听起来有点复杂,所以这里有一个带有运行代码的代码沙盒。请随意探索和玩弄代码。

starter.js 内的代码显示了包含动画逻辑的 Web 组件,用 JavaScript 编写。finisher.js 内的代码显示了包含 CSS 编写的动画逻辑的 Web 组件。

让我们更深入地了解代码:

页面设置为一个简单的 HTML 页面,该页面渲染一个 Web 组件。Web 组件包含可动画元素的封装逻辑。别担心,你不需要理解 Web 组件就可以跟上!

这是可动画元素的标记:

<div class="container">
    <div class="row">
        <div class="square-wrapper">
            <div class="square one"></div>
        </div>
        <div class="square-wrapper">
            <div class="square two"></div>
        </div>
    </div>
    <div class="row">
        <div class="square-wrapper">
            <div class="square three"></div>
        </div>
        <div class="square-wrapper">
            <div class="square four"></div>
        </div>
    </div>
</div>

(稍后我会解释为什么 .square 元素被包裹在一个 .square-wrapper div 中)

这将渲染到页面上:

four grey blocks rendered to the page

大部分 CSS 都是严格视觉的,但每个方块都有一些属性的值与当前滚动位置相关联:

.square.one {
    opacity: var(--animation-position);
    transform: translateX(calc(-50px + var(--animation-position) * 50px));
}

.square.two {
    opacity: var(--animation-position);
    transform: translateY(calc(50px - var(--animation-position) * 50px));
}

.square.three {
    opacity: var(--animation-position);
    transform: translateY(calc(-50px + var(--animation-position) * 50px));
}

.square.four {
    opacity: var(--animation-position);
    transform: translateX(calc(50px - var(--animation-position) * 50px));
}

opacity 的值是在滚动事件处理程序中设置的变量的值。至于 transform 属性,它使用动画位置将元素从起始位置移动到结束位置。

现在让我们看看 JavaScript。注意,我将其编写为伪代码,以避免用太多细节填充示例。

const #onScroll = () => {
    const elementsToAnimate = document.querySelectorAll('.square');
    elementsToAnimate.forEach((el) => {

        // 进入动画范围在视口高度的 70% 到 90% 之间
    if (elIsWithinEntryAnimationRange(el)) {
      el.style.setProperty('--animation-position', getAnimationPosition(el));
    }

        // 退出动画范围在视口高度的 10% 到 30% 之间
    if (elIsWithinExitAnimationRange(el)) {
      el.style.setProperty('--animation-position', getAnimationPosition(el));
    }

        // 动画在其完全状态在视口高度的 30% 到 70% 之间
    if (elIsWithinVisibleRange(el)) {
      el.style.setProperty('--animation-position', 1);
    }

        // 当元素在视口高度的 10% 以下或 90% 以上时,它处于静止状态
    if (elIsOutsideVisibleRange(el)) {
      el.style.setProperty('--animation-position', 0);
    }
  })
}

window.addEventListener('scroll', this.#onScroll);

完整实现需要大约 50 行代码。除了我编写的代码量外,这种方法还有其他缺点:

  1. 代码不可移植。我需要为任何滚动链接动画编写定制代码。
  2. 代码使用 JavaScript,这很好,但我们应该努力减少我们向用户发送的代码量。
  3. onScroll 函数在用户每次滚动时都会运行,这可能是每秒几次。如果您设置了多个计算量大的滚动处理程序,您的用户可能会注意到性能下降。
  4. 逻辑可能变得复杂。我需要自己添加保护措施,以确保动画只在特定的滚动范围内工作。我还需要自己计算中间动画值。还有许多其他边缘情况我也没有考虑到。

注意:实际上,一个主要的边缘情况是需要计算通过 transform 属性调整位置的元素的正确动画位置。这就是为什么每个方块都被包裹在一个 .square-wrapper div 中。

迁移到 CSS

让我们通过删除冗余元素来简化 DOM。

<div class="container">
    <div class="outer">
        <div class="row">
            <div class="square one"></div>
            <div class="square two"></div>
        </div>
        <div class="row">
            <div class="square three"></div>
            <div class="square four"></div>
        </div>
    </div>
</div>

第一步是删除所有 JavaScript,包括事件监听器。您可以用一个简单的关键帧动画替换它:

@keyframes appear {
    from,
    to {
        transform: translateX(var(--fade-in-start-pos-x)) translateY(var(--fade-in-start-pos-y));
        opacity: 0;
    }

    25%,
    75% {
        transform: translate(0px, 0px);
        opacity: 1;
    }
}

简而言之,动画执行以下操作:

  • 目标元素在动画的前 25% 出现
  • 从 25% 到 75% 的动画,目标元素将只是坐在那里看起来很漂亮
  • 目标元素在动画的最后 75% 退出

--fade-in-start-pos-x--fade-in-start-pos-y 变量在 .square 选择器内设置,以使每个方块都能控制其起始位置。

.square.one {
    --fade-in-start-pos-x: -50px;
    --fade-in-start-pos-y: 0;
}

.square.two {
    --fade-in-start-pos-x: 0;
    --fade-in-start-pos-y: 50px;
}

.square.three {
    --fade-in-start-pos-x: 0;
    --fade-in-start-pos-y: -50px;
}

.square.four {
    --fade-in-start-pos-x: 50px;
    --fade-in-start-pos-y: 0;
}

我需要做的最后一件事是将动画链接到滚动进度。我可以用以下三行代码来完成:

.square {
    animation: appear linear both;
    animation-timeline: view(block);
    animation-range: contain 10% contain 90%;
}

让我们逐行介绍:

animation

animation: appear linear both

这是动画属性的简写,它:

  • 指定我们想要使用的 keyframes 规则的名称,appear
  • 指定缓动函数,linear
  • 指定填充模式,both

fill-mode: both 确保动画完成后,动画的开始和结束样式在元素上持续存在。您可以在 MDN 上了解更多关于 fill-mode 的信息

animation-timeline

animation-timeline: view();

这行 CSS 有很多要解释的。

首先,这个 CSS 属性接受两种不同的函数,scroll()view()

scroll() 将动画链接到祖先元素的滚动进度。默认情况下,这个祖先元素是最近的有滚动条的祖先。

view() 将动画进度链接到目标元素的可见性。

animation-range

最后,最后一行 CSS 可以精细控制动画何时开始和结束。默认情况下,当目标元素的顶部进入视口时,动画开始,当元素的底部退出视口时,动画结束。

在我的案例中,我只想在元素完全包含在视口内时才开始动画。退出动画也是如此,它应该在元素到达视口末端的那一刻完成。

绿色阴影部分显示了元素需要通过的阈值,然后动画开始和结束。

要设置这种行为,您需要将 animation-range 设置为 contain

animation-range: contain;

但我还想再做一个改变。我想在动画的开始和结束时减少动画时间轴的范围,各减少 10%。

在上面的图片中,您可以看到两个 contain 障碍都被拉进了视口内 10% 的视口高度。

这是通过设置以下 CSS 完成的。

animation-range: contain 10% 90%

要吸收的内容很多,特别是当涉及到 animation-range 属性时。我建议查看 Bramus 的 惊人可视化工具 以帮助您熟悉其功能。

完成所有这些后,动画应该可以很好地工作

home-animation-simple-finisher.gif

注意:您可能需要调整一些偏移和缓动值,以获得等效的动画体验。

我们通过删除数十行 JS 代码并添加几行 CSS 代码来实现这一点。

分享于 2024-06-25

访问量 54

预览图片