2048 滑动拼图(第 4 期)实战解析:基于 p5.js 的视觉优化、Bug 修复与代码重构
教程文档示例工程【免费下载链接】website-archiveArchive of the Coding Train website (first version)项目地址https://gitcode.com/gh_mirrors/we/website-archive点击查看免费下载本篇以 Coding Train 多期编程挑战的第 4 期094.4-2048.md为核心结合仓库内 CC_094_2048 的最终源码完整拆解这一期中优化视觉效果、修复缺陷、重构代码三项工作的落地方式包括按数值区分配色的colorsSizes配置表、新增方块的高亮标记机制、无效移动检测、胜负判定以及把单文件拆分为config.js / grid.js / game.js / sketch.js的分层重构。读完本文你将掌握 2048 这类 4×4 滑动拼图游戏的完整实现脉络理解先滑动—合并—再滑动的行操作核心算法并能把同样的重构与可测试性思路迁移到自己的 p5.js 项目中。系列背景与第 4 期的定位2048 是 Coding Train 的第 94 号多期编程挑战目标是用 JavaScriptcanvas p5.js从零实现风靡一时的滑动拼图游戏。四期内容依次递进期数文档主题第 1 期094.1-2048.md游戏核心机制网格、滑动、合并第 2 期094.2-2048.md继续完善游戏机制第 3 期094.3-2048.md加入计分、调整字号、增加获胜判定第 4 期094.4-2048.md改进视觉并不算成功、修复 Bug、重构代码第 4 期文档对本期工作的概括非常坦诚I attempt but mostly fail to improve the visuals of the game, fix a bug and refactor the code a bit尝试但基本没能成功改进视觉、修复一个 Bug、稍微重构了一下代码。这期内容虽然篇幅不长但正好是观察游戏逻辑与表现层分离的绝佳样本——仓库中留下的最终代码就是这次重构的成果我们可以直接从源码出发逐层验证。视觉优化colorsSizes配置表与新增方块高亮第 4 期改进视觉的成果集中体现在两个地方独立的视觉配置表以及新生成方块的一次性高亮描边。按数值区分配色的配置表仓库把视觉常量从主逻辑中抽离到了独立的 config.js 文件用一张对象表把每个数值映射到字号 填充色// CodingChallenges/CC_094_2048/P5/config.js let colorsSizes { 2: { size: 64, color: #F35956 }, 4: { size: 64, color: #F35956 }, 8: { size: 64, color: #49BB6C }, 16: { size: 64, color: #2494C1 }, 32: { size: 64, color: #9659A7 }, 64: { size: 64, color: #F1C500 }, 128: { size: 36, color: #FF5956 }, 256: { size: 36, color: #F1C5FF }, 512: { size: 36, color: #00000 }, 1024: { size: 64, color: #24FFC1 }, 2048: { size: 64, color: #A659A9 } };在 sketch.js 的drawGrid()中绘制每个格子时直接查表取色和字号if (val ! 0) { fill(colorsSizes[s].color); } else { noFill(); } rect(i * w, j * w, w, w, 30); // 最后一个参数 30 是圆角半径 if (val ! 0) { textAlign(CENTER, CENTER); noStroke(); fill(0); textSize(colorsSizes[s].size); text(val, i * w w / 2, j * w w / 2); }把颜色、字号这类表现参数集中到一张配置表里是重构带来的直接好处调整配色不必再钻进绘图函数内部改代码。值得注意的是这份配置表正是文档所说mostly fail的物证——从最终代码看仍留有明显瑕疵512的颜色是#00000这是 5 位十六进制色值并非合法的 CSS 颜色合法值应为 3 位或 6 位该方块实际会以异常/黑色显示2与4共用#F35956而128用的是相近但不一致的#FF5956字号分配也欠合理三位数128/256/512用了 36px而四位数1024/2048反而用回 64px长数字有溢出格子的风险。这些细节恰好印证了作者对本期视觉改造的自我评价——方向是对的配置化但效果并不圆满。新增方块的一次性高亮描边第 4 期另一个视觉尝试是标记新生成的方块。实现方式是在 grid.js 的addNumber()中除了向grid写入新数值还同步在一张平行的标记网格grid_new中打点function addNumber() { let options []; for (let i 0; i 4; i) { for (let j 0; j 4; j) { if (grid[i][j] 0) { options.push({ x: i, y: j }); } } } if (options.length 0) { let spot random(options); let r random(1); grid[spot.x][spot.y] r 0.1 ? 2 : 4; // 90% 概率出 210% 概率出 4 grid_new[spot.x][spot.y] 1; // 标记为新方块 } }对应地在drawGrid()中读到grid_new[i][j] 1的格子会被绘制成紫色粗描边stroke(200, 0, 200)、strokeWeight(16)画完立刻把标记清回 0保证高亮只出现一次if (grid_new[i][j] 1) { stroke(200, 0, 200); strokeWeight(16); grid_new[i][j] 0; } else { strokeWeight(4); stroke(0); }由于主循环使用了noLoop()见下文 setup画面只在每次按键后由updateCanvas()主动重绘因此新方块高亮恰好能持续到下一次移动之前让玩家一眼看到刚生成的数字落在哪里。从数据结构上看grid数值与grid_new标记两张 4×4 网格平行维护是数据层 表现层分离的一种轻量实现。Bug 修复无效移动检测、胜负判定与新方块生成第 4 期修复一个 Bug从最终源码可以推断出几处为杜绝典型 2048 缺陷而设的机制它们共同保证了游戏状态的正确性。无效移动不生成新方块2048 中最常见的缺陷是按下没有产生任何变化的按键例如最右列已满时按右系统却仍然随机生成一个新方块。仓库在 sketch.js 的keyPressed()里通过操作前复制、操作后比较的方式规避了它let past copyGrid(grid); for (let i 0; i 4; i) { grid[i] operate(grid[i]); } let changed compare(past, grid); // ... 还原 flip / transpose ... if (changed) { addNumber(); // 只有棋盘真的变了才生成新方块 }配套的两个工具函数都在 grid.js 中copyGrid()L31-L39深拷贝当前棋盘用于事后比对compare()L20-L29逐格比较两个棋盘、只要任一格子不同就返回true。这一先快照、后校验的写法把移动与生成两个环节解耦是 2048 实现中非常关键的一处防御。胜负判定胜利与失败判定放在 game.js 中isGameWon()L41-L50遍历整个 4×4 棋盘一旦发现某个格子等于2048即判定获胜isGameOver()L52-L67只有当棋盘既没有空格、又没有相邻等值对时才返回true——即既无路可走也无对可合function isGameOver() { for (let i 0; i 4; i) { for (let j 0; j 4; j) { if (grid[i][j] 0) return false; // 还有空格 if (i ! 3 grid[i][j] grid[i 1][j]) return false; // 竖向可合并 if (j ! 3 grid[i][j] grid[i][j 1]) return false; // 横向可合并 } } return true; }注意两个函数的返回值语义是相反的isGameWon找到 2048 即返回true而isGameOver找到任何还能玩的证据即返回false。在keyPressed()末尾两者结果都只是通过console.log打印GAME WON/GAME OVER游戏本身并未真正停表或弹出结束界面——这同样是半成品式收尾的体现也是后续可以自行扩展的方向例如显示胜利/失败画面、禁止继续输入。90/10 的新方块数值概率addNumber()中grid[spot.x][spot.y] r 0.1 ? 2 : 4;实现了经典 2048 的概率规则90% 概率生成2、10% 概率生成4。随机空位从全部空格的候选列表中选出保证任何情况下新方块都落在合法位置。这一点与 Processing 版本r 0.5 ? 4 : 2的概率分配略有差异后者是 50/50——两版对照可以清楚看到概率参数对游戏节奏的影响。代码重构单文件拆分为四文件分层第 4 期最重要的产出是重构后的文件结构。仓库中 CC_094_2048/P5 目录下的四个源文件职责清晰文件职责关键内容config.js表现层配置colorsSizes数值→字号/配色映射grid.js网格数据结构与操作blankGrid、compare、copyGrid、flipGrid、transposeGrid、addNumbergame.js纯游戏逻辑operate、slide、combine、isGameWon、isGameOversketch.jsp5.js 入口与表现setup、keyPressed、updateCanvas、drawGrid加载顺序在 index.html 中体现为严格的依赖关系script srchttps://cdn.jsdelivr.net/npm/p51.4.1/lib/p5.min.js/script script srcconfig.js/script script srcgrid.js/script script srcgame.js/script script srcsketch.js/script先加载 p5 运行时再依次加载配置、网格工具、游戏逻辑最后是作为入口的 sketch。页面主体只有一个用于显示分数的元素p idscore分数更新在updateCanvas()中通过select(#score).html(score)完成sketch.js#L82-L86。这种分层最大的价值在于网格变换grid.js与移动判定game.js都不直接依赖 p5 的绘图 APIoperate/slide/combine/isGameOver等核心逻辑理论上可以被抽出来做纯函数单元测试——这正好呼应了第 4 期文档关联的 CircleCI: Unit Testing and Continuous Integration 视频系列见 094.4-2048.md 的videos字段。移动算法核心slide → combine → slide 与方向键映射行操作的三段式流程2048 的本质是对每一行或列做滑动-合并-滑动。仓库在 game.js#L11-L16 用operate()把三个步骤封装成一个不可分割的原子操作function operate(row) { row slide(row); // 1. 把所有非零数字挤向行尾对齐 row combine(row); // 2. 从右向左合并相邻等值 row slide(row); // 3. 合并产生的空格再次对齐 return row; }slide()L19-L25的实现非常简洁——用filter(val val)剔除零值再把缺失的零补到数组头部function slide(row) { let arr row.filter(val val); // 去掉所有 0 let missing 4 - arr.length; // 需要补回的空格数 let zeros Array(missing).fill(0); arr zeros.concat(arr); // 零在前非零数字挤到行尾 return arr; }combine()L28-L39从右向左i 3到1扫描一旦发现相邻两格相等就合并到右侧格子并累加分数function combine(row) { for (let i 3; i 1; i--) { let a row[i]; let b row[i - 1]; if (a b) { row[i] a b; // 合并结果落在右侧 score row[i]; // 分数即时累加 row[i - 1] 0; // 左侧清空 } } return row; }从右向左的遍历顺序保证了同一轮内已合并的格子不会再次被合并例如[2, 2, 2, 2]一轮后变成[0, 0, 4, 4]而不是[0, 0, 0, 8]这是 2048 合并语义的关键细节。注意score在这里是全局变量在 sketch.js#L13 声明combine直接读写它——若想进一步把 game.js 做成纯逻辑模块下一步可以考虑把分数作为返回值传出。四个方向统一为一维行操作p5 版本的技巧在于四个方向的移动全部归约为对行的operate调用。在 sketch.js#L27-L51 的keyPressed()中先把网格做翻转flipGrid和转置transposeGrid使目标方向上的数据都落到同一行上UP_ARROWflipGrid(grid)每行反转让下边数据对齐到右边处理逻辑RIGHT_ARROWtransposeGrid(grid)转置后列变行LEFT_ARROWtransposeGridflipGridDOWN_ARROW不处理即保持基准方向处理完所有行后再按原路反向还原变换使棋盘方向回到玩家视角。transposeGrid与flipGrid都在 grid.jsfunction flipGrid(grid) { for (let i 0; i 4; i) { grid[i].reverse(); // 就地反转每一行 } return grid; } function transposeGrid(grid) { let newGrid blankGrid(); for (let i 0; i 4; i) { for (let j 0; j 4; j) { newGrid[i][j] grid[j][i]; // 行列互换 } } return newGrid; }四个方向共用一份行逻辑是 2048 实现中最优雅的抽象之一只需写一次滑动合并代码再配合线性代数里的翻转/转置变换即可覆盖全部方向。作为对照Processing 版本采用了同样的思路只是在还原转置时把rotateGrid连续调用三次相当于转置的逆运算两版对照阅读有助于理解方向变换的对称性。主循环与按键处理sketch.js#L15-L24 的setup()建立了整个游戏的运行框架function setup() { createCanvas(400, 400); // 400×400 画布每格 100px noLoop(); // 关闭自动循环改为事件驱动重绘 grid blankGrid(); grid_new blankGrid(); addNumber(); // 开局生成两个初始方块 addNumber(); updateCanvas(); }noLoop()是本作的重要设计p5 默认每帧调用draw()而 2048 是回合制游戏只有按键时才需要重绘因此关闭循环、由keyPressed()主动调用updateCanvas()重绘既省性能又让新方块高亮这类一次性表现得以稳定呈现。可测试性重构与单元测试、CI 的衔接第 4 期文档的videos字段专门关联了 CircleCI: Unit Testing and Continuous Integration 系列文档见 _Streams/117.2-unit-testing-ci-circleci.md这暗示了本期的重构与让代码更容易被测试的意图直接相关。仓库根目录下的 unit_testing 目录正是这套理念的具体落地unit_testing/readme.md 说明该目录用 Jest 对所有scriptname.test.js文件做单元测试并为基于 CircleCI 的持续集成做准备unit_testing/yaml.test.js 会对_CodingChallenges等目录下每一个文档的 YAML front matter做格式与类型校验例如标题与内部编号一致video_id全局唯一repository与web_editor映射唯一且对应仓库目录真实存在unit_testing/formats.js 用prop-types定义了video文档的字段契约——title、video_number、date、video_id为必填repository、web_editor、parts等为可选字段。以本篇核心文档 094.4-2048.md 为例它的 front matter 完全符合这套契约video_number: 94.4与文件名编号094.4一致去掉前导零后匹配repository: CC_094_2048对应仓库中真实存在的 CodingChallenges/CC_094_2048 目录web_editor: aeIk9JZ-U关联 p5.js 在线编辑器版本。运行测试的方式见 unit_testing/package.json在unit_testing目录执行npm test即jest即可校验全站文档与仓库资源的一致性。从源码结构看把 game.js / grid.js 中的纯逻辑与 p5 绘图分离正是为了让这类算法函数能够脱离浏览器环境、在 Node Jest 下被直接断言——这是重构提升可测试性最直接的证据链。运行与调试方式要在本地运行这套 p5.js 版 2048无需安装任何构建工具打开 CodingChallenges/CC_094_2048/P5/index.html它通过 jsdelivr CDN 引入 p5.js 1.4.1需要网络环境页面按顺序加载config.js → grid.js → game.js → sketch.jssetup()会自动生成 400×400 画布并初始化两个方块用键盘方向键操作←左移、→右移、↑上移、↓下移每次有效移动后自动生成一个新方块90% 为 2、10% 为 4紫色描边即为新方块高亮分数显示在页面顶部的p idscore中每次合并即时累加打开浏览器控制台可看到GAME WON/GAME OVER的判定输出。小结第 4 期尝试改进视觉、修复一个 Bug、稍微重构代码这三件事在仓库最终代码中留下了清晰可查的痕迹视觉颜色/字号被抽入 config.js 的colorsSizes配置表新方块通过grid_new标记网格获得紫色高亮描边同时配置表里残留的非法色值#00000与字号分配问题如实印证了mostly fail的自我评价Bug 修复copyGrid compare的快照比对机制杜绝了无效移动误生新方块isGameWon/isGameOver提供了正确的胜负判定重构代码按配置 / 网格 / 游戏逻辑 / p5 入口拆分为四个文件slide → combine → slide的行操作配合翻转、转置统一处理四个方向纯逻辑与绘图分离也为单元测试与 CI 铺平了道路。对想深入学习 p5.js 游戏开发的读者建议顺着 094.1-2048.md 到 094.4-2048.md 的四期文档顺序阅读同时对照仓库中的 P5 版本 与 Processing 版本两版在同一算法框架下的实现差异本身就是极好的学习材料。赞分享教程文档示例工程【免费下载链接】website-archiveArchive of the Coding Train website (first version)项目地址https://gitcode.com/gh_mirrors/we/website-archive点击查看免费下载相关推荐Coding in the Cabana 第 1 期使用 p5.js 与 Processing 实现 Maurer Rose 可视化Coding in the Cabana 第 1 期使用 p5.js 与 Processing 实现 Maurer Rose 可视化 导读 Maurer Ro教程文档示例工程p5.js模块化编程代码组织与重用p5.js模块化编程代码组织与重用 你是否曾在p5.js项目中遇到过代码混乱、功能复用困难的问题本文将从基础到进阶系统讲解如何通过模块化编程解决这些痛点前端图形学3D渲染行为验证码 Android 接入实战基于 Anji Plus 滑动拼图与点选文字的 Kotlin/Java 双 Demo 全解析行为验证码 Android 接入实战基于 Anji Plus 滑动拼图与点选文字的 Kotlin/Java 双 Demo 全解析 本文以 view/andro后端应用安全图像处理创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

