# 基础图形

插件内置了丰富的基础图形,如下是相关实例,你还可以尝试拖拽他们。

点击以展开或折叠内置图形 Shape 类型定义
import { Point } from '../core/graph'
import { BezierCurve } from '@jiaminghi/bezier-curve/types/types'

export type CircleShape = {
  rx: number
  ry: number
  r: number
}

export type EllipseShape = {
  rx: number
  ry: number
  hr: number
  vr: number
}

export type RectShape = {
  x: number
  y: number
  w: number
  h: number
}

export type RingShape = {
  rx: number
  ry: number
  r: number
}

export type ArcShape = {
  rx: number
  ry: number
  r: number
  startAngle: number
  endAngle: number
  clockWise: boolean
}

export type SectorShape = {
  rx: number
  ry: number
  r: number
  startAngle: number
  endAngle: number
  clockWise: boolean
}

export type RegPolygonShape = {
  rx: number
  ry: number
  r: number
  side: number
}

export type RegPolygonShapeCache = {
  points?: Point[]
} & Partial<RegPolygonShape>

export type PolylineShape = {
  points: Point[]
  close: boolean
}

export type SmoothlineShape = {
  points: Point[]
  close: boolean
}

export type SmoothlineShapeCache = {
  points?: Point[]
  bezierCurve?: BezierCurve
  hoverPoints?: Point[]
}

export type BezierCurveShape = {
  points: BezierCurve | []
  close: boolean
}

export type BezierCurveShapeCache = {
  points?: BezierCurve
  hoverPoints?: Point[]
}

export type TextShape = {
  content: string
  position: [number, number]
  maxWidth: undefined | number
  rowGap: number
}

export type TextShapeCache = {
  x?: number
  y?: number
  w?: number
  h?: number
}

# 圆形

shape 属性表

属性名 类型 默认值 注解
rx number 0 圆心x轴坐标
ry number 0 圆心y轴坐标
r number 0 圆半径
点击以展开或折叠演示配置
export default function (render) {
  const {
    area: [w, h],
  } = render

  return {
    name: 'Circle',
    animationCurve: 'easeOutBack',
    hover: true,
    drag: true,
    shape: {
      rx: w / 2,
      ry: h / 2,
      r: 50,
    },
    style: {
      fill: '#9ce5f4',
      shadowBlur: 0,
      shadowColor: '#66eece',
      hoverCursor: 'pointer',
    },
    onMouseEnter(e) {
      this.animation('shape', { r: 70 }, true)
      this.animation('style', { shadowBlur: 20 })
    },
    onMouseOuter(e) {
      this.animation('shape', { r: 50 }, true)
      this.animation('style', { shadowBlur: 0 })
    },
  }
}
点击以展开或折叠 Circle 实现
import { CircleShape } from '../types/graphs/shape'
import { checkPointIsInCircle } from '../utils/graphs'
import Graph from '../core/graph.class'
import { GraphConfig, Point } from '../types/core/graph'

class Circle extends Graph<CircleShape> {
  name = 'circle'

  constructor(config: GraphConfig<Partial<CircleShape>>) {
    super(
      Graph.mergeDefaultShape(
        {
          rx: 0,
          ry: 0,
          r: 0,
        },
        config,
        ({ shape: { rx, ry, r } }) => {
          if (typeof rx !== 'number' || typeof ry !== 'number' || typeof r !== 'number')
            throw new Error('CRender Graph Circle: Circle shape configuration is invalid!')
        }
      )
    )
  }

  draw(): void {
    const {
      shape,
      render: { ctx },
    } = this
    const { rx, ry, r } = shape

    ctx.beginPath()
    ctx.arc(rx, ry, r > 0 ? r : 0, 0, Math.PI * 2)

    ctx.fill()
    ctx.stroke()
  }

  hoverCheck(point: Point): boolean {
    const { shape } = this

    return checkPointIsInCircle(point, shape)
  }

  setGraphCenter(): void {
    const { shape, style } = this
    const { rx, ry } = shape

    style.graphCenter = [rx, ry]
  }

  move({ movementX, movementY }: MouseEvent): void {
    const { shape } = this

    this.attr('shape', {
      rx: shape.rx + movementX,
      ry: shape.ry + movementY,
    })
  }
}

export default Circle

# 椭圆形

shape 属性表

属性名 类型 默认值 注解
rx number 0 圆心x轴坐标
ry number 0 圆心y轴坐标
hr number 0 横轴半径
vr number 0 竖轴半径
点击以展开或折叠演示配置
export default function (render) {
  const {
    area: [w, h],
  } = render

  return {
    name: 'Ellipse',
    animationCurve: 'easeOutBack',
    hover: true,
    drag: true,
    shape: {
      rx: w / 2,
      ry: h / 2,
      hr: 80,
      vr: 30,
    },
    style: {
      fill: '#9ce5f4',
      shadowBlur: 0,
      shadowColor: '#66eece',
      scale: [1, 1],
      hoverCursor: 'pointer',
    },
    onMouseEnter(e) {
      this.animation('style', { scale: [1.5, 1.5], shadowBlur: 20 })
    },
    onMouseOuter(e) {
      this.animation('style', { scale: [1, 1], shadowBlur: 0 })
    },
  }
}
点击以展开或折叠 Ellipse 实现
import { EllipseShape } from '../types/graphs/shape'
import { getTwoPointDistance } from '../utils/graphs'
import { Point, GraphConfig } from '../types/core/graph'
import Graph from '../core/graph.class'

class Ellipse extends Graph<EllipseShape> {
  name = 'ellipse'

