彩纸爆发

点击后喷出一束彩纸,彩纸翻滚着因空气阻力减速,然后落出画面。

  • Canvas
  • 背景
  • 点击
  • 收录于

AI 提示词

用 React 和 <canvas> 覆盖层制作一个不依赖第三方库的彩纸爆发效果。点击时,在按钮中心生成 90 片彩纸,每片的方向在朝上的 70 度锥形内随机,速度为每帧 6–12px,尺寸 6×10px,旋转速度随机,颜色从 4 种品牌色中选取。每次 requestAnimationFrame:施加重力(vy 加 0.25)、空气阻力(vx 和 vy 乘以 0.985),推进旋转,并把每片彩纸画成旋转的矩形,宽度乘以 Math.cos(tilt),看起来像在翻面。移除离开画布的彩纸,当没有彩纸时停止循环。按 devicePixelRatio 设置画布尺寸,并设置 pointer-events: none。在 prefers-reduced-motion 下不播放彩纸,改为让按钮做一次 150ms 的透明度闪动。按钮文字为“庆祝一下”。

源代码

Demo.tsx
'use client'

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

type Piece = { x: number; y: number; vx: number; vy: number; rot: number; vr: number; tilt: number; color: string }

export default function ConfettiBurst({ labels }: { labels: Record<string, string> }) {
  const canvas = useRef<HTMLCanvasElement>(null)
  const button = useRef<HTMLButtonElement>(null)
  const pieces = useRef<Piece[]>([])
  const raf = useRef(0)

  useEffect(() => () => cancelAnimationFrame(raf.current), [])

  const fire = () => {
    const c = canvas.current!
    const b = button.current!
    if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
      b.animate([{ opacity: 0.4 }, { opacity: 1 }], 150)
      return
    }
    const ctx = c.getContext('2d')!
    const dpr = window.devicePixelRatio || 1
    const w = c.clientWidth
    const h = c.clientHeight
    c.width = w * dpr
    c.height = h * dpr
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0)

    const css = getComputedStyle(c)
    const palette = ['--c1', '--c2', '--c3', '--c4'].map((v) => css.getPropertyValue(v).trim())
    const cr = c.getBoundingClientRect()
    const br = b.getBoundingClientRect()
    const ox = br.left + br.width / 2 - cr.left
    const oy = br.top + br.height / 2 - cr.top

    for (let i = 0; i < 90; i++) {
      const angle = -Math.PI / 2 + (Math.random() - 0.5) * ((70 * Math.PI) / 180)
      const speed = 6 + Math.random() * 6
      pieces.current.push({
        x: ox,
        y: oy,
        vx: Math.cos(angle) * speed,
        vy: Math.sin(angle) * speed,
        rot: Math.random() * Math.PI,
        vr: (Math.random() - 0.5) * 0.3,
        tilt: Math.random() * Math.PI,
        color: palette[i % palette.length],
      })
    }

    cancelAnimationFrame(raf.current)
    const tick = () => {
      ctx.clearRect(0, 0, w, h)
      pieces.current = pieces.current.filter((p) => p.y < h + 20 && p.x > -20 && p.x < w + 20)
      for (const p of pieces.current) {
        p.vy += 0.25
        p.vx *= 0.985
        p.vy *= 0.985
        p.x += p.vx
        p.y += p.vy
        p.rot += p.vr
        p.tilt += 0.12
        ctx.save()
        ctx.translate(p.x, p.y)
        ctx.rotate(p.rot)
        ctx.fillStyle = p.color
        ctx.fillRect(-3 * Math.cos(p.tilt), -5, 6 * Math.cos(p.tilt), 10)
        ctx.restore()
      }
      if (pieces.current.length) raf.current = requestAnimationFrame(tick)
    }
    raf.current = requestAnimationFrame(tick)
  }

  return (
    <div className={s.wrap}>
      <canvas ref={canvas} className={s.canvas} aria-hidden="true" />
      <button ref={button} type="button" className={s.button} onClick={fire}>
        {labels.cta}
      </button>
    </div>
  )
}
demo.module.css
.wrap {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: end center;
  padding-bottom: 36px;
}

.canvas {
  --c1: var(--stage-accent);
  --c2: var(--stage-ink);
  --c3: oklch(78% 0.12 85);
  --c4: oklch(62% 0.1 160);
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

.button {
  position: relative;
  height: 48px;
  padding: 0 24px;
  border: 0;
  border-radius: 12px;
  background: var(--stage-ink);
  color: var(--stage-bg);
  font: 600 var(--text-base) / 1 var(--font-body);
  white-space: nowrap;
  cursor: pointer;
}

.button:hover { background: var(--stage-accent); color: var(--stage-accent-ink); }
.button:active { transform: translateY(1px); }
.button:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 3px; }

投稿创意

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

你想投稿的是?

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