Skip to content

貓臉 util

各種表情變化,讓網站提供更多情緒價值。ヾ(◍'౪`◍)ノ゙

使用範例

基本用法

可以切換各種生動的表情。

查看範例原始碼
vue
<template>
  <div class="w-full flex flex-col gap-4 border border-gray-300 p-6">
    <util-cat-face
      class="h-[14vh]"
      :facial-expression
    />

    <div class="border-2 border-[#999] rounded">
      <select
        v-model="facialExpression"
        class="w-full p-2"
      >
        <option
          v-for="option in options"
          :key="option"
          :value="option"
        >
          {{ option }}
        </option>
      </select>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { FacialExpression } from '../type'
import UtilCatFace from '../util-cat-face.vue'

const facialExpression = ref<`${FacialExpression}`>('neutral')
const options = Object.values(FacialExpression)
</script>

簡單互動

配合使用者動作互動

查看範例原始碼
vue
<template>
  <div class="w-full flex-center gap-4 border border-gray-300 p-6">
    <div
      ref="faceRef"
      class="cursor-pointer"
    >
      <util-cat-face
        class="h-[14vh]"
        :facial-expression
      />
    </div>
  </div>
</template>

<script setup lang="ts">
import type { FacialExpression } from '../type'
import { useCycleList, useIntervalFn, useMouseInElement, useMousePressed } from '@vueuse/core'
import { computed, reactive, ref } from 'vue'
import UtilCatFace from '../util-cat-face.vue'

const faceRef = ref<HTMLDivElement>()
const mouseInElement = reactive(useMouseInElement(faceRef))
const { pressed: isPressed } = useMousePressed()

const { state, next } = useCycleList([
  'neutral',
  'angry',
  'pleasant',
  'derpy',
] satisfies `${FacialExpression}`[])

useIntervalFn(next, 3000)

const facialExpression = computed<`${FacialExpression}`>(() => {
  if (isPressed.value && !mouseInElement.isOutside) {
    return 'excited'
  }

  if (!mouseInElement.isOutside) {
    return 'happy'
  }

  return state.value
})
</script>

表單

滿滿的情緒價值,讓使用者更有共鳴。

玩到表單永遠填不完。ᕕ( ゚ ∀。)ᕗ

查看範例原始碼
vue
<template>
  <div class="w-full flex flex-col items-center gap-4 border border-gray-300 p-6">
    <util-cat-face
      class="h-[10vh] cursor-pointer"
      :facial-expression="currentFacialExpression"
      @click="handleClick()"
    />

    <form
      class="relative flex flex-col gap-4 p-8"
      @submit="handleSubmit"
    >
      <base-input
        v-model="form.name"
        label="姓名"
        required
        @invalid="handleInvalid"
        @blur="handleBlur(form.name)"
      />

      <base-input
        v-model="form.phone"
        label="電話"
        pattern="09\d{8}"
        required
        placeholder="必須為 09 開頭的 10 位數字"
        @invalid="handleInvalid"
        @blur="handleBlur(form.phone)"
      />

      <base-btn
        type="submit"
        label="送出"
      />

      <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-600 bg-opacity-90 text-white"
          @click="reset"
        >
          <span class="text-xl tracking-wide">
            表單已送出!(*´∀`)~♥
          </span>

          <span class="cursor-pointer text-xs">
            點一下再來一次
          </span>
        </div>
      </transition>
    </form>
  </div>
</template>

<script setup lang="ts">
import { refAutoReset } from '@vueuse/core'
import { sample } from 'remeda'
import { computed, ref } from 'vue'
import BaseBtn from '../../base-btn.vue'
import BaseInput from '../../base-input.vue'
import { FacialExpression } from '../type'
import UtilCatFace from '../util-cat-face.vue'

const form = ref({
  name: '',
  phone: '',
})
const isSubmitted = ref(false)

const facialExpression = refAutoReset(FacialExpression.NEUTRAL, 600)
function setFacialExpression(type: FacialExpression) {
  if (facialExpression.value !== FacialExpression.NEUTRAL) {
    return
  }
  facialExpression.value = type
}

const currentFacialExpression = computed(() => {
  if (isSubmitted.value) {
    return FacialExpression.HAPPY
  }

  return facialExpression.value
})

function handleClick() {
  const [type] = sample([
    FacialExpression.DERPY,
  ], 1)
  setFacialExpression(type)
}

function handleInvalid() {
  const [type] = sample([
    FacialExpression.SAD,
    FacialExpression.SURPRISED,
  ], 1)
  setFacialExpression(type)
}

function handleBlur(value?: any) {
  if (!value) {
    const [type] = sample([
      FacialExpression.ANGRY,
      FacialExpression.SPEECHLESS,
    ], 1)
    setFacialExpression(type)
    return
  }

  const [type] = sample([
    FacialExpression.PLEASANT,
    FacialExpression.EXCITED,
  ], 1)
  setFacialExpression(type)
}

function handleSubmit(evt: Event) {
  evt.preventDefault()
  isSubmitted.value = true
}

function reset() {
  isSubmitted.value = false

  form.value = {
    name: '',
    phone: '',
  }
}
</script>

<style lang="sass" scoped>
.rubbing
  padding: 0.75rem
  color: #ff7530
  opacity: 0.8
  border: 1px dashed #ff7530
  border-radius: 0.2rem
  white-space: nowrap
  text-align: center

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

原理

嘗試複雜一點的 svg 動畫,表情變化皆有完整的過度效果。

實務上推薦使用 LottieRive,功能更強更容易開發。

Lottie 比較早出來,社群較大,有素材網站販售;Rive 功能更強,有狀態機等功能,可以做出複雜互動,但是素材比較少。

原始碼

API

Props

interface Props {
  facialExpression?: `${FacialExpression}`;
  strokeColor?: string;
  /** 眼睛追蹤偏移半徑 */
  eyeOffsetRadius?: number;
}

Emits

interface Emits {
  change: [];
}

Methods

defineExpose({})

v0.28.0