经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 程序设计 » 编程经验 » 查看文章
前端使用 Konva 实现可视化设计器(12)- 连接线 - 直线
来源:cnblogs  作者:xachary  时间:2024/6/3 9:29:42  对本文有异议

这一章实现的连接线,目前仅支持直线连接,为了能够不影响原有的其它功能,尝试了2、3个实现思路,最终实测这个实现方式目前来说最为合适了。

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

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

github源码

gitee源码

示例地址

image

相关定义

  • 连接点
    image

记录了连接点相关信息,并不作为素材而存在,仅记录信息,即导出导入的时候,并不会出现所谓的连接点节点。

它存放在节点身上,因此导出、导入自然而然就可以持久化了。

src/Render/draws/LinkDraw.ts

  1. // 连接点
  2. export interface LinkDrawPoint {
  3. id: string
  4. groupId: string
  5. visible: boolean
  6. pairs: LinkDrawPair[]
  7. x: number
  8. y: number
  9. }
  • 连接对
    image

一个连接点,记录从该点出发的多条连接线信息,作为连接对信息存在。

src/Render/draws/LinkDraw.ts

  1. // 连接对
  2. export interface LinkDrawPair {
  3. id: string
  4. from: {
  5. groupId: string
  6. pointId: string
  7. }
  8. to: {
  9. groupId: string
  10. pointId: string
  11. }
  12. }
  • 连接点(锚点)
    image

它是拖入素材的时候生成的真实节点,归属于所在的节点中,存在却不可见,关键作用是同步连接点真实坐标,尤其是节点发生 transform 时候,必须依赖它获得 transform 后连接点变化。

src/Render/handlers/DragOutsideHandlers.ts

  1. // 略
  2. drop: (e: GlobalEventHandlersEventMap['drop']) => {
  3. // 略
  4. const points = [
  5. // 左
  6. { x: 0, y: group.height() / 2 },
  7. // 右
  8. {
  9. x: group.width(),
  10. y: group.height() / 2
  11. },
  12. // 上
  13. { x: group.width() / 2, y: 0 },
  14. // 下
  15. {
  16. x: group.width() / 2,
  17. y: group.height()
  18. }
  19. ]
  20. // 连接点信息
  21. group.setAttrs({
  22. points: points.map(
  23. (o) =>
  24. ({
  25. ...o,
  26. id: nanoid(),
  27. groupId: group.id(),
  28. visible: true,
  29. pairs: []
  30. }) as LinkDrawPoint
  31. )
  32. })
  33. // 连接点(锚点)
  34. for (const point of group.getAttr('points') ?? []) {
  35. group.add(
  36. new Konva.Circle({
  37. name: 'link-anchor',
  38. id: point.id,
  39. x: point.x,
  40. y: point.y,
  41. radius: this.render.toStageValue(1),
  42. stroke: 'rgba(0,0,255,1)',
  43. strokeWidth: this.render.toStageValue(2),
  44. visible: false
  45. })
  46. )
  47. }
  48. group.on('mouseenter', () => {
  49. // 显示 连接点
  50. this.render.linkTool.pointsVisible(true, group)
  51. })
  52. // hover 框(多选时才显示)
  53. group.add(
  54. new Konva.Rect({
  55. id: 'hoverRect',
  56. width: image.width(),
  57. height: image.height(),
  58. fill: 'rgba(0,255,0,0.3)',
  59. visible: false
  60. })
  61. )
  62. group.on('mouseleave', () => {
  63. // 隐藏 连接点
  64. this.render.linkTool.pointsVisible(false, group)
  65. // 隐藏 hover 框
  66. group.findOne('#hoverRect')?.visible(false)
  67. })
  68. // 略
  69. }
  70. // 略
  • 连接线
    image

根据连接点信息,绘制的线条,也不作为素材而存在,导出导入的时候,也不会出现所谓的连接点节点。不过,在导出图片、SVG和用于预览框的时候,会直接利用线条节点导出、显示。

