无处不飞花 發表於 2026-2-9 10:22:00

前端实现速度线

<h2 id="功能">功能</h2>
<p>前端实现速度线,在矩形内生成黑白三角形且闪动。</p>
<h2 id="思路">思路</h2>
<p>速度线可以使用多个角度相同的三角形分解矩形。三角形的渲染使用canvas连线fill就行,三角形在矩形上的两个点可以通过计算每个三角形的边长来获取。三角形在矩形上的边长使用三角函数获取。</p>
<ol>
<li>HTML结构:包含一个画布(Canvas)用于显示图片。</li>
<li>CSS样式:定义了页面的基本布局和样式,并设置了画布的大小和边框。</li>
<li>JavaScript功能:
<ul>
<li>处理三角形点、颜色。</li>
<li>循环更改颜色并渲染。</li>
</ul>
</li>
</ol>
<h2 id="实现">实现</h2>
<h3 id="html结构">HTML结构</h3>
<p>添加canvas元素</p>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang="cn-zh"&gt;
&lt;head&gt;
&lt;meta charset="UTF-8"&gt;
&lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
&lt;title&gt;SpeedLine&lt;/title&gt;
&lt;style&gt;
    #myCanvas {
      outline: 2px solid;
    }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;canvas id="myCanvas" width="500" height="500"&gt;&lt;/canvas&gt;
&lt;/body&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<h3 id="javascript">JavaScript</h3>
<p>获取canvas元素和canvas渲染上下文,初始化三角形,绘制填充的三角形,循环动画</p>
<blockquote>
<p>使用三角函数可以根据矩形长宽获取矩形中心点到矩形四个顶点的夹角,根据三角形数量参数获得每个三角形的角度。<br>
使用Math.tan传的参数是弧度,弧度 = 角度 * PI / 180。<br>
不需要求出所有三角形在矩形上的那一边宽度,只需要求得左上半边和上左半边的三角形边宽度再镜像计算。</p>
</blockquote>
<pre><code class="language-javascript">&lt;script&gt;
    const canvas = document.getElementById("myCanvas");
const ctx = canvas.getContext("2d");

// 定义矩形的初始位置和速度
// let x = 0;
// let y = 250;
// let dx = 5; // 每帧移动的像素值
// const ad = 2; // 每次移动的间隔帧
// let cd = 0; // 当前移动间隔帧
let triangles = []; // 三角形数组
const triangleCount = 96; // 三角形数量

