Dimension Blade Guidelines
Everything in this block is marked with v-dimension-blade; swing fast enough and it splits. =͟͟͞͞( •̀д•́)
Swing fast enough and you cleave the world. (「・ω・)「
The cutting idea extends from Brittle Wrapper's "DOM-to-image, then split" approach, but this time fragments are clipped along the blade line with CSS clip-path.
Mouse devices only
Dragging conflicts with scrolling on touch devices, so the component disables itself entirely there — no effect at all.
| Name | Description |
|---|---|
| Vue Directive | Custom Vue directives that encapsulate DOM manipulation logic and repeated behaviors |
| DOM to Image | Converts DOM elements into images, based on SVG foreignObject |
| CSS clip-path | Clips an element to a geometric path, commonly used for masks, cuts, and shaping |
| Anime.js | Lightweight JavaScript animation library |
| Vector Math | Math operations for direction, acceleration, velocity, and more |
| Pointer Events | Detects pointer movement, clicks, hovers, and more, providing coordinates and target information |
| JS Animation | JavaScript-driven animation for more complex, precise control; popular libraries include GSAP and anime.js |
Mount cursor-dimension-blade to enable the blade, then mark cuttable elements with v-dimension-blade.
Swing the cursor fast enough to strike — when the blade line sweeps a marked element, it cracks apart along the line. ( •̀ ω •́ )✧
Everything in this block is marked with v-dimension-blade; swing fast enough and it splits. =͟͟͞͞( •̀д•́)
<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>Responsibilities split three ways: the v-dimension-blade directive only registers targets, the cutting machinery lives in a shared controller, and the cursor-dimension-blade component drives the slash trail and the trigger.
mounted registers the element and params with the controller, updated refreshes params, unmounted removes it — it never touches cutting or animation itself.Map. Cutting borrows the "DOM-to-image, then split" idea from Brittle Wrapper, rendering polygon-clipped fragments with CSS clip-path — no PixiJS, so it stays lightweight.useMouse, measures swing speed with useMouseVelocity, and teleports a full-screen slash canvas to the body — the cursor itself is left untouched.The swing-and-cut runs in four steps. (ง •̀_•́)ง
Each frame the cursor reads the speed; only above speedThreshold does it count as "swinging" and push the point into the trail list. The trail is redrawn every requestAnimationFrame: the whole path expands along its normals into a single tail-tapered, head-wide ribbon polygon (quadratic curves smooth the outline), filled in three layers — dark underlay, soft glow, bright core — for a seamless Fruit-Ninja streak that reads clearly on both light and dark backgrounds. (๑•̀ㅂ•́)و✧
While swinging, the segment from the previous point to the current one becomes the blade line, handed to the controller. It uses the Liang–Barsky algorithm to test whether the line sweeps each element's rectangle, and cuts on a hit. ( •̀ω•́ )✧
The first cut captures the element's current look with snapdom. The fragment layer is absolutely positioned in document coordinates (so it scrolls with the page), and the blade line splits the element rectangle into two polygons (Sutherland–Hodgman); each embeds the same snapshot plus a clip-path. The original is hidden as the fragments seamlessly take over. (・∀・)
The two halves shift slightly apart along the blade line's normal with Anime.js, revealing a crack — no falling.
The same element can be cut again: each swing splits the swept fragments in two, splintering further (with a cooldown and a fragment cap, so one swing never shreds it frame by frame). (๑•̀ㅂ•́)
Calling restore() slides every fragment back along its path; the fragments are pixel-identical to the original, so the swap on contact is invisible — no original fading back in. With auto-restore on, cracks close automatically after each cut.
When the color theme switches, fragment snapshots hold stale colors. The controller watches the html element's class/data-theme and prefers-color-scheme, and instantly hands back the original so no fragment lingers in the old palette.
Directive params for marking elements:
/** 標記元素的次元刀參數 */
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;
}