src/Render/tools/ImportExportTool.ts

  1. // 略
  2. /**
  3. * 获得显示内容
  4. * @param withLink 是否包含线条
  5. * @returns
  6. */
  7. getView(withLink: boolean = false) {
  8. // 复制画布
  9. const copy = this.render.stage.clone()
  10. // 提取 main layer 备用
  11. const main = copy.find('#main')[0] as Konva.Layer
  12. const cover = copy.find('#cover')[0] as Konva.Layer
  13. // 暂时清空所有 layer
  14. copy.removeChildren()
  15. // 提取节点
  16. let nodes = main.getChildren((node) => {
  17. return !this.render.ignore(node)
  18. })
  19. if (withLink) {
  20. nodes = nodes.concat(
  21. cover.getChildren((node) => {
  22. return node.name() === Draws.LinkDraw.name
  23. })
  24. )
  25. }
  26. // 略
  27. }
  28. // 略

src/Render/draws/PreviewDraw.ts

  1. override draw() {
  2. // 略
  3. const main = this.render.stage.find('#main')[0] as Konva.Layer
  4. const cover = this.render.stage.find('#cover')[0] as Konva.Layer
  5. // 提取节点
  6. const nodes = [
  7. ...main.getChildren((node) => {
  8. return !this.render.ignore(node)
  9. }),
  10. // 补充连线
  11. ...cover.getChildren((node) => {
  12. return node.name() === Draws.LinkDraw.name
  13. })
  14. ]
  15. // 略
  16. }
  • 连接线(临时)
    image

起点鼠标按下 -> 拖动显示线条 -> 终点鼠标释放 -> 产生连接信息 LinkDrawPoint. LinkDrawPair

  1. // 连接线(临时)
  2. export interface LinkDrawState {
  3. linkingLine: {
  4. group: Konva.Group
  5. circle: Konva.Circle
  6. line: Konva.Line
  7. } | null
  8. }

代码文件

新增几个关键的代码文件:

src/Render/draws/LinkDraw.ts

根据 连接点.链接对 绘制 连接点、连接线,及其相关的事件处理

它的绘制顺序,应该放在绘制 比例尺、预览框之前。

src/Render/handlers/LinkHandlers.ts

根据 连接线(临时)信息,绘制/移除 连接线(临时)

src/Render/tools/LinkTool.ts

移除连接线,控制 连接点 的显示/隐藏

移除连接线,实际上就是移除其 连接对 信息

  1. // 略
  2. export class LinkTool {
  3. // 略
  4. pointsVisible(visible: boolean, group?: Konva.Group) {
  5. if (group) {
  6. this.pointsVisibleEach(visible, group)
  7. } else {
  8. const groups = this.render.layer.find('.asset') as Konva.Group[]
  9. for (const group of groups) {
  10. this.pointsVisibleEach(visible, group)
  11. }
  12. }
  13. // 更新连线
  14. this.render.draws[Draws.LinkDraw.name].draw()
  15. // 更新预览
  16. this.render.draws[Draws.PreviewDraw.name].draw()
  17. }
  18. remove(line: Konva.Line) {
  19. const { groupId, pointId, pairId } = line.getAttrs()
  20. if (groupId && pointId && pairId) {
  21. const group = this.render.layer.findOne(`#${groupId}`) as Konva.Group
  22. if (group) {
  23. const points = (group.getAttr('points') ?? []) as LinkDrawPoint[]
  24. const point = points.find((o) => o.id === pointId)
  25. if (point) {
  26. const pairIndex = (point.pairs ?? ([] as LinkDrawPair[])).findIndex(
  27. (o) => o.id === pairId
  28. )
  29. if (pairIndex > -1) {
  30. point.pairs.splice(pairIndex, 1)
  31. group.setAttr('points', points)
  32. // 更新连线
  33. this.render.draws[Draws.LinkDraw.name].draw()
  34. // 更新预览
  35. this.render.draws[Draws.PreviewDraw.name].draw()
  36. }
  37. }
  38. }
  39. }
  40. }
  41. }