// 初始化三角形
function initTriangle() {
    const angleLeft = 180 / Math.PI * Math.atan(canvas.height / canvas.width); // 左直角三角形角度
    const angleTop = 90 - angleLeft; // 上直角三角形角度
    const angleBase = 360 / triangleCount; // 基础角度
    const radianBase = (angleBase * Math.PI) / 180; // 基础弧度

    // 计算左上半边和上左半边三角形的边长并存储
    const lengthLefts = [], lengthTops = [];
    for(let i = 0; i &lt; Math.floor(triangleCount / 8); i++) {
      let otherAngleLeft = angleLeft - angleBase * (i + 1);
      let otherAngleTop = angleTop - angleBase * (i + 1);

      let otherLengthLeft = Math.tan(otherAngleLeft * Math.PI / 180) * canvas.width / 2;
      let lengthLeft = (canvas.height / 2) - (otherLengthLeft &gt; 0 ? otherLengthLeft : 0);
      lengthLefts.push(lengthLeft);

      let otherLengthTop = Math.tan(otherAngleTop * Math.PI / 180) * canvas.height / 2;
      let lengthTop = (canvas.width / 2) - (otherLengthTop &gt; 0 ? otherLengthTop : 0);
      lengthTops.push(lengthTop);
    }

    // 根据计算结果生成三角形
    for(let i = 0; i &lt; 8;i ++) {
      for(let j = 0; j &lt; Math.floor(triangleCount / 8); j++) {
      // 根据不同位置生成三角形
      switch(i) {
          case 0:
            triangles.push({
            x1: canvas.width / 2, y1: canvas.height / 2,
            x2: lengthLefts || 0, y2: 0,
            x3: lengthLefts, y3: 0,
            color: generateRandomHex(2, true)
            })
            break;
         case 1:
            triangles.push({
            x1: canvas.width / 2, y1: canvas.height / 2,
            x2: (lengthLefts || 0) + canvas.width / 2, y2: 0,
            x3: lengthLefts + canvas.width / 2, y3: 0,
            color: generateRandomHex(2, true)
            })
            break;
         case 2:
            triangles.push({
            x1: canvas.width / 2, y1: canvas.height / 2,
            x2: canvas.width, y2: lengthTops || 0,
            x3: canvas.width, y3: lengthLefts,
            color: generateRandomHex(2, true)
            })
            break;
         case 3:
            triangles.push({
            x1: canvas.width / 2, y1: canvas.height / 2,
            x2: canvas.width, y2: (lengthTops || 0) + canvas.height / 2,
            x3: canvas.width, y3: lengthLefts + canvas.height / 2,
            color: generateRandomHex(2, true)
            })
            break;
         case 4:
            triangles.push({
            x1: canvas.width / 2, y1: canvas.height / 2,
            x2: canvas.width - (lengthLefts || 0), y2: canvas.height,
            x3: canvas.width - lengthLefts, y3: canvas.height,
            color: generateRandomHex(2, true)
            })
            break;
         case 5:
            triangles.push({
            x1: canvas.width / 2, y1: canvas.height / 2,
            x2: canvas.width - ((lengthLefts || 0) + canvas.width / 2), y2: canvas.height,
            x3: canvas.width - lengthLefts - canvas.width / 2, y3: canvas.height,
            color: generateRandomHex(2, true)
            })
            break;
         case 6:
            triangles.push({
            x1: canvas.width / 2, y1: canvas.height / 2,
            x2: 0, y2: canvas.height - (lengthTops || 0),
            x3: 0, y3: canvas.height - lengthTops,
            color: generateRandomHex(2, true)
            })
            break;
         case 7:
            triangles.push({
            x1: canvas.width / 2, y1: canvas.height / 2,
            x2: 0, y2: canvas.height - ((lengthTops || 0) + canvas.height / 2),
            x3: 0, y3: canvas.height - lengthTops - canvas.height / 2,
            color: generateRandomHex(2, true)
            })
            break;
          default:
            break;
      }
      }
    }
}

// 动画函数
// function animate() {
//   if (cd % ad !== 0) {
//   cd ++;
//   return;
//   }
//   triangles.push(triangles.shift());
//   // 清除画布
//   ctx.clearRect(0, 0, canvas.width, canvas.height);

//   // 绘制三角形
//   triangles.forEach(e =&gt; {
//   drawFilledTriangle(e.x1, e.y1, e.x2, e.y2, e.x3, e.y3, e.color);
//   })

//   // 下一帧
//   requestAnimationFrame(animate);
// }

// 绘制填充的三角形
function drawFilledTriangle(x1, y1, x2, y2, x3, y3, color) {
    // 绘制三角形路径
    ctx.beginPath();
    ctx.moveTo(x1, y1);
    ctx.lineTo(x2, y2);
    ctx.lineTo(x3, y3);
    ctx.closePath();

    // 设置填充颜色
    ctx.fillStyle = color; // 颜色
    ctx.fill(); // 填充三角形
}

// 生成随机16进制颜色
function generateRandomHex(length, isGrayLevelColor) {
    let result = '';
    const characters = '0123456789abcdef'; // 16进制字符集
    const charactersLength = characters.length;

    for (let i = 0; i &lt; length; i++) {
      result += characters.charAt(Math.floor(Math.random() * charactersLength));
    }

    return isGrayLevelColor ? `#${result}${result}${result}` : result;
}