  constructor(config: GraphConfig<Partial<EllipseShape>>) {
    super(
      Graph.mergeDefaultShape(
        {
          rx: 0,
          ry: 0,
          hr: 0,
          vr: 0,
        },
        config,
        ({ shape: { rx, ry, hr, vr } }) => {
          if (
            typeof rx !== 'number' ||
            typeof ry !== 'number' ||
            typeof hr !== 'number' ||
            typeof vr !== 'number'
          )
            throw new Error('CRender Graph Ellipse: Ellipse shape configuration is invalid!')
        }
      )
    )
  }

  draw(): void {
    const {
      shape,
      render: { ctx },
    } = this
    const { rx, ry, hr, vr } = shape

    ctx.beginPath()
    ctx.ellipse(rx, ry, hr > 0 ? hr : 0, vr > 0 ? vr : 0, 0, 0, Math.PI * 2)

    ctx.fill()
    ctx.stroke()
  }

  hoverCheck(point: Point): boolean {
    const { shape } = this
    const { rx, ry, hr, vr } = shape

    const a = Math.max(hr, vr)
    const b = Math.min(hr, vr)

    const c = Math.sqrt(a * a - b * b)

    const leftFocusPoint: Point = [rx - c, ry]
    const rightFocusPoint: Point = [rx + c, ry]

    const distance =
      getTwoPointDistance(point, leftFocusPoint) + getTwoPointDistance(point, rightFocusPoint)

    return distance <= 2 * a
  }

  setGraphCenter(): void {
    const { shape, style } = this
    const { rx, ry } = shape

    style.graphCenter = [rx, ry]
  }

  move({ movementX, movementY }: MouseEvent): void {
    const { shape } = this

    this.attr('shape', {
      rx: shape.rx + movementX,
      ry: shape.ry + movementY,
    })
  }
}

export default Ellipse

# 矩形

shape 属性表

属性名 类型 默认值 注解
x number 0 矩形左上角x轴坐标
y number 0 矩形左上角y轴坐标
w number 0 矩形宽度
h number 0 矩形高度
点击以展开或折叠演示配置
export default function (render) {
  const {
    area: [w, h],
  } = render

  const rectWidth = 200
  const rectHeight = 50

  return {
    name: 'Rect',
    animationCurve: 'easeOutBack',
    hover: true,
    drag: true,
    shape: {
      x: w / 2 - rectWidth / 2,
      y: h / 2 - rectHeight / 2,
      w: rectWidth,
      h: rectHeight,
    },
    style: {
      fill: '#9ce5f4',
      shadowBlur: 0,
      shadowColor: '#66eece',
      hoverCursor: 'pointer',
      translate: [0, 0],
    },
    onMouseEnter(e) {
      this.animation('shape', { w: 400 }, true)
      this.animation('style', { shadowBlur: 20, translate: [-100, 0] })
    },
    onMouseOuter(e) {
      this.animation('shape', { w: 200 }, true)
      this.animation('style', { shadowBlur: 0, translate: [0, 0] })
    },
  }
}
点击以展开或折叠 Rect 实现
import { RectShape } from '../types/graphs/shape'
import { checkPointIsInRect } from '../utils/graphs'
import Graph from '../core/graph.class'
import { GraphConfig, Point } from '../types/core/graph'

class Rect extends Graph<RectShape> {
  name = 'rect'

  constructor(config: GraphConfig<Partial<RectShape>>) {
    super(
      Graph.mergeDefaultShape(
        {
          x: 0,
          y: 0,
          w: 0,
          h: 0,
        },
        config,
        ({ shape: { x, y, w, h } }) => {
          if (
            typeof x !== 'number' ||
            typeof y !== 'number' ||
            typeof w !== 'number' ||
            typeof h !== 'number'
          )
            throw new Error('CRender Graph Rect: Rect shape configuration is invalid!')
        }
      )
    )
  }

  draw(): void {
    const {
      shape,
      render: { ctx },
    } = this
    const { x, y, w, h } = shape

    ctx.beginPath()
    ctx.rect(x, y, w, h)

    ctx.fill()
    ctx.stroke()
  }

  hoverCheck(point: Point): boolean {
    const { shape } = this

    return checkPointIsInRect(point, shape)
  }

  setGraphCenter(): void {
    const { shape, style } = this
    const { x, y, w, h } = shape

    style.graphCenter = [x + w / 2, y + h / 2]
  }

  move({ movementX, movementY }: MouseEvent): void {
    const { shape } = this

    this.attr('shape', {
      x: shape.x + movementX,
      y: shape.y + movementY,
    })
  }
}

export default Rect

# 环形

shape 属性表

属性名 类型 默认值 注解
rx number 0 中心点x轴坐标
ry number 0 中心点y轴坐标
r number 0 环半径
点击以展开或折叠演示配置
export default function (render) {
  const {
    area: [w, h],
  } = render

  return {
    name: 'Ring',
    animationCurve: 'easeOutBack',
    hover: true,
    drag: true,
    shape: {
      rx: w / 2,
      ry: h / 2,
      r: 50,
    },
    style: {
      stroke: '#9ce5f4',
      lineWidth: 20,
      hoverCursor: 'pointer',
      shadowBlur: 0,
      shadowColor: '#66eece',
    },
    onMouseEnter(e) {
      this.animation('style', { shadowBlur: 20, lineWidth: 30 })
    },
    onMouseOuter(e) {
      this.animation('style', { shadowBlur: 0, lineWidth: 20 })
    },
  }
}
点击以展开或折叠 Ring 实现
import { RingShape } from '../types/graphs/shape'
import { getTwoPointDistance } from '../utils/graphs'
import Graph from '../core/graph.class'
import { GraphConfig, Point } from '../types/core/graph'

