Communitygithub.com

motion-advanced

React / Next.js 的進階動畫模式——拖放、手勢、文字動畫、SVG 路徑繪製、自訂 Hook、指令式序列 (useAnimate)、載入器及完整的 API 決策樹。需先設置 motion-foundations。適用於在 React 或 Next.js 中建構拖放、手勢、文字或 SVG 動畫,以及指令式動畫序列。

motion-advanced 是什麼?

這項技能提供了一系列進階動畫模式,專為 React 和 Next.js 應用程式設計,涵蓋複雜的互動與物理動畫。其核心功能包括拖放互動(如可拖曳卡片、拖曳關閉表單、可重新排序清單)、手勢偵測(滑動、長按)、文字動畫(逐字揭示、打字機效果、數字計數器)、SVG 動畫(路徑繪製、圖示變形、圓形進度條),以及自訂 Hook(如 useScrollReveal、游標跟隨器)。它支援「motion」函式庫的進階 API,例如 useSpring 用於彈性物理動畫、useMotionValue 和 useTransform 用於無渲染的反應式數值計算,以及 useAnimate 用於可中斷的指令式動畫序列。這項技能強調應遵循的原則,包括偏好基於物理的動畫以獲得自然手感、確保動畫值在伺服器端渲染 (SSR) 時安全、以及處理無限循環動畫時需在頁面隱藏時暫停以節省資源。它與 motion-foundations 技能搭配使用,適合在需要超越基礎模式、實現高度自訂且效能優化的動畫場景時啟用。

相容平台~Claude Code~Codex CLI✓Cursor
npx skills add https://github.com/affaan-m/ECC/tree/main/skills/motion-advanced

在你喜歡的 AI 中提問

開啟一個已預先載入此 Agent Skill 的新對話。

說明文件

Motion 進階

複雜、互動式且基於物理的動畫模式。 需先設置 motion-foundations。 當 motion-patterns 不夠用時,請使用此技能。

何時啟用

  • 建構拖曳關閉表單、滑動手勢或可重新排序的清單
  • 以逐字、逐字元或即時計數器方式製作文字動畫
  • 繪製 SVG 路徑、變換圖示或製作圓形進度條動畫
  • 編寫自訂動畫 Hook(useScrollReveal、磁性按鈕、游標跟隨器)
  • 使用 useAnimate 以指令式方式編排多步驟動畫序列
  • 建構旋轉器、微光骨架、脈衝指示器或按鈕載入狀態

產出

此技能產出:

  • 拖曳互動:可拖曳卡片、拖曳關閉表單、Reorder.Group 清單
  • 手勢 Hook:滑動偵測、長按、輪廓縮放
  • 文字動畫元件:逐字揭示、字元打字機、數字計數器
  • SVG 動畫:路徑繪製、圖示變形、筆劃進度環
  • 自訂 Hook:useScrollReveal、useHoverScale、useNavigationDirection、useInViewOnce
  • 透過 useAnimate 實現的指令式序列,具備可中斷的 async/await 支援
  • 載入器元件:旋轉器、微光、脈衝點、進度條、按鈕載入狀態

原則

  • 基於物理的動畫(useSpring、springs.*)在直接操作時,總是比基於時間的動畫更自然。
  • useMotionValue + useTransform 可計算衍生數值而不觸發重新渲染。
  • useAnimate 序列是指令式且可中斷的——在動畫進行中再次呼叫 animate() 會自動取消先前的動畫。
  • 動畫值(useMotionValue、useSpring)對伺服器端渲染 (SSR) 安全,不會導致 hydration 錯誤。

規則

  1. 拖曳互動必須在觸控裝置上測試,而不僅僅是滑鼠。drag 屬性在兩者上皆可運作,但觸感和閾值不同。
  2. 無限循環動畫必須在 document.visibilityState === "hidden" 時暫停。 背景分頁不得消耗 GPU/CPU 資源。
  3. 滑動閾值必須明確指定。 切勿僅憑速度推斷意圖;應結合 offset 和 velocity 檢查。
  4. useAnimate 的作用域 ref 必須附加到已掛載的 DOM 元素上。 在掛載前呼叫 animate() 會靜默失敗。
  5. 動畫值不得在渲染期間重新建立。 在元件本體內使用 useMotionValue(0) 是正確的;在渲染過程中 new MotionValue(0) 則不正確。
  6. 所有令牌值都從 motion-foundations 導入。 不允許內聯數值。
  7. 自訂 Hook 必須處理清理工作。 每個 window.addEventListener 都需要在 useEffect 的回傳中有一個對應的 removeEventListener。
  8. SVG 變形需要相等的路徑指令數量。 具有不同指令結構的路徑會直接跳轉,而不是進行插值。

