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

基于 Konva 实现Web PPT 编辑器(一)

前言

        目前Web PPT编辑比较好的库有PPTist(PPTist体验地址),是基于DOM 的渲染方案,相比 Canvas 渲染的方案,在复杂场景下性能会存在一定的差距。不过确实已经很不错了,本应用在一些实现思路、难点攻克上也参考了pptist的思想,使用konva进行搭建,大家自行查看哈~

        Konva 是一个HTML5 Canvas JavaScript 框架,它通过对 2d context 的扩展实现了在桌面端和移动端的可交互性。Konva 提供了高性能的动画,补间,节点嵌套,布局,滤镜,缓存,事件绑定(桌面/移动端)等等功能,本应用基于 konva 实现Web PPT 编辑器,以实现设计编辑、预览、切换、动画等核心功能,下图给出系统架构图(参考实现了Canvas-Editor优秀的架构思路):

核心对象

class Pptx {public command: Command;public eventBus: EventBus<EventBusMap>;public register: Register;public destroy: () => void;constructor(options: IPptxOptions) {// 创建 eventbusthis.eventBus = new EventBus<EventBusMap>();// 创建 drawconst draw = new Draw({ ...options, eventbus: this.eventBus });// 创建 commandthis.command = new Command(new CommandAdapt(draw));// 创建快捷键const shortcut = new ShortCut(this.command);// 提供用户注册方法this.register = new Register({ shortcut });// 提供 destroy 方法this.destroy = () => {shortcut.removeEvent();draw.destroy();};}
}

        如上架构图,通过 const pptx = new Pptx({...}),获取操作对象,可调用 command API 实现对数据的获取、操作指令等,通过 eventbus 实现对事件的监听、register 注册快捷键等。

页面布局

         用户提供的 container,需要在内部构建TopMenu、SlidePreview、FooterMenu及KonvaBox等结构,创建 konva时,注意宽高比保持 16:9 :

    // 处理宽高(始终保持 16:9 即可)const { width, height } = getKonvaBoxSize(konvaBox);const stageOption = { container: konvaBox, width, height };this.stage = new Konva.Stage(stageOption);// 创建默认的幻灯片提示const group = new Konva.Group({ x: 0, y: 0 });const { width, height } = this.stage.size();const { rectoption, textOption } = getDefaultSlideOptions(width, height);// 创建矩形const rect = new Konva.Rect({ ...rectoption, stroke });// 创建文字const text = new Konva.Text({ ...textOption, fill });

 Konva 基类设计

        为了满足页面设计中的文本编辑、拖拽缩放等多场景需求,因此,需要重写Konva图形基类,以满足统一的事件处理(就是给每一个基类都添加Group)。

        如上,我们大致采用 layer - group - shape(layer - group - group - shape)的模式,每一个原件都会包裹一个 Group ,通过Group进行统一的事件处理,讲解下大致的原因哈:

 const rect1 = this.konvaGraph.Rect({x: 10,y: 10,width: 100,height: 100,fill: "red",draggable: true,});

这里我们创建了一个可拖拽的矩形,双击的时候添加了一个文本:

graph.on("dblclick", (e) => {const group = e.target.parent;const text = new Konva.Text({ text: "13" });group!.add(text);});

如果我们采用单独的处理,则会出现如下情况(矩形拖动而文本不跟随):

因此,我们将公共的事件处理,统一封装为 group 即可。

const group = new Konva.Group({ draggable: true });

文本输入

        konva 自身是通过创建 text area实现的:

因此,我们在创建框架结构的时候,就创建一个 contenteditable,避免重复的DOM 操作(不用textarea有自己的考虑哈);

    // 这里还需要创建一个 contenteditable box// 多创建一个 div 是为了实现水平垂直居中哈const textareaBox = document.createElement("div");textareaBox.className = "konva-root-middle-textareaBox";const textarea = document.createElement("div");textarea.className = "konva-root-middle-textareaBox-textarea";textarea.setAttribute("contenteditable", "true");textareaBox.appendChild(textarea);const konvaSelector = ".konvajs-content";const konvaBoxParent = <HTMLDivElement>rootBox.querySelector(konvaSelector);konvaBoxParent.appendChild(textareaBox);

图片处理

        konva 的图片创建是基于 Image.onload 事件实现的,我们需要按照这个思路,进行统一处理,同时,还将支持 File | Blob | URL 的图片类型:

