# 科技新知

滾動式動畫:範例

作者:Oliver Xiong

日期:2024-02-15T00:00:00.000Z


| 應用案例

‧ Progress Indicator

Progress Indicator

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

Text Reveal

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

Text Inversion

Text Inversion

// 動畫
@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

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

Back to Top

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

Horizontal Scroll Section

Horizontal Scroll Section

Horizontal Scroll Section 示意圖

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

Stacked Cards

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 文章,更詳細內容可點及連結前往查看。

更多資訊公告