相关新闻

CAD/CAE工业智能体:从侧边栏到内核级嵌入的技术架构与落地实践

CAD/CAE工业智能体:从侧边栏到内核级嵌入的技术架构与落地实践

1. 从侧边栏困局说起:工业智能体到底该长什么样CAD 和 CAE 这两个词,干过制造业的人都不陌生。CAD 负责画图建模,CAE 负责仿真验证,一个管“长什么样”,一个管“扛不扛得住”。过去三十年,这两类软件的核心…

2026/10/12 16:09:40 阅读更多 →
开源安全工具怎么选怎么用:从入口仓库到落地部署指南

开源安全工具怎么选怎么用:从入口仓库到落地部署指南

简介:《探索hack仓库入口Hack91:网络安全开源项目的宝藏》是一份面向网络安全学习者、渗透测试爱好者与安全评估人员的图文笔记,系统梳理了Hack91项目的来龙去脉与核心价值。文档从诞生背景与创始团队讲起,说明该项目在SQL注入、X…

2026/10/13 23:16:06 阅读更多 →
安全隔离与信息交换系统Web界面配置指南:从策略下发到日志审计

安全隔离与信息交换系统Web界面配置指南:从策略下发到日志审计

简介:天融信安全隔离与信息交换系统V3版Web界面安装使用手册,面向网络安全运维人员、系统集成工程师及负责内外网隔离部署的技术人员。该手册针对隔离设备在保证内外网物理隔离前提下实现适度数据交换的部署与运维需求,系统讲解设备从硬件上架…

2026/10/13 21:17:51 阅读更多 →

最新新闻

中国起名服务正在发生什么变化?从“大师经验”到专业化、品牌化服务

中国起名服务正在发生什么变化?从“大师经验”到专业化、品牌化服务

很长时间里,人们对于“找人起名”的想象都非常简单: 找一位有经验的老师, 报出生时间, 老师根据经验给出几个名字。 整个服务高度依赖: “这个大师厉不厉害”。 但今天再观察国内专业起名市场,会发现它已经…

2026/10/14 3:22:34 阅读更多 →
测试复盘怎么做才有用:从追责到改进

测试复盘怎么做才有用:从追责到改进

很多团队的测试复盘,开着开着就变成了批斗会。“这个bug为什么没测出来?”“谁负责的模块?”“下次注意啊。”散会之后,问题还在,下次照样出。还有一种更常见的:复盘开了,大家轮流说了一遍“我觉…

2026/10/14 3:22:34 阅读更多 →
我的国模白月光文馨妍

我的国模白月光文馨妍

原文 免责声明 全文均为AIGI 「勿辩,辩则尔胜。」 5500亿,三个啊 它的大名是文心一言。 网友叫它 ChatPPT。 我叫它文馨妍。 发布会那天我没看。 直播没看。回放也没看。 网上说,那场发布会从头到尾没拿出一台真机,全靠李总一张嘴。 我…

