在fabric
基础系列博文中,我们通过代码向画布canvas
中添加矩形、圆形等对象。对于用户,我们不能指望他们可以理解代码,甚至编写代码去制作他的简历。你也许使用过PhotoShop或其他的绘图软件,这些软件中都是让用户点击各种图标来向画布中绘制对应的对象的。没有使用过也没关系,下文中有效果的预览图,也会教你一步一步实现它。
这篇博文是《前端canvas项目实战——简历制作网站》付费专栏系列博文的第一篇——左侧工具栏,主要的内容有:
动手体验
CodeSandbox会自动对代码的进行编译,并提供地址以供体验代码效果
由于CSDN的链接跳转有问题,会导致页面无法工作,请复制以下链接在浏览器打开:
https://wvclr3.csb.app/
在前面的博文HTML5画布框架fabricjs学习笔记(三)——自定义选择控制框样式中,我们通过fabric.Object.prototype.xxx = ...;
的方式按照自己的喜好,对选择框的控制点样式做了一些定制化的修改。这篇博文的内容开始前,我们先对代码结构进行一点优化,避免canvas-page.js
文件里写太多代码,不便于维护。
首先,我们把上述的代码拆分到一个wrap-object.js
文件中,其含义即对fabric.Object
类进行封装。
import { fabric } from "fabric";
import shortUUID from "short-uuid";
import rotateControlIcon from "../images/rotate.svg";
fabric.Object.prototype.noScaleCache = false;
// 修改控制点的样式
fabric.Object.prototype.transparentCorners = false;
fabric.Object.prototype.cornerColor = "white";
fabric.Object.prototype.borderColor = "dodgerblue";
fabric.Object.prototype.cornerStrokeColor = "gray";
fabric.Object.prototype.cornerSize = 8;
fabric.Object.prototype.cornerStyle = "circle";
fabric.Object.prototype.strokeUniform = true;
fabric.Object.prototype.padding = 10;
/**
* 覆盖fabric.Object的initialize方法,为对象添加id属性
* @type {function(...[*]): fabric.Object.initialize}
*/
fabric.Object.prototype.initialize = (function (fn) {
return function (...args) {
fn.call(this, ...args);
let { id } = args;
id ||= shortUUID().new();
this.set("id", id);
this.objectCaching = false;
return this;
};
})(fabric.Object.prototype.initialize);
const renderIcon = (image, initialAngle) => {
let imageIcon = document.createElement("img");
imageIcon.src = image;
return function (ctx, left, top, styleOverride, fabricObject) {
let size = this.cornerSize;
ctx.save();
ctx.translate(left, top);
ctx.rotate(fabric.util.degreesToRadians(fabricObject.angle + initialAngle));
ctx.drawImage(imageIcon, -size / 2, -size / 2, size, size);
ctx.restore();
};
};
/**
* 修改旋转控制按钮的样式
* @type {Control}
*/
fabric.Object.prototype.controls.mtr = new fabric.Control({
x: 0,
y: -0.5,
offsetY: -20,
cursorStyle: "pointer",
actionName: "rotate",
actionHandler: fabric.controlsUtils.rotationWithSnapping,
cursorStyleHandler: fabric.controlsUtils.rotationStyleHandler,
withConnection: false,
render: renderIcon(rotateControlIcon, 0),
cornerSize: 20
});
显而易见,这里一共三部分:
fabric.Object
的initialize
方法,为对象添加id
属性mtr
中间顶部旋转控制点的样式左侧工具栏用于用户点击后,在画布中创建出其想要的基础图形。初步,我们先实现最基础的四种对象:矩形
、圆形
、直线
、文字输入框
这里创建一个left-side-tools.js
,代码如下:
import "./index.css";
import squareIcon from "../../images/square.svg";
import circleIcon from "../../images/circle.svg";
import lineIcon from "../../images/line.svg";
import textIcon from "../../images/typeface.svg";
import { handleClickTool } from "./logics";
const Tool = (props) => {
let { icon, handleClick } = props;
return (
<div className="left-side-tool" onClick={handleClick}>
<img className="left-side-tool-icon" src={icon} alt="Icon of tool" />
</div>
);
};
const LeftSideTools = (props) => {
let { canvas } = props;
return (
<div className="left-side-tools-container">
<Tool icon={squareIcon} handleClick={(e) => handleClickTool(e, 0, canvas)} />
<Tool icon={circleIcon} handleClick={(e) => handleClickTool(e, 1, canvas)} />
<Tool icon={lineIcon} handleClick={(e) => handleClickTool(e, 2, canvas)} />
<Tool icon={textIcon} handleClick={(e) => handleClickTool(e, 3, canvas)} />
</div>
);
};
export default LeftSideTools;
UI部分的实现比较简单,就是在页面左侧绘制四个icon
,点击后调用handleClickTool
方法去分别向画布中创建对应的对象。
为了提高UI和JS逻辑的维护性,handleClickTool
放在同目录下的另一个文件logics.js
中,其代码如下:
import { fabric } from "fabric";
const handleClickTool = (e, func, canvas) => {
if (!canvas) {
return;
}
let newFabricObject;
switch (func) {
case 0:
newFabricObject = new fabric.Rect();
break;
case 1:
newFabricObject = new fabric.Circle();
break;
case 2:
newFabricObject = new fabric.Line();
break;
case 3:
newFabricObject = new fabric.Textbox();
break;
default:
throw RangeError(`Func ${func} not implemented!`);
}
if (newFabricObject) {
canvas.add(newFabricObject);
canvas.renderAll();
canvas.setActiveObject(newFabricObject);
canvas.renderAll();
}
e.stopPropagation();
e.preventDefault();
};
export { handleClickTool };
其代码逻辑可大致分为3个部分:
1) 根据用户点击的不同icon
传递过来的func
创建不同对象的实例
2) 将上述实例通过canvas.add
方法绘制到画布中
3) 避免click
事件向父标签传播
在新的canvas-page.js
中,我们将上述的代码“组装”,实现本节所要达到的功能,其代码如下:
import "./index.css";
import "../wrap-fabric/wrap-object";
import "../wrap-fabric/wrap-rect";
import "../wrap-fabric/wrap-circle";
import "../wrap-fabric/wrap-line";
import "../wrap-fabric/wrap-text";
import { fabric } from "fabric";
import React, { useEffect, useState } from "react";
import LeftSideTools from "./left-side-tools";
const CanvasPage = (props) => {
const [canvas, setCanvas] = useState(null);
useEffect(() => {
let canvas = new fabric.Canvas("canvas");
setCanvas(canvas);
}, []);
return (
<div className="content-container">
<LeftSideTools canvas={canvas} />
<canvas id="canvas" width="794px" height="1123px" />
</div>
);
};
export default CanvasPage;
这里有几点需要解释说明:
1) 我们通过#content-container
将刚刚实现的左侧工具栏LeftSideTools
和画布canvas
横向并排置于屏幕中
2) 画布的大小设为width="794px" height="1123px"
,这是1倍屏幕分辨率
下A4
纸大小210mm x 297mm
计算得到的像素值大小,具体的计算公式我们在晚些的博文中再谈
3) 我们在本节开头提到的wrap-object.js
通过import ../wrap-object
引入了进来,这样的代码拆分可以使UI和JS逻辑更便于阅读和维护
由于篇幅所限,尽数列出所有代码改动会导致博文冗长难读。本节完整的代码托管在CodeSandbox中,点击前往,查看完整代码
本节之后,我们可以用现有的功能将简历做成什么样子,我将图片贴到了博文开头,为了读者可以快速理解通过这节的代码我们可以实现什么效果。
点击左侧工具栏,不同的对象会被绘制在画布左上角,我们通过拖拽、按下并拖拽9
个控制点来改变该对象的位置
、宽
、高
、旋转角度
这几个属性,以此实现上述“简历”的样式。
目前我们在页面上还不能改变对象的其他样式,如边框颜色
、填充颜色
等。下一节,我们通过右侧属性栏
来实现这些需求。