class Ring extends Graph<RingShape> {
  name = 'ring'

  constructor(config: GraphConfig<Partial<RingShape>>) {
    super(
      Graph.mergeDefaultShape(
        {
          rx: 0,
          ry: 0,
          r: 0,
        },
        config,
        ({ shape: { rx, ry, r } }) => {
          if (typeof rx !== 'number' || typeof ry !== 'number' || typeof r !== 'number')
            throw new Error('CRender Graph Ring: Ring shape configuration is invalid!')
        }
      )
    )
  }

  draw(): void {
    const {
      shape,
      render: { ctx },
    } = this
    const { rx, ry, r } = shape

    ctx.beginPath()
    ctx.arc(rx, ry, r > 0 ? r : 0, 0, Math.PI * 2)

    ctx.stroke()
  }

  hoverCheck(point: Point): boolean {
    const { shape, style } = this
    const { rx, ry, r } = shape

    const { lineWidth } = style

    const halfLineWidth = lineWidth / 2

    const minDistance = r - halfLineWidth
    const maxDistance = r + halfLineWidth

    const distance = getTwoPointDistance(point, [rx, ry])

    return distance >= minDistance && distance <= maxDistance
  }

  setGraphCenter(): void {
    const { shape, style } = this
    const { rx, ry } = shape

    style.graphCenter = [rx, ry]
  }

  move({ movementX, movementY }: MouseEvent): void {
    const { shape } = this

    this.attr('shape', {
      rx: shape.rx + movementX,
      ry: shape.ry + movementY,
    })
  }
}

export default Ring

# 弧形

shape 属性表

属性名 类型 默认值 注解
rx number 0 中心点x轴坐标
ry number 0 中心点y轴坐标
r number 0 弧半径
startAngle number 0 弧起始弧度值
endAngle number 0 弧结束弧度值
clockWise boolean true 是否顺时针
点击以展开或折叠演示配置
export default function (render) {
  const {
    area: [w, h],
  } = render

  return {
    name: 'Arc',
    animationCurve: 'easeOutBack',
    hover: true,
    drag: true,
    shape: {
      rx: w / 2,
      ry: h / 2,
      r: 60,
      startAngle: 0,
      endAngle: Math.PI / 3,
    },
    style: {
      stroke: '#9ce5f4',
      lineWidth: 20,
      shadowBlur: 0,
      rotate: 0,
      shadowColor: '#66eece',
      hoverCursor: 'pointer',
    },
    onMouseEnter() {
      this.animation('shape', { endAngle: Math.PI }, true)
      this.animation('style', { shadowBlur: 20, rotate: -30, lineWidth: 30 })
    },
    onMouseOuter() {
      this.animation('shape', { endAngle: Math.PI / 3 }, true)
      this.animation('style', { shadowBlur: 0, rotate: 0, lineWidth: 20 })
    },
  }
}
点击以展开或折叠 Arc 实现
import Graph from '../core/graph.class'
import { ArcShape } from '../types/graphs/shape'
import { checkPointIsInSector } from '../utils/graphs'
import { GraphConfig, Point } from '../types/core/graph'

class Arc extends Graph<ArcShape> {
  name = 'arc'

  constructor(config: GraphConfig<Partial<ArcShape>>) {
    super(
      Graph.mergeDefaultShape(
        {
          rx: 0,
          ry: 0,
          r: 0,
          startAngle: 0,
          endAngle: 0,
          clockWise: true,
        },
        config,
        ({ shape }) => {
          const keys: (keyof ArcShape)[] = ['rx', 'ry', 'r', 'startAngle', 'endAngle']

          if (keys.find(key => typeof shape[key] !== 'number'))
            throw new Error('CRender Graph Arc: Arc shape configuration is invalid!')
        }
      )
    )
  }

  draw(): void {
    const {
      shape,
      render: { ctx },
    } = this

    const { rx, ry, r, startAngle, endAngle, clockWise } = shape

    ctx.beginPath()
    ctx.arc(rx, ry, r > 0 ? r : 0, startAngle, endAngle, !clockWise)

    ctx.stroke()
  }

  hoverCheck(point: Point): boolean {
    const { shape, style } = this

    const { rx, ry, r, startAngle, endAngle, clockWise } = shape

    const { lineWidth } = style

    const halfLineWidth = lineWidth / 2

    const insideRadius = r - halfLineWidth
    const outsideRadius = r + halfLineWidth

    const inSide = checkPointIsInSector(point, {
      rx,
      ry,
      r: insideRadius,
      startAngle,
      endAngle,
      clockWise,
    })

    const outSide = checkPointIsInSector(point, {
      rx,
      ry,
      r: outsideRadius,
      startAngle,
      endAngle,
      clockWise,
    })

    return !inSide && outSide
  }

  setGraphCenter(): void {
    const { shape, style } = this

    const { rx, ry } = shape

    style.graphCenter = [rx, ry]
  }

  move({ movementX, movementY }: MouseEvent): void {
    const { shape } = this

    this.attr('shape', {
      rx: shape.rx + movementX,
      ry: shape.ry + movementY,
    })
  }
}

export default Arc

# 扇形

shape 属性表