決策指南

選擇正確的進階 API

情境API
釋放時帶有物理效果的拖曳drag + dragTransition: springs.release
有序的拖曳重排清單Reorder.Group + Reorder.Item
透過拖曳偏移關閉表單drag="y" + onDragEnd 偏移量檢查
向左/向右滑動drag="x" + onDragEnd 偏移量檢查
長按useLongPress Hook
隨時間平滑的數值useSpring
從另一個數值衍生而來的數值useTransform
多步驟序列搭配 async/await 使用的 useAnimate
一次性指令式動畫來自 motion 的 animate()
逐字進入的文字在 inline-block 跨度上使用 Stagger
SVG 繪製pathLength 從 0 到 1
SVG 變形d 屬性補間(指令數量需相等)
圓形進度strokeDashoffset 補間

何時使用 useSpring 與 spring 轉場

useSpringtransition: springs.*
適用於游標跟隨器、指標追蹤數值離散的狀態變化
更新方式連續更新,在每個影格上由狀態變化觸發
中斷行為平滑——物理效果從當前速度繼續從當前數值重新開始

核心概念

useMotionValue + useTransform

無需重新渲染的反應式計算:

const x = useMotionValue(0)
const opacity = useTransform(x, [-200, 0, 200], [0, 1, 0])
// opacity 會隨著 x 的變化而在每個影格更新——無需 setState,無需重新渲染

useAnimate

回傳 [scope, animate]。作用域 ref 必須附加到 DOM 元素上。 animate() 呼叫是可中斷的——在動畫進行中再次呼叫會取消先前的執行。

const [scope, animate] = useAnimate()

async function play() {
  await animate(".step-1", { opacity: 1 }, { duration: 0.3 })
  await animate(".step-2", { x: 0 },       { duration: 0.4 })
        animate(".step-3", { scale: 1 },    { duration: 0.25 })  // 觸發後即忘
}

return <div ref={scope}>...</div>

程式碼範例

可拖曳卡片

"use client"
import { motion } from "motion/react"
import { springs, motionTokens } from "@/lib/motion-tokens"

<motion.div
  drag
  dragConstraints={{ left: -100, right: 100, top: -100, bottom: 100 }}
  dragElastic={0.1}
  whileDrag={{
    scale: motionTokens.scale.pop,
    boxShadow: "0 16px 40px rgba(0,0,0,0.2)",
  }}
  dragTransition={springs.release}
/>

拖曳關閉表單

"use client"
import { motion, useMotionValue, useTransform } from "motion/react"

export function BottomSheet({ onClose }: { onClose: () => void }) {
  const y = useMotionValue(0)
  const opacity = useTransform(y, [0, 200], [1, 0])

  return (
    <motion.div
      drag="y"
      dragConstraints={{ top: 0 }}
      style={{ y, opacity }}
      onDragEnd={(_, info) => {
        // 規則 3:結合 offset + velocity
        if (info.offset.y > 120 || info.velocity.y > 500) onClose()
      }}
    />
  )
}

可重新排序清單

"use client"
import { Reorder } from "motion/react"

export function SortableList() {
  const [items, setItems] = useState(initialItems)
  return (
    <Reorder.Group axis="y" values={items} onReorder={setItems}>
      {items.map((item) => (
        <Reorder.Item key={item.id} value={item}>
          {item.label}
        </Reorder.Item>
      ))}
    </Reorder.Group>
  )
}

滑動偵測

"use client"
import { motion } from "motion/react"

const OFFSET_THRESHOLD  = 50
const VELOCITY_THRESHOLD = 300

<motion.div
  drag="x"
  dragConstraints={{ left: 0, right: 0 }}
  onDragEnd={(_, info) => {
    const swipedRight = info.offset.x > OFFSET_THRESHOLD  || info.velocity.x > VELOCITY_THRESHOLD
    const swipedLeft  = info.offset.x < -OFFSET_THRESHOLD || info.velocity.x < -VELOCITY_THRESHOLD
    if (swipedRight) onSwipeRight()
    if (swipedLeft)  onSwipeLeft()
  }}