  // Image 图片public Image(payload: IKonvaImage) {return new Promise<Konva.Group>(async (resolve) => {// 解析图片资源 File、Blob 均创建 FileReader 读取,string 则默认urlconst source = await getImageSource(payload.source);const image = new Image();image.src = source;// 图片的处理需要基于 image.onload 事件回调image.onload = () => {const { width, height } = image;/*** 解析 payload 中的参数对象,* 判断 x,y,width,height,* 后面的参数会直接覆盖前面,不需要 || 判断* 注意参数的顺序!后面的覆盖前面的,因此,Image x,y 都应该是0** */const groupOption = { x: 0, y: 0, width, height };const group = this.getGroup({ ...groupOption, ...payload });const result = new Konva.Image({width,height,...payload,image,x: 0,y: 0,});this.overwriteGraph(group);group.add(result);resolve(group);};});}

具体用法如下:


/** 重写 konva image 参数 */
export type IKonvaImage = {source: string | File | Blob; // 图片来源
} & Konva.ImageConfig;// File 类型
const input = document.createElement("input");
input.type = "file";
input.setAttribute("id", "file");
document.querySelector("body")?.appendChild(input);
input.onchange = async (e: Event) => {const source = (e.target as HTMLInputElement).files![0];const image = await this.konvaGraph.Image({source,image: undefined,});console.log(image);
};// URL 类型
const image = await this.konvaGraph.Image({x: 100,y: 100,width: 100,height: 100,image: new Image(), // 需要是 Konva.ImageConfig 的类型 CanvasImageSource( HTMLOrSVGImageElement | HTMLVideoElement | HTMLCanvasElement | ImageBitmap | OffscreenCanvas | VideoFrame) | undefined source:"https://www.baidu.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf.png",
});

 图层管理

        图层管理的核心,就是对当前layer的管理,创建幻灯片之前,需要将上一个幻灯片的layer 进行缓存,同时,创建当前幻灯片的时候,也要将当前的图层进行缓存;在图形更新之后,调用 render,重新渲染更新图层信息:

private layer!: Konva.Layer; // 始终指向当前编辑区/** 添加幻灯片 */public addSlide() {// 都需要处理背景颜色// 创建新幻灯片之前,需要进行图层管理this.layerManager.cacheLayer();this.clearLayer();// 始终指向当前编辑器layerthis.layer = new Konva.Layer({ id: getUniqueId() });const { width, height } = this.stage.size();const slideOption = getSlideOptions(width, height);const rect = new Konva.Rect({ ...slideOption });this.layer.add(rect);this.stage.add(this.layer);this.render(); // 这个render是缓存当前图层}

         创建专门的 layerManager 进行管理,包括缓存、更新、删除、上一个、下一个、指定某一个、获取全部等方法(代码有点多哈,不粘出来了)


/*** 图层管理器 - layerList :Konva.Layer[]*  1. 添加图层*    1.1 新建图层后 addSlide*    1.2 更新元素后 render*    1.3 新建另一个图层前 addSlide*  2. 通过绑定唯一的 layer ID 识别图层*    2.1 如果添加已经存在的图层,则更新图层*    2.2 如果添加不存在的图层,则添加图层*  3. 切换至指定图层*    3.1 先清空所有图层*    3.2 将指定图层添加至 stage*    3.3 重新渲染 stage(重新渲染会重新更新图层)*/
export class LayerManager {private layerList: Konva.Layer[] = [];}

实现预览 

        预览的核心,就是创建一个 与stage 宽高一致的全屏元素,创建新的 stage ,以图层列表依次进行展示即可:

  /** 预览 - 通过 layerManage 实现 */public preview(mode?: PreviewMode) {// 如果mode存在,则更新modemode && this.setPreviewMode(mode);// 创建容器const previewBox = document.createElement("div");previewBox.className = "konva-root-preview";document.querySelector("body")?.appendChild(previewBox);// 设置与stage一致的宽高const { width, height } = this.stage.size();previewBox.style.width = width + "px";previewBox.style.height = height + "px";// 进入全屏previewBox.requestFullscreen();}export function fullscreenchange(e: Event, draw: Draw) {// 监听全屏事件const previewBoxSelector = ".konva-root-preview";const previewBox = <HTMLDivElement>document.querySelector(previewBoxSelector);if (document.fullscreenElement && previewBox) {// 如果处于全屏状态,并且全屏元素存在,才能执行预览操作const layerList = draw.getLayerManager().getLayerList();// 创建新的 stageconst stage = new Konva.Stage({container: previewBox,width: window.innerWidth,height: window.innerHeight,});stage.add(layerList[0]);} else {// 退出全屏后,删除元素previewBox?.remove();// 恢复默认预览模式draw.setPreviewMode(PreviewMode.start);}
}

