基于Canvas实现的简历编辑器

基于Canvas实现的简历编辑器

大概一个月前,我发现社区老是给我推荐Canvas相关的内容,比如很多 小游戏、流程图编辑器、图片编辑器 等等各种各样的项目,不知道是不是因为我某一天点击了相关内容触发了推荐机制,还是因为现在Canvas比较火大家都在卷,本着我可以用不上但是不能不会的原则,我也花了将近一个月的时间通过Canvas实现了简历编辑器。

关于Canvas简历编辑器项目的相关文章:

  • 社区老给我推Canvas,我也学习Canvas做了个简历编辑器
  • Canvas图形编辑器-数据结构与History(undo/redo)
  • Canvas图形编辑器-我的剪贴板里究竟有什么数据
  • Canvas简历编辑器-图形绘制与状态管理(轻量级DOM)
  • Canvas简历编辑器-Monorepo+Rspack工程实践

为什么要自行实现一个简历编辑器:

  1. 固定模版不好用,各种模版用起来细节上并不是很满意,要么是模块的位置固定,要么是页面边距不满意,而通过Canvas实现的简历编辑器都是图形,完全依靠画布绘制图形,在给定的基础图形上可以任意绘制,不会有排版问题。
  2. 数据安全不能保证,因为简历上通常会存在很多个人信息,例如电话、邮箱等等,这些简历网站通常都需要登录才能用,数据都存在服务端,虽然泄漏的可能性不大,但是保护隐私还是很重要的,此编辑器是纯前端项目,数据全部存储在本地,没有任何服务器上传行为,可以完全保证数据安全。
  3. 维持一页简历不易,之前使用某简历模版网站时,某一项写的字较多时导出就会出现多页的情况,而我们大家大概都听说过简历最好是一页,所以在实现此编辑器时是直接通过排版的方式生成PDF ,所以在设置页面大小后,导出的PDF总会是保持一页,看起来会更美观。

背景

我是有个基于DOM实现的简历编辑器项目的,因为暂时找不到可以用Canvas实现的比较有意思的场景,所以才选择了继续做简历编辑器,最开始做简历编辑器就是因为很多简历网站都是要开会员的,要不就是简历的自定义程度比较差,达不到我想要的效果,在学校的某一个晚上突发奇想于是自己做了一个出来。

因为是本着学习的态度以及对技术的好奇心来做的,所以除了一些工具类的包例如 ArcoDesignResizeObserveJest 等包之外,关于 数据结构packages/delta、插件化packages/plugin、核心模块packages/core 等都是手动实现的。实际上这也是本着 自己学习的项目能自己写就自己写,公司/商业化项目能有已有包就用已有包 的原则来的,在这里的目标是学习而不是做产品,自己学习肯定是希望能够更多地接触相对底层一些的能力,自己可以多踩一些坑会对相关能力有更深的理解,如果是公司的项目那肯定是成熟的产品优先,成熟的产品对于边界case的处理以及积攒的issue也不是轻易能够比拟的。

开源地址: https://github.com/WindrunnerMax/CanvasEditor 。
在线DEMO: https://windrunnermax.github.io/CanvasEditor/ 。

image.png

笔记

因为我的主要目标是学习基本的Canvas知识和能力,所以很多功能模块都是采用简单的方式实现的,主打一个能用就行。而实际上做好图形编程是一件非常困难的事,如果要做一些复杂的能力我会更倾向于用konva等工具包来实现,而即使是简单地实现功能,在写代码的时候我也遇到了很多问题,也记录一些思考来解决问题。

数据结构

数据结构的设计,类似于DeltaSet,最终呈现的数据结构形式是扁平化的,但是在Core中需要设计State来管理树形结构,因为要设计Undo/Redo的功能,在不全量存储快照的情况下就意味着必须设计原子化的Op,因为想实现的功能有组合这个能力,所以最终实现的形式实际上是树形的结构,而我希望的结构是扁平化的,因为树形结构查找起来比较费劲,需要实现的Op类型也会变多,我希望能尽量减少Op的类型并且能够做到History,所以最终定下的数据结构是DeltaSet作为存储,通过State来管理整个编辑器状态。

History

原子化的Op已经设计好了,所以在设计History模块时就不需要全量保存快照了,但是如果每个操作都需要并入History Stack的话可能并不是很好,通常都是有NOp的一并Undo/Redo,所以这个模块应该有一个定时器,如果在N毫秒秒内没有新的Op加入的话就将Op并入History Stack,但是当时我在思考一个问题,如果这N毫秒内用户进行了Undo操作应该怎么办,后来想想实际上很简单,此时只需要清除定时器,将暂存的Op[]立即放置于Redo Stack即可。