/>

長按 Hook

import { useRef } from "react"

export function useLongPress(callback: () => void, ms = 600) {
  const timerRef = useRef<ReturnType<typeof setTimeout>>()
  return {
    onPointerDown:  () => { timerRef.current = setTimeout(callback, ms) },
    onPointerUp:    () => clearTimeout(timerRef.current),
    onPointerLeave: () => clearTimeout(timerRef.current),
  }
}

逐字揭示

"use client"
import { motion } from "motion/react"
import { springs } from "@/lib/motion-tokens"

export function AnimatedText({ text }: { text: string }) {
  return (
    <motion.p
      variants={{ visible: { transition: { staggerChildren: 0.05 } } }}
      initial="hidden"
      animate="visible"
    >
      {text.split(" ").map((word, i) => (
        <motion.span
          key={i}
          className="inline-block mr-1"
          variants={{
            hidden:  { opacity: 0, y: 12 },
            visible: { opacity: 1, y: 0, transition: springs.gentle },
          }}
        >
          {word}
        </motion.span>
      ))}
    </motion.p>
  )
}

數字計數器

"use client"
import { useRef, useEffect } from "react"
import { animate } from "motion"
import { motionTokens } from "@/lib/motion-tokens"

export function Counter({ to }: { to: number }) {
  const nodeRef = useRef<HTMLSpanElement>(null)

  useEffect(() => {
    const controls = animate(0, to, {
      duration: motionTokens.duration.crawl,
      ease: motionTokens.easing.smooth,
      onUpdate: (v) => {
        if (nodeRef.current) nodeRef.current.textContent = Math.round(v).toString()
      },
    })
    return controls.stop   // 規則 7:清理
  }, [to])

  return <span ref={nodeRef} />
}

SVG 路徑繪製

"use client"
import { motion } from "motion/react"
import { motionTokens } from "@/lib/motion-tokens"

<motion.path
  d="M 0 100 Q 50 0 100 100"
  initial={{ pathLength: 0, opacity: 0 }}
  animate={{ pathLength: 1, opacity: 1 }}
  transition={{ duration: motionTokens.duration.slow, ease: motionTokens.easing.smooth }}
/>

筆劃進度環

"use client"
import { motion } from "motion/react"
import { motionTokens } from "@/lib/motion-tokens"

const CIRCUMFERENCE = 2 * Math.PI * 40   // r=40

export function ProgressRing({ progress }: { progress: number }) {
  return (
    <svg width="100" height="100" viewBox="0 0 100 100">
      <circle cx="50" cy="50" r="40" fill="none" stroke="#e5e7eb" strokeWidth="8" />
      <motion.circle
        cx="50" cy="50" r="40"
        fill="none" stroke="#6366f1" strokeWidth="8"
        strokeLinecap="round"
        strokeDasharray={CIRCUMFERENCE}
        animate={{ strokeDashoffset: CIRCUMFERENCE - (progress / 100) * CIRCUMFERENCE }}
        transition={{ duration: motionTokens.duration.normal, ease: motionTokens.easing.smooth }}
        style={{ rotate: -90, transformOrigin: "center" }}
      />
    </svg>
  )
}

useScrollReveal Hook

"use client"
import { useRef } from "react"
import { useScroll, useTransform } from "motion/react"
import { motionTokens } from "@/lib/motion-tokens"

export function useScrollReveal() {
  const ref = useRef(null)
  const { scrollYProgress } = useScroll({ target: ref, offset: ["start end", "end start"] })
  const opacity = useTransform(scrollYProgress, [0, 0.3], [0, 1])
  const y       = useTransform(scrollYProgress, [0, 0.3], [motionTokens.distance.lg, 0])
  return { ref, style: { opacity, y } }
}

// 使用方式
const { ref, style } = useScrollReveal()
<motion.section ref={ref} style={style} />

游標跟隨器

"use client"
import { useEffect } from "react"
import { motion, useMotionValue, useSpring } from "motion/react"
import { springs } from "@/lib/motion-tokens"