        因为 原来的 layer 是与原来的stage的宽高保持一致的,但是现在全屏预览后,尺寸肯定是比原来的大,因此,需要将 layer 等比放大,需要计算缩放比例:

    const { innerWidth, innerHeight } = window; // 全屏后最佳的预览尺寸const { width, height } = draw.getStage().size();const scaleX = innerWidth / width;const scaleY = innerHeight / height;const scale = Math.min(scaleX, scaleY);// 被预览元素与全屏最优尺寸一致const endWidth = scale * width;const endHeight = scale * height;previewBox.style.width = endWidth + "px";previewBox.style.height = endHeight + "px";// 创建新的 stageconst stage = new Konva.Stage({container: previewBox,width: endWidth,height: endHeight,});// 为了达到最优的效果,采用最小的缩放比例const layerList = draw.getLayerManager().getLayerList();const layer = layerList[0].clone().scale({ x: scale, y: scale });stage.add(layer);

        预览期间是不能移动元素的哈, 直接 layer.children.forEach(group=>group.draggable=false)即可,这也是我们重写 Konva.Node 的好处。

总结

        初步实现了元素添加、拖拽、缩放,搭建了基本的框架结构,实现了基本的编辑预览功能,下一篇我们重点讲述动画系统实现等其他功能点哈~

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • ORB-SLAM3(Failed to load image)问题解决(WSL2配置)
  • 电脑版视频剪辑软件哪个好?适合新手使用的剪辑软件!
  • 贪心算法介绍(Greedy Algorithm)
  • enhanced Input Action IA_Look中Action value引脚没有分割结构体引脚的选项
  • Repeat方法:取模运算教材与Unity控制台输出数值不同的原因
  • Linux 基本指令讲解 上
  • 详解Redis 高可用的方式 Redis Cluster
  • 【Hugging Face】 Hugging Face 公司和 Hugging Face 网站介绍
  • C#中常用的扩展类
  • 行业大模型:信用评分大模型、生产优化大模型、库存管理大模型、物流行业大模型、零售行业大模型
  • 财务会计与管理会计(四)
  • 【C++ 面试 - 基础题】每日 3 题(十九)
  • 【AI】智力即服务
  • 【Redis集群】集群原理最全解析
  • C++——list列表容器经典案例——手机按销量降序排列,若销量相同则按价格降序排列
  • Bytom交易说明(账户管理模式)
  • Docker下部署自己的LNMP工作环境
  • ES6, React, Redux, Webpack写的一个爬 GitHub 的网页
  • Facebook AccountKit 接入的坑点
  • Gradle 5.0 正式版发布
  • Java应用性能调优
  • LeetCode18.四数之和 JavaScript
  • log4j2输出到kafka
  • macOS 中 shell 创建文件夹及文件并 VS Code 打开
  • nginx 负载服务器优化
  • python3 使用 asyncio 代替线程
  • webgl (原生)基础入门指南【一】
  • 实习面试笔记
  • 网页视频流m3u8/ts视频下载
  • 用Node EJS写一个爬虫脚本每天定时给心爱的她发一封暖心邮件
  • 最近的计划
  • Play Store发现SimBad恶意软件,1.5亿Android用户成受害者 ...
  • ​​​​​​​​​​​​​​汽车网络信息安全分析方法论
  • ​sqlite3 --- SQLite 数据库 DB-API 2.0 接口模块​
  • ​如何在iOS手机上查看应用日志
  • (Git) gitignore基础使用
  • (编程语言界的丐帮 C#).NET MD5 HASH 哈希 加密 与JAVA 互通
  • (二)什么是Vite——Vite 和 Webpack 区别(冷启动)
  • (十二)springboot实战——SSE服务推送事件案例实现
  • (十三)Java springcloud B2B2C o2o多用户商城 springcloud架构 - SSO单点登录之OAuth2.0 根据token获取用户信息(4)...
  • *** 2003
  • .Net - 类的介绍
  • .NET 漏洞分析 | 某ERP系统存在SQL注入
  • .NET 设计一套高性能的弱事件机制
  • .net分布式压力测试工具(Beetle.DT)
  • .net使用excel的cells对象没有value方法——学习.net的Excel工作表问题
  • @for /l %i in (1,1,10) do md %i 批处理自动建立目录
  • @media screen 针对不同移动设备
  • @拔赤:Web前端开发十日谈
  • [000-01-022].第03节:RabbitMQ环境搭建
  • [20190416]完善shared latch测试脚本2.txt
  • [C#]C# winform实现imagecaption图像生成描述图文描述生成
  • [C++] 模拟实现list(二)
  • [Godot] 3D拾取
  • [IntelliJ IDEA插件]推荐一款简单方便的插件CodeChrono