REPENTOGON 教程:AnimationData 类——读取 ANM2 动画缓存的完整指南
游戏开发插件系统【免费下载链接】REPENTOGONScript extender for The Binding of Isaac: Repentance项目地址https://gitcode.com/gh_mirrors/re/REPENTOGON点击查看免费下载导读AnimationData是 REPENTOGON《以撒的结合忏悔》脚本扩展为 Lua Mod 开发者提供的只读动画数据类它缓存了 ANM2 文件中单个动画的全部静态信息名称、长度、图层、循环标志等并由所有使用同一 ANM2 文件的 Sprite 共享。本文以 docs/docs/AnimationData.md 文档为主体结合仓库中的结构定义libzhl/functions/AnimationData.zhl与 Lua 绑定源码repentogon/LuaInterfaces/LuaAnimationData.cpp系统讲解该类的获取方式、全部 API 的用法与返回值、底层内存布局及实际调用链并顺带说明与其配套的AnimationLayer、AnimationFrame与LayerState之间的层级关系。读完本文你将能够在自己的 Mod 中正确获取并解析任意 Sprite 的动画元数据例如判断动画是否循环、枚举图层、定位关键帧等。类概览一段动画的只读快照AnimationData在官方文档中的定义为Cached data for one animation from a loaded ANM2 file. Shared by all Sprites using the same ANM2 and cannot be modified.即它是从已加载的 ANM2 文件中提取的单个动画的缓存数据。有三个关键语义需要理解只读cannot be modified它只是动画的静态元数据任何修改动画行为的操作如切换播放动画、设置帧、替换贴图都发生在Sprite/ANM2/AnimationState层面而不是AnimationData层面共享shared多个 Sprite 加载同一个 ANM2 文件时指向的是同一份AnimationData数据因此用它做判断不会因 Sprite 实例不同而产生开销差异缓存cached这些数据在 ANM2 加载时解析完成读取时无需访问磁盘或重新解析文件。从源码结构看libzhl/functions/AnimationData.zhl 给出了它的内存布局结构体大小为0x13C包括名称_name偏移0x0、图层数组_animLayers0x18、图层数量_layerCount0x1C、事件触发器_eventTriggers0x28与_eventTriggerCount0x2C、动画总帧长_length0x30、循环标志_loop0x34以及 ID 到图层顺序的映射表_layerMap[64]0x38。REPENTOGON 正是基于这套反编译出来的真实结构向 Lua 暴露只读接口的。获取 AnimationData三种入口根据官方文档AnimationData对象可以通过Sprite的三个方法获得均为 REPENTOGON 在 docs/docs/Sprite.md 中登记的扩展方法方法作用Sprite:GetAnimationData(string AnimationName)按动画名获取该 ANM2 文件中指定动画的AnimationDataSprite:GetCurrentAnimationData()获取当前正在播放的动画对应的AnimationDataSprite:GetOverlayAnimationData()获取当前 overlay叠加层动画对应的AnimationData它们在源码中的注册与实现位于 repentogon/LuaInterfaces/LuaAnimationData.cppLua_SpriteGetCurrentAnimationData通过anm2-GetAnimationState()-GetAnimationData()取得当前动画数据Lua_SpriteGetOverlayAnimationData则取自anm2-GetOverlayAnimationState()-GetAnimationData()Lua_SpriteGetAnimationData先luaL_checkstring校验动画名再调用anm2-GetAnimationData(animName)按名查找。需要注意当动画不存在或当前未加载时这三个方法会返回nil源码中显式lua_pushnil因此调用后应做空值检查。底层ANM2::GetAnimationData同时支持按索引与按名称两种查询见 libzhl/functions/ANM2.zhl。另外Sprite:GetAllAnimationData()可以一次性返回该 ANM2 文件内全部动画的AnimationData表——这在运行时并不知道动画名、需要遍历所有动画时非常有用。其 Lua 实现LuaAnimationData.cpp遍历anm2-GetAnimationCount()个动画依次包装成 userdata 并填入 Lua 表。-- 示例安全地获取当前动画数据 local sprite Sprite() sprite:Load(gfx/effects/my_effect.anm2, true) sprite:Play(Idle, true) local animData sprite:GetCurrentAnimationData() if animData ~ nil then print(当前动画名: .. animData:GetName()) print(动画长度: .. animData:GetLength() .. 帧) print(是否循环: .. tostring(animData:IsLoopingAnimation())) end函数详解AnimationData共暴露 5 个成员函数。下面按官方文档顺序逐一说明并结合绑定源码给出参数与返回值细节。GetAllLayers()AnimationLayer[] GetAllLayers()返回一个包含全部AnimationLayer的 Lua 表按从底层到顶层的绘制顺序排列而不是按图层 ID 排列。实现要点LuaAnimationData.cpp遍历animationData-GetLayerCount()次每次调用GetLayerByOrder(i)取得第 i 层从下往上包装成AnimationLayeruserdata 后以i 1为键填入表。也就是说返回表的[1]是绘制顺序中最底下的图层。为什么顺序如此重要在 ANM2 编辑器中图层 ID 可以任意命名与编号而实际渲染由下到上的叠放顺序与 ID 数值大小并无必然关系。官方文档与源码注释都特别强调图层数组的顺序 ≠ 图层 ID 的顺序。AnimationData内部维护了一张_layerMap[64]的映射表把图层 ID翻译成从下往上的第几层见 AnimationData.zhl 中的注释。GetLayer()AnimationLayer GetLayer(int LayerId)按图层的 ID 数字获取对应的AnimationLayer。绑定实现为Lua_AnimationDataGetLayerByIdLuaAnimationData.cpp若LayerId 0直接返回nil内部经GetLayerById(layerID)查找找不到返回nullptr时返回nil。特别值得注意的一点源码注释也明确指出一个 ANM2 动画完全可能不存在某个 ID 的图层即便它拥有相邻 ID 的图层。所以不能假设 ID 连续调用后务必判空local layer animData:GetLayer(3) if layer ~ nil then print(图层 3 的 ID: .. layer:GetLayerID()) print(图层 3 是否可见: .. tostring(layer:IsVisible())) endGetLayerByOrder/GetLayerOrder/GetLayerById三者的关系在 AnimationData.zhl 中定义得很清楚GetLayerByOrder按第几层取带越界检查越界返回nullptrGetLayerOrder把图层 ID 翻译成顺序下标ID 越界返回-1GetLayerById则是两者的组合。GetLength()int GetLength()返回该动画的总帧长单位帧。它对应结构体中的_length字段偏移0x30见 AnimationData.zhlLua 绑定中直接lua_pushinteger(animationData-GetLength())。实际用途包括计算循环动画的进度百分比、为某帧做归一化计算、判断当前播放位置是否接近动画结尾等。例如配合Sprite:GetFrame()可以做播放到 75% 时触发某逻辑的判定local currentFrame sprite:GetFrame() local length animData:GetLength() if length 0 and currentFrame / length 0.75 then -- 播放到 75% 之后的逻辑 endGetName()string GetName()返回该动画在 ANM2 文件中的名称对应结构体_name字段。这在与Sprite:GetAnimationData(name)对照使用、或遍历GetAllAnimationData()时按名称识别动画非常有用。绑定实现LuaAnimationData.cpp直接animationData-GetName().c_str()压入字符串。IsLoopingAnimation()boolean IsLoopingAnimation()返回该动画是否为循环播放动画对应结构体_loop标志偏移0x34。循环动画在播放到末尾后会从开头继续而非循环动画播放完即停。绑定实现见 LuaAnimationData.cpp。可以用它来统一处理一类动画的播放行为例如 Mod 中的粒子特效循环动画如火焰、光环持续播放非循环动画如爆炸、受击闪白播放完自动销毁if animData:IsLoopingAnimation() then sprite:Play(animData:GetName(), true) else sprite:Play(animData:GetName(), true) -- 非循环动画播完后自行结束配合 Sprite:IsPlaying() 判断销毁时机 end与配套类的层级关系AnimationData → AnimationLayer → AnimationFrameAnimationData只是动画元数据的第一层。REPENTOGON 将 ANM2 数据按以下三层暴露给 LuaAnimationData一个动画的元数据 └─ AnimationLayer该动画中的一个图层 └─ AnimationFrame该图层中的一个具体帧AnimationLayer由AnimationData:GetLayer()/GetAllLayers()获得代表动画中的一个图层。其成员函数为GetFrame(int Frame)、GetLayerID()、IsVisible()见 docs/docs/AnimationLayer.md。其结构定义在 libzhl/functions/AnimationLayer.zhl注意注释中的关键细节GetFrameCount()返回的是独立帧数量不等同于动画总帧长——因为插值帧interpolated frames已被拆分成多个独立帧GetFrame(frame)带越界检查非法索引返回nullptr。AnimationFrame由AnimationLayer:GetFrame()获得代表图层中的一帧。其字段在 libzhl/functions/AnimationFrame.zhl 中定义crop、width、height、pos、scale、pivot、duration、visible、color、rotation、interpolated、startFrame、endFrame。完整 API 见 docs/docs/AnimationFrame.md。关于AnimationFrame有两条来自官方文档的重要提示值得记住插值与根动画root animations已被烘焙baked进这些值中——你读到的位置、缩放、旋转等已经是计算后的结果而不是编辑器里的原始关键帧值GetEndFrame()返回的结束帧实际上是下一个 AnimationFrame 的起始帧也就是说当前这个AnimationFrame在该帧起不再被显示。注意区分AnimationData:GetAllLayers()返回的是静态的 AnimationLayer 元数据而Sprite:GetAllLayers()见 docs/docs/Sprite.md返回的是每个图层的运行时状态 LayerState当前帧、当前颜色等可变数据。两者一个管动画长什么样一个管这个 Sprite 此刻怎么播不要混淆。底层实现与真实调用链结构体与字段映射结合 AnimationData.zhl 的完整结构定义AnimationData的只读接口与底层字段一一对应Lua 方法底层字段偏移说明GetName()_name0x0动画名称GetAllLayers()_animLayers/_layerCount0x18/0x1C图层数组按从下到上顺序 图层数量GetLayer(id)_layerMap[64]0x38ID → 顺序下标映射配合_animLayers取值GetLength()_length0x30动画总帧长IsLoopingAnimation()_loop0x34循环标志此外结构体还包含动画事件触发器数组_eventTriggers0x28与数量_eventTriggerCount0x2C其中每个触发器由eventId最大 32与animationFrame组成见 AnimationData.zhl。这些数据支撑着Sprite:GetEventTriggerFrames(animationName, eventName)这类运行时事件查询——其实现repentogon/LuaInterfaces/LuaSprite.cpp先通过ANM2::GetAnimationByName与GetEventByName把名字解析成 ID再遍历animData-_eventTriggers收集所有匹配帧号返回给 Lua如{5, 10, 12}。从 Lua 调用到游戏内存的完整路径以GetCurrentAnimationData为例一次调用的完整链路是Lua 代码调用sprite:GetCurrentAnimationData()绑定函数 Lua_SpriteGetCurrentAnimationData 取出 ANM2 指针调用anm2-GetAnimationState()-GetAnimationData()AnimationState持有指向当前AnimationData的指针_animData偏移0x4见 libzhl/functions/AnimationState.zhl绑定层把该指针包装成带AnimationDataMTmetatable 的 userdata 返回从而让 Lua 侧调用:GetName()等方法时直达游戏内存中的结构体字段。这意味着Lua 中拿到的AnimationData与游戏渲染线程用的是同一份数据读取零拷贝、零延迟且天然保证只读一致性。关于当前帧与图层帧的微妙区别在 AnimationState.zhl 的注释中还隐藏着一个容易踩坑的细节_layerFrames数组存储的是该图层当前指向内部 AnimationFrame 数组的哪个下标它与用户视角的动画当前帧不同——因为一个持续 duration 1 的帧在内部只占一个AnimationFrame索引。例如Sprite:GetLayerFrameData(layer)返回的正是该图层此刻正在显示的AnimationFrame实现见 LuaSprite.cpp它先用animData-GetLayerOrder(layerID)把图层 ID 换算成顺序下标再通过animState-_layerFrames[layerIndex]定位当前帧索引最后animLayer-GetFrame()取帧——这正是前面介绍的_layerMap映射在运行时被实际消费的场景。实战示例完整解析一个动画综合以上内容下面给出一个可运行的完整示例演示如何从 Sprite 一路解析到帧级数据local sprite Sprite() sprite:Load(gfx/effects/sample.anm2, true) sprite:Play(Idle, true) -- 1. 获取当前动画的 AnimationData local animData sprite:GetCurrentAnimationData() if animData nil then return end print(动画名: .. animData:GetName()) print(总帧长: .. animData:GetLength()) print(循环动画: .. tostring(animData:IsLoopingAnimation())) -- 2. 按绘制顺序枚举所有图层从下到上 local layers animData:GetAllLayers() for i, layer in ipairs(layers) do print(string.format(第 %d 层从下往上: ID%d, 可见%s, i, layer:GetLayerID(), tostring(layer:IsVisible()))) end -- 3. 按 ID 精确取图层并读取其首个 AnimationFrame 的属性 local layer animData:GetLayer(1) if layer ~ nil then local frame layer:GetFrame(0) if frame ~ nil then local pos frame:GetPos() print(string.format(首帧位置: (%.2f, %.2f), 尺寸: %.0fx%.0f, pos.X, pos.Y, frame:GetWidth(), frame:GetHeight())) end end -- 4. 查询某个动画事件在第几帧触发 local triggers sprite:GetEventTriggerFrames(Idle, SpawnFX) for _, f in ipairs(triggers) do print(事件 SpawnFX 触发帧: .. f) end小结与注意事项AnimationData是只读、共享的动画元数据快照修改动画行为请走Sprite/AnimationState接口三个获取入口GetAnimationData/GetCurrentAnimationData/GetOverlayAnimationData在动画不存在时都会返回nil务必判空图层顺序按从下到上返回与图层 ID 无关图层 ID 可能不连续GetLayer(id)也可能返回nilAnimationFrame的值已经烘焙了插值与根动画计算结果GetEndFrame()语义为下一个帧的起始帧想了解配套类的完整 API可继续阅读 docs/docs/AnimationLayer.md、docs/docs/AnimationFrame.md、docs/docs/Sprite.md底层结构定义集中在 libzhl/functions/ 下的AnimationData.zhl、AnimationLayer.zhl、AnimationFrame.zhl、AnimationState.zhl与ANM2.zhlLua 绑定实现则在 repentogon/LuaInterfaces/LuaAnimationData.cpp 与 repentogon/LuaInterfaces/LuaSprite.cpp 中。赞分享游戏开发插件系统【免费下载链接】REPENTOGONScript extender for The Binding of Isaac: Repentance项目地址https://gitcode.com/gh_mirrors/re/REPENTOGON点击查看免费下载相关推荐如何用 Rufus 制作 Windows 11 LTSC 启动盘并离线完成本地账户安装如何用 Rufus 制作 Windows 11 LTSC 启动盘并离线完成本地账户安装 手头一批老机器要批量装 Windows 11 LTSC结果全都卡在同一游戏开发插件系统REPENTOGON AnimationFrame 类详解ANM2 动画单帧数据的读取、内存布局与 Lua 实战REPENTOGON AnimationFrame 类详解ANM2 动画单帧数据的读取、内存布局与 Lua 实战 本文围绕 REPENTOGON 为《以撒的结游戏开发插件系统MagiskBoot 实战指南两条命令解包重打包 Android 启动镜像附排错决策表MagiskBoot 实战指南两条命令解包重打包 Android 启动镜像附排错决策表 手上刚拿到一个 boot.img 想加一个内核参数或者注入一个开游戏开发插件系统上一篇Theatre动画事件总线设计模块化与可扩展性下一篇GitHub_Trending/reg/register技术债务清理提升代码可维护性的计划创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

