前端使用 Konva 实现可视化设计器(2)
作为继续创作的动力,继续求 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)的更多相关文章
- 惊闻企业Web应用生成平台 活字格 V4.0 免费了,不单可视化设计器免费,服务器也免费!
官网消息: 针对活字格开发者,新版本完全免费!您可下载活字格 Web 应用生成平台 V4.0 Updated 1,方便的创建各类 Web 应用系统,任意部署,永不过期. 我之前学习过活字格,也曾经向用 ...
- (原创)【B4A】一步一步入门02:可视化界面设计器、控件的使用
一.前言 上篇 (原创)[B4A]一步一步入门01:简介.开发环境搭建.HelloWorld 中我们创建了默认的项目,现在我们来看一下B4A项目的构成,以及如何所见即所得的设计界面,并添加和使用自带的 ...
- Windows Phone 十二、设计器同步
在设计阶段为页面添加数据源 Blend或者VS的可视化设计器会跑我们的代码,然后来显示出来,当我们Build之后,设计器会进入页面的构造函数,调用InitializeComponent();方法来将U ...
- WinForms项目升级.Net Core 3.0之后,没有WinForm设计器?
目录 .NET Conf 2019 Window Forms 设计器 .NET Conf 2019 2019 9.23-9.25召开了 .NET Conf 2019 大会,大会宣布了 .Net Cor ...
- ActiveReports 9 新功能:可视化查询设计器(VQD)介绍
在最新发布的ActiveReports 9报表控件中添加了多项新功能,以帮助你在更短的时间里创建外观绚丽.功能强大的报表系统,本文将重点介绍可视化数据查询设计器,无需手动编写任何SQL语句,主要内容如 ...
- VS2015 android 设计器不能可视化问题解决。
近期安装了VS2015,体验了一下android 的开发,按模板创建执行了个,试下效果非常不错.也能够可视化设计.但昨天再次打开或创建一个android程序后,设计界面直接不能显示,显示错误:(可能是 ...
- 可视化流程设计——流程设计器演示(基于Silverlight)
上一篇文章<通用流程设计>对鄙人写的通用流程做了一定的介绍,并奉上了相关源码.但一个好的流程设计必少不了流程设计器的支持,本文将针对<通用流程设计>中的流程的设计器做一个简单的 ...
- Type Script 在流程设计器的落地实践
流程设计器项目介绍 从事过BPM行业的大佬必然对流程建模工具非常熟悉,做为WFMC三大体系结构模型中的核心模块,它是工作流的能力模型,其他模块都围绕工作流定义来构建. 成熟的建模工具通过可视化的操作界 ...
- .net erp(办公oa)开发平台架构概要说明之表单设计器
背景:搭建一个适合公司erp业务的开发平台. 架构概要图: 表单设计开发部署示例图 表单设计开发部署示例说明1)每个开发人员可以自己部署表单设计至本地一份(当然也可以共用一套开发环境,但是如 ...
- 解析大型.NET ERP系统核心组件 查询设计器 报表设计器 窗体设计器 工作流设计器 任务计划设计器
企业管理软件包含一些公共的组件,这些基础的组件在每个新项目立项阶段就必须考虑.核心的稳定不变功能,方便系统开发与维护,也为系统二次开发提供了诸多便利.比如通用权限管理系统,通用附件管理,通用查询等组件 ...
随机推荐
- Qt+QtWebApp开发笔记(二):http服务器日志系统介绍、添加日志系统至Demo测试
前言 上一篇使用QtWebApp的基于Qt的轻量级http服务器实现了一个静态网页返回的Demo,网页服务器很重要的就是日志,因为在服务器类上并没有直接返回,所以,本篇先把日志加上. Demo ...
- 正则表达式re模块---day18
1.匹配单个字符 import re lst = re.findall(正则表达式,要匹配的字符串) 返回的是列表,按照正则表达式匹配到的内容都扔到列表中 # ### 1.预定义字符集 # \d 匹配 ...
- 推荐10款C#开源好用的Windows软件
DevToys 项目简介:DevToys是一个专门为开发者设计的Windows工具箱,完全支持离线运行,无需使用许多不真实的网站来处理你的数据,常用功能有:格式化(支持 JSON.SQL.XML).J ...
- 【开发工具】Linux 服务器 Shell 脚本简单入门
记录一下学习Shell编程的关键知识点,使用最通俗简洁的语句,让阅读者能快速上手Shell脚本的编写 1.什么是Shell? Shell是一种常用于服务器运维的脚本语言.众所周知,脚本语言不需要编译器 ...
- Python函数每日一讲 - 一文让你彻底掌握Python中的getattr函数
引言 在 Python 中,getattr() 函数是一种强大的工具,它允许我们在运行时动态地访问对象的属性和方法.本文将介绍 getattr() 函数的基本语法.常见用法和高级技巧,帮助大家更好地理 ...
- 专访实在智能孙林君:颠覆传统RPA的实在IPA模式如何做到真正人人可用?
王吉伟对话实在智能孙林君:颠覆传统引领RPA行业的实在IPA模式是如何炼成的? 王吉伟对话实在智能孙林君:为什么第一款颠覆行业的RPA诞生在实在智能? 专访实在智能孙林君:打造出真正人人可用的实在 ...
- CPNtools协议建模安全分析---实例(二)
首先,token值是变迁的内部的,当变迁点火触发的时候token才会在网络中移动.在颜色Petri网中token是有区分的. 1.我么现在举一个学生吃饼的例子 ,颜色这样定义. s表示一个学生类 ...
- APISIX的安装和简单使用
APISIX 是一个云原生.高性能.可扩展的微服务 API 网关. 它是基于 Nginx 和 etcd 来实现,和传统 API 网关相比,APISIX 具备动态路由和插件热加载,特别适合微服务体系下的 ...
- yarn install --offline 离线安装 回头试试 npm install ./package.tgz
yarn install --offline npm pack npm install ./package.tgz 尝试了 npm-pack-all --dev-deps 也不行,太慢,等了20分钟 ...
- dotNet8 全局异常处理
前言 异常的处理在我们应用程序中是至关重要的,在 dotNet 中有很多异常处理的机制,比如MVC的异常筛选器, 管道中间件定义try catch捕获异常处理亦或者第三方的解决方案Hellang.Mi ...