修身养性,知行合一

  • 首页
  • 爱码
    • 系统
    • 数据库
    • JavaScript
    • CSharp
    • Python
  • 生活
    • 文化
    • 美食
  • 杂谈
  • 关于
修身养性,知行合一
码字,杂谈
  1. 首页
  2. 爱码
  3. 前端
  4. JavaScript
  5. 正文

绘制一个可重用的线条阴影

2025年6月17日 788点热度 0人点赞 0条评论

在绘制线条阴影的时候,一般来说有两种方案:

  • 绘制完整的一张图片
  • 绘制一个片段,然后重复渲染

我们今天讨论第二种。它更加高效,更适合大范围渲染。

考虑功能

  1. 看上去是一个整体,所以拼接时所有线条要可以完全连上
  2. 可以调整线条角度
  3. 可以调整线条间距
  4. 可以调整线条粗细和颜色

渲染思路

我们可以将线条分为三种状态:

  • 横线
  • 竖线
  • 斜线

前两种,特殊处理即可,我们需要关注最后一种 斜线,我们只需要关注斜线的角度即可。从根本上说,横线 与 竖线 也是斜线的特殊角度而已,所以我们可以统一计算,遇到 0 || 90 的时候,将它们处理成对应的横/竖即可。

为了绘制斜线,我们这样思考:我们让斜线作为当前图形的对角线,这样无论怎么拼接,这条线都是连续的。

file

如上图,这样渲染出来的整体,就是一条连续的线。然后,我们只需要在这条线的两侧再分别渲染一定间距之外的平行线,就形成了阴影线条。

那么,间距线条如何绘制?可以考虑一个取巧的简单方案:将间距设置为斜线与两个对角的距离:

file

如图:红线部分就是间距,而黄色虚线则是相邻的另一条连线的位置。

根据这种方案,我们可以轻松滑出重复无限的阴影效果。

考虑完整情况

我们需要两个参数:

  • 斜线的角度
  • 斜线之间的间距

有了这两个参数,我们就可以绘制出需要的斜线。但是需要考虑角度会影响斜线的出发点和方向。

综合以上的逻辑,我们把斜线统一从左上角出发,它将有几种情况(以角度 0 为从左至右的横线为原点):

  • 0度:横线,此时在矩形顶部画一条线即可
  • 1-89度:斜线,左上角到右下角画一条线
  • 90度:竖线,此时在矩形左侧画一条线即可
  • 91-179度:斜线,此时需要从右上角到左下角画一条
  • 180度:横线,同 0度

我们只考虑 180度 之内,超过了我们使用 % 将剩余角度取余,展示效果一样。

在以上的情况中,我们需要画四条线。原因有二:

  • 为了统一实现方法
  • 让连线看上去更加连贯(是的,在两个矩形对角相连的部分,连线是有一定分割的,连线如果过粗,它只会在当前矩形中展示,并不会溢出,所以连接处的垂直方向上,会出现空缺。如下图)

file

所以我们需要画四条线,它们出发的四个点分别是:

file

如图所示,四个点出发的线,无论是向左(绿色,相当于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;
}
本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可
标签: JavaScript
最后更新:2025年6月17日

jeremyjone

这个人很懒,什么都没留下

打赏 点赞
< 上一篇
下一篇 >

文章评论

取消回复

文章目录
  • 考虑功能
  • 渲染思路
  • 考虑完整情况
  • 具体实现
  • 完整代码
最新 热点 随机
最新 热点 随机
推一个vscode纯黑主题 vue 的递归插槽穿透 Github Pages SPA 重定向 行间距引出的 DOCTYPE 怪异行为 写个小彩蛋 绘制一个可重用的线条阴影
推一个vscode纯黑主题
使用 windows 命令启动某个程序 WSL 安装问题 0x8007019e、0x800701bc、0x80370102 Windows下Python创建进程池的问题 windows 无法登录便签、OneNote等应用 将flask程序部署在apache上 axios的二次封装

(っ•̀ω•́)っ✎⁾⁾ 开心每一天

COPYRIGHT © 2021 jeremyjone.com. ALL RIGHTS RESERVED.

THEME KRATOS MADE BY VTROIS

京ICP备19012859号-1

京公网安备 11010802028585号