作为继续创作的动力,继续求 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 = 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。

实现一个坐标参考线

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

// 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 这里传入的就是“比例尺”的粗细,目的是把“参考线”限制在“比例尺”的范围内。

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

素材面板的实现

首先把静态目录的素材 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
})
)
})
})
}

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

前端使用 Konva 实现可视化设计器(2)的更多相关文章

  1. 惊闻企业Web应用生成平台 活字格 V4.0 免费了,不单可视化设计器免费,服务器也免费!

    官网消息: 针对活字格开发者,新版本完全免费!您可下载活字格 Web 应用生成平台 V4.0 Updated 1,方便的创建各类 Web 应用系统,任意部署,永不过期. 我之前学习过活字格,也曾经向用 ...

  2. (原创)【B4A】一步一步入门02:可视化界面设计器、控件的使用

    一.前言 上篇 (原创)[B4A]一步一步入门01:简介.开发环境搭建.HelloWorld 中我们创建了默认的项目,现在我们来看一下B4A项目的构成,以及如何所见即所得的设计界面,并添加和使用自带的 ...

  3. Windows Phone 十二、设计器同步

    在设计阶段为页面添加数据源 Blend或者VS的可视化设计器会跑我们的代码,然后来显示出来,当我们Build之后,设计器会进入页面的构造函数,调用InitializeComponent();方法来将U ...

  4. WinForms项目升级.Net Core 3.0之后,没有WinForm设计器?

    目录 .NET Conf 2019 Window Forms 设计器 .NET Conf 2019 2019 9.23-9.25召开了 .NET Conf 2019 大会,大会宣布了 .Net Cor ...

  5. ActiveReports 9 新功能:可视化查询设计器(VQD)介绍

    在最新发布的ActiveReports 9报表控件中添加了多项新功能,以帮助你在更短的时间里创建外观绚丽.功能强大的报表系统,本文将重点介绍可视化数据查询设计器,无需手动编写任何SQL语句,主要内容如 ...

  6. VS2015 android 设计器不能可视化问题解决。

    近期安装了VS2015,体验了一下android 的开发,按模板创建执行了个,试下效果非常不错.也能够可视化设计.但昨天再次打开或创建一个android程序后,设计界面直接不能显示,显示错误:(可能是 ...

  7. 可视化流程设计——流程设计器演示(基于Silverlight)

    上一篇文章<通用流程设计>对鄙人写的通用流程做了一定的介绍,并奉上了相关源码.但一个好的流程设计必少不了流程设计器的支持,本文将针对<通用流程设计>中的流程的设计器做一个简单的 ...

  8. Type Script 在流程设计器的落地实践

    流程设计器项目介绍 从事过BPM行业的大佬必然对流程建模工具非常熟悉,做为WFMC三大体系结构模型中的核心模块,它是工作流的能力模型,其他模块都围绕工作流定义来构建. 成熟的建模工具通过可视化的操作界 ...

  9. .net erp(办公oa)开发平台架构概要说明之表单设计器

    背景:搭建一个适合公司erp业务的开发平台.   架构概要图: 表单设计开发部署示例图    表单设计开发部署示例说明1)每个开发人员可以自己部署表单设计至本地一份(当然也可以共用一套开发环境,但是如 ...

  10. 解析大型.NET ERP系统核心组件 查询设计器 报表设计器 窗体设计器 工作流设计器 任务计划设计器

    企业管理软件包含一些公共的组件,这些基础的组件在每个新项目立项阶段就必须考虑.核心的稳定不变功能,方便系统开发与维护,也为系统二次开发提供了诸多便利.比如通用权限管理系统,通用附件管理,通用查询等组件 ...

随机推荐

  1. 硬件开发笔记(十五):RK3568底板电路VGA显示接口原理图分析

    前言   前面输出了HDMI,LVDS,MIPI-DSI,这里还有一个常用的显示接口就是VGA了,这个用的不多了,一般板子都是hdmi了.  本篇分析底板VGA电路.   VGA接口   VGA(Vi ...

  2. C#程序全局异常处理—WPF和Web API两种模式

    C#程序的全局异常处理,网上搜下资料都是一大堆,我这里最近也是独立做一个B/S结构的小项目, 后面又增加了需求用WPF实现相同的功能,这里将我所使用的全局异常处理方式做一个简短的总结分享. Web A ...

  3. chrome浏览器配置自定义搜索引擎

    chrome谷歌浏览器配置自定义搜索引擎 放弃百度搜索已经酝酿许久,现在搜索结果简直不忍直视.如果你想放弃使用百度搜索,并转向其他搜索引擎,头条搜索可能是一个不错的选择. 使用以下方式可以丝滑的使用其 ...

  4. 【Azure 应用服务】azure function powershell 调用 New-AzADServicePrincipal -DisplayName $sp -PasswordCredential $spCred

    问题描述 powershell 调用New-AzADServicePrincipal -DisplayName $sp -PasswordCredential $spCred,出现如下错误: Reso ...

  5. 自定义ConditionalOnXX注解(二)

    一.前言 在之前的文章<自定义ConditionalOnXX注解>中,介绍了Conditional注解的实现原理和实现自定义Conditional注解的基础方法,但是有些场景我们需要用一个 ...

  6. STL-list模拟实现

    #pragma once #include"16my_Itetator.h" //测试用 #include<iostream> //测试用 using std::cou ...

  7. 分组聚合不再难:Pandas groupby使用指南

    处理大量数据时,经常需要对数据进行分组和汇总,groupby为我们提供了一种简洁.高效的方式来实现这些操作,从而简化了数据分析的流程. 1. 分组聚合是什么 分组是指根据一个或多个列的值将数据分成多个 ...

  8. Docker安装好后服务启动不了

    安装 安装方式参考地址:https://www.docker.org.cn/book/install/install-docker-on-rhel-29.html 问题 安装好后启动不了服务器.查看状 ...

  9. 适合新手练习的python开源经典源码

    一 前记 python学习离不开三要素: A.基础知识要熟悉 B. 练习源码要够量 C.实战项目要跟上 二 链接 书本的基础就看几本书就可以了,这里给出源码练习的经典开源源码: 该链接集合了pytho ...

  10. 一个简单的百万并发的TCP服务器的实现。

    我们紧接着上篇文章,看看我们上节课的代码有什么问题? 可以明显的看出来上节课的代码公用了一个同样的缓冲区进行读写,正常的情况下我们需要封装一个结构体,让每个对应的客户端的FD都有独立的结构进行读写还有 ...