属性名 类型 默认值 注解
rx number 0 中心点x轴坐标
ry number 0 中心点y轴坐标
r number 0 扇形半径
startAngle number 0 扇形起始弧度值
endAngle number 0 扇形结束弧度值
clockWise boolean true 是否顺时针
点击以展开或折叠演示配置
export default function (render) {
  const {
    area: [w, h],
  } = render

  return {
    name: 'Sector',
    animationCurve: 'easeOutBack',
    hover: true,
    drag: true,
    shape: {
      rx: w / 2,
      ry: h / 2,
      r: 60,
      startAngle: 0,
      endAngle: Math.PI / 3,
    },
    style: {
      fill: '#9ce5f4',
      shadowBlur: 0,
      rotate: 0,
      shadowColor: '#66eece',
      hoverCursor: 'pointer',
    },
    onMouseEnter(e) {
      this.animation('shape', { endAngle: Math.PI, r: 70 }, true)
      this.animation('style', { shadowBlur: 20, rotate: -30, lineWidth: 30 })
    },
    onMouseOuter(e) {
      this.animation('shape', { endAngle: Math.PI / 3, r: 60 }, true)
      this.animation('style', { shadowBlur: 0, rotate: 0, lineWidth: 20 })
    },
  }
}
点击以展开或折叠 Sector 实现
import { SectorShape } from '../types/graphs/shape'
import { checkPointIsInSector } from '../utils/graphs'
import Graph from '../core/graph.class'
import { GraphConfig, Point } from '../types/core/graph'

class Sector extends Graph<SectorShape> {
  name = 'sector'

  constructor(config: GraphConfig<Partial<SectorShape>>) {
    super(
      Graph.mergeDefaultShape(
        {
          rx: 0,
          ry: 0,
          r: 0,
          startAngle: 0,
          endAngle: 0,
          clockWise: true,
        },
        config,
        ({ shape }) => {
          const keys: (keyof SectorShape)[] = ['rx', 'ry', 'r', 'startAngle', 'endAngle']

          if (keys.find(key => typeof shape[key] !== 'number'))
            throw new Error('CRender Graph Sector: Sector shape configuration is invalid!')
        }
      )
    )
  }

  draw(): void {
    const {
      shape,
      render: { ctx },
    } = this
    const { rx, ry, r, startAngle, endAngle, clockWise } = shape

    ctx.beginPath()
    ctx.arc(rx, ry, r > 0 ? r : 0, startAngle, endAngle, !clockWise)
    ctx.lineTo(rx, ry)
    ctx.closePath()

    ctx.stroke()
    ctx.fill()
  }

  hoverCheck(point: Point): boolean {
    const { shape } = this

    return checkPointIsInSector(point, shape)
  }

  setGraphCenter(): void {
    const { shape, style } = this
    const { rx, ry } = shape

    style.graphCenter = [rx, ry]
  }

  move({ movementX, movementY }: MouseEvent): void {
    const { shape } = this
    const { rx, ry } = shape

    this.attr('shape', {
      rx: rx + movementX,
      ry: ry + movementY,
    })
  }
}

export default Sector

# 正多边形

shape 属性表

属性名 类型 默认值 注解
rx number 0 中心点x轴坐标
ry number 0 中心点y轴坐标
r number 0 外接圆半径
side number 0 边数
点击以展开或折叠演示配置
export default function (render) {
  const {
    area: [w, h],
  } = render

  return {
    name: 'RegPolygon',
    animationCurve: 'easeOutBack',
    hover: true,
    drag: true,
    shape: {
      rx: w / 2,
      ry: h / 2,
      r: 60,
      side: 6,
    },
    style: {
      fill: '#9ce5f4',
      hoverCursor: 'pointer',
      shadowBlur: 0,
      rotate: 0,
      shadowColor: '#66eece',
    },
    onMouseEnter(e) {
      this.animation('shape', { endAngle: Math.PI, r: 100 }, true)
      this.animation('style', { shadowBlur: 20, rotate: 180 })
    },
    onMouseOuter(e) {
      this.animation('shape', { endAngle: Math.PI / 3, r: 60 }, true)
      this.animation('style', { shadowBlur: 0, rotate: 0 })
    },
  }
}
点击以展开或折叠 RegPolygon 实现
import { RegPolygonShape, RegPolygonShapeCache } from '../types/graphs/shape'
import { getRegularPolygonPoints, checkPointIsInPolygon } from '../utils/graphs'
import { drawPolylinePath } from '../utils/canvas'
import Graph from '../core/graph.class'
import { GraphConfig, Point } from '../types/core/graph'

class RegPolygon extends Graph<RegPolygonShape> {
  name = 'regPolygon'

  private cache: RegPolygonShapeCache = {}

  constructor(config: GraphConfig<Partial<RegPolygonShape>>) {
    super(
      Graph.mergeDefaultShape(
        {
          rx: 0,
          ry: 0,
          r: 0,
          side: 0,
        },
        config,
        ({ shape }) => {
          const { side } = shape

          const keys: (keyof RegPolygonShape)[] = ['rx', 'ry', 'r', 'side']

          if (keys.find(key => typeof shape[key] !== 'number'))
            throw new Error('CRender Graph RegPolygon: RegPolygon shape configuration is invalid!')

          if (side! < 3) throw new Error('CRender Graph RegPolygon: RegPolygon at least trigon!')
        }
      )
    )
  }

  draw(): void {
    const {
      shape,
      cache,
      render: { ctx },
    } = this
    const { rx, ry, r, side } = shape

    if (
      cache.points ||
      cache.rx !== rx ||
      cache.ry !== ry ||
      cache.r !== r ||
      cache.side !== side
    ) {
      const points = getRegularPolygonPoints(shape)

      Object.assign(cache, { points, rx, ry, r, side })
    }

    const { points } = cache!

    ctx.beginPath()
    drawPolylinePath(ctx, points!)
    ctx.closePath()

    ctx.stroke()
    ctx.fill()
  }

