路径描绘

线条插画一笔一笔地画出来,画完后一个小圆点沿着路线移动。

  • CSS
  • SVG 与图形
  • 进场
  • 收录于

AI 提示词

用 CSS 让 React 中的 SVG 线条图自己画出来。对每个 <path> 用 getTotalLength() 读取长度并设为 --len;初始设置 stroke-dasharray: var(--len) 和 stroke-dashoffset: var(--len),然后在 1.6 秒内把 stroke-dashoffset 动画到 0,缓动为 cubic-bezier(0.65, 0, 0.35, 1),每条路径依次错开 250ms。使用 stroke-linecap: round 和 fill: none。画完后,用相同路径数据的 offset-path 让一个强调色的小圆点沿主路线移动,offset-distance 在 1.8 秒内从 0% 动画到 100%。添加一个“重播”按钮,通过改变 key 重新播放。给 SVG 设置 role="img" 和 aria-label“从家到工作室的路线”。在 prefers-reduced-motion 下直接显示完成的图,圆点停在终点。

源代码

Demo.tsx
'use client'

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

const ROUTE = 'M20 120 C 60 120, 60 40, 110 40 S 160 110, 200 80 S 240 30, 260 30'

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

  useLayoutEffect(() => {
    svg.current?.querySelectorAll('path').forEach((p) => p.style.setProperty('--len', String(Math.ceil(p.getTotalLength()))))
  }, [run])

  return (
    <div className={s.wrap}>
      <svg key={run} ref={svg} className={s.svg} viewBox="0 0 280 150" role="img" aria-label={labels.label}>
        <path className={s.grid} style={{ '--d': 0 } as React.CSSProperties} d="M20 135 H260" />
        <path className={s.house} style={{ '--d': 1 } as React.CSSProperties} d="M8 120 V100 L20 90 L32 100 V120 Z" />
        <path className={s.studio} style={{ '--d': 2 } as React.CSSProperties} d="M248 30 V10 H272 V30 Z M254 18 H266" />
        <path className={s.route} style={{ '--d': 3 } as React.CSSProperties} d={ROUTE} />
        <circle className={s.dot} r="5" style={{ offsetPath: `path("${ROUTE}")` }} />
      </svg>
      <button type="button" className={s.button} onClick={() => setRun(run + 1)}>
        {labels.replay}
      </button>
    </div>
  )
}
demo.module.css
.wrap { display: grid; justify-items: center; gap: 12px; width: min(300px, 100%); }

.svg { width: 100%; height: auto; overflow: visible; }

.svg path {
  fill: none;
  stroke: var(--stage-ink);
  stroke-width: 2.5;
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-dasharray: var(--len, 1000);
  stroke-dashoffset: var(--len, 1000);
  animation: draw 1.6s var(--ease-in-out) forwards;
  animation-delay: calc(var(--d) * 250ms);
}

.svg .grid { stroke: var(--stage-rule); }
.svg .route { stroke: var(--stage-accent); stroke-width: 3; stroke-dasharray: var(--len, 1000); }

.dot {
  fill: var(--stage-ink);
  offset-distance: 0%;
  opacity: 0;
  animation: ride 1.8s 2.2s var(--ease-in-out) forwards;
}

@keyframes draw { to { stroke-dashoffset: 0; } }
@keyframes ride {
  0% { offset-distance: 0%; opacity: 1; }
  100% { offset-distance: 100%; 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) {
  .svg path { animation: none; stroke-dashoffset: 0; }
  .dot { animation: none; opacity: 1; offset-distance: 100%; }
}

投稿创意

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

你想投稿的是?

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