Location>code7788 >text

Front-end implementation of the visual designer using Konva (21) - Drawing shapes (ellipses)

Popularity:119 ℃/2024-08-20 19:08:39

This chapter starts to add some basic graphics drawing, such as drawing: straight lines, curves, circles/ellipses, rectangles. This chapter shares how this example starts to draw a graphic with the goal of drawing a circle/ellipse as an implementation.

Please give me a free Star!

If you find any bugs, please feel free to raise an Issue!

github source code

gitee source code

sample address (computing)

Next main point:

  • UI
  • Graph
  • canvas2svg Patch
  • Inflection point rotation repair

UI - Graphics Drawing Type Switching

Start by finding a few icons and adding buttons that represent drawing shapes: straight lines, curves, circles/ellipses, rectangles:

image

Once the graphic type is selected, you can draw the graphic by dragging it (clearing the selection when drawing is complete):

image

Define the graphic type:

// src/Render/

/**
 * Graphics type
 */
export enum GraphType {
  Line = 'Line', // straight line
  Curve = 'Curve', // curve
  Rect = 'Rect', // rectangle
  Circle = 'Circle' // circle/ellipse
}

Records the current graphic type in Render and provides modification methods and events:

// src/Render/

// summarize

  // Drawing type
  graphType:  | undefined = undefined

// summarize

  // 改变Drawing type
  changeGraphType(type?: ) {
     = type
    ('graph-type-change', )
  }

Toolbar Button Newsletter:

// src/components/main-header/

// summarize

const emit = defineEmits([/* summarize */, 'update:graphType'])

const props = withDefaults(defineProps<{
    // summarize
    graphType?:
}>(), {
    // summarize
});

// summarize

watch(() => , () => {
    if () {
        // summarize

        ?.on('graph-type-change', (value) => {
            emit('update:graphType', value)
        })
    }

}, {
    immediate: true
})

// summarize

