Cat Face util
A variety of facial expressions to bring more emotional value to your website.ヾ(◍'౪`◍)ノ゙
Usage Examples
Basic Usage
You can switch between various vivid expressions.
View example source code
<template>
<div class="w-full flex flex-col gap-4 border border-gray-200 rounded-xl p-6">
<util-cat-face
class="h-[20vmin]"
:facial-expression
:stroke-color
/>
<select-stepper
v-model="facialExpression"
:options
class="w-full"
/>
</div>
</template>
<script setup lang="ts">
import { useData } from 'vitepress'
import { computed, ref } from 'vue'
import SelectStepper from '../../select-stepper.vue'
import { FacialExpression } from '../type'
import UtilCatFace from '../util-cat-face.vue'
const facialExpression = ref<`${FacialExpression}`>('neutral')
const options = Object.values(FacialExpression) as `${FacialExpression}`[]
const { isDark } = useData()
const strokeColor = computed(() => isDark.value ? '#FAFAFA' : '#222')
</script>Simple Interaction
Interactive behavior that responds to user actions.
View example source code
<template>
<div class="w-full flex-center gap-4 border border-gray-200 rounded-xl 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>Forms
Packed with emotional value so users can better relate.
So fun that the form never gets finished.ᕕ( ゚ ∀。)ᕗ
View example source code
<!-- #region main-code -->
<template>
<div class="w-full flex flex-col items-center gap-4 border border-gray-200 rounded-xl 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="t('姓名')"
required
@invalid="handleInvalid"
@focus="handleFocus()"
/>
<base-input
v-model="form.phone"
:label="t('電話')"
pattern="09\d{8}"
required
:placeholder="t('必須為 09 開頭的 10 位數字')"
@invalid="handleInvalid"
@focus="handleFocus()"
/>
<base-btn
type="submit"
:label="t('送出')"
/>
<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">
{{ t('表單已送出!(*´∀`)~♥') }}
</span>
<span class="cursor-pointer text-xs">
{{ t('點一下再來一次') }}
</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 { useI18n } from 'vue-i18n'
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 { t } = useI18n()
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>
<!-- #endregion main-code -->
<i18n lang="json">
{
"zh-hant": {
"姓名": "姓名",
"電話": "電話",
"送出": "送出",
"表單已送出!(*´∀`)~♥": "表單已送出!(*´∀`)~♥",
"點一下再來一次": "點一下再來一次",
"必須為 09 開頭的 10 位數字": "必須為 09 開頭的 10 位數字"
},
"en": {
"姓名": "Name",
"電話": "Phone",
"送出": "Submit",
"表單已送出!(*´∀`)~♥": "Form submitted! (*´∀`)~♥",
"點一下再來一次": "Click to try again",
"必須為 09 開頭的 10 位數字": "Must be 10 digits starting with 09"
}
}
</i18n>How It Works
This component experiments with slightly more complex SVG animations, ensuring smooth transitions between facial expressions.
In real-world projects, we recommend using Lottie or Rive, which are more powerful and easier to develop with.
Lottie has been around longer, has a larger community, and offers marketplaces for assets. Rive is more powerful, with features like state machines that allow complex interactions, but there are fewer ready-made assets available.
Source Code
API
Props
interface Props {
facialExpression?: `${FacialExpression}`;
strokeColor?: string;
/** 眼睛追蹤偏移半徑 */
eyeOffsetRadius?: number;
}Emits
interface Emits {
change: [];
}