本章分享一下如何使用 Konva 绘制基础图形:曲线,以及属性面板的基本实现思路,希望大家继续关注和支持哈(多求 5 个 Stars 谢谢)!

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

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

github源码

gitee源码

示例地址

绘制曲线

先上效果!

这里其实取巧了哈,基本就是在绘制折线的基础上,给 Konva.Line 添加一个关键的属性 tension 即可,参照官方示例

未来,在属性面板中,可以调节 tension 的值,基本可以实现绘制一些简单的曲线。

属性面板

早些时候,已经有小伙伴问,外部如何动态调整 Konva 内部各对象的一些特性,这里以页面的背景色、全局线条和填充颜色,及其素材各自的线条和填充颜色为例,分享一个基本可行实现思路是如何的。

这里以 svg 素材为例,可以调整 svg 素材的线条、填充颜色。

基本交互

UI

这里简单粗暴一些,使用 naive-ui 的组件组装一下就可以了:

<!-- src/App.vue -->

      <n-tabs type="line" size="small" animated v-model:value="tabCurrent">
<n-tab-pane name="page" tab="页面">
<n-form ref="formRef" :model="pageSettingsModel" :rules="{}" label-placement="top" size="small"
v-if="pageSettingsModel">
<n-form-item label="背景色" path="background">
<n-color-picker v-model:value="pageSettingsModelBackground" @update:show="(v: boolean) => {
pageSettingsModel && !v && (pageSettingsModelBackground = pageSettingsModel.background)
}" :actions="['clear', 'confirm']" show-preview
@confirm="(v: string) => { pageSettingsModel && (pageSettingsModel.background = v) }"
@clear="pageSettingsModel && (pageSettingsModel.background = Render.PageSettingsDefault.background)"></n-color-picker>
</n-form-item>
<n-form-item label="线条颜色" path="stroke">
<n-color-picker v-model:value="pageSettingsModelStroke" @update:show="(v: boolean) => {
pageSettingsModel && !v && (pageSettingsModelStroke = pageSettingsModel.stroke)
}" :actions="['clear', 'confirm']" show-preview
@confirm="(v: string) => { pageSettingsModel && (pageSettingsModel.stroke = v) }"
@clear="pageSettingsModel && (pageSettingsModel.stroke = Render.AssetSettingsDefault.stroke)"></n-color-picker>
</n-form-item>
<n-form-item label="填充颜色" path="fill">
<n-color-picker v-model:value="pageSettingsModelFill" @update:show="(v: boolean) => {
pageSettingsModel && !v && (pageSettingsModelFill = pageSettingsModel.fill)
}" :actions="['clear', 'confirm']" show-preview
@confirm="(v: string) => { pageSettingsModel && (pageSettingsModel.fill = v) }"
@clear="pageSettingsModel && (pageSettingsModel.fill = Render.AssetSettingsDefault.fill)"></n-color-picker>
</n-form-item>
</n-form>
</n-tab-pane>
<n-tab-pane name="asset" tab="素材" :disabled="assetCurrent === void 0">
<n-form ref="formRef" :model="assetSettingsModel" :rules="{}" label-placement="top" size="small"
v-if="assetSettingsModel">
<n-form-item label="线条颜色" path="stroke" v-if="assetCurrent?.attrs.imageType === Types.ImageType.svg">
<n-color-picker v-model:value="assetSettingsModelStorke" @update:show="(v: boolean) => {
assetSettingsModel && !v && (assetSettingsModelStorke = assetSettingsModel.stroke)
}" :actions="['clear', 'confirm']" show-preview
@confirm="(v: string) => { assetSettingsModel && (assetSettingsModel.stroke = v) }"
@clear="assetSettingsModel && (assetSettingsModel.stroke = '#000')"></n-color-picker>
</n-form-item>
<n-form-item label="填充颜色" path="fill" v-if="assetCurrent?.attrs.imageType === Types.ImageType.svg">
<n-color-picker v-model:value="assetSettingsModelFill" @update:show="(v: boolean) => {
assetSettingsModel && !v && (assetSettingsModelFill = assetSettingsModel.fill)
}" :actions="['clear', 'confirm']" show-preview
@confirm="(v: string) => { assetSettingsModel && (assetSettingsModel.fill = v) }"
@clear="assetSettingsModel && (assetSettingsModel.fill = '#000')"></n-color-picker>
</n-form-item>
</n-form>
</n-tab-pane>
</n-tabs>

