专栏名称: 奇舞精选
《奇舞精选》是由奇舞团维护的前端技术公众号。除周五外,每天向大家推荐一篇前端相关技术文章,每周五向大家推送汇总周刊内容。
目录
相关文章推荐
中科院物理所  ·  它从尿里被发现,却从小吓唬你到长大…… ·  昨天  
环球物理  ·  【物理动图】八年级下册物理动图合集 ·  2 天前  
环球物理  ·  【高中物理】高中物理太难学? ... ·  3 天前  
51好读  ›  专栏  ›  奇舞精选

初探海报编辑器

奇舞精选  · 公众号  ·  · 2025-01-21 18:00

正文

本文作者系360奇舞团前端开发工程师

最近在调研海报编辑器 想要做到对一张图片进行添加文本 图片之类的操作 我就去调研了一些开源的海报编辑器。其实在web上关于图片的编辑器还是很多的,种类也很丰富,比如 miniPaint基本复刻了 ps,基于 farbic.js的 Pintura.和 tui.image-editor,基于 Konva的 polotno等等。
那我们的现阶段是实现一个轻量级图文编辑器,实现一些特定的交互操作和属性配置就可以了,那我这里主要调研的是一个简单、功能齐全、插件化架构,适合二次开发网页版的海报编辑器。我调研了一些方案,最后选定了基于开源的海报编辑器讯排设计做二次开发,下面也会展示部分此项目中的代码。

github地址:https://github.com/palxiao/poster-design
分享给大家

捋捋功能

如何生产出海报展示给用户?

海报生产和制作,模板化,海报的绘制和渲染。
大致操作流程:
基于模板或上传底图,进行图文编辑,可添加图片和文字,文字可修改字体 颜色 大小。同时可控制元素的缩放旋转、层级移动、删除和复制。最后基于模板和元素,导出最终的图片。

海报内容元素抽象为两类:

基础元素:线条 几何图形 图片 文本等
业务元素:具有业务属性的元素 例如二维码 头像等等

模板与素材库:

编辑器内提供丰富的海报模板,涵盖不同行业、不同风格,用户可根据需求选择合适的模板作为基础。也可以自己上传PSD文件,等待解析完成后开始编辑。
素材库提供大量的图片、图标、背景等素材,用户可轻松添加到海报中,丰富视觉效果。
这里就涉及到一个模版的解析,也就是psd文件解析。
讯排的psd解析方案是基于开源的psd.js实现的。psd.js是一款Photoshop文件解析库,支持解析 photoshop cc2019 及更早版本的所有主要元素,包括图层,蒙版、文本、调整图层、形状等。支持NodeJS和浏览器环境。

文件解析:

psd.js通过创建一个树状结构来表示PSD文件中的图层和文件夹,从而解析PSD文件。它能够提取文档结构、尺寸、图层/文件夹的位置、名称、可见性、不透明度等重要数据.

图层数据处理:

psd.js允许开发者遍历PSD文件的图层树,逐个将图层数据转换为Fabric.js对象,实现图层数据的导入

跨平台兼容性:

作为一个纯JavaScript库,psd.js可以在浏览器环境和Node.js环境中运行,这意味着它可以无缝集成到Web应用程序或桌面应用程序中。

说明:

*psd.js 和 Fabric.js 可以结合使用:
psd.js负责解析PSD文件并提取图层数据,而Fabric.js则用于在网页上渲染和操作这些图层数据,两者结合可以实现从PSD设计到Web界面的转换。

核心编辑器模块:

功能上比如:复制粘贴元素、组合成组、拆组、添加文字、图片、拖拽、裁剪、快速改变层级等等。

  1. 利用HTML5, CSS3和JavaScript开发。特别是Canvas元素和一些canvas的扩展库例如fabricjs等,用于作为海报的画布。

  2. SVG & Canvas混合渲染:结合SVG的矢量图形优势和Canvas的像素级操作,保证了图像质量的同时,实现了丰富的动态效果

画布区域交互设计

  1. 拖拽:HTML5的Drag and Drop API,使得元素的添加与移动变得直观易用。也有很多封装的库可以用比如vuedraggable。就是mousedown、mousemove和mouseup事件的结合使用:在组件上按下鼠标后,记录组件当前位置,也就是x、y坐标(对应的是css中的left和top);每次鼠标移动时用当前最新的xy坐标减去最开始的xy坐标,计算出移动的距离,然后更新组件位置;鼠标抬起时结束移动。

  2. 工具栏:一些样式调整,元素的选择,模块切换等等

  3. 元素层级:展示当前画布内元素的层级信息,更加直观且方便快速调整。

  4. 放大缩小、撤销重做

