前端canvas项目实战——在线图文编辑器(六):加粗、斜体、下划线、删除线(上)

前端canvas项目实战——在线图文编辑器(六):加粗、斜体、下划线、删除线(上)

码农世界 2024-05-29 前端 84 次浏览 0个评论

目录

  • 前言
  • 一、效果展示
  • 二、实现步骤
    • 1. 视图部分:实现用于切换字体属性的按钮
    • 2. 逻辑部分:点击按钮之后要做什么?
    • 3. 根据Textbox的属性实时更新按钮的状态
    • 三、Show u the code
    • 后记

      前言

      上一篇博文中,我们实现了对文字的字体、字号和行间距的编辑。

      这篇博文是《前端canvas项目实战——简历制作网站》付费专栏系列博文的第六篇——加粗、斜体、下划线、删除线,主要的内容有:

      1. 针对文本框(fabric.Textbox)对象: 扩充属性列表,使用户可以为画布中选中的文本框设置加粗、斜体、下划线和删除线。

      如有需要,你可以:

      • 点击这里,阅读序文《前端canvas项目实战——在线图文编辑器:序》
      • 点击这里,返回上一篇《前端canvas项目实战——在线图文编辑器(五):字体、字号、行间距》
      • 点击这里,前往下一篇《前端canvas项目实战——在线图文编辑器(七):加粗、斜体、下划线、删除线(下)》

      一、效果展示

      • 动手体验

        CodeSandbox会自动对代码进行编译,并提供地址以供体验代码效果

        由于CSDN的链接跳转有问题,会导致页面无法工作,请复制以下链接在浏览器打开:

        https://63djym.csb.app/

      • 动态效果演示

        • 本节之后,我们的简历能做成什么样子

          我们可以对部分文字设置加粗,将标题或重要文字和其他部分区别开来,使面试官可以快速定位到重点信息。


          二、实现步骤

          1. 视图部分:实现用于切换字体属性的按钮

          在这里,我把这种按钮命名为SwitchValueButton:

          import "./index.css";
          import React from "react";
          import { Button, Tooltip } from "antd";
          const SwitchValueButton = (props) => {
            let { handleClick, tip, style, className, children } = props;
            let _props = {
              type: "text",
              className: className || "property-operation",
              onClick: (e) => handleClick(e),
              style: {
                ...style,
                height: "40px",
                padding: "0.5rem 0.75rem",
              },
            };
            
            return (
              tip}>
                
              
            );
          };
          export default SwitchValueButton;
          

          代码很简单,实现了一个带有图标的按钮。因为并不是每个用户都可以一看到图标就明白这个按钮的作用,所以还为按钮加上了提示。 即当鼠标的光标移动到按钮上时,按钮上方会弹出对应的Tips,表明这个按钮的作用。

          有了这个基础的按钮组件,相信大家实现一组 4 个按钮也没有什么困难。

          2. 逻辑部分:点击按钮之后要做什么?

          这里的代码和上一节有部分交集,但仍有很多改动。为了便于说明,仍列出了部分在上节出现过的代码。

          const updateFontProperty = (key, newValue) => {
            let { activeObject, canvas } = store.getState();
            if (_shouldUpdateTheWholeTextbox(activeObject, key)) {
              _updateFontPropertyForWholeObject(key, newValue);
            } else {
              _updateFontPropertyForSelection(key, newValue);
            }
            canvas.renderAll();
          };
          const _shouldUpdateTheWholeTextbox = (object, key) => {
            let { text, isEditing, selectionStart, selectionEnd } = object;
            let isSelectAll = text.length === selectionEnd - selectionStart;
            // Bug点1:容易忘记考虑编辑状态时,选中了全部文字
            if (!isEditing || isSelectAll) {
              return true;
            }
            return key === "lineHeight";
          };
          const _updateFontPropertyForWholeObject = (key, newValue) => {
            let { activeObject } = store.getState();
            // Bug点2:由于部分文字的样式优先级高于整个文本框的样式,先清除整个文本框的对应属性
            activeObject.removeStyle(key);
            newValue = _getNewValueForWholeObject(key, newValue);
            ObjectAPI.updateProperty(activeObject, key, newValue);
          };
          const _updateFontPropertyForSelection = (key, newValue) => {
            let { activeObject, canvas } = store.getState();
            let { selectionStart, selectionEnd } = activeObject;
            // Bug点3:编辑态时,如果没有选中任何文字,不做任何处理
            if (selectionEnd === selectionStart) {
              return;
            }
            newValue = _getNewValueForSelection(key, newValue);
            let style = {};
            style[key] = newValue;
            activeObject.setSelectionStyles(style);
            // 优化点:删除冗余的数据
            activeObject.minifySelectionStyles();
          };
          const _getNewValueForWholeObject = (key, value) => {
            let { activeObject } = store.getState();
            if (defaultValues.hasOwnProperty(key)) {
              value = defaultValues[key].init;
              if (activeObject[key] === value) {
                value = defaultValues[key].target;
              }
            }
            return value;
          };
          const _getNewValueForSelection = (key, value) => {
            let { activeObject } = store.getState();
            if (defaultValues.hasOwnProperty(key)) {
              let { init, target } = defaultValues[key];
              let selectionStyles = activeObject.getSelectionStyles();
              let valueOfFirstChar = selectionStyles[0][key];
              if (valueOfFirstChar === undefined) {
                value = activeObject[key] === init ? target : init;
              } else {
                value = activeObject[key];
              }
            }
            return value;
          };
          

          一共6个方法,分为3个部分来讲解:

          • 入口方法及分支判断逻辑
            • updateFontProperty方法: 作为入口方法,本节实现的加粗、斜体、下划线、删除线等4个按钮的点击事件都直接指向它,例如为一个Textbox添加下划线:handleClick={() => updateFontProperty("underline", true)}
            • _shouldUpdateTheWholeTextbox方法: 分支判断方法,判断当前应该为整个Textbox设置新的属性值,还是仅为用户选中的部分文字设置局部的属性。
            • 分支1:为整个Textbox设置新的属性值
              • _updateFontPropertyForWholeObject方法: 通过下面的_getNewValueForWholeObject方法获取到新的属性值后,将新值直接设置给Textbox对象。
              • _getNewValueForWholeObject方法: 获取新值。
                • 如果属性为颜色等用户从下拉菜单中选择的值,直接返回;
                • 如果属性为加粗、斜体、下划线、删除线这类点击了按钮进行切换的值,则返回取反后的值。如下划线,当前值为true则返回false,当前值为false则返回true。
                • 分支2:为用户选中的部分文字设置新的属性值
                  • _updateFontPropertyForSelection方法: 通过下面的_getNewValueForSelection方法获取到新的属性值后,通过setSelectionStyles方法为选中的文字设置新的属性值。
                  • _getNewValueForSelection方法: 以选中的文字的第一个字符的属性为准获取新值,取值的逻辑和上述的_getNewValueForWholeObject类似。

                    代码注释中的Bug点和优化点在下文中会详细介绍。

                    3. 根据Textbox的属性实时更新按钮的状态

                    前面两个小节中的代码组合起来,实现了通过点击按钮来更新Textbox的属性值。现在我们反过来,将Textbox的属性状态体现在这些按钮上。先通过下面的动图来看看我们要实现的效果:

                    可以看到,按钮的颜色会根据我们选中的文字属性实时变化。这样就完成了Textbox属性和按钮状态的双向链接。以下是代码:

                    fabric.Textbox.prototype.initialize = (function (fn) {
                      return function (text, options, id) {
                        fn.call(this, text, options, id);
                        
                        this.on("selection:changed", function () {
                          store.dispatch(actions.updateActiveObject(this));
                        });
                      };
                    })(fabric.Textbox.prototype.initialize);
                    const updateActiveObject = (newActiveObject) => {
                      newProperties = {
                        ...
                        fontWeight: newActiveObject.fontWeight),
                        fontStyle: newActiveObject.fontStyle),
                        underline: newActiveObject.underline),
                        linethrough: newActiveObject.linethrough"),
                      };
                      return {
                        ...
                      	activeObjectProperties: newProperties
                      };
                    }
                    const getIconColor = (key) => {
                      let { activeObjectProperties } = store.getState();
                      let value = activeObjectProperties[key];
                      let initValue = defaultValues[key].init;
                      if (value === initValue || value === undefined) {
                        return "#000000";
                      }
                      return "dodgerblue";
                    };
                    

                    为了便于说明,这里将整个流程简化为3个方法进行介绍。如需查看完整代码,请留意文末的「Show u the code」部分。

                    • 覆盖fabric原型代码: 为了在用户选择的文字更改时,可以立即更新按钮状态,这里覆盖了fabric.Textbox原型里的initialize方法,这个方法用于创建并初始化Textbox原型。我们的改动是:
                      • 当初始化结束后,通过Textbox.on("selection:changed")为其添加一个时间监听器,监听selection:change事件。
                      • 当代码中通过Textbox.fire("selection:changed")「引发」 一个selection:change事件时,监听器会立即执行立即执行我们设置的「回调方法」。
                      • 在回调方法中,我们向中央数据仓库store分发一个数据更新任务,要求通过updateActiveObject方法更新画布中当前选中的对象。
                      • updateActiveObject方法: 根据参数中新的被选中对象,更新activeObjectProperties字典。
                        • 注意: 为了便于说明,这个方法中的代码有简化,实际获取属性的方法要比直接newActiveObject.xxx要复杂一些,具体见后文中的源代码。
                        • getIconColor方法: 通过中央数据仓库中最新的activeObjectProperties字典,判断按钮中的图标应该是用什么颜色。
                          • 如果是初始值(如下划线underline的初始值为false,目标值为true),则设置为黑色#000000
                          • 否则设置为一种浅蓝色dodgerblue,上文的动图中有显示。

                            这样,一排美观、精致、且逻辑完整属性按钮就实现了


                            三、Show u the code

                            按照惯例,本节的完整代码我也托管在了CodeSandbox中,点击前往,查看完整代码


                            后记

                            这篇博文中,我们实现了用按钮设置文本框的加粗、斜体、下划线、删除线。

                            起初觉得并不复杂的几个功能,在实现中也涌现出了一个又一个的问题。本来想一篇博文写完,但限于篇幅,拆分到两篇博文中。

                            上述在实现过程中发现的Bug及其解决方法、需要优化的点以及是如何优化的,我都放在了下一篇博文中。相信通过动手解决这些实现过程中遇到的,真实的阻碍和问题,我们会走上学习和成长的快速车道。

                            如有需要,你可以:

                            • 点击这里,阅读序文《前端canvas项目实战——在线图文编辑器:序》
                            • 点击这里,返回上一篇《前端canvas项目实战——在线图文编辑器(五):字体、字号、行间距》
                            • 点击这里,前往下一篇《前端canvas项目实战——在线图文编辑器(七):加粗、斜体、下划线、删除线(下)》

转载请注明来自码农世界,本文标题:《前端canvas项目实战——在线图文编辑器(六):加粗、斜体、下划线、删除线(上)》

百度分享代码,如果开启HTTPS请参考李洋个人博客
每一天,每一秒,你所做的决定都会改变你的人生!

发表评论

快捷回复:

评论列表 (暂无评论,84人围观)参与讨论

还没有评论,来说两句吧...

Top