魔改一下组件样式:

/* src/App.vue */

      :deep(.n-tabs-nav-scroll-content) {
box-shadow: 0 -1px 0 0 rgb(230, 230, 230) inset;
border-bottom-color: rgb(230, 230, 230) !important;
} :deep(.n-tabs-tab-pad) {
width: 16px;
}

组件和表单的控制:

// src/App.vue

// 略

function init() {
if (boardElement.value && stageElement.value) {
resizer.init(boardElement.value, {
resize: async (x, y, width, height) => {
if (render === null) {
// 初始化渲染
render = new Render(stageElement.value!, {
width,
height,
//
showBg: true,
showRuler: true,
showRefLine: true,
attractResize: true,
attractBg: true,
showPreview: true,
attractNode: true,
}) // 同步页面设置
pageSettingsModel.value = render.getPageSettings() await nextTick() ready.value = true
}
render.resize(width, height) // 同步页面设置
render.on('page-settings-change', (settings: Types.PageSettings) => {
pageSettingsModelInnerChange.value = true
pageSettingsModel.value = settings
}) render.on('selection-change', (nodes: Konva.Node[]) => {
if (nodes.length === 0) {
// 清空选择
assetCurrent.value = undefined
assetSettingsModel.value = undefined tabCurrent.value = 'page'
} else if (nodes.length === 1) {
// 单选
assetCurrent.value = nodes[0]
assetSettingsModel.value = render!.getAssetSettings(nodes[0]) tabCurrent.value = 'asset'
} else {
// 多选
assetCurrent.value = undefined
assetSettingsModel.value = undefined tabCurrent.value = 'page'
}
})
}
})
}
} // 略 // 当前 tab
const tabCurrent = ref('page') // 页面设置
const pageSettingsModel: Ref<Types.PageSettings | undefined> = ref()
const pageSettingsModelInnerChange = ref(false) const pageSettingsModelBackground = ref('')
const pageSettingsModelStroke = ref('')
const pageSettingsModelFill = ref('') // 当前素材
const assetCurrent: Ref<Konva.Node | undefined> = ref() // 素材设置
const assetSettingsModel: Ref<Types.AssetSettings | undefined> = ref()
const assetSettingsModelInnerChange = ref(false) const assetSettingsModelStorke = ref('')
const assetSettingsModelFill = ref('') watch(() => pageSettingsModel.value, () => {
if (pageSettingsModel.value) {
pageSettingsModelBackground.value = pageSettingsModel.value.background
pageSettingsModelStroke.value = pageSettingsModel.value.stroke
pageSettingsModelFill.value = pageSettingsModel.value.fill if (ready.value && !pageSettingsModelInnerChange.value) {
render?.setPageSettings(pageSettingsModel.value)
}
} pageSettingsModelInnerChange.value = false
}, {
deep: true
}) watch(() => assetSettingsModel.value, () => {
if (assetSettingsModel.value && assetCurrent.value) {
assetSettingsModelStorke.value = assetSettingsModel.value.stroke
assetSettingsModelFill.value = assetSettingsModel.value.fill if (ready.value && !assetSettingsModelInnerChange.value) {
render?.setAssetSettings(assetCurrent.value, assetSettingsModel.value)
}
} assetSettingsModelInnerChange.value = false
}, {
deep: true
})

这里有几个小细节:

  • 颜色选择器 confirm 确认

没有直接用 v-model 绑定表单的颜色值,而定义了一些类似 xxxSettingsModelYyy 变量,原因是约束修改颜色必须通过 confirm 按钮才能使其颜色生效,需要一些变量作为缓存。

因此也多了一些初始化和同步赋值逻辑,看起来凌乱一些。

  • Tab自动切换

默认显示页面属性面板,选择单个素材(暂时只实现 svg 素材),切换至素材属性面板,清空选择则回到页面属性面板。

  • watch 逻辑锁

在监听 pageSettingsModel 的时候,需要判断 pageSettingsModelInnerChange 的状态,解决了因为 Render 的 上一步、下一步、导入 等操作,触发 page-settings-change 事件(自定义事件,后面细说),会改变 pageSettingsModel 的值,以此防止 重复的 setPageSettings(后面细说) 逻辑。

类型、事件定义