// 初始化三角形
initTriangle()

// 循环动画
let count = 0;
let addFlag = true;
const repeatCount = 2;
const repeatTime = 200;

setInterval(() =&gt; {
    // 清除画布
    ctx.clearRect(0, 0, canvas.width, canvas.height);

    // 绘制三角形
    // 循环改变颜色
    triangles.forEach((e, i) =&gt; {
      let colorIndex = count + i
      if (colorIndex &gt;= triangles.length) {
      colorIndex = colorIndex - triangles.length
      }
      drawFilledTriangle(e.x1, e.y1, e.x2, e.y2, e.x3, e.y3, triangles.color);
    })
    // 循环改变颜色count
    if (count === 0) {
      addFlag = true;
    } else if (count === repeatCount) {
      addFlag = false;
    }
    if (addFlag) {
      count++;
    } else {
      count--;
    }
}, repeatTime)

// 开始动画
// animate();
&lt;/script&gt;
</code></pre>
<h2 id="2025年4月3日-完善功能">2025年4月3日 完善功能</h2>
<h2 id="功能-1">功能</h2>
<ul>
<li>完成在三角形区域内生成随机宽度的三角形</li>
<li>添加动画渲染模式</li>
</ul>
<h2 id="思路-1">思路</h2>
<p>实现功能1,写一个生成两个随机数数组的方法(generateRandomNum),在triangles根据不同位置生成三角形时带入相应的x2,x3或者y2,y3获取位置赋值即可。<br>
generateRandomNum中,num1 &lt; num2 时,返回的数组为顺序排序否则为倒序排序,使用sort和reverse方法即可。<br>
实现功能2,定义动画渲染模式的变量或者定量(mode),在定时器中绘制三角形时判断mode分为三种执行逻辑。</p>
<ul>
<li>循环改变颜色,循环替换相邻三角形的颜色</li>
<li>重新生成颜色,使用generateRandomHex方法返回的随机色替换之前的颜色</li>
<li>重新初始化,triangles设置为空,调用initTriangle方法</li>
</ul>
<h2 id="实现-1">实现</h2>
<h3 id="javascript-1">JavaScript</h3>
<pre><code class="language-javascript">/**
   * 生成两个随机数,根据num1和num2的大小关系决定生成的随机数范围和排序方式
   * @param {number} num1 - 第一个数字,用于确定随机数的范围
   * @param {number} num2 - 第二个数字,用于确定随机数的范围
   * @returns {Array} - 包含两个随机数的数组,根据num1和num2的大小关系决定是否排序或逆序
   */
function generateRandomNum(num1, num2) {
    // 计算两个数字之间的差值,用于确定随机数的范围
    const diff = Math.abs(num1 - num2);
   
    // 如果num1小于num2,生成两个在[num1, num2)范围内的随机数,并按升序排列
    if (num1 &lt; num2) {
      return [
      Math.floor(Math.random() * diff + num1),
      Math.floor(Math.random() * diff + num1),
      ].sort()
    }
   
    // 如果num1大于num2,生成两个在[num2, num1)范围内的随机数,并按降序排列
    else if (num1 &gt; num2) {
      return [
      Math.floor(Math.random() * diff + num2),
      Math.floor(Math.random() * diff + num2),
      ].sort().reverse()
    }
   
    // 如果num1等于num2,直接返回传入的参数,因为没有足够的范围来生成随机数
    else {
      return arguments
    }
}

/**
   * 动画渲染模式:
   * 0:相邻三角形颜色替换
   * 1:三角形随机颜色
   * 2:三角形重新初始化
*/
const mode = 1;