绘制

任何元素都是矩形,数据结构也是据此设计抽象出来的,在绘制的时候分为两层Canvas重叠的方式,内层的Canvas是用来绘制具体图形的,这里预计需要实现增量更新,而外层的Canvas是用来绘制中间状态的,例如选中图形、多选、调整图形位置/大小等,在这里是会全量刷新的,并且后边可能会在这里绘制标尺。在实现交互的过程中我遇到了一个比较棘手的问题,因为不存在DOM,所有的操作都是需要根据位置信息来计算的,比如选中图形后调整大小的点就需要在选中状态下并且点击的位置恰好是那几个点外加一定的偏移量,然后再根据MouseMove事件来调整图形大小,而实际上在这里的交互会非常多,包括多选、拖拽框选、Hover效果,都是根据MouseDownMouseMoveMouseUp三个事件完成的,所以如何管理状态以及绘制UI交互就是个比较麻烦的问题,在这里我只能想到根据不同的状态来携带不同的Payload,进而绘制交互。

绘制状态

在实现绘制的时候,我一直在考虑应该如何实现这个能力,因为上边也说了这里是没有DOM的,所以最开始的时候我通过MouseDownMouseMoveMouseUp实现了一个非常混乱的状态管理,完全是基于事件的触发然后执行相关副作用从而调用Mask的方法进行重新绘制。再后来我觉得这样的代码根本没有办法维护,所以改动了一下,将我所需要的状态全部都存储到一个Store中,通过我自定义的事件管理来通知状态的改变,最终通过状态改变的类型来严格控制将要绘制的内容,也算是将相关的逻辑抽象了一层,只不过在这里相当于是我维护了大量的状态,而且这些状态是相互关联的,所以会有很多的if/else去处理不同类型的状态改变,而且因为很多方法会比较复杂,传递了多层,导致状态管理虽然比之前好了一些可以明确知道状态是因为哪里导致变化的,但是实际上依旧不容易维护。最终我又思考了一下,决定在绘图这里实现类似于DOM的能力,因为我想实现的能力似乎本质上就是DOM与事件的关联,而DOM结构是一种非常成熟的设计了,这其中有一些很棒的点子,例如DOM的事件流,我不需要扁平化地调整每个Node的事件,而是只需要保证事件是从ROOT节点起始,最终又在ROOT上结束即可,并且整个树形结构以及状态是靠用户利用DOMAPI来实现的,我们管理之需要处理ROOT就好了,这样就会很方便,下个阶段的状态管理是准备用这种方式来实现的。

渲染与事件

在前边我们提到了我们想通过模拟DOM来完成Canvas的绘制与交互,那么在这里就很明显涉及到DOM的两个重要内容,即DOM渲染与事件处理。那么就先聊下渲染方面的内容,使用Canvas实际上就很像将所有DOMposition设置为absolute,所有的渲染都是相对于Canvas这个DOM元素的位置绘制,那么我们就需要考虑重叠的情况,那么想一个例子,AzIndex10A的子元素BzIndex100CA是平级的且zIndex20,那么当这三个元素重叠的时候,在最顶部的元素是C,也就是说zIndex实际上只看平级元素,再假如AzIndex10A的子元素BzIndex1,那么在这两个元素重叠的时候,在最顶部的元素是B,也就是说子元素通常都是渲染在父元素之上的。那么我们在这里也需要模拟这个行为,但是因为我们没有浏览器的渲染合成层,我们能够操作的只有一层,所以在这里我们需要根据一定的策略进行渲染,在渲染时我们与DOM的渲染策略相同,即先渲染父元素再渲染子元素,类似于深度优先递归遍历的渲染顺序,不同的是我们需要在每个节点遍历之前,将子节点根据zIndex排序来保证同层级的节点渲染重叠关系。