属性面板 与 Render 属性同步,主要靠的是自定义事件,原有的 selection-change 事件,可以解决判断当前应该处理页面属性还是素材属性;需要新增一个 page-settings-change 事件,获知因为 Render 的 上一步、下一步、导入 等操作,需要更新 pageSettingsModel 到值。

// src/Render/types.ts

// 略

export type RenderEvents = {
['history-change']: { records: string[]; index: number }
['selection-change']: Konva.Node[]
['debug-change']: boolean
['link-type-change']: LinkType
['scale-change']: number
['loading']: boolean
['graph-type-change']: GraphType | undefined
// 新增
['page-settings-change']: PageSettings
} // 略 /**
* 页面设置
*/
export interface PageSettings {
background: string
stroke: string
fill: string
} /**
* 素材设置
*/
export interface AssetSettings {
stroke: string
fill: string
}

属性默认值、获取属性值、设置属性值

这里是通过把页面属性、素材属性分别存放在 stage 和 素材group 的 attrs 中,pageSettings 和 assetSettings。

// src/Render/index.ts

// 略

// 页面设置 默认值
static PageSettingsDefault: Types.PageSettings = {
background: 'transparent',
stroke: 'rgb(0,0,0)',
fill: 'rgb(0,0,0)'
} // 获取页面设置
getPageSettings(): Types.PageSettings {
return this.stage.attrs.pageSettings ?? { ...Render.PageSettingsDefault }
} // 更新页面设置
setPageSettings(settings: Types.PageSettings) {
this.stage.setAttr('pageSettings', settings) // 更新背景
this.updateBackground() // 更新历史
this.updateHistory() // console.log(this.stage.attrs)
} // 获取背景
getBackground() {
return this.draws[Draws.BgDraw.name].layer.findOne(
`.${Draws.BgDraw.name}__background`
) as Konva.Rect
} // 更新背景
updateBackground() {
const background = this.getBackground() if (background) {
background.fill(this.getPageSettings().background ?? 'transparent')
} this.draws[Draws.BgDraw.name].draw()
this.draws[Draws.PreviewDraw.name].draw()
} // 素材设置 默认值
static AssetSettingsDefault: Types.AssetSettings = {
stroke: '',
fill: ''
} // 获取素材设置
getAssetSettings(asset?: Konva.Node): Types.AssetSettings {
const base = asset?.attrs.assetSettings ?? { ...Render.AssetSettingsDefault }
return {
// 特定
...base,
// 继承全局
stroke: base.stroke || this.getPageSettings().stroke,
fill: base.fill || this.getPageSettings().fill
}
} // 设置 svgXML 样式(部分)
setSvgXMLSettings(xml: string, settings: Types.AssetSettings) {
const reg = /<(circle|ellipse|line|path|polygon|rect|text|textPath|tref|tspan)[^>/]*\/?>/g const shapes = xml.match(reg) const regStroke = / stroke="([^"]*)"/
const regFill = / fill="([^"]*)"/ for (const shape of shapes ?? []) {
let result = shape if (settings.stroke) {
if (regStroke.test(shape)) {
result = result.replace(regStroke, ` stroke="${settings.stroke}"`)
} else {
result = result.replace(/(<[^>/]*)(\/?>)/, `$1 stroke="${settings.stroke}" $2`)
}
} if (settings.fill) {
if (regFill.test(shape)) {
result = result.replace(regFill, ` fill="${settings.fill}"`)
} else {
result = result.replace(/(<[^>/]*)(\/?>)/, `$1 fill="${settings.fill}" $2`)
}
} xml = xml.replace(shape, result)
} return xml
} // 更新素材设置
async setAssetSettings(asset: Konva.Node, settings: Types.AssetSettings) {
asset.setAttr('assetSettings', settings)
if (asset instanceof Konva.Group) {
const node = asset.children[0] as Konva.Shape
if (node instanceof Konva.Image) {
if (node.attrs.svgXML) {
const n = await this.assetTool.loadSvgXML(
this.setSvgXMLSettings(node.attrs.svgXML, settings)
)
node.parent?.add(n)
node.remove()
node.destroy()
n.zIndex(0)
}
}
} this.draws[Draws.BgDraw.name].draw()
this.draws[Draws.GraphDraw.name].draw()
this.draws[Draws.LinkDraw.name].draw()
this.draws[Draws.PreviewDraw.name].draw()
}

这里素材的线条、填充默认值,是会继承页面的线条、填充值的,就是说,拖入的素材线条、填充值,会按当前页面的值初始化。

