vue+fabric.js实现简易的图文编辑器

1,706 阅读9分钟

前言

通过vue2和fabric.js实现一个简易的图文编辑器,可以在画布上添加文字,图片,设置背景图,对文字,图片的属性进行修改。最后生成图片。至于画布上对选中的对象进行拖动,缩放,旋转,这些能力fabric本身已经支持。

1 创建一个vue项目

2 安装fabric.js

建议使用4或5版本,最新版学习成本较高,相关经验文档少。

npm install fabric@4.6.0

3核心代码

页面基本结构

页面左侧为添加元素区域,可添加文字,图片等元素。中间为画布。右侧对选中的元素的属性进行修改。 具体代码可参考源码。项目中我用的node版本是18.17.1。

<div class="editor">
    <div class="sidebar left">
      <button @click="addText">添加文本</button>
      <button @click="addImage">添加图片</button>
      <button @click="setBackgroundImage">添加背景图</button>
    </div>

    <canvas id="c" width="600" height="600" class="canvas"></canvas>

    <div class="sidebar right">
      <!-- 右侧属性面板 -->
       <div v-if="selectedObject">
        属性修改...
       </div>
    </div>
</div>

image.png

初始化画布

首先确保 页面中已经有canvas标签。

data中定义需要用到到参数

data() {
    return {
      canvas: null,
      selectedObject: null, // 当前选中的元素对象
      canvasWidth: 800, // 初始画布宽度  
      canvasHeight: 600, // 初始画布高度
      canvasBackgroundColor: '#FFF', // 初始画布背景色
    };
},

mounted钩子函数中创建了Fabric.js画布并监听鼠标点击

  • 创建fabric画布,并指定背景色,大小。
  • 监听mouse:up事件,点击画布上的元素时,更新selectedObject,selectedObject对象表示当前选中元素的属性。
mounted() {
    this.$nextTick(() => {
      this.initCanvas();
    })
},

initCanvas() {
  // 创建画布
  this.canvas = new fabric.Canvas('c',{
    backgroundColor: this.canvasBackgroundColor,
    width: 800,
    height: 576,
  });
  
  // 监听点击
  this.canvas.on('mouse:up', (e) => {
    if (e.target) {
      this.selectedObject = e.target;
    } else {
      this.selectedObject = null
    }
  });
},

添加文本

addText() {
  const text = new fabric.IText('点击编辑', {
    left: 100,
    top: 100,
    fontSize: 30,
    fontFamily: 'arial', // 字体
    fill: '#333', // 颜色
    originX: 'left',
    originY: 'top',
    
  });
  this.canvas.add(text);
},

以上只添加了文本的基本属性,除此之外还有一些常用属性

  • editable:是否可编辑,值为布尔值;
  • lockUniScaling:控制四个正方向缩放,值为布尔值;
  • lockScalingX: 禁止横向缩放,值为布尔值;
  • lockScalingY: 禁止纵向缩放,值为布尔值;

同时还可以添加自定义的属性,例如我在添加文本元素时,自定义了属性system_name。

addText() {
  const text = new fabric.IText('当前日期', {
    ......
    system_name: 'current_date',
  });
  this.canvas.add(text);
},

添加图片

addImage() {
  fabric.Image.fromURL('图片url', (img) => {
    img.set({
      left: 100,
      top: 100,
      angle: 0,  // 你可以根据需要调整图片的旋转角度  
    });

    // 将图片添加到画布  
    this.canvas.add(img);

    // 重新渲染画布以显示新添加的图片  
    this.canvas.renderAll();
  }, { crossOrigin: 'anonymous' });
},

fabric.Image.fromURL('图片url', callback, options) 方法用于从指定的 URL 加载图片。这个方法接受三个参数。

  • '图片url':图片的 URL 地址。
  • callback(img):一个回调函数,当图片加载完成后执行。img 参数是加载后的 Fabric.js 图片对象。
  • options:一个对象,包含加载图片时的选项。在这个例子中,设置了 { crossOrigin: 'anonymous' },这允许跨域加载图片,避免在加载跨域图片时出现 CORS(跨源资源共享)错误。