在渲染的基础上,我们还需要考虑事件的实现,例如我们的选中状态,八向调整元素大小的点一定是在选区节点的上层的,那么假如现在我们需要实现onMouseEnter事件的模拟,那么因为Resize这八个点位与选区节点是有一定重叠的,所以如果此时鼠标移动到重叠的点因为Resize的实际渲染位置更高,所以只应该触发这个点的事件而不应该触发后边的选区节点事件,而实际上由于没有DOM结构的存在我们就只能使用坐标计算,那么在这里我们最简单的方法就是保证整个遍历的顺序,也就是说高节点的遍历一定是要先于低节点的,当我们找到这个节点就结束遍历然后触发事件,事件的捕获与冒泡机制我们也需要模拟,实际上这个顺序跟渲染是反过来的,我们想要的是优点顶部的元素,优先更像树的右子树优先后序遍历,也就是把前序遍历的输出、左子树、右子树三个位置调换一下即可,但是问题来了,在onMouseMove这种高频事件触发的时候,我们每次都去计算节点的位置并且采用深度优先遍历,是非常耗费性能的,所以在这里实现一个典型的空间换时间,将当前节点的子节点按顺序全部存储起来,如果有节点的变动,就直接通知该节点的所有每一层父节点重新计算,这里做成按需计算即可,这样当另一颗子树不变的时候还可以节省下次计算的时间,并且存储的是节点的引用,不会有太大的消耗,这样就变递归为迭代了,另外因为找到了当前的节点,在模拟捕获与冒泡的时候就不需要再递归触发了,通过两个栈即可模拟。

焦点

平时我做富文本相关的功能比较多,所以在实现画板的时候总想按照富文本的设计思路来实现,因为之前也说过要实现History以及在编辑面板富文本的能力,所以焦点就很重要,如果焦点不在画板上的时候如果按下Undo/Redo键画板是不应该响应的,所以现在就需要有一个状态来控制当前焦点是否在Canvas上,经过调研发现了两个方案,方案一是使用document.activeElement,但是Canvas是不会有焦点的,所以需要将tabIndex="-1"属性赋予Canvas元素,这样就可以通过activeElement拿到焦点状态了,方案二是在Canvas上方再覆盖一层div,通过pointerEvents: none来防止事件的鼠标指针事件,但是此时通过window.getSelection是可以拿到焦点元素的,此时只需要再判断焦点元素是不是设置的这个元素就可以了。

无限画布

之前因为没有打算实现平移拖拽也就是无限画布的能力,但是后来真的开始通过这个主框架来实现想做的业务功能的时候发现这样是不行的,所以在后期想把这个能力加上,虽然本身这个能力并不复杂,但是因为最开始没有设计这个能力,导致后边做的时候有点难受,比如Mask批量刷新频率不对齐、ctxtranslate应该是偏移值取反、之前多处超出画布不绘制的计算有误等等,就感觉在没有设计的情况下突然增加功能确实是有点难受的,不过好处是不需要大规模重构,只是个别点位的修正。

此外多扯点别的,这个项目除了一些辅助性的工具例如resize-observer以及组件库例如arco-design都是自己写的,相当于实现了Canvas的引擎,特别是在现在的core-delta-plugin-utils结构设计下,是完全可以抽离处理作为工具包使用的,当然易用性与性能方面肯定比不上那些有名的开源框架。只不过今天我恰好看到了一个评论说的挺好的:如果是个人能力提升,那么最好是首先理解开源库,然后仿照实现开源库的功能,主要的目标是学习;而如果是商业化的使用,那就变成了知名的开源库优先,这样可以很大程度上降低成本。

性能优化

在实现的过程中,绘制的性能优化主要有:

  1. 可视区域绘制,完全超出画布的元素不绘制。
  2. 按需绘制,只绘制当前操作影响范围内的元素。
  3. 分层绘制,高频操作绘制在上层画布,基础元素绘制在下层画布。
  4. 节流批量绘制,高频操作节流绘制,上层画布收集依赖批量绘制。

超链接

众所周知Canvas绘制出来就是纯粹的图片,而实际使用导出PDF的超链接是可以点击的,而我们当前就单纯只是图片无法做到这一点,所以需要解决这个问题,我想到的一个解决方案是在导出的时候,通过DOM生成透明的a标签,覆盖在原本的超链接位置,这样就可以实现点击跳转效果了。PDF本身也是文件格式,所以是可以借助PDFKit/PDFjsPDF排版生成工具来导出的,通过这种方式也可以直接在导出的时候直接将其写入固定位置,并且可以不受浏览器打印的分页限制。

TODO

