Skip to content

貓耳包裝器 wrapper

任何元素包起來都會長出貓耳,讓萬物皆可萌吧!(^・ω・^ )✧

路人:「這是甚麼鬼玩意兒?(っ´Ι`)っ」

鱈魚:「聽說有貓就給讚啊?( •̀ ω •́ )✧」

路人:「只有耳朵算啥貓啊。(˘・_・˘)」

使用範例

基本用法

使用元件包裹後,萬物都會長出貓耳,變得比較討喜(?

耳朵還會自動調整尺寸呦!ˋ( ° ▽、° )

查看範例原始碼
vue
<template>
  <div class="w-full flex flex-col items-center justify-center gap-16 border border-gray-300 py-16">
    <wrapper-cat-ear main-color="#AAA">
      <base-btn
        class=""
        label="按鈕"
      />
    </wrapper-cat-ear>

    <wrapper-cat-ear main-color="#AAA">
      <div class="">
        <img
          class="w-80 rounded"
          src="/codfish.webp"
        >
      </div>
    </wrapper-cat-ear>

    <wrapper-cat-ear main-color="#AAA">
      <div class="border border-[#AAA] rounded">
        <select class="px-10 py-2">
          <option
            disabled
            selected
            label="請選擇貓貓"
          />
          <option label="(>'-'<)ノ" />
          <option label="~(=^‥^)" />
          <option label="/ᐠ。ꞈ。ᐟ\" />
          <option label="(^._.^)ノ" />
          <option label="(^・ω・^ )" />
        </select>
      </div>
    </wrapper-cat-ear>
  </div>
</template>

<script setup lang="ts">
import BaseBtn from '../../base-btn.vue'
import WrapperCatEar from '../wrapper-cat-ear.vue'
</script>

切換動作

可以切換各種不同的動作

查看範例原始碼
vue
<template>
  <div class="h-[50vh] w-full flex flex-col items-center justify-center gap-16 border border-gray-300">
    <wrapper-cat-ear
      :action="action"
      main-color="#999"
    >
      <div class="border-2 border-[#999] rounded">
        <select
          v-model="action"
          class="p-2"
        >
          <option
            v-for="option in options"
            :key="option"
            :value="option"
          >
            {{ option }}
          </option>
        </select>
      </div>
    </wrapper-cat-ear>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { ActionName } from '..'
import WrapperCatEar from '../wrapper-cat-ear.vue'

const action = ref<`${ActionName}`>('relaxed')
const options = Object.values(ActionName)
</script>

調整顏色

調整毛色,個性化貓貓。

◕ ω ◕
◕ ω ◕
◕ ω ◕
> ◕ ω ◕ <

10 隻橘貓 9 隻胖,1 隻超級胖。(o゚v゚)ノ

查看範例原始碼
vue
<template>
  <div class="w-full flex flex-col items-center justify-center gap-10 border border-gray-300 py-10">
    <div class="flex items-center border border-gray-300 rounded p-10">
      <input
        v-model="mainColor"
        type="color"
      >

      <input
        v-model="innerColor"
        type="color"
      >

      <wrapper-cat-ear
        :main-color="mainColor"
        :inner-color="innerColor"
        class="ml-10"
      >
        <div
          class="rounded p-2 px-3 text-white"
          :style="{ backgroundColor: mainColor }"
          v-text="`◕ ω ◕`"
        />
      </wrapper-cat-ear>
    </div>

    <wrapper-cat-ear
      main-color="#3b3b3b"
      inner-color="#ffc2b8"
    >
      <div
        class="rounded bg-[#3b3b3b] p-2 px-3 text-white"
        v-text="`◕ ω ◕`"
      />
    </wrapper-cat-ear>

    <wrapper-cat-ear
      main-color="#03a1fc"
      inner-color="#8f003e"
    >
      <div
        class="rounded bg-[#03a1fc] p-2 px-3 text-white"
        v-text="`◕ ω ◕`"
      />
    </wrapper-cat-ear>

    <wrapper-cat-ear
      main-color="#ff852e"
      inner-color="#ffc2b8"
      class="mt-10"
    >
      <div
        class="rounded bg-[#ff852e] px-20 py-12 text-xl text-white"
        v-text="`> ◕ ω ◕ <`"
      />
    </wrapper-cat-ear>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import WrapperCatEar from '../wrapper-cat-ear.vue'

const mainColor = ref('#b38546')
const innerColor = ref('#ffc2b8')
</script>

互動效果

配合耳朵動作設計各類邏輯,讓互動更加有趣。

嘗試讓滑鼠從遠處慢慢靠近按鈕看看。(. ❛ ᴗ ❛.)

◕ ω ◕
查看範例原始碼
vue
<template>
  <div class="w-full flex flex-col items-center justify-center gap-16 border border-gray-300 py-20">
    <wrapper-cat-ear
      :action="action"
      main-color="#666"
      class="cat border-2 border-[#666] rounded"
    >
      <div
        ref="catRef"
        class="flex items-center justify-center bg-white px-16 py-8"
      >
        <transition
          name="cat"
          mode="out-in"
        >
          <div
            :key="face"
            class="absolute select-none text-nowrap text-2xl font-bold"
            v-text="face"
          />
        </transition>
      </div>
    </wrapper-cat-ear>
  </div>
</template>

<script setup lang="ts">
import type { ActionName } from '..'
import { throttleFilter, useMouseInElement, useMousePressed } from '@vueuse/core'
import { computed, ref } from 'vue'
import { getVectorLength } from '../../../common/utils'
import WrapperCatEar from '../wrapper-cat-ear.vue'

const faceMap: Record<ActionName, string> = {
  peekaboo: '送出',
  relaxed: '◕ ω ◕',
  fear: '´•̥̥̥ ω •̥̥̥`',
  displeased: '˘・ ω ・˘',
  shake: '≧ X ≦',
}

