驚艷同事的 Canvas 事件流程圖,這篇教會你

演示效果

HTML5 Canvas 繪製一個高顏值、支持互動的事件流程圖,展示從起飛降落的完整飛行事件時間線,包含播放 / 暫停 / 重置動畫控制功能。


大家複製代碼時,可能會因格式轉換出現錯亂,導致樣式失效。建議先少量複製代碼進行測試,若未能解決問題,私信回覆源码兩字,我會發送完整的壓縮包給你。

演示效果

演示效果

演示效果

HTML&CSS

<!DOCTYPE html>
<html lang="zh-TW">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Canvas事件流程圖</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
        }

        body {
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            min-height: 100vh;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            padding: 20px;
            color: #333;
        }

        .container {
            width: 100%;
            max-width: 1200px;
            background-color: rgba(255, 255, 255, 0.95);
            border-radius: 15px;
            box-shadow: 0 15px 30px rgba(0, 0, 0, 0.2);
            overflow: hidden;
            padding: 20px;
        }

        header {
            text-align: center;
            padding: 20px 0;
            margin-bottom: 20px;
            border-bottom: 1px solid #eee;
        }

        h1 {
            font-size: 2.5rem;
            color: #4a4a4a;
            margin-bottom: 10px;
            background: linear-gradient(to right, #667eea, #764ba2);
            -webkit-background-clip: text;
            -webkit-text-fill-color: transparent;
        }

        .description {
            color: #666;
            font-size: 1.1rem;
            max-width: 800px;
            margin: 0 auto;
            line-height: 1.6;
        }

        .canvas-container {
            position: relative;
            width: 100%;
            height: 500px;
            margin: 20px 0;
            border-radius: 10px;
            overflow: hidden;
            background-color: #f8f9fa;
            box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
        }

        canvas {
            display: block;
            width: 100%;
            height: 100%;
        }

        .controls {
            display: flex;
            justify-content: center;
            gap: 15px;
            margin: 20px 0;
            flex-wrap: wrap;
        }

        button {
            padding: 12px 25px;
            border: none;
            border-radius: 50px;
            background: linear-gradient(to right, #667eea, #764ba2);
            color: white;
            font-weight: 600;
            cursor: pointer;
            transition: all 0.3s ease;
            box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15);
        }

        button:hover {
            transform: translateY(-3px);
            box-shadow: 0 7px 15px rgba(0, 0, 0, 0.2);
        }

        button:active {
            transform: translateY(0);
        }

        footer {
            text-align: center;
            margin-top: 30px;
            color: rgba(255, 255, 255, 0.8);
            font-size: 0.9rem;
        }

        @media (max-width: 768px) {
            h1 {
                font-size: 2rem;
            }

            .canvas-container {
                height: 400px;
            }
        }
    </style>
</head>
<body>
    <div class="container">
        <header>
            <h1>事件流程圖</h1>
            <p class="description">使用Canvas實現的高顏值事件流程圖,展示從起飛到降落的完整飛行過程,支持互動和動畫效果。</p>
        </header>

        <div class="canvas-container">
            <canvas id="flowchartCanvas"></canvas>
        </div>

        <div class="controls">
            <button id="playBtn">播放動畫</button>
            <button id="pauseBtn">暫停動畫</button>
            <button id="resetBtn">重置視圖</button>
        </div>
    </div>

    <footer>
        <p>© 2025 事件流程圖 - 使用HTML5 Canvas實現</p>
    </footer>

    <script>
        document.addEventListener('DOMContentLoaded', function () {
            const canvas = document.getElementById('flowchartCanvas');
            const ctx = canvas.getContext('2d');
            const playBtn = document.getElementById('playBtn');
            const pauseBtn = document.getElementById('pauseBtn');
            const resetBtn = document.getElementById('resetBtn');

            // 設置Canvas尺寸
            function resizeCanvas() {
                canvas.width = canvas.offsetWidth;
                canvas.height = canvas.offsetHeight;
                drawFlowchart();
            }

            // 初始化事件資料
            const events = [
                { time: '2025-09-12 11:40', event: '起飛', position: 0.05 },
                { time: '2025-09-12 11:42', event: '轉彎', position: 0.15 },
                { time: '2025-09-12 11:42', event: '發現問題', position: 0.25 },
                { time: '2025-09-12 11:51', event: '返航', position: 0.35 },
                { time: '2025-09-12 11:53', event: '飛行', position: 0.45 },
                { time: '2025-09-12 11:55', event: '轉彎', position: 0.55 },
                { time: '2025-09-12 12:00', event: '飛行', position: 0.65 },
                { time: '2025-09-12 12:30', event: '降落', position: 0.75 },
                { time: '2025-09-12 12:30', event: '降落', position: 0.85 },
                { time: '2025-09-12 13:41', event: '返航', position: 0.95 }
            ];

            // 動畫狀態
            let animationProgress = 0;
            let animationId = null;
            let isAnimating = false;

            // 繪製流程圖
            function drawFlowchart() {
                const width = canvas.width;
                const height = canvas.height;
                const timelineY = height / 2;
                const nodeRadius = 12;

                // 清除畫布
                ctx.clearRect(0, 0, width, height);

                // 繪製時間軸
                ctx.beginPath();
                ctx.moveTo(width * 0.05, timelineY);
                ctx.lineTo(width * 0.95, timelineY);
                ctx.strokeStyle = '#667eea';
                ctx.lineWidth = 3;
                ctx.stroke();

                // 繪製箭頭
                ctx.beginPath();
                ctx.moveTo(width * 0.95, timelineY);
                ctx.lineTo(width * 0.93, timelineY - 8);
                ctx.lineTo(width * 0.93, timelineY + 8);
                ctx.closePath();
                ctx.fillStyle = '#667eea';
                ctx.fill();

                // 繪製事件節點和標籤
                events.forEach((ev, index) => {
                    const x = width * ev.position;
                    const isEven = index % 2 === 0;
                    const nodeY = isEven ? timelineY - 50 : timelineY + 50;

                    // 繪製連接線
                    ctx.beginPath();
                    ctx.moveTo(x, timelineY);
                    ctx.lineTo(x, nodeY);
                    ctx.strokeStyle = '#adb5bd';
                    ctx.lineWidth = 1.5;
                    ctx.setLineDash([5, 3]);
                    ctx.stroke();
                    ctx.setLineDash([]);

                    // 繪製節點
                    ctx.beginPath();
                    ctx.arc(x, nodeY, nodeRadius, 0, Math.PI * 2);
                    ctx.fillStyle = isAnimating && animationProgress >= ev.position ? '#F2050A' : '#667eea';
                    ctx.fill();
                    ctx.strokeStyle = 'white';
                    ctx.lineWidth = 2;
                    ctx.stroke();

                    // 繪製事件文本
                    ctx.font = '14px Segoe UI, sans-serif';
                    ctx.textAlign = 'center';
                    ctx.textBaseline = 'middle';
                    ctx.fillStyle = '#495057';
                    ctx.fillText(ev.event, x, nodeY + (isEven ? -30 : 30));

                    // 繪製時間文本
                    ctx.font = '12px Segoe UI, sans-serif';
                    ctx.fillStyle = '#6c757d';
                    ctx.fillText(ev.time, x, nodeY + (isEven ? -50 : 50));
                });

                // 繪製動畫進度
                if (isAnimating) {
                    ctx.beginPath();
                    ctx.moveTo(width * 0.05, timelineY);
                    ctx.lineTo(width * animationProgress, timelineY);
                    ctx.strokeStyle = '#F2050A';
                    ctx.lineWidth = 4;
                    ctx.stroke();
                }
            }

            // 動畫函數
            function animate() {
                if (animationProgress < 0.95) {
                    animationProgress += 0.005;
                    drawFlowchart();
                    animationId = requestAnimationFrame(animate);
                } else {
                    isAnimating = false;
                }
            }

            // 事件監聽器
            playBtn.addEventListener('click', function () {
                if (!isAnimating) {
                    isAnimating = true;
                    animate();
                }
            });

            pauseBtn.addEventListener('click', function () {
                if (isAnimating) {
                    cancelAnimationFrame(animationId);
                    isAnimating = false;
                }
            });

            resetBtn.addEventListener('click', function () {
                if (isAnimating) {
                    cancelAnimationFrame(animationId);
                    isAnimating = false;
                }
                animationProgress = 0;
                drawFlowchart();
            });

            // 初始化和響應式調整
            window.addEventListener('resize', resizeCanvas);
            resizeCanvas();

            // 初始繪製
            drawFlowchart();
        });
    </script>
</body>
</html>

HTML

  • container:包裹所有內容的核心容器,用於統一控制頁面佈局與背景
  • header:包含頁面標題與描述,用於引導用戶理解頁面功能
  • h1:頁面主標題,視覺焦點之一
  • description:說明頁面用途(飛行過程事件流展示),提升用戶體驗
  • canvas-container:包裹 Canvas 標籤,用於控制畫布的尺寸、陰影與邊框等樣式
  • flowchartCanvas:核心繪圖元素,通過 JavaScript 獲取其上下文(getContext('2d'))實現繪圖
  • controls:包含 “播放”“暫停”“重置” 三個按鈕,用於控制動畫互動
  • playBtn:觸發動畫播放的互動入口
  • pauseBtn:觸發動畫暫停的互動入口
  • resetBtn:將畫布恢復到初始狀態的互動入口
  • footer:顯示版權資訊,提升頁面完整性

CSS

  • .container :控制核心容器的寬度(最大 1200px)、白色半透明背景、圓角與陰影,增強立體感
  • h1 :通過背景裁剪實現文字漸變效果,替代傳統純色文字
  • .canvas-container :固定畫布高度(500px)、淺灰色背景與輕微陰影,讓畫布與容器區分開
  • button :按鈕採用圓角(50px)、漸變背景、陰影,transition 實現 hover 動畫過渡
  • button:hover:滑鼠懸浮時按鈕向上偏移 3px,陰影加深,增強互動反饋
  • button:active:點擊按鈕時恢復原位置,模擬 “按壓” 手感
  • @media (max-width: 768px):在移動設備上縮小標題字體、降低畫布高度(400px),避免內容溢出

JavaScript 部分:互動與動畫實現

負責 Canvas 繪圖、動畫控制與用戶互動邏輯,核心分為初始化繪圖動畫事件監聽四大模塊。

1. 初始化:準備工作

首先通過 DOMContentLoaded 事件確保 DOM 加載完成後再執行代碼,避免獲取不到元素的問題:

document.addEventListener('DOMContentLoaded', function () {
  // 1. 獲取DOM元素
  const canvas = document.getElementById('flowchartCanvas');
  const ctx = canvas.getContext('2d'); // 獲取2D繪圖上下文(核心)
  const playBtn = document.getElementById('playBtn');
  const pauseBtn = document.getElementById('pauseBtn');
  const resetBtn = document.getElementById('resetBtn');

  // 2. 響應式調整Canvas尺寸
  function resizeCanvas() {
    canvas.width = canvas.offsetWidth; // 讓Canvas寬度等於父容器寬度
    canvas.height = canvas.offsetHeight; // 讓Canvas高度等於父容器高度
    drawFlowchart(); // 尺寸變化後重新繪圖
  }

  // 3. 定義事件資料(飛行過程的關鍵事件)
  const events = [
    { time: '2025-09-12 11:40', event: '起飛', position: 0.05 }, // position:事件在時間軸上的比例(0~1)
    { time: '2025-09-12 11:42', event: '轉彎', position: 0.15 },
    { time: '2025-09-12 11:42', event: '發現問題', position: 0.25 },
    { time: '2025-09-12 11:51', event: '返航', position: 0.35 },
    { time: '2025-09-12 11:53', event: '飛行', position: 0.45 },
    { time: '2025-09-12 11:55', event: '轉彎', position: 0.55 },
    { time: '2025-09-12 12:00', event: '飛行', position: 0.65 },
    { time: '2025-09-12 12:30', event: '降落', position: 0.75 },
    { time: '2025-09-12 12:30', event: '降落', position: 0.85 },
    { time: '2025-09-12 13:41', event: '返航', position: 0.95 }
  ];

  // 4. 動畫狀態變數
  let animationProgress = 0; // 動畫進度(0~0.95,對應時間軸比例)
  let animationId = null; // 動畫請求ID(用於暫停動畫)
  let isAnimating = false; // 動畫是否正在播放
});

2. 核心函數:drawFlowchart () 繪圖邏輯

該函數是 Canvas 繪圖的核心,負責繪製時間軸、箭頭、事件節點、事件文本,並根據動畫進度更新節點顏色:

function drawFlowchart() {
  const width = canvas.width; // 畫布寬度
  const height = canvas.height; // 畫布高度
  const timelineY = height / 2; // 時間軸的Y坐標(垂直居中)
  const nodeRadius = 12; // 事件節點的半徑

  // 步驟1:清除畫布(每次繪圖前清空,避免重疊)
  ctx.clearRect(0, 0, width, height);

  // 步驟2:繪製時間軸(水平直線)
  ctx.beginPath(); // 開始路徑繪製
  ctx.moveTo(width * 0.05, timelineY); // 起點(左側留5%空白)
  ctx.lineTo(width * 0.95, timelineY); // 终点(右側留5%空白)
  ctx.strokeStyle = '#667eea'; // 線條顏色(紫藍色)
  ctx.lineWidth = 3; // 線條寬度
  ctx.stroke(); // 執行繪製

  // 步驟3:繪製時間軸箭頭(終點處的三角形)
  ctx.beginPath();
  ctx.moveTo(width * 0.95, timelineY); // 箭頭頂點
  ctx.lineTo(width * 0.93, timelineY - 8); // 左上點
  ctx.lineTo(width * 0.93, timelineY + 8); // 左下點
  ctx.closePath(); // 閉合路徑(形成三角形)
  ctx.fillStyle = '#667eea'; // 填充顏色
  ctx.fill(); // 執行填充

  // 步驟4:繪製每個事件的節點、連接線與文本
  events.forEach((ev, index) => {
    const x = width * ev.position; // 事件節點的X坐標(按比例計算)
    const isEven = index % 2 === 0; // 判斷索引是否為偶數(控制節點在時間軸上下兩側)
    const nodeY = isEven ? timelineY - 50 : timelineY + 50; // 節點Y坐標(偶數在上,奇數在下)

    // 4.1 繪製連接線(時間軸到節點的虛線)
    ctx.beginPath();
    ctx.moveTo(x, timelineY); // 起點(時間軸上的點)
    ctx.lineTo(x, nodeY); // 终点(事件節點)
    ctx.strokeStyle = '#adb5bd'; // 虛線顏色(淺灰色)
    ctx.lineWidth = 1.5; // 線條寬度
    ctx.setLineDash([5, 3]); // 設置虛線樣式(5px實線,3px空白)
    ctx.stroke();
    ctx.setLineDash([]); // 重置為實線(避免影響後續繪圖)

    // 4.2 繪製事件節點(圓形)
    ctx.beginPath();
    ctx.arc(x, nodeY, nodeRadius, 0, Math.PI * 2); // 畫圓(x,y,半徑,起始角度,結束角度)
    // 節點顏色:動畫進度覆蓋時為紅色(#F2050A),否則為紫藍色
    ctx.fillStyle = isAnimating && animationProgress >= ev.position ? '#F2050A' : '#667eea';
    ctx.fill(); // 填充圓形
    ctx.strokeStyle = 'white'; // 節點邊框顏色(白色)
    ctx.lineWidth = 2; // 邊框寬度
    ctx.stroke(); // 繪製邊框

    // 4.3 繪製事件名稱(如“起飛”“轉彎”)
    ctx.font = '14px Segoe UI, sans-serif'; // 字體樣式
    ctx.textAlign = 'center'; // 文本水平居中
    ctx.textBaseline = 'middle'; // 文本垂直居中
    ctx.fillStyle = '#495057'; // 文本顏色(深灰色)
    ctx.fillText(ev.event, x, nodeY + (isEven ? -30 : 30)); // 文本位置(節點上下30px處)

    // 4.4 繪製事件時間(如“2025-09-12 11:40”)
    ctx.font = '12px Segoe UI, sans-serif'; // 字體縮小
    ctx.fillStyle = '#6c757d'; // 文本顏色(淺灰色)
    ctx.fillText(ev.time, x, nodeY + (isEven ? -50 : 50)); // 文本位置(事件名稱外側)
  });

  // 步驟5:繪製動畫進度條(紅色實線,跟隨動畫進度)
  if (isAnimating) {
    ctx.beginPath();
    ctx.moveTo(width * 0.05, timelineY); // 起點(與時間軸一致)
    ctx.lineTo(width * animationProgress, timelineY); // 终点(隨進度變化)
    ctx.strokeStyle = '#F2050A'; // 進度條顏色(紅色)
    ctx.lineWidth = 4; // 進度條寬度(比時間軸粗)
    ctx.stroke();
  }
}

3. 動畫控制:animate () 與按鈕事件

動畫通過 requestAnimationFrame(瀏覽器原生動畫 API)實現,配合按鈕事件控制播放、暫停、重置:

// 動畫函數:逐幀更新進度並重新繪圖
function animate() {
  if (animationProgress < 0.95) { // 進度未到終點(0.95)
    animationProgress += 0.005; // 每次幀更新進度(控制動畫速度)
    drawFlowchart(); // 重新繪圖(更新進度條與節點顏色)
    animationId = requestAnimationFrame(animate); // 請求下一幀動畫
  } else {
    isAnimating = false; // 進度到終點,停止動畫
  }
}

// 播放按鈕事件:啟動動畫(僅當未播放時)
playBtn.addEventListener('click', function () {
  if (!isAnimating) {
    isAnimating = true;
    animate();
  }
});

// 暫停按鈕事件:取消動畫請求(停止動畫)
pauseBtn.addEventListener('click', function () {
  if (isAnimating) {
    cancelAnimationFrame(animationId); // 取消下一幀動畫
    isAnimating = false;
  }
});

// 重置按鈕事件:恢復初始狀態
resetBtn.addEventListener('click', function () {
  if (isAnimating) {
    cancelAnimationFrame(animationId); // 先停止動畫
    isAnimating = false;
  }
  animationProgress = 0; // 進度重置為0
  drawFlowchart(); // 重新繪圖(恢復初始樣式)
});

// 窗口 resize 事件:響應式調整畫布尺寸
window.addEventListener('resize', resizeCanvas);

// 初始化:首次加載時調整尺寸並繪圖
resizeCanvas();
drawFlowchart();

各位互聯網搭子,要是這篇文章成功引起了你的注意,別猶豫,關注、點讚、評論、分享走一波,讓我們把這份默契延續下去,一起在知識的海洋裡乘風破浪!


原文出處:https://juejin.cn/post/7548755697980571683


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝11   💬6   ❤️6
473
🥈
我愛JS
📝1   💬5   ❤️4
92
🥉
AppleLily
📝1   💬4   ❤️1
53
#4
💬1  
5
#5
xxuan
💬1  
3
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次