如果我希望添加的图片不超出画布,同时居中。上述代码可以这样改进: 计算缩放因子scale并应用,保证图片最长的边不会超出画布。

 addImage() {
  fabric.Image.fromURL('图片url', (img) => {
    // 获取画布的宽高
    const canvasWidth = this.canvas.getWidth();
    const canvasHeight = this.canvas.getHeight();
    const maxWidth = canvasWidth * 0.6; // 计算图片允许的最大宽度  
    const maxHeight = canvasHeight * 0.3; // 计算图片允许的最大高度 

    // 计算缩放比例   
    let scale = Math.min(maxWidth / img.width, maxHeight / img.height);   

    // 应用缩放比例  
    img.scale(scale).set({
      left: (canvasWidth - img.width * scale) / 2, // 居中图片  
      top: (canvasHeight - img.height * scale) / 2,
    });

    // 将图片添加到画布  
    this.canvas.add(img);

    // 重新渲染画布以显示新添加的图片  
    this.canvas.renderAll();
  }, { crossOrigin: 'anonymous' });
},

设置背景图

设置背景图有两种实现方式,

  • 1 通过canvas.setBackgroundImage()设置;
  • 2 添加一个图片,宽高与画布大小一致,将其放在所有其他对象的底层,并禁止选中、触发事件。

我这里使用的是第二种方法。

setBackgroundImage(imageUrl) {
  fabric.Image.fromURL(imageUrl, (img) => {
    // 获取画布的宽度和高度  
    const canvasWidth = this.canvas.getWidth();  
    const canvasHeight = this.canvas.getHeight();  
    // 直接设置图像的宽度和高度为画布的宽度和高度  
    img.set({
      left: 0, 
      top: 0,
      scaleX: canvasWidth / img.width,
      scaleY: canvasHeight / img.height,  
      selectable: false, // 让背景图不可选  
      evented: false,     // 让背景图不触发事件  
      is_background: true // 自定义属性,背景图标识 区别于普通图片元素  
    }); 

    // 将背景图添加到画布上  
    this.canvas.add(img);
    // 将背景图放在所有其他对象的底层  
    this.canvas.sendToBack(img);
    // 重新渲染画布  
    this.canvas.renderAll();
  }, { crossOrigin: 'anonymous' });
},

上述方法可以实现设置背景图,但如果我已经设置了背景图,现在又想替换其他背景图时。由于新添加的背景图被放到了最底层,旧的背景图还没删除掉,旧图覆盖在新的背景图上,所以上述代码需要优化,

解决方法: 因为我在添加背景图时自定义了属性is_background,所以每次添加背景图先前遍历画布上的元素,如果is_background属性为true则删除它。然后再添加背景图。完整逻辑如下

setBackgroundImage(imageUrl) {
  fabric.Image.fromURL(imageUrl, (img) => {
    // 获取画布的宽度和高度  
    const canvasWidth = this.canvas.getWidth();  
    const canvasHeight = this.canvas.getHeight();  
    // 直接设置图像的宽度和高度为画布的宽度和高度  
    img.set({
      left: 0, 
      top: 0,
      scaleX: canvasWidth / img.width,
      scaleY: canvasHeight / img.height,  
      selectable: false, // 让背景图不可选  
      evented: false,     // 让背景图不触发事件  
      is_background: true // 背景图标识  
    }); 

    // 遍历画布上的所有对象,查找并删除已存在的背景图  
    this.canvas.getObjects().forEach((obj) => {  
      if (obj.is_background) {  
        this.canvas.remove(obj);  
      }  
    });
    // 将背景图添加到画布上  
    this.canvas.add(img);
    // 将背景图放在所有其他对象的底层  
    this.canvas.sendToBack(img);
    // 重新渲染画布  
    this.canvas.renderAll();
  }, { crossOrigin: 'anonymous' });
},

修改属性

选中元素时,selectedObject表示选中的对象,此时右侧显示相应的属性值修改框。 通过selectedObject.type区分文本或图片。i-text为文本,image为图片

文字常见属性修改:

  • 字体颜色:fill;
  • 字体大小:fontSize;
  • 字体粗细:fontWeight,常规为normal,加粗bold;
  • 字体风格:fontStyle,常规为normal,斜体italic;
  • 下划线:underline,布尔值;
  • 删除线:linethrough,布尔值;