const catRef = ref()
const {
  elementX,
  elementY,
  elementWidth,
  elementHeight,
  isOutside,
} = useMouseInElement(catRef, {
  eventFilter: throttleFilter(35),
})
const { pressed } = useMousePressed()

/** 滑鼠與貓的距離 */
const distance = computed(() => getVectorLength({
  x: elementX.value - elementWidth.value / 2,
  y: elementY.value - elementHeight.value / 2,
}))
/** 煩躁的距離 */
const displeasedDistance = computed(
  () => Math.max(elementWidth.value, elementHeight.value),
)
/** 變成貓的距離 */
const catDistance = computed(
  () => displeasedDistance.value * 2,
)

const action = computed<`${ActionName}`>(() => {
  if (!isOutside.value) {
    return pressed.value ? 'shake' : 'fear'
  }

  if (distance.value > catDistance.value) {
    return 'peekaboo'
  }

  if (distance.value < displeasedDistance.value) {
    return 'displeased'
  }

  return 'relaxed'
})
const face = computed(() => faceMap[action.value])
</script>

<style lang="sass">
.cat
  transition-duration: 0.4s
  box-shadow: 3px 3px 0px 0px rgba(#AAA, 0.8)
  &:hover
    transition-duration: 0.3s
    scale: 1.02
    box-shadow: 5px 5px 0px 0px rgba(#AAA, 0.5)
  &:active
    transition-duration: 0.1s
    scale: 0.95
    animation: shake 0.3s infinite ease-in-out
    box-shadow: 0px 0px 0px 0px rgba(#AAA, 1)

.cat-enter-active, .cat-leave-active
  transition-duration: 0.2s
.cat-enter-from, .cat-leave-to
  transform: translateY(10px)
  opacity: 0 !important
.cat-leave-to
  transform: translateY(-10px)

@keyframes shake
  35%
    rotate: -3deg
  70%
    rotate: 3deg
</style>

調皮的貓貓

搭配調皮的按鈕,貓貓度更高了!ᕕ( ゚ ∀。)ᕗ

儲存
查看範例原始碼
vue
<template>
  <div class="w-full flex flex-col items-center justify-center gap-16 border border-gray-300 py-10">
    <div class="flex flex-col gap-4 border rounded p-4">
      <base-checkbox
        v-model="disabled"
        label="停用"
      />
    </div>

    <btn-naughty
      ref="btnRef"
      :disabled="disabled"
      class="cat-btn select-none"
    >
      <wrapper-cat-ear
        :action="currentAction"
        main-color="#777"
      >
        <div
          class="h-[3rem] w-[6rem] flex-center border border-[#777] rounded bg-white text-xl font-bold"
          v-text="face"
        />
      </wrapper-cat-ear>
    </btn-naughty>
  </div>
</template>

<script setup lang="ts">
import type { ActionName } from '..'
import { refAutoReset, whenever } from '@vueuse/core'
import { computed, ref } from 'vue'
import BaseCheckbox from '../../base-checkbox.vue'
import BtnNaughty from '../../btn-naughty/btn-naughty.vue'
import WrapperCatEar from '../wrapper-cat-ear.vue'

const btnRef = ref<InstanceType<typeof BtnNaughty>>()

const disabled = ref(false)
const action = refAutoReset<`${ActionName}`>('relaxed', 700)
const face = refAutoReset('儲存', 700)

const currentAction = computed(() => {
  if (!disabled.value) {
    return 'peekaboo'
  }

  return action.value
})

whenever(() => btnRef.value?.isRunning, () => {
  face.value = '˘・ ω ・˘'
  action.value = 'displeased'
})
</script>

<style lang="sass">
.cat-btn
  transition-duration: 0.4s
  &:active
    transition-duration: 0.01s
    scale: 0.95

.flex-center
  display: flex
  justify-content: center
  align-items: center
</style>

原理

使用 SVG 繪製耳朵,透過 anime.js 控制耳朵動畫。

📚 anime.js

為甚麼選擇 anime.js?

因為最主流的 GSAP 的 SVG 動畫要收費。

最酷炫的 Motion One 使用上有點小問題,而且資料太少。

最後選擇了資料還算齊全且 SVG 支援度不錯的 anime.js 了。

如果大家有更好的套件,拜託請推薦給我。(´▽`ʃ♡ƪ)

原始碼

API

ActionName

Props

interface Props {
  /** 目前動作 */
  action?: `${ActionName}`;
  /** 主要毛色 */
  mainColor?: string;
  /** 耳朵內部的顏色 */
  innerColor?: string;
}

v0.23.1