因为前边提到了我现在还是比较简单的实现方式,所以很多功能都不完善,还有很多想做的能力:

  • 层级调整,这个之前我想到了并且在core中设计了这个能力,现在只是缺乏调整的按钮用来调用,这个UI我还没考虑好应该怎么做。
  • 页面配置,我发现很多同学的简历都是不是标准的A4纸大小,所以这里还需要一个调整页面画布大小的问题。
  • 导入导出JSON,这个就不用多说了,就是把底层数据结构导入导出的能力。
  • 排版PDF导出,这个应该需要跟页面配置一起做,现在的PDF导出是依赖浏览器的打印,会有一些分页的限制,如果自己排版的话就可以突破这个问题,多长的画布都是一页的简历大小。
  • 复制粘贴模块,在编辑的时候这个操作是很有用的,需要增加这个模块。

最后

这次对于Canvas的体验让我感觉还是不错的,后边我也会写一些在实现的时候碰到的问题以及如何解决问题的文章,不过我目前的主业还是还是写富文本编辑器,富文本编辑器也是天坑中的一员,后边也可能会先写编辑器相关的文章。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/545106.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

DV证书——网站安全的第一道防线

简介 在日新月异的互联网世界中,网站安全已成为衡量用户信任度和企业责任的重要指标。域名验证SSL证书(Domain Validation, DV证书)作为最基本的加密证书类型,以其高效、便捷的特性,为网站开启了HTTPS加密之旅&#x…

Jmeter八大元件

Jmeter八大元件 一、定义二、Jmeter八大元件的作用域三、 Jmeter的执行顺序 一、定义 取样器:jmeter接口测试的核心,我们发送接口请求的配置都必须在取样器中完成。 逻辑控制器:可以控制Jmeter其他元件的运行方式。主要有循环、IF条件等功能…

电脑删除文件怎么恢复?掌握3个策略就足够!

“我的电脑文件不小心被我删除了,不知道应该怎么操作才能把我误删的文件找回来呢?希望大家能帮帮我!” 在使用电脑的过程中,我们可能会不小心删除一些重要的文件,这时就需要采取一些措施来恢复这些文件。电脑删除的文件…

HTML制作跳动的心形网页

作为一名码农 也有自己浪漫的小心思嗷~ 该网页 代码整体难度不大 操作性较强 祝大家都幸福hhhhh 效果成品&#xff1a; 全部代码&#xff1a; <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> <HTML><HEAD><TITLE> 一个…

攻防世界07Robots

7.Robots 打开是一个空白的&#xff08;没错下面是一张空白图片&#xff09; 查看源代码显示flag is not here&#xff0c;在后面加上robots.txt查看 robots协议也称爬虫协议、爬虫规则等,是指网站可建立一个robots.txt文件来告诉搜索引擎哪些页面可以抓取,哪些页面不能抓取,而…

MyBatis 源码分析 - 配置文件解析过程

* 本文速览 由于本篇文章篇幅比较大&#xff0c;所以这里拿出一节对本文进行快速概括。本篇文章对 MyBatis 配置文件中常用配置的解析过程进行了较为详细的介绍和分析&#xff0c;包括但不限于settings&#xff0c;typeAliases和typeHandlers等&#xff0c;本文的篇幅也主要在…

运动耳机怎么选择?五款超值必购开放式耳机推荐!

在喧嚣的城市中&#xff0c;如何找到一款既适合户外运动又能保持警觉性的耳机呢&#xff1f;开放式蓝牙耳机或许是你的理想之选。它的开放式设计让你在享受音乐的同时&#xff0c;也能时刻关注周围环境的变化。对于经常参与户外活动或需要在工作场所保持警觉的人来说&#xff0…

B端模块(1):用户管理模块的定义、功能、页面和设计原则。

B端管理系统都是各个模块的有机结合&#xff0c;保证系统的正常运转&#xff0c;这点和人体系统一样&#xff0c;比如消化、呼吸、循环系统等等。从本期开始&#xff0c;贝格前端工场将详细B端各个模块&#xff0c;一共分为20期&#xff0c;本期是第一期&#xff0c;欢迎老铁们…

精品方案- 智慧养殖业IOT项目技术建议书(免费下载)

本项目建设从实际需求出发&#xff0c;利用物联网信息化手段进行畜牧业经济运行监测&#xff0c;掌握畜牧业生产与畜牧业经济运行的动态&#xff0c;监测畜牧业生产经营的成本收益变化&#xff0c;对畜牧业生产经营活动提供分析。提高畜牧业市场监管的电子化、网络化水平&#…

Maven 项目 JDK 8、JDK 17 多版本 Java 编译依赖最佳实践

博主历时三年精心创作的《大数据平台架构与原型实现&#xff1a;数据中台建设实战》一书现已由知名IT图书品牌电子工业出版社博文视点出版发行&#xff0c;点击《重磅推荐&#xff1a;建大数据平台太难了&#xff01;给我发个工程原型吧&#xff01;》了解图书详情&#xff0c;…

