Created
March 26, 2025 09:49
-
-
Save Wxh16144/c7b9c14d8427fecb0b287cd36cc0de26 to your computer and use it in GitHub Desktop.
贝塞尔曲线可视化
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import React, { useEffect, useRef, useState } from 'react'; | |
| import { Flexbox } from 'react-layout-kit'; | |
| interface BezierVisualizerProps { | |
| controls: [number, number, number, number]; // [x1, y1, x2, y2] | |
| width?: number; | |
| height?: number; | |
| } | |
| const BezierVisualizer: React.FC<BezierVisualizerProps> = ({ | |
| controls: [x1, y1, x2, y2], | |
| width = 400, | |
| height = 400, | |
| }) => { | |
| const [progress, setProgress] = useState(0); | |
| const requestRef = useRef<number>(); | |
| const startTimeRef = useRef<number>(Date.now()); | |
| // 贝塞尔曲线参数方程 | |
| const getPosition = (t: number) => { | |
| const u = 1 - t; | |
| return { | |
| x: 3 * u * u * t * x1 + 3 * u * t * t * x2 + t * t * t, | |
| y: 3 * u * u * t * y1 + 3 * u * t * t * y2 + t * t * t, | |
| }; | |
| }; | |
| // 动画循环 | |
| const animate = () => { | |
| const elapsed = Date.now() - startTimeRef.current; | |
| const t = (elapsed % 2000) / 2000; // 2秒循环 | |
| setProgress(t); | |
| requestRef.current = requestAnimationFrame(animate); | |
| }; | |
| useEffect(() => { | |
| requestRef.current = requestAnimationFrame(animate); | |
| return () => cancelAnimationFrame(requestRef.current!); | |
| }, []); | |
| // 坐标转换到SVG视图 | |
| const scale = (val: number, axis: 'x' | 'y') => | |
| axis === 'x' ? val * width : height - val * height; | |
| // 当前小球位置 | |
| const { x, y } = getPosition(progress); | |
| const ballPos = { | |
| x: scale(x, 'x'), | |
| y: scale(y, 'y'), | |
| }; | |
| return ( | |
| <svg width={width} height={height} viewBox={`0 0 ${width} ${height}`}> | |
| {/* 网格背景 */} | |
| <pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse"> | |
| <path d="M 40 0 L 0 0 0 40" fill="none" stroke="#eee" strokeWidth="1" /> | |
| </pattern> | |
| <rect width="100%" height="100%" fill="url(#grid)" /> | |
| {/* 坐标轴 */} | |
| <path | |
| d={`M 0 ${height} L ${width} ${height} M 0 ${height} L 0 0`} | |
| stroke="#666" | |
| strokeWidth="2" | |
| /> | |
| {/* 贝塞尔曲线路径 */} | |
| <path | |
| d={` | |
| M 0 ${height} | |
| C ${scale(x1, 'x')} ${scale(y1, 'y')}, | |
| ${scale(x2, 'x')} ${scale(y2, 'y')}, | |
| ${width} 0 | |
| `} | |
| fill="none" | |
| stroke="#2196f3" | |
| strokeWidth="2" | |
| /> | |
| {/* 控制点连线 */} | |
| <path | |
| d={` | |
| M 0 ${height} | |
| L ${scale(x1, 'x')} ${scale(y1, 'y')} | |
| L ${scale(x2, 'x')} ${scale(y2, 'y')} | |
| L ${width} 0 | |
| `} | |
| stroke="#666" | |
| strokeDasharray="4 2" | |
| fill="none" | |
| /> | |
| {/* 控制点 */} | |
| <circle cx={scale(x1, 'x')} cy={scale(y1, 'y')} r="5" fill="#ff4757" /> | |
| <circle cx={scale(x2, 'x')} cy={scale(y2, 'y')} r="5" fill="#2ed573" /> | |
| {/* 运动小球 */} | |
| <circle cx={ballPos.x} cy={ballPos.y} r="8" fill="#ffa502" /> | |
| </svg> | |
| ); | |
| }; | |
| const App = () => { | |
| return ( | |
| <Flexbox wrap="wrap" gap={16} horizontal> | |
| <BezierVisualizer controls={[0.78, 0.14, 0.15, 0.86]} /> | |
| <BezierVisualizer controls={[0.755, 0.05, 0.855, 0.06]} /> | |
| <BezierVisualizer controls={[0.08, 0.82, 0.17, 1]} /> | |
| <BezierVisualizer controls={[0.23, 1, 0.32, 1]} /> | |
| </Flexbox> | |
| ); | |
| }; | |
| export default App; | |
| // export default BezierVisualizer; |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Kapture.2025-03-26.at.17.49.54.mp4