iT邦幫忙

2026 iThome 鐵人賽

DAY 11
0
Vibe Coding

Vibe Coding的30天,自然語言與AI共舞,從Prompt到高品質原型落地系列 第 11

Day 11|動畫與細節修飾用自然語言描述微互動

  • 分享至 

  • xImage
  •  

在Day10中,我們完成了MockData的動態渲染與各種狀態(EmptyState,Loading)的處理。然而,當你操作一個純靜態的畫面時,無論顏色配得多好看,總會有一股硬邦邦的草稿感。

微互動(Micro-interactions)與過渡動畫,正是拉開普通原型與高質感產品差距的最後10%。

今天的目標,是學習如何將人類對視覺物理感官的描述(彈性、錯落登場、微懸浮),精準轉譯為AI能識別的FramerMotion或CSSAnimation指令,為VibePulse的技能卡片與列表注入生命力!

1.視覺語言翻譯手冊:將物理感轉譯為AI能懂的詞彙
在Prompt中只寫加一點好看的動畫通常效果不好,因為AI無法判斷你想要的風格是商務沉穩還是活潑彈性。

想要精準引導AI,我們需要建立一張視覺體感與動畫參數的對照表:
https://ithelp.ithome.com.tw/upload/images/20260812/20163260340KkcZL9b.png

2.實戰步驟1:安裝FramerMotion與設定基礎動畫Prompt
在React生態系中,FramerMotion是最受歡迎且AI掌握度最高的動畫庫。

步驟A:安裝套件
在Terminal執行:

npm install framer-motion

步驟B:指引AI建立通用FadeIn封裝組件
發送Prompt對著@components/animations/FadeIn.tsx:

請幫我用`framer-motion`建立一個可重用的動畫包覆組件`components/animations/FadeIn.tsx`。

##需求細節:
1.元件屬性(Props):
-`children`:ReactNode
-`delay`:預設0秒
-`direction`:'up'|'down'|'left'|'right'|'none'(預設'up')
-`className`:允許傳入額外Tailwind類別
2.視覺體驗:
-使用輕量平滑的easing(easeInOut,duration:0.4s)。
-位移距離不要太誇張,10px-15px即可,避免過度浮誇。
3.`'useclient'`:必須標記為ClientComponent。

實戰步驟2:升級SkillCardList.tsx的入場與懸停微互動
現在,我們讓Day10完成的SkillCardList.tsx具備:

列表開啟時卡片逐張錯落向上淡入(StaggeredFade-in-up)。

滑鼠懸停時卡片微幅上升+陰影變化(HoverLift)。

點擊時的Q彈壓下(TapBounce)。

發送重構Prompt對著@components/SkillCardList.tsx:

@components/SkillCardList.tsx
請使用`framer-motion`升級`SkillCardList.tsx`的視覺微互動與入場動畫。

##動畫需求說明:
1.容器級動畫(ContainerVariants):
-當元件載入時,發動`staggerChildren:0.08`,讓裡面的卡片依序登場。
2.卡片級動畫(CardVariants):
-Initial:opacity:0,y:15
-Animate:opacity:1,y:0
-Transition:type:"spring",stiffness:260,damping:20
-Hover體驗(`whileHover`):y:-4(微幅上浮),陰影提升(`shadow-md`),縮放`scale:1.01`。
-Press體驗(`whileTap`):`scale:0.98`(給予輕微點擊反饋)。
3.注意事項:
-保留上一版所有的語義化CSS、Badge顏色邏輯與EmptyState。
-請確保動畫不會導致排版位移(LayoutShift)。

AI產出的Framer Motion程式碼:TypeScript'use client';

import React from 'react';
import { motion, Variants } from 'framer-motion';
import { SkillItem, mockSkills } from '@/mocks/skillsData';
import { Badge } from '@/components/ui/badge';
import { BookOpen, Calendar, CheckCircle2, CircleDashed } from 'lucide-react';

// 容器 Variants (處理 stagger 錯落)
const containerVariants: Variants = {
  hidden: { opacity: 0 },
  show: {
    opacity: 1,
    transition: {
      staggerChildren: 0.08,
      delayChildren: 0.05,
    },
  },
};

// 卡片 Variants (彈性物理運動)
const cardVariants: Variants = {
  hidden: { opacity: 0, y: 15 },
  show: {
    opacity: 1,
    y: 0,
    transition: {
      type: 'spring',
      stiffness: 260,
      damping: 20,
    },
  },
};

const categoryStyles: Record<SkillItem['category'], string> = {
  Frontend: 'bg-blue-500/10 text-blue-400 border-blue-500/20',
  Backend: 'bg-purple-500/10 text-purple-400 border-purple-500/20',
  AI: 'bg-emerald-500/10 text-emerald-400 border-emerald-500/20',
  DevOps: 'bg-amber-500/10 text-amber-400 border-amber-500/20',
};