Java SPI机制详解

Java SPI机制详解 1、什么是SPI&#xff1f; SPI 全称为 (Service Provider Interface) &#xff0c;是JDK内置的一种服务提供发现机制。SPI是一种动态替换发现的机制&#xff0c; 比如有个接口&#xff0c;想运行时动态的给它添加实现&#xff0c;你只需要添加一个实现。我们…

高中数学:三角函数-4个解题妙招

一、对偶式 1、针对题型 同角三角函数的问题 2、方法定义 对于形如下方的式子&#xff0c;就可以用对偶式方法解 3、练习 例题1 例题2 二、巧用三角函数定义 1、针对题型 没有给出具体三角函数值的问题 2、方法定义 3、练习 例题1 三、诱导公式 1、针对题型 锐…

VK1618 SOP18/DIP18高稳定LED驱动IC防干扰数显驱动控制器计量插座数显芯片 FAE支持

产品型号&#xff1a;VK1618 产品品牌&#xff1a;永嘉微电/VINKA 封装形式&#xff1a;SOP18/ DIP18 原厂&#xff0c;工程服务&#xff0c;技术支持&#xff01; 概述 VK1618是一种带键盘扫描接口的数码管或点阵LED驱动控制专用芯片&#xff0c;内部集成有3线串行接口、数…

高风险IP的来源及其影响

随着互联网的发展&#xff0c;网络安全问题越来越引人关注。其中&#xff0c;高风险IP的来源成为了研究和讨论的焦点之一。高风险IP指的是那些经常涉及到网络攻击、恶意软件传播以及其他不良行为的IP地址。它们的存在不仅对个人和组织的网络安全构成威胁&#xff0c;还可能给整…

2024年4月最新十大地推拉新APP一手接单平台!盘点地推网推项目渠道!

随着移动互联网的蓬勃发展&#xff0c;APP市场的竞争愈发激烈&#xff0c;各类APP需要不断创新&#xff0c;吸引更多用户。在这个背景下&#xff0c;拉新推广市场愈发兴盛。如果您正在寻找最新的APP拉新渠道&#xff0c;或者想了解如何获取和使用地推拉新资源&#xff1f;那么您…

matlab使用教程(44)—绘制带标记的二维曲线图

在线图中添加标记是区分多个线条或突出显示特定数据点的有用方法。使用下面的一种方式添加标记&#xff1a; • 在线条设定输入参数&#xff08;例如 plot(x,y,-s) &#xff09;中包含标记符号。 • 将 Marker 属性指定为一个名称-值对组&#xff0c;例如 plot(x,y,Marker,s…

元强化学习研究综述

源自&#xff1a;软件学报 作者&#xff1a;陈奕宇, 霍静, 丁天雨, 高阳 “人工智能技术与咨询” 发布 摘 要 近年来, 深度强化学习(deep reinforcement learning, DRL)已经在诸多序贯决策任务中取得瞩目成功, 但当前, 深度强化学习的成功很大程度依赖于海量的学习数据与计…

[阅读笔记5][MoCo]Momentum Contrast for Unsupervised Visual Representation Learning

接下来是MoCo这篇论文&#xff0c;facebook于20年2月发表。 这篇论文研究的是对比学习。 受NLP自监督预训练的模型影响&#xff0c;CV这边也希望能有一个自监督预训练的特征提取器&#xff0c;这样就能很方便的在其他下游任务微调了。而对比学习的目的就是能够自监督预训练得到…

postgresql 备份恢复相关知识点整理归纳 —— 筑梦之路

概述 PG一般有两种备份方式&#xff1a;逻辑备份和物理备份 逻辑备份对于数据量大的场景下耗时较长&#xff0c;恢复也会耗时较长 物理备份拷贝文件的方式相对来说耗时较短&#xff0c;跟磁盘读写性能和网络传输性能有关 逻辑备份 pg_dump pg_dump 将表结构及数据以SQL语句…

传感器展会现场直击!道合顺传感邀您共鉴气体传感器前沿技术

4月14日&#xff0c;#深圳国际传感器#与应用技术展览会在深圳会展中心&#xff08;福田&#xff09;如期举办。道合顺传感亮相本届大会并展示了对气体传感器的探索和最新研究成果&#xff0c;获得了传感器业内的广泛关注。 多年来&#xff0c;道合顺传感依托于雄厚的研发实力&a…