WebP Cloud Services Blog

水印新功能上线以及 bug 修复

· Tuki Deng

由 Fabric 实现的水印编辑器上线有一段时间了(详见「使用 Fabric.js 实现实时水印预览」)。上一版的代码以及调研工作都是 Benny 同学完成滴,作为 WebP Cloud Services 的前端开发,现在才开始学习 Fabric,真是惭愧惭愧 🤪。

在此期间也发现了一些 bug,同时又了一些新的想法。因此,我们把水印功能进行了一些改进,让水印功能看上去更加优雅且可用性更好。

新功能

这次更新总共有三个新功能,分别是:

  • 增加可编辑和宽度调整功能
  • 预设水印位置
  • 不同尺寸图片的水印效果图预览

页面效果大致如下图,是不是看上去大致上没啥变化呢?(雾

画布中文字增加编辑和宽度调整功能

之前的版本中,只能通过画布左侧的 config 来设置文字的内容,不能直接在画布中编辑。这次的改动增加了可编辑的功能,双击画布中文字后,可以直接进行编辑。 需要注意的是,由于 Fabric 的 TextBox 支持换行,所以当文字编辑完成后,需要再次点击画布空白处,才能保存编辑的内容。

增加了可编辑功能之后,这个时候发现了 Fabric 的第一个坑:文本框的宽度它居然变长了之后不能变短了~,且这个时候设置 width 属性也不生效。所以我去问了 ChatGPT:

好吧,那就改为文字的宽度也应该支持自定义吧!所以我们增加了左右两个控制点,让用户可以自己调节宽度。

代码层面上,也做了一些重构和优化:

  • canvas 实例绑定的事件从 object:scalingobject:moving 改为 mouse:up,由此来减少事件触发的频率,事件的触发需要直到用户操作完成(松开鼠标)。并判断宽高和偏移量是否有变化,如果有变化再进行后续步骤。

    this.canvas.on("mouse:up", this.handleObject.bind(this));
    
  • 将 canvas 实例从 Fabric.Text 改为 Fabric.Textbox, 这样可以支持文字的换行;并设置 editable: true,这样就可以在画布中编辑文字。

    const textObject = new Fabric.Textbox(text, {
      editable: true,
      // 初始化文字框
    });
    
    textObject.setControlsVisibility({
      // 隐藏控制点
    });
    
  • textbox 实例绑定了 editing:exited 事件,当内容编辑完成后触发。

    textObject.on("editing:exited", this.afterEditText.bind(this));
    this.canvas.add(textObject);
    
  • 减少 textbox 实例赋值的次数,将多个属性的赋值合并为一次。

    const updateProperties: Record<string, string | number> = {};
    
    keys.forEach((key) => {
      updateProperties[key] = value;
    });
    
    textObject.set(updateProperties);
    this.canvas.renderAll();
    

添加预设水印位置

根据大多数人使用水印的习惯(包括我们在内),我们增加了一些预设的水印位置,方便用户快速选择。这些预设位置包括左上、右上、左下、右下、中间等,可以根据自己的需求选择。点击按钮能直接将水印移动到对应的位置,且会请求预览图片,方便查看效果。

添加不同尺寸图片的水印效果图预览

给出水印画布的比例是 1:1,但是实际使用中,可能会有不同尺寸的图片,所以我们增加了不同尺寸图片的水印效果图预览。在预览图片中,可以看到不同尺寸的图片的水印效果。

bug 修复

水印位置超出画布边界没有恢复到画布中

水印超出边界的情况目前考虑到两种场景

  • 拖动文本框至边界外:这种场景下,需要将文本框恢复到画布中,根据实际拖动的位置和角度,调整文本框至边界为 0 的偏移量的位置。

    const boundingRect = textObject.getBoundingRect();
    const canvasWidth = this.canvas.width;
    const canvasHeight = this.canvas.height;
    
    const { left = 0, top = 0, width = 0, height = 0 } = boundingRect;
    
    // 检查并调整左边界
    if (left < 0) {
      textObject.left = 0;
    }
    // 检查并调整上边界
    if (top < 0) {
      textObject.top = 0;
    }
    // 检查并调整右边界
    if (left + width > canvasWidth) {
      textObject.left = canvasWidth - width;
    }
    // 检查并调整下边界
    if (top + height > canvasHeight) {
      textObject.top = canvasHeight - height;
    }
    
  • 缩放文本框宽/高度超出画布:这种场景下,需要根据画布的宽高,调整文本框的宽高,使其最大宽高在画布范围内。

    本以为一切顺利进行时,却发现了 Fabric 的第二个坑:Fabric 的 Textbox 有个特性,缩放文本框之后,文本框实例(也就是上面代码中的 textObject)的宽高不会更新,只有 scaleX/scaleY 的值会发生变化。

    那么要怎么取得当前的宽高的信息呢?还好 Fabric 提供了 getBoundingRect 给我们使用,这个方法会返回当前文本框的宽高和偏移量信息。

    由此可见,当控制文本框的宽高范围时,不能只简单的设置 widht/height 的值,而是需要更改 scaleX/scaleY(并且这俩值还得一样,不然字体就要变瘦/变胖咯)。同时还需要控制 left/top 的值,让文本框不要飘出画布外了。

    否则就会出现奇怪的现象:当缩放超出画布边界后,文本框的宽度甚至比缩放结束时更大了!原因自然是因为其宽度的计算是 width * scaleX,而不是 width 本身。

    const maxWidth = this.canvas.width,
      maxHeight = this.canvas.height;
    
    const { width, height } = boundingRect;
    
    // 检查并设置最大宽度
    if (width >= maxWidth) {
      if (!textObject.width) return;
      const max_scale = maxWidth / textObject.width;
      textObject.scaleX = max_scale;
      textObject.scaleY = max_scale;
      textObject.left = 0;
    }
    // 检查并设置最大高度
    if (height >= maxHeight) {
      if (!textObject.height) return;
      const max_scale = maxHeight / textObject.height;
      textObject.scaleX = max_scale;
      textObject.scaleY = max_scale;
      textObject.top = 0;
    }
    

优化

输入框增加 debounce,减少请求次数

之前线上的版本没有做输入框的防抖处理,导致每次输入都会请求一次新的预览图片,这样会导致请求次数过多,影响性能。所以我们在输入框中增加了防抖处理(现在是 500ms 发一次请求),减少请求次数。

    import { Subject, debounceTime } from 'rxjs';

    private searchSubject: Subject<string> = new Subject();

    ngOnInit(): void {
        this.searchSubject.pipe(debounceTime(500)).subscribe(() => {
            // 请求预览图片
        });
    }
    requestPreview() {
        this.searchSubject.next();
    }

画布和预览图片背景图片修改

之前的纯色图片由于带有颜色,文字颜色和背景色可能会与图片融合,导致水印效果不明显。所以我们将背景图片修改为通用的透明背景素材。

列表页面布局优化

现有的水印列表页面能看到所有的相关内容,展示的排列显得杂乱无章。所以我们重新设计和排版了列表页面,使其更加清晰。 因为列表页会展示每一个水印的预览图片,所以选择将图片放大,能一目了然的看到不同水印的效果;同时,为了更好的展示效果,我们将细节信息隐藏,在鼠标悬停时再展示。

水印字体展示优化

之前的版本中,我们没有选择提前下载所有的字体文件,而是在每次选择不同字体后,下载对应的字体文件,其实这样做的目的也是为了减少带宽消耗,毕竟所有的字体文件加起来体积很大。但是这样做的问题是,每次首次选择不同字体后,都会有一段时间的等待(因为没有缓存,并且我们设置了最长字体加载时间 10s),除了体验不好以外,还可能超时,最后字体可能并不会更新。 所以我们选择了不在画布中展示字体,而是在请求服务端生成好的预览图片中展示。


WebP Cloud Services 团队是一个来自上海和赫尔辛堡的三人小团队,由于我们不融资,且没有盈利压力 ,所以我们会坚持做我们认为正确的事情,力求在我们的资源和能力允许范围内尽量把事情做到最好, 同时也会在不影响对外提供的服务的情况下整更多的活,并在我们产品上实践各种新奇的东西。

如果你觉得我们的这个服务有意思或者对我们服务感兴趣,欢迎登录 WebP Cloud Dashboard 来体验,如果你好奇它还有哪些神奇的功能,可以来看看我们的文档 WebP Cloud Services Docs,希望大家玩的开心~


Discuss on Hacker News