博客 / 詳情

返回

前端實現速度線

功能

前端實現速度線,在矩形內生成黑白三角形且閃動。

思路

速度線可以使用多個角度相同的三角形分解矩形。三角形的渲染使用canvas連線fill就行,三角形在矩形上的兩個點可以通過計算每個三角形的邊長來獲取。三角形在矩形上的邊長使用三角函數獲取。

  1. HTML結構:包含一個畫布(Canvas)用於顯示圖片。
  2. CSS樣式:定義了頁面的基本佈局和樣式,並設置了畫布的大小和邊框。
  3. JavaScript功能:
    • 處理三角形點、顏色。
    • 循環更改顏色並渲染。

實現

[video(video-vfZFJk6W-1743643905911)(type-csdn)(url-https://live.csdn.net/v/embed/472154)(image-https://i-blog.csdnimg.cn/20230724024159.png?be=1&origin_url=https://v-blog.csdnimg.cn/asset/a5b18538725bb3b2d6b61e68d52f49c8/cover/Cover0.jpg)(title-前端速度線效果)]

HTML結構

添加 元素

<!DOCTYPE html>
<html lang="cn-zh">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>SpeedLine</title>
  <style>
    #myCanvas {
      outline: 2px solid;
    }
  </style>
</head>
<body>
  <canvas id="myCanvas" width="500" height="500"></canvas>
</body>
</body>
</html>

JavaScript

獲取canvas元素和canvas渲染上下文,初始化三角形,繪製填充的三角形,循環動畫

使用三角函數可以根據矩形長寬獲取矩形中心點到矩形四個頂點的夾角,根據三角形數量參數獲得每個三角形的角度。
使用Math.tan傳的參數是弧度,弧度 = 角度 * PI / 180。
不需要求出所有三角形在矩形上的那一邊寬度,只需要求得左上半邊和上左半邊的三角形邊寬度再鏡像計算。

<script>
    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 < 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 > 0 ? otherLengthLeft : 0);
      lengthLefts.push(lengthLeft);

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

    // 根據計算結果生成三角形
    for(let i = 0; i < 8;i ++) {
      for(let j = 0; j < Math.floor(triangleCount / 8); j++) {
        // 根據不同位置生成三角形
        switch(i) {
          case 0:
            triangles.push({
              x1: canvas.width / 2, y1: canvas.height / 2,
              x2: lengthLefts[j - 1] || 0, y2: 0,
              x3: lengthLefts[j], y3: 0,
              color: generateRandomHex(2, true)
            })
            break;
         case 1:
            triangles.push({
              x1: canvas.width / 2, y1: canvas.height / 2,
              x2: (lengthLefts[j - 1] || 0) + canvas.width / 2, y2: 0,
              x3: lengthLefts[j] + 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[j - 1] || 0,
              x3: canvas.width, y3: lengthLefts[j],
              color: generateRandomHex(2, true)
            })
            break;
         case 3:
            triangles.push({
              x1: canvas.width / 2, y1: canvas.height / 2,
              x2: canvas.width, y2: (lengthTops[j - 1] || 0) + canvas.height / 2,
              x3: canvas.width, y3: lengthLefts[j] + canvas.height / 2,
              color: generateRandomHex(2, true)
            })
            break;
         case 4:
            triangles.push({
              x1: canvas.width / 2, y1: canvas.height / 2,
              x2: canvas.width - (lengthLefts[j - 1] || 0), y2: canvas.height,
              x3: canvas.width - lengthLefts[j], y3: canvas.height,
              color: generateRandomHex(2, true)
            })
            break;
         case 5:
            triangles.push({
              x1: canvas.width / 2, y1: canvas.height / 2,
              x2: canvas.width - ((lengthLefts[j - 1] || 0) + canvas.width / 2), y2: canvas.height,
              x3: canvas.width - lengthLefts[j] - 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[j - 1] || 0),
              x3: 0, y3: canvas.height - lengthTops[j],
              color: generateRandomHex(2, true)
            })
            break;
         case 7:
            triangles.push({
              x1: canvas.width / 2, y1: canvas.height / 2,
              x2: 0, y2: canvas.height - ((lengthTops[j - 1] || 0) + canvas.height / 2),
              x3: 0, y3: canvas.height - lengthTops[j] - 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 => {
  //     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 < 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(() => {
    // 清除畫布
    ctx.clearRect(0, 0, canvas.width, canvas.height);

    // 繪製三角形
    // 循環改變顏色
    triangles.forEach((e, i) => {
      let colorIndex = count + i
      if (colorIndex >= triangles.length) {
        colorIndex = colorIndex - triangles.length
      }
      drawFilledTriangle(e.x1, e.y1, e.x2, e.y2, e.x3, e.y3, triangles[colorIndex].color);
    })
    // 循環改變顏色count
    if (count === 0) {
      addFlag = true;
    } else if (count === repeatCount) {
      addFlag = false;
    }
    if (addFlag) {
      count++;
    } else {
      count--;
    }
  }, repeatTime)

  // 開始動畫
  // animate();
</script>

2025年4月3日 完善功能

功能

  • 完成在三角形區域內生成隨機寬度的三角形
  • 添加動畫渲染模式

思路

實現功能1,寫一個生成兩個隨機數數組的方法(generateRandomNum),在triangles根據不同位置生成三角形時帶入相應的x2,x3或者y2,y3獲取位置賦值即可。
generateRandomNum中,num1 < num2 時,返回的數組為順序排序否則為倒序排序,使用sort和reverse方法即可。
實現功能2,定義動畫渲染模式的變量或者定量(mode),在定時器中繪製三角形時判斷mode分為三種執行邏輯。

  • 循環改變顏色,循環替換相鄰三角形的顏色
  • 重新生成顏色,使用generateRandomHex方法返回的隨機色替換之前的顏色
  • 重新初始化,triangles設置為空,調用initTriangle方法

實現

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 < num2) {
      return [
        Math.floor(Math.random() * diff + num1),
        Math.floor(Math.random() * diff + num1),
      ].sort()
    }
    
    // 如果num1大於num2,生成兩個在[num2, num1)範圍內的隨機數,並按降序排列
    else if (num1 > 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(() => {
    // 清除畫布
    ctx.clearRect(0, 0, canvas.width, canvas.height);

    // 繪製三角形
    if (mode === 0) {
      // 循環改變顏色
      triangles.forEach((e, i) => {
        let colorIndex = count + i
        if (colorIndex >= triangles.length) {
          colorIndex = colorIndex - triangles.length
        }
        drawFilledTriangle(e.x1, e.y1, e.x2, e.y2, e.x3, e.y3, triangles[colorIndex].color);
      })
    } else if (mode === 1) {
      // 重新生成顏色
      triangles.forEach((e, i) => {
        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) => {
        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)

全部代碼

<!DOCTYPE html>
<html lang="cn-zh">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>speed-line</title>
  <style>
    #myCanvas {
      outline: 2px solid;
    }
  </style>
</head>
<body>
  <canvas id="myCanvas" width="500" height="500"></canvas>
</body>
<script>
  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 < 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 > 0 ? otherLengthLeft : 0);
      lengthLefts.push(lengthLeft);

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

    // 根據計算結果生成三角形
    for(let i = 0; i < 8;i ++) {
      for(let j = 0; j < Math.floor(triangleCount / 8); j++) {
        let ramdomLinePositions = [];
        // 根據不同位置生成三角形
        switch(i) {
          case 0:
            ramdomLinePositions = generateRandomNum(lengthLefts[j - 1] || 0, lengthLefts[j])
            triangles.push({
              x1: canvas.width / 2, y1: canvas.height / 2,
              x2: ramdomLinePositions[0], y2: 0,
              x3: ramdomLinePositions[1], y3: 0,
              color: generateRandomHex(2, true)
            })
            break;
         case 1:
            ramdomLinePositions = generateRandomNum((lengthLefts[j - 1] || 0) + canvas.width / 2, lengthLefts[j] + canvas.width / 2)
            triangles.push({
              x1: canvas.width / 2, y1: canvas.height / 2,
              x2: ramdomLinePositions[0], y2: 0,
              x3: ramdomLinePositions[1], y3: 0,
              color: generateRandomHex(2, true)
            })
            break;
         case 2:
            ramdomLinePositions = generateRandomNum(lengthTops[j - 1] || 0, lengthLefts[j])
            triangles.push({
              x1: canvas.width / 2, y1: canvas.height / 2,
              x2: canvas.width, y2: ramdomLinePositions[0],
              x3: canvas.width, y3: ramdomLinePositions[1],
              color: generateRandomHex(2, true)
            })
            break;
         case 3:
            ramdomLinePositions = generateRandomNum((lengthTops[j - 1] || 0) + canvas.height / 2, lengthLefts[j] + canvas.height / 2)
            triangles.push({
              x1: canvas.width / 2, y1: canvas.height / 2,
              x2: canvas.width, y2: ramdomLinePositions[0],
              x3: canvas.width, y3: ramdomLinePositions[1],
              color: generateRandomHex(2, true)
            })
            break;
         case 4:
            ramdomLinePositions = generateRandomNum(canvas.width - (lengthLefts[j - 1] || 0), canvas.width - lengthLefts[j])
            triangles.push({
              x1: canvas.width / 2, y1: canvas.height / 2,
              x2: ramdomLinePositions[0], y2: canvas.height,
              x3: ramdomLinePositions[1], y3: canvas.height,
              color: generateRandomHex(2, true)
            })
            break;
         case 5:
            ramdomLinePositions = generateRandomNum(canvas.width - ((lengthLefts[j - 1] || 0) + canvas.width / 2), canvas.width - lengthLefts[j] - canvas.width / 2)
            triangles.push({
              x1: canvas.width / 2, y1: canvas.height / 2,
              x2: ramdomLinePositions[0], y2: canvas.height,
              x3: ramdomLinePositions[1], y3: canvas.height,
              color: generateRandomHex(2, true)
            })
            break;
         case 6:
            ramdomLinePositions = generateRandomNum(canvas.height - (lengthTops[j - 1] || 0), canvas.height - lengthTops[j])
            triangles.push({
              x1: canvas.width / 2, y1: canvas.height / 2,
              x2: 0, y2: ramdomLinePositions[0],
              x3: 0, y3: ramdomLinePositions[1],
              color: generateRandomHex(2, true)
            })
            break;
         case 7:
            ramdomLinePositions = generateRandomNum(canvas.height - ((lengthTops[j - 1] || 0) + canvas.height / 2), canvas.height - lengthTops[j] - canvas.height / 2)
            triangles.push({
              x1: canvas.width / 2, y1: canvas.height / 2,
              x2: 0, y2: ramdomLinePositions[0],
              x3: 0, y3: ramdomLinePositions[1],
              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 => {
      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 < 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 < num2) {
      return [
        Math.floor(Math.random() * diff + num1),
        Math.floor(Math.random() * diff + num1),
      ].sort()
    }
    
    // 如果num1大於num2,生成兩個在[num2, num1)範圍內的隨機數,並按降序排列
    else if (num1 > 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(() => {
    // 清除畫布
    ctx.clearRect(0, 0, canvas.width, canvas.height);

    // 繪製三角形
    if (mode === 0) {
      // 循環改變顏色
      triangles.forEach((e, i) => {
        let colorIndex = count + i
        if (colorIndex >= triangles.length) {
          colorIndex = colorIndex - triangles.length
        }
        drawFilledTriangle(e.x1, e.y1, e.x2, e.y2, e.x3, e.y3, triangles[colorIndex].color);
      })
    } else if (mode === 1) {
      // 重新生成顏色
      triangles.forEach((e, i) => {
        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) => {
        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();
</script>
</html>
user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.