波浪线条

细细的水平线条像平静的海面一样缓缓起伏,指针经过的地方会鼓起一道波峰。

  • Canvas
  • 背景
  • 跟随指针
  • 收录于
移动你的指针

AI 提示词

用 canvas 制作波浪线条背景,不使用第三方库。用 devicePixelRatio 和 ResizeObserver 让 canvas 匹配容器尺寸。均匀绘制约 14 条水平线。对每条线以 6px 为步长遍历 x,y 偏移 sin(x × 0.02 + 时间 + 线序号 × 0.5) × 4 形成缓慢的起伏,再加上一个幅度为 18 × exp(−距离² / 2400) 的鼓包(距离为该点到指针的距离),沿纵向把线条推离指针。每帧让指针位置向真实位置缓动,使鼓包平滑滑动。线条用弱化色描边,离指针最近的一条用主色。颜色从 CSS 自定义属性读取。离开屏幕时用 IntersectionObserver 暂停,在 prefers-reduced-motion 下只绘制一帧静态画面,组件卸载时清理。

源代码

Demo.tsx
'use client'

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

const LINES = 14

export default function WavyLines({ labels }: { labels: Record<string, string> }) {
  const ref = useRef<HTMLCanvasElement>(null)

  useEffect(() => {
    const canvas = ref.current!
    const ctx = canvas.getContext('2d')!
    const css = getComputedStyle(canvas)
    const muted = css.getPropertyValue('--line').trim()
    const hot = css.getPropertyValue('--hot').trim()
    const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches
    const target = { x: -999, y: -999 }
    const pointer = { x: -999, y: -999 }
    let w = 0
    let h = 0
    let raf = 0
    let t = 0
    let visible = true

    const draw = () => {
      ctx.clearRect(0, 0, w, h)
      pointer.x += (target.x - pointer.x) * 0.12
      pointer.y += (target.y - pointer.y) * 0.12
      const gap = h / (LINES + 1)
      const nearest = Math.round(pointer.y / gap) - 1
      for (let i = 0; i < LINES; i++) {
        const base = gap * (i + 1)
        ctx.beginPath()
        for (let x = 0; x <= w; x += 6) {
          const dx = x - pointer.x
          const dy = base - pointer.y
          const bump = 18 * Math.exp(-(dx * dx + dy * dy) / 2400) * Math.sign(dy || 1)
          const y = base + Math.sin(x * 0.02 + t + i * 0.5) * 4 + bump
          if (x === 0) ctx.moveTo(x, y)
          else ctx.lineTo(x, y)
        }
        ctx.strokeStyle = i === nearest ? hot : muted
        ctx.lineWidth = i === nearest ? 1.6 : 1
        ctx.stroke()
      }
    }

    const loop = () => {
      t += 0.02
      draw()
      if (visible) raf = requestAnimationFrame(loop)
    }

    const resize = () => {
      const dpr = window.devicePixelRatio || 1
      w = canvas.clientWidth
      h = canvas.clientHeight
      canvas.width = w * dpr
      canvas.height = h * dpr
      ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
      draw()
    }

    const onMove = (e: PointerEvent) => {
      const r = canvas.getBoundingClientRect()
      target.x = e.clientX - r.left
      target.y = e.clientY - r.top
      if (reduced) Object.assign(pointer, target) && draw()
    }
    const onLeave = () => Object.assign(target, { x: -999, y: -999 })

    const ro = new ResizeObserver(resize)
    ro.observe(canvas)
    const io = new IntersectionObserver(([e]) => {
      visible = e.isIntersecting && !reduced
      cancelAnimationFrame(raf)
      if (visible) raf = requestAnimationFrame(loop)
    })
    io.observe(canvas)
    canvas.addEventListener('pointermove', onMove)
    canvas.addEventListener('pointerleave', onLeave)
    return () => {
      cancelAnimationFrame(raf)
      ro.disconnect()
      io.disconnect()
      canvas.removeEventListener('pointermove', onMove)
      canvas.removeEventListener('pointerleave', onLeave)
    }
  }, [])

  return (
    <div className={s.wrap}>
      <canvas ref={ref} className={s.canvas} aria-hidden="true" />
      <span className={s.caption}>{labels.caption}</span>
    </div>
  )
}
demo.module.css
.wrap {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: end center;
  padding-bottom: 12px;
  background: var(--stage-bg);
}

.canvas {
  --line: var(--stage-rule);
  --hot: var(--stage-accent);
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

.caption {
  position: relative;
  padding: 4px 8px;
  border-radius: 6px;
  background: var(--stage-bg);
  font: 500 var(--text-xs) / 1 var(--font-mono);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--stage-muted);
  pointer-events: none;
}

投稿创意

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

你想投稿的是?

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