雷达扫描

一道光束绕着雷达盘扫过,每个目标点在光束经过的瞬间亮起,然后慢慢消失。

  • CSS
  • SVG 与图形
  • 循环
  • 收录于
扫描中

AI 提示词

用 SVG 和 CSS 制作雷达扫描效果。用弱化色描边画三个同心圆环和两条十字线。在其上放一个圆盘,背景为 conic-gradient(from 0deg, 50% 不透明度的主色, transparent 70deg),以 3s linear infinite 旋转 360deg,让一道柔和的扇形拖在明亮的前沿之后。在已知角度上放三个目标点。每个目标点运行一个 3s 的 keyframe(opacity 0 → 2% 时为 1 → 40% 时为 0,scale 1.6 → 1),animation-delay = (角度 / 360) × 3s,使它正好在光束经过时亮起。离开屏幕时暂停所有动画。在 prefers-reduced-motion 下光束保持静止,目标点以半透明显示。说明文字为“扫描中”。

源代码

Demo.tsx
'use client'

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

const PERIOD = 3
// Angles are measured clockwise from 12 o'clock, matching the conic-gradient's start.
const BLIPS = [
  { angle: 50, r: 0.55 },
  { angle: 160, r: 0.8 },
  { angle: 285, r: 0.35 },
]

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

  useEffect(() => {
    const el = ref.current
    if (!el) return
    const io = new IntersectionObserver(([e]) => el.toggleAttribute('data-paused', !e.isIntersecting))
    io.observe(el)
    return () => io.disconnect()
  }, [])

  return (
    <div ref={ref} className={s.wrap}>
      <div className={s.radar}>
        <svg viewBox="0 0 100 100" className={s.grid} aria-hidden="true">
          <circle cx="50" cy="50" r="48" />
          <circle cx="50" cy="50" r="32" />
          <circle cx="50" cy="50" r="16" />
          <path d="M50 2v96M2 50h96" />
        </svg>
        <div className={s.beam} />
        {BLIPS.map(({ angle, r }) => {
          const rad = (angle * Math.PI) / 180
          return (
            <span
              key={angle}
              className={s.blip}
              style={{
                left: `${50 + Math.sin(rad) * r * 48}%`,
                top: `${50 - Math.cos(rad) * r * 48}%`,
                animationDelay: `${(angle / 360) * PERIOD}s`,
              }}
            />
          )
        })}
      </div>
      <span className={s.caption}>{labels.caption}</span>
    </div>
  )
}
demo.module.css
.wrap {
  display: grid;
  justify-items: center;
  gap: 12px;
}

.radar {
  position: relative;
  width: 160px;
  aspect-ratio: 1;
  border-radius: 50%;
  overflow: hidden;
}

.grid {
  position: absolute;
  inset: 0;
  fill: none;
  stroke: var(--stage-rule);
  stroke-width: 0.6;
}

.beam {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: conic-gradient(from 0deg, color-mix(in oklch, var(--stage-accent) 50%, transparent), transparent 70deg);
  transform: scaleX(-1);
  animation: spin 3s linear infinite;
}

/* scaleX(-1) puts the bright edge in front of the wedge as it turns clockwise */
@keyframes spin {
  from { transform: scaleX(-1) rotate(0deg); }
  to { transform: scaleX(-1) rotate(-360deg); }
}

.blip {
  position: absolute;
  width: 8px;
  height: 8px;
  margin: -4px 0 0 -4px;
  border-radius: 50%;
  background: var(--stage-accent);
  box-shadow: 0 0 10px var(--stage-accent);
  opacity: 0;
  animation: blip 3s linear infinite backwards;
}

@keyframes blip {
  0% { opacity: 0; transform: scale(1.6); }
  2% { opacity: 1; transform: scale(1.6); }
  40% { opacity: 0; transform: scale(1); }
  100% { opacity: 0; }
}

.wrap[data-paused] .beam,
.wrap[data-paused] .blip { animation-play-state: paused; }

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

@media (prefers-reduced-motion: reduce) {
  .beam, .blip { animation: none; }
  .blip { opacity: 0.5; }
}

投稿创意

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

你想投稿的是?

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