首页 > 其他分享 >前端使用 Konva 实现可视化设计器(2)

前端使用 Konva 实现可视化设计器(2)

时间:2024-04-06 20:11:20浏览次数:390  
标签:src const render Konva stageState 可视化 toStageValue 前端 stage

作为继续创作的动力,继续求 github Star 能超过 50 个(目前惨淡的 0 个),望多多支持。
源码
示例地址

上一章,实现了“无限画布”、“画布移动”、“网格背景”、“比例尺”、“定位缩放”,并简单叙述了它们实现的基本思路。

image

关于位置和距离

从源码里可以发现,多处依赖了 Konva.Stage 的 width、height、x、y、scale。尤其是 scale,在绘制“网格背景”、“比例尺”中都必须利用它计算。

在这里需要清楚,在设计交互的时候要考虑一种是“逻辑上”的位置和距离,另一种是“真实的”位置和距离。

假设 stage 宽 800 高 600,可以说“逻辑上” stage 的尺寸就是 800 x 600,可是一旦进行了“缩放”,放大到 x2.0,“真实的” stage 的可视尺寸就变成了 1600 x 1200了。
然而,stage 包含的 layer、group 是相对于 stage 进行定义的,例如,存在一个 rect(x:0,y:0,width:100,height:200),当 stage 放大到 x2.0 的时候,“真实的”可视尺寸就变成了 200 x 400 了,但此时 rect 的(width,height)并没有改变。

因此,“逻辑上”和“真实的”的位置和距离之间就需要通过 scale 转换,简单地可以定义成:

  // 获取 stage 状态(这里获取的就是“真实的”位置和距离)
  getStageState() {
    return {
      width: this.stage.width(),
      height: this.stage.height(),
      scale: this.stage.scaleX(),
      x: this.stage.x(),
      y: this.stage.y()
    }
  }

  // 对于 stage 来说是保持 1:1 比例的,所以 scaleX 和 scaleY 是一样的
  
  // 相对大小(基于 stage,且无视 scale)
  toStageValue(boardPos: number) {
    return boardPos / this.stage.scaleX()
  }

  // 绝对大小(基于可视区域像素)
  toBoardValue(stagePos: number) {
    return stagePos * this.stage.scaleX()
  }

再举些代码里的例子:

      // src\Render\draws\BgDraw.ts
      // stage 状态(这里获取的就是“真实的”位置和距离)
      const stageState = this.render.getStageState()
      
      // 格子大小
      const cellSize = this.option.size

      // 列数
      const lenX = Math.ceil(this.render.toStageValue(stageState.width) / cellSize)
      // 行数
      const lenY = Math.ceil(this.render.toStageValue(stageState.height) / cellSize)

绘制网格的时候,基本就是针对可视区域绘制,所以通过“真实的” stageState.width 和 stageState.height,就需要根据 stage 的 scale 恢复成“逻辑上”的位置和距离,除以“逻辑上”网格大小,就可以得出应该要绘制多少行和列的线了。

又如:

      // src\Render\draws\RulerDraw.ts
      
      // stage 状态
      const stageState = this.render.getStageState()
      
      // 比例尺 - 上
      const groupTop = new Konva.Group({
        x: this.render.toStageValue(-stageState.x + this.option.size),
        y: this.render.toStageValue(-stageState.y),
        width: this.render.toStageValue(stageState.width - this.option.size),
        height: this.render.toStageValue(this.option.size)
      })
      
      // 比例尺 - 左
      const groupLeft = new Konva.Group({
        x: this.render.toStageValue(-stageState.x),
        y: this.render.toStageValue(-stageState.y + this.option.size),
        width: this.render.toStageValue(this.option.size),
        height: this.render.toStageValue(stageState.height - this.option.size)
      })

为了使“比例尺”一直贴在上边和左边,移动画布的时候,就要根据画布移动的偏移给“比例尺”定位,移动画布使通过鼠标移动的,属于“真实的”的位置和距离,同理需要进行转换。

在这里也许会绝对奇怪,this.option.size 就是“比例尺”的粗细,目前是 40,它看起来属于“逻辑上”的大小,为何还要经过 toStageValue 计算呢?因为视觉上“比例尺”的粗细是永远不变的,就需要反过来处理了。
例如,当 stage 放大到 x2.0 的时候,不处理之前,粗细 40 的“比例尺”就变成粗细 80了,视觉上粗细保持不变,这个时候就需要处于 2.0 缩放倍率,恢复成粗细 40。

实现一个坐标参考线

image

相比于“网格背景”、“比例尺”,更加简单:

