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

news2024/11/13 16:17:01

前言

        目前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) {
    // 创建 eventbus
    this.eventBus = new EventBus<EventBusMap>();

    // 创建 draw
    const draw = new Draw({ ...options, eventbus: this.eventBus });

    // 创建 command
    this.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 则默认url
      const 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();

    // 始终指向当前编辑器layer
    this.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存在,则更新mode
    mode && 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();
    // 创建新的 stage
    const 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";

    // 创建新的 stage
    const 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 的好处。

总结

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

1:html的介绍与基础1

目录 1.1html的介绍 1.2html的基础1 1.2.1标题&#xff0c;头部与基本的格式怎么写 1.2.1.1标题与基本格式 1.2.1.2头部 1.2.2段落 1.2.3链接 1.2.3.1基本的网页链接 1.2.3.2图像链接 1.2.4注释 1.1html的介绍 HTML是一种标记语言&#xff0c;用于创建&#xff0c;设…

EmguCV学习笔记 VB.Net和C# 下的OpenCv开发

版权声明&#xff1a;本文为博主原创文章&#xff0c;转载请在显著位置标明本文出处以及作者网名&#xff0c;未经作者允许不得用于商业目的。 笔者的博客网址&#xff1a;https://blog.csdn.net/uruseibest 本教程将分为VB.Net和C#两个版本分别进行发布。 教程VB.net版本请…

高效同步与处理:ADTF流服务在自动驾驶数采中的应用

目录 一、ADTF 流服务 1、流服务源&#xff08;Streaming Source&#xff09; 2、流服务汇&#xff08;Streaming Sink&#xff09; 二、数据链路 1、数据管道&#xff08;Data Pipe&#xff09; 2、子流&#xff08;Substreams&#xff09; 3、触发管道&#xff08;Tri…

遥感之常用各种指数总结大全

目前在遥感领域基本各种研究领域都会用到各种各样的指数&#xff0c;如水体指数&#xff0c;植被指数&#xff0c;农业长势指数&#xff0c;盐分指数&#xff0c;云指数&#xff0c;阴影指数&#xff0c;建筑物指数&#xff0c;水质指数&#xff0c;干旱指数等等众多。 本文对上…

Qt第十五章 动画和状态机

文章目录 动画框架动画架构动画框架类QPropertyAnimation串行动画组QSequentialAnimationGroup并行动画组QPararallelAnimationGroupQPauseAnimationQTimeLine窗口动画下坠效果抖动效果透明效果 状态机QStateQStateMachine 动画框架 动画架构 动画框架类 类名描述QAbstractAn…

字符串金额转换,字符串手机号屏蔽,身份证信息查看,敏感词替换

2135 在发票上面该写成零佰零拾零万贰仟壹佰叁拾伍元 我们用逆推法可以写成零零零贰壹叁伍->贰壹叁伍->2135 1.遍历获取到每一个数字&#xff0c;然后把大写放到数组里面&#xff0c;将数字当作索引&#xff0c;在数组里面查找大写 package stringdemo;import java.uti…

传输层安全性 ——TLS(Transport Layer Security)简介

TLS(Transport Layer Security)是一种广泛使用的安全协议,旨在确保互联网通信的隐私性和数据完整性。它是SSL(Secure Sockets Layer)的继任者,最初版本于1999年发布,最新版本是TLS 1.3。 TLS 握手为每个通信会话建立一个密码套件密码套件是一组算法,其中指定了一些细节…

如何轻松获取麒麟操作系统架构信息?

如何轻松获取麒麟操作系统架构信息&#xff1f; 一、使用uname -a命令二、用arch命令三、示例输出 &#x1f496;The Begin&#x1f496;点点关注&#xff0c;收藏不迷路&#x1f496; 在使用麒麟操作系统&#xff08;Kylin OS&#xff09;时&#xff0c;了解系统的架构信息对于…

stm32单片机学习 - stm32 的命名规则

STM32命名规则: 以STM 32 F 103 C 8 T 6 A xxx为例:

动手学深度学习(pytorch)学习记录9-图像分类数据集之Fashion-MNIST[学习记录]

注&#xff1a;本代码在jupyter notebook上运行 封面图片来源 Fashion-MNIST是一个广泛使用的图像数据集&#xff0c;主要用于机器学习算法的基准测试&#xff0c;特别是图像分类和识别任务。Fashion-MNIST由德国的时尚科技公司Zalando旗下的研究部门提供。作为MNIST手写数字集…

Java并发类API——CompletionService

CompletionService 是 Java 中 java.util.concurrent 包的一部分&#xff0c;用于管理并发任务的执行&#xff0c;并以完成的顺序提供结果。它结合了线程池和阻塞队列的功能&#xff0c;用于提交任务并按照任务完成的顺序来检索结果&#xff0c;而不是按照任务提交的顺序。 接…

uni-app--》打造个性化壁纸预览应用平台(二)

&#x1f3d9;️作者简介&#xff1a;大家好&#xff0c;我是亦世凡华、渴望知识储备自己的一名前端工程师 &#x1f304;个人主页&#xff1a;亦世凡华、 &#x1f306;系列专栏&#xff1a;uni-app &#x1f307;座右铭&#xff1a;人生亦可燃烧&#xff0c;亦可腐败&#xf…

python语言day7 函数式编程 面向对象编程

Java 函数式编程_java函数式编程-CSDN博客 25.Java函数式编程-CSDN博客 函数式编程&#xff1a; 通过调用函数send_email()&#xff0c;完成业务需求。将具体的业务需求封装成一个函数这样的一种解决问题的思想称它为函数式编程。 在java中本来没有函数的概念&#xff0c;因为…

指针详解

目录 1. 内存 2. 编址​编辑 3. 指针变量和地址 1&#xff09;取地址操作符&#xff08;&&#xff09; 2&#xff09;指针变量 3&#xff09;指针类型 4&#xff09;解引用操作符 4. 指针变量的大小 5. 指针变量类型的意义 1&#xff09;指针的解引用 6. 指针 -…

Java使用XXL-Job-Admin创建和管理调度任务的指南

文章目录 一、调度中心添加任务的基本方法二、配置文件中的任务配置三、创建并调用调度任务的客户端四、配置RestTemplate 总结 在日常开发中&#xff0c;我们经常需要处理各种定时任务&#xff0c;而XXL-Job作为一款强大的分布式任务调度平台&#xff0c;为我们提供了简单易用…

进程与线程(6)

有名管道&#xff1a; 目录 有名管道&#xff1a; 1.创建&#xff08;mkfifo&#xff09;&#xff1a; 2。打开&#xff08;open&#xff09;&#xff1a; 3.读写&#xff08;read /write&#xff09;&#xff1a; 4.关闭&#xff08;close&#xff09;&#xff1a; 5.卸…

presto高级用法(grouping、grouping sets)

目录 准备工作&#xff1a; 在hive中建表 在presto中计算 分解式 按照城市分组 统计人数 按照性别分组 统计人数 ​编辑 按照爱好分组 统计人数 ​编辑 按照城市和性别分组 统计人数 按照城市和爱好分组 统计人数 按照性别和爱好分组 统计人数 按照城市和性别还有…

【Qt开发】创建并打开子窗口(QWidget)的注意事项 禁止其他窗口点击、隐藏窗口、子窗口不退出的配置和解决方案

【Qt开发】创建并打开子窗口&#xff08;QWidget&#xff09;的注意事项 禁止其他窗口点击、隐藏窗口、子窗口不退出的配置和解决方案 文章目录 新建QWidget测试注意事项不要用多线程方式运行子窗口不要在打开子窗口后用阻塞死等不要用临时变量定义子窗口 禁止其他窗口的点击隐…

【Qt】常用控件QPushButton

常用控件QPushButton QWidget中涉及的各种属性/函数/使用方法&#xff0c;对Qt中的各种控件都是有效的。 QPushButton继承自QAbstractButton。这个类是抽象类&#xff0c;是其他按钮的父类。 QAbstractButton中和QPushButton相关性比较大的属性。 属性说明 text 按钮中的⽂本…

Vue中下载内容为word文档

1.使用 html-docx-js&#xff1a;这是一个将 HTML 转换为 Word 文档的库。 2. 利用 Blob 和 FileSaver.js&#xff1a;创建并下载生成的 Word 文档。 在 Vue.js 中实现步骤如下: 1. npm 安装 html-docx-js 和 file-saver npm install html-docx-js npm install file-saver2.…