图片裁切揭示

一块彩色面板扫过,把图片拉进画面,图片本身则从轻微放大的状态慢慢回落。

  • CSS
  • 图片
  • 进场
  • 收录于
金马伦高原,早上 6:40

AI 提示词

用 CSS 为 React 制作图片入场动画。把 <img> 包在一个 overflow hidden 的 figure 中。进入视野时(IntersectionObserver,threshold 0.4,只触发一次)同时播放两个动画:figure 在 900ms 内从 clip-path: inset(0 100% 0 0) 过渡到 inset(0 0 0 0),缓动为 cubic-bezier(0.65, 0, 0.35, 1);里面的图片在 1400ms 内从 1.15 缩放到 1,缓动为 cubic-bezier(0.16, 1, 0.3, 1)。再加一个纯强调色的 ::after 面板引领揭示:在同样的 900ms 内先以左侧为原点从 scaleX(0) 变为 scaleX(1),再以右侧为原点变为 scaleX(0)。图片出现 300ms 后淡入说明文字“金马伦高原,早上 6:40”。为图片提供真实的 alt 文本。在 prefers-reduced-motion 下改为 150ms 的图片淡入。

源代码

Demo.tsx
'use client'

import { useEffect, useRef, useState } from 'react'
import s from './demo.module.css'

export default function ClipReveal({ labels }: { labels: Record<string, string> }) {
  const ref = useRef<HTMLElement>(null)
  const [run, setRun] = useState(0)

  useEffect(() => {
    const el = ref.current
    if (!el) return
    const io = new IntersectionObserver(
      ([e]) => {
        if (!e.isIntersecting) return
        el.dataset.in = ''
        io.disconnect()
      },
      { threshold: 0.4 },
    )
    io.observe(el)
    return () => io.disconnect()
  }, [run])

  return (
    <div className={s.wrap}>
      <figure key={run} ref={ref} className={s.figure}>
        <div className={s.clip}>
          <div className={s.scene} role="img" aria-label={labels.caption} />
        </div>
        <figcaption className={s.caption}>{labels.caption}</figcaption>
      </figure>
      <button type="button" className={s.button} onClick={() => setRun(run + 1)}>
        {labels.replay}
      </button>
    </div>
  )
}
demo.module.css
.wrap { display: grid; justify-items: center; gap: 10px; width: min(220px, 100%); }

.figure {
  --sky-top: oklch(82% 0.05 150);
  --sky-low: oklch(74% 0.07 120);
  --sun: oklch(95% 0.06 95);
  --hill-far: oklch(52% 0.08 150);
  --hill-near: oklch(34% 0.06 155);
  display: grid;
  gap: 8px;
  width: 100%;
  margin: 0;
}

.clip {
  position: relative;
  aspect-ratio: 16 / 10;
  max-width: 100%;
  overflow: hidden;
  border-radius: 12px;
  clip-path: inset(0 100% 0 0);
}

.clip::after {
  content: '';
  position: absolute;
  inset: 0;
  background: var(--stage-accent);
  transform: scaleX(0);
  transform-origin: left;
}

.scene {
  position: absolute;
  inset: 0;
  background:
    radial-gradient(ellipse 60% 38% at 20% 104%, var(--hill-near) 98%, transparent 100%),
    radial-gradient(ellipse 75% 50% at 80% 102%, var(--hill-far) 98%, transparent 100%),
    radial-gradient(circle at 30% 38%, var(--sun) 0 9%, transparent 9.5%),
    linear-gradient(180deg, var(--sky-top), var(--sky-low));
  transform: scale(1.15);
}

.caption { font: 500 var(--text-xs) / 1.3 var(--font-mono); color: var(--stage-muted); opacity: 0; }

.figure[data-in] .clip { animation: open 900ms var(--ease-in-out) forwards; }
.figure[data-in] .clip::after { animation: wipe 900ms var(--ease-in-out) forwards; }
.figure[data-in] .scene { animation: settle 1400ms var(--ease-out) forwards; }
.figure[data-in] .caption { animation: fade 400ms 1200ms var(--ease-out) forwards; }

@keyframes open { to { clip-path: inset(0 0 0 0); } }
@keyframes wipe {
  0% { transform: scaleX(0); transform-origin: left; }
  50% { transform: scaleX(1); transform-origin: left; }
  51% { transform: scaleX(1); transform-origin: right; }
  100% { transform: scaleX(0); transform-origin: right; }
}
@keyframes settle { to { transform: scale(1); } }
@keyframes fade { to { opacity: 1; } }

.button {
  height: 32px;
  padding: 0 12px;
  border: 1px solid var(--stage-rule);
  border-radius: 8px;
  background: var(--stage-surface);
  color: var(--stage-ink);
  font: 500 var(--text-sm) / 1 var(--font-body);
  white-space: nowrap;
  cursor: pointer;
}

.button:hover { border-color: var(--stage-accent); }
.button:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 2px; }

@media (prefers-reduced-motion: reduce) {
  .clip { clip-path: none; opacity: 0; }
  .clip::after { display: none; }
  .scene { transform: none; }
  .figure[data-in] .clip { animation: fade 150ms linear forwards; }
  .figure[data-in] .scene { animation: none; }
  .figure[data-in] .caption { animation: fade 150ms linear forwards; }
}

投稿创意

看到想收录的动效或主题?简单描述一下,会打开一个预先填好的 GitHub Issue,确认后即可提交。

你想投稿的是?

需要 GitHub 账号,投稿内容会公开。