本编辑器(土豆猫图形编辑器)社区版代码已开源,开源库地址:
https://gitee.com/longhan13/lgxmap_community.git
https://gitcode.com/longx13/lgxmap_community.git
本编辑器(土豆猫图形编辑器)专业版已发布到个人网站:
正文开始之前,先安利CSDN的一个好东西:inscode,可以在线编辑、在线发布、在线运行。为了便于大家相互学习交流,我把本编辑器社区版后台功能做了阉割,做成了一个学习版,已发布在inscode上,大家可以直接线上体验,线上修改代码,在线看到效果,非常方便(本文的截图都是用它画出来的)地址:InsCode - 让你的灵感立刻落地
矢量图形中,用于填充封闭区域有多种方式:实体填充、图案填充、渐变填充,渐变填充又分线性渐变填充和辐射渐变填充。本文讲解本编辑器中经常用到的一种填充模式:线性渐变填充。效果如下图:
图1 渐变填充效果实例
我们首先看canvas渐变填充createLinearGradient定义:createLinearGradient(x0, y0, x1, y1)-创建一个从起点(x0,y0)到终点(x1,y1)的线性渐变填充,返回一个线性CanvasGradient对象,创建对象成功后,再调用返回对象的CanvasGradient的addColorStop(offset,color)来创建渐变光圈,所谓光圈就是一个用户指定的颜色,canvas绘制时就在相邻光圈指定的颜色中按canvas颜色渐变算法进行渐变,offset是一个从0到1的浮点数,表示从起点(x0,y0)到该光圈处的距离相对于整根渐变线段(由(x0,y0)到(x1,y1)决定)长度比,等于0表示起点,等于1表示终点,等于0.5则表示中点((x0 + y0)/2,(x1 + y1)/2),以此类推。例如下面这段代码就可以绘制出一根类似圆柱效果的矩形来:
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
let gradient = ctx.createLinearGradient(0, 0, 50, 0);
gradient.addColorStop(0, "blue");
gradient.addColorStop(0.5, "white");
gradient.addColorStop(1, "pink");
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, 50, 300);
讲到这里,如果只是为了解渐变原理,那就够了,但实际的图形软件应用中,渐变填充是图形对象的一个属性或属性值(本编辑器中线性渐变是填充属性的一个取值),例如图形发生了移动、旋转、缩放等行为,相应地,渐变也要跟着变化,所以我们了解其原理后还要知道怎么把它真正应用到自己的项目中,本文的下面章节将讲解本编辑器的实现方式。
首先,我们把渐变作为一个类对象定义出来(下面是代码局部,完整代码见:script/canvas/common/struct/fillinfo):
class GradientInfo {
constructor() {
let red = new lgxcom.LGXColor();
red.setVal(255, 0, 0);
let white = new lgxcom.LGXColor();
white.setVal(255, 255, 255);
//默认有两个光圈,起点光圈红色,终点光圈白色
this.stopColorArr = [{ stop: 0, color: red }, { stop: 1, color: white }];
//渐变起点和终点坐标都是局部坐标系,便于实现:所关联图形对象做了各种变换后,其渐变效果仍相对不变
this.stPt = { x: 0, y: 0 };
this.endPt = { x: 0, y: 0 };
this.degree = 0;//渐变旋转角度
this.userSetPosFlag = false;
}
toJson();//转为json对象以备后面持久化,具体代码省略
fromJson(jsonobj);//从持久化的json对象转为一个线性渐变填充对象,具体代码省略
//设置线性渐变的默认起点、终点坐标
//参数:refGraph是本渐变类对象所影响的图形对象
setStopDefaultPos(refGraph) {
let localZone = refGraph.getLocalZone();//获得图形矩形包围盒
this.stPt.x = localZone.left;//默认起点为左上角
this.stPt.y = localZone.top;
this.endPt.x = localZone.right;//默认终点为右上角
this.endPt.y = localZone.top;
this.userSetPosFlag = true;
}
//设置渐变光圈参数
setStop(stopArr) {
if (!stopArr) {
return;
}
this.stopColorArr = [];
for (let i = 0; i < stopArr.length; i++) {
let t = stopArr[i];
let proprotion = t.stop;
let colorVal = t.color;
let colorObj = new lgxcom.LGXColor();
colorObj.setColorByValue(colorVal);
this.stopColorArr.push({ stop: parseFloat(proprotion), color: colorObj });
}
}
}
然后我们再定义一个填充对象类,渐变作为填充的一个属性值存进去(下面是代码局部,完整代码见:script/canvas/common/struct/fillinfo):
class LGXFillDefInfo {
constructor() {
this.fillStyle = LGXEnums.LGXFillStyle.EMPTY;
this.fillColor = new lgxcom.LGXColor();
this.fillColor.setVal(255, 0, 0);
this.gradientInfo = null;
}
}
以上类定义完成后,我们就可以在实际绘图中生成图形后(例如在画布上鼠标拖动生成一个矩形),为该图形的填充对象(_fillDef)中渐变属性(gradientInfo )赋值:
graph._fillDef.gradientInfo = new GradientInfo(),并根据实际需要票配置相应起始点、终止点、以及光圈参数,最后在刷新流水线中,绘制该图形时,使用相应的填充参数调用canvas API显示出来。下面代码是本编辑器的实现代码片段(完整代码请见:script/canvas/graph/paintutil/fillutil.js)
FillUtil.active = function (fillDef, canvasCtx, graph, mapinfo, isPaintSybolUnitFlag, transformInfo) {
let grd = null;
let fillGradientInfo = fillDef.gradientInfo;
let mtx = graph.getLocalMatrix();//获得渐变绑定的图形对象的局部坐标系
let localP1 = fillGradientInfo.stPt;
let localP2 = fillGradientInfo.endPt;
//局部坐标转世界坐标
let worldPt1 = mtx.MultT({ x: localP1.x, y: localP1.y, z: 0 });
let worldPt2 = mtx.MultT({ x: localP2.x, y: localP2.y, z: 0 });
let p1 = worldPt1;
let p2 = worldPt2;
if (p1 && p2) {
//以渐变起点和终点指定的线段的中点为中心旋转渐变起点和终点
let rotAngle = funcs.degree2Radian(fillGradientInfo.degree);
let midPt = funcs.getMidPoint(p1, p2);
let rotp1 = funcs.rotate(p1, midPt, rotAngle);
let rotp2 = funcs.rotate(p2, midPt, rotAngle);
let sp1 = null;
let sp2 = null;
let errorFlag = false;
if (isPaintSybolUnitFlag) {
//如果图形属于图符内,则要先把坐标从图符对应的图形对象的局部坐标系转为世界坐标系
let wpt1 = CoordTRFUtil.transLocalPt2World(rotp1, 0, transformInfo);
let wpt2 = CoordTRFUtil.transLocalPt2World(rotp2, 0, transformInfo);
//渐变起点、终点转为屏幕坐标
sp1 = CoordTRFUtil.world2Screen(wpt1, mapinfo);
sp2 = CoordTRFUtil.world2Screen(wpt2, mapinfo);
}
else {
//渐变起点、终点转为屏幕坐标
sp1 = CoordTRFUtil.world2Screen(rotp1, mapinfo);
sp2 = CoordTRFUtil.world2Screen(rotp2, mapinfo);
}
try {
//创建一个线性渐变对象
grd = map.createLinearGradient(sp1.x, sp1.y, sp2.x, sp2.y);
//为渐变对象增加光圈
for (let i = 0; i < fillGradientInfo.stopColorArr.length; i++) {
let t = fillGradientInfo.stopColorArr[i];
let tmpColor = t.color.getCloneCopy();
if (graph.getSelect()) {
//如果图形被选中,让对应光圈颜色变成半透明,让图形选中效果体验好看一些,一目了然
tmpColor.alpha = Math.ceil(tmpColor.alpha / 2);
}
grd.addColorStop(t.stop, tmpColor.toHexString());
}
}
catch (e) {
errorFlag = true;
console.log(e);
}
finally {
if (errorFlag) {
grd = null;
}
}
}
if (grd != null) {
canvasCtx.fillStyle = grd;//改变画布当前的填充模式为本次创建的渐变填充
canvasCtx.fill();//正式填充
}
}
通过以上方法就可以实现各种复杂、绚丽的线性渐变效果,而不管图形位置、形状怎么变换都不会改变渐变与绑定图形的相对位置。下图是使用本编辑器的做出来的一些图形效果