基于 Fabric.js 实现一个简易画板

最近这段时间闲着没事做,就打算做一个简易编辑器来玩。

本文对于创建项目的过程就不再赘述,直接进入主题。

前言

本项目基于 Vue3 + fabric.js 技术栈开发,没有使用样式框架,图标库是 IconPark

此处附上 fabric.js 官网:fabric.js 中文教程

js 复制代码
// fabric.js
pnpm i fabric
// icon-park 图标库
pnpm i @icon-park/vue-next 
// 其他声明
pnpm i @types/fabric -D

在画板上添加文字或图形

定义 Sketchpad 画板类

Sketchpad 类用于存储画板实例对象、画板的相关属性和操作方法等。

首先,在入口文件 Index.vue(下文会给出代码)中创建一个 canvas 容器,并设置它的 id 属性。接下来,我们将生成的画板实例绑定到已创建的 canvas 容器上。

Sketchpad 类的构造函数 constructor 接受一个参数,就是用于绑定 canvas 容器的 id 标识。目前为止,需要实现的部分代码如下:

ts 复制代码
// Sketchpad.ts
import { Canvas } from 'fabric/fabric-impl';

class Sketchpad {
  public instance: Canvas; // canvas 实例

  private canvaProps = {
    width: 1000, // 默认宽度
    height: 720, // 默认高度
    fill: '#f6f6f6', // 默认填充色
    zoom: 1, // 缩放比例
    backgroundVpt: false, // 背景受视图缩放影响
    isDrawingMode: false, // 禁止使用画笔涂鸦模式
    preserveObjectStacking: true, // 元素被选中时保持原有层级
  };

  constructor(cvsId: string) {
    this.instance = new fabric.Canvas(cvsId, this.canvaProps); // 创建 canvas 实例
  }
}

图形列表的定义和渲染

由于可以组合、生成多种图形,我们将配置集成到文件 shapeList.tsx 中,以便后续的修改和管理。

tsx 复制代码
// shapeList.tsx
// 忽略图标的引入,可以按需在官网中搜索、替换

export type ShapeType = 'Textbox' | 'Line' | 'ArrowLeft' | 'ArrowRight' | 'ArrowUp' | 'ArrowDown' | 'Rect' | 'Square' | 'Triangle' | 'RightTriangle' | 'Circle' | 'Ellipse' | 'Parallel' | 'Diamond' | 'Polygon';

export interface ShapeProps {
  type: ShapeType; // 图形类型
  element: JSX.Element; // 图形对应的图标
}

export const shapeList: Array<ShapeProps> = [
  {
    type: 'Textbox',
    element: <FontSize />,
  },
  {
    type: 'Line',
    element: <Line />,
  },
  {
    type: 'ArrowLeft',
    element: <ArrowLeft />,
  },
  {
    type: 'ArrowRight',
    element: <ArrowRight />,
  },
  {
    type: 'ArrowUp',
    element: <ArrowUp />,
  },
  {
    type: 'ArrowDown',
    element: <ArrowDown />,
  },
  {
    type: 'Rect',
    element: <Rectangle />,
  },
  {
    type: 'Square',
    element: <Square />,
  },
  {
    type: 'Triangle',
    element: <Triangle />,
  },
  {
    type: 'RightTriangle',
    element: <RightTriangle />,
  },
  {
    type: 'Circle',
    element: <Round />,
  },
  {
    type: 'Ellipse',
    element: <Ellipse />,
  },
  {
    type: 'Parallel',
    element: <Parallel />,
  },
  {
    type: 'Diamond',
    element: <Diamond />,
  },
  {
    type: 'Polygon',
    element: <Polygon />,
  },
];

接下来,在图形面板 Shape 组件中导入并渲染 shapeList 列表。

ts 复制代码
// Shape.vue
<template>
  <div class="title">基础图形</div>
  <div class="icon-list">
    <div class="icon" v-for="item in shapeList" :key="item.type" @click="onInsertShape(item.type)">
      <component :is="item.element" />
    </div>
  </div>
</template>

<script setup lang="ts">
import { inject } from 'vue';
import { shapeList } from '@/const/shapeList';

const onInsertShape = inject('onInsertShape') as Function;
</script>

页面布局的设计

Index.vue 文件是本项目的入口文件,其中使用左右布局:侧边栏 Siderbar 组件固定宽度,右边容器自适应宽度,填充满剩余页面。上述提到的图形面板 Shape 组件将嵌入侧边栏。

Index.vueSidebar.vue 文件的代码分别如下:

ts 复制代码
// Index.vue
<template>
  <Sidebar />
  <main class="cvs-container">
    <canvas id="cvs" />
  </main>
</template>

<script setup lang="ts">
import { onMounted, reactive, provide } from 'vue';
import Sidebar from '@/components/Sidebar.vue';
import Sketchpad from '@/common/sketchpad';

let sketchpad: Sketchpad;

onMounted(() => {
  sketchpad = new Sketchpad('cvs');
  provide('sketchpad', reactive(sketchpad)); // 响应式传递
});

const onInsertShape = (shape: ShapeType) => {
  sketchpad.insertShape(shape);
};

provide('onInsertShape', onInsertShape); // 传递给子孙组件
</script>

<style lang="scss" scoped>
.cvs-container {
  width: calc(100vw - 350px);
  height: 100vh;
  display: grid;
  place-items: center;
  overflow: scroll;

  #cvs {
    width: 1000px;
    height: 720px;
    background-color: var(--bgColor);
    border: 2px solid var(--shadowColor);
  }
}
</style>
ts 复制代码
// Sidebar.vue
<template>
  <aside class="sidebar">
    <main class="content">
      <Shape />
    </main>
  </aside>