相关新闻

5 分钟搭一个实时高尔夫挥杆教练:Vision-Agents 的 YOLO 姿态检测 + 实时多模态 LLM

5 分钟搭一个实时高尔夫挥杆教练:Vision-Agents 的 YOLO 姿态检测 + 实时多模态 LLM

5 分钟搭一个实时高尔夫挥杆教练:Vision-Agents 的 YOLO 姿态检测 实时多模态 LLM 【免费下载链接】Vision-Agents Open Vision Agents by Stream. Build voice and vision agents quickly with any model or video provider. Uses Streams edge network for ultra…

2026/10/16 1:57:44 阅读更多 →
基于select的TCP服务端教学实践:Flash游戏通信模型

基于select的TCP服务端教学实践:Flash游戏通信模型

简介:本资源是一套基于TCP协议与select I/O多路复用模型实现的Flash网络游戏通信实践案例,面向网络编程初学者及C服务端开发入门者,聚焦于客户端(SWF游戏)与控制台服务器之间的实时交互机制。压缩包共15个文件&#xf…

2026/10/16 1:57:44 阅读更多 →
Familjen Grotesk 字体深度解析:ink trap 设计语言与 Fontsource 可变字体实战指南

Familjen Grotesk 字体深度解析:ink trap 设计语言与 Fontsource 可变字体实战指南