getBackground

这里获取的背景是一个放在网格线同 Layer 的 Rect,用于模拟页面背景的:

// src/Render/draws/BgDraw.ts

// 略

      group.add(
new Konva.Rect({
name: `${this.constructor.name}__background`,
x: this.render.toStageValue(-stageState.x + this.render.rulerSize),
y: this.render.toStageValue(-stageState.y + this.render.rulerSize),
width: this.render.toStageValue(stageState.width),
height: this.render.toStageValue(stageState.height),
listening: false,
fill: this.render.getPageSettings().background
})
) // 略

这里说“模拟”的意思是,背景最后是在 导入、导出 的时候才真正的处理:

// 恢复
async restore(json: string, silent = false) {
try {
// 略 // 往 main layer 插入新节点
this.render.layer.add(...nodes) // 同步页面设置
this.render.stage.setAttr('pageSettings', stage.attrs.pageSettings)
this.render.emit('page-settings-change', this.render.getPageSettings()) // 更新背景
this.render.updateBackground() // 略
} catch (e) {
console.error(e)
} finally {
// 略
}
} // 略 // 获取元素图片
getAssetImage(pixelRatio = 1, bgColor?: string) {
// 略 bg.setAttrs({
x: -copy.x(),
y: -copy.y(),
width: copy.width(),
height: copy.height(),
fill: bgColor ?? this.render.getPageSettings().background
}) // 略
} // 略 // 获取Svg
async getSvg() {
// 略 // 获得 svg
let rawSvg = c2s.getSerializedSvg()
console.log(rawSvg) // 添加背景
rawSvg = rawSvg.replace(
/(<defs\/><g><rect fill=")([^"]+)(")/,
`$1${this.render.getPageSettings().background}$3`
) // 略
}
// 略
} // 略 /**
* 获得元素(用于另存为元素)
* @returns Konva.Stage
*/
getAsset() {
const copy = this.getAssetView() // 添加背景
const background = this.render.getBackground()
background.width(copy.width())
background.height(copy.height())
copy.children[0].add(background)
background.moveToBottom() // 略
}

分别说说处理的思路:

  • 导出图片

在 toDataURL 之前在添加背景 Rect。

  • 导出 svg

这里的思路是,通过正则表达式替换 svg xml 内容,修改上面提到的 背景 Rect 对应的 svg xml rect 结构。

  • 导出素材 json

虽然这里也是添加背景 Rect,不同之处是,该层与其他素材同级,像似一个内部素材。

  • 导入 json

通过 stage 的 attrs 中 pageSettings 属性记录,通过事件 page-settings-change 恢复外部表单 model 的值。并同时更新 背景 Rect 的颜色。

setAssetSettings、setSvgXMLSettings

可以看到这里看起来明显有点复杂,由于素材 svg 最终是以 Konva.Image 的方式加载的,所以唯一可以影响显示的线条、填充颜色,只能在加载之前,通过替换 svg xml 实现。

替换 svg xml 分4步:

1、通过 attrs 取出 svgXml 的值;

2、通过正则表达式替换/插入线条、填充颜色值;

3、生成新的 Image 替换原来的 Image;

4、恢复新的 Image 的 zIndex(置顶);

替换 svg xml 思路比较简单粗暴,就是把可能的节点 circle|ellipse|line|path|polygon|rect|text|textPath|tref|tspan,识别提取出来,进行 stroke、fill 的替换/插入。

恢复加载 svg 素材的时候,也处理一遍:

// src/Render/tools/AssetTool.ts

// 略

  // 加载 svg
async loadSvg(src: string) {
const svgXML = await (await fetch(src)).text()
return this.loadSvgXML(this.render.setSvgXMLSettings(svgXML, this.render.getAssetSettings()))
} // 略

上面说到,拖入的 svg 素材,会基于 页面的线条、填充值,所以拖入的时候也要处理一下:

// src/Render/handlers/DragOutsideHandlers.ts

// 略

drop: (e: GlobalEventHandlersEventMap['drop']) => {
// 略
let group = null
// 默认连接点
let points: Types.AssetInfoPoint[] = [] // 图片素材
if (target instanceof Konva.Image) {
group = new Konva.Group({
id: nanoid(),
width: target.width(),
height: target.height(),
name: 'asset',
assetType: Types.AssetType.Image,
draggable: true,
imageType:
type !== 'json'
? type === Types.ImageType.svg
? Types.ImageType.svg
: type === Types.ImageType.gif
? Types.ImageType.gif
: Types.ImageType.other
: undefined
}) this.render.setAssetSettings(group, this.render.getAssetSettings()) // 略
} else {
// json 素材 // 略
} // 略
})
}
}
} // 略