export function CursorFollower() {
  const x = useMotionValue(-100)
  const y = useMotionValue(-100)
  const sx = useSpring(x, springs.gentle)
  const sy = useSpring(y, springs.gentle)

  useEffect(() => {
    const move = (e: MouseEvent) => { x.set(e.clientX); y.set(e.clientY) }
    window.addEventListener("mousemove", move)
    return () => window.removeEventListener("mousemove", move)   // 規則 7
  }, [])

  return (
    <motion.div
      className="fixed top-0 left-0 w-6 h-6 rounded-full bg-indigo-500
                 pointer-events-none -translate-x-1/2 -translate-y-1/2 z-50"
      style={{ x: sx, y: sy }}
    />
  )
}

微光骨架

"use client"
import { useEffect } from "react"
import { motion, useAnimation } from "motion/react"
import { motionTokens } from "@/lib/motion-tokens"

export function ShimmerSkeleton({ className = "" }: { className?: string }) {
  const controls = useAnimation()

  useEffect(() => {
    const play = () =>
      controls.start({
        x: ["-100%", "100%"],
        transition: {
          repeat: Infinity,
          duration: motionTokens.duration.crawl,
          ease: motionTokens.easing.linear,
        },
      })

    const handleVisibility = () => {
      if (document.visibilityState === "hidden") controls.stop()
      else void play()
    }

    void play()
    document.addEventListener("visibilitychange", handleVisibility)
    return () => {
      controls.stop()
      document.removeEventListener("visibilitychange", handleVisibility)
    }
  }, [controls])

  return (
    <div className={`relative overflow-hidden bg-gray-200 rounded ${className}`}>
      <motion.div
        className="absolute inset-0 bg-gradient-to-r from-transparent via-white/60 to-transparent"
        initial={{ x: "-100%" }}
        animate={controls}
      />
    </div>
  )
}

按鈕載入狀態

"use client"
import { motion, AnimatePresence } from "motion/react"
import { motionTokens, springs } from "@/lib/motion-tokens"

export function LoadingButton({
  loading,
  label,
  onClick,
}: {
  loading: boolean
  label: string
  onClick: () => void
}) {
  return (
    <motion.button
      onClick={onClick}
      animate={{ opacity: loading ? 0.7 : 1 }}
      whileTap={loading ? {} : { scale: motionTokens.scale.press }}
      transition={springs.snappy}
      disabled={loading}
    >
      <AnimatePresence mode="wait">
        {loading ? (
          <motion.span
            key="loading"
            initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}
            transition={{ duration: motionTokens.duration.fast }}
          >
            …
          </motion.span>
        ) : (
          <motion.span
            key="label"
            initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}
            transition={{ duration: motionTokens.duration.fast }}
          >
            {label}
          </motion.span>
        )}
      </AnimatePresence>
    </motion.button>
  )
}

附帶可見性暫停的無限動畫

"use client"
import { useEffect } from "react"
import { motion, useAnimation } from "motion/react"
import { motionTokens } from "@/lib/motion-tokens"

export function PulseDot() {
  const controls = useAnimation()

  useEffect(() => {
    const pulse = () =>
      controls.start({
        scale: [1, 1.4, 1],
        opacity: [1, 0.6, 1],
        transition: { repeat: Infinity, duration: motionTokens.duration.crawl },
      })

    // 規則 2:分頁隱藏時暫停
    const handleVisibility = () => {
      if (document.visibilityState === "hidden") {
        controls.stop()
      } else {
        void pulse()
      }
    }

    void pulse()
    document.addEventListener("visibilitychange", handleVisibility)
    return () => {
      controls.stop()
      document.removeEventListener("visibilitychange", handleVisibility)
    }
  }, [controls])

  return (
    <motion.div
      className="w-3 h-3 rounded-full bg-indigo-500"
      animate={controls}
    />
  )
}

Individual skills in this repo

This repo contains 20 individual skills — each has its own dedicated page.

accessibility

Design, implement, and audit inclusive digital products using WCAG 2.2 Level AA. Use when building or auditing UI that must meet WCAG 2.2 Level AA, or when reviewing a change for keyboard, contrast, or screen-reader support.

affaan-m/content-engine

Create platform-native content systems for X, LinkedIn, TikTok, YouTube, newsletters, and repurposed multi-platform campaigns. Use when the user wants social posts, threads, scripts, content calendars, or one source asset adapted cleanly across platforms.

affaan-m/ECC

Multi-platform content distribution across X, LinkedIn, Threads, and Bluesky. Adapts content per platform using content-engine patterns. Never posts identical content cross-platform. Use when the user wants to distribute content across social platforms.