前端CLI开发工具 【免费下载链接】fontsource Self-host Open Source fonts in neatly bundled NPM packages. 项目地址: https://gitcode.com/gh_mirrors/fo/fontsource 点击查看 免费下载 Familjen Grotesk 是瑞典设计工作室 Familjen STHLM 出品的当代 sans-ser…

2026/10/16 1:56:44 阅读更多 →

最新新闻

Agent落地三要素:记忆、目标与执行力的工程闭环

Agent落地三要素:记忆、目标与执行力的工程闭环

连着做了几个Agent项目之后,我越来越确定一件事:决定Agent上限的是模型本身,但决定它下限的,是记忆、目标与执行力这三块骨架。之前在做一个内部知识库问答项目时,最开始的版本就是把大模型接上对话接口,结…

2026/10/16 2:50:57 阅读更多 →
AlphaFin的Stock-Chain如何实现30.8%年化收益?多空回测与FinGPT对比深度拆解

AlphaFin的Stock-Chain如何实现30.8%年化收益?多空回测与FinGPT对比深度拆解

【免费下载链接】AlphaFin 项目地址: https://gitcode.com/gh_mirrors/alp/AlphaFin 点击查看 免费下载 AlphaFin 是一个面向金融分析的开源基准项目,其核心 Stock-Chain 框架通过检索增强(RAG) 微调大模型的方式完成股票趋势预测…