说到这里,基本实现了页面属性、素材属性及其继承关系的实现(还有很多优化空间)啦!

Thanks watching~

More Stars please!勾勾手指~

源码

gitee源码

示例地址

前端使用 Konva 实现可视化设计器(23)- 绘制曲线、属性面板的更多相关文章

  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. 解析大型.NET ERP系统核心组件 查询设计器 报表设计器 窗体设计器 工作流设计器 任务计划设计器

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

  9. F2工作流引擎之-纯JS Web在线可拖拽的流程设计器(八)

          Web纯JS流程设计器无需编程,完全是通过鼠标拖.拉.拽的方式来完成,支持串行.并行.分支.异或分支.M取N路分支.会签.聚合.多重聚合.退回.传阅.转交,都可以非常方便快捷地实现,管理员 ...

  10. 纯JS Web在线可拖拽的流程设计器

    F2工作流引擎之-纯JS Web在线可拖拽的流程设计器 Web纯JS流程设计器无需编程,完全是通过鼠标拖.拉.拽的方式来完成,支持串行.并行.分支.异或分支.M取N路分支.会签.聚合.多重聚合.退回. ...

随机推荐

  1. mysql 忘记root密码怎么办?

    忘记root可以跳过grant table来登录 1.打开命令行输入以下命令 mysqld -nt --grant-skip-tables 2.在打开一个新命令行,输入以下命令可以登录, mysql ...

  2. WordPress基础之菜单导航栏设置

    菜单是WordPress的一项重要功能,方便用户快速打开网站页面,我们通常说的网站导航栏就是菜单.菜单通常显示在网站的顶部或者底部,以Apple官网的为例: 这篇文章我们就学习下:如何添加.删除菜单: ...

  3. 倒装句&强调句

    倒装句 你[吃][胡萝卜]了吗? 吃胡萝卜了吗,[你]? 强调点不同 汉语常见于口语表达 英语则常见于书面用语 英语的语序是 主语 谓语(动词) 通常把谓语动词提前 1.完全倒装句 谓语部分完全放在主 ...

  4. Linux 备份命令 fsarchiver 基础使用教程

    1 安装配置 fsarchiver 使用yum安装[二者选一个即可,我使用的是下面那个]: yum install https://dl.fedoraproject.org/pub/epel/epel ...

  5. 【OracleDB】 08 子查询

    什么是子查询? 子查询是一种常用计算机语言SELECT-SQL语言中嵌套查询下层的程序模块. 当一个查询是另一个查询的条件时,称之为子查询. Oracle的子查询语法公式: SELECT select ...

  6. JDBC详解学习笔记

    JDBC简介 架构时--没有什么是加一层解决不了的,如果有,就再加一层. 如tomcat集群上面的Nginx,Nginx集群上面的LVS. JDBC是数据库驱动的接口规范,是SUN公司未来简化开发人员 ...

  7. 东北某海滨城市的某高校的某分校区的校园网登录程序,(python3, 模拟浏览器的登入方式)

    前些年写过这个登录程序,过了几年系统有所升级,于是做了一定的修改. 新版本的校园网登录程序依然是模拟浏览器去登录校园网. Python3.7编写. #encoding:UTF-8 from urlli ...

  8. 如何拉取指定CPU架构并且指定ubuntu版本并且指定cuda和cudnn版本的docker镜像

    本篇讲的重点是如何拉取带有cuda和cudnn的docker镜像,因此这些的镜像源的频道为NVIDIA: 官方地址: https://hub.docker.com/r/nvidia/cuda 根据官方 ...

  9. 【转载】解决WSL中Debian显示中文乱码的问题

    ---------------- 版权声明:本文为CSDN博主「捕鲸叉」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明.原文链接:https://blog.csdn. ...

  10. EF Core连接PostgreSQL数据库

    PostgreSQL数据库介绍 PostgreSQL是一个功能强大的开源对象关系型数据库管理系统(RDBMS).最初于1986年在加州大学伯克利分校的POSTGRES项目中诞生,PostgreSQL以 ...