const getProgressColor = (val: number) => {
  if (val >= 80) return 'bg-emerald-500';
  if (val >= 50) return 'bg-blue-500';
  return 'bg-amber-500';
};

export default function SkillCardList() {
  return (
    <motion.div
      variants={containerVariants}
      initial="hidden"
      animate="show"
      className="grid grid-cols-1 md:grid-cols-2 gap-4"
    >
      {mockSkills.map((skill) => (
        <motion.div
          key={skill.id}
          variants={cardVariants}
          whileHover={{ y: -4, scale: 1.01, transition: { duration: 0.2 } }}
          whileTap={{ scale: 0.98 }}
          className="p-5 rounded-xl bg-card border border-border text-card-foreground hover:border-primary/50 transition-colors shadow-sm hover:shadow-md flex flex-col justify-between space-y-4 cursor-pointer"
        >
          {/* Header */}
          <div>
            <div className="flex items-center justify-between gap-2 mb-2">
              <span
                className={`text-xs px-2.5 py-0.5 rounded-full border font-medium ${
                  categoryStyles[skill.category]
                }`}
              >
                {skill.category}
              </span>
              {skill.status === 'mastered' ? (
                <span className="flex items-center text-xs text-emerald-400 gap-1 font-medium">
                  <CheckCircle2 className="w-3.5 h-3.5" /> 已掌握
                </span>
              ) : (
                <span className="flex items-center text-xs text-muted-foreground gap-1">
                  <CircleDashed className="w-3.5 h-3.5 animate-spin-slow" /> 學習中
                </span>
              )}
            </div>
            <h3 className="font-semibold text-base line-clamp-1" title={skill.title}>
              {skill.title}
            </h3>
          </div>

          {/* Progress Section */}
          <div className="space-y-1.5">
            <div className="flex justify-between text-xs font-medium">
              <span className="text-muted-foreground">熟練度標記</span>
              <span className="text-foreground">{skill.proficiency}%</span>
            </div>
            <div className="w-full h-2 rounded-full bg-muted overflow-hidden">
              <motion.div
                className={`h-full ${getProgressColor(skill.proficiency)}`}
                initial={{ width: 0 }}
                animate={{ width: `${skill.proficiency}%` }}
                transition={{ duration: 0.8, delay: 0.2, ease: 'easeOut' }}
              />
            </div>
          </div>

          {/* Tags */}
          <div className="flex flex-wrap gap-1.5">
            {skill.tags.map((tag) => (
              <span
                key={tag}
                className="text-[11px] px-2 py-0.5 rounded-md bg-muted/60 text-muted-foreground"
              >
                #{tag}
              </span>
            ))}
          </div>

          {/* Footer Metrics */}
          <div className="pt-3 border-t border-border flex items-center justify-between text-xs text-muted-foreground">
            <div className="flex items-center gap-1">
              <BookOpen className="w-3.5 h-3.5" />
              <span>{skill.notesCount} 則筆記</span>
            </div>
            <div className="flex items-center gap-1">
              <Calendar className="w-3.5 h-3.5" />
              <span>最近練習:{new Date(skill.lastPracticedAt).toLocaleDateString()}</span>
            </div>
          </div>
        </motion.div>
      ))}
    </motion.div>
  );
}

4.動畫與UX驗收的三大硬指標雖然微互動能大幅提升產品質感,但過度動畫(Over-animation)會帶來嚴重的視覺疲勞與效能問題。在審查AI產出的動畫時,請牢記以下三大指標:優先使用GPU加速屬性:儘量只對opacity與transform(scale,translate,rotate)做動畫。避免頻繁對width、height、margin或padding動畫,否則會觸發瀏覽器的Reflow(重排),導致低配裝置卡頓。掌控動畫時間(Duration<300ms):微互動的靈魂在於不拖泥帶水。按鈕hover或卡片浮動應控制在150ms~250ms之間;頁面入場過渡不宜超過400ms。動態移除的元件搭配AnimatePresence:如果列表卡片支援刪除或過濾,記得請AI加上,這樣卡片消失時才會展現流暢的淡出與縮小離場動畫,而不是瞬間消失。

今天我們解鎖了讓VibeCoding作品躍升為精品級MVP的關牌技能:建立了視覺體感與FramerMotion參數的自然語言翻譯表。成功為SkillCardList加上了錯落登場(Stagger)、彈性物理(Spring)與卡片hover/press微互動。掌握了動態屬性與GPU加速的效能審查原


上一篇
Day 10|Mock Data生成與前端畫面動態渲染
下一篇
Day 12|階段性驗收第一階段MVP畫面展示與Code Review
系列文
Vibe Coding的30天,自然語言與AI共舞,從Prompt到高品質原型落地14
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言