次元刀使用須知
本頁內容皆已標記 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"
/>
<base-btn
:label="t('restore')"
@click="bladeRef?.restore()"
/>
</div>
<p class="hint">
{{ t('hint') }}
</p>
<!-- 標了 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 BaseBtn from '../../base-btn.vue'
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 平滑輪廓),疊暗色襯底、柔光、亮芯三層填色,做出水果忍者的刀光,無分節接縫、深淺色背景都清晰。clip-path。原件當下藏起,碎塊無縫接手。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;
}