使用svg绘图的常用姿势

481 阅读2分钟

记录常见svg绘图姿势,包括可拖动的锚点、画圆角、画角度圆弧、画可以显示角度的三角形、自由多边形等等。

类型

1. 锚点

可以用鼠标拖着走

image.png

  • 用法
import React, { useState } from 'react';
import AnchorPoint from './anchor-point';

export interface Point {
  x: number;
  y: number;
}

export default () => {
    const [point, setPoint] = useState({ x: 100, y: 100 } as Point);
    return (
        <AnchorPoint x={point.x} y={point.y} onChange={setPoint} />
    );
};

  • 源码
import React, { useState, useEffect } from 'react';

export interface Point {
  x: number;
  y: number;
}

interface IProps {
  x?: number;
  y?: number;
  radius?: number;
  stroke?: string;
  strokeWidth?: number;
  fill?: string;
  onChange: (point: Point) => void;
}
const initPoint: Point = { x: -1, y: -1 };

export default (props: IProps) => {
  const {
    x = 0,
    y = 0,
    radius = 5,
    onChange = () => 1,
    stroke = '#999',
    strokeWidth = '1',
    fill = 'orange',
    ...rest
  } = props;
  const [startPoint, setStartPoint] = useState(initPoint);
  const [startAnchorPoint, setStartAnchorPoint] = useState(initPoint);
  const [moving, setMoving] = useState(false);

  // 鼠标拖动锚点
  const onMouseMove = (e: MouseEvent) => {
    const { pageX, pageY } = e;
    // 计算在鼠标相对锚点的坐标
    const newPoint = {
      x: pageX - startPoint.x + startAnchorPoint.x,
      y: pageY - startPoint.y + startAnchorPoint.y,
    };
    onChange(newPoint);
  };

  // 鼠标抬起锚点
  const onMouseUp = () => {
    document.removeEventListener('mousemove', onMouseMove, true);
    document.removeEventListener('mouseup', onMouseUp, true);
    setStartPoint(initPoint);
    setMoving(false);
  };
  // 鼠标按下锚点
  const onMouseDown = (e: React.MouseEvent) => {
    const { pageX, pageY } = e;
    setStartPoint({ x: pageX, y: pageY });
    setStartAnchorPoint({ x, y });
    setMoving(true);
  };

  useEffect(() => {
    if (moving) {
      document.addEventListener('mousemove', onMouseMove, true);
      document.addEventListener('mouseup', onMouseUp, true);
    }
  }, [moving]);

  return (
    <circle
      style={{ cursor: 'pointer', zIndex: 99 }}
      {...rest}
      stroke={stroke}
      strokeWidth={strokeWidth}
      fill={fill}
      cx={x}
      cy={y}
      r={radius}
      onMouseDown={onMouseDown}></circle>
  );
};

2. 贝塞尔曲线

image.png

export default () => {
    const A = { x: 100, y: 200 };
    const B = { x: 200, y: 100 };
    const C = { x: 300, y: 200 };
    return (
      <path
        d={`M${A.x},${A.y}
            Q${B.x} ${B.y} ${C.x} ${C.y}`}
        stroke="#000"
        strokeWidth="1"
        fill="none"
     />
    );
};

3. 圆角

image.png

// 计算两点之间的距离
export const distance = (p1: Point, p2: Point) => {
  return Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2));
};

// 计算角点
const getPoints = (
  start: Point,
  center: Point,
  end: Point,
  radius = 15,
) => {
  const A = start;
  const B = center;
  const C = end;
  const AB = distance(A, B);
  const CB = distance(B, C);
  return {
    startPoint: {
      x: B.x + (radius / AB) * (A.x - B.x),
      y: B.y + (radius / AB) * (A.y - B.y),
    },
    endPoint: {
      x: B.x + (radius / CB) * (C.x - B.x),
      y: B.y + (radius / CB) * (C.y - B.y),
    },
  };
};

const renderBorderRadiusPath = (A: Point, B: Point, C: Point) => {
    const { startPoint, endPoint } = getPoints(A, B, C);
    const borderRadius = 15;
    return (
      <path
        d={`M${startPoint.x},${startPoint.y}
            Q${B.x} ${B.y} ${endPoint.x} ${endPoint.y}`}
        stroke="#000"
        strokeWidth="1"
        fill="none"
      />
    );
};

