<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Xuan</title><link>https://thunsis.github.io/</link><description>Recent content on Xuan</description><generator>Hugo</generator><language>zh-CN</language><lastBuildDate>Mon, 06 Jul 2026 19:50:00 +0800</lastBuildDate><atom:link href="https://thunsis.github.io/index.xml" rel="self" type="application/rss+xml"/><item><title>集成 Excalidraw：手绘风格白板的渲染引擎与数据架构</title><link>https://thunsis.github.io/blog/excalidraw-intro/</link><pubDate>Mon, 06 Jul 2026 19:50:00 +0800</pubDate><guid>https://thunsis.github.io/blog/excalidraw-intro/</guid><description>&lt;p&gt;&lt;em&gt;从渲染管线到数据序列化，拆解 Excalidraw 手绘白板的技术架构&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://thunsis.github.io/images/excalidraw-intro-cover.png" alt="Excalidraw 手绘白板"&gt;&lt;/p&gt;
&lt;h2 id="excalidraw-是什么"&gt;Excalidraw 是什么&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://excalidraw.com"&gt;Excalidraw&lt;/a&gt; 是一个开源的手绘风格虚拟白板。表面上它是一个绘图工具，但真正有趣的是它背后的&lt;strong&gt;渲染引擎和数据结构设计&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;这篇文章不聊「怎么用 Excalidraw」，而是拆解它&lt;strong&gt;怎么做到的&lt;/strong&gt;。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="一roughjs手绘效果的数学内核"&gt;一、Rough.js：手绘效果的数学内核&lt;/h2&gt;
&lt;p&gt;Excalidraw 的手绘效果并非滤镜或者纹理贴图，而是来自一个轻量级 JavaScript 库——&lt;strong&gt;&lt;a href="https://roughjs.com/"&gt;rough.js&lt;/a&gt;&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id="11-粗犷渲染的算法原理"&gt;1.1 粗犷渲染的算法原理&lt;/h3&gt;
&lt;p&gt;一个标准的 &lt;code&gt;rect(100, 100, 200, 150)&lt;/code&gt; 在 SVG 中是完美笔直的四个边。而 rough.js 做的事情是：&lt;strong&gt;在渲染路径之前，主动注入随机扰动&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;核心算法可以简化为：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="background-color:#f8f8f8;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-javascript" data-lang="javascript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#204a87;font-weight:bold"&gt;function&lt;/span&gt; &lt;span style="color:#000"&gt;drawRoughRect&lt;/span&gt;&lt;span style="color:#000;font-weight:bold"&gt;(&lt;/span&gt;&lt;span style="color:#000"&gt;x&lt;/span&gt;&lt;span style="color:#000;font-weight:bold"&gt;,&lt;/span&gt; &lt;span style="color:#000"&gt;y&lt;/span&gt;&lt;span style="color:#000;font-weight:bold"&gt;,&lt;/span&gt; &lt;span style="color:#000"&gt;w&lt;/span&gt;&lt;span style="color:#000;font-weight:bold"&gt;,&lt;/span&gt; &lt;span style="color:#000"&gt;h&lt;/span&gt;&lt;span style="color:#000;font-weight:bold"&gt;)&lt;/span&gt; &lt;span style="color:#000;font-weight:bold"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#8f5902;font-style:italic"&gt;// 在四个角的坐标上施加随机偏移
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#204a87;font-weight:bold"&gt;const&lt;/span&gt; &lt;span style="color:#000"&gt;perturbation&lt;/span&gt; &lt;span style="color:#ce5c00;font-weight:bold"&gt;=&lt;/span&gt; &lt;span style="color:#0000cf;font-weight:bold"&gt;2&lt;/span&gt;&lt;span style="color:#000;font-weight:bold"&gt;;&lt;/span&gt; &lt;span style="color:#8f5902;font-style:italic"&gt;// 扰动幅度（像素）
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#204a87;font-weight:bold"&gt;const&lt;/span&gt; &lt;span style="color:#000"&gt;corners&lt;/span&gt; &lt;span style="color:#ce5c00;font-weight:bold"&gt;=&lt;/span&gt; &lt;span style="color:#000;font-weight:bold"&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#000;font-weight:bold"&gt;{&lt;/span&gt; &lt;span style="color:#000"&gt;x&lt;/span&gt;&lt;span style="color:#ce5c00;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#000"&gt;x&lt;/span&gt; &lt;span style="color:#ce5c00;font-weight:bold"&gt;+&lt;/span&gt; &lt;span style="color:#000"&gt;random&lt;/span&gt;&lt;span style="color:#000;font-weight:bold"&gt;(&lt;/span&gt;&lt;span style="color:#ce5c00;font-weight:bold"&gt;-&lt;/span&gt;&lt;span style="color:#000"&gt;perturbation&lt;/span&gt;&lt;span style="color:#000;font-weight:bold"&gt;,&lt;/span&gt; &lt;span style="color:#000"&gt;perturbation&lt;/span&gt;&lt;span style="color:#000;font-weight:bold"&gt;),&lt;/span&gt; &lt;span style="color:#000"&gt;y&lt;/span&gt;&lt;span style="color:#ce5c00;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#000"&gt;y&lt;/span&gt; &lt;span style="color:#ce5c00;font-weight:bold"&gt;+&lt;/span&gt; &lt;span style="color:#000"&gt;random&lt;/span&gt;&lt;span style="color:#000;font-weight:bold"&gt;(&lt;/span&gt;&lt;span style="color:#ce5c00;font-weight:bold"&gt;-&lt;/span&gt;&lt;span style="color:#000"&gt;perturbation&lt;/span&gt;&lt;span style="color:#000;font-weight:bold"&gt;,&lt;/span&gt; &lt;span style="color:#000"&gt;perturbation&lt;/span&gt;&lt;span style="color:#000;font-weight:bold"&gt;)&lt;/span&gt; &lt;span style="color:#000;font-weight:bold"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#000;font-weight:bold"&gt;{&lt;/span&gt; &lt;span style="color:#000"&gt;x&lt;/span&gt;&lt;span style="color:#ce5c00;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#000"&gt;x&lt;/span&gt; &lt;span style="color:#ce5c00;font-weight:bold"&gt;+&lt;/span&gt; &lt;span style="color:#000"&gt;w&lt;/span&gt; &lt;span style="color:#ce5c00;font-weight:bold"&gt;+&lt;/span&gt; &lt;span style="color:#000"&gt;random&lt;/span&gt;&lt;span style="color:#000;font-weight:bold"&gt;(&lt;/span&gt;&lt;span style="color:#ce5c00;font-weight:bold"&gt;-&lt;/span&gt;&lt;span style="color:#000"&gt;perturbation&lt;/span&gt;&lt;span style="color:#000;font-weight:bold"&gt;,&lt;/span&gt; &lt;span style="color:#000"&gt;perturbation&lt;/span&gt;&lt;span style="color:#000;font-weight:bold"&gt;),&lt;/span&gt; &lt;span style="color:#000"&gt;y&lt;/span&gt;&lt;span style="color:#ce5c00;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#000"&gt;y&lt;/span&gt; &lt;span style="color:#ce5c00;font-weight:bold"&gt;+&lt;/span&gt; &lt;span style="color:#000"&gt;random&lt;/span&gt;&lt;span style="color:#000;font-weight:bold"&gt;(&lt;/span&gt;&lt;span style="color:#ce5c00;font-weight:bold"&gt;-&lt;/span&gt;&lt;span style="color:#000"&gt;perturbation&lt;/span&gt;&lt;span style="color:#000;font-weight:bold"&gt;,&lt;/span&gt; &lt;span style="color:#000"&gt;perturbation&lt;/span&gt;&lt;span style="color:#000;font-weight:bold"&gt;)&lt;/span&gt; &lt;span style="color:#000;font-weight:bold"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#000;font-weight:bold"&gt;{&lt;/span&gt; &lt;span style="color:#000"&gt;x&lt;/span&gt;&lt;span style="color:#ce5c00;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#000"&gt;x&lt;/span&gt; &lt;span style="color:#ce5c00;font-weight:bold"&gt;+&lt;/span&gt; &lt;span style="color:#000"&gt;w&lt;/span&gt; &lt;span style="color:#ce5c00;font-weight:bold"&gt;+&lt;/span&gt; &lt;span style="color:#000"&gt;random&lt;/span&gt;&lt;span style="color:#000;font-weight:bold"&gt;(&lt;/span&gt;&lt;span style="color:#ce5c00;font-weight:bold"&gt;-&lt;/span&gt;&lt;span style="color:#000"&gt;perturbation&lt;/span&gt;&lt;span style="color:#000;font-weight:bold"&gt;,&lt;/span&gt; &lt;span style="color:#000"&gt;perturbation&lt;/span&gt;&lt;span style="color:#000;font-weight:bold"&gt;),&lt;/span&gt; &lt;span style="color:#000"&gt;y&lt;/span&gt;&lt;span style="color:#ce5c00;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#000"&gt;y&lt;/span&gt; &lt;span style="color:#ce5c00;font-weight:bold"&gt;+&lt;/span&gt; &lt;span style="color:#000"&gt;h&lt;/span&gt; &lt;span style="color:#ce5c00;font-weight:bold"&gt;+&lt;/span&gt; &lt;span style="color:#000"&gt;random&lt;/span&gt;&lt;span style="color:#000;font-weight:bold"&gt;(&lt;/span&gt;&lt;span style="color:#ce5c00;font-weight:bold"&gt;-&lt;/span&gt;&lt;span style="color:#000"&gt;perturbation&lt;/span&gt;&lt;span style="color:#000;font-weight:bold"&gt;,&lt;/span&gt; &lt;span style="color:#000"&gt;perturbation&lt;/span&gt;&lt;span style="color:#000;font-weight:bold"&gt;)&lt;/span&gt; &lt;span style="color:#000;font-weight:bold"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#000;font-weight:bold"&gt;{&lt;/span&gt; &lt;span style="color:#000"&gt;x&lt;/span&gt;&lt;span style="color:#ce5c00;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#000"&gt;x&lt;/span&gt; &lt;span style="color:#ce5c00;font-weight:bold"&gt;+&lt;/span&gt; &lt;span style="color:#000"&gt;random&lt;/span&gt;&lt;span style="color:#000;font-weight:bold"&gt;(&lt;/span&gt;&lt;span style="color:#ce5c00;font-weight:bold"&gt;-&lt;/span&gt;&lt;span style="color:#000"&gt;perturbation&lt;/span&gt;&lt;span style="color:#000;font-weight:bold"&gt;,&lt;/span&gt; &lt;span style="color:#000"&gt;perturbation&lt;/span&gt;&lt;span style="color:#000;font-weight:bold"&gt;),&lt;/span&gt; &lt;span style="color:#000"&gt;y&lt;/span&gt;&lt;span style="color:#ce5c00;font-weight:bold"&gt;:&lt;/span&gt; &lt;span style="color:#000"&gt;y&lt;/span&gt; &lt;span style="color:#ce5c00;font-weight:bold"&gt;+&lt;/span&gt; &lt;span style="color:#000"&gt;h&lt;/span&gt; &lt;span style="color:#ce5c00;font-weight:bold"&gt;+&lt;/span&gt; &lt;span style="color:#000"&gt;random&lt;/span&gt;&lt;span style="color:#000;font-weight:bold"&gt;(&lt;/span&gt;&lt;span style="color:#ce5c00;font-weight:bold"&gt;-&lt;/span&gt;&lt;span style="color:#000"&gt;perturbation&lt;/span&gt;&lt;span style="color:#000;font-weight:bold"&gt;,&lt;/span&gt; &lt;span style="color:#000"&gt;perturbation&lt;/span&gt;&lt;span style="color:#000;font-weight:bold"&gt;)&lt;/span&gt; &lt;span style="color:#000;font-weight:bold"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#000;font-weight:bold"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#8f5902;font-style:italic"&gt;// 每条边使用贝塞尔曲线链接，而不是直线
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#8f5902;font-style:italic"&gt;// 控制点也施加随机偏移，制造&amp;#34;抖动&amp;#34;效果
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#8f5902;font-style:italic"&gt;//
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#8f5902;font-style:italic"&gt;// 并且不是画一条线，而是画 2-3 条几乎重叠的线
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#8f5902;font-style:italic"&gt;// 模拟铅笔/马克笔的多次描边
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#204a87;font-weight:bold"&gt;for&lt;/span&gt; &lt;span style="color:#000;font-weight:bold"&gt;(&lt;/span&gt;&lt;span style="color:#204a87;font-weight:bold"&gt;let&lt;/span&gt; &lt;span style="color:#000"&gt;stroke&lt;/span&gt; &lt;span style="color:#ce5c00;font-weight:bold"&gt;=&lt;/span&gt; &lt;span style="color:#0000cf;font-weight:bold"&gt;0&lt;/span&gt;&lt;span style="color:#000;font-weight:bold"&gt;;&lt;/span&gt; &lt;span style="color:#000"&gt;stroke&lt;/span&gt; &lt;span style="color:#ce5c00;font-weight:bold"&gt;&amp;lt;&lt;/span&gt; &lt;span style="color:#000"&gt;strokeCount&lt;/span&gt;&lt;span style="color:#000;font-weight:bold"&gt;;&lt;/span&gt; &lt;span style="color:#000"&gt;stroke&lt;/span&gt;&lt;span style="color:#ce5c00;font-weight:bold"&gt;++&lt;/span&gt;&lt;span style="color:#000;font-weight:bold"&gt;)&lt;/span&gt; &lt;span style="color:#000;font-weight:bold"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#000"&gt;moveTo&lt;/span&gt;&lt;span style="color:#000;font-weight:bold"&gt;(&lt;/span&gt;&lt;span style="color:#000"&gt;corners&lt;/span&gt;&lt;span style="color:#000;font-weight:bold"&gt;[&lt;/span&gt;&lt;span style="color:#0000cf;font-weight:bold"&gt;0&lt;/span&gt;&lt;span style="color:#000;font-weight:bold"&gt;].&lt;/span&gt;&lt;span style="color:#000"&gt;x&lt;/span&gt; &lt;span style="color:#ce5c00;font-weight:bold"&gt;+&lt;/span&gt; &lt;span style="color:#000"&gt;random&lt;/span&gt;&lt;span style="color:#000;font-weight:bold"&gt;(),&lt;/span&gt; &lt;span style="color:#000"&gt;corners&lt;/span&gt;&lt;span style="color:#000;font-weight:bold"&gt;[&lt;/span&gt;&lt;span style="color:#0000cf;font-weight:bold"&gt;0&lt;/span&gt;&lt;span style="color:#000;font-weight:bold"&gt;].&lt;/span&gt;&lt;span style="color:#000"&gt;y&lt;/span&gt; &lt;span style="color:#ce5c00;font-weight:bold"&gt;+&lt;/span&gt; &lt;span style="color:#000"&gt;random&lt;/span&gt;&lt;span style="color:#000;font-weight:bold"&gt;());&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#000"&gt;bezierCurveTo&lt;/span&gt;&lt;span style="color:#000;font-weight:bold"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#000"&gt;corners&lt;/span&gt;&lt;span style="color:#000;font-weight:bold"&gt;[&lt;/span&gt;&lt;span style="color:#0000cf;font-weight:bold"&gt;0&lt;/span&gt;&lt;span style="color:#000;font-weight:bold"&gt;].&lt;/span&gt;&lt;span style="color:#000"&gt;x&lt;/span&gt; &lt;span style="color:#ce5c00;font-weight:bold"&gt;+&lt;/span&gt; &lt;span style="color:#000"&gt;random&lt;/span&gt;&lt;span style="color:#000;font-weight:bold"&gt;(),&lt;/span&gt; &lt;span style="color:#000"&gt;corners&lt;/span&gt;&lt;span style="color:#000;font-weight:bold"&gt;[&lt;/span&gt;&lt;span style="color:#0000cf;font-weight:bold"&gt;0&lt;/span&gt;&lt;span style="color:#000;font-weight:bold"&gt;].&lt;/span&gt;&lt;span style="color:#000"&gt;y&lt;/span&gt; &lt;span style="color:#ce5c00;font-weight:bold"&gt;+&lt;/span&gt; &lt;span style="color:#000"&gt;random&lt;/span&gt;&lt;span style="color:#000;font-weight:bold"&gt;(),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#000"&gt;corners&lt;/span&gt;&lt;span style="color:#000;font-weight:bold"&gt;[&lt;/span&gt;&lt;span style="color:#0000cf;font-weight:bold"&gt;1&lt;/span&gt;&lt;span style="color:#000;font-weight:bold"&gt;].&lt;/span&gt;&lt;span style="color:#000"&gt;x&lt;/span&gt; &lt;span style="color:#ce5c00;font-weight:bold"&gt;+&lt;/span&gt; &lt;span style="color:#000"&gt;random&lt;/span&gt;&lt;span style="color:#000;font-weight:bold"&gt;(),&lt;/span&gt; &lt;span style="color:#000"&gt;corners&lt;/span&gt;&lt;span style="color:#000;font-weight:bold"&gt;[&lt;/span&gt;&lt;span style="color:#0000cf;font-weight:bold"&gt;1&lt;/span&gt;&lt;span style="color:#000;font-weight:bold"&gt;].&lt;/span&gt;&lt;span style="color:#000"&gt;y&lt;/span&gt; &lt;span style="color:#ce5c00;font-weight:bold"&gt;+&lt;/span&gt; &lt;span style="color:#000"&gt;random&lt;/span&gt;&lt;span style="color:#000;font-weight:bold"&gt;(),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#000"&gt;corners&lt;/span&gt;&lt;span style="color:#000;font-weight:bold"&gt;[&lt;/span&gt;&lt;span style="color:#0000cf;font-weight:bold"&gt;1&lt;/span&gt;&lt;span style="color:#000;font-weight:bold"&gt;].&lt;/span&gt;&lt;span style="color:#000"&gt;x&lt;/span&gt; &lt;span style="color:#ce5c00;font-weight:bold"&gt;+&lt;/span&gt; &lt;span style="color:#000"&gt;random&lt;/span&gt;&lt;span style="color:#000;font-weight:bold"&gt;(),&lt;/span&gt; &lt;span style="color:#000"&gt;corners&lt;/span&gt;&lt;span style="color:#000;font-weight:bold"&gt;[&lt;/span&gt;&lt;span style="color:#0000cf;font-weight:bold"&gt;1&lt;/span&gt;&lt;span style="color:#000;font-weight:bold"&gt;].&lt;/span&gt;&lt;span style="color:#000"&gt;y&lt;/span&gt; &lt;span style="color:#ce5c00;font-weight:bold"&gt;+&lt;/span&gt; &lt;span style="color:#000"&gt;random&lt;/span&gt;&lt;span style="color:#000;font-weight:bold"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#000;font-weight:bold"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#8f5902;font-style:italic"&gt;// ... 四条边
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#000;font-weight:bold"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#000;font-weight:bold"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;关键参数有三个：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;参数&lt;/th&gt;
					&lt;th&gt;含义&lt;/th&gt;
					&lt;th&gt;Excalidraw 默认值&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;roughness&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;扰动幅度（越高越歪）&lt;/td&gt;
					&lt;td&gt;1-2（可调）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;bowing&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;曲线弯曲度&lt;/td&gt;
					&lt;td&gt;0.85&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;strokeWidth&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;描边宽度&lt;/td&gt;
					&lt;td&gt;1（可调）&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;设定 &lt;code&gt;roughness: 0&lt;/code&gt; 则回退到标准 SVG 精准渲染，这正是我之前踩的坑——AI 生成的 Excalidraw 风格图片没有用 rough.js 引擎，所以线条是完美笔直的，&lt;strong&gt;没有手绘质感&lt;/strong&gt;。&lt;/p&gt;</description></item><item><title>About</title><link>https://thunsis.github.io/about/</link><pubDate>Mon, 06 Jul 2026 19:00:00 +0800</pubDate><guid>https://thunsis.github.io/about/</guid><description>&lt;p&gt;Hi, I&amp;rsquo;m Xuan.&lt;/p&gt;