image.png

<!-- 字体属性修改 -->
<div v-if="selectedObject.type === 'i-text'">
    <div class="style-title">颜色:</div>
    <el-color-picker style="width: 100%;" v-model="selectedObject.fill" @change="updateColor"></el-color-picker>

    <div class="style-title">字体大小:</div>
    <el-input-number  size="small" v-model="selectedObject.fontSize" controls-position="right" @change="canvasRender" :min="1"></el-input-number>
    <div class="style-title">常用属性:</div>
    <!-- 加粗,斜体,下划线,删除线 -->
    <div class="font-style">
      <i class="fa fa-bold" @click="updateTextProps('bold')"></i>
      <i class="fa fa-italic" @click="updateTextProps('italic')"></i>
      <i class="fa fa-underline" @click="updateTextProps('underline')"></i>
      <i class="fa fa-strikethrough" @click="updateTextProps('linethrough')"></i>
    </div>
</div>

......
methods: {
    // 修改颜色
    updateColor(newColor) {
      this.selectedObject.fill = newColor
      this.selectedObject.dirty = true;
      this.canvas.renderAll();
    },
    
    // 渲染画布
    canvasRender(e) {
      this.canvas.renderAll();
    },
    
    // 修改文字属性
    updateTextProps(type) {
      if(type == 'bold') {
      // 加粗
        this.selectedObject.fontWeight = this.selectedObject.fontWeight == 'normal' ? 'bold' : 'normal'
      }
      if(type == 'italic') {
      // 斜体
        this.selectedObject.fontStyle = this.selectedObject.fontStyle == 'normal' ? 'italic' : 'normal'
      }
      if(type == 'linethrough') {
      // 删除线
        this.selectedObject.linethrough = !this.selectedObject.linethrough
      }
      if(type == 'underline') {
      // 下划线
        this.selectedObject.underline = !this.selectedObject.underline
      }
      this.selectedObject.dirty = true;
      this.canvas.renderAll();
    },
}

图片常见属性修改

图片我主要做了尺寸的修改。 image.png 直接修改width,height并不能改变图片的大小。这是图片本身的物理大小,不能修改的。画布上展现的图片实际大小为物理大小*缩放比,例如宽默认为:width*scaleX,高为height*scaleY。修改图片尺寸,实际上就是修改缩放比scale。因此,在添加图片时,我需要为图片添加两个自定义属性,来表示图片在画布上的实际大小。添加自定义属性scaleWidth,scaleHeight,表示缩放后图片的实际大小。上述添加图片的方法可做以下优化:

addImage() {
  fabric.Image.fromURL('图片url', (img) => {
    ......
    img.scale(scale).set({
      ......
      scaleWidth: img.width * scale,
      scaleHeight: img.height * scale
    });
    ......
  }, { crossOrigin: 'anonymous' });
},

接下来,在右侧属性编辑区修改图片尺寸,实际上要根据修改后的尺寸scaleWidth去计算新的缩放比scaleX,height同理。然后更新图片属性的scaleX,scaleY属性即可。直接用鼠标拖拽图片的边去进行缩放也是在修改scaleX,scaleY。

<!-- 图片属性修改 -->
<div v-if="selectedObject.type === 'image'">
    <div class="style-title">尺寸:</div>  
    <div class="place-line">
      <el-input-number v-model="selectedObject.scaleWidth" size="small" placeholder="宽度" controls-position="right" @change="updateImgScale" :min="1" :max="1000" :precision="0" /> 
      <el-input-number v-model="selectedObject.scaleHeight" size="small" placeholder="高度" controls-position="right" @change="updateImgScale" :min="1" :max="1000" :precision="0" /> 
    </div>
</div>

methods: {
    // 缩放图片
    updateImgScale() { 
      const newScaleX = this.selectedObject.scaleWidth / this.selectedObject.width; 
      const newScaleY = this.selectedObject.scaleHeight / this.selectedObject.height; 
      this.selectedObject.set({
        scaleX: newScaleX,
        scaleY: newScaleY,
      })
      this.canvas.renderAll();
    },
}