setInterval(() =&gt; {
    // 清除画布
    ctx.clearRect(0, 0, canvas.width, canvas.height);

    // 绘制三角形
    if (mode === 0) {
      // 循环改变颜色
      triangles.forEach((e, i) =&gt; {
      let colorIndex = count + i
      if (colorIndex &gt;= triangles.length) {
          colorIndex = colorIndex - triangles.length
      }
      drawFilledTriangle(e.x1, e.y1, e.x2, e.y2, e.x3, e.y3, triangles.color);
      })
    } else if (mode === 1) {
      // 重新生成颜色
      triangles.forEach((e, i) =&gt; {
      let color = generateRandomHex(2, true);
      e.color = color;
      drawFilledTriangle(e.x1, e.y1, e.x2, e.y2, e.x3, e.y3, e.color);
      })
    } else if (mode === 2) {
      // 重新初始化
      triangles = [];
      initTriangle();
      triangles.forEach((e, i) =&gt; {
      drawFilledTriangle(e.x1, e.y1, e.x2, e.y2, e.x3, e.y3, e.color);
      })
    }
    // 循环改变颜色count
    if (count === 0) {
      addFlag = true;
    } else if (count === repeatCount) {
      addFlag = false;
    }
    if (addFlag) {
      count++;
    } else {
      count--;
    }
}, repeatTime)
</code></pre>
<h2 id="全部代码">全部代码</h2>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang="cn-zh"&gt;
&lt;head&gt;
&lt;meta charset="UTF-8"&gt;
&lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
&lt;title&gt;speed-line&lt;/title&gt;
&lt;style&gt;
    #myCanvas {
      outline: 2px solid;
    }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;canvas id="myCanvas" width="500" height="500"&gt;&lt;/canvas&gt;
&lt;/body&gt;
&lt;script&gt;
const canvas = document.getElementById("myCanvas");
const ctx = canvas.getContext("2d");

// 定义矩形的初始位置和速度
let x = 0;
let y = 250;
let dx = 5; // 每帧移动的像素值
const ad = 2; // 每次移动的间隔帧
let cd = 0; // 当前移动间隔帧
let triangles = []; // 三角形数组
const triangleCount = 96; // 三角形数量