// stage 状态
      const stageState = this.render.getStageState()

      const group = new Konva.Group()

      const pos = this.render.stage.getPointerPosition()
      if (pos) {
        if (pos.y >= this.option.padding) {
          // 横
          group.add(
            new Konva.Line({
              name: this.constructor.name,
              points: _.flatten([
                [
                  this.render.toStageValue(-stageState.x),
                  this.render.toStageValue(pos.y - stageState.y)
                ],
                [
                  this.render.toStageValue(stageState.width - stageState.x),
                  this.render.toStageValue(pos.y - stageState.y)
                ]
              ]),
              stroke: 'rgba(255,0,0,0.2)',
              strokeWidth: this.render.toStageValue(1),
              listening: false
            })
          )
        }

        if (pos.x >= this.option.padding) {
          // 竖
          group.add(
            new Konva.Line({
              name: this.constructor.name,
              points: _.flatten([
                [
                  this.render.toStageValue(pos.x - stageState.x),
                  this.render.toStageValue(-stageState.y)
                ],
                [
                  this.render.toStageValue(pos.x - stageState.x),
                  this.render.toStageValue(stageState.height - stageState.y)
                ]
              ]),
              stroke: 'rgba(255,0,0,0.2)',
              strokeWidth: this.render.toStageValue(1),
              listening: false
            })
          )
        }
      }
      this.group.add(group)

直接根据鼠标定位绘制横竖两条线即可,在鼠标 mousemove 和 mouseout 的时候重绘,特别地,option.padding 这里传入的就是“比例尺”的粗细,目的是把“参考线”限制在“比例尺”的范围内。

实现把素材从左侧面板拖入设计区域

素材面板的实现

image

首先把静态目录的素材 import 进来,获得其 url:

const assetsModules: Record<string, { default: string }> = import.meta.glob(
  ['./assets/*/*.{svg,png,jpg,gif}'],
  {
    eager: true
  }
)

const assetsInfos = computed(() => {
  return Object.keys(assetsModules).map((o) => ({
    url: assetsModules[o].default
  }))
})

接着简单的迭代展示在左边的区域:

    & > header {
      box-shadow: 1px 0 2px 0 rgba(0, 0, 0, 0.05);
      overflow: auto;
      & > ul {
        display: flex;
        flex-wrap: wrap;
        & > li {
          width: 33.33%;
          flex-shrink: 0;
          border: 1px solid #eee;
          cursor: move;
        }
      }
    }
      <header>
        <ul>
          <li
            v-for="(item, idx) of assetsInfos"
            :key="idx"
            draggable="true"
            @dragstart="onDragstart($event, item)"
          >
            <img :src="item.url" style="object-fit: contain; width: 100%; height: 100%" />
          </li>
        </ul>
      </header>

注意设置 draggable="true",后面需利用 dragstart 事件实现拖拽素材到设计区域。

// src\App.vue
function onDragstart(e: GlobalEventHandlersEventMap['dragstart'], item: Types.AssetInfo) {
  if (e.dataTransfer) {
    e.dataTransfer.setData('src', item.url)
    e.dataTransfer.setData('type', item.url.match(/([^./]+)\.([^./]+)$/)?.[2] ?? '')
  }
}

加载素材

设计区域通过 drop 事件获取素材的基本信息,用一个 group 包裹素材。加载素材后,得知素材的原始大小,根据素材大小,以鼠标坐标作为素材拖入的中心点:

      // src\Render\handlers\DragOutsideHandlers.ts
      drop: (e: GlobalEventHandlersEventMap['drop']) => {
        const src = e.dataTransfer?.getData('src')
        const type = e.dataTransfer?.getData('type')

        if (src && type) {
          // stage 状态
          const stageState = this.render.getStageState()

          this.render.stage.setPointersPositions(e)

          const pos = this.render.stage.getPointerPosition()
          if (pos) {
            this.render.assetTool[
              type === 'svg' ? `loadSvg` : type === 'gif' ? 'loadGif' : 'loadImg'
            ](src).then((image: Konva.Image) => {
              const group = new Konva.Group({
                id: nanoid(),
                width: image.width(),
                height: image.height()
              })

              this.render.layer.add(group)

              image.setAttrs({
                x: 0,
                y: 0
              })

              group.add(image)

              const x = this.render.toStageValue(pos.x - stageState.x) - group.width() / 2
              const y = this.render.toStageValue(pos.y - stageState.y) - group.height() / 2

              group.setAttrs({
                x,
                y
              })
            })
          }
        }
      }

目标是支持一般的图片、svg 矢量图、git 动图,加载一般的图片比较简单,直接用 Konva.Image 的 API:

  // 加载图片
  async loadImg(src: string) {
    return new Promise<Konva.Image>((resolve) => {
      Konva.Image.fromURL(src, (imageNode) => {
        imageNode.setAttrs({ src })
        resolve(imageNode)
      })
    })
  }

加载 svg 矢量图,相比一般的图片,记录了 svg XML 内容,为后续做数据恢复的时候,可以通过 json 数据,无损恢复 svg 矢量图。

  // 加载 svg
  async loadSvg(src: string) {
    const svgXML = await (await fetch(src)).text()
    const blob = new Blob([svgXML], { type: 'image/svg+xml' })
    const url = URL.createObjectURL(blob)

    return new Promise<Konva.Image>((resolve) => {
      Konva.Image.fromURL(url, (imageNode) => {
        imageNode.setAttrs({
          svgXML
        })
        resolve(imageNode)
      })
    })
  }

