0


前端使用 Konva 实现可视化设计器(2)- 参考线、svg、gif图片加载

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

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

在这里插入图片描述

关于位置和距离

从源码里可以发现,多处依赖了 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 =newKonva.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 =newKonva.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。

实现一个坐标参考线

在这里插入图片描述
相比于“网格背景”、“比例尺”,更加简单:

// stage 状态const stageState =this.render.getStageState()const group =newKonva.Group()const pos =this.render.stage.getPointerPosition()if(pos){if(pos.y >=this.option.padding){// 横
          group.add(newKonva.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(newKonva.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 这里传入的就是“比例尺”的粗细,目的是把“参考线”限制在“比例尺”的范围内。

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

素材面板的实现

在这里插入图片描述
首先把静态目录的素材 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><liv-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.vuefunctiononDragstart(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 =newKonva.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()/2const y =this.render.toStageValue(pos.y - stageState.y)- group.height()/2

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

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

// 加载图片asyncloadImg(src:string){returnnewPromise<Konva.Image>((resolve)=>{
      Konva.Image.fromURL(src,(imageNode)=>{
        imageNode.setAttrs({ src })resolve(imageNode)})})}

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

// 加载 svgasyncloadSvg(src:string){const svgXML =await(awaitfetch(src)).text()const blob =newBlob([svgXML],{ type:'image/svg+xml'})const url =URL.createObjectURL(blob)returnnewPromise<Konva.Image>((resolve)=>{
      Konva.Image.fromURL(url,(imageNode)=>{
        imageNode.setAttrs({
          svgXML
        })resolve(imageNode)})})}

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

// 加载 gifasyncloadGif(src:string){returnnewPromise<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(newKonva.Image({
            image: canvas,
            gif: src
          }))})})}

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

接下来,计划增加下面功能:

  • 鼠标、键盘移动节点
  • 鼠标、键盘单选、多选节点
  • 单个、多个节点拖动时的磁贴交互
  • 键盘复制、粘贴
  • 节点层次单个、批量调整
  • 等等。。。

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


本文转载自: https://blog.csdn.net/xachary2/article/details/137427290
版权归原作者 @xachary 所有, 如有侵权,请联系我们删除。

“前端使用 Konva 实现可视化设计器(2)- 参考线、svg、gif图片加载”的评论:

还没有评论