2026/10/14 3:22:34 阅读更多 →
让数据先会说话:数据字典、查询契约与接口语义化

让数据先会说话:数据字典、查询契约与接口语义化

> 系列:《VeapAI 自然语言操作实战》第 2 篇(核心篇)> 相关源码:veap-cloud/veap-modules/veap-gen/(在线开发与代码生成,含 src/main/resources/vm/ 模板)、veap-cloud/veap-modules/vea…

2026/10/14 3:22:34 阅读更多 →
跑得快,也查得到:性能契约、运行时间线与排障留痕

跑得快,也查得到:性能契约、运行时间线与排障留痕

> 系列:《VeapAI 自然语言操作实战》第 4 篇(基础篇)> 相关源码:veap-ui/src/components/AiAgentEngine/(调度、状态与写回)、veap-ui/src/views/ai/aiAgentProcessInstance/(实例与节点执…

2026/10/14 3:22:34 阅读更多 →
企业级 Agent 规模化落地:从单场景试点到收藏级架构蓝图,小白程序员必备指南

企业级 Agent 规模化落地:从单场景试点到收藏级架构蓝图,小白程序员必备指南

企业部署 Agent 多从单场景开始,但规模化会面临诸多挑战,如数据权威、权限边界、运行责任等。文章提出企业级 Agent 架构需厘清与公共能力、存量系统的协同关系,明确中央与业务域权责。核心设计原则包括保留核心系统权威、增强现有架构、新增…

2026/10/14 3:21:34 阅读更多 →

日新闻

AnyPS5通用适配层设计:兼容性与性能的工程取舍

AnyPS5通用适配层设计:兼容性与性能的工程取舍

1. 从"AnyPS5"这个名字说起:它到底想解决什么问题第一次看到"AnyPS5"这个标题,我脑子里冒出来的第一个念头是:这大概率是一个围绕"跨平台运行"或者"通用适配"做文章的项目。名字里的"Any"…

2026/10/14 0:00:41 阅读更多 →
Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

1. 项目概述:Flutter跨平台二手交易应用开发作为一名长期从事移动应用开发的工程师,我最近完成了一个基于Flutter框架的二手物品交易平台项目。这个项目最吸引我的地方在于它完美结合了Flutter的跨平台优势与HarmonyOS的生态特性,实现了"…

2026/10/14 0:00:41 阅读更多 →
DFS算法解决飞机降落调度问题实战

DFS算法解决飞机降落调度问题实战

1. 项目背景与核心思路飞机降落问题是一个经典的调度优化场景,也是算法竞赛和面试中的高频题目。这个题目要求我们合理安排多架飞机的降落顺序和时间,确保所有飞机都能安全着陆而不发生冲突。在实际航空管制中,这类问题直接关系到飞行安全和机…

2026/10/14 0:00:41 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/13 2:54:56 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/13 16:22:04 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/13 1:58:08 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 22:01:27 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 14:57:39 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 18:11:17 阅读更多 →