在这里插入图片描述

每日一句正能量

当你变强了,世界自然会对你保持尊重。
个人的能力、价值和能量,是赢得平等对话权与基本尊重的前提。将焦点从外部抱怨转向内在建设,因为自身强大才是解决大多数问题的钥匙。

摘要

当你第一次打开 Codex 官网,背景的光晕从中心缓缓扩散,标题从下方轻盈上浮,CTA 按钮在最后一刻优雅地缩放入场——这不是随意的动效堆砌,而是一场精确到毫秒的「动画交响乐」。本文将拆解 Codex 官网的微交互动画编排逻辑,从 Hero 区的三段式入场到滚动触发的 stagger 动画,给出完整的时序设计与工程化实现。


一、动画不是装饰,而是叙事

很多前端开发者将动画视为「锦上添花」,但在 Codex 这类以技术公信力为核心的产品页中,动画承担着引导注意力、建立层级、传递品牌质感的三重使命。一个精心编排的入场动画,能够在用户尚未阅读任何文字之前,就建立起「这个产品很精致」的第一印象。

动画编排的核心挑战在于时序(Timing)。如果所有元素同时入场,页面会显得杂乱无章;如果间隔过长,用户会在等待中流失。Codex 官网的解决方案是:将动画按叙事节奏分层,每一层负责传递一个信息,层与层之间保持 200-300ms 的呼吸间隔


二、Hero 区三段式入场:从背景到行动的叙事链

Codex 官网的 Hero 区域采用了经典的三段式入场编排,总时长控制在 900ms 以内,恰好处于人类感知「即时响应」的心理阈值(1 秒)之下。

2.1 第一阶段:背景径向渐变展开(0–300ms)

背景是用户视线最先接触的区域。Codex 使用了从中心向四周扩散的径向渐变光晕,模拟「能量汇聚」的视觉效果。这一阶段的动画只涉及 opacitytransform: scale(),完全运行在浏览器的合成器线程上,不会阻塞主线程的解析与渲染。

// components/HeroBackground.tsx
'use client';

import { useEffect, useRef } from 'react';

export function HeroBackground() {
  const canvasRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const el = canvasRef.current;
    if (!el) return;

    // 使用 CSS 动画而非 JS 定时器,确保与浏览器渲染帧同步
    el.style.opacity = '1';
    el.style.transform = 'scale(1)';
  }, []);

  return (
    <div
      ref={canvasRef}
      className="absolute inset-0 pointer-events-none"
      style={{
        background: 'radial-gradient(ellipse 80% 60% at 50% 40%, rgba(16, 163, 127, 0.12) 0%, transparent 70%)',
        opacity: 0,
        transform: 'scale(0.85)',
        transition: 'opacity 800ms cubic-bezier(0.22, 1, 0.36, 1), transform 800ms cubic-bezier(0.22, 1, 0.36, 1)',
      }}
    />
  );
}

这里使用了 cubic-bezier(0.22, 1, 0.36, 1) 这个缓动曲线——它是一种「先快后慢」的 ease-out 变体,让光晕在展开末期自然减速,避免机械感。

2.2 第二阶段:标题 fade-up(300–600ms)

标题是 Hero 区的信息核心。Codex 采用了「从下方淡入上浮」的经典模式,但细节在于:标题的每一行都有独立的动画延迟,形成微妙的 stagger 效果

// components/HeroTitle.tsx
'use client';

import { useEffect, useState } from 'react';

const titleLines = [
  '由 AI 驱动的编程助手',
  '让代码编写如对话般自然',
];

export function HeroTitle() {
  const [visible, setVisible] = useState(false);

  useEffect(() => {
    const timer = setTimeout(() => setVisible(true), 300);
    return () => clearTimeout(timer);
  }, []);

  return (
    <h1 className="relative z-10 text-center">
      {titleLines.map((line, index) => (
        <span
          key={index}
          className="block overflow-hidden"
        >
          <span
            className="block transition-all duration-700"
            style={{
              transform: visible ? 'translateY(0)' : 'translateY(100%)',
              opacity: visible ? 1 : 0,
              transitionTimingFunction: 'cubic-bezier(0.22, 1, 0.36, 1)',
              transitionDelay: `${300 + index * 100}ms`,
            }}
          >
            {line}
          </span>
        </span>
      ))}
    </h1>
  );
}

关键细节:

  • 外层 overflow-hidden 配合内层 translateY(100%) → 0 实现「从遮罩下方滑出」的效果,比单纯的 opacity 变化更有质感。
  • 每行之间的 transitionDelay 差值为 100ms,形成自然的阅读节奏引导。

