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-[20vmin]"
      :facial-expression
      :stroke-color
    />

    <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 class="flex gap-4">
      <base-btn
        label="上一個"
        class="flex-1"
        @click="prev()"
      />
      <base-btn
        label="下一個"
        class="flex-1"
        @click="next()"
      />
    </div>
  </div>
</template>

<script setup lang="ts">
import { useData } from 'vitepress'
import { computed, ref } from 'vue'
import BaseBtn from '../../base-btn.vue'
import { FacialExpression } from '../type'
import UtilCatFace from '../util-cat-face.vue'

const facialExpression = ref<`${FacialExpression}`>('neutral')
const options = Object.values(FacialExpression) as `${FacialExpression}`[]

function next() {
  const index = options.indexOf(facialExpression.value)
  facialExpression.value = options[(index + 1) % options.length] ?? FacialExpression.NEUTRAL
}
function prev() {
  const index = options.indexOf(facialExpression.value)
  facialExpression.value = options[(index - 1 + options.length) % options.length] ?? FacialExpression.NEUTRAL
}

const { isDark } = useData()
const strokeColor = computed(() => isDark.value ? '#ddd' : '#222')
</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-[20vmin]"
        :facial-expression
        :stroke-color
      />
    </div>
  </div>
</template>

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

const { isDark } = useData()
const strokeColor = computed(() => isDark.value ? '#ddd' : '#222')

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

const { state, next } = useCycleList([
  'neutral',
  'confidence',
  '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-[20vmin] cursor-pointer"
      :facial-expression="currentFacialExpression"
      :stroke-color
      @click="handleClick()"
    />

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

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

      <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 { useData } from 'vitepress'
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 { isDark } = useData()
const strokeColor = computed(() => isDark.value ? '#ddd' : '#222')

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,
    FacialExpression.SPEECHLESS,
  ], 1)
  setFacialExpression(type)
}

function handleFocus() {
  const [type] = sample([
    FacialExpression.PLEASANT,
    FacialExpression.EXCITED,
    FacialExpression.CONFIDENCE,
  ], 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.32.3