Skip to content
Welcome to vote for your favorite component! You can also tell me anything you want to say! (*´∀`)~♥

Anamorphosis Button button

You need to rotate the button to a certain angle to display it normally, which can effectively block humans ( •̀ ω •́ )✧

Passerby: "Shouldn't it block robots! (╯°Д°)╯︵ ┻━┻"

Example

Basic Usage

Rotate and restore the button!

View Example Source Code
vue
<template>
  <div class="w-full flex flex-col gap-4 border border-gray-200 rounded-xl p-6">
    <div class="flex justify-center">
      <btn-anamorphosis
        :label="t('按鈕')"
        @click="handleClick"
      />
    </div>
  </div>
</template>

<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import BtnAnamorphosis from '../btn-anamorphosis.vue'

const { t } = useI18n()

function handleClick() {
  // eslint-disable-next-line no-alert
  alert(t('點擊成功'))
}
</script>

Principle

The core concept comes from Visual Anamorphosis, where a complete picture is broken and misaligned in 3D space, meaning you can only see the original complete image from a specific viewing angle (i.e. the front view). The implementation details are as follows:

Polygon Slicing and Misalignment:

  • CSS clip-path is used to randomly cut the button into multiple irregular polygon slices (rows and columns are adjustable via rowCount and colCount).
  • Using the CSS 3D transform: translateZ, these slices are distributed across the Z-axis in a staggered manner (depth adjustable via zSpread). When viewed from non-front angles, the slices look shattered due to parallax. ◝( •ω• )◟

Cursor Lock and Perspective Rotation:

  • Using VueUse's usePointerLock and useMouse, the cursor is locked when the user presses (pointerdown) the element, and the relative movement of the cursor (movementX, movementY) is captured.
  • These movements are converted into the 3D rotation angles of the component container (rotateX, rotateY), allowing users to manually "rotate" this broken object.

Visual Interference:

  • To increase difficulty and blocking effect, in addition to the main slices, multiple sets of "interference slices" rotated at different angles have been duplicated.
  • As the user rotates the object closer to the target angle (rotateX and rotateY approach a multiple of 0), the opacity (noiseOpacity) of these interference slices decreases correspondingly, serving as visual feedback to guide users to the correct angle.

Auto-snap and Unlock:

  • When the difference between the current angle and the front view is extremely small (interference slice opacity is extremely low), the cursor lock is automatically released, triggering the "auto-align" (isAligning), forcing the rotation angle to 0 smoothly via CSS Transition.
  • Once the alignment animation finishes (isDone), all spatial misalignment effects and noise are hidden. Only then does the true underlying button lose the pointer-events-none state, allowing users to interact with it, such as clicking.

Source Code

API

Props

interface Props {
  label?: string;
  /** 行數 */
  rowCount?: number;
  /** 列數 */
  colCount?: number;
  /** 切片之間的 Z 軸間距 (px) */
  zSpread?: number;
}

Emits

const emit = defineEmits<{
  click: [];
  unlock: [];
}>()

Methods

interface Expose {
  init: () => void;
}

Slots

interface Slots {
  default?: () => unknown;
}

v0.58.0