2.3 第三阶段:CTA 按钮 scale-in(600–900ms)

CTA(Call to Action)按钮是 Hero 区的最终目标。它的入场时机被刻意安排在背景和标题之后,确保用户的注意力已经被引导到正确的位置。

// components/HeroCTA.tsx
'use client';

import { useEffect, useState } from 'react';

export function HeroCTA() {
  const [visible, setVisible] = useState(false);

  useEffect(() => {
    const timer = setTimeout(() => setVisible(true), 600);
    return () => clearTimeout(timer);
  }, []);

  return (
    <div
      className="relative z-10 mt-8 transition-all duration-500"
      style={{
        transform: visible ? 'scale(1) translateY(0)' : 'scale(0.9) translateY(20px)',
        opacity: visible ? 1 : 0,
        transitionTimingFunction: 'cubic-bezier(0.34, 1.56, 0.64, 1)', // 轻微的 overshoot 弹性
        transitionDelay: '600ms',
      }}
    >
      <button className="px-8 py-3 bg-accent text-inverse rounded-lg font-medium hover:bg-accent-hover transition-colors">
        开始使用 Codex
      </button>
    </div>
  );
}

这里使用了 cubic-bezier(0.34, 1.56, 0.64, 1)——一个带有轻微 overshoot(过冲)的弹性缓动,让按钮在缩放到位时产生「落定」的物理感,增强点击欲望。

以下这张图完整展示了 Hero 区三段式入场的时序编排:

在这里插入图片描述


三、滚动触发的 Feature 卡片:GSAP ScrollTrigger 实战

Hero 区以下的内容区域,Codex 官网使用了滚动触发的 stagger 动画来展示功能特性。当用户滚动到 Feature 区域时,卡片依次以 0.1 秒的间隔淡入上浮,形成「内容随着滚动逐渐揭晓」的叙事效果。

3.1 ScrollTrigger 的触发点配置

GSAP 的 ScrollTrigger 插件提供了精确的滚动位置控制。Codex 官网的配置逻辑是:当卡片顶部到达视口 80% 位置时触发动画,这样既不会让用户等待太久,也不会在元素尚未进入视野时就开始动画。

// components/FeatureCards.tsx
'use client';

import { useEffect, useRef } from 'react';
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';

gsap.registerPlugin(ScrollTrigger);

const features = [
  { title: '自然语言编程', desc: '用中文描述需求,Codex 生成代码' },
  { title: '多语言支持', desc: 'Python、JavaScript、Go 等主流语言' },
  { title: '实时预览', desc: '边写边看,即时验证代码效果' },
  { title: '智能补全', desc: '基于上下文的代码建议与补全' },
];

export function FeatureCards() {
  const containerRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const ctx = gsap.context(() => {
      // 获取所有卡片元素
      const cards = gsap.utils.toArray<HTMLElement>('.feature-card');

      gsap.fromTo(
        cards,
        {
          y: 60,
          opacity: 0,
        },
        {
          y: 0,
          opacity: 1,
          duration: 0.8,
          ease: 'power3.out',
          stagger: 0.1, // 每张卡片间隔 100ms
          scrollTrigger: {
            trigger: containerRef.current,
            start: 'top 80%', // 容器顶部到达视口 80% 时触发
            end: 'bottom 20%',
            toggleActions: 'play none none none', // 只播放一次
            // markers: true, // 调试用,显示触发线
          },
        }
      );
    }, containerRef);

    return () => ctx.revert(); // 清理动画与 ScrollTrigger 实例
  }, []);

  return (
    <div ref={containerRef} className="grid grid-cols-1 md:grid-cols-2 gap-6 max-w-5xl mx-auto px-6 py-24">
      {features.map((feature, index) => (
        <div
          key={index}
          className="feature-card bg-secondary border border-border rounded-xl p-8 hover:border-border-focus transition-colors"
        >
          <h3 className="text-primary text-xl font-medium mb-3">{feature.title}</h3>
          <p className="text-secondary leading-relaxed">{feature.desc}</p>
        </div>
      ))}
    </div>
  );
}

3.2 为什么使用 GSAP 而非纯 CSS