function onGraph(type: ) {
    emit('update:graphType', === type ? undefined : type)

Above is the toolbar entry for drawing graphics.

Graph - Graph definition and its related implementation

Related code files:
1. src/Render/graphs/ - Abstract class: defines generic properties, logic, external interface definitions.
2. src/Render/graphs/ Inherit BaseGraph - construct circle/ellipsoid; handle creation of part of the interactive information; implementation of key logic.
3. src/Render/handlers/ - collects the interaction information needed for the creation of the graphic and passes it to the Circle static handlers.
4. src/Render/draws/ - draws graphics, adjustment points - draws anchor points for adjustment points; collects and processes interaction information, then and hands it off to Circle static processing methods.

BaseGraph abstract class

// src/Render/graphs/

// summarize

/**
 * graphics class
 * Examples are mainly used when creating new graphics,Includes new and simultaneous size dragging。
 * Static methods are mainly used after new,pass (a bill or inspection etc) adjustment point Logical definition of adjustment
 */
export abstract class BaseGraph {
  /**
   * update depiction (used form a nominal expression) adjustment point (used form a nominal expression) anchor location
   * @param width depiction (used form a nominal expression) height
   * @param height depiction (used form a nominal expression) high degree
   * @param rotate depiction (used form a nominal expression) angle of rotation
   * @param anchorShadows depiction (used form a nominal expression) adjustment point (used form a nominal expression) anchor point
   */
  static updateAnchorShadows(
    width: number,
    height: number,
    rotate: number,
    anchorShadows: []
  ) {
    ('Please realize updateAnchorShadows', width, height, anchorShadows)
  }

  /**
   * update depiction (used form a nominal expression) connection point (used form a nominal expression) anchor location
   * @param width depiction (used form a nominal expression) height
   * @param height depiction (used form a nominal expression) high degree
   * @param rotate depiction (used form a nominal expression) angle of rotation
   * @param anchors depiction (used form a nominal expression) adjustment point (used form a nominal expression) anchor point
   */
  static updateLinkAnchorShadows(
    width: number,
    height: number,
    rotate: number,
    linkAnchorShadows: []
  ) {
    ('Please realize updateLinkAnchorShadows', width, height, linkAnchorShadows)
  }

  /**
   * generating adjustment point
   * @param render Rendering Example
   * @param graph depiction
   * @param anchor adjustment point define
   * @param anchorShadow adjustment point anchor point
   * @param adjustingId 正在操作(used form a nominal expression) adjustment point id
   * @returns
   */
  static createAnchorShape(
    render: Render,
    graph: ,
    anchor: ,
    anchorShadow: ,
    adjustType: string,
    adjustGroupId: string
  ):  {
    ('Please realize createAnchorShape', render, graph, anchor, anchorShadow, adjustingId, adjustGroupId)
    return new ()
  }

  /**
   * align depiction
   * @param render Rendering Example
   * @param graph depiction
   * @param graphSnap depiction (used form a nominal expression) backing up
   * @param rect be facing (us) adjustment point
   * @param rects possess adjustment point
   * @param startPoint Mouse Down Position
   * @param endPoint Mouse Drag Position
   */
  static adjust(
    render: Render,
    graph: ,
    graphSnap: ,
    rect: ,
    rects: [],
    startPoint: Konva.Vector2d,
    endPoint: Konva.Vector2d
  ) {
    ('Please realize updateAnchorShadows', render, graph, rect, startPoint, endPoint)
  }
  //
  protected render: Render
  group:
  id: string // just like group (used form a nominal expression)id
  /**
   * Mouse Down Position
   */
  protected dropPoint: Konva.Vector2d = { x: 0, y: 0 }
  /**
   * adjustment point define
   */
  protected anchors: [] = []
  /**
   * adjustment point (used form a nominal expression) anchor point
   */
  protected anchorShadows: [] = []

  /**
   * adjustment point define
   */
  protected linkAnchors: [] = []
  /**
   * connection point (used form a nominal expression) anchor point
   */
  protected linkAnchorShadows: [] = []

  constructor(
    render: Render,
    dropPoint: Konva.Vector2d,
    config: {
      anchors: []
      linkAnchors: []
    }
  ) {
     = render
     = dropPoint

     = nanoid()

     = new ({
      id: ,
      name: 'asset',
      assetType:
    })

    // adjustment point define
     = ((o) => ({
      ...o,
      // Additional information
      name: 'anchor',
      groupId: ()
    }))

    // recorded in group center
    ('anchors', )

    // newly built adjustment point (used form a nominal expression) anchor point
    for (const anchor of ) {
      const circle = new ({
        adjustType: ,
        name: ,
        radius: 0
        // radius: (1),
        // fill: 'red'
      })
      (circle)
      (circle)
    }

    // connection point define
     = (
      (o) =>
        ({
          ...o,
          id: nanoid(),
          groupId: (),
          visible: false,
          pairs: [],
          direction: ,
          alias:
        }) as
    )

    // connection point信息
    ({
      points:
    })
    // newly built connection point (used form a nominal expression) anchor point
    for (const point of ) {
      const circle = new ({
        name: 'link-anchor',
        id: ,
        x: ,
        y: ,
        radius: (1),
        stroke: 'rgba(0,0,255,1)',
        strokeWidth: (2),
        visible: false,
        direction: ,
        alias:
      })
      (circle)
      (circle)
    }

    ('mouseenter', () => {
      // demonstrate connection point
      (true, )
    })
    ('mouseleave', () => {
      // harbor (i.e. keep sth hidden) connection point
      (false, )
      // harbor (i.e. keep sth hidden) hover circle (i.e. draw a circle around sth)
      ('#hoverRect')?.visible(false)
    })

    ()

    ()
  }

  /**
   * align进行时
   * @param point mouse position relative position
   */
  abstract drawMove(point: Konva.Vector2d): void

  /**
   * align结束
   */
  abstract drawEnd(): void
}

Here:

  • Static methods, rather than defining the necessary tool methods for drawing graphics, the specific implementation is left to the definition of the specific graphics class;
  • This is followed by the attributes necessary for drawing graphics and their initialization;
  • Finally, abstract methods constrain the methods necessary for graph instances.

Drawing Circles/Ellipses

The graphic is adjustable, here the circle/ellipse has 8 adjustment points:

image

Consider also that the graphic is rotated and still reasonably adjustable:

image

The tweak itself supports tiles:

image

Graphics also supports connection points:

image

Graphics Class - Circle

// src/Render/graphs/

// summarize

/**
 * depiction unit of Chinese currency (Yuan)/椭unit of Chinese currency (Yuan)
 */
export class Circle extends BaseGraph {
  // realization:update depiction (used form a nominal expression) adjustment point (used form a nominal expression) anchor location
  static override updateAnchorShadows(
    width: number,
    height: number,
    rotate: number,
    anchorShadows: []
  ): void {
    for (const shadow of anchorShadows) {
      switch () {
        case 'top':
          ({
            x: width / 2,
            y: 0
          })
          break
        case 'bottom':
          ({
            x: width / 2,
            y: height
          })
          break
        case 'left':
          ({
            x: 0,
            y: height / 2
          })
          break
        case 'right':
          ({
            x: width,
            y: height / 2
          })
          break
        case 'top-left':
          ({
            x: 0,
            y: 0
          })
          break
        case 'top-right':
          ({
            x: width,
            y: 0
          })
          break
        case 'bottom-left':
          ({
            x: 0,
            y: height
          })
          break
        case 'bottom-right':
          ({
            x: width,
            y: height
          })
          break
      }
    }
  }
  // realization:update depiction (used form a nominal expression) connection point (used form a nominal expression) anchor location
  static override updateLinkAnchorShadows(
    width: number,
    height: number,
    rotate: number,
    linkAnchorShadows: []
  ): void {
    for (const shadow of linkAnchorShadows) {
      switch () {
        case 'top':
          ({
            x: width / 2,
            y: 0
          })
          break
        case 'bottom':
          ({
            x: width / 2,
            y: height
          })
          break
        case 'left':
          ({
            x: 0,
            y: height / 2
          })
          break
        case 'right':
          ({
            x: width,
            y: height / 2
          })
          break
        case 'center':
          ({
            x: width / 2,
            y: height / 2
          })
          break
      }
    }
  }
  // realization:generating adjustment point
  static createAnchorShape(
    render: ,
    graph: ,
    anchor: ,
    anchorShadow: ,
    adjustType: string,
    adjustGroupId: string
  ):  {
    // stage state of affairs
    const stageState = ()

    const x = (().x - ),
      y = (().y - )

    const offset =  + 5

    const shape = new ({
      name: 'anchor',
      anchor: anchor,
      //
      // stroke: colorMap[] ?? 'rgba(0,0,255,0.2)',
      stroke:
        adjustType ===  && () === adjustGroupId
          ? 'rgba(0,0,255,0.8)'
          : 'rgba(0,0,255,0.2)',
      strokeWidth: (2),
      // placement
      x,
      y,
      // trails
      points:
        {
          'top-left': _.flatten([
            [-offset, offset / 2],
            [-offset, -offset],
            [offset / 2, -offset]
          ]),
          top: _.flatten([
            [-offset, -offset],
            [offset, -offset]
          ]),
          'top-right': _.flatten([
            [-offset / 2, -offset],
            [offset, -offset],
            [offset, offset / 2]
          ]),
          right: _.flatten([
            [offset, -offset],
            [offset, offset]
          ]),
          'bottom-right': _.flatten([
            [-offset / 2, offset],
            [offset, offset],
            [offset, -offset / 2]
          ]),
          bottom: _.flatten([
            [-offset, offset],
            [offset, offset]
          ]),
          'bottom-left': _.flatten([
            [-offset, -offset / 2],
            [-offset, offset],
            [offset / 2, offset]
          ]),
          left: _.flatten([
            [-offset, -offset],
            [-offset, offset]
          ])
        }[] ?? [],
      // angle of rotation
      rotation: ()
    })

    ('mouseenter', () => {
      ('rgba(0,0,255,0.8)')
       = 'move'
    })
    ('mouseleave', () => {
      ( ? 'rgba(0,0,255,0.8)' : 'rgba(0,0,255,0.2)')
       =  ? 'move' : 'default'
    })

    return shape
  }
  // realization:align depiction
  static override adjust(
    render: ,
    graph: ,
    graphSnap: ,
    shapeRecord: ,
    shapeRecords: [],
    startPoint: Konva.Vector2d,
    endPoint: Konva.Vector2d
  ) {
    // goal unit of Chinese currency (Yuan)/椭unit of Chinese currency (Yuan)
    const circle = ('.graph') as 
    // mirroring
    const circleSnap = ('.graph') as 

    // adjustment point anchor point
    const anchors = (('.anchor') ?? []) as []

    // connection point anchor point
    const linkAnchors = (('.link-anchor') ?? []) as []

    const { shape: adjustShape } = shapeRecord

    if (circle && circleSnap) {
      let [graphWidth, graphHeight] = [(), ()]
      const [graphRotation, anchorId, ex, ey] = [
        (()),
        ?.id,
        ,
        
      ]

      let anchorShadow:  | undefined, anchorShadowAcross:  | undefined

      switch (anchorId) {
        case 'top':
          {
            anchorShadow = (`#top`)
            anchorShadowAcross = (`#bottom`)
          }
          break
        case 'bottom':
          {
            anchorShadow = (`#bottom`)
            anchorShadowAcross = (`#top`)
          }
          break
        case 'left':
          {
            anchorShadow = (`#left`)
            anchorShadowAcross = (`#right`)
          }
          break
        case 'right':
          {
            anchorShadow = (`#right`)
            anchorShadowAcross = (`#left`)
          }
          break
        case 'top-left':
          {
            anchorShadow = (`#top-left`)
            anchorShadowAcross = (`#bottom-right`)
          }
          break
        case 'top-right':
          {
            anchorShadow = (`#top-right`)
            anchorShadowAcross = (`#bottom-left`)
          }
          break
        case 'bottom-left':
          {
            anchorShadow = (`#bottom-left`)
            anchorShadowAcross = (`#top-right`)
          }
          break
        case 'bottom-right':
          {
            anchorShadow = (`#bottom-right`)
            anchorShadowAcross = (`#top-left`)
          }
          break
      }

      if (anchorShadow && anchorShadowAcross) {
        const { x: sx, y: sy } = ()
        const { x: ax, y: ay } = ()

        // anchorShadow:它是当前操作(used form a nominal expression) adjustment point anchor point
        // anchorShadowAcross:它是当前操作(used form a nominal expression) adjustment point 反方向对面(used form a nominal expression) anchor point

        // alignadults and children
        {
           // summarize
           // Complicated calculations,Not necessarily optimal,See project code for details。
           // basic logic:
           // 1、By mouse movement,计算当前鼠标placement、当前操作(used form a nominal expression) adjustment point anchor point placement(原placement) Respectively, with anchorShadowAcross(原placement)(used form a nominal expression)距离;
           // 2、 preserve anchorShadowAcross placement固定,通过上面两距离(used form a nominal expression)变化比例,计算最新(used form a nominal expression)宽高adults and children;
           // 3、期间要约束不同角度不同方向(used form a nominal expression)宽高处理,有(used form a nominal expression)只改变宽、有(used form a nominal expression)只改变高、有(used form a nominal expression)同时改变宽和高。
        }

        // alignplacement
        {
          // summarize
          // Complicated calculations,Not necessarily optimal,See project code for details。
          // basic logic:
          // Using trigonometric functions,通过最新(used form a nominal expression)宽高,aligndepiction(used form a nominal expression)坐标。
        }
      }

      // update unit of Chinese currency (Yuan)/椭unit of Chinese currency (Yuan) adults and children
      (graphWidth / 2)
      (graphWidth / 2)
      (graphHeight / 2)
      (graphHeight / 2)

      // update adjustment point (used form a nominal expression) anchor point placement
      (graphWidth, graphHeight, graphRotation, anchors)

      // update depiction (used form a nominal expression) connection point (used form a nominal expression) anchor location
      (graphWidth, graphHeight, graphRotation, linkAnchors)

      // stage state of affairs
      const stageState = ()

      // update adjustment point placement
      for (const anchor of anchors) {
        for (const { shape } of shapeRecords) {
          if (?.adjustType === ) {
            const anchorShadow = (`#${}`)
            if (anchorShadow) {
              ({
                x: (().x - ),
                y: (().y - )
              })
              (())
            }
          }
        }
      }
    }
  }
  /**
   * 默认depictionadults and children
   */
  static size = 100
  /**
   * unit of Chinese currency (Yuan)/椭unit of Chinese currency (Yuan) 对应(used form a nominal expression) Konva an actual example
   */
  private circle: 

  constructor(render: , dropPoint: Konva.Vector2d) {
    super(render, dropPoint, {
      // defined 8 classifier for individual things or people, general, catch-all classifier adjustment point
      anchors: [
        { adjustType: 'top' },
        { adjustType: 'bottom' },
        { adjustType: 'left' },
        { adjustType: 'right' },
        { adjustType: 'top-left' },
        { adjustType: 'top-right' },
        { adjustType: 'bottom-left' },
        { adjustType: 'bottom-right' }
      ].map((o) => ({
        adjustType: , // adjustment point type definition
        type:  // Record affiliation depiction
      })),
      linkAnchors: [
        { x: 0, y: 0, alias: 'top', direction: 'top' },
        { x: 0, y: 0, alias: 'bottom', direction: 'bottom' },
        { x: 0, y: 0, alias: 'left', direction: 'left' },
        { x: 0, y: 0, alias: 'right', direction: 'right' },
        { x: 0, y: 0, alias: 'center' }
      ] as []
    })

    // newly built unit of Chinese currency (Yuan)/椭unit of Chinese currency (Yuan)
     = new ({
      name: 'graph',
      x: 0,
      y: 0,
      radiusX: 0,
      radiusY: 0,
      stroke: 'black',
      strokeWidth: 1
    })

    // become a member
    ()
    // 鼠标按下placement as a starting point
    ()
  }

  // realization:drag-and-drop (computing)
  override drawMove(point: Konva.Vector2d): void {
    // Mouse Drag Offset
    let offsetX =  - ,
      offsetY =  - 

    // Make sure it doesn't flip over
    if (offsetX < 1) {
      offsetX = 1
    }
    if (offsetY < 1) {
      offsetY = 1
    }

    // radius
    const radiusX = offsetX / 2,
      radiusY = offsetY / 2

    // unit of Chinese currency (Yuan)/椭unit of Chinese currency (Yuan) placementadults and children
    (radiusX)
    (radiusY)
    (radiusX)
    (radiusY)

    // group adults and children
    ({
      width: offsetX,
      height: offsetY
    })

    // update depiction (used form a nominal expression) adjustment point (used form a nominal expression) anchor location
    (offsetX, offsetY, 1, )

    // update depiction (used form a nominal expression) connection point (used form a nominal expression) anchor location
    (offsetX, offsetY, 1, )

    // repaint
    ([, ])
  }

  // realization:End of Drag
  override drawEnd(): void {
    if (() <= 1 && () <= 1) {
      // become a member只点击,no drag

      // 默认adults and children
      const width = ,
        height = width

      const radiusX =  / 2,
        radiusY = radiusX

      // unit of Chinese currency (Yuan)/椭unit of Chinese currency (Yuan) placementadults and children
      (radiusX)
      (radiusY)
      (radiusX - ())
      (radiusY - ())

      // group adults and children
      ({
        width,
        height
      })

      // update depiction (used form a nominal expression) adjustment point (used form a nominal expression) anchor location
      (width, height, 1, )

      // update depiction (used form a nominal expression) connection point (used form a nominal expression) anchor location
      (width, height, 1, )

      // alignments cleared
      ()

      // repaint
      ([, ])
    }
  }
}

GraphHandlers

// src/Render/handlers/

// summarize

export class GraphHandlers implements {
  // summarize

  /**
   * In New Graphics
   */
  graphing = false

  /**
   * Current New Graphic Type
   */
  currentGraph:  | undefined

  /**
   * Get mouse position,and processed as relative size
   * @param attract Calculation with Magnetic Stickers
   * @returns
   */
  getStagePoint(attract = false) {
    const pos = ()
    if (pos) {
      const stageState = ()
      if (attract) {
        // paste paste (e.g. on a computer screen)
        const { pos: transformerPos } = (pos)
        return {
          x: ( - ),
          y: ( - )
        }
      } else {
        return {
          x: ( - ),
          y: ( - )
        }
      }
    }
    return null
  }

  handlers = {
    stage: {
      mousedown: (e: <GlobalEventHandlersEventMap['mousedown']>) => {
        if () {
          // Selected graphic type,commencement

          if ( === ) {
             = true

            ()

            const point = ()
            if (point) {
              if ( === ) {
                // newly built unit of Chinese currency (Yuan)/椭unit of Chinese currency (Yuan) an actual example
                 = new (, point)
              }
            }
          }
        }
      },
      mousemove: () => {
        if () {
          if () {
            const pos = (true)
            if (pos) {
              // newly built并马上调整图形
              (pos)
            }
          }
        }
      },
      mouseup: () => {
        if () {
          if () {
            // End of adjustment
            ()
          }

          // End of adjustment
           = false

          // Clear graphic type selection
          ()

          // alignments cleared
          ()

          // repaint
          ([, ])
        }
      }
    }
  }
}

GraphDraw

// src/Render/draws/

// summarize

export interface GraphDrawState {
  /**
   * in the pipeline
   */
  adjusting: boolean

  /**
   * in the pipeline id
   */
  adjustType: string
}

// summarize

export class GraphDraw extends implements {
  // summarize

  state: GraphDrawState = {
    adjusting: false,
    adjustType: ''
  }

  /**
   * Mouse over adjustment point placement
   */
  startPoint: Konva.Vector2d = { x: 0, y: 0 }

  /**
   * depiction group mirroring
   */
  graphSnap:  | undefined

  constructor(render: , layer: , option: GraphDrawOption) {
    super(render, layer)

     = option

    ()
  }

  /**
   * 获取鼠标placement,and processed as relative size
   * @param attract Calculation with Magnetic Stickers
   * @returns
   */
  getStagePoint(attract = false) {
    const pos = ()
    if (pos) {
      const stageState = ()
      if (attract) {
        // paste paste (e.g. on a computer screen)
        const { pos: transformerPos } = (pos)
        return {
          x: ( - ),
          y: ( - )
        }
      } else {
        return {
          x: ( - ),
          y: ( - )
        }
      }
    }
    return null
  }

  // align preprocessing、Locating static methods
  adjusts(
    shapeDetailList: {
      graph:
      shapeRecords: { shape: ; anchorShadow:  }[]
    }[]
  ) {
    for (const { shapeRecords, graph } of shapeDetailList) {
      for (const { shape } of shapeRecords) {
        ('adjusting', false)
      }
      for (const shapeRecord of shapeRecords) {
        const { shape } = shapeRecord
        // Mouse over
        ('mousedown', () => {
           = true
           = ?.adjustType
           = ()

          ('adjusting', true)

          const pos = ()
          if (pos) {
             = pos

            // depiction group mirroring,用于计算placement、Offset in size
             = ()
          }
        })

        // in the pipeline
        ('mousemove', () => {
          if ( && ) {
            if (?.type === ) {
              // align unit of Chinese currency (Yuan)/椭unit of Chinese currency (Yuan) depiction
              if () {
                const pos = (true)
                if (pos) {
                  // utilization unit of Chinese currency (Yuan)/椭unit of Chinese currency (Yuan) Static processing methods
                  (
                    ,
                    graph,
                    ,
                    shapeRecord,
                    shapeRecords,
                    ,
                    pos
                  )

                  // repaint
                  ([, ])
                }
              }
            }
          }
        })

        // align结束
        ('mouseup', () => {
           = false
           = ''
           = ''

          // Restore the display of all adjustment point
          for (const { shape } of shapeRecords) {
            (1)
            ('adjusting', false)
            ('rgba(0,0,255,0.2)')
             = 'default'
          }

          // destroy (by melting or burning) mirroring
          ?.destroy()

          // alignments cleared
          ()
        })

        (shape)
      }
    }
  }

  override draw() {
    ()

    // 所有depiction
    const graphs =
      .find('.asset')
      .filter((o) => === ) as []

    const shapeDetailList: {
      graph:
      shapeRecords: { shape: ; anchorShadow:  }[]
    }[] = []

    for (const graph of graphs) {
      // Displayed only when not selected adjustment point
      if (!) {
        const anchors = ( ?? []) as []
        const shapeRecords: { shape: ; anchorShadow:  }[] = []

        // according to adjustment point text,establish
        for (const anchor of anchors) {
          // adjustment point displayed relies on its hidden anchor point placement、大小等text
          const anchorShadow = (`#${}`) as
          if (anchorShadow) {
            const shape = (
              ,
              graph,
              anchor,
              anchorShadow,
              ,
              
            )

            ({ shape, anchorShadow })
          }
        }

        ({
          graph,
          shapeRecords
        })
      }
    }

    (shapeDetailList)
  }
}

Slightly bloated, slowly optimize later -_-

canvas2svg Patch

The above has been implemented to draw graphics (circle/ellipse), but it reported an error when exporting the svg. After locating the error and reading the source code, it was found:

1、It is not possible to export svg file when it contains
2、When adjusting properties such as radiusX and radiusY, the path path cannot be output correctly.

1. canvas2svg tries to give the path attribute to the g node, resulting in an exception.

Now add the scenario where nodeName === 'g' is handled by hacking the __applyCurrentDefaultPath method.

2. Check the source code of . _sceneFunc method source code, Konva draws Ellipse by canvas arc + scale, corresponding to code comment A.

In practice, this does not mimic the average scale of canvas, and the stroke will be of different thicknesses.

Therefore, trying to fix this issue by recognizing the scale modification path feature.

// src/Render/tools/

. __applyCurrentDefaultPath = function () {
  // Patch: Fix the following issues:
  // 1. Can't export svg file when include.
  // 2. When adjusting properties such as radiusX, radiusY, the path path could not be exported correctly.
  //
  // PS.
  // 1. canvas2svg attempted to assign a path attribute to the g node, resulting in an error.
  // Now hack the __applyCurrentDefaultPath method to handle the nodeName === 'g' scenario.
  // The nodeName === 'g' is now added by hacking __applyCurrentDefaultPath.
  // 2. _sceneFunc method source code.
  // Konva draws the Ellipse by using arc + scale on the canvas, which corresponds to code comment A. // The actual effect can't be modeled after this method.
  // In practice, it's not possible to emulate the average scale of canvas, and the strokes are not of the same thickness.
  // Therefore, we tried to fix this by modifying the path feature to recognize the scale.
  // (The above hack is for the example only.
  // (The above hack only deals with the characteristics of the example graphic, and does not delve into the logic of why canvas2svg gets into the wrong place).

  if (this.__currentElement.nodeName === 'g') {
    const g = this.__currentElement.querySelector('g')
    if (g) {
      // Annotate A
      // const d = this.__currentDefaultPath
      // const path = ('http:///2000/svg', 'path') as SVGElement
      // ('d', d)
      // ('fill', 'none')
      // (path)

      const scale = ('transform')
      if (scale) {
        const match = (/scale\(([^),]+),([^)]+)\)/)
        if (match) {
          const [sx, sy] = [parseFloat(match[1]), parseFloat(match[2])]
          let d = this.__currentDefaultPath
          const reg = /A ([^ ]+) ([^ ]+) /
          const match2 = (reg)
          if (match2) {
            const [rx, ry] = [parseFloat(match2[1]), parseFloat(match2[2])]
            d = (reg, `A ${rx * sx} ${ry * sy} `)
            const path = (
              'http:///2000/svg',
              'path'
            ) as SVGElement
            ('d', d)
            ('fill', 'none')
            this.__currentElement.append(path)
          }
        }
      } else {
        const d = this.__currentDefaultPath
        const path = ('http:///2000/svg', 'path') as SVGElement
        ('d', d)
        ('fill', 'none')
        this.__currentElement.append(path)
      }
    }

    (
      '[Hacked] Attempted to apply path command to node ' + this.__currentElement.nodeName
    )
    return
  }

  // Original logic
  if (this.__currentElement.nodeName === 'path') {
    const d = this.__currentDefaultPath
    this.__currentElement.setAttribute('d', d)
  } else {
    throw new Error('Attempted to apply path command to node ' + this.__currentElement.nodeName)
  }
}

