本章分享一下如何使用 Konva 绘制基础图形:矩形、直线、折线,希望大家继续关注和支持哈!
请大家动动小手,给我一个免费的 Star 吧~
大家如果发现了 Bug,欢迎来提 Issue 哟~
github源码
gitee源码
示例地址
矩形
先上效果!
实现方式基本和《前端使用 Konva 实现可视化设计器(21)- 绘制图形(椭圆)》是一致的,主要区别矩形的大小和椭圆形的大小设置方式不一样,特别是矩形无需设置 offset。其它就不再赘述了哈。
直线、折线
先上效果!
简单描述一下上面的交互:
首先,绘制一条直线,淡出画一条直线还是比较简单的,根据记录鼠标按下的位置和鼠标释放的位置,就很容易得到 Konva.Line 的 points 应该设定的值了。
然后,沿用绘制 椭圆形、矩形 的思路,它只有特定的 2 个“调整点”,分别代表 起点 和 终点。
// src/Render/graphs/Line.ts// 略/*** 直线、折线*/
export class Line extends BaseGraph {// 略constructor(render: Types.Render, dropPoint: Konva.Vector2d) {super(render, dropPoint, {type: Types.GraphType.Line,// 定义了 2 个 调整点anchors: [{ adjustType: 'start' }, { adjustType: 'end' }].map((o) => ({adjustType: o.adjustType // 调整点 类型定义})),linkAnchors: [{ x: 0, y: 0, alias: 'start' },{ x: 0, y: 0, alias: 'end' }] as Types.AssetInfoPoint[]})// 新建 直线、折线this.line = new Konva.Line({name: 'graph',x: 0,y: 0,stroke: 'black',strokeWidth: 1,hitStrokeWidth: render.toStageValue(5)})// 给予 1 像素,防止导出图片 toDataURL 失败this.group.size({width: 1,height: 1})// 加入this.group.add(this.line)// 鼠标按下位置 作为起点this.group.position(this.dropPoint)}// 实现:拖动进行时override drawMove(point: Konva.Vector2d): void {// 鼠标拖动偏移量const offsetX = point.x - this.dropPoint.x,offsetY = point.y - this.dropPoint.y// 起点、终点const linkPoints = [[this.line.x(), this.line.y()],[this.line.x() + offsetX, this.line.y() + offsetY]]// 直线、折线 路径this.line.points(_.flatten(linkPoints))// 更新 图形 的 调整点 的 锚点位置Line.updateAnchorShadows(this.group, this.anchorShadows, this.line)// 更新 图形 的 连接点 的 锚点位置Line.updateLinkAnchorShadows(this.group, this.linkAnchorShadows, this.line)// 重绘this.render.redraw([Draws.GraphDraw.name, Draws.LinkDraw.name, Draws.PreviewDraw.name])}// 实现:拖动结束override drawEnd(): void {if (this.line.width() <= 1 && this.line.height() <= 1) {// 加入只点击,无拖动// 默认大小const width = Line.size,height = width// 起点、终点const linkPoints = [[this.line.x(), this.line.y()],[this.line.x() + width, this.line.y() + height]]// 直线、折线 位置大小this.line.points(_.flatten(linkPoints))}// 更新 调整点(拐点)Line.updateAnchor(this.render, this.group)// 更新 图形 的 调整点 的 锚点位置Line.updateAnchorShadows(this.group, this.anchorShadows, this.line)// 更新 图形 的 连接点 的 锚点位置Line.updateLinkAnchorShadows(this.group, this.linkAnchorShadows, this.line)// 对齐线清除this.render.attractTool.alignLinesClear()// 更新历史this.render.updateHistory()// 重绘this.render.redraw([Draws.GraphDraw.name, Draws.LinkDraw.name, Draws.PreviewDraw.name])}// 略
}
调整点,可以改变 直线、折线 的 起点、终点。
// 略/*** 直线、折线*/
export class Line extends BaseGraph {// 实现:更新 图形 的 调整点 的 锚点位置static override updateAnchorShadows(graph: Konva.Group,anchorShadows: Konva.Circle[],shape?: Konva.Line): void {if (shape) {const points = shape.points()//for (const shadow of anchorShadows) {switch (shadow.attrs.adjustType) {case 'start':shadow.position({x: points[0],y: points[1]})breakcase 'end':shadow.position({x: points[points.length - 2],y: points[points.length - 1]})break}}}}// 略// 实现:生成 调整点static override createAnchorShapes(render: Types.Render,graph: Konva.Group,anchorAndShadows: {anchor: Types.GraphAnchoranchorShadow: Konva.Circleshape?: Konva.Shape}[],adjustAnchor?: Types.GraphAnchor): {anchorAndShadows: {anchor: Types.GraphAnchoranchorShadow: Konva.Circleshape?: Konva.Shape | undefined}[]} {// stage 状态const stageState = render.getStageState()const graphShape = graph.findOne('.graph') as Konva.Lineif (graphShape) {const points = graphShape.points()for (const anchorAndShadow of anchorAndShadows) {let rotate = 0const { anchor, anchorShadow } = anchorAndShadowconst x = render.toStageValue(anchorShadow.getAbsolutePosition().x - stageState.x),y = render.toStageValue(anchorShadow.getAbsolutePosition().y - stageState.y)if (anchor.adjustType === 'manual') {// 略} else {if (anchor.adjustType === 'start') {rotate = Line.calculateAngle(points[2] - points[0], points[3] - points[1])} else if (anchor.adjustType === 'end') {rotate = Line.calculateAngle(points[points.length - 2] - points[points.length - 4],points[points.length - 1] - points[points.length - 3])}const cos = Math.cos((rotate * Math.PI) / 180)const sin = Math.sin((rotate * Math.PI) / 180)const offset = render.toStageValue(render.pointSize + 5)const offsetX = offset * sinconst offsetY = offset * cosconst anchorShape = new Konva.Circle({name: 'anchor',anchor: anchor,//fill:adjustAnchor?.adjustType === anchor.adjustType && adjustAnchor?.groupId === graph.id()? 'rgba(0,0,255,0.8)': 'rgba(0,0,255,0.2)',radius: render.toStageValue(3),strokeWidth: 0,// 位置x: x,y: y,offsetX:anchor.adjustType === 'start' ? offsetX : anchor.adjustType === 'end' ? -offsetX : 0,offsetY:anchor.adjustType === 'start' ? offsetY : anchor.adjustType === 'end' ? -offsetY : 0,// 旋转角度rotation: graph.getAbsoluteRotation()})anchorShape.on('mouseenter', () => {anchorShape.fill('rgba(0,0,255,0.8)')document.body.style.cursor = 'move'})anchorShape.on('mouseleave', () => {anchorShape.fill(anchorShape.attrs.adjusting ? 'rgba(0,0,255,0.8)' : 'rgba(0,0,255,0.2)')document.body.style.cursor = anchorShape.attrs.adjusting ? 'move' : 'default'})anchorAndShadow.shape = anchorShape}}}return { anchorAndShadows }}// 略// 实现:调整 图形static override adjust(render: Types.Render,graph: Konva.Group,graphSnap: Konva.Group,adjustShape: Konva.Shape,anchorAndShadows: {anchor: Types.GraphAnchoranchorShadow: Konva.Circleshape?: Konva.Shape | undefined}[],startPoint: Konva.Vector2d,endPoint: Konva.Vector2d) {// 目标 直线、折线const line = graph.findOne('.graph') as Konva.Line// 镜像const lineSnap = graphSnap.findOne('.graph') as Konva.Line// 调整点 锚点const anchors = (graph.find('.anchor') ?? []) as Konva.Circle[]// 镜像const anchorsSnap = (graphSnap.find('.anchor') ?? []) as Konva.Circle[]// 连接点 锚点const linkAnchors = (graph.find('.link-anchor') ?? []) as Konva.Circle[]if (line && lineSnap) {// stage 状态const stageState = render.getStageState(){const [graphRotation, adjustType, ex, ey] = [Math.round(graph.rotation()),adjustShape.attrs.anchor?.adjustType,endPoint.x,endPoint.y]const { x: cx, y: cy, width: cw, height: ch } = graphSnap.getClientRect()const { x, y } = graph.position()const [centerX, centerY] = [cx + cw / 2, cy + ch / 2]const { x: sx, y: sy } = Line.rotatePoint(ex, ey, centerX, centerY, -graphRotation)const { x: rx, y: ry } = Line.rotatePoint(x, y, centerX, centerY, -graphRotation)const points = line.points()const manualPoints = (line.attrs.manualPoints ?? []) as Types.LineManualPoint[]if (adjustType === 'manual') {// 略} else {const anchor = anchors.find((o) => o.attrs.adjustType === adjustType)const anchorShadow = anchorsSnap.find((o) => o.attrs.adjustType === adjustType)if (anchor && anchorShadow) {{const linkPoints = [[points[0], points[1]],...manualPoints.sort((a, b) => a.index - b.index).map((o) => [o.x, o.y]),[points[points.length - 2], points[points.length - 1]]]switch (adjustType) {case 'start':{linkPoints[0] = [sx - rx, sy - ry]line.points(_.flatten(linkPoints))}breakcase 'end':{linkPoints[linkPoints.length - 1] = [sx - rx, sy - ry]line.points(_.flatten(linkPoints))}break}}}}}// 更新 调整点(拐点)Line.updateAnchor(render, graph)// 更新 调整点 的 锚点 位置Line.updateAnchorShadows(graph, anchors, line)// 更新 图形 的 连接点 的 锚点位置Line.updateLinkAnchorShadows(graph, linkAnchors, line)// 更新 调整点 位置for (const anchor of anchors) {for (const { shape } of anchorAndShadows) {if (shape) {if (shape.attrs.anchor?.adjustType === anchor.attrs.adjustType) {const anchorShadow = graph.find(`.anchor`).find((o) => o.attrs.adjustType === anchor.attrs.adjustType)if (anchorShadow) {shape.position({x: render.toStageValue(anchorShadow.getAbsolutePosition().x - stageState.x),y: render.toStageValue(anchorShadow.getAbsolutePosition().y - stageState.y)})shape.rotation(graph.getAbsoluteRotation())}}}}}// 重绘render.redraw([Draws.GraphDraw.name, Draws.LinkDraw.name, Draws.PreviewDraw.name])}}// 略
}
折线
相比绘制 椭圆形、矩形 比较不一样的地方在于,椭圆形、矩形 的“调整点”是固定的,而绘制 折线 不一样,没调整一个新的拐点,就会新增 2 个新调整点,整体交互与 手动连接线 类似。
// src/Render/draws/GraphDraw.ts// 略export interface GraphDrawState {// 略/*** 调整中 调整点*/adjustAnchor?: Types.GraphAnchor/*** 鼠标按下 调整点 位置*/startPointCurrent: Konva.Vector2d/*** 图形 group*/graphCurrent?: Konva.Group/*** 图形 group 镜像,用于计算位置、大小的偏移*/graphCurrentSnap?: Konva.Group
}// 略export class GraphDraw extends Types.BaseDraw implements Types.Draw {// 略state: GraphDrawState = {adjusting: false,adjustGroupId: '',startPointCurrent: { x: 0, y: 0 }}// 略override draw() {this.clear()// 所有图形const graphs = this.render.layer.find('.asset').filter((o) => o.attrs.assetType === Types.AssetType.Graph) as Konva.Group[]for (const graph of graphs) {// 非选中状态才显示 调整点if (!graph.attrs.selected) {// 略for (const anchorAndShadow of anchorAndShadows) {const { shape } = anchorAndShadowif (shape) {// 鼠标按下shape.on('mousedown', () => {const pos = this.getStagePoint()if (pos) {this.state.adjusting = truethis.state.adjustAnchor = shape.attrs.anchorthis.state.adjustGroupId = graph.id()this.state.startPointCurrent = posthis.state.graphCurrent = graphthis.state.graphCurrentSnap = graph.clone()shape.setAttr('adjusting', true)if (this.state.adjustAnchor) {switch (shape.attrs.anchor?.type) {case Types.GraphType.Line:// 使用 直线、折线 静态处理方法Graphs.Line.adjustStart(this.render, graph, this.state.adjustAnchor, pos)break}}}})// 略// 调整结束this.render.stage.on('mouseup', () => {// 略this.state.adjusting = falsethis.state.adjustAnchor = undefinedthis.state.adjustGroupId = ''// 恢复显示所有 调整点for (const { shape } of anchorAndShadows) {if (shape) {shape.opacity(1)shape.setAttr('adjusting', false)if (shape.attrs.anchor?.type === Types.GraphType.Line) {if (shape.attrs.anchor.adjusted) {shape.fill('rgba(0,0,0,0.4)')} else {shape.fill('rgba(0,0,255,0.2)')}} else {shape.stroke('rgba(0,0,255,0.2)')}}// 略}// 略})// 略}}}}}
}
上面除了需要更多的状态记录 调整 信息,还需要定义 Line 特有的 adjustStart 方法:
// src/Render/graphs/Line.ts// 略/*** 直线、折线*/
export class Line extends BaseGraph {// 略/*** 调整之前*/static adjustStart(render: Types.Render,graph: Konva.Group,adjustAnchor: Types.GraphAnchor & { manualIndex?: number; adjusted?: boolean },endPoint: Konva.Vector2d) {const { x: gx, y: gy } = graph.position()const shape = graph.findOne('.graph') as Konva.Lineif (shape && typeof adjustAnchor.manualIndex === 'number') {const manualPoints = (shape.attrs.manualPoints ?? []) as Types.LineManualPoint[]if (adjustAnchor.adjusted) {//} else {manualPoints.push({x: endPoint.x - gx,y: endPoint.y - gy,index: adjustAnchor.manualIndex})shape.setAttr('manualPoints', manualPoints)}// 更新 调整点(拐点)Line.updateAnchor(render, graph)}}
}// 略
动态的调整点,会记录在 line 的 attrs 中 manualPoints,每次首次调整一处 拐点,就会新增一个 新 拐点,主要应用在:
// 略/*** 直线、折线*/
export class Line extends BaseGraph {// 略// 实现:调整 图形static override adjust(render: Types.Render,graph: Konva.Group,graphSnap: Konva.Group,adjustShape: Konva.Shape,anchorAndShadows: {anchor: Types.GraphAnchoranchorShadow: Konva.Circleshape?: Konva.Shape | undefined}[],startPoint: Konva.Vector2d,endPoint: Konva.Vector2d) {// 目标 直线、折线const line = graph.findOne('.graph') as Konva.Line// 镜像const lineSnap = graphSnap.findOne('.graph') as Konva.Line// 调整点 锚点const anchors = (graph.find('.anchor') ?? []) as Konva.Circle[]// 镜像const anchorsSnap = (graphSnap.find('.anchor') ?? []) as Konva.Circle[]// 连接点 锚点const linkAnchors = (graph.find('.link-anchor') ?? []) as Konva.Circle[]if (line && lineSnap) {// stage 状态const stageState = render.getStageState(){const [graphRotation, adjustType, ex, ey] = [Math.round(graph.rotation()),adjustShape.attrs.anchor?.adjustType,endPoint.x,endPoint.y]const { x: cx, y: cy, width: cw, height: ch } = graphSnap.getClientRect()const { x, y } = graph.position()const [centerX, centerY] = [cx + cw / 2, cy + ch / 2]const { x: sx, y: sy } = Line.rotatePoint(ex, ey, centerX, centerY, -graphRotation)const { x: rx, y: ry } = Line.rotatePoint(x, y, centerX, centerY, -graphRotation)const points = line.points()const manualPoints = (line.attrs.manualPoints ?? []) as Types.LineManualPoint[]if (adjustType === 'manual') {if (adjustShape.attrs.anchor?.manualIndex !== void 0) {const index = adjustShape.attrs.anchor?.adjusted? adjustShape.attrs.anchor?.manualIndex: adjustShape.attrs.anchor?.manualIndex + 1const manualPointIndex = manualPoints.findIndex((o) => o.index === index)if (manualPointIndex > -1) {manualPoints[manualPointIndex].x = sx - rxmanualPoints[manualPointIndex].y = sy - ry}const linkPoints = [[points[0], points[1]],...manualPoints.sort((a, b) => a.index - b.index).map((o) => [o.x, o.y]),[points[points.length - 2], points[points.length - 1]]]line.setAttr('manualPoints', manualPoints)line.points(_.flatten(linkPoints))//const adjustAnchorShadow = anchors.find((o) => o.attrs.adjustType === 'manual' && o.attrs.manualIndex === index)if (adjustAnchorShadow) {adjustAnchorShadow.position({x: sx - rx,y: sy - ry})}}} else {// 略}}// 略}}// 略/*** 更新 调整点(拐点)* @param render* @param graph*/static updateAnchor(render: Types.Render, graph: Konva.Group) {const anchors = graph.attrs.anchors ?? []const anchorShadows = graph.find('.anchor') ?? []const shape = graph.findOne('.graph') as Konva.Lineif (shape) {// 已拐let manualPoints = (shape.attrs.manualPoints ?? []) as Types.LineManualPoint[]const points = shape.points()// 调整点 + 拐点const linkPoints = [[points[0], points[1]],...manualPoints.sort((a, b) => a.index - b.index).map((o) => [o.x, o.y]),[points[points.length - 2], points[points.length - 1]]]// 清空 调整点(拐点),保留 start endanchors.splice(2)const shadows = anchorShadows.splice(2)for (const shadow of shadows) {shadow.remove()shadow.destroy()}manualPoints = []for (let i = linkPoints.length - 1; i > 0; i--) {linkPoints.splice(i, 0, [])}// 调整点(拐点)for (let i = 1; i < linkPoints.length - 1; i++) {const anchor = {type: graph.attrs.graphType,adjustType: 'manual',//name: 'anchor',groupId: graph.id(),//manualIndex: i,adjusted: false}if (linkPoints[i].length === 0) {anchor.adjusted = false// 新增const prev = linkPoints[i - 1]const next = linkPoints[i + 1]const circle = new Konva.Circle({adjustType: anchor.adjustType,anchorType: anchor.type,name: anchor.name,manualIndex: anchor.manualIndex,radius: 0,// radius: render.toStageValue(2),// fill: 'red',//x: (prev[0] + next[0]) / 2,y: (prev[1] + next[1]) / 2,anchor})graph.add(circle)} else {anchor.adjusted = true// 已拐const circle = new Konva.Circle({adjustType: anchor.adjustType,anchorType: anchor.type,name: anchor.name,manualIndex: anchor.manualIndex,adjusted: true,radius: 0,// radius: render.toStageValue(2),// fill: 'red',//x: linkPoints[i][0],y: linkPoints[i][1],anchor})graph.add(circle)manualPoints.push({x: linkPoints[i][0],y: linkPoints[i][1],index: anchor.manualIndex})}anchors.push(anchor)}shape.setAttr('manualPoints', manualPoints)graph.setAttr('anchors', anchors)}}// 略
}
上面简单的说,就是处理 manualPoints 的算法,负责控制新增拐点,然后把“点”们插入到 起点、终点 之间,最后处理成 Konva.Line 的 points 的值。
顺带一说。区分 起点、终点 和 拐点 是通过 attrs 中的 adjustType 字段;区分 拐点 是否已经操作过 是通过 attrs 中的 adjusted 字段;拐点是存在明确的顺序的,会记录在 attrs 的 manualIndex 字段中。
个人觉得,目前,绘制图形的 代码结构 和 变量命名 容易产生歧义,后面尽量抽出时间重构一下,大家支持支持 👇!
Thanks watching~
More Stars please!勾勾手指~
源码
gitee源码
示例地址