CSS 的 @scroll-timelineanimation-timeline 虽然可以实现滚动驱动动画,但目前的浏览器兼容性(尤其是 Safari)仍然不足。GSAP ScrollTrigger 的优势在于:

  • 跨浏览器一致性:所有现代浏览器表现一致。
  • 精确控制:可以设置 scrub: true 实现滚动位置与动画进度的 1:1 映射,也可以像上文那样使用触发点模式。
  • 自动清理ctx.revert() 在组件卸载时自动移除所有 ScrollTrigger 实例,避免内存泄漏。

四、按钮微交互:Hover 与 Click 的反馈设计

按钮是用户与页面交互最频繁的元素,其微交互设计直接影响「这个产品是否跟手」的感知。

4.1 Hover 状态:150ms 的 ease-out 过渡

/* styles/interactions.css */
.btn-interactive {
  transition: transform 150ms ease-out, 
              background-color 150ms ease-out,
              box-shadow 150ms ease-out;
}

.btn-interactive:hover {
  transform: translateY(-1px);
  box-shadow: 0 4px 12px rgba(16, 163, 127, 0.25);
}

.btn-interactive:active {
  transform: scale(0.98) translateY(0);
  transition-duration: 100ms;
}

设计要点:

  • translateY(-1px) 的微小上浮暗示「可点击」,但幅度必须极小(1-2px),否则会显得轻浮。
  • box-shadow 的变化模拟光源照射下的物理抬升。
  • Hover 过渡时长 150ms,处于人类感知「即时反馈」的最佳区间(100-200ms)。

4.2 Click 状态:100ms 的 scale(0.98)

点击时的 scale(0.98) 模拟了物理按钮被按下的凹陷感。时长缩短到 100ms,因为点击是一个快速动作,过长的动画会让用户感觉「滞后」。

// components/InteractiveButton.tsx
'use client';

export function InteractiveButton({ children, onClick }: { children: React.ReactNode; onClick?: () => void }) {
  return (
    <button
      onClick={onClick}
      className="relative px-6 py-3 bg-accent text-inverse rounded-lg font-medium 
                 transition-all duration-150 ease-out
                 hover:-translate-y-0.5 hover:shadow-glow
                 active:scale-[0.98] active:duration-100"
    >
      {children}
    </button>
  );
}

五、性能铁律:只动画 transform 与 opacity

动画性能的核心在于浏览器渲染管线。一个完整的渲染帧需要经历以下阶段:

JavaScript → Style → Layout → Paint → Composite
  • Layout(布局):计算元素的几何位置(如 widthheighttopleft)。这是最昂贵的阶段。
  • Paint(绘制):填充像素(如 background-colorbox-shadow)。
  • Composite(合成):将图层合并为最终画面。transformopacity 可以完全在此阶段完成。

Codex 官网的所有动画都严格遵循这一原则:

可动画属性 避免使用的属性
transform(translate, scale, rotate) width, height, top, left
opacity margin, padding, border-width
filter(谨慎使用,会触发 Paint) background-color(会触发 Paint)

如果确实需要动画 background-color(如主题切换),应当使用 CSS 变量并在父级元素上统一过渡,而非在单个组件上频繁触发。


六、无障碍降级:prefers-reduced-motion

并非所有用户都能享受动画。对于患有前庭功能障碍(vestibular disorders)的用户,过多的动画会导致眩晕和不适。Codex 官网应当尊重用户的系统偏好,提供无动画的降级方案。

6.1 CSS 层面的全局降级

/* styles/accessibility.css */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

这段代码的核心逻辑是:将所有的 animation-durationtransition-duration 强制设为 0.01ms。这个值足够小以至于人眼无法感知动画,但又足够大以避免某些浏览器在 0ms 时的渲染 bug。

6.2 JavaScript 动画的降级处理

对于 GSAP 等 JavaScript 驱动的动画,需要在初始化时检测用户偏好:

// hooks/useReducedMotion.ts
import { useState, useEffect } from 'react';

export function useReducedMotion(): boolean {
  const [reduced, setReduced] = useState(false);

  useEffect(() => {
    const mql = window.matchMedia('(prefers-reduced-motion: reduce)');
    setReduced(mql.matches);

    const handler = (e: MediaQueryListEvent) => setReduced(e.matches);
    mql.addEventListener('change', handler);
    return () => mql.removeEventListener('change', handler);
  }, []);

  return reduced;
}
// 在 FeatureCards 中使用
const reduced = useReducedMotion();

useEffect(() => {
  if (reduced) return; // 如果用户偏好减少动画,跳过 GSAP 初始化

  const ctx = gsap.context(() => {
    // ... GSAP 动画配置
  }, containerRef);

  return () => ctx.revert();
}, [reduced]);