  hoverCheck(point: Point): boolean {
    const { points } = this.cache!

    return checkPointIsInPolygon(point, points!)
  }

  setGraphCenter(): void {
    const { shape, style } = this
    const { rx, ry } = shape

    style.graphCenter = [rx, ry]
  }

  move({ movementX, movementY }: MouseEvent): void {
    const { shape, cache } = this
    const { rx, ry } = shape

    cache.rx! += movementX
    cache.ry! += movementY

    this.attr('shape', {
      rx: rx + movementX,
      ry: ry + movementY,
    })

    cache.points = cache.points!.map(([x, y]) => [x + movementX, y + movementY])
  }
}

export default RegPolygon

# 折线

shape 属性表

属性名 类型 默认值 注解
points Point[] [] 构成折线的点
close boolean false 是否闭合折线
点击以展开或折叠演示配置
export default function (render) {
  const {
    area: [w, h],
  } = render

  const top = h / 3
  const bottom = (h / 3) * 2
  const gap = w / 10

  const beginX = w / 2 - gap * 2

  const points = new Array(5).fill('').map((t, i) => [beginX + gap * i, i % 2 === 0 ? top : bottom])

  return {
    name: 'Polyline',
    animationCurve: 'easeOutBack',
    hover: true,
    drag: true,
    shape: {
      points,
    },
    style: {
      stroke: '#9ce5f4',
      shadowBlur: 0,
      lineWidth: 10,
      shadowColor: '#66eece',
      hoverCursor: 'pointer',
    },
    onMouseEnter(e) {
      this.animation('style', { lineWidth: 20, shadowBlur: 20 })
    },
    onMouseOuter(e) {
      this.animation('style', { lineWidth: 10, shadowBlur: 0 })
    },
  }
}
点击以展开或折叠 Polyline 实现
import { PolylineShape } from '../types/graphs/shape'
import { drawPolylinePath } from '../utils/canvas'
import { eliminateBlur, checkPointIsInPolygon, checkPointIsNearPolyline } from '../utils/graphs'
import Graph from '../core/graph.class'
import { GraphConfig, Point } from '../types/core/graph'

class Polyline extends Graph<PolylineShape> {
  name = 'polyline'

  constructor(config: GraphConfig<Partial<PolylineShape>>) {
    super(
      Graph.mergeDefaultShape(
        {
          points: [],
          close: false,
        },
        config,
        ({ shape: { points } }) => {
          if (!(points instanceof Array))
            throw new Error('CRender Graph Polyline: Polyline points should be an array!')
        }
      )
    )
  }

  draw(): void {
    const {
      shape,
      style: { lineWidth },
      render: { ctx },
    } = this
    const { points, close } = shape

    ctx.beginPath()
    drawPolylinePath(ctx, lineWidth === 1 ? eliminateBlur(points) : points)

    if (close) {
      ctx.closePath()

      ctx.fill()
      ctx.stroke()
    } else {
      ctx.stroke()
    }
  }

  hoverCheck(point: Point): boolean {
    const { shape, style } = this
    const { points, close } = shape

    const { lineWidth } = style

    if (close) {
      return checkPointIsInPolygon(point, points)
    } else {
      return checkPointIsNearPolyline(point, points, lineWidth)
    }
  }

  setGraphCenter(): void {
    const { shape, style } = this
    const { points } = shape

    style.graphCenter = points[0]
  }

  move({ movementX, movementY }: MouseEvent): void {
    const {
      shape: { points },
    } = this

    const moveAfterPoints = points.map(([x, y]) => [x + movementX, y + movementY])

    this.attr('shape', {
      points: moveAfterPoints,
    })
  }
}

export default Polyline

# 折线(闭合)

点击以展开或折叠演示配置
import { deepClone } from '../../../es/utils/common'

export default function (render) {
  const {
    area: [w, h],
  } = render

  const top = h / 3
  const bottom = (h / 3) * 2
  const gap = w / 10

  const beginX = w / 2 - gap * 2

  const points = new Array(5).fill('').map((t, i) => [beginX + gap * i, i % 2 === 0 ? top : bottom])

  points[2][1] += top * 1.3

  return {
    name: 'Polyline',
    animationCurve: 'easeOutBack',
    hover: true,
    drag: true,
    shape: {
      points,
      close: true,
    },
    style: {
      fill: '#9ce5f4',
      shadowBlur: 0,
      lineWidth: 10,
      shadowColor: '#66eece',
      hoverCursor: 'pointer',
    },
    onMouseEnter(e) {
      this.animation('style', { shadowBlur: 20 }, true)
      const pointsCloned = deepClone(this.shape.points)
      pointsCloned[2][1] += top * 0.3
      this.animation('shape', { points: pointsCloned })
    },
    onMouseOuter(e) {
      this.animation('style', { shadowBlur: 0 }, true)
      const pointsCloned = deepClone(this.shape.points)
      pointsCloned[2][1] -= top * 0.3
      this.animation('shape', { points: pointsCloned })
    },
  }
}

# 光滑曲线

shape 属性表

