滚动绘制路径

路线随着滚动一点点画出来,往回滚又会倒退,把进度交到读者手里。

  • JS
  • SVG 与图形
  • 滚动
  • 收录于
向下滚动 ↓

AI 提示词

用 React 制作随滚动同步绘制的 SVG 路径。给路径设置 pathLength="1",这样无论实际长度多少,stroke-dasharray: 1 和 stroke-dashoffset: 1 都能把它完全隐藏。滚动时计算区块内 0 到 1 的进度,写入 CSS 自定义属性 --p;在路径上设置 stroke-dashoffset: calc(1 - var(--p)),向下滚动时向前绘制,向上滚动时倒退。区块滚动期间让 SVG 保持在 sticky 容器中。在下方画一条淡色的同样路径作为轨道,进度达到 1 时终点圆点被填满。在 prefers-reduced-motion 下直接显示完整路径。两个端点文字为“起点”和“终点”。

源代码

Demo.tsx
'use client'

import s from './demo.module.css'

const D = 'M20 150 C 70 150, 60 60, 120 70 S 170 150, 220 110 S 270 30, 300 40'

export default function ScrollDraw({ labels }: { labels: Record<string, string> }) {
  const onScroll = (e: React.UIEvent<HTMLDivElement>) => {
    const el = e.currentTarget
    const p = el.scrollTop / (el.scrollHeight - el.clientHeight || 1)
    el.style.setProperty('--p', String(p))
    el.toggleAttribute('data-done', p > 0.99)
  }

  return (
    <div className={s.frame} tabIndex={0} onScroll={onScroll}>
      <div className={s.section}>
        <div className={s.sticky}>
          <svg viewBox="0 0 320 180" className={s.svg} aria-hidden="true">
            <path d={D} className={s.track} />
            <path d={D} className={s.path} pathLength={1} />
            <circle cx="20" cy="150" r="5" className={s.startDot} />
            <circle cx="300" cy="40" r="6" className={s.endDot} />
            <text x="20" y="172" className={s.text}>
              {labels.start}
            </text>
            <text x="300" y="22" className={s.text} textAnchor="end">
              {labels.end}
            </text>
          </svg>
          <span className={s.label}>{labels.hint} ↓</span>
        </div>
      </div>
    </div>
  )
}
demo.module.css
.frame {
  position: absolute;
  inset: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: thin;
}

.frame:focus-visible { outline: 2px solid var(--color-focus); outline-offset: -4px; }

.hint {
  display: block;
  font: 500 var(--text-xs) / 1 var(--font-mono);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  text-align: center;
  color: var(--stage-muted);
}

.frame { --p: 0; }

.section { height: 300%; }

.sticky {
  position: sticky;
  top: 0;
  display: grid;
  place-items: center;
  align-content: center;
  gap: 4px;
  height: 240px;
}

.svg { width: min(300px, 90%); overflow: visible; }

.track, .path {
  fill: none;
  stroke-width: 3;
  stroke-linecap: round;
}

.track { stroke: var(--stage-rule); stroke-dasharray: 2 8; }

.path {
  stroke: var(--stage-accent);
  stroke-dasharray: 1;
  stroke-dashoffset: calc(1 - var(--p));
}

.startDot { fill: var(--stage-accent); }

.endDot {
  fill: var(--stage-bg);
  stroke: var(--stage-accent);
  stroke-width: 2;
  transition: fill var(--dur-short);
}

.frame[data-done] .endDot { fill: var(--stage-accent); }

.text { fill: var(--stage-muted); font: 500 11px var(--font-mono); }

.label {
  font: 500 var(--text-xs) / 1 var(--font-mono);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--stage-muted);
}

@media (prefers-reduced-motion: reduce) {
  .path { stroke-dashoffset: 0; }
}

投稿创意

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

你想投稿的是?

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