6.3 Framer Motion 的 reducedMotion 支持

如果你使用 Framer Motion,它内置了对 prefers-reduced-motion 的支持:

// components/AnimatedCard.tsx
'use client';

import { motion } from 'framer-motion';

const cardVariants = {
  hidden: { y: 60, opacity: 0 },
  visible: { y: 0, opacity: 1 },
};

export function AnimatedCard({ children }: { children: React.ReactNode }) {
  return (
    <motion.div
      initial="hidden"
      whileInView="visible"
      viewport={{ once: true, margin: '-20%' }}
      variants={cardVariants}
      transition={{
        duration: 0.8,
        ease: [0.22, 1, 0.36, 1], // 自定义 cubic-bezier
      }}
      // Framer Motion 会自动检测 prefers-reduced-motion
      // 也可以通过 reducedMotion="user" 显式启用
      reducedMotion="user"
    >
      {children}
    </motion.div>
  );
}

七、Framer Motion 的 Stagger 配置:声明式动画编排

对于偏好 React 生态的开发者,Framer Motion 提供了更声明式的 stagger 实现方式:

// components/StaggerContainer.tsx
'use client';

import { motion } from 'framer-motion';

const containerVariants = {
  hidden: { opacity: 0 },
  visible: {
    opacity: 1,
    transition: {
      staggerChildren: 0.1,      // 子元素间隔 100ms
      delayChildren: 0.3,        // 容器动画后延迟 300ms 开始子元素
      duration: 0.5,
    },
  },
};

const itemVariants = {
  hidden: { y: 40, opacity: 0 },
  visible: {
    y: 0,
    opacity: 1,
    transition: {
      duration: 0.7,
      ease: [0.22, 1, 0.36, 1],
    },
  },
};

export function StaggerContainer() {
  return (
    <motion.div
      variants={containerVariants}
      initial="hidden"
      whileInView="visible"
      viewport={{ once: true, amount: 0.3 }}
      className="grid grid-cols-1 md:grid-cols-3 gap-6"
    >
      {[1, 2, 3].map((i) => (
        <motion.div
          key={i}
          variants={itemVariants}
          className="bg-secondary border border-border rounded-xl p-6"
        >
          <div className="w-12 h-12 bg-accent/10 rounded-lg mb-4" />
          <h3 className="text-primary font-medium mb-2">特性 {i}</h3>
          <p className="text-secondary text-sm">功能描述文案</p>
        </motion.div>
      ))}
    </motion.div>
  );
}

Framer Motion 的 variants 系统让动画编排变得像写状态机一样清晰。父容器的 staggerChildrendelayChildren 控制了整体节奏,子元素的 variants 定义了个体行为,两者解耦又协同。


八、性能验证:Chrome DevTools Performance 面板

动画写完后,必须通过 Performance 面板验证其实际表现。验证步骤如下:

  1. 打开 Chrome DevTools → Performance 标签。
  2. 勾选「Screenshots」和「Web Vitals」。
  3. 点击录制按钮,刷新页面并等待 Hero 动画完成。
  4. 停止录制,观察以下指标:
    • Main 线程占用:应当保持较低,没有长任务(Long Task)。
    • Compositor 线程transformopacity 动画应当完全在此线程运行。
    • Layout Shift:确认没有意外的布局偏移。

如果观察到 Paint 或 Layout 阶段的橙色条块,说明某个动画触发了重排或重绘,需要回溯检查是否使用了非合成器属性。


结语

Codex 官网的动画之所以「感觉舒服」,不是因为用了多么复杂的技术,而是因为每一个动画都有明确的叙事目的、精确的时序控制和严格的性能边界。从 Hero 区 900ms 的三段式入场,到滚动触发的 stagger 卡片,再到按钮的 150ms hover 反馈——这些数字背后是对用户体验的深刻尊重。

动画编排的本质是「时间的设计」。当你开始用毫秒为单位思考用户的视觉旅程时,你的产品就已经超越了功能的堆砌,进入了体验的维度。


转载自:https://blog.csdn.net/sghtgjfhv/article/details/163981682
欢迎 👍点赞✍评论⭐收藏,欢迎指正

Logo

葡萄城是专业的软件开发技术和低代码平台提供商,聚焦软件开发技术,以“赋能开发者”为使命,致力于通过表格控件、低代码和BI等各类软件开发工具和服务,一站式满足开发者需求,帮助企业提升开发效率并创新开发模式。

更多推荐