Skip to content

會眨眼、會揮手、會掀桌的 Logo:拆解顏文字工具站的動態 emoji Logo

會眨眼、會揮手、會掀桌的 Logo:拆解顏文字工具站的動態 emoji Logo

先講個真心話:我這個設計師轉工程師的人,對「小細節」特別沒有抵抗力。今天想跟你分享一個我最近一直回去看的工具站——Kaomoji Garden 顏文字花園。它本身是一個複製貼上用的顏文字 / 西式表情符號(emoticon)資料庫,但真正讓我「哇」出聲的,是它左上角那顆 會動的 Logo

它不是一張 GIF,也不是 SVG 動畫,而是一張用「文字」拼出來的顏文字臉——而且眼睛會飄、會眨、手會揮、心會跳,偶爾還會給你來一個 (╯°□°)╯︵ ┻━┻ 掀桌。每隔幾秒還會換一張臉。我盯著它看了大概兩分鐘,然後就把原始碼挖出來研究了 😂 這篇就帶你一起拆。

↑ 這就是本尊,直接跑在這一頁上——純 CSS 動畫,不是圖片也不是影片。每 3.2 秒換一張臉,把游標移上去還會 wiggle 一下。

上面那顆會動的 Logo,是我把原站的 AnimatedLogo.vue 移植進這篇文章的 live demo:你看到的眨眼、揮手、掀桌全是瀏覽器即時算出來的。接下來就用它來逐層拆解原理。想看它在原站長怎樣,隨時可以開 Kaomoji Garden

先聊它的價值:為什麼要有這種站

