滾動式動畫:範例
日期:2024-02-15T00:00:00.000Z
| 應用案例
‧ Progress Indicator
// 動畫
@keyframes progress {
from {
transform: scaleX(0);
}
to {
transform: scaleX(1);
}
}
// 滾動容器
html {
scroll-timeline: --progress block; // 命名及指定滾動容器,並設定滾動方向
// 動畫元素: 進度指示
.progress {
transform-origin: 0 50%;
animation: progress auto linear;
animation-timeline: --progress; // 指定動畫時間軸
}
}
‧ Text Reveal
<p class="text">
<span>
<!-- 文字內容 -->
</span>
</p>
:root {
// 依照字元數設定變數
--chars: 445;
}
// 動畫
@keyframes text-reveal {
0% {
background-size: 0ch; // 1ch 是一個數字 0 的寬度
}
100% {
background-size: calc(var(--chars) * 1ch);
}
}
body {
// 設定滾動空間
height: 500dvh;
.text {
// 固定文字位置
position: fixed;
top: 50%;
left: 50%;
translate: -50% -50%;
/* 若設定 text-align: justify 會影響動畫呈現,若要使用需再自行調整 */
span {
// 設定背景顏色 (初始文字顏色) 和圖片 (reveal 文字顏色)
background: #eee linear-gradient(to right, #37ecba 0%, #72afd3 100%) 0 / 0
no-repeat;
// 設定文字遮罩
background-clip: text;
color: transparent;
// 設定動畫、匿名滾動進度時間軸及滾動容器
animation: text-reveal steps(var(--chars)) forwards;
animation-timeline: scroll(root);
}
}
}
‧ Text Reveal
// 動畫
@keyframes invert {
from {
scale: 0 1;
}
}
html {
// 設定滾動空間
height: 200%;
background-color: #000;
color: #fff;
&::before {
content: "";
position: fixed;
inset: 0;
background-color: #fff;
transform-origin: 100%;
// 設定動畫及匿名滾動進度時間軸
animation: invert linear;
animation-timeline: scroll(); // scroll(nearest block)
}
// invert 元素
.element {
// 固定位置
position: fixed;
top: 50%;
left: 50%;
translate: -50% -50%;
// 負片效果
mix-blend-mode: exclusion;
}
}
‧ Sticky Header
// 動畫
@keyframes sticky-header {
from {
height: 100vh;
font-size: calc(10vw + 1rem);
}
to {
height: 10vh;
font-size: 2rem;
}
}
// 動畫元素: Header
header {
animation: sticky-header linear both;
animation-timeline: scroll(); // 建立匿名時間軸 scroll(nearest block)
animation-range: 0vh 100vh; // 設定動畫開始與結束範圍
}
‧ Back to Top
// 動畫
@keyframes reveal {
from {
transform: translateY(200%);
}
to {
transform: translateY(0%);
}
}
// 動畫元素: Back to Top 按鈕
.back {
animation: reveal linear;
animation-timeline: scroll();
animation-range: 0vh 10vh;
}
‧ Horizontal Scroll Section
<section class="section-pin">
<div class="pin-wrap-sticky">
<div class="pin-wrap">
<!-- 橫向滾動內容 -->
</div>
</div>
</section>
// 動畫
@keyframes move {
to {
// 水平移動使「滾動內容右側」與「視窗」對齊
transform: translateX(calc(-100% + 100vw));
left: 0;
}
}
.section-pin {
// 伸展區塊高度,為橫向滾動動畫創造空間
height: 500vh;
// 使子元素的 position: sticky 能正常運作
// 父元素設定 over: hidden 會讓子元素的 position: stikcy 失效
overflow: visible;
// 建立並命名察看進度時間軸
view-timeline-name: --section-pin-tl;
// 設定滾動方向
view-timeline-axis: block;
.pin-wrap-sticky {
// 使元素固定在滾動區塊的頂端
position: sticky;
top: 0;
width: 100vw;
height: 100vh;
overflow-x: hidden;
.pin-wrap {
width: 250vmax;
height: 100vh;
// 提示瀏覽器該元素會有 CSS 改變
will-change: transform;
animation: linear move forwards;
// 將動畫與 view-timeline 命名時間軸串聯
animation-timeline: --section-pin-tl;
// 設定動畫開始與結束範圍
animation-range: contain 0% contain 100%;
}
}
}
‧ Stacked Cards
<section class="stacked">
<ui class="cards">
<li class="card" id="card1">
<div class="content">
<!-- 卡片內容 -->
</div>
</li>
<li class="card" id="card2">
<div class="content">
<!-- 卡片內容 -->
</div>
</li>
<li class="card" id="card3">
<div class="content">
<!-- 卡片內容 -->
</div>
</li>
<li class="card" id="card4">
<div class="content">
<!-- 卡片內容 -->
</div>
</li>
</ui>
</section>
// 設定 CSS 變數
:root {
--card-height: 40vw;
--card-margin: 4vw;
--card-top-offset: 1rem;
}
// 設定 index => 編號 - 1
#card1 {
--index: 0;
}
#card2 {
--index: 1;
}
#card3 {
--index: 2;
}
#card4 {
--index: 3;
}
.stacked {
.cards {
// 設定卡片數量變數
--cards: 4;
display: grid;
grid-template-columns: 1fr;
grid-template-rows: repeat(var(--cards), var(--card-height));
gap: var(--card-margin);
margin-bottom: var(--card-margin);
padding-bottom: calc(var(--card-s) * var(--card-top-offset));
list-style: none;
// 設定命名查看進度時間軸
view-timeline-name: --stacked-cards;
.card {
// 設定反向 index
--r-index: calc(var(--cards) - var(--index) - 1);
// 使卡片固定在距離頂端 2.5vh 處
position: sticky;
top: 2.5vh;
// 每張卡片設定不同 padding-top => 堆疊效果
// var(--index) + 1 使 #card1 的 padding-top 不為 0
padding-top: calc((var(--index) + 1) * var(--card-top-offset));
.content {
// 設定動畫開始範圍變數
--start: calc(var(--index) / var(--cards) * 100%);
// 設定動畫結束範圍變數
// var(--index) + 1 使動畫結束範圍 > 開始範圍,且 #card1 的動畫結束範圍不為 0%
--end: calc((var(--index) + 1) / var(--cards) * 100%);
// 設定 transform 起始點 => x 軸 50% 、 y 軸 0% 處
transform-origin: 50% 0%;
// 提示瀏覽器該元素會有 CSS 改變
will-change: transform;
// 設定動畫、時間軸及範圍
animation: linear scale forwards;
animation-timeline: --stacked-cards;
animation-range: exit-crossing var(--start) exit-crossing var(--end);
}
}
}
}
@keyframes scale {
to {
// 設定卡片縮放尺寸 => 越前面卡片越小、越後面卡片越大
transform: scale(calc(1 - calc(0.1 * var(--r-index))));
}
}
本文轉載自筆者的 Medium 文章,更詳細內容可點及連結前往查看。