4. 角度圆弧

// 计算两点之间的距离
export const distance = (p1: Point, p2: Point) => {
  return Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2));
};

// 计算角点
const getPoints = (
  start: Point,
  center: Point,
  end: Point,
  radius = 15,
) => {
  const A = start;
  const B = center;
  const C = end;
  const AB = distance(A, B);
  const CB = distance(B, C);
  return {
    startPoint: {
      x: B.x + (radius / AB) * (A.x - B.x),
      y: B.y + (radius / AB) * (A.y - B.y),
    },
    endPoint: {
      x: B.x + (radius / CB) * (C.x - B.x),
      y: B.y + (radius / CB) * (C.y - B.y),
    },
  };
};

const renderBorderRadiusPath = (A: Point, B: Point, C: Point) => {
    const { startPoint, endPoint } = getPoints(A, B, C);
    const borderRadius = 15;
    return (
      <path
        d={`M${startPoint.x},${startPoint.y}
          A${borderRadius},${borderRadius}  0,0,0
          ${endPoint.x},${endPoint.y}`}
        stroke="#000"
        strokeWidth="1"
        fill="none"
      />
    );
};

应用

1. 三角形

image.png

// 计算两点之间的距离
export const distance = (p1: Point, p2: Point) => {
  return Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2));
};

// 计算角点
const getPoints = (
  start: Point,
  center: Point,
  end: Point,
  radius = 15,
) => {
  const A = start;
  const B = center;
  const C = end;
  const AB = distance(A, B);
  const CB = distance(B, C);
  return {
    startPoint: {
      x: B.x + (radius / AB) * (A.x - B.x),
      y: B.y + (radius / AB) * (A.y - B.y),
    },
    endPoint: {
      x: B.x + (radius / CB) * (C.x - B.x),
      y: B.y + (radius / CB) * (C.y - B.y),
    },
  };
};

const renderBorderRadiusPath = (A: Point, B: Point, C: Point) => {
    const { startPoint, endPoint } = getPoints(A, B, C);
    const borderRadius = 15;
    return (
      <path
        d={`M${startPoint.x},${startPoint.y}
          A${borderRadius},${borderRadius}  0,0,0
          ${endPoint.x},${endPoint.y}`}
        stroke="#000"
        strokeWidth="1"
        fill="none"
      />
    );
};

export default () => {
    const A = { x: 200, y: 100 };
    const B = { x: 100, y: 200 };
    const C = { x: 300, y: 240 };
    return (
        <>
          <path
            d={`M${A.x},${A.y}
              L${B.x},${B.y}
              L${C.x},${C.y}
              Z`}
            stroke="#000"
            strokeWidth="1"
            fill="none"
          />
            {renderBorderRadiusPath(A, B, C)}
        </>
    );
};

2.自由多边形

image.png

import React from 'react';
// import Polygon from "./polygon";
import AnchorPoint from '../anchor-point';
import { Point } from './interface';
import './index.css';

export * from './interface';

export type Coordinate = Point;
interface IProps {
  radius?: number;
  borderRadius?: number;
  points?: Point[];
  onPointsChange: (points: Point[]) => void;
}

export default (props: IProps) => {
  const { points = [], radius = 5, onPointsChange, ...rest } = props;
  /** ******************** handler **********************************/

  /** ******************** render **********************************/
  return (
    <g>
      <polygon
        stroke="#000"
        fill="none"
        {...rest}
        points={points.map((p: Point) => `${p.x},${p.y}`).join(' ')}
      />
      {points.map((p: Point, index: number) => {
        return (
          <g key={`${p.x},${p.y}-g`}>
            {/* 锚点 */}
            <AnchorPoint
              x={p.x}
              y={p.y}
              radius={radius}
              onChange={(newPoint: Point) => {
                const newPoints = [...points];
                newPoints[index] = newPoint;
                onPointsChange(newPoints);
              }}
            />
          </g>
        );
      })}
    </g>
  );
};

  • 待补充