加载 gif 比较麻烦,需要第三方工具按帧绘制动图,可以参考 konva 官方示例,并记录 gif 原始路径。

  // 加载 gif
  async loadGif(src: string) {
    return new Promise<Konva.Image>((resolve) => {
      const canvas = document.createElement('canvas')

      gifler(src).frames(canvas, (ctx: CanvasRenderingContext2D, frame: any) => {
        canvas.width = frame.width
        canvas.height = frame.height
        ctx.drawImage(frame.buffer, 0, 0)

        this.render.layer.draw()

        resolve(
          new Konva.Image({
            image: canvas,
            gif: src
          })
        )
      })
    })
  }

至此,就实现了“把素材从左侧面板拖入设计区域”这个交互功能了。

标签:src,const,render,Konva,stageState,可视化,toStageValue,前端,stage
From: https://www.cnblogs.com/xachary/p/18117864

相关文章

  • python基于flask汽车4s店服务销售配件管理系统django+echart 数据可视化_od8kr
     该系统采用python技术,结合flask框架使页面更加完善,后台使用MySQL数据库进行数据存储。系统主要分为三大模块:即管理员模块,员工管理模块和用户模块。本文从汽车服务流程分析入手,分析了其功能性需求和非功能性需求,设计了一个由管理员,用户和员工三部分组成的汽车服务管理系统。用......
  • 【NS-3学习(三)】可视化NetAnim使用笔记
    NS-3可视化NetAnim使用笔记1,下载安装包:包的下载hgclonehttp://code.nsnam.org/jabraham3/netanim2,解压并安装:(1)解压(2)NetAnim是基于Qt图形库的,所以需要事先安装qtsudoapt-getinstallqt4-dev-tools(3)但是这样可能会出现问题:E:Unabletolocatepackageqt4-de......
  • 基于巴法云物联网云平台构建可视化控制网页(以控制LED为例)
    0前言如今大大小小的物联网云平台非常多,但大部分要收取费用,免费的物联网云平台功能则有很多限制使用起来非常不方便。以百度云物联网云平台为例,它的物可视不支持发布主题,等于可视化界面只能作为数据监控而不具备双向通信的能力。为了解决这个问题,本文使用免费的巴法云物......
  • 全网短剧搜索前端源码开源分享可改自己的接口
    全网短剧搜索前端源码内含7000+短剧资源(不支持在线播放)源码全开源,可以修改成自己的接口178、226、347行修改源码免费下载地址抄笔记(chaobiji.cn)https://chaobiji.cn/......
  • 前端开发和后端开发什么人更适合
    前端开发和后端开发都是软件开发领域的重要角色,每个人的适合程度取决于其个人兴趣、技能和职业目标。以下是一些因素可以帮助确定哪种开发更适合某个人:###适合前端开发的人:1.**创意和设计感**:喜欢设计和用户体验,对网页设计、交互设计有兴趣,擅长将设计概念转化为具体的用户......
  • Docker 安装 Linux 系统可视化监控 Netdata
    docker安装netdata前提准备Docker两种方式部署Netdata1、使用dockerrun命令运行netdata服务2、使用dockercompose运行netdata服务Netdata服务可视化界面Netdata汉化处理前提准备说明:此处使用windows11安装的dockerdesktop&wsl2/apline环境......
  • 62.html+css网页设计实例/“动漫”主题海贼王介绍/web前端期末大作业/
    一、前言  本实例以“动漫”海贼王为主题设计,div+css布局,页面代码简单,质量好,是个不错的学生网页设计作业源码。【关注作者|获取更多源码(2000+个Web案例源码)|优质文章】;您的支持是我创作的动力!【点赞收藏博文】,Web开发、课程设计、毕业设计有兴趣的联系我交流分享,3Q!二、......
  • HOW - 前端国际化之多语言通用方案
    目录一、国际化二、多语言支持1.i18n库或插件2.存在的问题2.1全量语言包影响打包体积2.2语言切换的流畅性2.3SEO问题三、多语言通用方案:不再需要硬编码、重新打包和部署才生效1.独立的文案文件2.语言包管理平台3.版本管理3.1问题3.2......
  • 前端系列-三次握手
     客户端和服务器端的交互简单过程:seq=xseq=yack=x+1seq=y+1 第一次握手(SYN)客户端(Client)向服务器(Server)发出一个带有SYN标志的数据段,其中包含一个随机序列号seq=x(x为随机生成的数字)。1Client->Server:SYN(seq=x)第二次握手(SYN+ACK)服务器接收到客户端的SYN数......
  • 前端系列-HTML5新特性
      HTML5引入了许多新特性和改进,其中包括但不限于:语义化标签:新增了像 <header>、<footer>、<nav>、<article>、<section> 和 <aside> 等元素,用于更好地表现文档结构。表单增强:添加了新的输入类型,如 type="email"、type="url"、type="date" 等,并支持 required、place......