</template>

<script setup lang="ts">
import Shape from '@/components/panel/Shape.vue';
</script>

<style lang="scss" scoped>
.sidebar {
  width: 320px;
  height: calc(100vh - 50px);
  background-color: var(--bgColor);
  box-shadow: 0 0 15px 0 var(--borderColor);
  font-size: 14px;
  overflow: hidden;
  display: flex;
  position: relative;
}

.content {
  flex: 1;
  padding: 15px;
  overflow-y: scroll;
}
</style>

我们可以在 Index.vue 文件中看到,实际上发挥作用的是 sketchpad.insertShape(shape) 方法。所以,接下来切回 Sketchpad.ts 文件中,完善这个函数的实现。

为了方便后续的修改和优化,我们将生成图形的核心逻辑抽取出来,单独创建一个 createShape 函数。

ts 复制代码
// Sketchpad.ts
import { Canvas, Object } from 'fabric/fabric-impl';
import { createShape } from '@/utils/createShape';

interface Instance extends Object {
  [key: string]: any; // 兼容自定义属性
}

class Sketchpad {
  // ...

  insertShape(type: ShapeType) {
    const shape: Instance = createShape(type);
    shape.top = 150;
    shape.left = 150;
    
    this.instance.add(shape).renderAll(); // 添加到画板上并重新渲染画板
  }
}

核心逻辑

createShape 函数的功能:根据传入的 ShapeType 类型,在画板上添加一个对应的新实例。

ts 复制代码
import { ShapeType } from '@/const/shapeList';

export const createShape = (shape: ShapeType) => {
  switch (shape) {
    // 文本
    case 'Textbox': {
      return new fabric.Textbox('编辑文本', {
        width: 170,
        fontSize: 24,
        splitByGrapheme: true,
      });
    }
    // 线条
    case 'Line': {
      return new fabric.Path('M 0 0 L 200 0', {
        strokeWidth: 1.5,
        stroke: '#3c3c3c',
      });
    }
    // 左箭头
    case 'ArrowLeft': {
      const triangle = new fabric.Triangle({
        width: 15,
        height: 15,
        fill: '#3c3c3c',
      });
      const rect = new fabric.Rect({
        top: 55,
        width: 2,
        height: 100,
        fill: '#3c3c3c',
      });
      return new fabric.Group([triangle, rect], {
        top: 50,
        left: 150,
        angle: 270,
      });
    }
    // 右箭头
    case 'ArrowRight': {
      const triangle = new fabric.Triangle({
        width: 15,
        height: 15,
        fill: '#3c3c3c',
      });
      const rect = new fabric.Rect({
        top: 55,
        width: 2,
        height: 100,
        fill: '#3c3c3c',
      });
      return new fabric.Group([triangle, rect], {
        top: 50,
        left: 150,
        angle: 90,
      });
    }
    // 上箭头
    case 'ArrowUp': {
      const triangle = new fabric.Triangle({
        width: 15,
        height: 15,
        fill: '#3c3c3c',
      });
      const rect = new fabric.Rect({
        top: 55,
        width: 2,
        height: 100,
        fill: '#3c3c3c',
      });
      return new fabric.Group([triangle, rect], {
        top: 50,
        left: 150,
        angle: 360,
      });
    }
    // 下箭头
    case 'ArrowDown': {
      const triangle = new fabric.Triangle({
        width: 15,
        height: 15,
        fill: '#3c3c3c',
      });
      const rect = new fabric.Rect({
        top: 55,
        width: 2,
        height: 100,
        fill: '#3c3c3c',
      });
      return new fabric.Group([triangle, rect], {
        top: 50,
        left: 150,
        angle: 180,
      });
    }
    // 矩形
    case 'Rect': {
      return new fabric.Rect({
        width: 150,
        height: 100,
        fill: '#ffa727',
      });
    }
    // 正方形
    case 'Square': {
      return new fabric.Rect({
        width: 150,
        height: 150,
        fill: '#ffa727',
      });
    }
    // ...
  }
};

到此,将图形或文本添加到画板上的核心函数已经实现,看一下实际效果如何:

相关推荐
我要洋人死10 分钟前
导航栏及下拉菜单的实现
前端·css·css3
科技探秘人22 分钟前
Chrome与火狐哪个浏览器的隐私追踪功能更好
前端·chrome
科技探秘人22 分钟前
Chrome与傲游浏览器性能与功能的深度对比
前端·chrome
JerryXZR28 分钟前
前端开发中ES6的技术细节二
前端·javascript·es6
七星静香30 分钟前
laravel chunkById 分块查询 使用时的问题
java·前端·laravel
q24985969333 分钟前
前端预览word、excel、ppt
前端·word·excel
小华同学ai38 分钟前
wflow-web:开源啦 ,高仿钉钉、飞书、企业微信的审批流程设计器,轻松打造属于你的工作流设计器
前端·钉钉·飞书
Gavin_9151 小时前
【JavaScript】模块化开发
前端·javascript·vue.js
懒大王爱吃狼2 小时前
Python教程:python枚举类定义和使用
开发语言·前端·javascript·python·python基础·python编程·python书籍
逐·風6 小时前
unity关于自定义渲染、内存管理、性能调优、复杂物理模拟、并行计算以及插件开发
前端·unity·c#