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

@xachary 2024-09-14 17:03:01 阅读 74

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

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

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

github源码

gitee源码

示例地址

接下来主要说说:

UIGraph(图形)canvas2svg 打补丁拐点旋转修复

UI - 图形绘制类型切换

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

在这里插入图片描述

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

在这里插入图片描述

定义图形类型:

<code>// 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 = 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

})

// 略

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: Render

group: Konva.Group

id: 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 = render

this.dropPoint = dropPoint

this.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

<code>// 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

})

break

case 'bottom':

shadow.position({

x: width / 2,

y: height

})

break

case 'left':

shadow.position({

x: 0,

y: height / 2

})

break

case 'right':

shadow.position({

x: width,

y: height / 2

})

break

case 'top-left':

shadow.position({

x: 0,

y: 0

})

break

case 'top-right':

shadow.position({

x: width,

y: 0

})

break

case 'bottom-left':

shadow.position({

x: 0,

y: height

})

break

case '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

})

break

case 'bottom':

shadow.position({

x: width / 2,

y: height

})

break

case 'left':

shadow.position({

x: 0,

y: height / 2

})

break

case 'right':

shadow.position({

x: width,

y: height / 2

})

break

case '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 + 5

const 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 } = 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

]

let anchorShadow: Konva.Circle | undefined, anchorShadowAcross: Konva.Circle | undefined

switch (anchorId) {

case 'top':

{

anchorShadow = graphSnap.findOne(`#top`)

anchorShadowAcross = graphSnap.findOne(`#bottom`)

}

break

case 'bottom':

{

anchorShadow = graphSnap.findOne(`#bottom`)

anchorShadowAcross = graphSnap.findOne(`#top`)

}

break

case 'left':

{

anchorShadow = graphSnap.findOne(`#left`)

anchorShadowAcross = graphSnap.findOne(`#right`)

}

break

case 'right':

{

anchorShadow = graphSnap.findOne(`#right`)

anchorShadowAcross = graphSnap.findOne(`#left`)

}

break

case 'top-left':

{

anchorShadow = graphSnap.findOne(`#top-left`)

anchorShadowAcross = graphSnap.findOne(`#bottom-right`)

}

break

case 'top-right':

{

anchorShadow = graphSnap.findOne(`#top-right`)

anchorShadowAcross = graphSnap.findOne(`#bottom-left`)

}

break

case 'bottom-left':

{

anchorShadow = graphSnap.findOne(`#bottom-left`)

anchorShadowAcross = graphSnap.findOne(`#top-right`)

}

break

case '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.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 = 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 = 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

// 略

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 = true

this.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 | undefined

constructor(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)

}

}

}

return null

}

// 调整 预处理、定位静态方法

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 = true

this.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 = false

this.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.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[]

const shapeRecords: { 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 {

throw new Error('Attempted to apply path command to node ' + this.__currentElement.nodeName)

}

}

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

拐点旋转修复

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

在这里插入图片描述

<code>// 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 / 2

const centerY = y + height / 2

const 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源码

示例地址



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。