2026/10/16 2:50:57 阅读更多 →
3 步装回 Win10 经典任务栏:ExplorerPatcher 免费上手笔记

3 步装回 Win10 经典任务栏:ExplorerPatcher 免费上手笔记

3 步装回 Win10 经典任务栏:ExplorerPatcher 免费上手笔记 【免费下载链接】ExplorerPatcher This project aims to enhance the working environment on Windows 项目地址: https://gitcode.com/GitHub_Trending/ex/ExplorerPatcher Windows 11 升级后&…

2026/10/16 2:50:57 阅读更多 →
AI笔记工具实战:创始人如何用语音速记和自动整理打造高效工作流

AI笔记工具实战:创始人如何用语音速记和自动整理打造高效工作流

我见过太多创始人的手机备忘录:一百多段语音躺在输入框里,四十几张聊天截图堆在相册,还有一堆只有自己看得懂的缩写关键词。问他们上周和客户聊了什么,回答通常是——"我记了,但找不到了。"这句话才是创始人…

2026/10/16 2:50:57 阅读更多 →
ET6238驱动详解:I2C接口LED显示与按键扫描的C源码实现

ET6238驱动详解:I2C接口LED显示与按键扫描的C源码实现

简介:ET6238驱动是一份基于I2C总线协议的LED显示控制源码,采用C语言编写,目标芯片为ET6238,适用于驱动十六行八列的LED矩阵。资源面向嵌入式开发者、单片机学习者及需要实现信息显示功能的相关从业者,可帮助快速构建数…