关键逻辑

  • 绘制 连接线(临时)
    image

src/Render/draws/LinkDraw.ts

起点鼠标按下 'mousedown' -> 略 -> 终点鼠标释放 'mouseup'

  1. // 略
  2. export class LinkDraw extends Types.BaseDraw implements Types.Draw {
  3. // 略
  4. override draw() {
  5. this.clear()
  6. // stage 状态
  7. const stageState = this.render.getStageState()
  8. const groups = this.render.layer.find('.asset') as Konva.Group[]
  9. const points = groups.reduce((ps, group) => {
  10. return ps.concat(Array.isArray(group.getAttr('points')) ? group.getAttr('points') : [])
  11. }, [] as LinkDrawPoint[])
  12. const pairs = points.reduce((ps, point) => {
  13. return ps.concat(point.pairs ? point.pairs : [])
  14. }, [] as LinkDrawPair[])
  15. // 略
  16. // 连接点
  17. for (const point of points) {
  18. const group = groups.find((o) => o.id() === point.groupId)
  19. // 非 选择中
  20. if (group && !group.getAttr('selected')) {
  21. const anchor = this.render.layer.findOne(`#${point.id}`)
  22. if (anchor) {
  23. const circle = new Konva.Circle({
  24. id: point.id,
  25. groupId: group.id(),
  26. x: this.render.toStageValue(anchor.absolutePosition().x - stageState.x),
  27. y: this.render.toStageValue(anchor.absolutePosition().y - stageState.y),
  28. radius: this.render.toStageValue(this.option.size),
  29. stroke: 'rgba(255,0,0,0.2)',
  30. strokeWidth: this.render.toStageValue(1),
  31. name: 'link-point',
  32. opacity: point.visible ? 1 : 0
  33. })
  34. // 略
  35. circle.on('mousedown', () => {
  36. this.render.selectionTool.selectingClear()
  37. const pos = this.render.stage.getPointerPosition()
  38. if (pos) {
  39. // 临时 连接线 画
  40. this.state.linkingLine = {
  41. group: group,
  42. circle: circle,
  43. line: new Konva.Line({
  44. name: 'linking-line',
  45. points: _.flatten([
  46. [circle.x(), circle.y()],
  47. [
  48. this.render.toStageValue(pos.x - stageState.x),
  49. this.render.toStageValue(pos.y - stageState.y)
  50. ]
  51. ]),
  52. stroke: 'blue',
  53. strokeWidth: 1
  54. })
  55. }
  56. this.layer.add(this.state.linkingLine.line)
  57. }
  58. })
  59. // 略
  60. }
  61. }
  62. }
  63. }

src/Render/handlers/LinkHandlers.ts

拖动显示线条、移除 连接线(临时)

从起点到鼠标当前位置

  1. handlers = {
  2. stage: {
  3. mouseup: () => {
  4. const linkDrawState = (this.render.draws[Draws.LinkDraw.name] as Draws.LinkDraw).state
  5. // 临时 连接线 移除
  6. linkDrawState.linkingLine?.line.remove()
  7. linkDrawState.linkingLine = null
  8. },
  9. mousemove: () => {
  10. const linkDrawState = (this.render.draws[Draws.LinkDraw.name] as Draws.LinkDraw).state
  11. const pos = this.render.stage.getPointerPosition()
  12. if (pos) {
  13. // stage 状态
  14. const stageState = this.render.getStageState()
  15. // 临时 连接线 画
  16. if (linkDrawState.linkingLine) {
  17. const { circle, line } = linkDrawState.linkingLine
  18. line.points(
  19. _.flatten([
  20. [circle.x(), circle.y()],
  21. [
  22. this.render.toStageValue(pos.x - stageState.x),
  23. this.render.toStageValue(pos.y - stageState.y)
  24. ]
  25. ])
  26. )
  27. }
  28. }
  29. }
  30. }
  31. }
  • 产生连接信息

src/Render/draws/LinkDraw.ts

  1. // 略
  2. export class LinkDraw extends Types.BaseDraw implements Types.Draw {
  3. // 略
  4. override draw() {
  5. this.clear()
  6. // stage 状态
  7. const stageState = this.render.getStageState()
  8. const groups = this.render.layer.find('.asset') as Konva.Group[]
  9. const points = groups.reduce((ps, group) => {
  10. return ps.concat(Array.isArray(group.getAttr('points')) ? group.getAttr('points') : [])
  11. }, [] as LinkDrawPoint[])
  12. const pairs = points.reduce((ps, point) => {
  13. return ps.concat(point.pairs ? point.pairs : [])
  14. }, [] as LinkDrawPair[])
  15. // 略
  16. // 连接点
  17. for (const point of points) {
  18. const group = groups.find((o) => o.id() === point.groupId)
  19. // 非 选择中
  20. if (group && !group.getAttr('selected')) {
  21. const anchor = this.render.layer.findOne(`#${point.id}`)
  22. if (anchor) {
  23. const circle = new Konva.Circle({
  24. id: point.id,
  25. groupId: group.id(),
  26. x: this.render.toStageValue(anchor.absolutePosition().x - stageState.x),
  27. y: this.render.toStageValue(anchor.absolutePosition().y - stageState.y),
  28. radius: this.render.toStageValue(this.option.size),
  29. stroke: 'rgba(255,0,0,0.2)',
  30. strokeWidth: this.render.toStageValue(1),
  31. name: 'link-point',
  32. opacity: point.visible ? 1 : 0
  33. })
  34. // 略
  35. circle.on('mouseup', () => {
  36. if (this.state.linkingLine) {
  37. const line = this.state.linkingLine
  38. // 不同连接点
  39. if (line.circle.id() !== circle.id()) {
  40. const toGroup = groups.find((o) => o.id() === circle.getAttr('groupId'))
  41. if (toGroup) {
  42. const fromPoints = (
  43. Array.isArray(line.group.getAttr('points')) ? line.group.getAttr('points') : []
  44. ) as LinkDrawPoint[]
  45. const fromPoint = fromPoints.find((o) => o.id === line.circle.id())
  46. if (fromPoint) {
  47. const toPoints = (
  48. Array.isArray(toGroup.getAttr('points')) ? toGroup.getAttr('points') : []
  49. ) as LinkDrawPoint[]
  50. const toPoint = toPoints.find((o) => o.id === circle.id())
  51. if (toPoint) {
  52. if (Array.isArray(fromPoint.pairs)) {
  53. fromPoint.pairs = [
  54. ...fromPoint.pairs,
  55. {
  56. id: nanoid(),
  57. from: {
  58. groupId: line.group.id(),
  59. pointId: line.circle.id()
  60. },
  61. to: {
  62. groupId: circle.getAttr('groupId'),
  63. pointId: circle.id()
  64. }
  65. }
  66. ]
  67. }
  68. // 更新历史
  69. this.render.updateHistory()
  70. this.draw()
  71. // 更新预览
  72. this.render.draws[Draws.PreviewDraw.name].draw()
  73. }
  74. }
  75. }
  76. }
  77. // 临时 连接线 移除
  78. this.state.linkingLine?.line.remove()
  79. this.state.linkingLine = null
  80. }
  81. })
  82. this.group.add(circle)
  83. }
  84. // 略
  85. }
  86. }
  87. }
  88. }
  • 绘制 连接线
    image

src/Render/draws/LinkDraw.ts

这里就是利用了上面提到的 连接点(锚点),通过它的 absolutePosition 获得真实位置。

  1. // 略
  2. export class LinkDraw extends Types.BaseDraw implements Types.Draw {
  3. // 略
  4. override draw() {
  5. this.clear()
  6. // stage 状态
  7. const stageState = this.render.getStageState()
  8. const groups = this.render.layer.find('.asset') as Konva.Group[]
  9. const points = groups.reduce((ps, group) => {
  10. return ps.concat(Array.isArray(group.getAttr('points')) ? group.getAttr('points') : [])
  11. }, [] as LinkDrawPoint[])
  12. const pairs = points.reduce((ps, point) => {
  13. return ps.concat(point.pairs ? point.pairs : [])
  14. }, [] as LinkDrawPair[])
  15. // 连接线
  16. for (const pair of pairs) {
  17. const fromGroup = groups.find((o) => o.id() === pair.from.groupId)
  18. const fromPoint = points.find((o) => o.id === pair.from.pointId)
  19. const toGroup = groups.find((o) => o.id() === pair.to.groupId)
  20. const toPoint = points.find((o) => o.id === pair.to.pointId)
  21. if (fromGroup && toGroup && fromPoint && toPoint) {
  22. const fromAnchor = this.render.layer.findOne(`#${fromPoint.id}`)
  23. const toAnchor = this.render.layer.findOne(`#${toPoint.id}`)
  24. if (fromAnchor && toAnchor) {
  25. const line = new Konva.Line({
  26. name: 'link-line',
  27. // 用于删除连接线
  28. groupId: fromGroup.id(),
  29. pointId: fromPoint.id,
  30. pairId: pair.id,
  31. //
  32. points: _.flatten([
  33. [
  34. this.render.toStageValue(fromAnchor.absolutePosition().x - stageState.x),
  35. this.render.toStageValue(fromAnchor.absolutePosition().y - stageState.y)
  36. ],
  37. [
  38. this.render.toStageValue(toAnchor.absolutePosition().x - stageState.x),
  39. this.render.toStageValue(toAnchor.absolutePosition().y - stageState.y)
  40. ]
  41. ]),
  42. stroke: 'red',
  43. strokeWidth: 2
  44. })
  45. this.group.add(line)
  46. // 连接线 hover 效果
  47. line.on('mouseenter', () => {
  48. line.stroke('rgba(255,0,0,0.6)')
  49. document.body.style.cursor = 'pointer'
  50. })
  51. line.on('mouseleave', () => {
  52. line.stroke('red')
  53. document.body.style.cursor = 'default'
  54. })
  55. }
  56. }
  57. }
  58. // 略
  59. }
  60. }
  • 绘制 连接点

src/Render/draws/LinkDraw.ts

  1. // 略
  2. export class LinkDraw extends Types.BaseDraw implements Types.Draw {
  3. // 略
  4. override draw() {
  5. this.clear()
  6. // stage 状态
  7. const stageState = this.render.getStageState()
  8. const groups = this.render.layer.find('.asset') as Konva.Group[]
  9. const points = groups.reduce((ps, group) => {
  10. return ps.concat(Array.isArray(group.getAttr('points')) ? group.getAttr('points') : [])
  11. }, [] as LinkDrawPoint[])
  12. const pairs = points.reduce((ps, point) => {
  13. return ps.concat(point.pairs ? point.pairs : [])
  14. }, [] as LinkDrawPair[])
  15. // 略
  16. // 连接点
  17. for (const point of points) {
  18. const group = groups.find((o) => o.id() === point.groupId)
  19. // 非 选择中
  20. if (group && !group.getAttr('selected')) {
  21. const anchor = this.render.layer.findOne(`#${point.id}`)
  22. if (anchor) {
  23. const circle = new Konva.Circle({
  24. id: point.id,
  25. groupId: group.id(),
  26. x: this.render.toStageValue(anchor.absolutePosition().x - stageState.x),
  27. y: this.render.toStageValue(anchor.absolutePosition().y - stageState.y),
  28. radius: this.render.toStageValue(this.option.size),
  29. stroke: 'rgba(255,0,0,0.2)',
  30. strokeWidth: this.render.toStageValue(1),
  31. name: 'link-point',
  32. opacity: point.visible ? 1 : 0
  33. })
  34. // hover 效果
  35. circle.on('mouseenter', () => {
  36. circle.stroke('rgba(255,0,0,0.5)')
  37. circle.opacity(1)
  38. document.body.style.cursor = 'pointer'
  39. })
  40. circle.on('mouseleave', () => {
  41. circle.stroke('rgba(255,0,0,0.2)')
  42. circle.opacity(0)
  43. document.body.style.cursor = 'default'
  44. })
  45. // 略
  46. }
  47. }
  48. }
  49. }
  • 复制

有几个关键:

  1. 更新 id,包括:节点、连接点、锚点、连接对
  2. 重新绑定相关事件

src/Render/tools/CopyTool.ts

  1. // 略
  2. export class CopyTool {
  3. // 略
  4. /**
  5. * 复制粘贴
  6. * @param nodes 节点数组
  7. * @param skip 跳过检查
  8. * @returns 复制的元素
  9. */
  10. copy(nodes: Konva.Node[]) {
  11. const clones: Konva.Group[] = []
  12. for (const node of nodes) {
  13. if (node instanceof Konva.Transformer) {
  14. // 复制已选择
  15. const backup = [...this.render.selectionTool.selectingNodes]
  16. this.render.selectionTool.selectingClear()
  17. this.copy(backup)
  18. return
  19. } else {
  20. // 复制未选择(先记录,后处理)
  21. clones.push(node.clone())
  22. }
  23. }
  24. // 处理克隆节点
  25. // 新旧 id 映射
  26. const groupIdChanges: { [index: string]: string } = {}
  27. const pointIdChanges: { [index: string]: string } = {}
  28. // 新 id、新事件
  29. for (const copy of clones) {
  30. const gid = nanoid()
  31. groupIdChanges[copy.id()] = gid
  32. copy.id(gid)
  33. const pointsClone = _.cloneDeep(copy.getAttr('points') ?? [])
  34. copy.setAttr('points', pointsClone)
  35. for (const point of pointsClone) {
  36. const pid = nanoid()
  37. pointIdChanges[point.id] = pid
  38. const anchor = copy.findOne(`#${point.id}`)
  39. anchor?.id(pid)
  40. point.id = pid
  41. point.groupId = copy.id()
  42. point.visible = false
  43. }
  44. copy.off('mouseenter')
  45. copy.on('mouseenter', () => {
  46. // 显示 连接点
  47. this.render.linkTool.pointsVisible(true, copy)
  48. })
  49. copy.off('mouseleave')
  50. copy.on('mouseleave', () => {
  51. // 隐藏 连接点
  52. this.render.linkTool.pointsVisible(false, copy)
  53. // 隐藏 hover 框
  54. copy.findOne('#hoverRect')?.visible(false)
  55. })
  56. // 使新节点产生偏移
  57. copy.setAttrs({
  58. x: copy.x() + this.render.toStageValue(this.render.bgSize) * this.pasteCount,
  59. y: copy.y() + this.render.toStageValue(this.render.bgSize) * this.pasteCount
  60. })
  61. }
  62. // pairs 新 id
  63. for (const copy of clones) {
  64. const points = copy.getAttr('points') ?? []
  65. for (const point of points) {
  66. for (const pair of point.pairs) {
  67. // id 换新
  68. pair.id = nanoid()
  69. pair.from.groupId = groupIdChanges[pair.from.groupId]
  70. pair.from.pointId = pointIdChanges[pair.from.pointId]
  71. pair.to.groupId = groupIdChanges[pair.to.groupId]
  72. pair.to.pointId = pointIdChanges[pair.to.pointId]
  73. }
  74. }
  75. }
  76. // 略
  77. }
  78. }

接下来,计划实现下面这些功能:

  • 连接线 - 折线(头疼)
  • 等等。。。

More Stars please!勾勾手指~

源码

gitee源码

示例地址

原文链接:https://www.cnblogs.com/xachary/p/18226877

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号