Skip to content

摺紙轉場 transition

將元素過度變成折紙藝術


大家有玩過 Switch 的遊戲「紙片瑪利歐:摺紙國王」嗎?

整體風格、美術設計、音樂與遊戲性都極具巧思

真的很讚,強力推薦大家玩玩看!ദ്ദി ˉ͈̀꒳ˉ͈́ )

我一直很喜歡裡面的摺紙兵登場的方式,所以將效果實作成過度元件了!( ´ ▽ ` )ノ

技術關鍵字

名稱描述
Babylon.js3D 引擎
DOM to Image將 DOM 元素轉換為圖片的技術,基於 SVG foreignObject 實現
質點彈簧系統把物體離散成質點並以彈簧相連,透過反覆鬆弛約束模擬布料、繩索等柔性材質
Canvas Shader使用 GLSL 開發,直接在 GPU 上執行,比 Canvas 2D API 更快,但也更難
Dynamic Texture可動態修改的材質,常用於即時產生圖案或文字
Vue Transition內建元件,可以處理單個項目的進出動畫
CSS Anchor PositioningCSS 新功能,允許元素相對於其他元素(錨點)進行定位

使用範例

基本用法

用法與 Vue Transition 相同,包住一段被 v-if 控制顯示的內容即可。

查看範例原始碼
vue
<template>
  <div class="example-wrap w-full flex flex-col gap-4">
    <div
      class="example-ctrl grid grid-cols-4 items-center gap-2"
      :class="{ 'pointer-events-none': isTransitioning }"
    >
      <select-stepper
        v-model="shape"
        :label="t('shapeLabel')"
        :options="shapeOptionList"
        :option-label-map="shapeLabelMap"
        class="col-span-4"
      />

      <div
        class="col-span-4 border rounded-lg duration-300"
        :class="{
          'cursor-not-allowed opacity-30': isTransitioning,
          'cursor-pointer': !isTransitioning,
        }"
      >
        <base-checkbox
          v-model="visible"
          :label="t('show')"
          class="w-full cursor-pointer p-4"
        />
      </div>
    </div>

    <div
      class="card-slot flex flex-1 items-center justify-center"
      :class="{ 'pointer-events-none': isTransitioning }"
    >
      <transition-origami
        :shape="shape"
        @enter="isTransitioning = true"
        @leave="isTransitioning = true"
        @after-enter="isTransitioning = false"
        @after-leave="isTransitioning = false"
      >
        <div
          v-if="visible"
          class="card max-w-[90vw] w-80 flex flex-col items-center gap-2 p-6"
        >
          <img
            src="/low/profile.webp"
            alt=""
            class="mb-4 h-[180px] w-[180px] overflow-hidden border-4 border-white rounded-full shadow-xl"
          >

          <div class="text-xl">
            {{ t('codfishName') }}
          </div>

          <p class="text-center text-sm">
            {{ t('body') }}
          </p>
        </div>
      </transition-origami>
    </div>
  </div>
</template>

<script setup lang="ts">
import type { PaperShape } from '../use-paper-solver'
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import BaseCheckbox from '../../base-checkbox.vue'
import SelectStepper from '../../select-stepper.vue'
import TransitionOrigami from '../transition-origami.vue'

const { t } = useI18n()

const isTransitioning = ref(false)
const visible = ref(true)
const shape = ref<PaperShape>('plane')

const shapeOptionList: PaperShape[] = ['plane', 'rocket', 'flapping-bird']
const shapeLabelMap = computed<Record<PaperShape, string>>(() => ({
  'plane': t('plane'),
  'rocket': t('rocket'),
  'flapping-bird': t('bird'),
}))
</script>

<style lang="sass" scoped>
.card-slot
  // 內容消失時會整個從 DOM 移除,沒有這個固定高度版面會塌陷、上下內容跟著跳動
  min-height: 26rem

.card
  // snapdom 快照不含元素邊界外效果,box-shadow 會在摺紙瞬間消失,故不使用
  background: light-dark(#FFFDF5, #3A3830)
  border: 1px solid light-dark(#E4DFCB, #55524A)
</style>

原理

摺紙效果分兩層,外層攔截 Vue Transition 事件、把畫面交給替身,內層是一套獨立的摺紙運算引擎,兩者透過 initenterleave 溝通,概念與 VFX 轉場 同源。

替身定位

Actor 是一顆 Canvas,用 CSS Anchor Positioning 疊在真實內容正上方,尺寸現場量測後放大 6 倍,留出環繞運鏡與飛出畫面的餘裕,快照另外超取樣 2~3 倍。

錨點名稱與定位樣式全寫在行內,不放進全域樣式,免得與其他元件的同名類名在正式版互相覆蓋;瀏覽器不支援錨點定位時,改用現場量測的座標。

摺痕圖固定 3:4,貼圖先鋪滿紙色再把內容以原生比例置中疊上,鏡頭以「內容那一格」對齊;擺向取直放 3:4 與橫放 4:3 之中較接近內容比例者。留白攤平時裁掉,可見範圍與 DOM 完全一致,起摺後才淡入成紙色(paperColor)。

進出場流程

before-enterbefore-leave 掛上 CSS Anchor Name,init(el)snapdom 擷取快照、重建摺痕網格,此刻紙張攤平、與真實內容完全重合。

Leave 沿摺痕收攏成型,鏡頭拉開至環繞視角,摺畢滑出畫面。Enter 從那一格接手,滑入、攤平、鏡頭歸位,最後真實 DOM 淡入。

摺紙運算引擎

質點彈簧運動學求解器,作法與 Origami Simulator 同派。

  • 摺痕圖編譯:讀入山摺、谷摺線段,吸附鄰近頂點、切割交點成平面圖,耳切三角化成可渲染網格
  • 質點彈簧求解:軸向彈簧維持紙張不可拉伸,摺痕依二面角約束朝目標角收斂,並依手摺順序排入時間窗
  • 插畫式紙張渲染:Babylon.js 自訂 shader,Half Lambert 明暗映射成柔和漸層。整套只做減法,最亮處保持快照原色,文字才讀得清楚
  • 疊層深度偏移:頂點 shader 依 uv 算固定的 NDC 深度偏移,會疊在一起的紙層必定來自紙上不同位置,前後關係就此釘死

動畫節奏

時長分配、憑空自轉與飛行都由摺痕排程推導,換一份摺法會自動跟著改。

  • 步驟時長走「慢 → 快 → 慢」,以「步驟」為單位取 easeInOutCirc 斜率的倒數當權重,正規化鋪滿整段時間
  • 正轉、倒轉各有自己的時間結構
  • 整體姿態走憑空摺紙,紙一邊摺一邊緩緩轉動,兩個自轉軸各佔一段錯落的時間窗
  • 飛行採指數緩動,飛離用 easeInExpo 先黏在原地再驟然抽離,位移以鏡頭座標給出
  • 每種造型各有飛行性格。紙飛機斜滑出右上方、從畫面另一側進場;火箭垂直升空、邊震邊竄(約 10 Hz),落下照自由落體與回彈建構;紙鶴由左往右橫渡,深度位移與翻滾一律為零
  • 淡出、淡入的時間窗兩個方向分開

原始碼

API

Props

interface Props {
  /** 初次渲染是否播放進場動畫
   * @default false
   */
  appear?: boolean;
  /** 單趟轉場的總時長(摺疊/攤開加上飛行),單位 ms
   * @default 2600
   */
  duration?: number;
  /** 摺紙造型
   * @default 'plane'
   */
  shape?: PaperShape;
  /** 內容填不滿紙面時,補上的紙色(十六進位)
   * @default '#FFFFFF'
   */
  paperColor?: string;
}

Emits

const emit = defineEmits<{
  (e: 'enter'): void;
  (e: 'afterEnter'): void;
  (e: 'leave'): void;
  (e: 'afterLeave'): void;
}>()

Slots

defineSlots<{
  default?: () => unknown;
}>()

v0.79.0