<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>早上好喵</title>
    <link>https://blog.silvershaded.top/</link>
    <description>银渐层的个人主页与技术博客与摄影作品</description>
    <language>zh-CN</language>
    <lastBuildDate>Wed, 04 Nov 2026 00:00:00 GMT</lastBuildDate>
    <atom:link href="https://blog.silvershaded.top/feed.xml" rel="self" type="application/rss+xml"/>
    <item>
      <title>Blog 说明</title>
      <link>https://blog.silvershaded.top/posts/blog-guide/</link>
      <guid isPermaLink="true">https://blog.silvershaded.top/posts/blog-guide/</guid>
      <pubDate>Wed, 04 Nov 2026 00:00:00 GMT</pubDate>
      <description>这个站怎么加图、怎么发文、怎么改标签、相关阅读是怎么算的；顺手把图片和 Markdown 的能力边界一次说清。</description>
      <content:encoded><![CDATA[<p>这份说明写给两个读者：半年后忘了细节的我，和想照着改的你。</p>
<p>先说结论：<strong>这是一个纯静态站</strong>。写 Markdown，<code>npm run build</code>，上传 <code>dist/</code>。没有数据库，没有后台，没有运行时框架。</p>
<h2 id="一图片">一、图片</h2>
<h3 id="放在哪里">放在哪里</h3>
<pre><code>assets/images/        站内零散图片（头像、封面、首页大图、友链头像）
assets/gallery/full/      相册原图
assets/gallery/preview/   相册预览（列表用）
assets/gallery/thumbs/    相册缩略图（网格用）
</code></pre>
<p><code>assets/</code> 会被整目录复制进 <code>dist/</code>，所以路径就是它们未来的 URL。</p>
<h3 id="文章里怎么引用">文章里怎么引用</h3>
<p>写相对路径就行，不用带 <code>images/</code>：</p>
<pre><code class="language-markdown">![温室里的两只](greenhouse.webp)
</code></pre>
<p>构建时会做两件事：</p>
<ol>
<li>补全成 <code>images/greenhouse.webp</code>（已经写了 <code>images/</code> 或以 <code>/</code>、<code>http(s)</code> 开头的原样保留）；</li>
<li><strong>读出真实宽高写进 <code>width</code>/<code>height</code></strong>，再挂上 <code>loading=&quot;lazy&quot;</code> —— 这是这个站几乎不抖的原因。</li>
</ol>
<p>能读尺寸的格式：<strong>PNG、JPEG、WebP</strong>。SVG 和 GIF 读不到，会退化成没有尺寸（可能抖一下），建议别放正文。</p>
<h3 id="封面">封面</h3>
<pre><code class="language-yaml">cover: greenhouse.webp
</code></pre>
<p><code>cover</code> 会出现在首页置顶卡和文章页顶部。同样只写文件名。</p>
<h3 id="相册">相册</h3>
<p>相册不走 Markdown，走数据：<code>data/gallery.json</code>。</p>
<pre><code class="language-json">{
  &quot;slug&quot;: &quot;greenhouse&quot;,
  &quot;title&quot;: &quot;温室里的两只&quot;,
  &quot;desc&quot;: &quot;向日葵开满的那个下午。&quot;,
  &quot;alt&quot;: &quot;两只小兽依偎在花丛中&quot;,
  &quot;date&quot;: &quot;2026-09-18&quot;,
  &quot;width&quot;: 1600,
  &quot;height&quot;: 901,
  &quot;tags&quot;: [&quot;插画&quot;],
  &quot;license&quot;: &quot;CC BY-NC-SA 4.0&quot;,
  &quot;meta&quot;: [[&quot;尺寸&quot;, &quot;1600 × 901&quot;], [&quot;格式&quot;, &quot;WebP&quot;]]
}
</code></pre>
<p>三档图按 <code>&lt;slug&gt;.webp</code> 命名放在三个目录下；<code>src</code> / <code>preview</code> / <code>thumb</code> 不写时按约定拼。
相册页支持双击、回车、触控打开灯箱，左右键切换，打开时会预取相邻两张。</p>
<h3 id="图片的缺陷先讲清楚">图片的缺陷，先讲清楚</h3>
<div class="table-wrap"><table>
<thead>
<tr>
<th>限制</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>文章里的图<strong>没有灯箱</strong></td>
<td>灯箱只给相册。想放大只能浏览器缩放</td>
</tr>
<tr>
<td>不能指定显示尺寸</td>
<td><code>![x](a.webp =300x)</code> 这种 Obsidian 写法不被解析，只能靠原图和 CSS</td>
</tr>
<tr>
<td>不能直接粘贴</td>
<td>没有上传接口，得自己把文件放进 <code>assets/images/</code></td>
</tr>
<tr>
<td>不做自动压缩</td>
<td>放多大的图就发多大的图，投放前自己压</td>
</tr>
<tr>
<td>没有响应式 <code>srcset</code></td>
<td>正文图只输出一个 <code>src</code></td>
</tr>
</tbody>
</table></div>
<h3 id="自定义壁纸">自定义壁纸</h3>
<p>最底层那张背景默认是内置渐变。想换成图片，改 <code>site.json</code>：</p>
<pre><code class="language-json">&quot;canvas&quot;: {
  &quot;wallpaper&quot;: &quot;images/hero-1600.webp&quot;,
  &quot;veil&quot;: 0
}
</code></pre>
<ul>
<li>路径相对 <code>assets/</code>，不用写 <code>assets/</code> 本身；构建时会按页面深度补前缀，子页面也不会断。</li>
<li>留空就用渐变壁纸。</li>
<li>设了图片会自动压一层底色（亮色白纱、暗色黑纱），避免正文直接压在照片纹理上。</li>
<li>文件找不到时构建会打印一行提示并忽略，不会产出 404。</li>
<li>壁纸层同时是玻璃窗口的折射源，换成图片后折射会跟着换。</li>
</ul>
<p>背景里那三个飘动的光团（<code>.bubble</code>）是纯装饰：位置、时长在 <code>assets/style.css</code> 里，关掉不影响任何功能。</p>
<h2 id="二markdown-支持到什么程度">二、Markdown 支持到什么程度</h2>
<p>渲染器是 <strong>markdown-it</strong> 加几个插件，再补一层 Obsidian 方言。支持：</p>
<ul>
<li><strong>基础</strong>：标题、段落、粗斜体、行内代码、围栏代码块（带语言 class）、引用、有序/无序/嵌套列表、链接、图片、分隔线</li>
<li><strong>表格</strong>：带 <code>:---:</code> 对齐，外面自动包一层 <code>.table-wrap</code>（窄屏横向滚动）</li>
<li><strong>删除线</strong> <code>~~删~~</code>、<strong>高亮</strong> <code>==亮==</code></li>
<li><strong>任务列表</strong> <code>- [ ]</code> / <code>- [x]</code>：真 checkbox（禁用态），勾选的自动划线</li>
<li><strong>脚注</strong> <code>文字[^1]</code> + <code>[^1]: 内容</code>：正文上标、文末「脚注」区、双向锚点</li>
<li><strong>callout</strong>：<code>&gt; [!note] 标题</code>，类型有 note / info / tip / warning / danger / success / example / quote / fail / bug / important，另有 abstract、summary、tldr、todo、hint、question、help、faq、caution、attention、check、done、failure、missing、error、cite 这些别名</li>
<li><strong>内链</strong> <code>[[某个标题]]</code> 或 <code>[[某个标题|别名]]</code>：如果指的是<strong>本文里的标题</strong>，会变成锚点跳转；指不到就渲染成斜体虚线标记，并在构建时提示</li>
<li><strong>注释</strong> ``</li>
<li><strong>外链</strong>自动加 <code>target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;</code></li>
</ul>
<h3 id="标题的两条规矩">标题的两条规矩</h3>
<ol>
<li>文档里<strong>最浅的那一级标题对齐到 h2</strong>，其余保持相对层级。所以整篇只用 <code>#</code> 写也不会有两个 h1。</li>
<li>id 由标题文字生成（中文也保留），重复的自动加 <code>-1</code>。目录、锚点、内链都靠它。</li>
</ol>
<h3 id="中文软换行">中文软换行</h3>
<p>源码里在句子中间换行，中文之间不会多出空格；中英之间保留一个空格。</p>
<h3 id="markdown-的缺陷">Markdown 的缺陷</h3>
<div class="table-wrap"><table>
<thead>
<tr>
<th>不支持的写法</th>
<th>现在的结果</th>
</tr>
</thead>
<tbody>
<tr>
<td>原始 HTML <code>&lt;kbd&gt;</code>、<code>&lt;span&gt;</code></td>
<td>一律转义成文本（只有代码块里有意义）</td>
</tr>
<tr>
<td>数学公式 <code>$E=mc^2$</code></td>
<td>原样显示</td>
</tr>
<tr>
<td>mermaid 图</td>
<td>当普通代码块</td>
</tr>
<tr>
<td>Obsidian 嵌入 <code>![[图片.webp]]</code></td>
<td>显示成行内代码，构建时提示改用 <code>![](images/…)</code></td>
</tr>
<tr>
<td>图片尺寸/对齐语法</td>
<td>忽略</td>
</tr>
<tr>
<td>定义列表、目录块 <code>[TOC]</code></td>
<td>不支持（目录由模板生成）</td>
</tr>
</tbody>
</table></div>
<h2 id="三新增一篇文章">三、新增一篇文章</h2>
<ol>
<li>在 <code>content/posts/</code> 新建 <code>&lt;slug&gt;.md</code>，文件名就是 URL：<code>content/posts/hello.md</code> → <code>/posts/hello/</code>。</li>
<li>写 frontmatter：</li>
</ol>
<pre><code class="language-yaml">---
title: 标题
date: 2026-11-04        # 2026-11-4 也认
tags: [design, css]
summary: 一句话摘要      # 不写就取正文首段前 96 字
cover: cover.webp       # 可选
draft: false            # 可选，true 时不生成
---
</code></pre>
<ol start="3">
<li>正文随便写，图片放 <code>assets/images/</code>。</li>
<li><code>npm run build</code>。</li>
</ol>
<p>首页、文章列表、归档、标签页、RSS、Atom、搜索索引、相关阅读，都是从 <code>content/posts/</code> 里算出来的，<strong>不用手动登记任何地方</strong>。</p>
<h2 id="四改标签">四、改标签</h2>
<p>标签只写在 frontmatter 里：</p>
<pre><code class="language-yaml">tags: [design, css]
</code></pre>
<p>改完重新构建，三件事会自动发生：</p>
<ul>
<li>标签页 <code>/tags/</code> 重新统计数量；</li>
<li>每个标签的 <code>/tags/&lt;slug&gt;/</code> 重新生成，没有文章了就不生成；</li>
<li>上一页/下一页的归档、搜索索引一起更新。</li>
</ul>
<p>标签是「按名字统计」的，大小写和空格都算不同标签；中文标签可以用，URL 会做 percent 编码。</p>
<h2 id="五相关阅读怎么来的">五、相关阅读怎么来的</h2>
<p>文章页底部的「相关阅读」规则很短：</p>
<ol>
<li>取<strong>共享至少一个标签</strong>的其他文章；</li>
<li>按文章顺序（日期倒序）取前 <strong>2 篇</strong>。</li>
</ol>
<p>所以想让两篇文章互相出现，给它们同一个标签就够了。没有共享标签时，这一块整个不显示——不会硬凑。</p>
<h2 id="六构建与部署">六、构建与部署</h2>
<pre><code class="language-bash">npm install      # 构建期依赖：markdown-it 与几个插件
npm run build    # 产出 dist/
npm run dev      # 构建 + 本地 8899 预览
</code></pre>
<p><code>dist/</code> 是纯静态文件，丢给 Cloudflare Workers / Pages 即可，仓库里的 <code>wrangler.jsonc</code> 没有任何 <code>main</code>，只当静态资源用。</p>
<h2 id="七已知边界">七、已知边界</h2>
<ul>
<li>构建时警告不影响产物：未解析的内链、不支持的嵌入、解析失败的日期，都会打印一行提示。</li>
<li>同屏最多两扇「真折射」窗口（首页 Hero + 最近文章卡），这是性能红线。</li>
<li>纸材质自 v5 起冻结，只修可读性问题，不再加新效果。</li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>摄影心得</title>
      <link>https://blog.silvershaded.top/posts/test-blog/</link>
      <guid isPermaLink="true">https://blog.silvershaded.top/posts/test-blog/</guid>
      <pubDate>Wed, 04 Nov 2026 00:00:00 GMT</pubDate>
      <description>这是一份偏向“速查手册”性质的说明书，素材精简，旨在提供快速检索。</description>
      <content:encoded><![CDATA[<div class="callout callout--info"><p class="callout__title">阅读指引</p><div class="callout__body"><p>这是一份偏向“速查手册”性质的说明书，素材精简，旨在提供快速检索。<br>
建议使用MarkDown阅读器打开，并安利一下obsidian（（</p>
</div></div><h2 id="00拍照现场-光">0.0.拍照现场 · 光</h2>
<h3 id="0前言">0.前言</h3>
<p>无论怎样，在你按下 <code>#快门</code> 的瞬间，就已经决定了你要表达的大部分东西。</p>
<div class="callout callout--quote"><p class="callout__title">Introduce</p><div class="callout__body"><p><strong>摄影是光学的艺术。</strong></p>
<p>——客观上有多少光打到 #cmos 上、光以什么样的形式打到 #cmos 上决定了你的 #相机 客观承载了多少信息（至于为什么是 #相机 而不是 #照片 在0.0.2 #素材宽容度 有解释）
——同时，你的相机怎么处理光学信号也决定了一家相机厂商的风格。这一点主要影响相机 #直出 ，但也会细微影响 #raw 文件。</p>
</div></div><hr>
<h3 id="1-cmos-与宽容度">1. CMOS 与宽容度</h3>
<h4 id="11-什么是-cmos">1.1 什么是 CMOS？</h4>
<p>这里不赘述画幅概念。在相机领域，<code>#CMOS</code> 特指那块将光信号转换为电信号的感光元件。</p>
<ul>
<li>
<p><strong>成像原理</strong>：CMOS 将光信号切分为无数方形小格（像素）。采用 <code>#拜耳阵列</code> 设计，每四个小格包含两个绿色、一个红色、一个蓝色（<code>#GRGB</code>），每个小格传回独立的亮度信号采集一组 <code>#RGB</code> 值后合成为数字图像的一个像素。</p>
</li>
<li>
<p><strong>物理构成</strong>：通常所说的“CMOS”不仅指感光芯片本身，还包括其前方的 <code>#低通滤光片</code> 和 <code>#红外截止滤光片</code>。</p>
<div class="callout callout--warning"><p class="callout__title">维护警示</p><div class="callout__body"><p>当人们说“CMOS 上有灰”时，灰尘通常附着在最外层的滤镜玻璃上。即使该玻璃可更换，也请<strong>极其爱惜</strong>，因为镀膜层极易损坏或沾上清洁液而导致镀膜不平，进而整机光学素质严重下降；而且更换拆卸过程极为繁琐。</p>
</div></div></li>
<li>
<p><strong>市场格局</strong>：主流制造商为 <code>#索尼</code> 和 <code>#佳能</code>。索尼对外供应 CMOS，因此民用级非佳能品牌相机，极大概率搭载索尼传感器。</p>
<div class="callout callout--tip"><p class="callout__title">SONY IMX410 sensor</p><div class="callout__body"><p>质优平价，大多2480w左右像素的全画幅相机都搭载了这一块传感器</p>
</div></div></li>
</ul>
<h4 id="12-宽容度-动态范围-dynamic-range">1.2 宽容度 / 动态范围 (Dynamic Range)</h4>
<p><code>#宽容度</code>（等同 <code>#动态范围</code>）衡量的是传感器记录光线明暗跨度的能力。</p>
<ul>
<li>
<p><strong>定义</strong>：从最低有效非零信号到最高不饱和信号之间的比值，取以 2 为底的对数。单位为 <code>#档</code> 或 <code>#EV</code>。</p>
<div class="callout callout--example"><p class="callout__title">计算示例</p><div class="callout__body"><p>若最高电平为 1024µV，最低有效电平为 1µV，则宽容度 = log⁡2 (1024/1)=10EV 。</p>
</div></div></li>
<li>
<p><strong>死黑与死白</strong>：超出上述范围的数据将被截断。</p>
<ul>
<li><code>#死黑</code>：RGB 全为 0，无明度信息，无法提亮恢复。</li>
<li><code>#死白</code>：单像素达到最大光强溢出，无色彩细节，无法压暗找回。</li>
</ul>
</li>
<li>
<p><strong>ISO 与宽容度的关系</strong>：<strong>ISO 每提升一倍，宽容度约降低一档。</strong></p>
</li>
</ul>
<h4 id="13-主流机型宽容度参考表">1.3 主流机型宽容度参考表</h4>
<div class="callout callout--note"><p class="callout__title">数据说明</p><div class="callout__body"><p>以下数据基于基准 ISO 测试，作为理论分析根据；实际表现受固件、ADC 位数及读取架构影响。一般认为 <strong>14 EV+</strong> 足以支持深度调色。</p>
</div></div><h5 id="全画幅-中画幅梯队">全画幅 / 中画幅梯队</h5>
<div class="table-wrap"><table>
<thead>
<tr>
<th>机型 / 传感器</th>
<th>基准ISO下的 DR</th>
<th>核心优势</th>
<th>备注</th>
</tr>
</thead>
<tbody>
<tr>
<td>Hasselblad X2D 100C</td>
<td>~15.8 EV</td>
<td>1亿像素 + 16bit ADC</td>
<td>目前量产相机宽容度天花板</td>
</tr>
<tr>
<td>Fujifilm GFX100 II/S</td>
<td>~15.5 EV</td>
<td>1亿像素 BSI</td>
<td>与哈苏处于同一水平线</td>
</tr>
<tr>
<td>Nikon Z9 / Z8</td>
<td>~15.2 EV</td>
<td>堆栈式 BSI + 双增益</td>
<td>全画幅宽容度标杆</td>
</tr>
<tr>
<td>Sony A7R V / A1</td>
<td>~15.0-15.2 EV</td>
<td>61MP BSI Exmor R</td>
<td>高像素与高 DR 的完美平衡</td>
</tr>
<tr>
<td>Canon EOS R5 Mark II</td>
<td>~15.0 EV</td>
<td>背照堆栈式新架构</td>
<td>相比初代 R5 提升约 1 档</td>
</tr>
<tr>
<td>Sony A7 IV / A7C II</td>
<td>~14.7 EV</td>
<td>33MP BSI</td>
<td>性价比全能机</td>
</tr>
<tr>
<td>Nikon Z6 III / Zf</td>
<td>~14.6 EV</td>
<td>部分堆栈/BSI</td>
<td>高感优秀</td>
</tr>
<tr>
<td>Canon EOS R6 Mark II</td>
<td>~14.5 EV</td>
<td>24MP 前照式</td>
<td>读出噪声控制极佳</td>
</tr>
<tr>
<td>Panasonic S5M2 / S9</td>
<td>~14.4 EV</td>
<td>24MP BSI</td>
<td>视频/照片双修</td>
</tr>
<tr>
<td>Sony A7S III / FX3</td>
<td>~14.3 EV</td>
<td>12MP 超大像元</td>
<td>高感 DR 衰减最慢</td>
</tr>
</tbody>
</table></div>
<h5 id="aps-c-m43-梯队">APS-C / M43 梯队</h5>
<div class="table-wrap"><table>
<thead>
<tr>
<th>机型 / 传感器</th>
<th>基准 ISO DR</th>
<th>备注</th>
</tr>
</thead>
<tbody>
<tr>
<td>Fujifilm X-H2S / X-T5</td>
<td>~13.8 EV</td>
<td>APS-C 天花板，BSI + 双增益</td>
</tr>
<tr>
<td>Canon EOS R7</td>
<td>~13.6 EV</td>
<td>APS-C 高分辨率兼顾 DR</td>
</tr>
<tr>
<td>OM-1 Mark II</td>
<td>~13.4 EV</td>
<td>M43 堆栈式 BSI，计算摄影弥补差距</td>
</tr>
<tr>
<td>Sony A6700</td>
<td>~13.3 EV</td>
<td>AI 芯片加持，裸 DR 略逊于富士</td>
</tr>
</tbody>
</table></div>
<div class="callout callout--tip"><p class="callout__title">关于电影机</p><div class="callout__body"><p>现代专业电影机的宽容度通常在 <strong>16 EV 或更高</strong>，代价往往是像素量的妥协（但不绝对）。这部分内容距离普通摄影较远，<em>欢迎有相关经验的后来者补充修订此段</em>。</p>
</div></div><hr>
<h4 id="本章节后续索引">本章节后续索引</h4>
<ul>
<li><a class="wikilink is-resolved" href="#3光比">0.0.2 光比</a>：从传感器到文件，数据是如何被“压缩”的？</li>
<li><a class="wikilink is-resolved" href="#3光比">0.0.3 光比</a>：现场光线的明暗跨度如何应对？</li>
<li><a class="wikilink is-resolved" href="#4白平衡">0.0.4 白平衡</a>：色温与色调的物理本质及校正逻辑</li>
<li><a class="wikilink is-resolved" href="#5暗光环境和cmos热燥">0.0.5 暗光环境和CMOS热燥</a>：长曝光与高感下的 CMOS 热管理</li>
<li><a class="wikilink is-resolved" href="#6题外话分辨率和二次裁切构图">0.0.6 题外话——分辨率和二次裁切构图</a>：像素冗余对二次构图的意义</li>
</ul>
<h3 id="2素材宽容度">2.素材宽容度</h3>
<div class="callout callout--info"><p class="callout__title">这里形如 2bit<strong>1</strong> 指的都是RGB通道中任意通道位深。</p><div class="callout__body"></div></div><div class="callout callout--info"><p class="callout__title">核心差异</p><div class="callout__body"><p><strong>CMOS 宽容度 ≠ 素材宽容度</strong><br>
上一节提到的 14-15 EV 是传感器的<strong>物理极限</strong>；而你实际拿到手的 RAW 或视频文件能保留多少细节，取决于<strong>数据封装格式</strong>。</p>
<p>宽容度与色深直接挂钩。</p>
<pre><code>常见的**视频**色深一般有**8bit、10bit、12bit**。其中，**12bit**一般会出现在**raw**格式上。

 注意：4:4:2 指的是采样方式，与色深无关。
</code></pre>
<p>常见的<strong>照片</strong>色深一般有<strong>8bit</strong>——典型的jpg；<strong>10bit</strong>——HEIF；<strong>12bit、14bit</strong>——RAW</p>
<p>我们常见的<strong>素材压缩格式</strong>有H.265(HEVC)、H.264(更原始的MP4，尽管HEVC的后缀名也叫*.mp4*)。但这是压缩形式，只代表了压缩算法。算法、码率只代表了怎么把一段素材压缩的更小，与画质相关；但不涉及我们这里讲的<strong>宽容度</strong>。</p>
</div></div><h4 id="11-从cmos读取的模拟信号的数字化adc-非线性映射与位深">1.1 从CMOS读取的模拟信号的数字化——ADC 、非线性映射与位深</h4>
<p>CMOS 捕获的是连续模拟信号，必须经过 <code>#ADC</code>（模数转换器）量化为数字信号。这个过程是素材宽容度的第一道瓶颈。</p>
<ul>
<li><strong>位深 (<code>#BitDepth</code>)</strong>：决定了灰阶的离散层级数量。
<ul>
<li><strong>12-bit</strong>：4,096 级灰阶。早期入门机或高速连拍模式常见，暗部断层风险较高。</li>
<li><strong>14-bit</strong>：16,384 级灰阶。当前主流全画幅标准，平衡了画质与读取速度。</li>
<li><strong>16-bit</strong>：65,536 级灰阶。中画幅/旗舰机专属，为极端后期提供巨大余量。</li>
</ul>
</li>
<li><strong>非线性映射</strong>：人眼对暗部敏感、对亮部迟钝。为了高效存储，ADC 通常采用<strong>对数或伽马曲线</strong>分配码值——暗部给更多码值，亮部压缩码值。这意味着 RAW 文件的“数字宽容度”分布并不均匀。</li>
</ul>
<div class="callout callout--warning"><p class="callout__title">误区提醒</p><div class="callout__body"><p>“我的相机能拍摄 14-bit RAW” ≠ “14 档动态范围全域可用”。<br>
若传感器本底噪声过高或 ADC 性能不足，或者ISO过高，低位数据可能被噪声淹没，<strong>ke y可用位深</strong>往往低于标称值。</p>
</div></div><h4 id="12-raw-vs-视频编码宽容度的两种命运">1.2 RAW vs. 视频编码：宽容度的两种命运</h4>
<div class="table-wrap"><table>
<thead>
<tr>
<th>维度</th>
<th>RAW (Photo / Video)</th>
<th>Log 视频 (10-bit)</th>
<th>H.264/H.265 (8-bit Rec.709)</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>本质</strong></td>
<td>传感器原始数据 + 元数据</td>
<td>经数学变换的“数字底片”</td>
<td>已烘焙的显示就绪图像</td>
</tr>
<tr>
<td><strong>宽容度利用率</strong></td>
<td>≈ 传感器物理极限</td>
<td>约 12-13 EV（取决于曲线设计）</td>
<td>约 6-8 EV</td>
</tr>
<tr>
<td><strong>高光恢复</strong></td>
<td>依赖传感器未溢出区域</td>
<td>依赖 Log 曲线肩部设计</td>
<td>❌ 几乎不可能</td>
</tr>
<tr>
<td><strong>暗部提拉</strong></td>
<td>受限于读出噪声</td>
<td>受限于编码量化精度</td>
<td>❌ 色带/噪点爆炸</td>
</tr>
<tr>
<td><strong>调色空间</strong></td>
<td>线性/无损变换</td>
<td>需还原 LUT 或手动转换</td>
<td>仅微调</td>
</tr>
</tbody>
</table></div>
<h5 id="关于-log-曲线的补充">关于 Log 曲线的补充</h5>
<p><code>#Log</code> 不是魔法，它是一种<strong>重新分配码值的策略</strong>：</p>
<ul>
<li>将传感器的高光信息压缩进有限的 10-bit 容器中</li>
<li>牺牲中间调对比度，换取高光和暗部的记录空间</li>
<li><strong>代价</strong>：若曝光失误（尤其是欠曝），10-bit 的码值不足以支撑拉伸，比 RAW 更容易出现 <code>#色彩断层</code></li>
</ul>
<h4 id="13-影响素材宽容度的隐性因素">1.3 影响素材宽容度的隐性因素</h4>
<p>除了位深和编码，以下因素同样关键：</p>
<ul>
<li><strong>双增益架构 (<code>#DualGain</code>)</strong>：在特定 ISO 切换点（如 Sony A7S III 的 ISO 640/12800），传感器切换至高转换增益电路，<strong>读出噪声骤降</strong>，暗部宽容度瞬间提升约 2-3 档。<em>拍摄时务必了解你相机的第二增益触发点。</em></li>
<li><strong>压缩算法</strong>：即使是 RAW，也有无损压缩、有损压缩之分。有损压缩（如 Sony Lossy Compressed）在高光过渡区可能引入 <code>#色调分离</code>，实测宽容度损失约 0.5-1 EV。</li>
<li><strong>过热保护</strong>：长时间录制时，CMOS 温度升高导致 <code>#热噪声</code> 增加，暗部信噪比恶化，<strong>实际可用宽容度随时间衰减</strong>。这也是为什么电影机需要主动散热。</li>
</ul>
<h4 id="14-实战建议如何最大化素材宽容度">1.4 实战建议：如何最大化素材宽容度</h4>
<div class="callout callout--tip"><p class="callout__title">✅ 拍摄检查清单</p><div class="callout__body"><ol>
<li><strong>永远使用最高质量 RAW</strong>（除非明确只需要直出 JPG）</li>
<li><strong>向右曝光 (<code>#ETTR</code>)</strong>：在不溢出高光的前提下尽量提亮，让信号远离本底噪声区。RAW 的暗部不是“免费”的。</li>
<li><strong>了解你的双增益节点</strong>：在暗光环境下，宁可跳到第二增益 ISO，也不要在两档之间犹豫</li>
<li><strong>视频优先选 10-bit Log</strong>：8-bit Log 是伪命题，量化精度不足以支撑 Log 曲线的拉伸</li>
<li><strong>监看时用辅助工具</strong>：<code>#波形图</code> / <code>#假色</code> 比屏幕肉眼更可靠地判断高光是否真正溢出</li>
</ol>
</div></div><h3 id="3光比">3.光比</h3>
<h3 id="4白平衡">4.白平衡</h3>
<h3 id="5暗光环境和cmos热燥">5.暗光环境和CMOS热燥</h3>
<h3 id="6题外话分辨率和二次裁切构图">6.题外话——分辨率和二次裁切构图</h3>
<h2 id="01调色">0.1.调色</h2>
<h3 id="1一级调色与二级调色">1.一级调色与二级调色</h3>
<h3 id="2对象">2.对象</h3>
<h3 id="3工具">3.工具</h3>
<h3 id="4审美">4.审美</h3>
<h3 id="5交付">5.交付</h3>
<h2 id="02视觉呈现">0.2.视觉呈现</h2>
<h3 id="1色彩空间">1.色彩空间</h3>
<h3 id="2视觉的ex曲线">2.视觉的e^x曲线</h3>
<h3 id="3有限色域-sdr">3.有限色域 · SDR</h3>
<h3 id="4无限色域-hdr">4.无限色域 · HDR</h3>
<h2 id="03蒙版">0.3.蒙版</h2>
<h3 id="1蒙版的选择-我怎么选中">1.蒙版的选择 · 我怎么选中</h3>
<h3 id="2蒙版的选择-我怎么选中">2.蒙版的选择 · 我怎么选中</h3>
<h3 id="3lrc的蒙版能做什么">3.Lrc的蒙版能做什么</h3>
<h3 id="4lrc蒙版缺陷相比于图层">4.Lrc蒙版缺陷——相比于图层</h3>
<h2 id="04镜头视角及长宽比">0.4.镜头视角及长宽比</h2>
<h3 id="1焦段">1.焦段</h3>
<h3 id="2画幅">2.画幅</h3>
<h3 id="3光圈与景深">3.光圈与景深</h3>
<h3 id="4t值电影进阶">4.T值——电影进阶</h3>
<h3 id="5长宽比">5.长宽比</h3>
]]></content:encoded>
    </item>
    <item>
      <title>两套美工，不是两套皮肤</title>
      <link>https://blog.silvershaded.top/posts/two-materials/</link>
      <guid isPermaLink="true">https://blog.silvershaded.top/posts/two-materials/</guid>
      <pubDate>Wed, 04 Nov 2026 00:00:00 GMT</pubDate>
      <description>玻璃和纸不该共用一套变量。这一版把它们拆成两个对等的渲染程序：各写各的 token，各画各的层，切换只是换一个程序。</description>
      <content:encoded><![CDATA[<p>上一版里，纸是玻璃的「干态」：同一套 <code>--glass-*</code> 变量，<code>[data-surface=&quot;paper&quot;]</code> 只是把数值改小、把模糊关掉。</p>
<p>能跑，但有个隐患——<strong>纸永远长在玻璃的骨架上</strong>。玻璃加一个新特性（比如跟着指针反光），纸就得跟着写一条「不要反光」的规则。久而久之，两套材质都变成了补丁。</p>
<p>这一版反过来了：它们是两个程序。</p>
<h2 id="窗口只有骨架材质是画上去的">窗口只有骨架，材质是画上去的</h2>
<pre><code class="language-html">&lt;div class=&quot;window&quot;&gt;
  &lt;span class=&quot;window__backdrop&quot;&gt;&lt;/span&gt;  &lt;!-- 材质层：折射 或 纸面 --&gt;
  &lt;span class=&quot;window__sheen&quot;&gt;&lt;/span&gt;     &lt;!-- 反射层：只有玻璃有 --&gt;
  &lt;div class=&quot;window__content&quot;&gt;…&lt;/div&gt;    &lt;!-- 内容 --&gt;
&lt;/div&gt;
</code></pre>
<p>骨架只负责几何：圆角、裁剪、过渡。材质层由 <code>data-surface</code> 选择用哪套程序来画：</p>
<div class="table-wrap"><table>
<thead>
<tr>
<th></th>
<th>liquidglass</th>
<th>paper</th>
</tr>
</thead>
<tbody>
<tr>
<td>材质层</td>
<td>壁纸副本 + SVG 位移场（真折射）</td>
<td>暖白底色 + 纤维纹理</td>
</tr>
<tr>
<td>反射层</td>
<td>指针驱动的 specular 热点</td>
<td><strong>没有</strong></td>
</tr>
<tr>
<td>纸纹</td>
<td>水印（<code>.05</code>）</td>
<td>本体（<code>.07</code>，暗色 <code>.09</code>）</td>
</tr>
<tr>
<td>变量前缀</td>
<td><code>--lg-*</code></td>
<td><code>--pp-*</code></td>
</tr>
</tbody>
</table></div>
<p>两者没有任何共享的材质变量。<code>--lg-*</code> 在纸态下不存在，<code>--pp-*</code> 在玻璃态下也不存在——<strong>不存在，就没人会误用</strong>。</p>
<h2 id="为什么非要拆开">为什么非要拆开</h2>
<p>除了「概念干净」，还有一个很实际的原因：<strong>性能预算不一样</strong>。</p>
<ul>
<li>玻璃要算折射、要重绘、要 Worker，所以同屏最多两扇窗用真折射，其余退化成「色层 + 高光边」。</li>
<li>纸什么都不用算，只是贴一张静态纹理。</li>
</ul>
<p>如果共用一套变量，这种差异只能靠一堆 <code>if</code> 条件去表达；拆成两个程序之后，它是<strong>结构上的差异</strong>——玻璃实例登记进渲染器，纸实例压根不登记。</p>
<h2 id="按钮也是窗口">按钮也是窗口</h2>
<div class="callout callout--note"><p class="callout__title">一条小规矩</p><div class="callout__body"><p>按钮不是「长得像玻璃的按钮」，它就是一个窗口实例：只指定位置与大小，宽度由文字决定。</p>
</div></div><pre><code class="language-html">&lt;a class=&quot;window window--btn&quot; data-window data-mode=&quot;soft&quot; href=&quot;…&quot;&gt;
  &lt;span class=&quot;window__backdrop&quot;&gt;&lt;/span&gt;
  &lt;span class=&quot;window__sheen&quot;&gt;&lt;/span&gt;
  &lt;span class=&quot;window__content&quot;&gt;读文章&lt;/span&gt;
&lt;/a&gt;
</code></pre>
<p>因为走的是同一套骨架与同一个材质程序，按钮的圆角、高光边、悬停位移和卡片永远一致——不是「对齐了」，是<strong>同源</strong>。</p>
<h2 id="切换那一下">切换那一下</h2>
<p><code>data-surface</code> 一变，两个程序交替挂载：</p>
<ol>
<li>玻璃的反射参数被清空（不再有人写 <code>--sx/--sy</code>）；</li>
<li>纸程序给每扇窗分配一点纤维偏移，避免整页纹理重复；</li>
<li>CSS 的 240ms 过渡负责「水分蒸发」。</li>
</ol>
<p>骨架一点没动，内容一点没动。这就是「切换成本低」的真正含义——<strong>换的不是样式，是渲染程序</strong>。</p>
<h2 id="代价">代价</h2>
<p>说实话：代码量比一套变量多了大约三成，第一次读的人得同时看两段 CSS。</p>
<p>但换来的是一句可以一直用下去的话：</p>
<blockquote>
<p>想改纸，只改纸；想改玻璃，只改玻璃。互不牵连。</p>
</blockquote>
]]></content:encoded>
    </item>
    <item>
      <title>这个站是怎么长出来的</title>
      <link>https://blog.silvershaded.top/posts/dev-log/</link>
      <guid isPermaLink="true">https://blog.silvershaded.top/posts/dev-log/</guid>
      <pubDate>Tue, 03 Nov 2026 00:00:00 GMT</pubDate>
      <description>从「换一套主题」到「自己写一个生成器」，再到把边框当成透镜来算——记录几轮推倒重来的判断过程，以及最后留下的取舍。</description>
      <content:encoded><![CDATA[<p>这篇不是教程，是一份工程记录。写它是因为几个月后我会忘掉当初为什么这么做，而「为什么」比「怎么做」更容易消失。</p>
<h2 id="起点">起点</h2>
<p>原来的站是 Hugo 加一套改过的主题。用起来没问题，改起来处处别扭：想调一层玻璃的折射，要先在模板里找它属于哪个 partial；想换首页的结构，得跟主题的短代码较劲。</p>
<p>真正让我下决心的是两件事：</p>
<ol>
<li>我想要的首页是「一张图 + 一层玻璃」，主题的骨架里没有这个概念；</li>
<li>每次改完都要重新理解一遍别人的抽象。</li>
</ol>
<p>于是先做减法：<strong>整站只有两层——壁纸，和浮在壁纸上的窗口。</strong></p>
<p>这句话后来成了所有设计决策的标尺。窗口是玻璃，它折射壁纸；窗口是纸，光就退回边缘。没有第三层，也就不需要去解释第三层。</p>
<h2 id="第一版把窗口拆成三层">第一版：把窗口拆成三层</h2>
<p>一个窗口拆成三个兄弟节点，各自负责一件事：</p>
<pre><code class="language-html">&lt;div class=&quot;window&quot;&gt;
  &lt;span class=&quot;window__backdrop&quot;&gt;&lt;/span&gt;   &lt;!-- 材质：折射 或 纸面 --&gt;
  &lt;span class=&quot;window__sheen&quot;&gt;&lt;/span&gt;      &lt;!-- 反射：只有玻璃有 --&gt;
  &lt;div class=&quot;window__content&quot;&gt;…&lt;/div&gt;     &lt;!-- 内容 --&gt;
&lt;/div&gt;
</code></pre>
<p>这样拆的理由不是好看，是<strong>顺序</strong>：反射必须在内容之下，否则文字会被一起糊掉。第一版我把 sheen 放在内容之上，页面上所有白字都变成了雾——这是最早踩的坑，也是最值得记的一个：分层要按光的路径排，不是按重要性排。</p>
<h2 id="材质从看起来像到算出来">材质：从「看起来像」到「算出来」</h2>
<p>玻璃的第一版是老实做法：<code>backdrop-filter: blur()</code> 加几个渐变高光。像，但不对——真实玻璃的边缘会<strong>弯折</strong>背后的画面。</p>
<p>于是有了第二版：把壁纸复制进窗口，用 <code>feTurbulence</code> 加 <code>feDisplacementMap</code> 推一下。问题随之而来：位移是假的（噪声位移没有方向），而且只对固定背景成立。</p>
<p>第三版换了思路，把边框当成一块<strong>有厚度的斜面透镜</strong>：</p>
<pre><code class="language-text">σ(d) = σ_max                        d ≤ rs        ← 与玻璃内部同模糊
       σ_max · (1 − (d−rs)/(rl−rs)) rs &lt; d &lt; rl   ← 线性衰减
       0                            d ≥ rl        ← 中心清晰

Δ(d) = rs · tan(θt − θ) · (1 − d/rs)^1.5
</code></pre>
<p>距离用圆角矩形的 SDF 求，位移方向取外法线，<code>θt</code> 由 Snell 定律给出。这样直边、圆角、角落的模糊带宽度自动一致，位移尺度也由折射率和斜面倾角决定，而不是拍脑袋的常数。</p>
<p>实现落在 WebGL：三级模糊纹理、逐像素算 SDF、R/B 通道按 <code>n(λ)</code> 错开做色散。中心是清晰的，边缘是糊的，斜线在边界处有一点点彩边——这就是液态玻璃区别于「毛玻璃」的地方。</p>
<h2 id="首页让一张图变成一张卡">首页：让一张图变成一张卡</h2>
<p>原本的设计是「满屏图 → 滚动 → 变成玻璃卡」两个状态，中间靠 CSS 过渡。做出来很难看：两个状态之间是跳的。</p>
<p>改成<strong>一个变量驱动</strong>：</p>
<div class="table-wrap"><table>
<thead>
<tr>
<th>量</th>
<th><code>--p = 0</code></th>
<th><code>--p = 1</code></th>
</tr>
</thead>
<tbody>
<tr>
<td>宽度</td>
<td>100vw</td>
<td>正文宽度</td>
</tr>
<tr>
<td>高度</td>
<td>视口高</td>
<td>460px</td>
</tr>
<tr>
<td>圆角</td>
<td>0</td>
<td>24px</td>
</tr>
<tr>
<td>材质</td>
<td>全关（纯图）</td>
<td>玻璃 + 边框 + 投影</td>
</tr>
</tbody>
</table></div>
<p><code>--p</code> 由 <code>scrollY / (0.38 × 视口高)</code> 算出来，写到 <code>&lt;html&gt;</code> 上。整个形变就是一段插值，没有状态机。</p>
<p>顺带的收益：透镜的强度也跟着 <code>--p</code> 走——<code>p=0</code> 时画布就是一张原图，<code>p=1</code> 时才是完整玻璃。一个变量管两件事，改起来不会脱节。</p>
<h2 id="内容层把解析器交出去">内容层：把解析器交出去</h2>
<p>Markdown 最初是我手写的，一千多行。写的时候很爽，因为每个 Obsidian 语法都能照顾到；维护起来很痛，因为<strong>每一条语法都是我自己的责任</strong>。</p>
<p>转折点是一篇带 <code>date: 2026-11-4</code> 的文章——没补零，<code>new Date()</code> 返回 <code>Invalid Date</code>，整个构建挂掉。那一刻我意识到：这类坑不该由我埋。</p>
<p>于是换成 markdown-it 加三个插件，自己只留 Obsidian 方言那一层：callout、内链、标题层级对齐、中文软换行。渲染器从 1139 行降到 321 行。</p>
<p>迁移时做了一件事：<strong>同一批文章用新旧两个渲染器各跑一遍，统计标题、callout、表格、任务项、脚注、内链、目录条数</strong>。十个文件全部一致，才敢删旧的。这种比对很笨，但比「看起来没问题」可靠。</p>
<h2 id="交互目录是块玻璃">交互：目录是块玻璃</h2>
<p>目录换过两次。</p>
<p>第一版是侧栏里的一棵静态树，缩进加序号。问题是它太安静——你永远不知道自己在文章的哪一段。</p>
<p>第二版加了实时高亮和折叠，但序号和箭头堆在一起，缩进又吃掉一半宽度，读起来像文件管理器。</p>
<p>现在是第三版：<strong>目录是一块贴在右缘的玻璃面板</strong>，位置在导航之下、文章之上。</p>
<ul>
<li>有子级才给箭头，没有子级是圆点，序号全部去掉；</li>
<li>收起按钮 38×38 放在面板左上角（上一版是 14px 的 <code>»</code>，点不中）；</li>
<li>收起后文章居中，点右侧空白或边缘的 <code>‹</code> 再唤出，唤出时把文章往左推。</li>
</ul>
<p>推开这个动作是有意的：它让「目录」和「文章」成为两个独立的东西，而不是一列固定的装饰。</p>
<h2 id="可读性把感觉能读变成数字">可读性：把「感觉能读」变成数字</h2>
<p>玻璃和纸两套材质、亮暗两个模式，四种组合。我一直以为都没问题，直到真的去量。</p>
<p>方法是：同一页在四种组合下各截两张图，一张正常、一张把所有文字设为透明，然后按元素矩形在「只有背景」的那张上取真实像素，算 WCAG 对比度。</p>
<p>量出来的问题都很朴素：</p>
<ul>
<li>Hero 名片的材质层被一条<strong>后代选择器</strong>误关，白字直接压在亮照片上（3.15:1）；</li>
<li>纸模式的 Hero 只有浅色 veil，白字在没有暗角的照片上（1.5:1）；</li>
<li>导航当前项、目录当前项的底色写死成浅色，暗色模式下变成「浅底浅字」（1.5–1.8:1）。</li>
</ul>
<p>修完再量，四种组合的最低值都在 4.6:1 以上。方法记在 <code>DESIGN.md</code>，复查时直接照做。</p>
<p>这件事的教训不是「要测对比度」，而是：<strong>写死的颜色终会在某个模式里翻车</strong>，而模式的数量总是比你以为的多一个。</p>
<h2 id="取舍">取舍</h2>
<p>有几件事最后没做，理由值得记下来。</p>
<p><strong>没有上 <code>liquid-glass-react</code>。</strong> 装了、接了、也真跑通了，但它靠 <code>feImage</code> 生成位移图，而 Chrome 的 <code>backdrop-filter</code> 不执行滤镜里的 <code>feImage</code>——核心效果在主浏览器上是空操作。打出来的包 41 KB gzip，静止状态又几乎没有可见材质。最后只取了它的高光层和饱和度配方。</p>
<p><strong>构建依赖不再坚持为零。</strong> 「零依赖」曾经是个卖点，但它换来的是我自己维护一个 Markdown 解析器。现在构建期依赖 markdown-it，产物仍然是纯静态文件——<strong>运行时不依赖任何库</strong>，这条线才是真的重要。</p>
<p><strong>纸材质冻结。</strong> 两套材质都持续迭代的结果是两边都半成品。定下「纸只修 bug」之后，玻璃反而推进得更快。</p>
<h2 id="现状">现状</h2>
<ul>
<li>12 篇文章，30 个页面，构建约 0.1 秒；</li>
<li>首屏 JS 3.9 KB（gzip），CSS 17.4 KB，光学引擎 10.3 KB 按需加载；</li>
<li>同屏最多两扇真折射窗口；</li>
<li>四种「材质 × 明暗」组合，正文对比度 ≥ 4.6:1。</li>
</ul>
<p>这个站现在大概到了「想改哪里都知道该动哪个文件」的状态。对我来说，这比任何一项具体的效果都更重要。</p>
]]></content:encoded>
    </item>
    <item>
      <title>在浏览器里做「真折射」</title>
      <link>https://blog.silvershaded.top/posts/true-refraction/</link>
      <guid isPermaLink="true">https://blog.silvershaded.top/posts/true-refraction/</guid>
      <pubDate>Tue, 03 Nov 2026 00:00:00 GMT</pubDate>
      <description>backdrop-filter 的十个函数没有一个能位移像素。想在网页上做真的折射，只能把壁纸复制一层、用 feDisplacementMap 去推它——而且只在固定背景上成立。</description>
      <content:encoded><![CDATA[<p><code>backdrop-filter</code> 很能干：模糊、亮度、对比度、饱和度、色相……但你把它支持的函数表从头看到尾，会发现<strong>没有一个能位移像素</strong>。</p>
<p>位移不了，就折射不了。这是浏览器给玻璃效果划的硬边界。</p>
<h2 id="那就自己造一层">那就自己造一层</h2>
<p>既然背景是固定的，我可以把它<strong>复制</strong>到窗口里，然后对这份副本做位移：</p>
<pre><code class="language-css">.wallpaper,
[data-surface=&quot;glass&quot;] .window__backdrop {
  background-image: var(--wallpaper-img), var(--wallpaper-image);
  background-size: cover, auto;
  background-position: center, center;
  background-attachment: fixed, fixed;
}
</code></pre>
<p>关键在于 <code>background-attachment: fixed</code>：它让背景的定位基准变成<strong>视口</strong>，于是窗口里的副本和窗口背后的壁纸像素级对齐。</p>
<p>对齐之后再加位移：</p>
<pre><code class="language-xml">&lt;filter id=&quot;lg-refract&quot; x=&quot;-10%&quot; y=&quot;-10%&quot; width=&quot;120%&quot; height=&quot;120%&quot;&gt;
  &lt;feTurbulence type=&quot;fractalNoise&quot; baseFrequency=&quot;0.009 0.017&quot; numOctaves=&quot;2&quot; seed=&quot;11&quot;/&gt;
  &lt;feGaussianBlur in=&quot;noise&quot; stdDeviation=&quot;1.1&quot; result=&quot;field&quot;/&gt;
  &lt;feDisplacementMap in=&quot;SourceGraphic&quot; in2=&quot;field&quot; scale=&quot;18&quot;
                     xChannelSelector=&quot;R&quot; yChannelSelector=&quot;G&quot;/&gt;
  &lt;feGaussianBlur in=&quot;displaced&quot; stdDeviation=&quot;7&quot;/&gt;
&lt;/filter&gt;
</code></pre>
<p><code>feTurbulence</code> 生成一张平滑的噪声场，<code>feDisplacementMap</code> 用它把每个像素推一点点，最后补一次模糊——玻璃的「毛」和「歪」就都在了。</p>
<h2 id="边界必须写死">边界必须写死</h2>
<p>这套做法只在三种条件下成立，缺一个就会露馅：</p>
<ol>
<li><strong>背景必须是固定的。</strong> 壁纸贴了 <code>position: fixed</code> 才谈得上「复制一份并对齐」。滚动内容不行。</li>
<li><strong>窗口必须在壁纸之上、且不压别的内容。</strong> 副本里只有壁纸，压着图片或文字就会穿帮。</li>
<li><strong>位移场不能动。</strong> <code>feTurbulence</code> 是 CPU 渲染的，每帧重算等于自杀。</li>
</ol>
<p>所以我的实现里只有首页 Hero 和置顶卡用真折射，其余窗口用「同色层 + 高光边」——远看是玻璃，滚动不掉帧。</p>
<h2 id="滚动时降级">滚动时降级</h2>
<p>这是设计里最舒服的一处取舍：</p>
<pre><code class="language-css">[data-surface=&quot;glass&quot;].is-scrolling .window[data-mode=&quot;refract&quot;] .window__backdrop {
  filter: blur(26px) saturate(150%);
}
</code></pre>
<p>滚动中，位移场换成纯模糊（便宜），停手 140ms 后再切回折射。</p>
<p>视觉上像是玻璃「重新对焦」——而实际上，是浏览器替我做了它最擅长的事。</p>
<h2 id="反射交给-worker">反射交给 Worker</h2>
<p>折射是静态的，反射不是：光斑要跟着指针跑，还要随速度变亮。</p>
<p>这部分数学不重，但我还是塞进了 Web Worker：</p>
<ul>
<li>主线程只做一件事：<code>pointermove</code> 时把坐标发给 Worker；</li>
<li>Worker 算出每扇窗的光斑位置（<code>--sx/--sy</code>）与强度（<code>--li</code>）；</li>
<li>主线程收到结果，写三个 CSS 变量。</li>
</ul>
<pre><code class="language-js">el.style.setProperty('--sx', data[i * 3].toFixed(1) + '%');
el.style.setProperty('--sy', data[i * 3 + 1].toFixed(1) + '%');
el.style.setProperty('--li', data[i * 3 + 2].toFixed(3));
</code></pre>
<p>这样滚动、移动指针时，主线程上没有任何逐帧计算——只有三次赋值。</p>
<div class="callout callout--tip"><p class="callout__title">想验证真的在折射</p><div class="callout__body"><p>打开首页，滚动一下让 Hero 变成窗口，然后看窗口边缘：壁纸的斜向光带会在窗内被「推歪」一点点。那就是位移场在干活。</p>
</div></div><h2 id="一句话总结">一句话总结</h2>
<p>网页上的「真折射」不是魔法，是<strong>复制 + 对齐 + 位移</strong>三件事，前提是你得接受它只对固定背景成立。</p>
<p>接受这个前提之后，剩下的都是工程问题。<sup class="fn-ref"><a href="#fn1" id="fnref1">[1]</a></sup></p>
<section class="footnotes"><h2 class="footnotes__title">脚注</h2>
<ol class="footnotes-list">
<li id="fn1" class="footnote-item"><p>顺带一提：<code>background-attachment: fixed</code> 在 iOS Safari 上有历史遗留的定位差异，所以我给移动端直接关掉了折射，只留模糊——反正屏幕小，看不出区别。 <a href="#fnref1" class="footnote-backref">↩︎</a></p>
</li>
</ol>
</section>
]]></content:encoded>
    </item>
    <item>
      <title>把博客推倒重来</title>
      <link>https://blog.silvershaded.top/posts/hello-world/</link>
      <guid isPermaLink="true">https://blog.silvershaded.top/posts/hello-world/</guid>
      <pubDate>Mon, 02 Nov 2026 00:00:00 GMT</pubDate>
      <description>第五次重做个人主页。这一次不换框架，换材质——从一张暖纸，换成一块会反光的玻璃。</description>
      <content:encoded><![CDATA[<p>这大概是我第五次重做这个站。</p>
<p>前面四次分别死在：主题太花、构建太慢、样式无从下手、以及最经典的——改完首页之后，文章页还是旧样子，像两个网站拼在一起。</p>
<p>这一次我给自己立了一条规矩：<strong>先把&quot;美工&quot;拆成可独立替换的积木，再动第一行代码。</strong></p>
<h2 id="拆成五块之后事情变得简单了">拆成五块之后，事情变得简单了</h2>
<div class="table-wrap"><table>
<thead>
<tr>
<th>模块</th>
<th>职责</th>
<th>能不能单独换</th>
</tr>
</thead>
<tbody>
<tr>
<td>A 背景</td>
<td>会发光的多色渐变，给玻璃当折射源</td>
<td>能，纯 CSS 变量</td>
</tr>
<tr>
<td>B 装饰</td>
<td>柔光圆斑、光斑、气泡，唯一动起来的东西</td>
<td>能，一个 <code>div</code></td>
</tr>
<tr>
<td>C 页面</td>
<td>玻璃与纸，同一个材质的两种含水量</td>
<td>能，换一套 token</td>
</tr>
<tr>
<td>D 图章</td>
<td>朱红落款，盖章入场一次</td>
<td>能，一个 <code>span</code></td>
</tr>
<tr>
<td>E 鼠标</td>
<td>指针光斑与压按涟漪</td>
<td>能，整块删掉也不影响功能</td>
</tr>
</tbody>
</table></div>
<p>五块之间只通过 CSS 自定义属性和固定的层叠次序说话，谁也不 import 谁。于是我修改背景的时候，不需要重新理解文章页；删掉鼠标特效的时候，也不用担心导航挂掉。</p>
<blockquote>
<p>一个界面的可维护性，往往取决于&quot;改一个地方要读几个文件&quot;。</p>
</blockquote>
<h2 id="为什么最后选了玻璃">为什么最后选了玻璃</h2>
<p>老实说，一开始是因为好看。玻璃会折射背后的颜色，页面一动，光就跟着流动，那种&quot;活物感&quot;是纯色卡片给不了的。</p>
<p>但真正让我决定的是另一件事：<strong>玻璃天然自带层次</strong>。</p>
<p>头像可以压在卡片上，卡片可以压在封面上，页头可以浮在内容之上。前后景关系一出来，界面就不需要靠边框和分割线去解释了——这正是我在旧版里最缺的东西。</p>
<h2 id="玻璃的代价它必须一直是湿的">玻璃的代价：它必须一直是&quot;湿&quot;的</h2>
<p>问题在于，玻璃不适合读文章。正文后面永远有一片晃动的颜色，长段落读十分钟就想关掉。</p>
<p>所以我把同一块材质做成了两种状态：</p>
<ul>
<li><strong>入口页是玻璃</strong>：含水、透明、会反光，适合浏览和&quot;哇一下&quot;。</li>
<li><strong>文章页是纸</strong>：抽干、不透明、只剩字，适合待很久。</li>
</ul>
<p>这就是那句总纲——<strong>毛玻璃也是一种纸</strong>。不是两套风格，是同一块材料的含水量不同。</p>
<h2 id="于是有了那个按钮">于是有了那个按钮</h2>
<p>页头右上角有一枚小小的药丸按钮：💧 和 📄。</p>
<p>它不做主题切换，只做一件事：把当前页面从&quot;湿&quot;调到&quot;干&quot;，或者反过来。手选过一次之后，它会记住。</p>
<p>我知道这是个很小的功能。但当你亲手把一篇文章从玻璃里&quot;抽干&quot;、看着纸纹一层层浮上来的时候，你大概能明白我为什么愿意为它写那 300 字节。</p>
<h2 id="接下来">接下来</h2>
<p>站是新的，文章是旧的。我会慢慢把以前散在别处的东西搬过来，顺便把剩下的两块——图章和鼠标特效——再打磨一下。</p>
<p>如果哪天你觉得这里的字太小、玻璃太亮、或者动画太吵，欢迎直接告诉我。<strong>好界面是被抱怨出来的。</strong></p>
]]></content:encoded>
    </item>
    <item>
      <title>让 Markdown 和 Obsidian 对齐</title>
      <link>https://blog.silvershaded.top/posts/obsidian-markdown/</link>
      <guid isPermaLink="true">https://blog.silvershaded.top/posts/obsidian-markdown/</guid>
      <pubDate>Sun, 01 Nov 2026 00:00:00 GMT</pubDate>
      <description>目标是「把 vault 里的笔记直接拖进 content/ 就能发」，于是渲染器把 callout、脚注、内链、任务列表、表格对齐全部补齐了，还顺手收拾了中文软换行。</description>
      <content:encoded><![CDATA[<p>写作环境是 Obsidian，发布环境是一个零依赖的 Node 脚本。两边语法不一致的话，每次发文都要先检查一遍——那就本末倒置了。</p>
<p>这一轮把渲染器补齐到「复制过来就能发」。</p>
<h2 id="必须支持的清单">必须支持的清单</h2>
<div class="table-wrap"><table>
<thead>
<tr>
<th>语法</th>
<th>处理方式</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>#</code>–<code>######</code></td>
<td><code>h1</code> 降级为 <code>h2</code>，自动锚点，重复 id 加后缀</td>
</tr>
<tr>
<td><code>**粗**</code> <code>*斜*</code> <code>~~删~~</code> <code>==高亮==</code></td>
<td>与 Obsidian 一致</td>
</tr>
<tr>
<td>围栏 / 缩进代码</td>
<td>语言 class、完整转义</td>
</tr>
<tr>
<td><code>[文本](url)</code> / 裸 URL</td>
<td>外链自动加 <code>rel=&quot;noopener&quot;</code></td>
</tr>
<tr>
<td><code>![[图片.webp]]</code></td>
<td>内嵌图，读真实宽高防抖</td>
</tr>
<tr>
<td>任务列表 <code>- [ ]</code></td>
<td>真 checkbox（禁用态），完成项加删除线</td>
</tr>
<tr>
<td>引用 / 嵌套引用</td>
<td>支持引用里放列表和代码</td>
</tr>
<tr>
<td>表格 + 对齐</td>
<td><code>.table-wrap</code> 横向滚动</td>
</tr>
<tr>
<td>脚注 <code>[^1]</code></td>
<td>文末脚注区 + 双向锚点</td>
</tr>
<tr>
<td>内链 <code>[[笔记]]</code></td>
<td>渲染成斜体标记，<strong>构建时提示未解析</strong></td>
</tr>
<tr>
<td>callout <code>&gt; [!note]</code></td>
<td>按类型着色 + 图标 + 标题</td>
</tr>
<tr>
<td>``</td>
<td>整段删除</td>
</tr>
</tbody>
</table></div>
<h2 id="callout-是回报率最高的一项">callout 是回报率最高的一项</h2>
<p>以前写提示框要手搓 HTML：</p>
<pre><code class="language-html">&lt;div class=&quot;callout callout--warning&quot;&gt;…&lt;/div&gt;
</code></pre>
<p>现在只要：</p>
<pre><code class="language-markdown">&gt; [!warning] 注意
&gt; 位移场每帧重算会掉帧。
</code></pre>
<p>渲染器会认领首行的 <code>[!type]</code>，把剩下的内容当成正常的块级 Markdown 继续渲染——所以 callout 里照样能放列表、代码、链接。</p>
<div class="callout callout--warning"><p class="callout__title">未知识别</p><div class="callout__body"><p>没见过的 <code>type</code> 不会炸，会降级成普通引用块，同时打一条构建警告。</p>
</div></div><h2 id="内链诚实地提示">内链：诚实地提示</h2>
<p>静态站没有「双链索引」，所以我没打算假装有：</p>
<pre><code class="language-markdown">参考 [[搜索相似度算法·设计]] 里的阈值公式。
</code></pre>
<p>渲染成 <code>&lt;span class=&quot;wikilink&quot;&gt;</code>（斜体 + 虚线下划线），鼠标悬停显示「未解析引用」，构建时再打印一行警告。</p>
<div class="callout callout--note"><p class="callout__title">为什么不做跳转</p><div class="callout__body"><p>如果哪天真的做了笔记库，这个 span 换成 <code>&lt;a&gt;</code> 就行。现在造一个假的跳转，只会让人点进去 404。</p>
</div></div><h2 id="中文软换行">中文软换行</h2>
<p>Markdown 里一个换行不产生空格，但中文被换行拆开时如果补一个空格，就成了「中 文」。</p>
<pre><code class="language-js">const a = out.slice(-1);
const b = line[0];
out += CJK.test(a) &amp;&amp; CJK.test(b) ? line : ' ' + line;
</code></pre>
<p>两端都是中日韩字符就贴着，否则补空格。三行代码，解决了困扰我很久的排版问题。</p>
<h2 id="测试">测试</h2>
<p>渲染器现在有 152 条断言：每种语法的关键 HTML、目录编号（<code>1</code> / <code>1.1</code> / <code>1.1.1</code>）、脚注双向锚点、未解析内链告警、代码块里的 <code>&lt;script&gt;</code> 是否被转义……</p>
<p>跑一次不到 100ms，构建前顺手执行：</p>
<pre><code class="language-bash">node scripts/test-markdown.mjs
</code></pre>
<blockquote>
<p>152/152 通过，剩下的事交给写作。</p>
</blockquote>
]]></content:encoded>
    </item>
    <item>
      <title>图床：缩略图、灯箱与边缘分发</title>
      <link>https://blog.silvershaded.top/posts/gallery-on-cloudflare/</link>
      <guid isPermaLink="true">https://blog.silvershaded.top/posts/gallery-on-cloudflare/</guid>
      <pubDate>Fri, 30 Oct 2026 00:00:00 GMT</pubDate>
      <description>照片不放仓库里，放 Cloudflare 的边缘；网页端只加载缩略图，双击进画廊看原图，元信息有则显示、无则省略。</description>
      <content:encoded><![CDATA[<p>照片和壁纸是两类东西：</p>
<ul>
<li><strong>壁纸</strong>是前端的资源，参与材质渲染，必须小、必须快、必须可缓存一辈子；</li>
<li><strong>照片</strong>是内容，可能很大、会一直增加，应该交给边缘节点。</li>
</ul>
<p>所以图床单独做。</p>
<h2 id="三档尺寸">三档尺寸</h2>
<p>每张图在上传时就切成三档，构建时只负责生成索引：</p>
<pre><code class="language-text">gallery/
  index.json          # 构建产物：条目 + 尺寸 + 元信息
  thumbs/&lt;slug&gt;.webp  # 400w  列表用
  preview/&lt;slug&gt;.webp # 1200w 灯箱先显示这张
  full/&lt;slug&gt;.webp    # 1600w 灯箱随后替换成这张
</code></pre>
<p>列表页只加载 <code>thumbs</code>，并带上 <code>srcset</code>。灯箱打开时先给 <code>preview</code>，等 <code>full</code> 加载完再换——眼睛几乎察觉不到，但首屏省下几百 KB。</p>
<h2 id="双击进画廊">双击进画廊</h2>
<p>桌面端<strong>双击</strong>缩略图进入，单击不动作（防误触）；触控端没有双击，改成单击；键盘用户按 <code>Enter</code>。</p>
<p>三种入口，一个动作。</p>
<p><img src="cover-2.webp" alt="相册里的温室那张"></p>
<p>灯箱里除了大图，还有一栏信息：</p>
<div class="table-wrap"><table>
<thead>
<tr>
<th>字段</th>
<th>有就显示</th>
<th>没有就不显示</th>
</tr>
</thead>
<tbody>
<tr>
<td>标题 / 简介</td>
<td>✓</td>
<td>✓</td>
</tr>
<tr>
<td>日期 / 地点</td>
<td>✓</td>
<td>✓</td>
</tr>
<tr>
<td>相机 / 焦距</td>
<td>✓</td>
<td>✓</td>
</tr>
<tr>
<td>版权</td>
<td>✓</td>
<td>用站点默认许可</td>
</tr>
</tbody>
</table></div>
<p>这条「空则省略」的规矩很重要——不然每张插画下面都会挂着「相机：—」，很像模板没填完。</p>
<h2 id="为什么不用-worker">为什么不用 Worker</h2>
<p>图床不需要实时缩放：</p>
<ul>
<li>缩略图是<strong>构建时</strong>（或上传时）一次性生成的；</li>
<li>边缘节点只负责把文件吐出去；</li>
<li>目录由 <code>gallery/index.json</code> 描述。</li>
</ul>
<p>一旦引入实时缩放，就多了一个会冷启动、会超时、会按请求计费的东西——而它解决的问题，一张预生成的 WebP 就能解决。</p>
<div class="callout callout--tip"><p class="callout__title">部署形态</p><div class="callout__body"><p>全站依然是「只有 assets、没有 main」的 Worker：没有业务脚本，图片和 HTML 走同一套边缘缓存。</p>
</div></div><h2 id="玻璃配方保持一致">玻璃配方保持一致</h2>
<p>相册卡片也是窗口：同样的圆角、同样的高光边、同样的悬停位移。</p>
<p>灯箱是玻璃层的「高潮态」——深色遮罩 + 强模糊 + 中央大图，但配色仍然取自同一套 token（<code>--brand</code>、<code>--seal</code>、<code>--radius</code>）。所以它看起来不像外挂组件，更像这块玻璃被举到了眼前。</p>
<h2 id="剩下的问题">剩下的问题</h2>
<ul class="task-list">
<li class="task"><label><input class="task__box" checked="" disabled="" type="checkbox"> 三档尺寸 + <code>srcset</code></label></li>
<li class="task"><label><input class="task__box" checked="" disabled="" type="checkbox"> 双击/单击/Enter 三种入口</label></li>
<li class="task"><label><input class="task__box" checked="" disabled="" type="checkbox"> 焦点圈定 + Esc 关闭 + ±1 预取</label></li>
<li class="task"><label><input class="task__box" disabled="" type="checkbox"> 图片条目从文件系统自动扫出来</label></li>
<li class="task"><label><input class="task__box" disabled="" type="checkbox"> EXIF 自动读取相机与焦距</label></li>
<li class="task"><label><input class="task__box" disabled="" type="checkbox"> 画廊内的双指缩放</label></li>
</ul>
<p>前三个是工程活，最后一个（手势缩放）是产品决定——双击已经被「进入画廊」占用了，再放大就得换手势。</p>
<p>先把能确定性做完的做掉。</p>
]]></content:encoded>
    </item>
    <item>
      <title>毛玻璃也是一种纸：一次材质设计</title>
      <link>https://blog.silvershaded.top/posts/glass-is-paper/</link>
      <guid isPermaLink="true">https://blog.silvershaded.top/posts/glass-is-paper/</guid>
      <pubDate>Thu, 29 Oct 2026 00:00:00 GMT</pubDate>
      <description>Liquid Glass 用久了会冷。直到我把一层纸纹压进玻璃中央，才明白玻璃和纸不是两个阵营，而是同一个材质轴上的两端。</description>
      <content:encoded><![CDATA[<p>Liquid Glass 刚流行那阵子，我把所有卡片都换成了毛玻璃。</p>
<p>一周之后我把它改回来了。原因很简单：<strong>冷。</strong></p>
<p>玻璃折射、高光边、饱和度拉满的背景，单看每一屏都很漂亮，连着读三篇东西就像在机场广告灯箱前面站了二十分钟。</p>
<p>后来我意识到问题不在玻璃本身，而在&quot;玻璃背后什么都没有&quot;。玻璃之所以像玻璃，是因为它压着东西。</p>
<h2 id="一个念头纸是被抽干的玻璃">一个念头：纸是被抽干的玻璃</h2>
<p>某个凌晨我盯着磨砂玻璃杯看，突然想到：如果玻璃背后压着的是一张纸呢？</p>
<p>磨砂玻璃本来就是&quot;不完全透明&quot;的材料，它把背后的东西糊成一层薄薄的色块——这不正是<strong>被水浸润的纸</strong>吗？反过来，纸是抽干了水分的玻璃：不透明、不折射、但保留了纤维的肌理。</p>
<p>于是我把两者放到同一根轴上：</p>
<pre><code class="language-text">高含水量 ── 玻璃（透明·折射·水印纸纹 .04）
             │
             │  同一个材质轴
             │
低含水量 ── 纸（不透明·暖白·纸纹本体 .55）
</code></pre>
<p>这件事的好处立刻显现：入口页和文章页不再是两个网站，它们只是同一块材料的两端。</p>
<h2 id="玻璃的三要素">玻璃的三要素</h2>
<p>要让一块面板真的读作&quot;玻璃&quot;，而不是&quot;贴了层灰膜的方框&quot;，必须同时具备三样东西。</p>
<h3 id="1-顶边高光">1. 顶边高光</h3>
<pre><code class="language-css">--specular: inset 0 1.5px 0 rgba(255,255,255,.85),
            inset 0 -1px 0 rgba(255,255,255,.14);
</code></pre>
<p>只有顶边一条 1.5px 的亮线，加底边一条 1px 的微反。少一条，面板立刻变平。</p>
<h3 id="2-背景折射">2. 背景折射</h3>
<pre><code class="language-css">backdrop-filter: blur(18px) saturate(170%);
</code></pre>
<p><code>blur</code> 决定&quot;毛&quot;的程度，<code>saturate</code> 决定它吸上来多少颜色。只模糊不增饱和，玻璃会显脏。</p>
<h3 id="3-中央纸纹">3. 中央纸纹</h3>
<p>这是我自己加的那一笔。用 <code>feTurbulence</code> 生成一层噪点，铺在玻璃里，<code>opacity: .04</code>：</p>
<pre><code class="language-css">.glass::after {
  background-image: url(&quot;data:image/svg+xml,...&quot;);  /* 静态可平铺，不动画 */
  opacity: var(--grain-opacity);
  mix-blend-mode: var(--grain-mix);   /* 亮色 multiply / 暗色 screen */
}
</code></pre>
<p>它几乎看不见。但把 <code>opacity</code> 从 <code>.04</code> 调到 <code>0</code> 再对比，玻璃会立刻从&quot;压着纸的磨砂玻璃&quot;变回&quot;一块塑料&quot;。</p>
<h2 id="纸的那一半">纸的那一半</h2>
<p>纸态反而更简单：不透明度拉满，纸纹浓度从 <code>.04</code> 提到 <code>.55</code>，投影全部去掉。</p>
<pre><code class="language-css">.paper {
  background: var(--bg);
  --grain-opacity: .55;
  box-shadow: none;   /* 纸不抬离页面 */
}
</code></pre>
<p><strong>纸不投影</strong>是条硬规矩。加了阴影的纸就是卡片，不是纸。</p>
<h2 id="两条性能红线">两条性能红线</h2>
<p>材质用得爽，但有两件事千万别做：</p>
<ol>
<li><strong>不动画 <code>blur</code> 半径。</strong> 每一帧都要重新做一次高斯模糊，中端手机上直接掉帧。</li>
<li><strong>不动画纸纹。</strong> <code>feTurbulence</code> 是 CPU 渲染的，动一下就是灾难。纸纹只能是静态的可平铺贴图。</li>
</ol>
<p>所以我的圆斑只用 <code>transform</code> 漂移，纸纹一次生成、永不再动。</p>
<h2 id="那个开关">那个开关</h2>
<p>材质轴需要一个显式开关，不然用户永远不知道&quot;纸态&quot;存在。</p>
<p>页头那枚 💧 / 📄 的药丸做的就是这件事：<code>&lt;html data-surface=&quot;glass|paper&quot;&gt;</code>，三值同拍过渡——<code>--glass-bg</code>、<code>backdrop-filter</code>、<code>--grain-opacity</code>——240ms。</p>
<p>入口页默认给你玻璃，文章页默认给你纸，但每一页你都可以亲手把它们调回去。</p>
<blockquote>
<p>毛玻璃也是一种纸。玻璃是纸的含水态，纸是玻璃的干态，我们只是在同一个材质轴上滑动。</p>
</blockquote>
<p>这句话后来被我写进了设计文档的第一行。</p>
]]></content:encoded>
    </item>
    <item>
      <title>零依赖：把 Markdown 编译成博客</title>
      <link>https://blog.silvershaded.top/posts/zero-dep-blog/</link>
      <guid isPermaLink="true">https://blog.silvershaded.top/posts/zero-dep-blog/</guid>
      <pubDate>Mon, 26 Oct 2026 00:00:00 GMT</pubDate>
      <description>不用 Hugo，不用 Astro，不上任何构建框架。一个 400 行的 Node 脚本，把 content/posts/*.md 变成一整个静态站。</description>
      <content:encoded><![CDATA[<p>我的博客以前是 Hugo 的。主题很漂亮，构建很快，但每次想改一处布局，都要先在 Go template 的 <code>{{ }}</code> 里绕十分钟。</p>
<p>后来我把整套东西删了，只留一个文件：<code>build.mjs</code>。</p>
<h2 id="需要的其实只有三件事">需要的其实只有三件事</h2>
<p>一个静态博客生成器要做的事，拆开看只有三件：</p>
<ol>
<li>读 frontmatter，拿到标题、日期、标签</li>
<li>把 Markdown 变成 HTML</li>
<li>套上模板，写到 <code>dist/</code></li>
</ol>
<p>Node 内置的 <code>fs</code> 和 <code>path</code> 就够了。不需要 remark，不需要 unified，不需要插件生态——因为<strong>我写的 Markdown 是我自己写的</strong>，我不需要兼容整个 CommonMark。</p>
<h2 id="markdown-子集就够用了">Markdown 子集就够用了</h2>
<p>我实现的是这样一个子集：</p>
<ul>
<li>标题、段落、粗体斜体、行内代码</li>
<li>围栏代码块（带语言 class）</li>
<li>有序/无序列表、引用、分割线</li>
<li>表格、图片、链接</li>
<li>任务列表</li>
</ul>
<p>加起来不到 200 行。剩下的都是我不写的语法，所以它永远不会因为&quot;不支持&quot;而咬我。</p>
<p>有个小细节值得一提：<strong>中文软换行</strong>。</p>
<pre><code class="language-js">const CJK = /[\u2e80-\u9fff\u3000-\u303f\uff00-\uffef]/;

function joinSoft(text) {
  const lines = text.split('\n').map(l =&gt; l.trim()).filter(Boolean);
  let out = '';
  for (const l of lines) {
    if (!out) { out = l; continue; }
    const a = out.slice(-1), b = l[0];
    out += CJK.test(a) &amp;&amp; CJK.test(b) ? l : ' ' + l;
  }
  return out;
}
</code></pre>
<p>Markdown 里换行不产生空格，但中文被换行拆开时如果补一个空格，就成了&quot;中 文&quot;。所以判断两端是不是中日韩字符，是就贴着，不是就补空格。</p>
<h2 id="图片尺寸在构建时补上">图片尺寸在构建时补上</h2>
<p>CLS 最容易被图片搞坏。但我又不想在每篇文章里手写 <code>width</code> / <code>height</code>。</p>
<p>解法很简单：构建时读一遍本地图片的头部，把真实尺寸塞进 <code>&lt;img&gt;</code>。</p>
<pre><code class="language-js">// WebP 的三种容器格式，尺寸藏在不同的位置
if (fmt === 'VP8X') return { w: 1 + b.readUIntLE(24, 3), h: 1 + b.readUIntLE(27, 3) };
if (fmt === 'VP8L') { const v = b.readUInt32LE(21); return { w: (v &amp; 0x3fff) + 1, h: ((v &gt;&gt; 14) &amp; 0x3fff) + 1 }; }
</code></pre>
<p>写完之后，<code>![封面](cover-1.webp)</code> 出来就是 <code>&lt;img width=&quot;1200&quot; height=&quot;675&quot; loading=&quot;lazy&quot;&gt;</code>，布局一次都不会跳。</p>
<h2 id="用断言代替肉眼">用断言代替肉眼</h2>
<p>发布之前跑一遍 <code>node scripts/verify.mjs</code>，它检查：</p>
<div class="table-wrap"><table>
<thead>
<tr>
<th>检查</th>
<th>为什么</th>
</tr>
</thead>
<tbody>
<tr>
<td>每页只有一个 <code>h1</code></td>
<td>结构正确性</td>
</tr>
<tr>
<td>每个链接都能落到文件</td>
<td>死链是静态站最常见的病</td>
</tr>
<tr>
<td>没有残留的 <code>**</code>、<code>#</code> 标题或三反引号围栏</td>
<td>渲染漏了会很难看</td>
</tr>
<tr>
<td><code>feed.xml</code> 的链接都在站点域名下</td>
<td>防止站点搬家后 RSS 失效</td>
</tr>
<tr>
<td>JS 总体积 ≤ 4KB（gzip）</td>
<td>性能预算</td>
</tr>
</tbody>
</table></div>
<p>死链检查最值钱。有一次我把 <code>posts/</code> 改成 <code>post/</code>，本地看着一切正常，校验脚本直接报出 17 条死链。</p>
<h2 id="代价是什么">代价是什么</h2>
<p>说点实话。自己写生成器的代价是：</p>
<ul>
<li>没有热更新，改完要重跑 <code>node build.mjs</code></li>
<li>语法支持靠自己加，想用脚注就得自己写</li>
<li>没有插件生态，搜索、评论、图床都得自己接</li>
</ul>
<p>但收益也很直接：<strong>整站只有两种语言，HTML 和 CSS</strong>。任何一处不满意，我都能在十分钟内改到满意，不用查文档，不用跟抽象层搏斗。</p>
<blockquote>
<p>工具的价值不在于它能做多少，而在于它不挡路。</p>
</blockquote>
<h2 id="现在这个站的构建时间">现在这个站的构建时间</h2>
<pre><code>$ time node build.mjs
✓ 生成 5 篇文章 · 6 个标签 · 21 条 sitemap -&gt; dist/

real    0m0.09s
</code></pre>
<p>九十毫秒。改完一篇文章，敲一个命令，刷新浏览器就看到了。</p>
<p>这大概就是我愿意继续手写它的原因。</p>
]]></content:encoded>
    </item>
    <item>
      <title>把静态站丢上 Cloudflare</title>
      <link>https://blog.silvershaded.top/posts/cloudflare-static/</link>
      <guid isPermaLink="true">https://blog.silvershaded.top/posts/cloudflare-static/</guid>
      <pubDate>Thu, 22 Oct 2026 00:00:00 GMT</pubDate>
      <description>一个只有 assets、没有 main 的 Worker：没有请求计费，没有冷启动，没有运行时。顺便记几个每年都会忘的坑。</description>
      <content:encoded><![CDATA[<p>静态站部署这件事，我折腾过 Vercel、Netlify、GitHub Pages，最后落在 Cloudflare。</p>
<p>不是因为它最快——虽然它确实很快——而是因为它的部署模型足够无聊：<strong>一个只有 <code>assets</code>、没有 <code>main</code> 的 Worker</strong>。</p>
<p>没有脚本在跑，就没有冷启动，没有请求计费，没有&quot;函数执行超时&quot;。你的 <code>dist/</code> 被推到边缘节点上，然后 CDN 直接吐文件。</p>
<h2 id="配置短得离谱">配置短得离谱</h2>
<pre><code class="language-jsonc">{
  &quot;name&quot;: &quot;dream-blog&quot;,
  &quot;compatibility_date&quot;: &quot;2026-04-01&quot;,
  &quot;assets&quot;: {
    &quot;directory&quot;: &quot;./dist&quot;,
    &quot;html_handling&quot;: &quot;auto-trailing-slash&quot;,
    &quot;not_found_handling&quot;: &quot;404-page&quot;
  }
}
</code></pre>
<p>三个字段值得解释：</p>
<ul>
<li><code>html_handling: auto-trailing-slash</code>：让 <code>/posts/hello-world</code> 和 <code>/posts/hello-world/</code> 指向同一个文件，不产生跳转链。</li>
<li><code>not_found_handling: 404-page</code>：用你自己的 <code>dist/404.html</code>，而不是 Cloudflare 默认那页。</li>
<li>没有 <code>main</code> 字段：这就是它&quot;只是个静态站&quot;的证据。</li>
</ul>
<h2 id="一键部署">一键部署</h2>
<pre><code class="language-bash">npm run deploy
</code></pre>
<p>脚本做六件事：</p>
<pre><code class="language-text">1/6 环境检查   node 版本、wrangler 是否可用
2/6 构建       node build.mjs
3/6 产物校验   死链、h1、占位符、RSS、配置一致性
4/6 登录状态   wrangler whoami，没登录就停下
5/6 部署       dry-run 预检 -&gt; wrangler deploy
6/6 线上自检   curl 五条关键路径
</code></pre>
<p>第 3 步是关键。<strong>永远不要部署没校验过的产物</strong>——线上排错的成本是本地的一百倍。</p>
<h2 id="三个每年都会忘的坑">三个每年都会忘的坑</h2>
<h3 id="1-workersdev-子域要先注册">1. workers.dev 子域要先注册</h3>
<p>第一次部署大概率会报：</p>
<pre><code class="language-text">You need to register a workers.dev subdomain before publishing
</code></pre>
<p>这不是权限问题，是账号还没在 Workers &amp; Pages 里走完 onboarding。去控制台点一次就行，全局唯一、全账号共用。</p>
<h3 id="2-headers-不生效往往是因为缩进">2. <code>_headers</code> 不生效往往是因为缩进</h3>
<p>Cloudflare 的 <code>_headers</code> 文件对格式很敏感——每条规则下面的响应头必须<strong>缩进两个空格</strong>，规则之间空一行：</p>
<pre><code class="language-text">/*
  X-Content-Type-Options: nosniff
  Referrer-Policy: strict-origin-when-cross-origin

/images/*
  Cache-Control: public, max-age=31536000, immutable
</code></pre>
<p>不缩进的话整段会被静默忽略，你会在三个月后才发现图片没有缓存。</p>
<h3 id="3-没有浏览器的机器用-token">3. 没有浏览器的机器用 Token</h3>
<p><code>wrangler login</code> 需要开浏览器。CI 里只能：</p>
<pre><code class="language-bash">export CLOUDFLARE_API_TOKEN=你的token
</code></pre>
<p>Token 权限要包含 <code>Workers Scripts:Edit</code>、<code>Account Settings:Read</code>、<code>User Details:Read</code>、<code>Zone:Read</code>。少了哪一项，报错信息都不会直说是权限问题。</p>
<h2 id="顺手加的几个头">顺手加的几个头</h2>
<pre><code class="language-text">X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
X-Frame-Options: SAMEORIGIN
Permissions-Policy: geolocation=(), microphone=(), camera=()
</code></pre>
<p>成本为零，但能挡掉一批低级的扫描告警。</p>
<h2 id="为什么不直接-github-pages">为什么不直接 GitHub Pages</h2>
<p>GitHub Pages 也很好，尤其是配合 Actions 的自动构建。区别在于：</p>
<ul>
<li>Cloudflare 的免费额度对静态资源几乎无感，Pages 有 100GB/月的软限制</li>
<li>Cloudflare 的 <code>_headers</code> 能精细控制缓存与安全头，Pages 只能靠 <code>_headers</code> 支持的部分</li>
<li>我的域名解析本来就在 Cloudflare</li>
</ul>
<p>如果你的解析不在 Cloudflare，多出来的那一步 DNS 迁移可能就不值了。<strong>部署方案要跟着域名走，不是反过来。</strong></p>
]]></content:encoded>
    </item>
    <item>
      <title>慢下来的六个月</title>
      <link>https://blog.silvershaded.top/posts/slow-reading/</link>
      <guid isPermaLink="true">https://blog.silvershaded.top/posts/slow-reading/</guid>
      <pubDate>Sun, 18 Oct 2026 00:00:00 GMT</pubDate>
      <description>关掉大部分推送之后，我发现一天里其实有很多个可以发呆的缝。这篇是碎念，没有结论。</description>
      <content:encoded><![CDATA[<p>六月的时候我把手机上的推送关了，只留电话和日历。</p>
<p>原因很丢人：我在写一段 CSS 的时候，被一条&quot;您的订单已发货&quot;打断，然后顺手刷了四十分钟。等回过神来，<code>box-shadow</code> 的第三个值还是空的。</p>
<h2 id="第一个月很难受">第一个月很难受</h2>
<p>不是想刷手机的那种难受，是<strong>手不知道往哪放</strong>。</p>
<p>排队的时候、等构建的时候、电梯里那二十秒——这些缝以前都被填满了，现在空着，空得让人心慌。</p>
<p>后来我发现自己开始看东西了。楼下修车摊的大爷用一把改锥就能把链条装回去；地铁站的天花板有一块瓷砖颜色不一样；便利店的关东煮汤底在下午三点会换一次。</p>
<p>这些事没有任何用。但记住它们之后，我写的东西好像变得具体了一点。</p>
<h2 id="关掉订阅号之后">关掉订阅号之后</h2>
<p>我退订了 70 多个公众号，取消关注了 200 多个技术博主。</p>
<p>留下来的只有三类：</p>
<ol>
<li><strong>写代码写到我用得上的人</strong>——他们的文章我会真的去跑一遍</li>
<li><strong>写生活写到我不敢写的人</strong>——他们的句子让我知道中文还能这么用</li>
<li><strong>我现实里认识的人</strong>——哪怕他们一年只发一条</li>
</ol>
<p>其余的都很好，但跟我没关系。<strong>资讯的边际收益下降得非常快</strong>，而它的边际成本是一条命。</p>
<h2 id="一些具体的变化">一些具体的变化</h2>
<div class="table-wrap"><table>
<thead>
<tr>
<th>以前</th>
<th>现在</th>
</tr>
</thead>
<tbody>
<tr>
<td>一天开 40 次 GitHub</td>
<td>一天开 4 次</td>
</tr>
<tr>
<td>睡前刷到两点</td>
<td>睡前读 20 页纸书</td>
</tr>
<tr>
<td>想做的事列了 30 条</td>
<td>列了 3 条</td>
</tr>
<tr>
<td>看到新框架就焦虑</td>
<td>看到新框架先问&quot;我要解决什么问题&quot;</td>
</tr>
</tbody>
</table></div>
<p>第三行影响最大，但第五行最有用。</p>
<h2 id="一个不成熟的结论">一个不成熟的结论</h2>
<p>我不觉得&quot;少&quot;本身是美德。少只是让你有力气去做那一两件事。</p>
<p>这半年我做完的东西其实比前一年多：这个站、两个小工具、一本读了三个月才读完的书。数量上没什么可骄傲的，但它们都还活着。</p>
<blockquote>
<p>时间不是被夺走的，是被切碎的。切碎之后，每一块都小到不值得开始。</p>
</blockquote>
<p>写到这里，窗外在修路，冲击钻停了。我也该回去把那个 <code>box-shadow</code> 补完了。</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
