前端使用 Konva 实现可视化设计器(21)- 绘制图形(椭圆)
本章开始补充一些基础的图形绘制,比如绘制:直线、曲线、圆/椭形、矩形。这一章主要分享一下本示例是如何开始绘制一个图形的,并以绘制圆/椭形为实现目标。
请大家动动小手,给我一个免费的 Star 吧~
大家如果发现了 Bug,欢迎来提 Issue 哟~
github源码
gitee源码
示例地址
接下来主要说说:
- UI
- Graph(图形)
- canvas2svg 打补丁
- 拐点旋转修复
UI - 图形绘制类型切换
先找几个图标,增加按钮,分别代表绘制图形:直线、曲线、圆/椭形、矩形:
选中图形类型后,即可通过拖动绘制图形(绘制完成后,清空选择):
定义图形类型:
// src/Render/types.ts /*** 图形类型*/
export enum 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 = typethis.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
})// 略function onGraph(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 class BaseGraph {/*** 更新 图形 的 调整点 的 锚点位置* @param width 图形 的 宽度* @param height 图形 的 高度* @param rotate 图形 的 旋转角度* @param anchorShadows 图形 的 调整点 的 锚点*/static updateAnchorShadows(width: number,height: number,rotate: number,anchorShadows: Konva.Circle[]) {console.log('请实现 updateAnchorShadows', width, height, anchorShadows)}/*** 更新 图形 的 连接点 的 锚点位置* @param width 图形 的 宽度* @param height 图形 的 高度* @param rotate 图形 的 旋转角度* @param anchors 图形 的 调整点 的 锚点*/static updateLinkAnchorShadows(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*/static createAnchorShape(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)return new Konva.Shape()}/*** 调整 图形* @param render 渲染实例* @param graph 图形* @param graphSnap 图形 的 备份* @param rect 当前 调整点* @param rects 所有 调整点* @param startPoint 鼠标按下位置* @param endPoint 鼠标拖动位置*/static adjust(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)}//protected render: Rendergroup: Konva.Groupid: string // 就是 group 的id/*** 鼠标按下位置*/protected dropPoint: Konva.Vector2d = { x: 0, y: 0 }/*** 调整点 定义*/protected anchors: Types.GraphAnchor[] = []/*** 调整点 的 锚点*/protected anchorShadows: Konva.Circle[] = []/*** 调整点 定义*/protected linkAnchors: Types.LinkDrawPoint[] = []/*** 连接点 的 锚点*/protected linkAnchorShadows: Konva.Circle[] = []constructor(render: Render,dropPoint: Konva.Vector2d,config: {anchors: Types.GraphAnchor[]linkAnchors: Types.AssetInfoPoint[]}) {this.render = renderthis.dropPoint = dropPointthis.id = nanoid()this.group = new Konva.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 of this.anchors) {const circle = new Konva.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 of this.linkAnchors) {const circle = new Konva.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// 略/*** 图形 圆/椭圆*/
export class Circle extends BaseGraph {// 实现:更新 图形 的 调整点 的 锚点位置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}}}// 实现:生成 调整点static createAnchorShape(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 = new Konva.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 } = shapeRecordif (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]let anchorShadow: 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 实例*/private circle: Konva.Ellipseconstructor(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 = new Konva.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 = widthconst 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 // 略export class GraphHandlers implements Types.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)}}}return null}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 = new Graphs.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 // 略export interface GraphDrawState {/*** 调整中*/adjusting: boolean/*** 调整中 id*/adjustType: string
}// 略export class GraphDraw extends Types.BaseDraw implements Types.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 = optionthis.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)}}}return null}// 调整 预处理、定位静态方法adjusts(shapeDetailList: {graph: Konva.GroupshapeRecords: { 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?.adjustTypethis.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[]const shapeDetailList: {graph: Konva.GroupshapeRecords: { shape: Konva.Shape; anchorShadow: Konva.Circle }[]}[] = []for (const graph of graphs) {// 非选中状态才显示 调整点if (!graph.attrs.selected) {const anchors = (graph.attrs.anchors ?? []) as Types.GraphAnchor[]const shapeRecords: { shape: Konva.Shape; anchorShadow: Konva.Circle }[] = []// 根据 调整点 信息,创建for (const anchor of anchors) {// 调整点 的显示 依赖其隐藏的 锚点 位置、大小等信息const anchorShadow = graph.findOne(`#${anchor.id}`) as Konva.Circleif (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.__currentDefaultPathconst 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 SVGElementpath.setAttribute('d', d)path.setAttribute('fill', 'none')this.__currentElement.append(path)}}} else {const d = this.__currentDefaultPathconst path = document.createElementNS('http://www.w3.org/2000/svg', 'path') as SVGElementpath.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.__currentDefaultPaththis.__currentElement.setAttribute('d', d)} else {throw new Error('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) + centerXconst newY = Math.sin(radians) * (x - centerX) + Math.cos(radians) * (y - centerY) + centerYreturn { 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.lastRotationconst 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源码
示例地址
相关文章:
![](https://i-blog.csdnimg.cn/direct/71d94c2786914e14ba9560b141ebade1.png)
前端使用 Konva 实现可视化设计器(21)- 绘制图形(椭圆)
本章开始补充一些基础的图形绘制,比如绘制:直线、曲线、圆/椭形、矩形。这一章主要分享一下本示例是如何开始绘制一个图形的,并以绘制圆/椭形为实现目标。 请大家动动小手,给我一个免费的 Star 吧~ 大家如果发现了 Bug,…...
![](https://www.ngui.cc/images/no-images.jpg)
Python 将单词拆分为单个字母组成的列表对象
Python 将单词拆分为单个字母组成的列表对象 正文 正文 这里介绍一个简单算法,将英文单词拆分为其对应字母组成的列表。 str1 ACG lst1 [i for i in str1] lst2 list(str1)# Method 1 print(lst1) # Method 2 print(lst2) """ result: [A, C, G…...
![](https://www.ngui.cc/images/no-images.jpg)
欧洲 摩纳哥税务知识
摩纳哥是一个位于法国南部的城邦国家,以其豪华的生活环境和宽松的税收政策而闻名。自1869年以来,摩纳哥取消了个人所得税的征收,这使得它成为富裕人士和外籍人士的理想居住地。然而,这并不意味着摩纳哥的税收制度完全不存在。以下…...
![](https://www.ngui.cc/images/no-images.jpg)
域控制器的四大支柱分别是车载以太网、自适应Autosar
域控制器的四大支柱分别是车载以太网、自适应Autosar、高性能处理器和集中式E/E架构。 百度安全验证 。自适应Autosar采用Proxy/Skeleton的通信架构,同时采用中间件SOME/IP...
![](https://img-blog.csdnimg.cn/img_convert/88e6b3d44d44a1064061f2ad59036030.png)
写给大数据开发:如何优化临时数据查询流程
你是否曾因为频繁的临时数据查询请求而感到烦恼?这些看似简单的任务是否正在蚕食你的宝贵时间,影响你的主要工作?如果是,那么这篇文章正是为你而写。 目录 引言:数据开发者的困境问题剖析:临时数据查询的…...
![](https://img-blog.csdnimg.cn/img_convert/babe31829dfe932f09e9c9a69624431c.png)
【MongoDB】Java连接MongoDB
连接URI 连接 URI提供驱动程序用于连接到 MongoDB 部署的指令集。该指令集指示驱动程序应如何连接到 MongoDB,以及在连接时应如何运行。下图解释了示例连接 URI 的各个部分: 连接的URI 主要分为 以下四个部分 第一部分 连接协议 示例中使用的 连接到具有…...
![](https://www.ngui.cc/images/no-images.jpg)
nginx支持的不同事件驱动模型
Nginx 支持的不同事件驱动模型 Nginx 是一款高性能的 Web 和反向代理服务器,它支持多种事件驱动模型来处理网络 I/O 操作。不同的操作系统及其版本支持不同的事件驱动模型,这些模型对于 Nginx 的并发处理能力和性能至关重要。下面详细介绍 Nginx 支持的…...
![](https://img-blog.csdnimg.cn/img_convert/ebfa358f13986a8adf491c35610493eb.png)
C++ TinyWebServer项目总结(7. Linux服务器程序规范)
进程 PID 进程的PID(Process ID)是操作系统中用于唯一标识一个进程的整数值。每个进程在创建时,操作系统都会分配一个唯一的PID,用来区分不同的进程。 PID的特点 唯一性: 在操作系统运行的某一时刻,每个…...
![](https://i-blog.csdnimg.cn/direct/7d9c3cbc3aed463680c1d0dda8604ab7.png)
基于STM32单片机设计的秒表时钟计时器仿真系统——程序源码proteus仿真图设计文档演示视频等(文末工程资料下载)
基于STM32单片机设计的秒表时钟计时器仿真系统 演示视频 基于STM32单片机设计的秒表时钟计时器仿真系统 摘要 本设计基于STM32单片机,设计并实现了一个秒表时钟计时器仿真系统。系统通过显示器实时显示当前时间,并通过定时器实现秒表计时功能。显示小时…...
![](https://i-blog.csdnimg.cn/direct/62c88548d70b4013bf1f9668a8d07bc9.png)
人才流失预测项目
在本项目中,通过数据科学和AI的方法,分析挖掘人力资源流失问题,并基于机器学习构建解决问题的方法,并且,我们通过对AI模型的反向解释,可以深入理解导致人员流失的主要因素,HR部门也可以根据分析…...
![](https://i-blog.csdnimg.cn/direct/9156ed4423524e5ea519f8c4a67292a4.png)
BUG——imx6u开发_结构体导致的死机问题(未解决)
简介: 最近在做imx6u的linux下裸机驱动开发,由于是学习的初级阶段,既没有现成的IDE可以使用,也没有GDB等在线调试工具,只能把代码烧写在SD卡上再反复插拔,仅靠卑微的亮灯来判断程序死在哪一步。 至于没有使…...
![](https://www.ngui.cc/images/no-images.jpg)
问答:什么是对称密钥、非对称密钥,http怎样变成https的?
文章目录 对称密钥 vs 非对称密钥HTTP 变成 HTTPS 的过程 对称密钥 vs 非对称密钥 1. 对称密钥加密 定义: 对称密钥加密是一种加密算法,其中加密和解密使用的是同一个密钥。特点: 速度快: 因为只使用一个密钥,所以加密和解密速度较快。密钥分发问题: 双…...
![](https://i-blog.csdnimg.cn/direct/503a6235e74545f2a1642f51cbfeecd3.jpeg)
虚拟滚动列表组件ReVirtualList
虚拟滚动列表组件ReVirtualList 组件实现基于 Vue3 Element Plus Typescript,同时引用 vueUse lodash-es tailwindCss (不影响功能,可忽略) 在 ReList 的基础上,增加虚拟列表功能,在固定高度的基础上,可以优化大数…...
![](https://img-blog.csdnimg.cn/img_convert/791fbded99bfe0127c00aa56d462869f.jpeg)
稳定、耐用、美观 一探究竟六角头螺钉螺栓如何选择
在机器与技术未被发现的过去,紧固件设计和品质并不稳定。但是,他们已成为当今许多行业无处不在的构成部分。六角头标准件或六角头标准件是紧固件中持续的头部设计之一,它有六个面,对广泛工业应用大有益处。六角头标准件或常分成六…...
![](https://i-blog.csdnimg.cn/direct/21360692a6b14b17becc04daeaee24a1.png)
数据库Mybatis基础操作
目录 基础操作 删除 预编译SQL 增、改、查 自动封装 基础操作 环境准备 删除 根据主键动态删除数据:使用了mybatis中的参数占位符#{ },里面是传进去的参数。 单元测试: 另外,这个方法是有返回值的,返回这次操作…...
![](https://i-blog.csdnimg.cn/direct/12b1241c21414346a4bccaea46485f3b.png)
人物形象设计:塑造独特角色的指南
引言 人物形象设计是一种创意过程,它利用强大的设计工具,通过视觉和叙述元素塑造角色的外在特征和内在性格。这种设计不仅赋予角色以生命,还帮助观众或读者在心理层面上与角色建立联系。人物形象设计的重要性在于它能够增强故事的吸引力和说…...
![](https://i-blog.csdnimg.cn/direct/eefcbf721a704444962b6e66a414fcd1.png#pic_center)
网络安全-安全策略初认识
文章目录 前言理论介绍1. 安全策略1.1 定义:1.2 关键术语: 2. 防火墙状态监测 实战步骤1:实验环境搭建步骤2:配置实现 总结1. 默认安全策略2. 自定义安全策略3. 防火墙状态会话表 前言 who:本文主要写给入门防火墙的技…...
![](https://www.ngui.cc/images/no-images.jpg)
python import相对导入与绝对导入
文章目录 相对导入与绝对导入绝对导入相对导入何时使用相对导入何时使用绝对导入示例 相对导入与绝对导入 在Python中,from .file_manager import SomeFunction 和 from file_manager import SomeFunction 两种导入方式看似相似,但在模块寻找机制上存在…...
![](https://www.ngui.cc/images/no-images.jpg)
深入理解 Go 语言原子内存操作
原子内存操作提供了实现其他同步原语所需的低级基础。一般来说,你可以用互斥体和通道替换并发算法的所有原子操作。然而,它们是有趣且有时令人困惑的结构,应该深入了解它们是如何工作的。如果你能够谨慎地使用它们,那么它们完全可以成为代码优化的好工具,而不会增加复杂性…...
![](https://www.ngui.cc/images/no-images.jpg)
PostgreSQL几个扩展可以帮助实现数据的分词和快速查询
在 PostgreSQL 数据库中,有几个扩展可以帮助实现数据的分词和快速查询,特别是在处理全文搜索和文本分析时。以下是几个常用的扩展: 1. pg_trgm pg_trgm(Trigram)扩展是 PostgreSQL 中的一个强大的工具,它可以通过计算字符串之间的相似度来实现快速文本搜索。它支持基于…...
![](https://i-blog.csdnimg.cn/direct/38b6df093a684c9195b7442400ec187f.webp)
C盘满了怎么办?教你清理C盘的20个大招,值得收藏备用
C盘满了怎么办?教你清理C盘的20个大招,值得收藏备用 今天给大家介绍20种C盘清理的方法,下次遇到C盘满了红了就知道怎么做了,喜欢请点赞收藏关注点评。 清理更新缓存 清理微信缓存 查找大文件清理或者迁移 磁盘缓存清理 系统还…...
![](https://i-blog.csdnimg.cn/direct/820533631e7d4457ba288f35baeb327c.png)
原生js实现下滑到当前模块进度条填充
<div style"height: 1500px;"></div> <div class"progress-container"><div class"progress-bar" data-progress"90%"><p class"progress-text">Google Ads在Google搜索引擎上覆盖超过90%的互…...
![](https://i-blog.csdnimg.cn/direct/7dcfd158e2c74963a00457973e1f7eb5.png#pic_center)
显示弹出式窗口的方法
文章目录 1. 概念介绍2. 使用方法3. 示例代码 我们在上一章回中介绍了Sliver综合示例相关的内容,本章回中将介绍PopupMenuButton组件.闲话休提,让我们一起Talk Flutter吧。 1. 概念介绍 我们在本章回中介绍的PopupMenuButton组件位于AppBar右侧…...
![](https://www.ngui.cc/images/no-images.jpg)
Java-什么是缓存线程池?
什么是缓存线程池? 缓存线程池 (CachedThreadPool) 是一种特殊的线程池,它能够动态地调整线程的数量,以适应任 务的需求。这种线程池非常适合处理大量短暂的任务,因为它会根据任务的数量自动增加或减少线 程的数量。 缓存线程池的特点: 线程数量动态调整:缓存线程池…...
![](https://www.ngui.cc/images/no-images.jpg)
esbuild中的Binary Loader:处理二进制文件
在前端或Node.js项目中,有时需要处理二进制文件,如图片、音频、视频或其他非文本资源。esbuild提供了一款名为Binary Loader的插件,它能够在构建时将二进制文件加载为二进制缓冲区,并使用Base64编码将其嵌入到打包文件中。在运行时…...
![](https://img-blog.csdnimg.cn/img_convert/188d1d90a2ad90c740bc0ac26c534966.webp?x-oss-process=image/format,png)
深度好文:从《黑神话:悟空》看未来游戏趋势:高互动性、个性化与全球化
引言 在数字时代的浪潮中,游戏产业以其独特的魅力和无限的可能性,成为了全球娱乐文化的重要组成部分。随着科技的飞速发展,特别是高性能计算和人工智能技术的突破,游戏的世界变得越来越真实、细腻且富有深度。而在这股技术洪流中…...
![](https://i-blog.csdnimg.cn/direct/b0b5d7df7b3c4b0ab79cac11ea3d2405.png)
【中项第三版】系统集成项目管理工程师 | 第 12 章 执行过程组
前言 本章属于10大管理的内容,上午题预计会考8-10分,下午案例分析也会进行考查。学习要以教材为主。 目录 12.1 指导与管理项目工作 12.1.1 主要输入 12.1.2 主要输出 12.2 管理项目知识 12.2.1 主要输入 12.2.2 主要输出 12.3 管理质量 12.3.…...
![](https://www.ngui.cc/images/no-images.jpg)
C语言自动生成宏定义枚举类型和字符串
#include <stdio.h>// 定义错误枚举 #define ERROR_LIST(e) \e(SUCCESS) \e(FAILURE) \e(NOT_FOUND) \e(TIMEOUT)// 使用宏生成枚举 #define GENERATE_ENUM(ENUM) ENUM, typedef enum {ERROR_LIST(GENERATE_ENUM) } ErrorCode;// 使用宏生成字符串数组…...
![](https://www.ngui.cc/images/no-images.jpg)
C#单例模式
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks;namespace _3._3._6_单例模式 {public class Singleton{private static Singleton s_instance;private int _state;private Singleton(int …...
![](https://i-blog.csdnimg.cn/direct/7322d8321b2d475e8f11777bbf7a60e0.png)
10-使用sentinel流控
本文介绍sentinel的直接流控的使用。 0、环境 jdk 1.8sentinel 1.8.2springboot 2.4.2 1、sentinel环境搭建 从官方发布的网站上下载: sentinel Jar,下载对应版本。 下载完成后,进入刚才下载的Jar文件所在的目录,执行如下命令:…...
![](/images/no-images.jpg)
合肥电子商务开发网站建设/seo专员岗位要求
内容篇幅较长,请点击这里阅读全文...
![](https://img-blog.csdnimg.cn/2020042217384474.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L25ldHdvcmtodW50ZXI=,size_16,color_FFFFFF,t_70)
wordpress 最简单皮肤/哪个搜索引擎能搜敏感内容
问题起因 近日做的一个项目,我们提供jar包给其它开发方做开发,用户调用jar包里的一个功能,该功能执行后写了数据库。客户需要在该该功能执行完后能得到通知,这客户可以去数据库中取新的刷新后的数据。 什么是回调 java中一个类&…...
![](/images/no-images.jpg)
网站建设课程培训/企业网站运营推广
文章目录 前言I 第三方SDK分享文件1.1 微信SDK1.2 友盟SDK1.3 判断是否安装微信II 原生API的文件预览及其他应用打开2.1 预览文件2.2 文件分享2.3 控制是否显示copy、 print、saveToCameraRollIII 案例3.1 文件下载和预览3.2 使用数据模型保存下载文件路径3.3 使用数据模型分享…...
![](/images/no-images.jpg)
淄博企业网站建设有限公司/seo托管服务
一、关于Nginx的负载均衡 在服务器集群中,Nginx起到一个代理服务器的角色(即反向代理),为了避免单独一个服务器压力过大,将来自用户的请求转发给不同的服务器。 二、Nginx负载均衡策略 负载均衡用于从“upstream”模…...
昆明做网站建设的公司排名/30条新闻摘抄
欢迎关注”生信修炼手册”!ENCODE官方提供了chip_seq分析的pipeline以供参考,在peak calling前的预处理环节,流程示意如下可以看到其中包含了一个名为phantompeakqualtools的工具,这个工具可以进行cross-correlation分析,计算得到…...
![](/images/no-images.jpg)
建设银行征信中心网站/seo网站内容优化有哪些
修改密码:1.例如你的 root用户现在没有密码,你希望的密码修改为123456,那么命令是:mysqladmin -u root password 1234562.如果你的root现在有密码了(123456),那么修改密码为abcdef的命令是&…...