0


前端使用 Konva 实现可视化设计器(21)- 绘制图形(椭圆)

本章开始补充一些基础的图形绘制,比如绘制:直线、曲线、圆/椭形、矩形。这一章主要分享一下本示例是如何开始绘制一个图形的,并以绘制圆/椭形为实现目标。

请大家动动小手,给我一个免费的 Star 吧~

大家如果发现了 Bug,欢迎来提 Issue 哟~

github源码

gitee源码

示例地址

接下来主要说说:

  • UI
  • Graph(图形)
  • canvas2svg 打补丁
  • 拐点旋转修复

UI - 图形绘制类型切换

先找几个图标,增加按钮,分别代表绘制图形:直线、曲线、圆/椭形、矩形:

在这里插入图片描述

选中图形类型后,即可通过拖动绘制图形(绘制完成后,清空选择):

在这里插入图片描述

定义图形类型:

// src/Render/types.ts    /**
 * 图形类型
 */exportenum GraphType {
  Line ='Line',// 直线
  Curve ='Curve',// 曲线
  Rect ='Rect',// 矩形
  Circle ='Circle'// 圆/椭圆形}

在 Render 中记录当前图形类型,并提供修改方法与事件:

// src/Render/index.ts    // 略// 画图类型graphType: Types.GraphType |undefined=undefined// 略// 改变画图类型changeGraphType(type?: Types.GraphType){this.graphType = type
    this.emit('graph-type-change',this.graphType)}

工具栏按钮通讯:

// src/components/main-header/index.vue    // 略const emit =defineEmits([/* 略 */,'update:graphType'])const props =withDefaults(defineProps<{// 略
    graphType?: Types.GraphType
}>(),{// 略});// 略watch(()=> props.render,()=>{if(props.render){// 略

        props.render?.on('graph-type-change',(value)=>{emit('update:graphType', value)})}},{immediate:true})// 略functiononGraph(type: Types.GraphType){emit('update:graphType', props.graphType === type ?undefined: type)

以上就是绘制图形的工具栏入口。

Graph - 图形定义及其相关实现

相关代码文件:
1、src/Render/graphs/BaseGraph.ts - 抽象类:定义通用属性、逻辑、外部接口定义。
2、src/Render/graphs/Circle.ts 继承 BaseGraph - 构造 圆/椭形 ;处理创建部分交互信息;关键逻辑的实现。
3、src/Render/handlers/GraphHandlers.ts - 收集图形创建所需交互信息,接着交给 Circle 静态处理方法处理。
4、src/Render/draws/GraphDraw.ts - 绘制图形、调整点 - 绘制 调整点 的锚点;收集并处理交互信息,接着并交给 Circle 静态处理方法处理。

BaseGraph 抽象类

