基于 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',
      });
    }
    // ...
  }
};

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

相关推荐
顾平安1 小时前
Promise/A+ 规范 - 中文版本
前端
聚名网1 小时前
域名和服务器是什么?域名和服务器是什么关系?
服务器·前端
桃园码工1 小时前
4-Gin HTML 模板渲染 --[Gin 框架入门精讲与实战案例]
前端·html·gin·模板渲染
沈剑心1 小时前
如何在鸿蒙系统上实现「沉浸式」页面?
前端·harmonyos
一棵开花的树,枝芽无限靠近你1 小时前
【PPTist】组件结构设计、主题切换
前端·笔记·学习·编辑器
m0_748237052 小时前
Chrome 关闭自动添加https
前端·chrome
prall2 小时前
实战小技巧:下划线转驼峰篇
前端·typescript
开心工作室_kaic2 小时前
springboot476基于vue篮球联盟管理系统(论文+源码)_kaic
前端·javascript·vue.js
川石教育2 小时前
Vue前端开发-缓存优化
前端·javascript·vue.js·缓存·前端框架·vue·数据缓存
搏博2 小时前
使用Vue创建前后端分离项目的过程(前端部分)
前端·javascript·vue.js