// 初始化三角形
function initTriangle() {
    const angleLeft = 180 / Math.PI * Math.atan(canvas.height / canvas.width); // 左直角三角形角度
    const angleTop = 90 - angleLeft; // 上直角三角形角度
    const angleBase = 360 / triangleCount; // 基础角度
    const radianBase = (angleBase * Math.PI) / 180; // 基础弧度

    // 计算左上半边和上左半边三角形的边长并存储
    const lengthLefts = [], lengthTops = [];
    for(let i = 0; i &lt; Math.floor(triangleCount / 8); i++) {
      let otherAngleLeft = angleLeft - angleBase * (i + 1);
      let otherAngleTop = angleTop - angleBase * (i + 1);

      let otherLengthLeft = Math.tan(otherAngleLeft * Math.PI / 180) * canvas.width / 2;
      let lengthLeft = (canvas.height / 2) - (otherLengthLeft &gt; 0 ? otherLengthLeft : 0);
      lengthLefts.push(lengthLeft);

      let otherLengthTop = Math.tan(otherAngleTop * Math.PI / 180) * canvas.height / 2;
      let lengthTop = (canvas.width / 2) - (otherLengthTop &gt; 0 ? otherLengthTop : 0);
      lengthTops.push(lengthTop);
    }

    // 根据计算结果生成三角形
    for(let i = 0; i &lt; 8;i ++) {
      for(let j = 0; j &lt; Math.floor(triangleCount / 8); j++) {
      let ramdomLinePositions = [];
      // 根据不同位置生成三角形
      switch(i) {
          case 0:
            ramdomLinePositions = generateRandomNum(lengthLefts || 0, lengthLefts)
            triangles.push({
            x1: canvas.width / 2, y1: canvas.height / 2,
            x2: ramdomLinePositions, y2: 0,
            x3: ramdomLinePositions, y3: 0,
            color: generateRandomHex(2, true)
            })
            break;
         case 1:
            ramdomLinePositions = generateRandomNum((lengthLefts || 0) + canvas.width / 2, lengthLefts + canvas.width / 2)
            triangles.push({
            x1: canvas.width / 2, y1: canvas.height / 2,
            x2: ramdomLinePositions, y2: 0,
            x3: ramdomLinePositions, y3: 0,
            color: generateRandomHex(2, true)
            })
            break;
         case 2:
            ramdomLinePositions = generateRandomNum(lengthTops || 0, lengthLefts)
            triangles.push({
            x1: canvas.width / 2, y1: canvas.height / 2,
            x2: canvas.width, y2: ramdomLinePositions,
            x3: canvas.width, y3: ramdomLinePositions,
            color: generateRandomHex(2, true)
            })
            break;
         case 3:
            ramdomLinePositions = generateRandomNum((lengthTops || 0) + canvas.height / 2, lengthLefts + canvas.height / 2)
            triangles.push({
            x1: canvas.width / 2, y1: canvas.height / 2,
            x2: canvas.width, y2: ramdomLinePositions,
            x3: canvas.width, y3: ramdomLinePositions,
            color: generateRandomHex(2, true)
            })
            break;
         case 4:
            ramdomLinePositions = generateRandomNum(canvas.width - (lengthLefts || 0), canvas.width - lengthLefts)
            triangles.push({
            x1: canvas.width / 2, y1: canvas.height / 2,
            x2: ramdomLinePositions, y2: canvas.height,
            x3: ramdomLinePositions, y3: canvas.height,
            color: generateRandomHex(2, true)
            })
            break;
         case 5:
            ramdomLinePositions = generateRandomNum(canvas.width - ((lengthLefts || 0) + canvas.width / 2), canvas.width - lengthLefts - canvas.width / 2)
            triangles.push({
            x1: canvas.width / 2, y1: canvas.height / 2,
            x2: ramdomLinePositions, y2: canvas.height,
            x3: ramdomLinePositions, y3: canvas.height,
            color: generateRandomHex(2, true)
            })
            break;
         case 6:
            ramdomLinePositions = generateRandomNum(canvas.height - (lengthTops || 0), canvas.height - lengthTops)
            triangles.push({
            x1: canvas.width / 2, y1: canvas.height / 2,
            x2: 0, y2: ramdomLinePositions,
            x3: 0, y3: ramdomLinePositions,
            color: generateRandomHex(2, true)
            })
            break;
         case 7:
            ramdomLinePositions = generateRandomNum(canvas.height - ((lengthTops || 0) + canvas.height / 2), canvas.height - lengthTops - canvas.height / 2)
            triangles.push({
            x1: canvas.width / 2, y1: canvas.height / 2,
            x2: 0, y2: ramdomLinePositions,
            x3: 0, y3: ramdomLinePositions,
            color: generateRandomHex(2, true)
            })
            break;
          default:
            break;
      }
      }
    }
}

// 动画函数
function animate() {
    if (cd % ad !== 0) {
      cd ++;
      return;
    }
    triangles.push(triangles.shift());
    // 清除画布
    ctx.clearRect(0, 0, canvas.width, canvas.height);

    // 绘制三角形
    triangles.forEach(e =&gt; {
      drawFilledTriangle(e.x1, e.y1, e.x2, e.y2, e.x3, e.y3, e.color);
    })

    // 下一帧
    requestAnimationFrame(animate);
}

// 绘制填充的三角形
function drawFilledTriangle(x1, y1, x2, y2, x3, y3, color) {
    // 绘制三角形路径
    ctx.beginPath();
    ctx.moveTo(x1, y1);
    ctx.lineTo(x2, y2);
    ctx.lineTo(x3, y3);
    ctx.closePath();

    // 设置填充颜色
    ctx.fillStyle = color; // 颜色
    ctx.fill(); // 填充三角形
}

