likes
comments
collection
share

FabricJS 实现模版文本

作者站长头像
站长
· 阅读数 26

官网地址 fabricjs.com/

成果

FabricJS 实现模版文本

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

FabricJS 实现模版文本

选择Vue + TS来进行开发。

安装fabric

npm install fabric@4.6.0 --save

然后把项目内的文件整理一下,把style.cssHelloWorld.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);
});

完成了初始化

FabricJS 实现模版文本

它这个选中元素的样式很难看,我们怎么修改,可以使用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,
});

FabricJS 实现模版文本

这样就好看多了

接下来获取颜色的操作了。

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的也可以看懂。

模版实现

FabricJS 实现模版文本

我们有这样的一段内容,希望将这个模版变量替换成数据,那有人就会说,这样不是很简单嘛?但是你又没考虑过样式也要替换呢? 我们可以到fabricjs对样式的存储是一个二维数组的方式,我们将${name}设置成红色,它会存储10-16共6个字符的样式,如果name变量最终所对应的只有2个字符呢?这就会导致后面的内容也会有样式,这样就不对了。

FabricJS 实现模版文本

所以我们要设计一个算法,用来计算模版变量和真实数据字符之间的差值。那么接下来我们来尝试的写一下。

实现模版文本主要算法

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真的很强大,它把canvasAPI简化了,使用起来非常简单。fabricJS主要用于海报设计、图片设计等项目。

如果需要源代码的话,我稍后也会上传到github上。

转载自:https://juejin.cn/post/7397617342162386954
评论
请登录