前端嘛 Logo
前端嘛

让图标动起来

2026-09-20
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <title>让图标动起来</title>
  <style>
    * {
      box-sizing: border-box;
    }

    body {
      min-height: 100vh;
      color: #222;
      background-color: #ececec;
      font-size: clamp(1rem, 2vw + 0.5rem, 2rem);
      font-family: "Space Grotesk", sans-serif;
      display: flex;
      justify-content: center;
      align-items: center;
    }

    canvas {
      width: 500px;
    }
  </style>

</head>

<body>
  <div id="app"></div>
  <script type="module">
    import React, { useState, useEffect, useRef, useMemo } from "https://esm.sh/react@19";
    import { createRoot } from "https://esm.sh/react-dom@19/client";
    import { renderToStaticMarkup } from 'https://esm.sh/react-dom@19/server';
    import rough from "https://esm.sh/roughjs";
    import { Dog, Rabbit, Fish, Bird, Turtle, Rocket } from "https://esm.sh/lucide-react";

    const App = () => {
      const [content, setContent] = useState('something something');
      const canvasRef = useRef(null);
      const lastDrawTimeRef = useRef(0);
      const rafRef = useRef(null);

      const icons = useMemo(() => {
        return [
          { icon: Dog, color: 'darkorange' },
          { icon: Rabbit, color: 'hotpink' },
          { icon: Bird, color: 'royalblue' },
          { icon: Fish, color: 'gold' },
          { icon: Turtle, color: 'green' },
          { icon: Rocket, color: 'red' }].
          map(({ icon, color }) => {
            var _svgEl$querySelectorA;
            const svg = renderToStaticMarkup(React.createElement(icon));
            const parser = new DOMParser();
            const doc = parser.parseFromString(svg, 'image/svg+xml');
            const svgEl = doc.querySelector('svg');
            const pathEls = (_svgEl$querySelectorA = svgEl === null || svgEl === void 0 ? void 0 : svgEl.querySelectorAll('path')) !== null && _svgEl$querySelectorA !== void 0 ? _svgEl$querySelectorA : [];
            const paths = [...pathEls].map(p => p.getAttribute('d'));
            return { paths, color };
          });
      }, []);

      const drawIcons = (cw, ch, ctx, rc) => {
        const scale = 2;
        const gap = 24;
        const baseSize = 24;

        const iconWidth = baseSize * scale;

        const totalWidth = icons.length * iconWidth + (icons.length - 1) * gap;

        let x = (cw - totalWidth) / 2;
        const y = (ch - iconWidth) / 2;

        for (const { paths, color } of icons) {
          ctx.save();
          ctx.translate(x, y);
          ctx.scale(scale, scale);

          for (const d of paths) {
            rc.path(d, {
              stroke: color,
              roughness: 0.7,
              strokeWidth: 1.5 / scale
            });

          }

          ctx.restore();
          x += iconWidth + gap;
        }

      };

      useEffect(() => {
        if (!canvasRef.current) return;

        const canvas = canvasRef.current;
        const ctx = canvas.getContext('2d');

        const dpr = window.devicePixelRatio || 1;
        canvas.width = canvas.clientWidth * dpr;
        canvas.height = canvas.clientHeight * dpr;
        ctx.scale(dpr, dpr);

        const rc = rough.canvas(canvas);

        const loop = t => {
          rafRef.current = requestAnimationFrame(loop);
          if (t - lastDrawTimeRef.current < 150) return;

          lastDrawTimeRef.current = t;
          ctx.clearRect(0, 0, canvas.width, canvas.height);

          drawIcons(canvas.clientWidth, canvas.clientHeight, ctx, rc);
        };

        rafRef.current = requestAnimationFrame(loop);

        return () => cancelAnimationFrame(rafRef.current);
      }, []);

      return /*#__PURE__*/(
        React.createElement("div", null, /*#__PURE__*/
          React.createElement("canvas", { ref: canvasRef })));


    };

    const root = createRoot(document.getElementById("app"));

    root.render( /*#__PURE__*/React.createElement(App, null));
  </script>

</body>

</html>