/**
   * 生成随机十六进制颜色代码
   * 如果isGrayLevelColor为真,生成的将是灰度颜色
   *
   * @param {number} length - 生成十六进制代码的长度,如果isGrayLevelColor为真,这个长度会翻三倍
   * @param {boolean} isGrayLevelColor - 指定是否生成灰度颜色的标志
   * @return {string} 生成的随机十六进制颜色代码
   */
function generateRandomHex(length, isGrayLevelColor) {
      let result = '';
      const characters = '0123456789abcdef'; // 16进制字符集
      const charactersLength = characters.length;

      // 根据指定的长度生成随机的十六进制字符串
      for (let i = 0; i &lt; length; i++) {
          result += characters.charAt(Math.floor(Math.random() * charactersLength));
      }

      // 如果要求生成灰度颜色,则将生成的随机字符串重复三次,分别作为RGB值
      return isGrayLevelColor ? `#${result}${result}${result}` : result;
}

/**
   * 生成两个随机数,根据num1和num2的大小关系决定生成的随机数范围和排序方式
   * @param {number} num1 - 第一个数字,用于确定随机数的范围
   * @param {number} num2 - 第二个数字,用于确定随机数的范围
   * @returns {Array} - 包含两个随机数的数组,根据num1和num2的大小关系决定是否排序或逆序
   */
function generateRandomNum(num1, num2) {
    // 计算两个数字之间的差值,用于确定随机数的范围
    const diff = Math.abs(num1 - num2);
   
    // 如果num1小于num2,生成两个在[num1, num2)范围内的随机数,并按升序排列
    if (num1 &lt; num2) {
      return [
      Math.floor(Math.random() * diff + num1),
      Math.floor(Math.random() * diff + num1),
      ].sort()
    }
   
    // 如果num1大于num2,生成两个在[num2, num1)范围内的随机数,并按降序排列
    else if (num1 &gt; num2) {
      return [
      Math.floor(Math.random() * diff + num2),
      Math.floor(Math.random() * diff + num2),
      ].sort().reverse()
    }
   
    // 如果num1等于num2,直接返回传入的参数,因为没有足够的范围来生成随机数
    else {
      return arguments
    }
}

// 初始化三角形
initTriangle()

// 循环动画
let count = 0;
let addFlag = true;
const repeatCount = 2;
const repeatTime = 50;
/**
   * 动画渲染模式:
   * 0:相邻三角形颜色替换
   * 1:三角形随机颜色
   * 2:三角形重新初始化
*/
const mode = 1;

setInterval(() =&gt; {
    // 清除画布
    ctx.clearRect(0, 0, canvas.width, canvas.height);

    // 绘制三角形
    if (mode === 0) {
      // 循环改变颜色
      triangles.forEach((e, i) =&gt; {
      let colorIndex = count + i
      if (colorIndex &gt;= triangles.length) {
          colorIndex = colorIndex - triangles.length
      }
      drawFilledTriangle(e.x1, e.y1, e.x2, e.y2, e.x3, e.y3, triangles.color);
      })
    } else if (mode === 1) {
      // 重新生成颜色
      triangles.forEach((e, i) =&gt; {
      let color = generateRandomHex(2, true);
      e.color = color;
      drawFilledTriangle(e.x1, e.y1, e.x2, e.y2, e.x3, e.y3, e.color);
      })
    } else if (mode === 2) {
      // 重新初始化
      triangles = [];
      initTriangle();
      triangles.forEach((e, i) =&gt; {
      drawFilledTriangle(e.x1, e.y1, e.x2, e.y2, e.x3, e.y3, e.color);
      })
    }
    // 循环改变颜色count
    if (count === 0) {
      addFlag = true;
    } else if (count === repeatCount) {
      addFlag = false;
    }
    if (addFlag) {
      count++;
    } else {
      count--;
    }
}, repeatTime)

// 开始动画
// animate();
&lt;/script&gt;
&lt;/html&gt;
</code></pre><br><br>
来源:https://www.cnblogs.com/carrote/p/19593942
頁: [1]
查看完整版本: 前端实现速度线