顏文字這東西的痛點很實際:你想打一個無奈的 (´・_・),但根本記不起來怎麼組,輸入法也不一定有。Kaomoji Garden 把 1,700 多個顏文字 + 西式表情符號照「情緒」分類,點一下就複製,還能切換日式 ( ´▽`)跟西式:)` 兩種風格、收藏常用的。整站是 Vite + Vue 3 用 vite-ssg 預渲染出來的靜態頁,載入超快、對 SEO 也友善。

工具好用是基本盤,但「願意在 Logo 這種沒人會檢查的地方下苦工」,這件事最打動我。

拆解一:Logo 是「文字」,不是圖片

關鍵檔案在 src/components/AnimatedLogo.vue。第一個讓我意外的設計是——整顆 Logo 是用一個個 <span> 把顏文字拆開來拼的。每一張臉都被「逐字」規劃好:哪個字是會飄的眼睛、哪個是會揮的手、哪個是會閃的星星。

js
// 每張臉都拆成 parts;a = 動畫 class,d = 各部位的延遲,wrap = 整張臉的動畫
const FACES = [
  { parts: [{ t: '(' }, { t: '◕', a: 'a-eyeLR' }, { t: '‿' }, { t: '◕', a: 'a-eyeLRrev' }, { t: ')' }] },
  { parts: [{ t: '(' }, { t: '•', a: 'a-eyeUD' }, { t: 'ω' }, { t: '•', a: 'a-eyeUD', d: '.25s' }, { t: ')' }, { t: 'ノ', a: 'a-hand' }] },
  { parts: [{ t: '(╯°□°)' }, { t: '╯', a: 'a-flip' }, { t: '︵' }, { t: '┻━┻', a: 'a-tbl' }] },
  // …總共 20 張臉
]

你看第一張臉 (◕‿◕),兩顆 分別掛了 a-eyeLRa-eyeLRrev——一個往右飄、一個往左飄(reverse),眼神就「對在一起」活了起來。第二張的右眼還加了 d: '.25s' 延遲,讓兩眼不要同步,看起來更自然。這種「把一張臉當成有部位的角色去拆」的思路,完全是動畫師的腦袋。

模板端就是一個雙層 v-for,把這份資料渲染出來:

vue
<span class="kao-text inline-flex text-brand-500" :class="face.wrap">
  <span
    v-for="(p, i) in face.parts"
    :key="i"
    class="inline-block"
    :class="p.a"
    :style="p.d ? { animationDelay: p.d } : null"
  >{{ p.t }}</span>
</span>

拆解二:動畫全靠純 CSS @keyframes

這裡沒有任何動畫函式庫,全部是 CSS @keyframes。每個部位的「個性」就是一段 keyframe。我特別喜歡這幾個,因為它們很「便宜」(只改 transform / opacity,GPU 友善)卻很有戲:

css
/* 眼睛左右飄:位移才 1.4px,但人眼超敏感,就活了 */
@keyframes eyeLR { 0%, 100% { transform: translateX(-1.4px); } 50% { transform: translateX(1.4px); } }

/* 眨眼:88% 的時間都睜著,只在最後一瞬把 Y 軸壓扁成 0.15 */
@keyframes blink { 0%, 88%, 100% { transform: scaleY(1); } 94% { transform: scaleY(0.15); } }

/* 揮手:邊抬高邊旋轉,再回擺一下 */
@keyframes handWave {
  0%, 100% { transform: translateY(0) rotate(0deg); }
  25% { transform: translateY(-2px) rotate(16deg); }
  60% { transform: translateY(0.5px) rotate(-8deg); }
}

/* 掀桌:桌子被往上甩起來並翻轉 */
@keyframes tableBob { 0%, 100% { transform: translateY(0) rotate(0deg); } 40% { transform: translateY(-3px) rotate(-8deg); } }

兩個「為什麼這樣做」的小心法,我覺得很值得抄走:

  • 位移量都很小(1~3px)。 顏文字本身才十幾 px 大,眼睛動 1.4px 就剛好「活」而不會「脫窗」。動畫不是越大越好,是越貼合尺寸越好。
  • 眨眼要設 transform-origin: center scaleY(0.15) 是把高度壓扁,如果原點在頂端,眼睛會「往下塌」很怪;設成 center 才會像真的閉眼。

而像「縮在一起發抖」(。>﹏<。) 這種整張臉一起抖的,就用 wrap 給整個外層套 a-shake,不必逐字處理。

拆解三:每 3.2 秒換一張臉的「輪播」

Logo 會換臉,靠的是一個很樸素的 setInterval

js
const idx = ref(0) // SSR 階段固定,避免 hydration 不一致
const face = computed(() => FACES[idx.value])

function pickRandom() {
  let n = Math.floor(Math.random() * FACES.length)
  if (n === idx.value) n = (n + 1) % FACES.length // 避免抽到同一張
  idx.value = n
}

onMounted(() => {
  pickRandom()
  timer = setInterval(pickRandom, 3200)
})
onUnmounted(() => clearInterval(timer))

這裡有兩個我很欣賞的細節。第一,idx 初始固定為 0、等 onMounted(也就是在瀏覽器端)才隨機——因為這站是 SSG 預渲染的,伺服器跟瀏覽器算出同一張臉,hydration 才不會打架。第二,換臉時還特地避開「抽到自己」,不然你會看到 Logo「明明換了卻沒變」的尷尬。

新臉出場時還會套一個 Tailwind 定義的 pop-in,輕輕彈一下:

js
'pop-in': {
  '0%': { opacity: '0', transform: 'scale(0.96) translateY(4px)' },
  '100%': { opacity: '1', transform: 'scale(1) translateY(0)' },
}

最後一個,也是我最尊敬的:尊重「不想看動畫」的人

整支元件最後有這段,我給它鼓掌:

css
@media (prefers-reduced-motion: reduce) {
  .logo-float,
  .logo-float * {
    animation: none !important;
  }
}

只要使用者在系統設定裡開了「減少動態效果」(前庭功能敏感、容易頭暈的人會開),所有動畫直接全關。一顆裝飾性的 Logo 都願意做無障礙,這就是把使用者放在心上的態度。整顆 Logo 還加了 aria-hidden="true",讓螢幕報讀器直接略過這串對它沒意義的符號。

收尾

這顆 Logo 的厲害之處,不在用了什麼黑科技,而在它把「文字拆成角色部位 → 純 CSS 給每個部位一段微動畫 → 定時輪播換臉 → 還顧到 SSR 與無障礙」這條鏈路想得很完整。給我們的啟發很簡單:動畫的靈魂在觀察,不在工具。 你不需要 Lottie,幾段 translate 1.4px 的 keyframe 就能讓一張臉活起來。

想親眼看它眨眼、揮手、掀桌,去這裡:kaomoji.pocketool.app,或從工具站入口逛 /tools/kaomoji。記得把游標移到 Logo 上——它還會 wiggle 給你看 😉

我們下篇見,我是卡斯伯。