Dimension Blade Guidelines
Everything on this page is marked with v-dimension-blade; swing fast enough and it splits. Slow movement never cuts, so reading is safe — flick the cursor whenever the urge strikes.
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.
Swing the cursor fast to crack the content below along the blade line; press Restore to close the crack ⚔️
Everything on this page is marked with v-dimension-blade; swing fast enough and it splits. Slow movement never cuts, so reading is safe — flick the cursor whenever the urge strikes.
<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>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:
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.clip-path. The original is hidden as the fragments seamlessly take over.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 changes, 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;
}