当前位置: 首页 > news >正文

咨询邯郸网站建设动漫网站设计与实现

咨询邯郸网站建设,动漫网站设计与实现,阿里云买域名,服务器安全卫士Canvas图形编辑器-数据结构与History(undo/redo) 这是作为 社区老给我推Canvas#xff0c;于是我也学习Canvas做了个简历编辑器 的后续内容#xff0c;主要是介绍了对数据结构的设计以及History能力的实现。 在线编辑: https://windrunnermax.github.io/CanvasEditor开源地…Canvas图形编辑器-数据结构与History(undo/redo) 这是作为 社区老给我推Canvas于是我也学习Canvas做了个简历编辑器 的后续内容主要是介绍了对数据结构的设计以及History能力的实现。 在线编辑: https://windrunnermax.github.io/CanvasEditor开源地址: https://github.com/WindrunnerMax/CanvasEditor 关于Canvas简历编辑器项目的相关文章: 社区老给我推Canvas我也学习Canvas做了个简历编辑器Canvas图形编辑器-数据结构与History(undo/redo)Canvas图形编辑器-我的剪贴板里究竟有什么数据Canvas简历编辑器-图形绘制与状态管理(轻量级DOM)Canvas简历编辑器-MonorepoRspack工程实践 描述 对于编辑器而言History也就是undo和redo是必不可少的能力实现历史记录的方法通常有两种: 存储全量快照也就是说我我们每进行一个操作,都需要将全量的数据通常也就是JSON格式的数据存到一个数组里如果用户此时触发了redo就将全量的数据取出应用到Editor对象当中。这种实现方式的优点是简单不需要过多的设计缺点就是一旦操作的多了就容易炸内存。 基于Op的实现Op就是对于一个操作的原子化记录举个例子如果将图形A向右移动3px那么这个Op就可以是type: MOVE, offset: [3, 0]那么如果想要做回退操作依然很简单只需要将其反向操作即type: MOVE, offset: [-3, 0]就可以了这种方式的优点是粒度更细存储压力小缺点是需要复杂的设计以及计算。 既然我们是从零开始设计一个编辑器那么大概率是不会采用方案1的我们更希望能够设计原子化的Op来实现History所以从这个方向开始我们就需要先设计数据结构。 数据结构 我特别推荐大家去看一下 quill-delta 的数据结构设计这个数据结构的设计非常棒其可以用来描述一篇富文本同时也可以用来构建change对富文本做完整的增删改操作对于数据的compose、invert、diff等操作也一应俱全而且quill-delta也可以是富文本OT协同算法的实现这其中的设计还是非常牛逼的。 其实我之前也没有设计过数据结构更不用谈设计Op去实现历史记录功能了所以我在设计数据结构的时候是抓耳挠腮、寝食难安想设计出 quill-delta 这种级别的数据描述几乎是不可能了所以只能依照我的想法来简单地设计这其中有很多不完善的地方后边可能还会有所改动。 因为之前也没有接触过Canvas所以我的主要目标是学习所以我希望任何的实现都以尽可能简单的方向走。那么在这里我认为任何元素都是矩形因为绘制矩阵是比较简单的所以图形元素基类的x, y, width, height属性是确定的再加上还有层级结构那么就再加一个z此外由于需要标识图形所以还需要给其设置一个id。 class Delta {public readonly id: string;protected x: number;protected y: number;protected z: number;protected width: number;protected height: number; }因为我想做一个插件化的实现也就是说所有的图形都应该继承这个类那么这个自定义的函数体肯定是需要存储自己的数据所以在这里加一个attrs属性又因为想简单实现整个功能所以这个数据类型就被定义为Recordstring, string。因为是插件化的每个图形的绘制应该由子类来实现所以需要定义绘制函数的抽象方法于是一个数据结构就这么设计好了关于插件化的设计我们后续可以再继续聊。 abstract class Delta {public readonly id: string;protected x: number;protected y: number;protected z: number;protected width: number;protected height: number;public attrs: DeltaAttributes;public abstract drawing: (ctx: CanvasRenderingContext2D) void; }那么现在已经有了基本的数据结构我们可以设想一下究竟应该有哪几种操作经过考虑大概无非是 插入INSERT、删除DELETE、移动MOVE、调整大小RESIZE、修改属性REVISE这五个Op就可以覆盖我们对于当前编辑器图形的所有操作了所以我们后续的设计都要围绕着这五个操作来进行。 看起来其实并不难但实际上想要将其设计好并不容易因为我们目标是History所以我们不光要顾及正向的操作还需要设计好invert也就是反向操作依旧以之前的MOVE操作举例我们移动一个元素可以使用MOVE(3, 0)反向操作就可以直接生成也就是MOVE(3, 0).invert MOVE(-3, 0)那么RESIZE操作呢尤其是在多选操作时的RESIZE我们需要想办法让其能够实现invert操作一种方法是记录每个点的移动距离但是这样对于每个Op存储的信息有点过多我们在构造一个正向的Op时也需要将相关的数据拉到Op中同样对于REVISE而言我们需要将属性的前值和后值都放在Op中才可以继续执行。 那么如何比较好的解决这个问题呢很明显如果我们想用轻量的数据来承载内容那么先前的数据在不一定会使用的情况下我们是没必要存储的那是不是可以自动提取相关的内容作为invert-op呢当然是可以的我们可以在进行invert的时候将未操作前的Delta一并作为参数传入就好了我们可以来验证一下我们的函数签名将会是Op.invert(Delta) Op。 // Prev DeltaSet [{id: xxx, x: x1, y: y1, width: w1, height: h1}] // ResizeOp RESIZE({id: xxx, x: x2, y: y2}) // Next DeltaSet [{id: xxx, x: x1 x2, y: y1 y2, width: w1, height: w1}] // Invert InsertOp RESIZE({id: xxx, x: -x2, y: -y2})// Prev DeltaSet [{id: xxx, x: x1, y: y1, width: w1, height: h1}] // ResizeOp RESIZE({id: xxx, x: x2, y: y2, width: w2, height: h2}) // Next DeltaSet [{id: xxx, x: x2, y: y2, width: w2, height: h2}] // Invert InsertOp RESIZE({id: xxx, x: x1, y: y1, width: w1, height: h1})看起来是没有问题的所以我们现在可以设计全量的Op和Invert方法了在这里因为我最开始是预计要设计组合也就是将几个图形组合在一起操作的能力所以还预留了一个parentId作为后期开发拓展用但是暂时是用不上的所以这个字段暂时可以忽略。下面的Invert实际上就是case by case地进行转换INSERT - DELETE、DELETE - INSERT、MOVE - MOVE、RESIZE - RESIZE、REVISE - REVISE。这其中的DeltaSet可以理解为当前的所有Delta数据类型签名类似于Recordstring, Delta是扁平的结构便于数据查找。 export type OpPayload {[OP_TYPE.INSERT]: { delta: Delta; parentId: string };[OP_TYPE.DELETE]: { id: string; parentId: string };[OP_TYPE.MOVE]: { ids: string[]; x: number; y: number };[OP_TYPE.RESIZE]: { id: string; x: number; y: number; width: number; height: number };[OP_TYPE.REVISE]: { id: string; attrs: DeltaAttributes }; };export class OpT extends OpType {public readonly type: T;public readonly payload: OpPayload[T];constructor(type: T, payload: OpPayload[T]) {this.type type;this.payload payload;}public invert(prev: DeltaSet) {switch (this.type) {case OP_TYPE.INSERT: {const payload this.payload as OpPayload[typeof OP_TYPE.INSERT];const { delta, parentId } payload;return new Op(OP_TYPE.DELETE, { id: delta.id, parentId });}case OP_TYPE.DELETE: {const payload this.payload as OpPayload[typeof OP_TYPE.DELETE];const { id, parentId } payload;const delta prev.get(id);if (!delta) return null;return new Op(OP_TYPE.INSERT, { delta, parentId });}case OP_TYPE.MOVE: {const payload this.payload as OpPayload[typeof OP_TYPE.MOVE];const { x, y, ids } payload;return new Op(OP_TYPE.MOVE, { ids, x: -x, y: -y });}case OP_TYPE.RESIZE: {const payload this.payload as OpPayload[typeof OP_TYPE.RESIZE];const { id } payload;const delta prev.get(id);if (!delta) return null;const { x, y, width, height } delta.getRect();return new Op(OP_TYPE.RESIZE, { id, x, y, width, height });}case OP_TYPE.REVISE: {const payload this.payload as OpPayload[typeof OP_TYPE.REVISE];const { id, attrs } payload;const delta prev.get(id);if (!delta) return null;const prevAttrs: DeltaAttributes {};for (const key of Object.keys(attrs)) {prevAttrs[key] delta.getAttr(key);}return new Op(OP_TYPE.REVISE, { id, attrs: prevAttrs });}default:break;}return null;} }History 既然我们已经设计好了基于Op的原子化操作以及数据结构那么紧接着我们就可以开始做History能力了在这里首先需要注意我们先前对于Invert的思想是让其根据DeltaSet自动先生成InvertOp在这里我们可以有两种方案来实现。 第一种方式是在应用Op之前我们先根据当前的DeltaSet自动生成一个InvertOp然后将这个Op交给History模块存储起来作为Undo的组操作即可。 第二种方式是我们在应用Op之前首先生成一遍新的Previous DeltaSet是一个immer的副本然后将Prev DeltaSet以及Next DeltaSet一并作为OnChangeEvent交给History模块进行后续的操作。 最终我是选择了方案二作为整体实现倒是没有什么具体依据只是觉得这个immer的副本可能不仅会在这里使用作为事件的一部分分发先前的数据值我认为是合理的所以在应用Op的时候大致实现如下。 public apply(op: OpSetType, applyOptions?: ApplyOptions) {const options applyOptions || { source: user, undoable: true };const previous new DeltaSet(this.editor.deltaSet.getDeltas());switch (op.type) {// 根据不同的Op执行不同的操作}this.editor.event.trigger(EDITOR_EVENT.CONTENT_CHANGE, {previous,current: this.editor.deltaSet,changes: op,options,}); }其实我们也可以看到整个编辑器内部的通信是依赖于event这个模块的也就是说这个apply函数不会直接调用History的相关内容我们的History模块是独立挂载CONTENT_CHANGE事件的。那么紧接着我们需要设计History模块的数据存储我们先来明确一下想要实现的内容现在原子化的Op已经设计好了所以在设计History模块时就不需要全量保存快照了但是如果每个操作都需要并入History Stack的话可能并不是很好通常都是有N个Op的一并Undo/Redo所以这个模块应该有一个定时器与缓存数组还有最大时间如果在N毫秒秒内没有新的Op加入的话就将Op并入History Stack还有就是常规的undo stack以及redo stack栈存储的内容也不应该很大所以还需要设置最大存储量。 export class History {private readonly DELAY 800;private readonly STACK_SIZE 100;private temp: OpSetType[];private undoStack: OpSetType[][];private redoStack: OpSetType[][];private timer: ReturnTypetypeof setTimeout | null; }前边也提到过我们都是通过事件来进行通信的所以这里需要先挂载事件并且在这里将Invert的Op构建好将其置入批量操作的缓存中。 constructor(private editor: Editor) {this.editor.event.on(EDITOR_EVENT.CONTENT_CHANGE, this.onContentChange, 10);}destroy() {this.editor.event.off(EDITOR_EVENT.CONTENT_CHANGE, this.onContentChange);}private onContentChange (e: ContentChangeEvent) {if (!e.options.undoable) return void 0;this.redoStack [];const { previous, changes } e;const invert changes.invert(previous);if (invert) {this.temp.push(invert);if(!this.timer) {this.timer setTimeout(this.collectImmediately, this.DELAY);}}};后来我在思考一个问题如果这N毫秒内用户进行了Undo操作应该怎么办后来想想实际上很简单此时只需要清除定时器将暂存的Op[]立即放置于Redo Stack即可。 private collectImmediately () {if (!this.temp.length) return void 0;this.undoStack.push(this.temp);this.temp [];this.redoStack [];this.timer clearTimeout(this.timer);this.timer null;if (this.undoStack.length this.STACK_SIZE) this.undoStack.shift();};后边就是实际进行redo和undo的操作了只不过在这里批量操作是使用循环每个Op都需要单独Apply的这样感觉并不是很好毕竟需要修改多次虽然后边的渲染我只会进行一次批量渲染但是这里事件触发的次数有点多另外这里有个点还需要注意我们在History模块里进行的操作本身不应该再记入History中所以这里还有一个ApplyOptions的设置需要注意。此外在undo之后需要将这部分内容再次invert之后入redo stack反过来也是一样的此时我们直接取当前编辑器的DeltaSet即可。 public undo() {this.collectImmediately();if (!this.undoStack.length) return void 0;const ops this.undoStack.pop();if (!ops) return void 0;this.editor.canvas.mask.clearWithOp();this.redoStack.push(ops.map(op op.invert(this.editor.deltaSet)).filter(Boolean) as OpSetType[]);this.editor.logger.debug(UNDO, ops);ops.forEach(op this.editor.state.apply(op, { source: undo, undoable: false }));}public redo() {if (!this.redoStack.length) return void 0;const ops this.redoStack.pop();if (!ops) return void 0;this.editor.canvas.mask.clearWithOp();this.undoStack.push(ops.map(op op.invert(this.editor.deltaSet)).filter(Boolean) as OpSetType[]);this.editor.logger.debug(REDO, ops);ops.forEach(op this.editor.state.apply(op, { source: redo, undoable: false }));}最后 本文我们介绍总结了我们的图形编辑器中数据结构的设计以及History模块的实现虽然暂时不涉及到Canvas本身但是这都是作为编辑器本身的基础能力也是通用的能力可以学习。后边我们可以介绍的能力还有很多例如复制粘贴模块、画布分层、事件管理、无限画布、按需绘制、性能优化、焦点控制、参考线、富文本、快捷键、层级控制、渲染顺序、事件模拟、PDF排版等等整体来说还是比较有意思的欢迎关注我并留意后续的文章。
http://www.zqtcl.cn/news/229323/

