让图标动起来
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>