次元刀使用須知
此區塊內容皆已標記 v-dimension-blade,游標揮得夠快就能切開。=͟͟͞͞( •̀д•́)
手速夠快就能斬開世界。(「・ω・)「
切割原理延伸自易碎包裝器的「DOM 轉圖片再分割」,不過這次改以 CSS clip-path 沿刀線裁切碎塊。
僅支援滑鼠裝置
觸控裝置的拖曳與捲動衝突,元件會自動停用,不會有任何效果。
| 名稱 | 描述 |
|---|---|
| Vue Directive | 自定義 Vue 指令,用於封裝 DOM 操作邏輯和重複行為 |
| DOM to Image | 將 DOM 元素轉換為圖片的技術,基於 SVG foreignObject 實現 |
| CSS clip-path | 以幾何路徑裁切元素的可見區域,常用於遮罩、切割與造型 |
| Anime.js | 輕量級 JavaScript 動畫函式庫 |
| 向量計算 | 處理方向、加速度、速度等等數學運算 |
| Pointer 事件 | 偵測滑鼠或觸控點移動、點擊、懸停等等事件,取得座標、目標等等資訊 |
| JS 動畫 | 基於 JavaScript 實現的動畫,達成更複雜、精準的動畫控制,常見套件有 GSAP、anime.js 等 |
掛上 cursor-dimension-blade 元件啟用次元刀,再用 v-dimension-blade 標記可被切割的元素。
游標揮得夠快就可以出刀,刀線掃過標記元素就會沿刀線裂開。( •̀ ω •́ )✧
此區塊內容皆已標記 v-dimension-blade,游標揮得夠快就能切開。=͟͟͞͞( •̀д•́)
<template>
<div class="example-wrap flex flex-col items-center gap-6">
<div class="flex flex-wrap items-center justify-center gap-4">
<!-- 啟用後快速揮動游標才會出刀 -->
<base-checkbox
v-model="bladeEnabled"
:label="t('enable')"
class="example-ctrl"
/>
</div>
<!-- 標了 v-dimension-blade 的元素才切得動 -->
<section class="article flex flex-col gap-4">
<h4
v-dimension-blade
class="article-title"
>
{{ t('articleTitle') }}
</h4>
<p
v-dimension-blade
class="article-text"
>
{{ t('articleText') }}
</p>
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2">
<div
v-for="card in cardList"
:key="card.key"
v-dimension-blade
class="info-card flex flex-col gap-1"
>
<span class="info-card-title">{{ t(`${card.key}.title`) }}</span>
<span class="info-card-text">{{ t(`${card.key}.text`) }}</span>
</div>
</div>
</section>
<cursor-dimension-blade
v-if="bladeEnabled"
ref="bladeRef"
/>
</div>
</template>
<script setup lang="ts">
import { ref, useTemplateRef } from 'vue'
import { useI18n } from 'vue-i18n'
import BaseCheckbox from '../../base-checkbox.vue'
import CursorDimensionBlade from '../cursor-dimension-blade.vue'
import { vDimensionBlade } from '../v-dimension-blade'
const { t } = useI18n()
const bladeEnabled = ref(false)
const bladeRef = useTemplateRef('bladeRef')
const cardList = [
{ key: 'cardSpeed' },
{ key: 'cardTrail' },
]
</script>
<style scoped lang="sass">
.hint
font-size: 0.9rem
opacity: 0.75
.article
width: 100%
padding: 1.5rem
border: 1px solid rgba(125, 125, 125, 0.25)
border-radius: 1rem
background: var(--vp-c-bg-soft)
.article-title
margin: 0
font-size: 1.15rem
font-weight: 700
.article-text
margin: 0
font-size: 0.9rem
line-height: 1.8
opacity: 0.85
.info-card
padding: 1rem
border-radius: 0.75rem
background: var(--vp-c-bg)
.info-card-title
font-size: 0.9rem
font-weight: 700
.info-card-text
font-size: 0.8rem
line-height: 1.6
opacity: 0.75
</style>職責切成三塊,v-dimension-blade 指令只做登記,切割機構集中在共享 controller,cursor-dimension-blade 元件掌控刀痕與觸發。
mounted 登記元素與參數至 controller、updated 更新參數、unmounted 註銷,本身不碰切割與動畫。Map 保管所有標記元素。切割沿用易碎包裝器「DOM 轉圖片再分割」的想法,但改以多邊形裁切搭配 CSS clip-path 呈現碎塊,毋須 PixiJS,載入輕量。useMouse 取得座標、useMouseVelocity 算揮動速度,teleport 一層全螢幕刀痕畫布至 body,不改變游標本身。揮刀到切割,一共四步。(ง •̀_•́)ง
游標每幀讀取速度,超過 speedThreshold 才算「揮動中」,將座標點推入刀痕點列。刀痕以 requestAnimationFrame 逐幀重繪,整段路徑沿法線展開成「尾尖頭寬」的緞帶多邊形(quadratic curve 平滑輪廓),疊暗色襯底、柔光、亮芯三層,做出水果忍者般的刀光,無接縫、深淺背景都清晰。(๑•̀ㅂ•́)و✧
揮動中,取「上一刀點到目前點」的線段當刀線,交給 controller。controller 以 Liang–Barsky 演算法判斷刀線是否掃過各元素矩形,命中就開切。( •̀ω•́ )✧
首刀以 snapdom 擷取元素當下外觀為快照,碎塊圖層以文件座標絕對定位(跟著頁面捲動),再以刀線將元素矩形裁成兩塊多邊形(Sutherland–Hodgman),各嵌同一張快照並套 clip-path。原件隨即藏起,碎塊無縫接手。(・∀・)
兩半藉由 Anime.js 產生動畫,沿刀線法線反向微微錯開,露出裂縫。
同一元素能再切,每刀將掃過的碎塊再裂成兩半,越切越碎。有冷卻時間與塊數上限,不會一次揮動就逐幀切成碎屑。(๑•̀ㅂ•́)
呼叫 restore() 時碎塊沿原路閉合。碎塊與原件像素一致,接合到位直接交換,沒有原件淡入的突兀感。開 auto-restore 則每刀後自動閉合。
切換深淺色主題時,碎塊快照配色會過時。controller 監看 html 的 class/data-theme 與 prefers-color-scheme 變化,立即交還原件,避免碎塊殘留舊配色。
標記元素的指令參數:
/** 標記元素的次元刀參數 */
export interface DimensionBladeParams {
/** 暫時免疫,`true` 時次元刀切不動此元素。@default false */
invincible?: boolean;
}interface Props {
/** 揮動速度門檻(px/ms),超過才留下刀痕並切割。@default 5 */
speedThreshold?: number;
/** 刀痕粗細(px)。@default 14 */
trailWidth?: number;
/** 刀痕顏色。@default '#e8f6ff' */
slashColor?: string;
/** 切開後自動復原,關閉時需呼叫 `restore()` 手動復原。@default false */
autoRestore?: boolean;
/** 自動復原前的等待時間(ms),僅 autoRestore 開啟時有效。@default 900 */
restoreDelay?: number;
/** 單一元素碎塊數上限,達上限後不再切碎。@default 24 */
maxFragmentCount?: number;
/** 疊放層級。@default 2147483646 */
zIndex?: number;
}interface Emits {
/** 揮出一刀且確實切到元素時觸發,帶當刀切到的元素數量 */
slice: [count: number];
}interface Expose {
/** 復原所有被切開的元素 */
restore: () => void;
}