属性名 类型 默认值 注解
points Point[] [] 构成光滑曲线的点
close boolean false 是否闭合光滑曲线
点击以展开或折叠演示配置
export default function (render) {
  const {
    area: [w, h],
  } = render

  const top = h / 3
  const bottom = (h / 3) * 2
  const gap = w / 10

  const beginX = w / 2 - gap * 2

  const points = new Array(5).fill('').map((t, i) => [beginX + gap * i, i % 2 === 0 ? top : bottom])

  return {
    name: 'Smoothline',
    animationCurve: 'easeOutBack',
    hover: true,
    drag: true,
    shape: {
      points,
    },
    style: {
      stroke: '#9ce5f4',
      shadowBlur: 0,
      lineWidth: 10,
      shadowColor: '#66eece',
      hoverCursor: 'pointer',
    },
    onMouseEnter(e) {
      this.animation('style', { lineWidth: 20, shadowBlur: 20 })
    },
    onMouseOuter(e) {
      this.animation('style', { lineWidth: 10, shadowBlur: 0 })
    },
  }
}
点击以展开或折叠 Smoothline 实现
import { Point, GraphConfig } from '../types/core/graph'
import { SmoothlineShape, SmoothlineShapeCache } from '../types/graphs/shape'
import { deepClone } from '../utils/common'
import { drawBezierCurvePath } from '../utils/canvas'
import { checkPointIsInPolygon, checkPointIsNearPolyline } from '../utils/graphs'
import Graph from '../core/graph.class'
import { polylineToBezierCurve, bezierCurveToPolyline } from '@jiaminghi/bezier-curve'
import { BezierCurveSegment, BezierCurve } from '@jiaminghi/bezier-curve/types/types'

class Smoothline extends Graph<SmoothlineShape> {
  name = 'smoothline'

  private cache: SmoothlineShapeCache = {}

  constructor(config: GraphConfig<Partial<SmoothlineShape>>) {
    super(
      Graph.mergeDefaultShape(
        {
          points: [],
          close: false,
        },
        config,
        ({ shape: { points } }) => {
          if (!(points instanceof Array))
            throw new Error('CRender Graph Smoothline: Smoothline points should be an array!')
        }
      )
    )
  }

  draw(): void {
    const {
      shape,
      cache,
      render: { ctx },
    } = this
    const { points, close } = shape

    if (!cache.points || cache.points.toString() !== points.toString()) {
      const bezierCurve = polylineToBezierCurve(points, close)
      const hoverPoints = bezierCurveToPolyline(bezierCurve)

      Object.assign(cache, {
        points: deepClone(points),
        bezierCurve,
        hoverPoints,
      })
    }

    const { bezierCurve } = cache

    ctx.beginPath()

    drawBezierCurvePath(ctx, bezierCurve!.slice(1) as Point[][], bezierCurve![0])

    if (close) {
      ctx.closePath()

      ctx.fill()
      ctx.stroke()
    } else {
      ctx.stroke()
    }
  }

  hoverCheck(point: Point): boolean {
    const { cache, shape, style } = this
    const { hoverPoints } = cache

    const { close } = shape

    const { lineWidth } = style

    if (close) {
      return checkPointIsInPolygon(point, hoverPoints!)
    } else {
      return checkPointIsNearPolyline(point, hoverPoints!, lineWidth)
    }
  }

  setGraphCenter(): void {
    const {
      shape: { points },
      style,
    } = this

    style.graphCenter = points[0]
  }

  move({ movementX, movementY }: MouseEvent): void {
    const { shape, cache } = this
    const { points } = shape

    const moveAfterPoints = points.map<Point>(([x, y]) => [x + movementX, y + movementY])

    cache.points = moveAfterPoints

    const [fx, fy] = cache.bezierCurve![0]
    const curves = cache.bezierCurve!.slice(1)

    cache.bezierCurve = [
      [fx + movementX, fy + movementY],
      ...(curves as BezierCurveSegment[]).map(curve =>
        curve.map<Point>(([x, y]) => [x + movementX, y + movementY])
      ),
    ] as BezierCurve

    cache.hoverPoints = cache.hoverPoints!.map(([x, y]) => [x + movementX, y + movementY])

    this.attr('shape', {
      points: moveAfterPoints,
    })
  }
}

export default Smoothline

# 光滑曲线(闭合)

点击以展开或折叠演示配置
import { getCircleRadianPoint } from '../../../es/utils/graphs'

function getPoints(radius, centerPoint, pointNum) {
  const PIDived = (Math.PI * 2) / pointNum

  const points = new Array(pointNum)
    .fill('')
    .map((foo, i) => getCircleRadianPoint(...centerPoint, radius, PIDived * i))

  return points
}

export default function (render) {
  const {
    area: [w, h],
  } = render

  const radius = h / 3
  const centerPoint = [w / 2, h / 2]

  return {
    name: 'Smoothline',
    animationCurve: 'easeOutBack',
    hover: true,
    drag: true,
    shape: {
      points: getPoints(radius, centerPoint, 3),
      close: true,
    },
    style: {
      fill: '#9ce5f4',
      shadowBlur: 0,
      lineWidth: 10,
      shadowColor: '#66eece',
      hoverCursor: 'pointer',
      rotate: 0,
    },
    onMouseEnter(e) {
      this.animation('style', { lineWidth: 20, shadowBlur: 20, rotate: 120 })
    },
    onMouseOuter(e) {
      this.animation('style', { lineWidth: 10, shadowBlur: 0, rotate: 0 })
    },
    setGraphCenter(e) {
      const { style } = this

      if (e) {
        const { movementX, movementY } = e
        const [cx, cy] = style.graphCenter

        style.graphCenter = [cx + movementX, cy + movementY]
      } else {
        style.graphCenter = [...centerPoint]
      }
    },
  }
}

# 贝塞尔曲线

