最近这段时间闲着没事做,就打算做一个简易编辑器来玩。
本文对于创建项目的过程就不再赘述,直接进入主题。
前言
本项目基于 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.vue
和 Sidebar.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',
});
}
// ...
}
};
到此,将图形或文本添加到画板上的核心函数已经实现,看一下实际效果如何: