阅读视图

发现新文章,点击刷新页面。

惊艳同事的 Canvas 事件流程图,这篇教会你

HTML5 Canvas 绘制一个高颜值、支持交互的事件流程图,展示从起飞降落的完整飞行事件时间线,包含播放 / 暂停 / 重置动画控制功能。


大家复制代码时,可能会因格式转换出现错乱,导致样式失效。建议先少量复制代码进行测试,若未能解决问题,私信回复源码两字,我会发送完整的压缩包给你。

演示效果

演示效果

演示效果

HTML&CSS


<!DOCTYPE html>
<html lang="zh-CN">

<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;
            }

            .event-list {
                grid-template-columns: 1fr;
            }
        }
    </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();

各位互联网搭子,要是这篇文章成功引起了你的注意,别犹豫,关注、点赞、评论、分享走一波,让我们把这份默契延续下去,一起在知识的海洋里乘风破浪!

HTML&CSS:有趣的漂流瓶

这个 HTML 文件是一个漂流瓶效果网页,展示了一个动态的漂流瓶效果,包含液体、光影和动画效果。 大家复制代码时,可能会因格式转换出现错乱,导致样式失效。建议先少量复制代码进行测试,若未能解决问题,私

只用2行CSS实现响应式布局,比媒体查询更优雅的布局方案

你是不是有过这样的经历:辛辛苦苦做好的网页,在电脑上看起来整整齐齐,但一放到手机或平板上,布局就变得乱七八糟,要么挤成一团,要么空白一大片。

然后你上网一搜解决办法,全都是让你写一堆看起来超复杂的 @media(媒体查询)代码,不同的屏幕尺寸就要写不同的样式,光是想想就头大。

别担心!其实,只需要 2 行 CSS 代码,就能让你的一排排卡片、图片、内容块自动适应屏幕宽度,再也不用写繁琐的媒体查询了!

这就是 CSS Grid 的 auto-fillauto-fit 属性。

先想再学

假设我们要做一排卡片,每个卡片最小宽度是 200px。我们希望:

  • 屏幕很宽时,自动显示很多列,并且每列平均分配剩余空间。

  • 屏幕变窄时,卡片自动换行,保证布局不会乱。

以往:可能需要写 3-4 个媒体查询,分别针对手机、平板、电脑。

现在:只需要给卡片的爸爸(父容器)加上 2 行 CSS!

.container {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 16px; /* (可选)增加卡片之间的间距 */
}

把这段代码放到样式里,无论你怎么拉浏览器的窗口,卡片都会自动调整排列

auto-fit演示效果

代码拆解:理解每部分的作用

这行核心代码 grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); 看起来有点复杂,我们把它拆开揉碎了讲,保证你能懂!

display: grid:将元素定义为网格容器

grid-template-columns:定义网格的列数和每列的宽度

repeat() :重复函数,避免重复书写相同的模式

auto-fit:自动调整网格轨道数量以填充容器

minmax(200px, 1fr) :定义每列的最小和最大宽度

minmax 函数详解

  • minmax()函数接受两个参数:最小值和最大值。例如:

    minmax(200px, 1fr)表示列宽最小 200px,最大为 1fr(剩余空间的等分)

  • minmax(auto, 300px)表示高度自适应内容,但不超过 300px

fr 单位的作用

  • fr(fraction)单位按比例分配剩余空间。例如:

grid-template-columns: 2fr 1fr 1fr 会创建三列,比例为 2:1:1

auto-fill vs auto-fit:关键区别

虽然两者都用于自动创建网格,但它们的行为有重要区别:

特性 auto-fill auto-fit
行为特点 保留空轨道,保持网格结构,内容不拉伸 折叠空轨道,让内容拉伸
内容拉伸 不拉伸内容 拉伸内容填满容器
适用场景 需要固定网格数的布局(日历、表格、固定布局) 需要内容自适应填满的布局(卡片、图片)

auto-fill (填充模式) :它会老老实实地生成 10 个位置,5 个有内容块,另外 5 个是空的但占着位置。这样你的 5 个块宽度就是固定的 200px,不会变。

auto-fit (适配模式) :它很聪明!它发现后面 5 个位置是空的,就会把它们折叠掉,然后让已有的 5 个内容块自动拉伸变宽,平分一整行的空间

auto-fit和auto-fill 对比演示

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>auto-fit 和 auto-fill 对比演示</title>
    <style>
        .container {
            display: grid;
            gap: 16px; /* 设置网格间隙 */
        }
        /* auto-fill 效果 - 保留空位 */
        .container.fill {
            grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
            background-color: #ffe6e6; /* 浅红色背景,方便看容器范围 */
        }
        /* auto-fit 效果 - 拉伸填充 */
        .container.fit {
            grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
            background-color: #e6ffe6; /* 浅绿色背景 */
        }
        .item {
            background-color: rgb(141, 141, 255);
            height: 100px;
            border-radius: 8px;
            display: flex;
            align-items: center;
            justify-content: center;
            color: white;
            font-weight: bold;
        }
    </style>
</head>
<body>
    <h2>auto-fill (保留空轨道,适合日历/表格)</h2>
    <div class="container fill">
      <div class="item">卡片1</div>
      <div class="item">卡片2</div>
      <div class="item">卡片3</div>
    </div>

    <h2>auto-fit (拉伸填充,适合卡片/相册)</h2>
    <div class="container fit">
      <div class="item">卡片1</div>
      <div class="item">卡片2</div>
      <div class="item">卡片3</div>
    </div>
</body>
</html>

下次当你再需要做响应式布局时,别第一时间就想着一堆媒体查询了。试试这个,让它帮你自动搞定,省时又省力!

Auto-Fit vs Auto-Fill 演练场

Auto-Fit vs Auto-Fill演练场


大家复制代码时,可能会因格式转换出现错乱,导致样式失效。建议先少量复制代码进行测试,若未能解决问题,私信回复源码两字,我会发送完整的压缩包给你。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Auto-Fit vs Auto-Fill 可视化对比</title>
    <style>
        * {
            box-sizing: border-box;
            margin: 0;
            padding: 0;
        }

        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            line-height: 1.6;
            color: #333;
            background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
            min-height: 100vh;
            padding: 2rem;
        }

        .container {
            max-width: 1200px;
            margin: 0 auto;
        }

        header {
            text-align: center;
            margin-bottom: 3rem;
            padding: 2rem;
            background: rgba(255, 255, 255, 0.8);
            border-radius: 12px;
            box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
        }

        h1 {
            font-size: 2.5rem;
            margin-bottom: 1rem;
            color: #2c3e50;
        }

        .subtitle {
            font-size: 1.2rem;
            color: #7f8c8d;
            max-width: 800px;
            margin: 0 auto;
        }

        .comparison {
            display: flex;
            gap: 2rem;
            margin-bottom: 3rem;
        }

        @media (max-width: 768px) {
            .comparison {
                flex-direction: column;
            }
        }

        .panel {
            flex: 1;
            background: white;
            border-radius: 12px;
            overflow: hidden;
            box-shadow: 0 8px 20px rgba(0, 0, 0, 0.1);
        }

        .panel-header {
            padding: 1.5rem;
            background: #3498db;
            color: white;
            text-align: center;
        }

        .auto-fill .panel-header {
            background: #e74c3c;
        }

        .auto-fit .panel-header {
            background: #2ecc71;
        }

        .panel-content {
            padding: 2rem;
            border: 2px dashed #ddd;
            margin: 1rem;
            border-radius: 8px;
            transition: all 0.3s ease;
            position: relative;
            min-height: 300px;
        }

        .auto-fill .panel-content {
            background: rgba(231, 76, 60, 0.05);
        }

        .auto-fit .panel-content {
            background: rgba(46, 204, 113, 0.05);
        }

        .grid {
            display: grid;
            gap: 1rem;
            grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
        }

        .auto-fit .grid {
            grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
        }

        .card {
            background: linear-gradient(135deg, #3498db 0%, #2980b9 100%);
            color: white;
            border-radius: 8px;
            padding: 1.5rem;
            text-align: center;
            box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
            display: flex;
            flex-direction: column;
            justify-content: center;
            align-items: center;
            min-height: 120px;
            transition: transform 0.3s ease, box-shadow 0.3s ease;
        }

        .auto-fill .card {
            background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%);
        }

        .auto-fit .card {
            background: linear-gradient(135deg, #2ecc71 0%, #27ae60 100%);
        }

        .card:hover {
            transform: translateY(-5px);
            box-shadow: 0 8px 15px rgba(0, 0, 0, 0.2);
        }

        .card-number {
            font-size: 2rem;
            font-weight: bold;
            margin-bottom: 0.5rem;
        }

        .controls {
            background: white;
            padding: 2rem;
            border-radius: 12px;
            box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
            margin-bottom: 2rem;
        }

        .control-group {
            margin-bottom: 1.5rem;
        }

        .control-group h3 {
            margin-bottom: 0.5rem;
            color: #2c3e50;
        }

        .slider-container {
            display: flex;
            align-items: center;
            gap: 1rem;
        }

        .slider {
            flex: 1;
            -webkit-appearance: none;
            height: 8px;
            border-radius: 4px;
            background: #ddd;
            outline: none;
        }

        .slider::-webkit-slider-thumb {
            -webkit-appearance: none;
            width: 20px;
            height: 20px;
            border-radius: 50%;
            background: #3498db;
            cursor: pointer;
        }

        .auto-fill .slider::-webkit-slider-thumb {
            background: #e74c3c;
        }

        .auto-fit .slider::-webkit-slider-thumb {
            background: #2ecc71;
        }

        .value-display {
            min-width: 60px;
            text-align: center;
            font-weight: bold;
        }

        .button-group {
            display: flex;
            gap: 1rem;
        }

        button {
            padding: 0.8rem 1.5rem;
            border: none;
            border-radius: 4px;
            background: #3498db;
            color: white;
            font-weight: bold;
            cursor: pointer;
            transition: background 0.3s ease;
        }

        button:hover {
            background: #2980b9;
        }

        .explanation {
            background: white;
            padding: 2rem;
            border-radius: 12px;
            box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
        }

        .explanation h2 {
            margin-bottom: 1rem;
            color: #2c3e50;
        }

        .key-differences {
            display: grid;
            grid-template-columns: 1fr 1fr;
            gap: 2rem;
            margin-top: 2rem;
        }

        @media (max-width: 768px) {
            .key-differences {
                grid-template-columns: 1fr;
            }
        }

        .difference {
            padding: 1.5rem;
            border-radius: 8px;
        }

        .auto-fill-difference {
            background: rgba(231, 76, 60, 0.1);
            border-left: 4px solid #e74c3c;
        }

        .auto-fit-difference {
            background: rgba(46, 204, 113, 0.1);
            border-left: 4px solid #2ecc71;
        }

        .difference h3 {
            display: flex;
            align-items: center;
            margin-bottom: 1rem;
        }

        .difference-icon {
            margin-right: 0.5rem;
            font-size: 1.5rem;
        }

        .grid-track-visual {
            height: 4px;
            background: rgba(255, 255, 255, 0.5);
            margin-top: 0.5rem;
            border-radius: 2px;
        }

        .grid-track-indicator {
            height: 100%;
            background: white;
            border-radius: 2px;
            width: 100%;
        }

        .code-snippet {
            background: #2d3436;
            color: #dfe6e9;
            padding: 1rem;
            border-radius: 4px;
            font-family: 'Fira Code', monospace;
            margin: 1rem 0;
            overflow-x: auto;
        }

        .property {
            color: #81ecec;
        }

        .value {
            color: #fab1a0;
        }
    </style>
</head>
<body>
    <div class="container">
        <header>
            <h1>Auto-Fit vs Auto-Fill 可视化对比</h1>
            <p class="subtitle">通过实时演示直观了解CSS Grid中auto-fit和auto-fit属性的区别,以及它们如何影响响应式布局</p>
        </header>

        <div class="controls">
            <div class="control-group">
                <h3>容器宽度控制</h3>
                <div class="slider-container">
                    <span></span>
                    <input type="range" min="200" max="1000" value="800" class="slider" id="width-slider">
                    <span></span>
                    <span class="value-display" id="width-value">800px</span>
                </div>
            </div>

            <div class="control-group">
                <h3>卡片数量</h3>
                <div class="slider-container">
                    <span></span>
                    <input type="range" min="1" max="12" value="6" class="slider" id="card-slider">
                    <span></span>
                    <span class="value-display" id="card-value">6 卡片</span>
                </div>
            </div>

            <div class="button-group">
                <button id="reset-btn">重置演示</button>
                <button id="toggle-cards-btn">切换卡片可见性</button>
            </div>
        </div>

        <div class="comparison">
            <div class="panel auto-fill">
                <div class="panel-header">
                    <h2>Auto-Fill 布局</h2>
                    <p>保留空轨道,适合需要固定网格结构的布局</p>
                </div>
                <div class="panel-content" id="auto-fill-container">
                    <div class="grid" id="auto-fill-grid">
                        <div class="card"><div class="card-number">1</div>卡片</div>
                        <div class="card"><div class="card-number">2</div>卡片</div>
                        <div class="card"><div class="card-number">3</div>卡片</div>
                        <div class="card"><div class="card-number">4</div>卡片</div>
                        <div class="card"><div class="card-number">5</div>卡片</div>
                        <div class="card"><div class="card-number">6</div>卡片</div>
                    </div>
                </div>
            </div>

            <div class="panel auto-fit">
                <div class="panel-header">
                    <h2>Auto-Fit 布局</h2>
                    <p>折叠空轨道,内容拉伸填充可用空间</p>
                </div>
                <div class="panel-content" id="auto-fit-container">
                    <div class="grid" id="auto-fit-grid">
                        <div class="card"><div class="card-number">1</div>卡片</div>
                        <div class="card"><div class="card-number">2</div>卡片</div>
                        <div class="card"><div class="card-number">3</div>卡片</div>
                        <div class="card"><div class="card-number">4</div>卡片</div>
                        <div class="card"><div class="card-number">5</div>卡片</div>
                        <div class="card"><div class="card-number">6</div>卡片</div>
                    </div>
                </div>
            </div>
        </div>

        <div class="explanation">
            <h2>理解 Auto-Fit 和 Auto-Fill</h2>
            <p>CSS Grid布局中的<code>auto-fit</code><code>auto-fill</code>关键字允许我们创建灵活的响应式布局,而无需编写复杂的媒体查询。它们都用于<code>repeat()</code>函数中,但与<code>minmax()</code>结合使用时,行为有所不同。</p>

            <div class="code-snippet">
                <span class="property">grid-template-columns</span>: <span class="value">repeat(<select id="code-type">
                    <option value="auto-fill">auto-fill</option>
                    <option value="auto-fit" selected>auto-fit</option>
                </select>, minmax(150px, 1fr))</span>;
            </div>

            <div class="key-differences">
                <div class="difference auto-fill-difference">
                    <h3><span class="difference-icon">↔️</span> Auto-Fill 行为</h3>
                    <p>Auto-fill会尽可能多地创建网格轨道,即使没有网格项填充它们。空轨道仍然占用空间,影响布局。</p>
                    <ul>
                        <li>保持网格结构完整</li>
                        <li>空轨道保持最小宽度</li>
                        <li>适合需要严格对齐的布局</li>
                        <li>在内容数量变化但网格结构需要保持不变时非常有用</li>
                    </ul>
                </div>

                <div class="difference auto-fit-difference">
                    <h3><span class="difference-icon">↕️</span> Auto-Fit 行为</h3>
                    <p>Auto-fit会折叠任何空轨道,并拉伸现有网格项以填充可用空间。它更注重内容的填充而非网格结构的保留。</p>
                    <ul>
                        <li>折叠空轨道</li>
                        <li>内容拉伸填满容器</li>
                        <li>适合卡片、画廊和内容块布局</li>
                        <li>在希望内容充分利用可用空间时非常有用</li>
                    </ul>
                </div>
            </div>
        </div>
    </div>

    <script>
        // 获取DOM元素
        const widthSlider = document.getElementById('width-slider');
        const widthValue = document.getElementById('width-value');
        const cardSlider = document.getElementById('card-slider');
        const cardValue = document.getElementById('card-value');
        const autoFillContainer = document.getElementById('auto-fill-container');
        const autoFitContainer = document.getElementById('auto-fit-container');
        const autoFillGrid = document.getElementById('auto-fill-grid');
        const autoFitGrid = document.getElementById('auto-fit-grid');
        const resetBtn = document.getElementById('reset-btn');
        const toggleCardsBtn = document.getElementById('toggle-cards-btn');
        const codeType = document.getElementById('code-type');

        // 初始化卡片
        const initialCards = Array.from(autoFillGrid.children).map(card => card.outerHTML);

        // 更新容器宽度
        function updateContainerWidth() {
            const width = widthSlider.value;
            widthValue.textContent = `${width}px`;

            autoFillContainer.style.width = `${width}px`;
            autoFitContainer.style.width = `${width}px`;
        }

        // 更新卡片数量
        function updateCardCount() {
            const count = parseInt(cardSlider.value);
            cardValue.textContent = `${count} 卡片`;

            // 更新auto-fill网格
            autoFillGrid.innerHTML = '';
            for (let i = 0; i < count; i++) {
                autoFillGrid.innerHTML += initialCards[i % initialCards.length];
            }

            // 更新auto-fit网格
            autoFitGrid.innerHTML = '';
            for (let i = 0; i < count; i++) {
                autoFitGrid.innerHTML += initialCards[i % initialCards.length];
            }
        }

        // 切换卡片可见性
        function toggleCards() {
            const cards = document.querySelectorAll('.card');
            const isVisible = cards[0].style.opacity !== '0';

            cards.forEach(card => {
                card.style.opacity = isVisible ? '0' : '1';
                card.style.visibility = isVisible ? 'hidden' : 'visible';
            });

            toggleCardsBtn.textContent = isVisible ? '显示卡片' : '隐藏卡片';
        }

        // 重置演示
        function resetDemo() {
            widthSlider.value = 800;
            cardSlider.value = 6;
            updateContainerWidth();
            updateCardCount();

            const cards = document.querySelectorAll('.card');
            cards.forEach(card => {
                card.style.opacity = '1';
                card.style.visibility = 'visible';
            });

            toggleCardsBtn.textContent = '切换卡片可见性';
        }

        // 初始化
        updateContainerWidth();
        updateCardCount();

        // 事件监听器
        widthSlider.addEventListener('input', updateContainerWidth);
        cardSlider.addEventListener('input', updateCardCount);
        resetBtn.addEventListener('click', resetDemo);
        toggleCardsBtn.addEventListener('click', toggleCards);
        codeType.addEventListener('change', function() {
            if (this.value === 'auto-fill') {
                autoFillGrid.style.gridTemplateColumns = 'repeat(auto-fill, minmax(150px, 1fr))';
                autoFitGrid.style.gridTemplateColumns = 'repeat(auto-fill, minmax(150px, 1fr))';
            } else {
                autoFillGrid.style.gridTemplateColumns = 'repeat(auto-fit, minmax(150px, 1fr))';
                autoFitGrid.style.gridTemplateColumns = 'repeat(auto-fit, minmax(150px, 1fr))';
            }
        });
    </script>
</body>
</html>

各位互联网搭子,要是这篇文章成功引起了你的注意,别犹豫,关注、点赞、评论、分享走一波,让我们把这份默契延续下去,一起在知识的海洋里乘风破浪!

❌