数据结构

模版化的原理是定义了一套描述海报模板的DSL,渲染引擎可以渲染并解析DSL。

我们的内容元素和素材作为最小单位,用可配置化的数据结构来描述海报。在讯排中 是以JSON数据作为DSL描述海报,保存工程数据,将工程数据传入渲染内核就可以进行视图的预览、编辑、导出等操作。

图层管理:在这里,引入图层概念,创建Layer类管理单个元素的位置、大小等信息。后面我会重点说一下这个。层级数据格式案例:

数据&视图

数据和画布中的视图做了双向绑定,所以数据改变后只需要调用方法触发视图的更新。为了节约渲染开销,大部分都采用了手动触发的方式来通知视图的更新,做很多数据改动后才会去触发一次 update。

// 修改坐标
layer.x = 100;
layer.y = 200;

// 通知视图更新
store.update();

绘制 保存

方式有很多种 我这简单介绍一下。
前端:

  1. 使用HTML元素制作和渲染海报,由html2canvas转换为图片,开发效率很高,但是清晰度很低而且性能有一些差。

  2. 使用Canvas开发绘制海报,由Canvas生成图片,但是存在一定的性能瓶颈,比如在客户端,弱网环境 / 低性能安卓机上耗时较长。

服务端:

  1. 用Puppeteer 或 Phantomjs截图生成图片:先把海报在网页中渲染出来,对页面进行截图

  2. 用node-canvas绘制并导出图片 :先把海报在网页中渲染出来,将网页转换成canvas后再导出图片。

在讯排中:

  1. 在服务端,他们使用 puppeteer 启动无头浏览器,利用 Chrome 打开绘制页,并往其 BOM 中注入广播通知方法。

  2. 在绘制页面请求保存页面中元素集合的json信息,将页面呈现出来。通过一系列方法判断元素是否加载完成,

  3. 一旦整个页面以及资源都加载完成则调用 window 下的广播通知开始截图,这样就完成了整个图片生成操作的闭环。

*Puppeteer 是一个 Node 库,它提供了一个高级的 API 来通过 DevTools 协议控制 Chrome 或 Chromium。Puppeteer 默认以无头模式运行,但也可以配置为运行“有头”模式。它常用于网页自动化测试、生成页面截图或PDF、爬取 SPA(单页应用)并生成预渲染内容等。

扩展与集成:

  1. 集成图片处理库:如使用Apache Commons Imaging进行图片处理,实现图片的裁剪、旋转、滤镜等效果

  2. 插件化架构:如果按照硬编码的方式实现,那么组件将会错综复杂,相互紧密耦合,修改一个微小功能都要梳理众多组件的影响范围。有可能出现一些问题比如:导入逻辑出现过多业务处理代码。多个组件的订阅事件出现相互依赖。也是我们未来会重点扩展的方向。

  3. 社交媒体分享:集成小红书、微信,等社交媒体API,实现一键分享海报到社交媒体。

到这里基本就是现在市面上的海报编辑器的重点功能了。

图层管理

下面我给大家介绍其中的图层管理这个点。图层面板主要是控制组件的显示/隐藏、不同组件的层级关系以及点击选中。

在canvas中:

canvas 元素提供了一个绘图环境,但是它本身并没有提供方法来控制绘图元素的层级顺序。

在canvas中,默认「后创建的对象」在z轴上高于「先创建的对象」。通常,这意味着你需要在每次重绘画布时,按照正确的顺序重新绘制每个图形元素。
例如,你可能会先绘制位于底部的元素,然后是中间的元素,最后是顶部的元素。这样,最后绘制的元素会覆盖先前绘制的元素,从而在视觉上创建了层级效果。

一般情况下,我们不会一开始就想好所有对象的创建顺序,然后依次创建它们。所以需要灵活得调整对象之间的层叠顺序。

如果他要实现层级,需要自己管理绘图顺序。这时候可以选择一些框架协助做这件事。

Fabric.js,是一个可以让 HTML5 Canvas 开发变得简单的框架 。它是一种基于 Canvas 元素的 可交互 对象模型,也是一个 SVG 到 Canvas 的解析器(让SVG 渲染到 Canvas 上)。另外,提供了许多额外的功能来处理图形对象,包括层级控制。

fabric提供了一些方法使我们可以方便的更改元素的层级:

toTop: 置于顶层,调用canvas的bringToFront方法
up: 向上一层,调用canvas的bringForward方法
down: 向下一层,调用canvas的sendBackwards方法
toBottom: 置于底层,调用canvas的sendToBack方法

但是我发现讯排没有使用canvas和fabric去操作。那他是如何做的呢?

正常情况下,我们会选择使用z-index来控制层级。但是我发现他并没有使用z-index,而是利用了层叠领域黄金准则的第二条。

层叠领域黄金准则:
1、谁大谁上:当具有明显的层叠水平标示的时候,如识别的z-indx值,在同一个层叠上下文领域,层叠水平值大的那一个覆盖小的那一个。
2、后来居上:当元素的层叠水平一致、层叠顺序相同的时候,在DOM流中处于后面的元素会覆盖前面的元素。

这样做的好处就是不仅操作方便,也不用增加额外冗余代码。就是计算起来我感觉还挺麻烦的。

我看他的具体实现

他创建Layer类管理单个元素的位置、大小等信息,并使用列表管理所有图层,通知所有注册的监听器改变。

Layer类通常是一个在图形编辑软件中用来表示图层的对象。在海报编辑器这样的应用中,Layer类可以封装一个图层的所有属性和行为,使得图层的管理变得更加简单和直观。

使用 Layer 类:

  1. 定义Layer类
    首先,你需要定义Layer类,包括它的属性和方法。这通常在你的项目中的一个单独的文件或模块里完成。

// Layer.js
class Layer {
constructor(id, name) {
this.id = id;
this.name = name; //名称
this.visible = true; //是否展示
this.opacity = 1; //透明度
this.position = { x: 0, y: 0 }; //位置
this.size = { width: 0, height: 0 }; //大小
this.rotation = 0; //旋转
this.elements = []; //包含元素
}
}
  1. 创建Layer实例
    当需要一个新的图层时,创建Layer类的实例。

// main.js
import { Layer } from './Layer.js';

const backgroundLayer = new Layer('layer1', 'Background');
const foregroundLayer = new Layer('layer2', 'Foreground');
  1. 管理图层
    在你的编辑器或应用中,你可能需要一个管理器来处理所有图层的逻辑,比如添加、删除、重排序图层等。

// LayerManager.js
class LayerManager {
constructor() {
this.layers = [];
}

addLayer(layer) {
this.layers.push(layer);
}

removeLayer(layerId) {
this.layers = this.layers.filter(layer => layer.id !== layerId);
}

// 其他管理方法...
}
  1. 渲染图层
    你需要一个渲染函数来将图层绘制到画布上。这个函数会遍历所有图层,并根据每个图层的属性来绘制它们的内容。

// Renderer.js
function renderLayer(layer, context) {
if (!layer.visible) return;

context.save();
context.globalAlpha = layer.opacity;
context.translate(layer.position.x, layer.position.y);
context.rotate(layer.rotation * Math.PI / 180);

layer.elements.forEach(element => {
// 根据元素类型绘制元素
});

context.restore();
}
//渲染全部的图层
function renderAllLayers(layers, canvasContext) {
layers.forEach(layer => {
renderLayer(layer, canvasContext);
});
}
  1. 与用户界面交互
    在你的用户界面中,你需要提供工具让用户能够与图层交互,比如拖动图层来改变顺序,或者改变图层的属性。








// main.js
document.getElementById('canvas').addEventListener('click', (event) => {
// 根据点击事件处理图层交互
});

// 假设你有一个函数来更新画布
function updateCanvas() {
const canvas = document.getElementById('canvas');
const context = canvas.getContext('2d');
renderAllLayers(layerManager.layers, context);
}
  1. 保存和加载图层状态
    你可能需要保存和加载图层的状态,以便用户可以保存他们的工作并在以后加载。

// 保存图层状态
function saveLayers() {
const layersState = layerManager.layers.map(layer => ({
id: layer.id,
name: layer.name,
visible: layer.visible,
opacity: layer.opacity,
position: { x: layer.position.x, y: layer.position.y },
size: { width: layer.size.width, height: layer.size.height },
rotation: layer.rotation,
elements: layer.elements.map(element => ({ /* 元素的保存逻辑 */ }))
}));
// 将layersState保存到本地存储或服务器
}

// 加载图层状态
function loadLayers() {
// 从本地存储或服务器加载layersState
const layersState = /* 加载逻辑 */;
layersState.forEach(layerState => {
const layer = new Layer(layerState.id, layerState.name);






请到「今天看啥」查看全文