Skip to content
歡迎來票選你最喜歡的元件! 也可以告訴我任何你想說的話喔!(*´∀`)~♥

歪像按鈕 button

需要將按鈕旋轉至某個角度,才能正常顯示,可以有效防堵人類 ( •̀ ω •́ )✧

路人:「防機器人才對吧!(╯°Д°)╯︵ ┻━┻」

技術關鍵字

名稱 描述
Pointer 事件偵測滑鼠或觸控點移動、點擊、懸停等等事件,取得座標、目標等等資訊
JS 動畫基於 JavaScript 實現的動畫,達成更複雜、精準的動畫控制,常見套件有 GSAP、anime.js 等
CSS 3D使用 CSS transform 和 perspective 實現 3D 效果

使用範例

基本用法

將按鈕轉回原樣吧!◝( •ω• )◟

查看範例原始碼
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>

取消訂閱

避免用戶落跑,避免用戶誤觸的貼心設計。◝( •ω• )◟

感謝您訂閱超級會員!

每月將自動扣款 $999

如果您真的不想再支持我們了,您可以嘗試點擊這裡...
(視角轉正才能拼起來點擊哦)

查看範例原始碼
vue
<template>
  <div
    class="relative w-full flex flex-col items-center justify-center gap-6 overflow-hidden border border-gray-200 rounded-xl p-6"
  >
    <div class="text-center">
      <p class="mb-2 text-2xl font-bold">
        {{ t('感謝您訂閱超級會員!') }}
      </p>
      <p class="">
        {{ t('每月將自動扣款 $999') }}
      </p>
    </div>

    <button
      class="transform rounded-full from-green-400 to-green-600 bg-gradient-to-r px-10 py-4 text-lg text-white font-bold shadow-lg transition active:scale-95 hover:scale-105 hover:from-green-500 hover:to-green-700"
      @click="handleUpgrade"
    >
      🌟 {{ t('升級為終身會員 ($9999)') }} 🌟
    </button>

    <div class="mt-3 flex flex-col items-center gap-4 text-center">
      <p class="text-xs text-gray-400">
        {{ t('如果您真的不想再支持我們了,您可以嘗試點擊這裡...') }}<br>
        <span class="text-[10px] text-gray-400">
          ({{ t('視角轉正才能點擊哦') }})
        </span>
      </p>

      <btn-anamorphosis
        ref="btnRef"
        @click="handleUnsubscribe"
      >
        <button
          class="rounded bg-gray-100 px-3 py-1 text-xs text-gray-400 underline transition-colors hover:bg-gray-200 hover:text-gray-600"
        >
          {{ t('取消訂閱') }}
        </button>
      </btn-anamorphosis>
    </div>

    <transition name="opacity">
      <div
        v-if="isSubmitted"
        class="absolute inset-0 z-[40] flex flex-col items-center justify-center gap-6 rounded-xl bg-slate-800 bg-opacity-95 text-white"
        @click="reset"
      >
        <span class="text-xl font-bold tracking-wide">
          {{ t('退訂成功') }}
        </span>

        <span class="cursor-pointer text-xs text-gray-400 transition-colors hover:text-white">
          {{ t('點一下再來一次') }}
        </span>
      </div>
    </transition>
  </div>
</template>

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

const { t } = useI18n()

const btnRef = useTemplateRef('btnRef')
const isSubmitted = ref(false)

function handleUnsubscribe() {
  isSubmitted.value = true
}

function handleUpgrade() {
  open('https://codlin.me', '_blank')
}

function reset() {
  isSubmitted.value = false
  btnRef.value?.init()
}
</script>

<style lang="sass" scoped>
.opacity-enter-active, .opacity-leave-active
  transition-duration: 0.4s
.opacity-enter-from, .opacity-leave-to
  opacity: 0 !important
</style>

原理

核心概念源自視覺歪像(Anamorphosis),透過將完整的畫面打碎並在 3D 空間中錯位排列,只有在特定的觀察角度下才能看到原本完整的模樣。實作細節如下:

多邊形切片與錯位

  • 使用 CSS clip-path 將按鈕隨機切割成多個不規則的多邊形切片(透過 rowCountcolCount 調整行列數量)。
  • 透過 CSS 3D 的 transform: translateZ 將這些切片在 Z 軸上前後錯開分布(透過 zSpread 調整深度)。在非正面的角度觀看時,切片之間會因為視差而支離破碎。◝( •ω• )◟

游標鎖定與視角旋轉

  • 藉由 VueUse 的 usePointerLockuseMouse,在使用者按下(pointerdown)元素時鎖定游標,並擷取游標的相對移動量(movementXmovementY)。
  • 將這些移動量轉換為元件容器的 3D 旋轉角度(rotateXrotateY),讓使用者能親手「轉動」這個破碎的物件。

視覺干擾

  • 為了增加難度與防堵效果,除了主切片外,還複製了多組旋轉過角度的「干擾切片」。
  • 隨著使用者將物件旋轉到越接近目標角度(rotateXrotateY 趨近於 0 的倍數),這些干擾切片的透明度(noiseOpacity)會隨之降低,作為視覺回饋引導使用者找到正確的角度。

自動吸附歸位與解鎖

  • 當旋轉角度與正面差異極小(干擾切片透明度極低)時,自動解除游標鎖定,並觸發「自動對齊」(isAligning),透過 CSS Transition 緩動強制歸零轉角。
  • 對齊動畫結束後(isDone),隱藏所有空間錯位特效與雜訊,此時底下真正的按鈕才會解除 pointer-events-none 狀態,讓使用者得以進行點擊等互動。

原始碼

API

Props

interface Props {
  label?: string;
  /** 行數 */
  rowCount?: number;
  /** 列數 */
  colCount?: number;
  /** 切片之間的 Z 軸間距 (px) */
  zSpread?: number;
  /** 旋轉接近歸零時,是否淡出干擾切片。設為 false 則干擾切片永遠不透明 */
  noiseFadeOut?: boolean;
}

Emits

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

Methods

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

Slots

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

v0.60.0