affaan-m/fal-ai-media

Unified media generation via fal.ai MCP — image, video, and audio. Covers text-to-image (Nano Banana), text/image-to-video (Seedance, Kling, Veo 3), text-to-speech (CSM-1B), and video-to-audio (ThinkSound). Use when the user wants to generate images, videos, or audio with AI.

affaan-m/manim-video

日本語翻訳:このファイルは manim-video 用の日本語翻訳が必要です

affaan-m/remotion-video-creation

Remotion のベストプラクティス - React で動画を作成する。3D、アニメーション、音声、字幕、チャート、トランジションなどをカバーするドメイン固有の29のルール。

affaan-m/video-editing

AI-assisted video editing workflows for cutting, structuring, and augmenting real footage. Covers the full pipeline from raw capture through FFmpeg, Remotion, ElevenLabs, fal.ai, and final polish in Descript or CapCut. Use when the user wants to edit video, cut footage, create vlogs, or build video content.

agent-architecture-audit

Full-stack diagnostic for agent and LLM applications. Audits the 12-layer agent stack for wrapper regression, memory pollution, tool discipline failures, hidden repair loops, and rendering corruption. Produces severity-ranked findings with code-first fixes. Essential for developers building agent applications, autonomous loops, or any LLM-powered feature. Use when an agent or LLM feature misbehaves and the failing layer is unknown, or before shipping an agent stack.

agent-eval

Head-to-head comparison of coding agents (Claude Code, Aider, Codex, etc.) on custom tasks with pass rate, cost, time, and consistency metrics. Use when choosing between coding agents, or when a change to an agent setup needs measured pass rate, cost, and time rather than an impression.

agent-harness-construction

Design and optimize AI agent action spaces, tool definitions, and observation formatting for higher completion rates. Use when defining or revising an agent

agentic-engineering

Operate as an agentic engineer using eval-first execution, decomposition, and cost-aware model routing. Use when planning or executing engineering work that agents will carry out end to end.

agentic-os

Build persistent multi-agent operating systems on Claude Code. Covers kernel architecture, specialist agents, slash commands, file-based memory, scheduled automation, and state management without external databases. Use when building a persistent multi-agent system on Claude Code with its own memory, commands, and scheduling.

agent-introspection-debugging

Structured self-debugging workflow for AI agent failures using capture, diagnosis, contained recovery, and introspection reports. Use when an agent run fails and you need a reproducible diagnosis instead of a retry.

agent-payment-x402

Add x402 payment execution to AI agents with per-task budgets, spending controls, and non-custodial wallets. Supports Base through agentwallet-sdk and X Layer through OKX Payments / OKX Agent Payments Protocol. Use when an agent must pay for something itself and needs per-task budgets, spending controls, and a non-custodial wallet.

agent-self-evaluation

Use after completing any non-trivial task. The agent self-rates its output on 5 axes — accuracy, completeness, clarity, actionability, conciseness — with concrete evidence per criterion. Produces a structured 1-5 scorecard with specific improvement suggestions.

agent-sort

Build an evidence-backed ECC install plan for a specific repo by sorting skills, commands, rules, hooks, and extras into DAILY vs LIBRARY buckets using parallel repo-aware review passes. Use when ECC should be trimmed to what a project actually needs instead of loading the full bundle.

ai-first-engineering

Engineering operating model for teams where AI agents generate a large share of implementation output. Use when setting team process, review gates, or ownership rules for a codebase largely written by agents.

ai-regression-testing

Regression testing strategies for AI-assisted development. Sandbox-mode API testing without database dependencies, automated bug-check workflows, and patterns to catch AI blind spots where the same model writes and reviews code. Use when adding regression coverage to AI-assisted code, or when the same model both wrote and reviewed a change.

android-clean-architecture

Clean Architecture patterns for Android and Kotlin Multiplatform projects — module structure, dependency rules, UseCases, Repositories, and data layer patterns. Use when structuring modules, layers, or data flow in an Android or KMP project.

angular-developer

Generates Angular code and provides architectural guidance. Trigger when creating projects, components, or services, or for best practices on reactivity (signals, linkedSignal, resource), forms, dependency injection, routing, SSR, accessibility (ARIA), animations, styling (component styles, Tailwind CSS), testing, or CLI tooling.

相關技能