// src/Render/graphs/BaseGraph.ts// 略/**
 * 图形类
 * 实例主要用于新建图形时,含新建同时的大小拖动。
 * 静态方法主要用于新建之后,通过 调整点 调整的逻辑定义
 */export abstract classBaseGraph{/**
   * 更新 图形 的 调整点 的 锚点位置
   * @param width 图形 的 宽度
   * @param height 图形 的 高度
   * @param rotate 图形 的 旋转角度
   * @param anchorShadows 图形 的 调整点 的 锚点
   */staticupdateAnchorShadows(width: number,height: number,rotate: number,anchorShadows: Konva.Circle[]){
    console.log('请实现 updateAnchorShadows', width, height, anchorShadows)}/**
   * 更新 图形 的 连接点 的 锚点位置
   * @param width 图形 的 宽度
   * @param height 图形 的 高度
   * @param rotate 图形 的 旋转角度
   * @param anchors 图形 的 调整点 的 锚点
   */staticupdateLinkAnchorShadows(width: number,height: number,rotate: number,linkAnchorShadows: Konva.Circle[]){
    console.log('请实现 updateLinkAnchorShadows', width, height, linkAnchorShadows)}/**
   * 生成 调整点
   * @param render 渲染实例
   * @param graph 图形
   * @param anchor 调整点 定义
   * @param anchorShadow 调整点 锚点
   * @param adjustingId 正在操作的 调整点 id
   * @returns
   */staticcreateAnchorShape(render: Render,graph: Konva.Group,anchor: Types.GraphAnchor,anchorShadow: Konva.Circle,adjustType: string,adjustGroupId: string
  ): Konva.Shape {
    console.log('请实现 createAnchorShape', render, graph, anchor, anchorShadow, adjustingId, adjustGroupId)returnnewKonva.Shape()}/**
   * 调整 图形
   * @param render 渲染实例
   * @param graph 图形
   * @param graphSnap 图形 的 备份
   * @param rect 当前 调整点
   * @param rects 所有 调整点
   * @param startPoint 鼠标按下位置
   * @param endPoint 鼠标拖动位置
   */staticadjust(render: Render,graph: Konva.Group,graphSnap: Konva.Group,rect: Types.GraphAnchorShape,rects: Types.GraphAnchorShape[],startPoint: Konva.Vector2d,endPoint: Konva.Vector2d){
    console.log('请实现 updateAnchorShadows', render, graph, rect, startPoint, endPoint)}//protectedrender: Render
  group: Konva.Group
  id: string // 就是 group 的id/**
   * 鼠标按下位置
   */protecteddropPoint: Konva.Vector2d ={x:0,y:0}/**
   * 调整点 定义
   */protectedanchors: Types.GraphAnchor[]=[]/**
   * 调整点 的 锚点
   */protectedanchorShadows: Konva.Circle[]=[]/**
   * 调整点 定义
   */protectedlinkAnchors: Types.LinkDrawPoint[]=[]/**
   * 连接点 的 锚点
   */protectedlinkAnchorShadows: Konva.Circle[]=[]constructor(render: Render,dropPoint: Konva.Vector2d,config:{anchors: Types.GraphAnchor[]linkAnchors: Types.AssetInfoPoint[]}){this.render = render
    this.dropPoint = dropPoint

    this.id =nanoid()this.group =newKonva.Group({id:this.id,name:'asset',assetType: Types.AssetType.Graph
    })// 调整点 定义this.anchors = config.anchors.map((o)=>({...o,// 补充信息name:'anchor',groupId:this.group.id()}))// 记录在 group 中this.group.setAttr('anchors',this.anchors)// 新建 调整点 的 锚点for(const anchor ofthis.anchors){const circle =newKonva.Circle({adjustType: anchor.adjustType,name: anchor.name,radius:0// radius: this.render.toStageValue(1),// fill: 'red'})this.anchorShadows.push(circle)this.group.add(circle)}// 连接点 定义this.linkAnchors = config.linkAnchors.map((o)=>({...o,id:nanoid(),groupId:this.group.id(),visible:false,pairs:[],direction: o.direction,alias: o.alias
        })as Types.LinkDrawPoint
    )// 连接点信息this.group.setAttrs({points:this.linkAnchors
    })// 新建 连接点 的 锚点for(const point ofthis.linkAnchors){const circle =newKonva.Circle({name:'link-anchor',id: point.id,x: point.x,y: point.y,radius:this.render.toStageValue(1),stroke:'rgba(0,0,255,1)',strokeWidth:this.render.toStageValue(2),visible:false,direction: point.direction,alias: point.alias
      })this.linkAnchorShadows.push(circle)this.group.add(circle)}this.group.on('mouseenter',()=>{// 显示 连接点this.render.linkTool.pointsVisible(true,this.group)})this.group.on('mouseleave',()=>{// 隐藏 连接点this.render.linkTool.pointsVisible(false,this.group)// 隐藏 hover 框this.group.findOne('#hoverRect')?.visible(false)})this.render.layer.add(this.group)this.render.redraw()}/**
   * 调整进行时
   * @param point 鼠标位置 相对位置
   */
  abstract drawMove(point: Konva.Vector2d):void/**
   * 调整结束
   */
  abstract drawEnd():void}

这里的:

  • 静态方法,相当定义了绘制图形必要的工具方法,具体实现交给具体的图形类定义;
  • 接着是绘制图形必要的属性及其初始化;
  • 最后,抽象方法约束了图形实例必要的方法。

绘制 圆/椭形

图形是可以调整的,这里 圆/椭形 拥有 8 个 调整点:

在这里插入图片描述

还要考虑图形被旋转后,依然能合理调整:

在这里插入图片描述

调整本身也是支持磁贴的:

在这里插入图片描述

图形也支持 连接点:

在这里插入图片描述

图形类 - Circle

// src/Render/graphs/Circle.ts// 略/**
 * 图形 圆/椭圆
 */exportclassCircleextendsBaseGraph{// 实现:更新 图形 的 调整点 的 锚点位置static override updateAnchorShadows(width: number,height: number,rotate: number,anchorShadows: Konva.Circle[]):void{for(const shadow of anchorShadows){switch(shadow.attrs.id){case'top':
          shadow.position({x: width /2,y:0})breakcase'bottom':
          shadow.position({x: width /2,y: height
          })breakcase'left':
          shadow.position({x:0,y: height /2})breakcase'right':
          shadow.position({x: width,y: height /2})breakcase'top-left':
          shadow.position({x:0,y:0})breakcase'top-right':
          shadow.position({x: width,y:0})breakcase'bottom-left':
          shadow.position({x:0,y: height
          })breakcase'bottom-right':
          shadow.position({x: width,y: height
          })break}}}// 实现:更新 图形 的 连接点 的 锚点位置static override updateLinkAnchorShadows(width: number,height: number,rotate: number,linkAnchorShadows: Konva.Circle[]):void{for(const shadow of linkAnchorShadows){switch(shadow.attrs.alias){case'top':
          shadow.position({x: width /2,y:0})breakcase'bottom':
          shadow.position({x: width /2,y: height
          })breakcase'left':
          shadow.position({x:0,y: height /2})breakcase'right':
          shadow.position({x: width,y: height /2})breakcase'center':
          shadow.position({x: width /2,y: height /2})break}}}// 实现:生成 调整点staticcreateAnchorShape(render: Types.Render,graph: Konva.Group,anchor: Types.GraphAnchor,anchorShadow: Konva.Circle,adjustType: string,adjustGroupId: string
  ): Konva.Shape {// stage 状态const stageState = render.getStageState()const x = render.toStageValue(anchorShadow.getAbsolutePosition().x - stageState.x),
      y = render.toStageValue(anchorShadow.getAbsolutePosition().y - stageState.y)const offset = render.pointSize +5const shape =newKonva.Line({name:'anchor',anchor: anchor,//// stroke: colorMap[anchor.id] ?? 'rgba(0,0,255,0.2)',stroke:
        adjustType === anchor.adjustType && graph.id()=== adjustGroupId
          ?'rgba(0,0,255,0.8)':'rgba(0,0,255,0.2)',strokeWidth: render.toStageValue(2),// 位置
      x,
      y,// 路径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]])}[anchor.id]??[],// 旋转角度rotation: graph.getAbsoluteRotation()})

    shape.on('mouseenter',()=>{
      shape.stroke('rgba(0,0,255,0.8)')
      document.body.style.cursor ='move'})
    shape.on('mouseleave',()=>{
      shape.stroke(shape.attrs.adjusting ?'rgba(0,0,255,0.8)':'rgba(0,0,255,0.2)')
      document.body.style.cursor = shape.attrs.adjusting ?'move':'default'})return shape
  }// 实现:调整 图形static override adjust(render: Types.Render,graph: Konva.Group,graphSnap: Konva.Group,shapeRecord: Types.GraphAnchorShape,shapeRecords: Types.GraphAnchorShape[],startPoint: Konva.Vector2d,endPoint: Konva.Vector2d){// 目标 圆/椭圆const circle = graph.findOne('.graph')as Konva.Ellipse
    // 镜像const circleSnap = graphSnap.findOne('.graph')as Konva.Ellipse

    // 调整点 锚点const anchors =(graph.find('.anchor')??[])as Konva.Circle[]// 连接点 锚点const linkAnchors =(graph.find('.link-anchor')??[])as Konva.Circle[]const{shape: adjustShape }= shapeRecord

    if(circle && circleSnap){let[graphWidth, graphHeight]=[graph.width(), graph.height()]const[graphRotation, anchorId, ex, ey]=[
        Math.round(graph.rotation()),
        adjustShape.attrs.anchor?.id,
        endPoint.x,
        endPoint.y
      ]letanchorShadow: Konva.Circle |undefined,anchorShadowAcross: Konva.Circle |undefinedswitch(anchorId){case'top':{
            anchorShadow = graphSnap.findOne(`#top`)
            anchorShadowAcross = graphSnap.findOne(`#bottom`)}breakcase'bottom':{
            anchorShadow = graphSnap.findOne(`#bottom`)
            anchorShadowAcross = graphSnap.findOne(`#top`)}breakcase'left':{
            anchorShadow = graphSnap.findOne(`#left`)
            anchorShadowAcross = graphSnap.findOne(`#right`)}breakcase'right':{
            anchorShadow = graphSnap.findOne(`#right`)
            anchorShadowAcross = graphSnap.findOne(`#left`)}breakcase'top-left':{
            anchorShadow = graphSnap.findOne(`#top-left`)
            anchorShadowAcross = graphSnap.findOne(`#bottom-right`)}breakcase'top-right':{
            anchorShadow = graphSnap.findOne(`#top-right`)
            anchorShadowAcross = graphSnap.findOne(`#bottom-left`)}breakcase'bottom-left':{
            anchorShadow = graphSnap.findOne(`#bottom-left`)
            anchorShadowAcross = graphSnap.findOne(`#top-right`)}breakcase'bottom-right':{
            anchorShadow = graphSnap.findOne(`#bottom-right`)
            anchorShadowAcross = graphSnap.findOne(`#top-left`)}break}if(anchorShadow && anchorShadowAcross){const{x: sx,y: sy }= anchorShadow.getAbsolutePosition()const{x: ax,y: ay }= anchorShadowAcross.getAbsolutePosition()// anchorShadow:它是当前操作的 调整点 锚点// anchorShadowAcross:它是当前操作的 调整点 反方向对面的 锚点// 调整大小{// 略// 计算比较复杂,不一定是最优方案,详情请看工程代码。// 基本逻辑:// 1、通过鼠标移动,计算当前鼠标位置、当前操作的 调整点 锚点 位置(原位置) 分别与 anchorShadowAcross(原位置)的距离;// 2、 保持 anchorShadowAcross 位置固定,通过上面两距离的变化比例,计算最新的宽高大小;// 3、期间要约束不同角度不同方向的宽高处理,有的只改变宽、有的只改变高、有的同时改变宽和高。}// 调整位置{// 略// 计算比较复杂,不一定是最优方案,详情请看工程代码。// 基本逻辑:// 利用三角函数,通过最新的宽高,调整图形的坐标。}}// 更新 圆/椭圆 大小
      circle.x(graphWidth /2)
      circle.radiusX(graphWidth /2)
      circle.y(graphHeight /2)
      circle.radiusY(graphHeight /2)// 更新 调整点 的 锚点 位置
      Circle.updateAnchorShadows(graphWidth, graphHeight, graphRotation, anchors)// 更新 图形 的 连接点 的 锚点位置
      Circle.updateLinkAnchorShadows(graphWidth, graphHeight, graphRotation, linkAnchors)// stage 状态const stageState = render.getStageState()// 更新 调整点 位置for(const anchor of anchors){for(const{ shape }of shapeRecords){if(shape.attrs.anchor?.adjustType === anchor.attrs.adjustType){const anchorShadow = graph.findOne(`#${anchor.attrs.id}`)if(anchorShadow){
              shape.position({x: render.toStageValue(anchorShadow.getAbsolutePosition().x - stageState.x),y: render.toStageValue(anchorShadow.getAbsolutePosition().y - stageState.y)})
              shape.rotation(graph.getAbsoluteRotation())}}}}}}/**
   * 默认图形大小
   */static size =100/**
   * 圆/椭圆 对应的 Konva 实例
   */privatecircle: Konva.Ellipse

  constructor(render: Types.Render,dropPoint: Konva.Vector2d){super(render, dropPoint,{// 定义了 8 个 调整点anchors:[{adjustType:'top'},{adjustType:'bottom'},{adjustType:'left'},{adjustType:'right'},{adjustType:'top-left'},{adjustType:'top-right'},{adjustType:'bottom-left'},{adjustType:'bottom-right'}].map((o)=>({adjustType: o.adjustType,// 调整点 类型定义type: Types.GraphType.Circle // 记录所属 图形})),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 Types.AssetInfoPoint[]})// 新建 圆/椭圆this.circle =newKonva.Ellipse({name:'graph',x:0,y:0,radiusX:0,radiusY:0,stroke:'black',strokeWidth:1})// 加入this.group.add(this.circle)// 鼠标按下位置 作为起点this.group.position(this.dropPoint)}// 实现:拖动进行时
  override drawMove(point: Konva.Vector2d):void{// 鼠标拖动偏移量let offsetX = point.x -this.dropPoint.x,
      offsetY = point.y -this.dropPoint.y

    // 确保不翻转if(offsetX <1){
      offsetX =1}if(offsetY <1){
      offsetY =1}// 半径const radiusX = offsetX /2,
      radiusY = offsetY /2// 圆/椭圆 位置大小this.circle.x(radiusX)this.circle.y(radiusY)this.circle.radiusX(radiusX)this.circle.radiusY(radiusY)// group 大小this.group.size({width: offsetX,height: offsetY
    })// 更新 图形 的 调整点 的 锚点位置
    Circle.updateAnchorShadows(offsetX, offsetY,1,this.anchorShadows)// 更新 图形 的 连接点 的 锚点位置
    Circle.updateLinkAnchorShadows(offsetX, offsetY,1,this.linkAnchorShadows)// 重绘this.render.redraw([Draws.GraphDraw.name, Draws.LinkDraw.name])}// 实现:拖动结束
  override drawEnd():void{if(this.circle.radiusX()<=1&&this.circle.radiusY()<=1){// 加入只点击,无拖动// 默认大小const width = Circle.size,
        height = width

      const radiusX = Circle.size /2,
        radiusY = radiusX

      // 圆/椭圆 位置大小this.circle.x(radiusX)this.circle.y(radiusY)this.circle.radiusX(radiusX -this.circle.strokeWidth())this.circle.radiusY(radiusY -this.circle.strokeWidth())// group 大小this.group.size({
        width,
        height
      })// 更新 图形 的 调整点 的 锚点位置
      Circle.updateAnchorShadows(width, height,1,this.anchorShadows)// 更新 图形 的 连接点 的 锚点位置
      Circle.updateLinkAnchorShadows(width, height,1,this.linkAnchorShadows)// 对齐线清除this.render.attractTool.alignLinesClear()// 重绘this.render.redraw([Draws.GraphDraw.name, Draws.LinkDraw.name])}}}

GraphHandlers

// src/Render/handlers/GraphHandlers.ts    // 略exportclassGraphHandlersimplementsTypes.Handler{// 略/**
   * 新建图形中
   */
  graphing =false/**
   * 当前新建图形类型
   */currentGraph: Graphs.BaseGraph |undefined/**
   * 获取鼠标位置,并处理为 相对大小
   * @param attract 含磁贴计算
   * @returns
   */getStagePoint(attract =false){const pos =this.render.stage.getPointerPosition()if(pos){const stageState =this.render.getStageState()if(attract){// 磁贴const{pos: transformerPos }=this.render.attractTool.attractPoint(pos)return{x:this.render.toStageValue(transformerPos.x - stageState.x),y:this.render.toStageValue(transformerPos.y - stageState.y)}}else{return{x:this.render.toStageValue(pos.x - stageState.x),y:this.render.toStageValue(pos.y - stageState.y)}}}returnnull}

  handlers ={stage:{mousedown:(e: Konva.KonvaEventObject<GlobalEventHandlersEventMap['mousedown']>)=>{if(this.render.graphType){// 选中图形类型,开始if(e.target ===this.render.stage){this.graphing =truethis.render.selectionTool.selectingClear()const point =this.getStagePoint()if(point){if(this.render.graphType === Types.GraphType.Circle){// 新建 圆/椭圆 实例this.currentGraph =newGraphs.Circle(this.render, point)}}}}},mousemove:()=>{if(this.graphing){if(this.currentGraph){const pos =this.getStagePoint(true)if(pos){// 新建并马上调整图形this.currentGraph.drawMove(pos)}}}},mouseup:()=>{if(this.graphing){if(this.currentGraph){// 调整结束this.currentGraph.drawEnd()}// 调整结束this.graphing =false// 清空图形类型选择this.render.changeGraphType()// 对齐线清除this.render.attractTool.alignLinesClear()// 重绘this.render.redraw([Draws.GraphDraw.name, Draws.LinkDraw.name])}}}}}

GraphDraw

// src/Render/draws/GraphDraw.ts    // 略exportinterfaceGraphDrawState{/**
   * 调整中
   */adjusting: boolean

  /**
   * 调整中 id
   */adjustType: string
}// 略exportclassGraphDrawextendsTypes.BaseDrawimplementsTypes.Draw{// 略state: GraphDrawState ={adjusting:false,adjustType:''}/**
   * 鼠标按下 调整点 位置
   */startPoint: Konva.Vector2d ={x:0,y:0}/**
   * 图形 group 镜像
   */graphSnap: Konva.Group |undefinedconstructor(render: Types.Render,layer: Konva.Layer,option: GraphDrawOption){super(render, layer)this.option = option

    this.group.name(this.constructor.name)}/**
   * 获取鼠标位置,并处理为 相对大小
   * @param attract 含磁贴计算
   * @returns
   */getStagePoint(attract =false){const pos =this.render.stage.getPointerPosition()if(pos){const stageState =this.render.getStageState()if(attract){// 磁贴const{pos: transformerPos }=this.render.attractTool.attractPoint(pos)return{x:this.render.toStageValue(transformerPos.x - stageState.x),y:this.render.toStageValue(transformerPos.y - stageState.y)}}else{return{x:this.render.toStageValue(pos.x - stageState.x),y:this.render.toStageValue(pos.y - stageState.y)}}}returnnull}// 调整 预处理、定位静态方法adjusts(shapeDetailList:{graph: Konva.Group
      shapeRecords:{shape: Konva.Shape; anchorShadow: Konva.Circle }[]}[]){for(const{ shapeRecords, graph }of shapeDetailList){for(const{ shape }of shapeRecords){
        shape.setAttr('adjusting',false)}for(const shapeRecord of shapeRecords){const{ shape }= shapeRecord
        // 鼠标按下
        shape.on('mousedown',()=>{this.state.adjusting =truethis.state.adjustType = shape.attrs.anchor?.adjustType
          this.state.adjustGroupId = graph.id()

          shape.setAttr('adjusting',true)const pos =this.getStagePoint()if(pos){this.startPoint = pos

            // 图形 group 镜像,用于计算位置、大小的偏移this.graphSnap = graph.clone()}})// 调整中this.render.stage.on('mousemove',()=>{if(this.state.adjusting &&this.graphSnap){if(shape.attrs.anchor?.type === Types.GraphType.Circle){// 调整 圆/椭圆 图形if(shape.attrs.adjusting){const pos =this.getStagePoint(true)if(pos){// 使用 圆/椭圆 静态处理方法
                  Graphs.Circle.adjust(this.render,
                    graph,this.graphSnap,
                    shapeRecord,
                    shapeRecords,this.startPoint,
                    pos
                  )// 重绘this.render.redraw([Draws.GraphDraw.name, Draws.LinkDraw.name])}}}}})// 调整结束this.render.stage.on('mouseup',()=>{this.state.adjusting =falsethis.state.adjustType =''this.state.adjustGroupId =''// 恢复显示所有 调整点for(const{ shape }of shapeRecords){
            shape.opacity(1)
            shape.setAttr('adjusting',false)
            shape.stroke('rgba(0,0,255,0.2)')
            document.body.style.cursor ='default'}// 销毁 镜像this.graphSnap?.destroy()// 对齐线清除this.render.attractTool.alignLinesClear()})this.group.add(shape)}}}

  override draw(){this.clear()// 所有图形const graphs =this.render.layer
      .find('.asset').filter((o)=> o.attrs.assetType === Types.AssetType.Graph)as Konva.Group[]constshapeDetailList:{graph: Konva.Group
      shapeRecords:{shape: Konva.Shape; anchorShadow: Konva.Circle }[]}[]=[]for(const graph of graphs){// 非选中状态才显示 调整点if(!graph.attrs.selected){const anchors =(graph.attrs.anchors ??[])as Types.GraphAnchor[]constshapeRecords:{shape: Konva.Shape; anchorShadow: Konva.Circle }[]=[]// 根据 调整点 信息,创建for(const anchor of anchors){// 调整点 的显示 依赖其隐藏的 锚点 位置、大小等信息const anchorShadow = graph.findOne(`#${anchor.id}`)as Konva.Circle
          if(anchorShadow){const shape = Graphs.Circle.createAnchorShape(this.render,
              graph,
              anchor,
              anchorShadow,this.state.adjustingId,this.state.adjustGroupId
            )

            shapeRecords.push({ shape, anchorShadow })}}

        shapeDetailList.push({
          graph,
          shapeRecords
        })}}this.adjusts(shapeDetailList)}}

稍显臃肿,后面慢慢优化吧 -_-

canvas2svg 打补丁

上面已经实现了绘制图形(圆/椭形),但是导出 svg 的时候报错了。经过错误定位以及源码阅读,发现:

1、当 Konva.Group 包含 Konva.Ellipse 的时候,无法导出 svg 文件
2、对 Konva.Ellipse 调整如 radiusX、radiusY 属性时,无法正确输出 path 路径

1、canvas2svg 尝试给 g 节点赋予 path 属性,导致异常报错。

现通过 hack __applyCurrentDefaultPath 方法,增加处理 nodeName === ‘g’ 的场景

2、查看 Konva.Ellipse.prototype._sceneFunc 方法源码,Konva 绘制 Ellipse 是通过 canvas 的 arc + scale 实现的,对应代码注释 A。

实际效果,无法仿照 canvas 的平均 scale,会出现 stroke 粗细不一。

因此,尝试通过识别 scale 修改 path 特征,修复此问题。

// src/Render/tools/ImportExportTool.ts    C2S.prototype.__applyCurrentDefaultPath=function(){// 补丁:修复以下问题:// 1、当 Konva.Group 包含 Konva.Ellipse 的时候,无法导出 svg 文件// 2、对 Konva.Ellipse 调整如 radiusX、radiusY 属性时,无法正确输出 path 路径//// PS:// 1、canvas2svg 尝试给 g 节点赋予 path 属性,导致异常报错。// 现通过 hack __applyCurrentDefaultPath 方法,增加处理 nodeName === 'g' 的场景//// 2、查看 Konva.Ellipse.prototype._sceneFunc 方法源码,// Konva 绘制 Ellipse 是通过 canvas 的 arc + scale 实现的,对应代码注释 A,// 实际效果,无法仿照 canvas 的平均 scale,会出现 stroke 粗细不一。// 因此,尝试通过识别 scale 修改 path 特征,修复此问题。//// (以上 hack 仅针对示例绘制 图形 时的特征进行处理,并未深入研究 canvas2svg 为何会进入错误的逻辑)if(this.__currentElement.nodeName ==='g'){const g =this.__currentElement.querySelector('g')if(g){// 注释 A// const d = this.__currentDefaultPath// const path = document.createElementNS('http://www.w3.org/2000/svg', 'path') as SVGElement// path.setAttribute('d', d)// path.setAttribute('fill', 'none')// g.append(path)const scale = g.getAttribute('transform')if(scale){const match = scale.match(/scale\(([^),]+),([^)]+)\)/)if(match){const[sx, sy]=[parseFloat(match[1]),parseFloat(match[2])]let d =this.__currentDefaultPath
          const reg =/A ([^ ]+) ([^ ]+) /const match2 = d.match(reg)if(match2){const[rx, ry]=[parseFloat(match2[1]),parseFloat(match2[2])]
            d = d.replace(reg,`A ${rx * sx}${ry * sy}`)const path = document.createElementNS('http://www.w3.org/2000/svg','path')as SVGElement
            path.setAttribute('d', d)
            path.setAttribute('fill','none')this.__currentElement.append(path)}}}else{const d =this.__currentDefaultPath
        const path = document.createElementNS('http://www.w3.org/2000/svg','path')as SVGElement
        path.setAttribute('d', d)
        path.setAttribute('fill','none')this.__currentElement.append(path)}}

    console.warn('[Hacked] Attempted to apply path command to node '+this.__currentElement.nodeName
    )return}// 原逻辑if(this.__currentElement.nodeName ==='path'){const d =this.__currentDefaultPath
    this.__currentElement.setAttribute('d', d)}else{thrownewError('Attempted to apply path command to node '+this.__currentElement.nodeName)}}

以上 hack 仅针对示例绘制 图形 时的特征进行处理,并未深入研究 canvas2svg 为何会进入错误的逻辑

拐点旋转修复

测试发现,连接线 的 拐点 并没有能跟随旋转角度调整坐标,因此补充一个修复:

在这里插入图片描述

// src/Render/handlers/SelectionHandlers.ts    // 略/**
   * 矩阵变换:坐标系中的一个点,围绕着另外一个点进行旋转
   * -  -   -        - -   -   - -
   * |x`|   |cos -sin| |x-a|   |a|
   * |  | = |        | |   | +
   * |y`|   |sin  cos| |y-b|   |b|
   * -  -   -        - -   -   - -
   * @param x 目标节点坐标 x
   * @param y 目标节点坐标 y
   * @param centerX 围绕的点坐标 x
   * @param centerY 围绕的点坐标 y
   * @param angle 旋转角度
   * @returns
   */rotatePoint(x: number,y: number,centerX: number,centerY: number,angle: number){// 将角度转换为弧度const radians =(angle * Math.PI)/180// 计算旋转后的坐标const newX = Math.cos(radians)*(x - centerX)- Math.sin(radians)*(y - centerY)+ centerX
    const newY = Math.sin(radians)*(x - centerX)+ Math.cos(radians)*(y - centerY)+ centerY

    return{x: newX,y: newY }}

  lastRotation =0// 略
  handlers ={// 略transformer:{transform:()=>{// 旋转时,拐点也要跟着动const back =this.render.transformer.findOne('.back')if(back){// stage 状态const stageState =this.render.getStageState()const{ x, y, width, height }= back.getClientRect()const rotation = back.getAbsoluteRotation()-this.lastRotation
          const centerX = x + width /2const centerY = y + height /2const groups =this.render.transformer.nodes()const points = groups.reduce((ps, group)=>{return ps.concat(Array.isArray(group.getAttr('points'))? group.getAttr('points'):[])},[]as Types.LinkDrawPoint[])const pairs = points.reduce((ps, point)=>{return ps.concat(point.pairs ? point.pairs.filter((o)=>!o.disabled):[])},[]as Types.LinkDrawPair[])for(const pair of pairs){const fromGroup = groups.find((o)=> o.id()=== pair.from.groupId)const toGroup = groups.find((o)=> o.id()=== pair.to.groupId)// 必须成对移动才记录if(fromGroup && toGroup){// 移动if(fromGroup.attrs.manualPointsMap && fromGroup.attrs.manualPointsMapBefore){let manualPoints = fromGroup.attrs.manualPointsMap[pair.id]const manualPointsBefore = fromGroup.attrs.manualPointsMapBefore[pair.id]if(Array.isArray(manualPoints)&& Array.isArray(manualPointsBefore)){
                  manualPoints = manualPointsBefore.map((o: Types.ManualPoint)=>{const{ x, y }=this.rotatePoint(this.render.toBoardValue(o.x)+ stageState.x,this.render.toBoardValue(o.y)+ stageState.y,
                      centerX,
                      centerY,
                      rotation
                    )return{x:this.render.toStageValue(x - stageState.x),y:this.render.toStageValue(y - stageState.y)}})

                  fromGroup.setAttr('manualPointsMap',{...fromGroup.attrs.manualPointsMap,[pair.id]: manualPoints
                  })}}}}}// 重绘this.render.redraw([Draws.GraphDraw.name, Draws.LinkDraw.name, Draws.PreviewDraw.name])}}// 略}

Thanks watching~

More Stars please!勾勾手指~

源码

gitee源码

示例地址

标签: 前端 canva可画 vue

本文转载自: https://blog.csdn.net/xachary2/article/details/141354363
版权归原作者 @xachary 所有, 如有侵权,请联系我们删除。

“前端使用 Konva 实现可视化设计器(21)- 绘制图形(椭圆)”的评论:

还没有评论