The above hack only deals with the characteristics of the example graphic as it is drawn, and does not delve into the logic of why canvas2svg gets into the wrong place.

Inflection point rotation repair

Tests showed that the inflection points of the connecting lines did not follow the rotation angle, so a fix was added:

image

// src/Render/handlers/

// summarize

  /**
   * matrix transformation:A point in the coordinate system,Rotate around another point
   * - - - - - - - -
   * |x`| |cos -sin| |x-a| |a|
   * | | = | | | | +
   * |y`| |sin cos| |y-b| |b|
   * - - - - - - - -
   * @param x Target node coordinates x
   * @param y Target node coordinates y
   * @param centerX Coordinates of the point around which x
   * @param centerY Coordinates of the point around which y
   * @param angle angle of rotation
   * @returns
   */
  rotatePoint(x: number, y: number, centerX: number, centerY: number, angle: number) {
    // Converting angles to radians
    const radians = (angle * ) / 180

    // Calculate the rotated coordinates
    const newX = (radians) * (x - centerX) - (radians) * (y - centerY) + centerX
    const newY = (radians) * (x - centerX) + (radians) * (y - centerY) + centerY

    return { x: newX, y: newY }
  }

  lastRotation = 0


// summarize
  handlers = {
// summarize
    transformer: {
      transform: () => {
        // rotating,The inflection point has to follow suit
        const back = ('.back')

        if (back) {
          // stage state of affairs
          const stageState = ()

          const { x, y, width, height } = ()
          const rotation = () -
          const centerX = x + width / 2
          const centerY = y + height / 2

          const groups = ()

          const points = ((ps, group) => {
            return ((('points')) ? ('points') : [])
          }, [] as [])

          const pairs = ((ps, point) => {
            return ( ? ((o) => !) : [])
          }, [] as [])

          for (const pair of pairs) {
            const fromGroup = ((o) => () === )
            const toGroup = ((o) => () === )
            // Must move in pairs to be recorded
            if (fromGroup && toGroup) {
              // mobility
              if ( && ) {
                let manualPoints = []
                const manualPointsBefore = []
                if ((manualPoints) && (manualPointsBefore)) {
                  manualPoints = ((o: ) => {
                    const { x, y } = (
                      () + ,
                      () + ,
                      centerX,
                      centerY,
                      rotation
                    )

                    return {
                      x: (x - ),
                      y: (y - )
                    }
                  })

                  ('manualPointsMap', {
                    ...,
                    []: manualPoints
                  })
                }
              }
            }
          }
        }

        // repaint
        ([, , ])
      }
    }
// summarize
  }

Thanks watching~

More Stars please!

source code (computing)

gitee source code

sample address (computing)