相关文章:

  • 建站公司是什么郴州网站建设哪家做的好
  • 鞍山市住房和城乡建设网站网站几个数据库
  • 网站的内容建设安徽做网站
  • 有建网站的软件深圳专业做网站专业公司
  • 成都建设网站的公司汕尾海丰建设规划局网站
  • 南京cms建站企业网站的优化
  • 织梦网络设计工作室网站模板wordpress %postname%
  • 网站建设默认字体2020广东黄页
  • 金融电子商务网站建设深圳有什么公司名称
  • 网站设计 术语wordpress 图片弹出
  • 哪些域名不能够做淘宝客网站查建设公司年度保证金网站
  • 自己怎样用手机建网站网站优化 北京
  • 深圳小语种网站建设深圳做网站哪个平台好
  • 给个高质量的网站做网站优化有前景吗
  • 外贸网站 源怎么利用互联网平台赚钱
  • 营销型网站建设平台wordpress 添加 常规
  • php主做哪种类型网站高端公司小程序建设
  • 网站域名301是什么意思在一呼百应上做网站行吗
  • 怎么做百度口碑网站郑州网站设计专家
  • 珠海网络公司网站建设邯郸铸邯网络信息科技有限公
  • 室内设计者联盟官网哈尔滨百度搜索排名优化
  • 网站公司打电话来说做网站天下信息网
  • 汕头制作企业网站百度舆情监测平台
  • 怎样跟网站做优化呢火狐搜索引擎
  • 如何做网站的维护和推广水利网站建设管理汇报
  • 申请网站就是做网站吗怎样凡科建设网站
  • 怎样做吓人网站网页制作成品图
  • 前端的网站重构怎么做做网站用的编程语言
  • 长沙网站设计多少钱一个月百度网盘app下载安装电脑版
  • 你好南京网站网站开发 seo