使用 Fabric.js 构建一个在线白板组件(支持绘图 / 拖拽 / 导出)

发布于:2025-04-08 ⋅ 阅读:(26) ⋅ 点赞:(0)

一、前言

在线白板在教育、远程协作、设计等场景中越来越常见。如果用原生 Canvas 来实现白板,绘图、交互、变换、导出等功能实现起来会非常复杂,而 Fabric.js 提供了一个强大的封装库,大幅降低了开发成本。

本篇文章将基于 Vue 3 + Fabric.js 实现一个功能完整的白板组件,支持:

  • ✏️ 手绘线条
  • 🔲 拖拽图形(矩形 / 圆形)
  • 📦 添加文字
  • 💾 导出画布为图片
  • ♻️ 清空画布
  • ↩️ 撤销 / 重做
  • 🔒 锁定图形
  • 🎨 颜色 / 线宽自定义
  • 📋 复制 / 粘贴 / 删除图形

二、技术栈

  • Vue 3 + <script setup>:组件式开发体验
  • Fabric.js:Canvas 图形绘制封装
  • Vite:快速构建工具

三、项目初始化

npm create vite@latest vue-whiteboard --template vue
cd vue-whiteboard
npm install
npm install fabric

四、创建基础白板组件

4.1 Whiteboard.vue 基础结构

<template>
  <div class="toolbar">
    <button @click="drawLine">画线</button>
    <button @click="addRect">矩形</button>
    <button @click="addCircle">圆形</button>
    <button @click="addText">文字</button>
    <button @click="exportImage">导出</button>
    <button @click="clearCanvas">清空</button>
  </div>
  <canvas id="canvas" width="1000" height="600"></canvas>
</template>

<script setup>
import { onMounted, ref } from 'vue';
import { fabric } from 'fabric';

let canvas = null;
const isDrawing = ref(false);

onMounted(() => {
  canvas = new fabric.Canvas('canvas', {
    isDrawingMode: false,
    backgroundColor: '#ffffff'
  });
});
</script>

4.2 绘图和形状功能

function drawLine() {
  canvas.isDrawingMode = true;
}

function addRect() {
  const rect = new fabric.Rect({
    left: 100,
    top: 100,
    fill: 'skyblue',
    width: 100,
    height: 60
  });
  canvas.add(rect);
  canvas.isDrawingMode = false;
}

function addCircle() {
  const circle = new fabric.Circle({
    left: 200,
    top: 200,
    fill: 'lightgreen',
    radius: 40
  });
  canvas.add(circle);
  canvas.isDrawingMode = false;
}

function addText() {
  const text = new fabric.IText('请输入文字', {
    left: 300,
    top: 300,
    fontSize: 24,
    fill: '#333'
  });
  canvas.add(text);
  canvas.isDrawingMode = false;
}

function exportImage() {
  const dataURL = canvas.toDataURL({
    format: 'png',
    quality: 1
  });
  const link = document.createElement('a');
  link.href = dataURL;
  link.download = 'whiteboard.png';
  link.click();
}

function clearCanvas() {
  canvas.clear();
  canvas.setBackgroundColor('#ffffff', () => {});
}

五、功能增强

5.1 撤销 / 重做

const undoStack = [];
const redoStack = [];

function saveState() {
  redoStack.length = 0;
  const json = canvas.toJSON();
  undoStack.push(json);
}

canvas.on('object:added', saveState);
canvas.on('object:modified', saveState);
canvas.on('object:removed', saveState);

function undo() {
  if (undoStack.length > 0) {
    const last = undoStack.pop();
    redoStack.push(canvas.toJSON());
    canvas.loadFromJSON(last, () => canvas.renderAll());
  }
}

function redo() {
  if (redoStack.length > 0) {
    const next = redoStack.pop();
    undoStack.push(canvas.toJSON());
    canvas.loadFromJSON(next, () => canvas.renderAll());
  }
}

5.2 删除选中对象

function deleteActiveObject() {
  const active = canvas.getActiveObject();
  if (active) {
    canvas.remove(active);
  }
}

window.addEventListener('keydown', (e) => {
  if (e.key === 'Delete') {
    deleteActiveObject();
  }
});

5.3 复制 / 粘贴图形

let clipboard = null;

function copy() {
  const active = canvas.getActiveObject();
  if (active) {
    active.clone((cloned) => {
      clipboard = cloned;
    });
  }
}

function paste() {
  if (clipboard) {
    clipboard.clone((clonedObj) => {
      canvas.discardActiveObject();
      clonedObj.set({
        left: clonedObj.left + 20,
        top: clonedObj.top + 20
      });
      canvas.add(clonedObj);
      canvas.setActiveObject(clonedObj);
      canvas.requestRenderAll();
    });
  }
}

window.addEventListener('keydown', (e) => {
  if (e.ctrlKey && e.key === 'c') copy();
  if (e.ctrlKey && e.key === 'v') paste();
});

5.4 自定义线条颜色和宽度

<div class="toolbar">
  <input type="color" v-model="lineColor" />
  <input type="range" v-model="lineWidth" min="1" max="20" />
</div>
const lineColor = ref('#000000');
const lineWidth = ref(2);

watch([lineColor, lineWidth], () => {
  canvas.freeDrawingBrush.color = lineColor.value;
  canvas.freeDrawingBrush.width = lineWidth.value;
});

onMounted(() => {
  canvas.freeDrawingBrush.color = lineColor.value;
  canvas.freeDrawingBrush.width = lineWidth.value;
});

5.5 图形锁定 / 解锁

function toggleLock() {
  const active = canvas.getActiveObject();
  if (active) {
    const locked = !active.lockMovementX;
    active.set({
      lockMovementX: locked,
      lockMovementY: locked,
      hasControls: !locked,
      selectable: !locked
    });
    canvas.requestRenderAll();
  }
}

六、样式优化

<style scoped>
.toolbar {
  margin-bottom: 10px;
}
button {
  margin-right: 8px;
  padding: 6px 12px;
}
canvas {
  border: 1px solid #ccc;
}
</style>

七、页面使用示例

<template>
  <Whiteboard />
</template>

<script setup>
import Whiteboard from './components/Whiteboard.vue';
</script>

八、总结与拓展方向

我们基于 Vue 3 和 Fabric.js 构建了一个功能完整的在线白板组件,涵盖了从基本的绘图到高阶的图层操作、撤销重做、导出等常见需求。

如果你希望把它进一步升级成生产级组件,可以继续扩展以下方向:

  • 🧠 协同绘图:结合 WebSocket 实现多人实时操作同步
  • 🧩 图层管理面板:可见性、锁定、命名等功能
  • 🧲 对齐吸附 / 网格背景
  • ☁️ 白板 JSON 文件存储 / 导入
  • 🖼️ 拖入图片编辑支持

Fabric.js 是一款非常适合前端开发者构建图形编辑器的工具,配合 Vue 等现代框架可以开发出媲美专业工具的应用。


到这里,这篇文章就和大家说再见啦!我的主页里还藏着很多 篇 前端 实战干货,感兴趣的话可以点击头像看看,说不定能找到你需要的解决方案~
创作这篇内容花了很多的功夫。如果它帮你解决了问题,或者带来了启发,欢迎:
点个赞❤️ 让更多人看到优质内容
关注「前端极客探险家」🚀 每周解锁新技巧
收藏文章⭐️ 方便随时查阅
📢 特别提醒:
转载请注明原文链接,商业合作请私信联系
感谢你的阅读!我们下篇文章再见~ 💕

在这里插入图片描述


网站公告

今日签到

点亮在社区的每一天
去签到