FabricJS 实现模版文本
官网地址 fabricjs.com/
成果
API
先来看一下fabricJS
的一些基础API:
IText
创建一个文本元素
const iText = new fabric.IText(text, options)
canvas.add(iText)
Textbox
创建一个允许用户调整文本矩形的大小并自动换行的文本盒子
const textBox = new fabric.Textbox(text, options);
canvas.add(textBox)
Image
创建一个图片元素
const image = new fabric.Image.formUrl(url, (imgInstance)=>{
canvas.add(imgInstance)
})
getActiveObject
获取当前选中的元素
const active = canvas.getActiveObject()
set
设置激活元素样式
当前选中元素.set('fontSize', 12)
当前选中元素.set({fontSize: 12})
setSelectionStyles
设置文字选中样式
当前选中元素.setSelectionStyles('fontSize', 12)
当前选中元素.setSelectionStyles({fontSize: 12})
mouse:down
鼠标按下事件
canvas.on("mouse:down", options => {})
mouse:up
鼠标抬起事件
canvas.on("mouse:up", options => {})
toJSON
将画布内容转换成数据。
const value = fabric实例.toJSON()
loadFromJSON
将数据加载到画布中
fabric实例.loadFromJSON(value, fabric实例.renderAll.bind(fabric实例))
renderAll
重新渲染画布
fabric实例.renderAll()
创建项目
npm init vite@latest fabricjs-test
选择Vue + TS
来进行开发。
安装fabric
npm install fabric@4.6.0 --save
然后把项目内的文件整理一下,把style.css
、HelloWorld.vue
这两个文件删了。然后吧App.vue
文件初始化一下。
创建一个canvas元素
<div>
<canvas ref="canvasRef" id="canvas"></canvas>
</div>
加个背景颜色
<style scoped>
#canvas {
background-color: #efefef;
}
</style>
初始化fabric
const canvasRef = ref<HTMLCanvasElement>(); // canvas 元素
const fabricElement = shallowRef(); // fabric元素
onMounted(() => {
fabricElement.value = new fabric.Canvas(canvasRef.value!, {
width: 600,
height: 400,
});
let iText = new fabric.IText("我是fabric IText元素", {
fontSize: 20,
});
fabricElement.value.add(iText);
});
完成了初始化
它这个选中元素的样式很难看,我们怎么修改,可以使用fabirc.Object.prototype.set
方法
fabric.Object.prototype.set({
cornerStrokeColor: "#000000", //边角描边颜色
cornerColor: "#000000", //边角颜色
cornerStyle: "rect", //边角形状
transparentCorners: false, // 边角背景颜色
cornerSize: 6, //边角大小
borderScaleFactor: 1, //描边边框大小
borderColor: "#000000", //描边边框颜色
borderDashArray: [6],
padding: 5,
});
这样就好看多了
接下来获取颜色的操作了。
在utils/fabricElement.json
文件,主要是用来记录元素类型对应的样式属性。这里我们只来设置text
文本元素,后续如果有其他元素控件时可以在此处添加所对应要修改的样式。
{
"text": [
"fontSize",
"fill",
"textAlign",
"textBackgroundColor",
"fontWeight",
"underline",
"fontStyle",
"lineHeight"
]
}
创建selectFabricOptions
变量用来存储当前元素的样式。
const selectFabricOptions = ref({});
在onMounted
中监听鼠标按下事件,从而获取激活元素样式、获取当前激活的元素currentActiveFabric
和修改状态fabricIsEditing
。
import propsJson from "./utils/fabricElement.json";
const currentActiveFabric = ref();
const fabricIsEditing = ref(false);
fabricElement.value.on("mouse:down", handleMouseDown);
const handleMouseDown = (options) => {
const { target } = options;
currentActiveFabric.value = fabricElement.value.getActiveObject(); // 当前激活的元素
fabricIsEditing.value = fabricElement.value.getActiveObject()?.isEditing; // 是否处于修改状态
if (target && target.text) {
let styles = {};
propsJson.text.forEach((key) => {
styles[key] = target[key];
});
selectFabricOptions.value = styles;
} else {
selectFabricOptions.value = {};
}
};
把样式单独提出来当一个组件components/FabricOptions.vue
。接收了一个props
和一个emits
。
<script setup lang="ts">
import { computed } from "vue";
interface StyleType {
fontSize: number;
fill: string;
textBackgroundColor: string;
textAlign: "left" | "center" | "right";
fontWeight: "bold" | "normal";
underline: boolean;
fontStyle: "italic" | "normal";
}
interface Props {
modelValue: StyleType;
}
const props = defineProps<Props>();
const selectFabricOptions = computed<StyleType>({
get() {
return props.modelValue;
},
set(value) {
emits("update:modelValue", value);
},
});
const emits = defineEmits<{
(event: "setFabricTextStyle", prop: string): void;
(event: "update:modelValue", value: StyleType): void;
}>();
const setFabricTextStyle = (prop) => {
selectFabricOptions.value = { // 这样设置值是为了出发computed的set方法
...selectFabricOptions.value,
[prop]: selectFabricOptions.value[prop],
};
emits("setFabricTextStyle", prop);
};
</script>
<template>
<div class="options">
<div>
<span>文字大小:</span>
<select
v-model="selectFabricOptions.fontSize"
@change="setFabricTextStyle('fontSize')"
>
<option
v-for="item in new Array(101).fill(1).map((_, index) => index + 10)"
:value="item"
:label="item + ''"
></option>
</select>
</div>
<div>
<span>文字颜色:</span>
<input
type="color"
v-model="selectFabricOptions.fill"
@input="setFabricTextStyle('fill')"
/>
</div>
<div>
<span>文本背景颜色:</span>
<input
type="color"
v-model="selectFabricOptions.textBackgroundColor"
@input="setFabricTextStyle('textBackgroundColor')"
/>
</div>
<div>
<span> 对齐方式:</span>
<button
:class="{ alignActive: selectFabricOptions.textAlign === 'left' }"
@click="
selectFabricOptions.textAlign = 'left';
setFabricTextStyle('textAlign');
"
>
左对齐
</button>
<button
:class="{ alignActive: selectFabricOptions.textAlign === 'center' }"
@click="
selectFabricOptions.textAlign = 'center';
setFabricTextStyle('textAlign');
"
>
居中
</button>
<button
:class="{ alignActive: selectFabricOptions.textAlign === 'right' }"
@click="
selectFabricOptions.textAlign = 'right';
setFabricTextStyle('textAlign');
"
>
右对齐
</button>
</div>
<div>
<span>文字加粗:</span>
<button
:class="{ alignActive: selectFabricOptions.fontWeight === 'bold' }"
@click="
selectFabricOptions.fontWeight =
selectFabricOptions.fontWeight === 'bold' ? 'normal' : 'bold';
setFabricTextStyle('fontWeight');
"
>
加粗
</button>
</div>
<div>
文字下划线:
<button
:class="{ alignActive: selectFabricOptions.underline }"
@click="
selectFabricOptions.underline = !selectFabricOptions.underline;
setFabricTextStyle('underline');
"
>
文字下划线
</button>
</div>
<div>
文字斜体:
<button
:class="{ alignActive: selectFabricOptions.fontStyle === 'italic' }"
@click="
selectFabricOptions.fontStyle =
selectFabricOptions.fontStyle === 'italic' ? 'normal' : 'italic';
setFabricTextStyle('fontStyle');
"
>
文字斜体
</button>
</div>
</div>
</template>
<style scoped>
.options {
display: flex;
flex-direction: column;
gap: 10px;
}
.options > div {
display: flex;
align-items: center;
gap: 10px;
}
.alignActive {
background-color: #409eff;
}
</style>
在App.vue
中使用这个组件
<template>
<div class="fabric">
<canvas ref="canvasRef" id="canvas"></canvas>
<fabric-options
v-if="Object.keys(selectFabricOptions).length"
v-model="selectFabricOptions"
@setFabricTextStyle="setFabricTextStyle"
/>
</div>
</template>
<style scoped>
.fabric {
display: flex;
gap: 40px;
align-items: center;
}
#canvas {
background-color: #efefef;
}
</style>
setFabricTextStyle
方法
const setFabricTextStyle = (prop: string) => {
if (fabricIsEditing.value) { // 是否是修改状态
// 设置选中文字的样式
currentActiveFabric.value.setSelectionStyles({
[prop]: selectFabricOptions.value[prop],
});
} else {
// 设置所有样式
currentActiveFabric.value.set(selectFabricOptions.value);
}
// 重新渲染
fabricElement.value.renderAll();
};
目前一个简单的文本样式修改就实现了,接下来我们即将进入正题。之前啰嗦那么多也是为了让没有用过fabricjs
的也可以看懂。
模版实现
我们有这样的一段内容,希望将这个模版变量替换成数据,那有人就会说,这样不是很简单嘛?但是你又没考虑过样式也要替换呢? 我们可以到fabricjs
对样式的存储是一个二维数组的方式,我们将${name}
设置成红色,它会存储10-16共6个字符的样式,如果name
变量最终所对应的只有2个字符呢?这就会导致后面的内容也会有样式,这样就不对了。
所以我们要设计一个算法,用来计算模版变量和真实数据字符之间的差值。那么接下来我们来尝试的写一下。
实现模版文本主要算法
在utils/compile.ts
创建文件
let text = "";
let styles = {};
let object = {};
let fibreTree = []; // 记录每一行变量以及变量的位置和长度
let textArr = []; // 替换变量之后的值
function init() {
text.split("\n").forEach((item, index) => {
const codeArr = item.match(reg_g);
let _index = 0;
fibreTree[index] = {
codeAll: [],
code: [],
index: [],
endIndex: [],
currIndex: [],
styles: [],
};
codeArr.forEach((code) => {
_index = item.indexOf(code, _index);
fibreTree[index].codeAll.push(code);
fibreTree[index].code.push(code.substring(2, code.length - 1));
fibreTree[index].index.push(_index);
fibreTree[index].endIndex.push(_index + code.length - 1);
fibreTree[index].currIndex.push(
object[code.substring(2, code.length - 1)]
? object[code.substring(2, code.length - 1)].length
: code.length
);
if (styles[index] && styles[index][_index]) {
fibreTree[index].styles.push(styles[index][_index]);
}
});
});
}
// 对text文本变量进行替换
function rewriteText() {
text.split("\n").forEach((item, index) => {
fibreTree[index].codeAll.forEach((_item, _index) => {
item = item.replaceAll(
_item,
object[fibreTree[index].code[_index]]
? object[fibreTree[index].code[_index]]
: fibreTree[index].codeAll[_index]
);
});
textArr.push(item);
});
}
// 对styles进行重构
function rewriteStyles() {
for (let [key, value] of Object.entries(styles)) {
let obj = {};
for (let _key in value as object) {
if (isTrue(key, _key)) {
let beginIndex = isBeginIndex(key, _key);
// 数据字段之前的数据,样式不进行改变
if (beginIndex === 0) {
obj[_key] = value[_key];
}
// 数据字段之后的数据,样式要根据前面数据字段的长度来改变
else {
let len1 = 0;
for (let i = 0; i < beginIndex; i++) {
let codeLength = fibreTree[key].codeAll[i].length;
let dataLength = object[fibreTree[key].code[i]].length;
if (i === beginIndex - 1) {
obj[Number(_key) + Number(dataLength - codeLength) + len1] =
value[_key];
}
len1 += Number(dataLength - codeLength);
}
}
}
}
let startIndex = 0;
fibreTree[key].code.forEach((_value, _key) => {
startIndex = textArr[key].indexOf(object[_value], startIndex);
let endIndex = startIndex + object[_value].length;
for (let i = startIndex; i < endIndex; i++) {
if (fibreTree[key].styles[_key]) obj[i] = fibreTree[key].styles[_key];
}
});
newStyles[key] = obj;
}
}
// 获取不是属性的索引样式
function isTrue(key, value) {
var trueNum = 0;
fibreTree[key].index.forEach((item, index) => {
if (
value < Number(item) ||
value > Number(fibreTree[key].endIndex[index])
) {
trueNum++;
}
});
return trueNum == fibreTree[key].index.length;
}
function isBeginIndex(key, value) {
var trueNum = 0;
fibreTree[key].index.forEach((_, index) => {
if (value > Number(fibreTree[key].endIndex[index])) {
trueNum++;
}
});
return trueNum;
}
export default function compile(fabricObject, dataObject) {
text = fabricObject.text;
styles = fabricObject.styles;
object = dataObject;
init();
rewriteText();
rewriteStyles();
return {
text: textArr.join("\n"),
styles: newStyles,
};
}
我们的编译方法已经完成了,
接下来我们使用codemirror
来编写json数据。
npm install codemirror@5.65.16 @types/codemirror --save
创建一个components/CodeEditor.vue
组件
<script setup lang="ts">
import { onMounted, ref, watchEffect } from "vue";
import CodeMirror from "../codemirror/codemirror";
interface Props {
value?: string;
}
const codeMirrorRef = ref();
const props = withDefaults(defineProps<Props>(), {
value: "",
});
const emits = defineEmits<(e: "change", value: string) => void>();
onMounted(() => {
const addonOptions = {
autoCloseBrackets: true,
autoCloseTags: true,
foldGutter: true,
gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"],
keyMap: "sublime",
};
const editor = CodeMirror(codeMirrorRef.value!, {
value: props.value,
mode: "application/json",
tabSize: 2,
lineWrapping: true,
lineNumbers: true,
...addonOptions,
});
editor.on("change", () => {
emits("change", editor.getValue());
});
watchEffect(() => {
const cur = editor.getValue();
if (props.value !== cur) {
editor.setValue(props.value);
}
});
setTimeout(() => {
editor.refresh();
}, 50);
window.addEventListener("resize", () => {
editor.refresh();
});
});
</script>
<template>
<div ref="codeMirrorRef" class="editor"></div>
</template>
<style>
.editor {
height: 100%;
width: 100%;
overflow: hidden;
}
.CodeMirror {
font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
line-height: 1.5;
height: 100%;
}
</style>
创建codemirror
所用的实例,/codemirror/codemirror.ts
目录
import CodeMirror from 'codemirror'
import 'codemirror/addon/dialog/dialog.css'
import './codemirror.css' // 这里网上都是有内容了,太多了我就不贴代码了
// modes
import 'codemirror/mode/javascript/javascript.js'
// addons
import 'codemirror/addon/edit/closebrackets.js'
import 'codemirror/addon/edit/closetag.js'
import 'codemirror/addon/comment/comment.js'
import 'codemirror/addon/fold/foldcode.js'
import 'codemirror/addon/fold/foldgutter.js'
import 'codemirror/addon/fold/brace-fold.js'
import 'codemirror/addon/fold/indent-fold.js'
import 'codemirror/addon/fold/comment-fold.js'
import 'codemirror/addon/search/search.js'
import 'codemirror/addon/search/searchcursor.js'
import 'codemirror/addon/dialog/dialog.js'
// keymap
import 'codemirror/keymap/sublime.js'
export default CodeMirror
在App.vue
中使用
通过fabric
提供的toJSON
方法将内容转换成JSON对象,然后通过loadFromJSON
方法将数据渲染到画布上。
const dataJson = ref([]); // js对象格式的数据
const dataString = ref(JSON.stringify(dataJson.value)); // 字符串格式的数据
const run = ref(false); // 是否生成
const change = (value) => {
try {
dataString.value = value;
} catch {
dataJson.value = [];
}
};
const save = () => {
try {
dataJson.value = JSON.parse(dataString.value);
} catch {
return alert("请检查JSON格式是否正确");
}
run.value = true;
const options = fabricElement.value.toJSON();
dataJson.value.forEach((value, index) => {
let obj = JSON.parse(JSON.stringify({ ...options }));
obj.objects.forEach((item) => {
if (item.type === "i-text") {
let { text, styles } = compile(item, value);
item.text = text;
item.styles = styles;
}
});
nextTick(() => {
let dom = new fabric.Canvas("canvas" + index, {
width: 600,
height: 400,
});
dom.loadFromJSON(obj, dom.renderAll.bind(dom));
});
});
};
<div class="fabric">
<div class="code">
<code-editor :value="dataString" @change="change" />
</div>
<canvas ref="canvasRef" id="canvas"></canvas>
<fabric-options
v-if="Object.keys(selectFabricOptions).length"
v-model="selectFabricOptions"
@setFabricTextStyle="setFabricTextStyle"
/>
</div>
<div style="margin: 40px">
<button @click="save">生成</button>
</div>
<div v-if="run" class="runtime">
<div class="container">
<canvas
v-for="(_, index) in dataJson"
:key="index"
class="box"
:id="'canvas' + index"
/>
</div>
</div>
到这里就完结了,代码很多。其实最主要的还是模版算法。那我为什么这篇文章写了那么多与模版算法没有关的内容呢?主要还是因为对于没有使用过fabricJS
的,让他们体验一下fabricJS
能做什么。fabricJS
真的很强大,它把canvas
的API
简化了,使用起来非常简单。fabricJS
主要用于海报设计、图片设计等项目。
如果需要源代码的话,我稍后也会上传到github上。
转载自:https://juejin.cn/post/7397617342162386954