删除

将当前选中的对象或对象的集合删除。我这里用到的是getActiveObjects,获取所有选中的对象,遍历并通过canvas.remove()全部删除,如果想获取单个对象,可以用this.canvas.getActiveObject()。

<i class="el-icon-delete" style="color: red;" @click="deleteSelectedObjects"> 
删除</i>
......

// 删除元素
deleteSelectedObjects() {
  const selectedObjects = this.canvas.getActiveObjects();
  if (selectedObjects.length > 0) {
    selectedObjects.forEach(obj => {
      this.canvas.remove(obj);
    });
    this.canvas.renderAll();
    // 清除 selectedObject 引用,如果你需要在其他地方使用它  
    this.selectedObject = null;
  }
},

生成JSON

image.png 将画布上的所有内容生成JSON文件。通过canvas.toJSON将画布上的图形对象转为JSON对象,生成的JSON对象默认情况下是包含对象的所有属性,但自定义属性我们需要手动指定。this.canvas.toJSON(['属性A', '属性B',...])

<el-button type="primary" size="small" @click="exportCanvasAsJSON">生成JSON</el-button>
......

exportCanvasAsJSON() {
  // 获取画布上所有对象的JSON表示  
  const jsonData = this.canvas.toJSON(['selectable', 'evented', 'is_background', 'scaleX', 'scaleY', 'scaleWidth', 'scaleHeight']); // 你可以根据需要包含或排除属性
  // 将JSON对象转换为字符串  
  const jsonString = JSON.stringify(jsonData, null, 2);
    
  // 以下是下载的逻辑=====================
  // 创建一个Blob对象  
  const blob = new Blob([jsonString], { type: 'text/json' });

  // 创建一个指向blob的URL  
  const url = window.URL.createObjectURL(blob);

  // 创建一个临时的a标签用于下载  
  const a = document.createElement('a');
  a.href = url;
  a.download = 'canvas_data.json'; // 指定下载的文件名  
  document.body.appendChild(a);
  a.click(); // 模拟点击以触发下载  

  // 清理  
  document.body.removeChild(a);
  window.URL.revokeObjectURL(url);
},

生成图片

通过canvas.toDataURL将画布内容导出为数据 URL,并指定为图像格式(如 PNG 或 JPEG)。

<el-button type="primary" size="small" @click="exportCanvasAsImage">下载图片</el-button>
......

exportCanvasAsImage() {
  // 设置图片的质量和格式,这里以PNG格式为例,质量为0.8  
  const imageUrl = this.canvas.toDataURL({  
      format: 'png',  
      quality: 0.8  
  });
  
  // 以下是下载的逻辑=====================
  // 创建一个指向该DataURL的a标签用于下载
  const a = document.createElement('a');
  a.href = imageUrl;
  a.download = 'canvas_image.png'; // 指定下载的文件名
  document.body.appendChild(a);
  a.click(); // 模拟点击以触发下载
  document.body.removeChild(a);
  window.URL.revokeObjectURL(imageUrl);
}

渲染JSON为图像

前面生成的JSON文件,我们通常会保存到本地或传给后端。一般二次编辑时,是需要回显画布的。回显画布可通过canvas.loadFromJSON(json, [callback])来实现。

  • json (String): 描述画布状态的 JSON 字符串。
  • callback (Function, 可选): 当 JSON 数据加载完成并渲染到画布上后调用的函数
mounted() {
    this.initData()
    // 如果是编辑时
    if(isEdit) {
      // 请求接口,或读取本地的JSON文件...
      // jsonData为需要渲染的JSON
      this.canvas.loadFromJSON(jsonData, this.canvas.renderAll.bind(this.canvas))
    }
},

总结

以上是我实现的一个基本的图文编辑器,全程基本依靠问AI以及一些大佬的帖子,可能会有错的不足的地方,欢迎大家指出。实现的功能并不多。因为我的业务上暂时用不到太多的功能哈哈。对于属性的修改也没有实现太多。另外像撤回,左对齐右对齐,缩放画布等,还有好多的功能待完善。后续如果有时间会一点点实现。