本章开始补充一些基础的图形绘制,比如绘制:直线、曲线、圆/椭形、矩形。这一章主要分享一下本示例是如何开始绘制一个图形的,并以绘制圆/椭形为实现目标。
请大家动动小手,给我一个免费的 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源码
示例地址
版权归原作者 @xachary 所有, 如有侵权,请联系我们删除。