&lt;p&gt;A big data engineer passionate about AI, distributed systems, and building things that matter.&lt;/p&gt;</description></item><item><title>为什么 Claude Code 那么好用</title><link>https://thunsis.github.io/blog/why-claude-code/</link><pubDate>Mon, 06 Jul 2026 19:00:00 +0800</pubDate><guid>https://thunsis.github.io/blog/why-claude-code/</guid><description>&lt;p&gt;&lt;em&gt;从 agentic 系统的内部视角，拆解 Claude Code 的架构设计与技术哲学&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://thunsis.github.io/images/why-claude-code-cover.png" alt="Claude Code"&gt;&lt;/p&gt;
&lt;h2 id="引言"&gt;引言&lt;/h2&gt;
&lt;p&gt;市面上分析 Claude Code 的文章不少，但大多停留在「产品体验」层面。而我在过去几个月里，深度使用过 OpenClaw（一个类似 Claude Code 的自部署 agentic 系统），同时也看着 Cursor、Copilot、Windsurf 的发展。&lt;/p&gt;
&lt;p&gt;这篇文章我会从 &lt;strong&gt;Agentic 架构的视角&lt;/strong&gt;拆解 Claude Code 为什么技术选型走对了，以及它背后那些不显而易见的设计决策。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="一tool-use-架构-vs-补全架构"&gt;一、Tool-Use 架构 vs 补全架构&lt;/h2&gt;
&lt;p&gt;理解 Claude Code 和传统 AI 编程工具的差异，先看一张架构对比：&lt;/p&gt;
&lt;h3 id="copilot--cursor-的补全模式"&gt;Copilot / Cursor 的补全模式&lt;/h3&gt;
&lt;p&gt;&lt;img src="https://thunsis.github.io/images/why-claude-code/%E8%A1%A5%E5%85%A8%E6%9E%B6%E6%9E%84_6Gw6haPvTj3.png" alt="补全架构"&gt;&lt;/p&gt;
&lt;p&gt;核心是 &lt;strong&gt;stateless 的代码补全&lt;/strong&gt;。无论 Cursor 做得多好（检索增强、多文件上下文、Agent 模式），它的底层基因仍然是补全器。&lt;/p&gt;
&lt;h3 id="claude-code-的-tool-use-模式"&gt;Claude Code 的 Tool-Use 模式&lt;/h3&gt;
&lt;p&gt;&lt;img src="https://thunsis.github.io/images/why-claude-code/Tool-Use%E6%9E%B6%E6%9E%84_AQxkiPlDZGR.png" alt="Tool-Use 架构"&gt;&lt;/p&gt;
&lt;p&gt;核心是 &lt;strong&gt;stateful 的代理循环&lt;/strong&gt;。Claude Code 不是一个代码补全器，而是一个&lt;strong&gt;能够在终端的 Unix 环境下自主行动的代理&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;这个差异决定了几乎所有下游体验的不同。补全器只能在你当前上下文中做建议，而代理可以：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;find . -name &amp;quot;*.py&amp;quot; | xargs grep &amp;quot;deprecated_api&amp;quot;&lt;/code&gt; — 真实搜索整个项目&lt;/li&gt;
&lt;li&gt;读 A 文件、改 B 文件、测试 C 文件、提交 — 完整工作流&lt;/li&gt;
&lt;li&gt;碰到编译错误，自己读错误日志，修正代码，再试一次&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这不是渐进式的改进，是&lt;strong&gt;范式的切换&lt;/strong&gt;。&lt;/p&gt;</description></item><item><title>Sketch</title><link>https://thunsis.github.io/draw/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://thunsis.github.io/draw/</guid><description>&lt;style&gt;
.draw-container {
 width: 100%;
 height: calc(100vh - 200px);
 min-height: 600px;
 border: 2px solid #e0e0e0;
 border-radius: 8px;
 overflow: hidden;
 margin: 1em 0;
}
.draw-container iframe {
 width: 100%;
 height: 100%;
 border: none;
}
@media (max-width: 600px) {
 .draw-container {
 height: calc(100vh - 250px);
 min-height: 400px;
 }
}
&lt;/style&gt;
&lt;div class="draw-container"&gt;
 &lt;iframe src="https://excalidraw.com/" title="Excalidraw" allow="clipboard-read; clipboard-write" loading="lazy"&gt;&lt;/iframe&gt;
&lt;/div&gt;</description></item></channel></rss>