2026/10/16 2:50:57 阅读更多 →
建筑改造项目的表计难题:拆不了换不动,水电气数据怎么远程拿?

建筑改造项目的表计难题:拆不了换不动,水电气数据怎么远程拿?

建筑改造项目做到水电配套这一步,表计怎么处理,常常是第一个卡住的地方。新建工程的表计配套相对简单。但改造项目和存量建筑不一样,每栋楼的情况都不同。有的管网锈了二十年不敢碰,有的管井窄到人侧身才能进去,有的本…

2026/10/16 2:49:57 阅读更多 →

日新闻

个人 AI Agent 平台接入 TaoToken:统一 Key 与 API 通道的配置大纲

个人 AI Agent 平台接入 TaoToken:统一 Key 与 API 通道的配置大纲

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

2026/10/16 0:00:05 阅读更多 →
信号与系统频域分析作业解题指南

信号与系统频域分析作业解题指南

1. 作业背景与核心要求解析信号与系统作为电子信息类专业的核心基础课,其作业设计往往直接关联课程的核心知识框架。2026年春季第六次作业的布置,通常对应着课程进展到频域分析的关键阶段。根据常规教学进度推测,本次作业可能涉及以下核心内容…

2026/10/16 0:00:05 阅读更多 →
Django MVT实战:从零构建图书借阅系统,彻底搞懂Model-View-Template分层

Django MVT实战:从零构建图书借阅系统,彻底搞懂Model-View-Template分层

1. 从一个"四不像"的练手项目说起如果你已经看完了Django的官方教程前几章,跟着敲完了投票应用,大概会有一种"我好像会了,又好像什么都不会"的割裂感。教程里每个知识点都是拆开的——这一章讲模型,下一章讲视…

2026/10/16 0:00:05 阅读更多 →

周新闻

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

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

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

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

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

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

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

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

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

2026/10/15 13:48:15 阅读更多 →

月新闻

我发现了一个新思路:用 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/15 0:14:36 阅读更多 →
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/14 22:45:47 阅读更多 →
黑夜航拍船只数据集训练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/15 9:49:14 阅读更多 →