shape 属性表

属性名 类型 默认值 注解
points BezierCurve | [] [] 构成贝塞尔曲线的点
close boolean false 是否闭合贝塞尔曲线
点击以展开或折叠演示配置
export default function (render) {
  const {
    area: [w, h],
  } = render

  const offsetX = w / 2
  const offsetY = h / 2

  const points = [
    // 起始点
    [-100 + offsetX, -50 + offsetY],
    // N组贝塞尔曲线数据
    [
      // 贝塞尔曲线控制点1,控制点2,结束点
      [0 + offsetX, -50 + offsetY],
      [0 + offsetX, 50 + offsetY],
      [100 + offsetX, 50 + offsetY],
    ],
  ]

  return {
    name: 'BezierCurve',
    animationCurve: 'easeOutBack',
    hover: true,
    drag: true,
    shape: {
      points,
    },
    style: {
      lineWidth: 10,
      stroke: '#9ce5f4',
      shadowBlur: 0,
      shadowColor: '#66eece',
      hoverCursor: 'pointer',
    },
    onMouseEnter(e) {
      this.animation('style', { lineWidth: 20, shadowBlur: 20 })
    },
    onMouseOuter(e) {
      this.animation('style', { lineWidth: 10, shadowBlur: 0 })
    },
  }
}
点击以展开或折叠 BezierCurve 实现
import { Point, GraphConfig } from '../types/core/graph'
import { BezierCurveShape, BezierCurveShapeCache } from '../types/graphs/shape'
import { deepClone } from '../utils/common'
import { drawBezierCurvePath } from '../utils/canvas'
import { checkPointIsInPolygon, checkPointIsNearPolyline } from '../utils/graphs'
import Graph from '../core/graph.class'
import { bezierCurveToPolyline } from '@jiaminghi/bezier-curve'
import {
  BezierCurveSegment,
  BezierCurve as BezierCurveType,
} from '@jiaminghi/bezier-curve/types/types'

class BezierCurve extends Graph<BezierCurveShape> {
  name = 'bezierCurve'

  private cache: BezierCurveShapeCache = {}

  constructor(config: GraphConfig<Partial<BezierCurveShape>>) {
    super(
      Graph.mergeDefaultShape(
        {
          points: [],
          close: false,
        },
        config,
        ({ shape: { points } }) => {
          if (!(points instanceof Array))
            throw new Error('CRender Graph BezierCurve: BezierCurve points should be an array!')
        }
      )
    )
  }

  draw(): void {
    const { shape, cache, render } = this
    const { points, close } = shape
    const { ctx } = render

    if (!cache.points || cache.points.toString() !== points.toString()) {
      const hoverPoints = bezierCurveToPolyline(points as BezierCurveType, 20)

      Object.assign(cache, {
        points: deepClone(points),
        hoverPoints,
      })
    }

    ctx.beginPath()

    drawBezierCurvePath(ctx, points.slice(1) as Point[][], points[0])

    if (close) {
      ctx.closePath()

      ctx.fill()
      ctx.stroke()
    } else {
      ctx.stroke()
    }
  }

  hoverCheck(point: Point): boolean {
    const { cache, shape, style } = this
    const { hoverPoints } = cache
    const { close } = shape
    const { lineWidth } = style

    if (close) {
      return checkPointIsInPolygon(point, hoverPoints!)
    } else {
      return checkPointIsNearPolyline(point, hoverPoints!, lineWidth)
    }
  }

  setGraphCenter(): void {
    const { shape, style } = this
    const { points } = shape

    style.graphCenter = points[0]
  }

  move({ movementX, movementY }: MouseEvent): void {
    const {
      shape: { points },
      cache,
    } = this

    const [fx, fy] = points[0] as Point
    const curves = points.slice(1)

    const bezierCurvePoints = [
      [fx + movementX, fy + movementY],
      ...(curves as BezierCurveSegment[]).map(curve =>
        curve.map(([x, y]) => [x + movementX, y + movementY])
      ),
    ] as BezierCurveType

    cache.points = bezierCurvePoints
    cache.hoverPoints = cache.hoverPoints!.map(([x, y]) => [x + movementX, y + movementY])

    this.attr('shape', {
      points: bezierCurvePoints,
    })
  }
}

export default BezierCurve

# 贝塞尔曲线(闭合)

点击以展开或折叠演示配置
import { getCircleRadianPoint } from '../../../es/utils/graphs'

function getPetalPoints(insideRadius, outsideRadius, petalNum, petalCenter) {
  const PI2Dived = (Math.PI * 2) / (petalNum * 3)

  let points = new Array(petalNum * 3)
    .fill('')
    .map((_, i) =>
      getCircleRadianPoint(...petalCenter, i % 3 === 0 ? insideRadius : outsideRadius, PI2Dived * i)
    )

  const startPoint = points.shift()
  points.push(startPoint)

  points = new Array(petalNum).fill('').map(_ => points.splice(0, 3))

  points.unshift(startPoint)

  return points
}

