在绘制线条阴影的时候,一般来说有两种方案:
- 绘制完整的一张图片
- 绘制一个片段,然后重复渲染
我们今天讨论第二种。它更加高效,更适合大范围渲染。
考虑功能
- 看上去是一个整体,所以拼接时所有线条要可以完全连上
- 可以调整线条角度
- 可以调整线条间距
- 可以调整线条粗细和颜色
渲染思路
我们可以将线条分为三种状态:
- 横线
- 竖线
- 斜线
前两种,特殊处理即可,我们需要关注最后一种 斜线
,我们只需要关注斜线的角度即可。从根本上说,横线
与 竖线
也是斜线的特殊角度而已,所以我们可以统一计算,遇到 0 || 90
的时候,将它们处理成对应的横/竖即可。
为了绘制斜线,我们这样思考:我们让斜线作为当前图形的对角线,这样无论怎么拼接,这条线都是连续的。
如上图,这样渲染出来的整体,就是一条连续的线。然后,我们只需要在这条线的两侧再分别渲染一定间距之外的平行线,就形成了阴影线条。
那么,间距线条如何绘制?可以考虑一个取巧的简单方案:将间距设置为斜线与两个对角的距离:
如图:红线部分就是间距,而黄色虚线则是相邻的另一条连线的位置。
根据这种方案,我们可以轻松滑出重复无限的阴影效果。
考虑完整情况
我们需要两个参数:
- 斜线的角度
- 斜线之间的间距
有了这两个参数,我们就可以绘制出需要的斜线。但是需要考虑角度会影响斜线的出发点和方向。
综合以上的逻辑,我们把斜线统一从左上角出发,它将有几种情况(以角度 0 为从左至右的横线为原点):
- 0度:横线,此时在矩形顶部画一条线即可
- 1-89度:斜线,左上角到右下角画一条线
- 90度:竖线,此时在矩形左侧画一条线即可
- 91-179度:斜线,此时需要从右上角到左下角画一条
- 180度:横线,同 0度
我们只考虑 180度 之内,超过了我们使用 %
将剩余角度取余,展示效果一样。
在以上的情况中,我们需要画四条线。原因有二:
- 为了统一实现方法
- 让连线看上去更加连贯(是的,在两个矩形对角相连的部分,连线是有一定分割的,连线如果过粗,它只会在当前矩形中展示,并不会溢出,所以连接处的垂直方向上,会出现空缺。如下图)
所以我们需要画四条线,它们出发的四个点分别是:
如图所示,四个点出发的线,无论是向左(绿色,相当于91-179度)还是向右(黄色,相当于 1-89度),它们都可以覆盖到展示区域。
具体实现
有了以上的思路,我们就可以实现它。
首先,获取参数:
- 角度:angle
- 间距:spacing
- 线条宽度:width
将角度固定在我们需要的范围内:
const θ = angle % 180;
const rad = (θ * Math.PI) / 180;
有了角度,我们就可以通过间距,来获取我们应该创建多大的一个图片(图片的宽高就是斜线对角线的宽高):
let canvasWidth: number;
let canvasHeight: number;
if (θ === 0) {
canvasWidth = spacing;
canvasHeight = width + spacing;
} else if (θ === 90) {
canvasWidth = width + spacing;
canvasHeight = spacing;
} else {
canvasWidth = Math.abs(Math.ceil((width + spacing) / Math.sin(rad)));
canvasHeight = Math.abs(Math.ceil((width + spacing) / Math.cos(rad)));
}
有了图片的宽高,就可以绘制线条:
if (θ === 0 || θ === 90) {
if (θ === 0) {
// 水平线
ctx.moveTo(0, 0);
ctx.lineTo(canvasWidth, 0);
} else {
// 垂直线
ctx.moveTo(0, 0);
ctx.lineTo(0, canvasHeight);
}
} else {
// 倾斜线条
for (let x = -canvasWidth; x < canvasWidth * 2; x += canvasWidth) {
// 计算线条的起点和终点
const x1 = x;
const y1 = 0;
const x2 = x + (Math.cos(rad) < 0 ? -canvasWidth : canvasWidth);
const y2 = canvasHeight;
ctx.moveTo(Math.ceil(x1), Math.ceil(y1));
ctx.lineTo(Math.ceil(x2), Math.ceil(y2));
}
}
最后,转为图片并在完整的图片中重复它就可以了。
完整代码
function createStripePattern(options = {}) {
const {
color = "#c9c9c9",
width = 1,
angle = 30,
spacing = 10
} = options;
// 将角度转换到 0-180 范围内
const θ = angle % 180;
const rad = (θ * Math.PI) / 180;
let canvasWidth;
let canvasHeight;
if (θ === 0) {
canvasWidth = spacing;
canvasHeight = width + spacing;
} else if (θ === 90) {
canvasWidth = width + spacing;
canvasHeight = spacing;
} else {
canvasWidth = Math.abs(Math.ceil((width + spacing) / Math.sin(rad)));
canvasHeight = Math.abs(Math.ceil((width + spacing) / Math.cos(rad)));
}
// 创建临时 canvas
const canvas = document.createElement('canvas');
canvas.width = canvasWidth;
canvas.height = canvasHeight;
const ctx = canvas.getContext('2d');
// 设置线条样式
ctx.strokeStyle = color;
ctx.lineWidth = width;
ctx.lineCap = 'butt';
ctx.lineJoin = 'miter';
ctx.beginPath();
// 绘制线条
if (θ === 0 || θ === 90) {
if (θ === 0) {
// 水平线
ctx.moveTo(0, 0);
ctx.lineTo(canvasWidth, 0);
} else {
// 垂直线
ctx.moveTo(0, 0);
ctx.lineTo(0, canvasHeight);
}
} else {
// 倾斜线条
for (let x = -canvasWidth; x < canvasWidth * 2; x += canvasWidth) {
// 计算线条的起点和终点
const x1 = x;
const y1 = 0;
const x2 = x + (Math.cos(rad) < 0 ? -canvasWidth : canvasWidth);
const y2 = canvasHeight;
ctx.moveTo(Math.ceil(x1), Math.ceil(y1));
ctx.lineTo(Math.ceil(x2), Math.ceil(y2));
}
}
ctx.stroke();
// 转换为图片并返回
const img = new Image();
img.src = canvas.toDataURL();
return img;
}
文章评论