export default function (render) {
  const {
    area: [w, h],
  } = render

  const petalCenter = [w / 2, h / 2]
  const [raidus1, raidus2, raidus3, raidus4] = [h / 6, h / 2.5, h / 3, h / 2]

  return {
    name: 'BezierCurve',
    animationCurve: 'easeOutBack',
    hover: true,
    drag: true,
    shape: {
      points: getPetalPoints(raidus1, raidus2, 6, petalCenter),
      close: true,
    },
    style: {
      fill: '#9ce5f4',
      shadowBlur: 0,
      shadowColor: '#66eece',
      hoverCursor: 'pointer',
    },
    onMouseEnter() {
      const {
        style: { graphCenter },
      } = this

      this.animation('style', { lineWidth: 20, shadowBlur: 20 }, true)
      this.animation('shape', { points: getPetalPoints(raidus3, raidus4, 6, graphCenter) })
    },
    onMouseOuter() {
      const {
        style: { graphCenter },
      } = this

      this.animation('style', { lineWidth: 10, shadowBlur: 0 }, true)
      this.animation('shape', { points: getPetalPoints(raidus1, raidus2, 6, graphCenter) })
    },
    setGraphCenter(e) {
      const { style } = this

      if (e) {
        const { movementX, movementY } = e
        const [cx, cy] = style.graphCenter

        style.graphCenter = [cx + movementX, cy + movementY]
      } else {
        style.graphCenter = [...petalCenter]
      }
    },
  }
}

# 文本

shape 属性表

属性名 类型 默认值 注解
content string '' 文本内容
position [number, number] [0, 0] 文本起始位置
maxWidth number undefined 文本最大宽度
rowGap number 0 行间距
点击以展开或折叠演示配置
export default function (render) {
  const {
    area: [w, h],
  } = render

  const centerPoint = [w / 2, h / 2]

  return {
    name: 'Text',
    animationCurve: 'easeOutBounce',
    hover: true,
    drag: true,
    shape: {
      content: 'CRender',
      position: centerPoint,
      maxWidth: 200,
    },
    style: {
      fill: '#9ce5f4',
      fontSize: 50,
      shadowBlur: 0,
      rotate: 0,
      shadowColor: '#66eece',
      hoverCursor: 'pointer',
      scale: [1, 1],
      rotate: 0,
    },
    onMouseEnter() {
      this.animation('style', { shadowBlur: 20, scale: [1.5, 1.5] })
    },
    onMouseOuter() {
      this.animation('style', { shadowBlur: 0, scale: [1, 1] })
    },
  }
}
点击以展开或折叠 Text 实现
import { TextShape, TextShapeCache } from '../types/graphs/shape'
import { Point, GraphConfig } from '../types/core/graph'
import Graph from '../core/graph.class'
import { checkPointIsInRect } from '../utils/graphs'

class Text extends Graph<TextShape> {
  name = 'text'

  private cache: TextShapeCache = {}

  constructor(config: GraphConfig<Partial<TextShape>>) {
    super(
      Graph.mergeDefaultShape(
        {
          content: '',
          position: [0, 0],
          maxWidth: undefined,
          rowGap: 0,
        },
        config,
        ({ shape: { content, position, rowGap } }) => {
          if (typeof content !== 'string')
            throw new Error('CRender Graph Text: Text content should be a string!')

          if (!Array.isArray(position))
            throw new Error('CRender Graph Text: Text position should be an array!')

          if (typeof rowGap !== 'number')
            throw new Error('CRender Graph Text: Text rowGap should be a number!')
        }
      )
    )
  }

  draw(): void {
    const {
      shape,
      render: { ctx },
    } = this
    const { content, position, maxWidth, rowGap } = shape
    const { textBaseline, font } = ctx

    const contentArr = content.split('\n')
    const rowNum = contentArr.length

    const fontSize = parseInt(font.replace(/\D/g, ''))
    const lineHeight = fontSize + rowGap
    const allHeight = rowNum * lineHeight - rowGap

    let offset = 0
    const x = position[0]
    let y = position[1]

    if (textBaseline === 'middle') {
      offset = allHeight / 2
      y += fontSize / 2
    }

    if (textBaseline === 'bottom' || textBaseline === 'alphabetic') {
      offset = allHeight
      y += fontSize
    }

    const positions: Point[] = new Array(rowNum)
      .fill(0)
      .map((_, i) => [x, y + i * lineHeight - offset])

    ctx.beginPath()

    let realMaxWidth = 0
    contentArr.forEach((text, i) => {
      // calc text width and height for hover check
      const width = ctx.measureText(text).width
      if (width > realMaxWidth) realMaxWidth = width

      ctx.fillText(text, positions[i][0], positions[i][1], maxWidth)
      ctx.strokeText(text, positions[i][0], positions[i][1], maxWidth)
    })

    ctx.closePath()

    this.setCache(realMaxWidth, allHeight)
  }

  private setCache(width: number, height: number): void {
    const {
      cache,
      shape: {
        position: [x, y],
      },
      render: { ctx },
    } = this
    const { textAlign, textBaseline } = ctx

    cache.w = width
    cache.h = height
    cache.x = x
    cache.y = y
    if (textAlign === 'center') {
      cache.x = x - width / 2
    } else if (textAlign === 'end' || textAlign === 'right') {
      cache.x = x - width
    }

    if (textBaseline === 'middle') {
      cache.y = y - height / 2
    } else if (textBaseline === 'bottom' || textBaseline === 'alphabetic') {
      cache.y = y - height
    }
  }

  setGraphCenter(): void {
    const {
      shape: { position },
      style,
    } = this

    style.graphCenter = [...position] as [number, number]
  }

  move({ movementX, movementY }: MouseEvent): void {
    const {
      position: [x, y],
    } = this.shape

    this.attr('shape', {
      position: [x + movementX, y + movementY],
    })
  }

  hoverCheck(point: Point): boolean {
    const {
      cache: { x, y, w, h },
    } = this

    return checkPointIsInRect(point, { x: x!, y: y!, w: w!, h: h! })
  }
}

export default Text

TIP

文本中插入\n可以进行换行。