<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
  <channel>
    <title>牛牛万事屋</title>
    <link>https://zdn.codeberg.page/</link>
    <description>关于牛牛与计算机的博客</description>
    <lastBuildDate>Fri, 16 Oct 2026 16:15:22 +0800</lastBuildDate>
    <item>
      <title>WebGL 入门</title>
      <link>https://zdn.codeberg.page/public/WebGl-beginner-tutorial.html</link>
      <guid isPermaLink="true">https://zdn.codeberg.page/public/WebGl-beginner-tutorial.html</guid>
      <pubDate>Tue, 27 May 2025 00:00:00 +0800</pubDate>
      <description><![CDATA[<h1>WebGL 入门</h1>
<div role="doc-toc">
<h2>&#30446;&#24405;</h2>
<div role="doc-toc">
<ul>
<li><a href="#org3de812d">1. 定义 class</a></li>
<li><a href="#orgd3011a6">2. 创建 WebGL 程序</a>
<ul>
<li><a href="#org879f764">2.1. vertexShader</a></li>
<li><a href="#org89d4a0f">2.2. fragmentShader</a></li>
<li><a href="#org969f737">2.3. OpenGL 代码解释</a></li>
</ul>
</li>
<li><a href="#orgce70bba">3. 实现 WebGLProgram</a>
<ul>
<li><a href="#orgabec50d">3.1. WebGL 的变量和 js 交互</a></li>
</ul>
</li>
<li><a href="#orge4986a3">4. 最后</a></li>
</ul>
</div>
</div>
<p>
本文主要介绍WebGL的一些基础知识,及用WebGL实现一个简单的颜色盘.<br />
涉及到一些基础数学知识.<br />
</p>

<p>
对应的代码,我开源在 <a href="https://github.com/zHaOdANiuu/ColorPicker-WebGl">github</a>.<br />
直接开始写吧.<br />
</p>
<div>
<h2><span>1.</span> 定义 class</h2>
<div>
<div>
<pre><code><span>class</span> <span>ColorPicker</span> {
  <span>public</span> <span>canvas</span>: <span>HTMLCanvasElement</span>
  <span>private</span> <span>_radius</span>: <span>number</span>
  <span>constructor</span>() {
    <span>// &#39068;&#33394;&#30424;&#26159;&#22278;&#30340;,&#25152;&#20197;&#25105;&#20204;&#35774;&#32622;&#40664;&#35748;&#21322;&#24452;&#20026; 400,</span>
    <span>// &#32771;&#34385;&#21040;&#35201;&#35774;&#32622;&#30340;&#24773;&#20917;,&#25105;&#20204;&#35201;&#35774;&#32622;&#19968;&#19979;&#21333;&#29420;&#30340; getter &#21644; setter &#26041;&#27861;</span>
    <span>this</span>._radius = <span>400</span>
    <span>this</span>.canvas = <span>document</span>.createElement(<span>'canvas'</span>)
    <span>this</span>.canvas.width = <span>400</span>
    <span>this</span>.canvas.height = <span>400</span>
    <span>this</span>.canvas.style.width = <span>this</span>.canvas.style.height = <span>400</span> + <span>'px'</span>
  }
  <span>public</span> <span>get</span> <span>radius</span>(){
    <span>return</span> <span>this</span>._radius
  }
  <span>public</span> <span>set</span> <span>radius</span>(<span>val</span>: <span>number</span>){
    <span>this</span>.canvas.width = val
    <span>this</span>.canvas.height = val
    <span>this</span>.canvas.style.width = <span>this</span>.canvas.style.height = val + <span>'px'</span>
    <span>this</span>._radius = val
  }
}
</code></pre>
</div>

<p>
定义 class 以后,准备创建 webgl 的渲染上下文, 通过 canvas 的 getContext 方法来获取渲染<br />
上下文. 因为是比较常用的属性,所以绑定在 this 上,但是是私有属性.<br />
这样子别人引用的时候,lsp 服务会没有相关的属性,因为这是无关的属性,我们在书写代码的时候,这种属性都<br />
应该是私有的. class 也应该和 function 一样简洁,这里说完以后,下面就不会再提了.<br />
</p>

<div>
<pre><code><span>class</span> <span>ColorPicker</span> {
  <span>private</span> <span>gl</span>: <span>WebGLRenderingContext</span>
  <span>constructor</span>(){
    <span>const</span> <span>tmp</span> = <span>this</span>.canvas.getContext(<span>'webgl'</span>, { <span>antialias</span>: <span>true</span> })
    <span>if</span> (tmp === <span>null</span>) {
      <span>throw</span> <span>new</span> <span>Error</span>(<span>'WebGL2 is not supported by your browser'</span>)
    }
    <span>this</span>.gl = tmp
  }
}
</code></pre>
</div>

<p>
这是代码.<br />
</p>
</div>
</div>
<div>
<h2><span>2.</span> 创建 WebGL 程序</h2>
<div>
<p>
接下来创建一个工具函数,用来创建 <code>webgl</code> 的顶点着色器和片段着色器, 然后把 <code>glsl</code> 的代码<br />
传进去就可以了.<br />
</p>
</div>
<div>
<h3><span>2.1.</span> vertexShader</h3>
<div>
<div>
<pre><code>precision <span>mediump</span> <span>float</span>;
attribute <span>vec2</span> <span>a_position</span>;
<span>void</span> <span>main</span>() {
  gl_Position = vec4(a_position, 0, 1);
}
</code></pre>
</div>
</div>
</div>
<div>
<h3><span>2.2.</span> fragmentShader</h3>
<div>
<div>
<pre><code>precision <span>mediump</span> <span>float</span>;
<span>const</span> <span>float</span> <span>PI</span> = acos(-1.0);
<span>const</span> <span>float</span> <span>FULL_CIRCLE_RADIANS</span> = 2.0 * PI;
<span>vec4</span> <span>line</span>(<span>vec2</span> <span>ndc</span>, <span>vec2</span> <span>sPos</span>, <span>vec2</span> <span>ePos</span>, <span>float</span> <span>w</span>, <span>vec3</span> <span>color</span>)
{
  <span>vec2</span> <span>dir</span> = normalize(ePos - sPos);
  <span>vec2</span> <span>perpDir</span> = vec2(-dir.y, dir.x);
  <span>float</span> <span>d</span> = abs(dot(ndc - sPos, perpDir));
  <span>float</span> <span>l</span> = dot(ndc - sPos, dir);
  <span>float</span> <span>insidePerpendicular</span> = smoothstep(w + 0.005, w - 0.005, d);
  <span>float</span> <span>insideSegment</span> = step(l, length(ePos - sPos)) * step(0.0, l);
  <span>float</span> <span>t</span> = insidePerpendicular * insideSegment;
  <span>return</span> mix(vec4(color, t), vec4(0.0), step(t, 0.0));
}
<span>vec4</span> <span>fCircle</span>(<span>vec2</span> <span>ndc</span>, <span>vec3</span> <span>color</span>, <span>float</span> <span>radius</span>, <span>vec2</span> <span>pos</span>)
{
  <span>float</span> <span>t</span> = smoothstep(radius, radius - 0.01, length(ndc - pos));
  <span>return</span> vec4(color, t);
}
<span>vec4</span> <span>sCircle</span>(<span>vec2</span> <span>ndc</span>, <span>vec3</span> <span>color</span>, <span>float</span> <span>radius</span>, <span>float</span> <span>stroke</span>, <span>vec2</span> <span>pos</span>)
{
  <span>float</span> <span>len</span> = length(ndc - pos);
  <span>float</span> <span>r1</span> = radius - stroke;
  <span>float</span> <span>r2</span> = radius + stroke;
  <span>float</span> <span>t</span> = smoothstep(r1, r1 + 0.01, len) - smoothstep(r2, r2 + 0.01, len);
  <span>return</span> mix(vec4(color, t), vec4(0.0), step(t, 0.0));
}
<span>vec4</span> <span>circle</span>(<span>vec2</span> <span>ndc</span>, <span>vec3</span> <span>fillColor</span>, <span>vec3</span> <span>strokeColor</span>, <span>float</span> <span>strokeWidth</span>, <span>float</span> <span>radius</span>, <span>vec2</span> <span>pos</span>)
{
  <span>vec4</span> <span>stroke</span> = sCircle(ndc, strokeColor, radius, strokeWidth, pos);
  <span>return</span> mix(fCircle(ndc, fillColor, radius, pos), stroke, stroke.a);
}
<span>vec3</span> <span>hsvToRgb</span>(<span>float</span> <span>h</span>, <span>float</span> <span>s</span>, <span>float</span> <span>v</span>)
{
  <span>vec4</span> <span>K</span> = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
  <span>vec3</span> <span>p</span> = abs(fract(vec3(h) + K.xyz) * 6.0 - K.w);
  <span>return</span> v * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), s);
}
<span>float</span> <span>linear</span>(<span>float</span> <span>t</span>, <span>float</span> <span>tMin</span>, <span>float</span> <span>tMax</span>)
{
  <span>return</span> tMin * (1.0 - t) + tMax * t;
}
<span>vec3</span> <span>getColorFromPoint</span>(<span>vec2</span> <span>point</span>, <span>float</span> <span>brig</span>)
{
  <span>float</span> <span>radius</span> = length(point);
  <span>if</span> (radius &gt; 1.0)
  {
    <span>return</span> vec3(0.0);
  }
  <span>float</span> <span>hue</span> = atan(point.y, point.x) / FULL_CIRCLE_RADIANS; <span>// </span><span>Convert to range [0, 1]
</span>  <span>float</span> <span>saturation</span> = radius;
  <span>float</span> <span>value</span> = 1.0;
  <span>return</span> hsvToRgb(hue, saturation, value);
}
<span>void</span> <span>calculateColor</span>(<span>vec4</span> <span>data</span>[3], out <span>vec4</span> <span>result</span>) {
  result = vec4(0.0);
  <span>for</span> (<span>int</span> <span>i</span> = 0; i &lt; 3; i++) {
      <span>vec4</span> <span>c</span> = data[i];
      result = mix(result, c, c.a);
  }
}
uniform <span>vec2</span> <span>u_resolution</span>;
uniform <span>vec2</span> <span>u_position</span>;
uniform <span>float</span> <span>u_brightness</span>;
uniform <span>float</span> <span>u_btnSize</span>;
uniform <span>float</span> <span>u_btnStrokeWidth</span>;
uniform <span>float</span> <span>u_lineStrokeWidth</span>;
<span>void</span> <span>main</span>() {
  <span>vec2</span> <span>uv</span> = (gl_FragCoord.xy / u_resolution) * 2.0 - 1.0;
  <span>vec4</span> <span>data</span>[3];
  <span>vec3</span> <span>strokeColor</span> = vec3(1.0);
  <span>vec3</span> <span>rgb</span> = hsvToRgb((atan(uv.y, uv.x)) / FULL_CIRCLE_RADIANS, length(uv), 1.0);
  <span>float</span> <span>scale</span> = 1.0 - u_btnSize - u_btnStrokeWidth;
  data[0] = fCircle(uv, rgb, scale, vec2(0.0, 0.0));
  data[1] = line(uv, vec2(0.0), u_position - u_position * u_btnSize, u_lineStrokeWidth, strokeColor);
  data[2] = circle(uv, getColorFromPoint(u_position, u_brightness), strokeColor, u_btnStrokeWidth, u_btnSize, u_position * scale);
  calculateColor(data, gl_FragColor);
}
</code></pre>
</div>
</div>
</div>
<div>
<h3><span>2.3.</span> OpenGL 代码解释</h3>
<div>
<p>
我们先来看看顶点着色器的代码.<br />
</p>

<p>
首先开头的 <code>precision mediump float;</code> 声明了精度,我们用 <code>mediump</code> 就可以了,高精度的效果<br />
在这个案例里不明显.<br />
</p>

<p>
接着就是 <code>attribute vec2 a_position;</code> 声明了一个属性,这个属性是用来给顶点着色器传递坐标的,<br />
待会可以用 js 获得. <code>gl_Position = vec4(a_position, 0, 1);</code> 这个代码设置了顶点的位置,<br />
是内置的属性.<br />
</p>

<p>
然后来看片段着色器的代码, 这个比较有难度了,首先需要理解一些代码运行的概念, 它的 <code>entry</code> 函数<br />
工作方式是类似于迭代器的. 意思就是说会遍历所有的像素,然后对每个像素进行处理,处理完之后, 会输<br />
出到屏幕上. 所以我们要处理的东西很多. 我们首先得实现一下图层关系,为什么？ 因为我们弄的颜色盘<br />
是有叠加的, 不是简单的绘制一个颜色盘, 得绘制手柄什么的.<br />
</p>

<p>
来看实现的代码:<br />
</p>

<div>
<pre><code><span>void</span> <span>calculateColor</span>(<span>vec4</span> <span>data</span>[3], out <span>vec4</span> <span>result</span>) {
  result = vec4(0.0);
  <span>for</span> (<span>int</span> <span>i</span> = 0; i &lt; 3; i++) {
      <span>vec4</span> <span>c</span> = data[i];
      result = mix(result, c, c.a);
  }
}
</code></pre>
</div>

<p>
要绘制的有 button, line, circle, 所以我们需要三个数据,所以我们定义了一个数组 <code>data[3]</code><br />
来存储三个数据. 然后定义一个函数 <code>calculateColor</code> 来计算三个数据中最亮的那个,简单的图层关<br />
系就有了. 这里的 <code>out vec4 result</code> 就类似于指针, 待会会把 <code>gl_FragColor</code> 给传进去,这个变量<br />
也是内置的, 给这个变量赋值就相当于在屏幕上绘制颜色了.<br />
</p>

<p>
在 <code>main</code> 函数里拿一下对应的颜色:<br />
</p>

<ul>
<li><code>uv</code> 拿到当前的坐标.<br /></li>
<li><code>atan</code> 和 <code>length</code> 计算出当前的角度和半径.<br /></li>
<li><code>hsvToRgb</code> 转换成颜色.<br /></li>
<li><code>fCircle</code> 绘制一个圆.<br /></li>
<li><code>line</code> 绘制一个线.<br /></li>
<li><code>circle</code> 绘制一个圆.<br /></li>
</ul>

<p>
这个圆就是颜色盘,这些函数都是工具函数.<br />
接下来就是交互,需要一个工具函数来根据位置获得颜色.<br />
</p>

<div>
<pre><code><span>vec3</span> <span>getColorFromPoint</span>(<span>vec2</span> <span>point</span>, <span>float</span> <span>brig</span>)
{
  <span>float</span> <span>radius</span> = length(point);
  <span>if</span> (radius &gt; 1.0)
  {
    <span>return</span> vec3(0.0);
  }
  <span>float</span> <span>hue</span> = atan(point.y, point.x) / FULL_CIRCLE_RADIANS; <span>// </span><span>Convert to range [0, 1]
</span>  <span>float</span> <span>saturation</span> = radius;
  <span>float</span> <span>value</span> = 1.0;
  <span>return</span> hsvToRgb(hue, saturation, value);
}
</code></pre>
</div>

<p>
Uniform 变量说明:<br />
</p>

<ul>
<li><code>u_position</code> 鼠标点击的位置, 根据这个位置计算出当前的颜色.<br /></li>
<li><code>u_brightness</code> 滑动条的值, 根据这个值计算出当前的颜色.<br /></li>
<li><code>u_resolution</code> 屏幕的大小, 根据这个值计算出当前的位置.<br /></li>
<li><code>u_btnSize</code> 按钮的大小, 根据这个值计算出当前的按钮大小.<br /></li>
<li><code>u_btnStrokeWidth</code> 按钮的边框宽度, 根据这个值计算出当前的按钮边框宽度.<br /></li>
<li><code>u_lineStrokeWidth</code> 线条的宽度, 根据这个值计算出当前的线条宽度.<br /></li>
<li><code>u_position</code> 按钮的位置, 根据这个值计算出当前的按钮位置.<br /></li>
</ul>

<p>
这些变量都是要通过 <code>js</code> 来控制的, 等会会实现具体的绑定.<br />
</p>
</div>
</div>
</div>
<div>
<h2><span>3.</span> 实现 WebGLProgram</h2>
<div>
<p>
写好对应的 shader 代码以后, 我们来定义一个私有方法:<br />
</p>

<div>
<pre><code><span>private</span> createWebGLProgram() {
  <span>const</span> { <span>gl</span> } = <span>this</span>
  <span>const</span> <span>vertexShader</span> = <span>gl</span>.createShader(<span>gl</span>.VERTEX_SHADER)
  <span>if</span> (vertexShader === <span>null</span>) {
    <span>throw</span> <span>new</span> <span>Error</span>(<span>'Create Vertex Shader return null'</span>)
  }
  <span>gl</span>.shaderSource(
    <span>vertexShader</span>,
    <span>`&#21018;&#21018;&#30340;&#39030;&#28857;&#30528;&#33394;&#22120;&#20195;&#30721;`</span>
  )
  <span>gl</span>.compileShader(<span>vertexShader</span>)
  <span>if</span> (<span>!</span><span>gl</span>.getShaderParameter(<span>vertexShader</span>, <span>gl</span>.COMPILE_STATUS)) {
    <span>gl</span>.deleteShader(<span>vertexShader</span>)
    <span>throw</span> <span>new</span> <span>Error</span>(
      <span>`Compile shader error: </span>${<span>gl</span>.getShaderInfoLog(<span>vertexShader</span>)}<span>`</span>
    )
  }
  <span>const</span> <span>fragmentShader</span> = <span>gl</span>.createShader(<span>gl</span>.FRAGMENT_SHADER)
  <span>if</span> (fragmentShader === <span>null</span>) {
    <span>throw</span> <span>new</span> <span>Error</span>(<span>'Create Fragment Shader return null'</span>)
  }
  <span>gl</span>.shaderSource(
    <span>fragmentShader</span>,
    <span>`&#21018;&#21018;&#30340;&#29255;&#27573;&#30528;&#33394;&#22120;&#20195;&#30721;`</span>
  )
  <span>gl</span>.compileShader(<span>fragmentShader</span>)
  <span>if</span> (<span>!</span><span>gl</span>.getShaderParameter(<span>fragmentShader</span>, <span>gl</span>.COMPILE_STATUS)) {
    <span>gl</span>.deleteShader(<span>fragmentShader</span>)
    <span>throw</span> <span>new</span> <span>Error</span>(
      <span>`Compile shader error: </span>${<span>gl</span>.getShaderInfoLog(<span>fragmentShader</span>)}<span>`</span>
    )
  }
  <span>const</span> <span>program</span> = <span>gl</span>.createProgram()
  <span>if</span> (program === <span>null</span>) {
    <span>throw</span> <span>new</span> <span>Error</span>(<span>'Create program return null'</span>)
  }
  <span>gl</span>.attachShader(<span>program</span>, <span>vertexShader</span>)
  <span>gl</span>.attachShader(<span>program</span>, <span>fragmentShader</span>)
  <span>gl</span>.linkProgram(<span>program</span>)
  <span>if</span> (<span>!</span><span>gl</span>.getProgramParameter(<span>program</span>, <span>gl</span>.LINK_STATUS)) {
    <span>gl</span>.deleteProgram(<span>program</span>)
    <span>throw</span> <span>new</span> <span>Error</span>(<span>`Link program error: </span>${<span>gl</span>.getProgramInfoLog(<span>program</span>)}<span>`</span>)
  }
  <span>gl</span>.useProgram(<span>program</span>)
  <span>return</span> program
}
</code></pre>
</div>

<p>
只需要写一次就行了, 这个可以抽离出来, 不过平时不怎么用.<br />
</p>
</div>
<div>
<h3><span>3.1.</span> WebGL 的变量和 js 交互</h3>
<div>
<p>
回到 <code>constructor</code> 函数里, 再定义之前片段着色器里写好的代码所对应的变量,并初始化:<br />
</p>

<div>
<pre><code><span>class</span> <span>ColorPicker</span> {
  <span>private</span> <span>uniformData</span>: <span>UniformData</span>
  <span>private</span> <span>uniformLocation</span>: <span>Record</span>&lt;<span>keyof</span> <span>UniformData</span>, <span>WebGLUniformLocation</span>&gt;
  <span>private</span> <span>selectorPos</span>: <span>Float32Array</span>&lt;<span>ArrayBuffer</span>&gt;
  <span>constructor</span>() {
    <span>this</span>.uniformData = {
      <span>brightness</span>: <span>1.0</span>,
      <span>btnSize</span>: <span>0.06667</span>,
      <span>btnStrokeWidth</span>: <span>0.008</span>,
      <span>lineStrokeWidth</span>: <span>0.004</span>
    }
    <span>this</span>.uniformLocation = {
      <span>brightness</span>: <span>0</span>,
      <span>btnSize</span>: <span>0</span>,
      <span>btnStrokeWidth</span>: <span>0</span>,
      <span>lineStrokeWidth</span>: <span>0</span>
    }
    <span>// &#20197;&#38450;&#25353;&#38062;&#25302;&#21160;&#30340;&#26102;&#20505;,&#25302;&#21160;&#26368;&#22806;&#38754;,&#25353;&#38062;&#20250;&#26377;&#19968;&#21322;&#22312;&#39068;&#33394;&#30424;&#22806;&#38754;,&#25152;&#20197;&#25105;&#20204;&#35268;&#23450;&#20943;&#21435;&#25353;&#38062;&#22823;&#23567;&#21644;&#25551;&#36793;</span>
    <span>this</span>.scale =
      <span>1.0</span> - <span>this</span>.uniformData.btnSize - <span>this</span>.uniformData.btnStrokeWidth
    <span>// &#35760;&#24405;&#19968;&#19979;&#40736;&#26631;&#30340;&#20301;&#32622;</span>
    <span>this</span>.selectorPos = <span>new</span> <span>Float32Array</span>(<span>2</span>)
    <span>// &#32465;&#23450;&#21464;&#37327;</span>
    <span>this</span>.init()
  }
  <span>// &#21160;&#24577;&#35745;&#31639;&#24403;&#21069;&#30340;&#39068;&#33394;&#30424;&#30340;&#22823;&#23567;</span>
  <span>private</span> <span>get</span> <span>size</span>() {
    <span>return</span> <span>Math</span>.max(<span>this</span>.canvas.width, <span>this</span>.canvas.height) * <span>this</span>.scale
  }
  <span>// &#24403;&#35201;&#35775;&#38382;&#40736;&#26631;&#25152;&#23545;&#24212;&#30340;&#39068;&#33394;&#26102;,&#25105;&#20204;&#36890;&#36807;&#19968;&#28857;&#28857;&#30340;&#22352;&#26631;&#26469;&#35745;&#31639;&#20986;&#24403;&#21069;&#30340;&#39068;&#33394;,&#26080;&#38656;&#22810;&#35328;</span>
  <span>public</span> <span>get</span> <span>hsb</span>() {
    <span>const</span> <span>x</span> = <span>this</span>.selectorPos[<span>0</span>]
    <span>const</span> <span>y</span> = <span>this</span>.selectorPos[<span>1</span>]
    <span>const</span> <span>radius</span> = <span>Math</span>.sqrt(x * x + y * y)
    <span>let</span> <span>h</span> = <span>Math</span>.atan2(<span>y</span>, <span>x</span>) * (<span>180</span> / <span>Math</span>.PI)
    <span>if</span> (h &lt; <span>0</span>) {
      h += <span>360</span>
    }
    <span>return</span> [h, (radius / <span>this</span>.scale) * <span>100</span>, <span>this</span>.uniformData.brightness * <span>100</span>]
  }
  <span>private</span> <span>init</span>() {
    <span>const</span> { <span>gl</span>, <span>canvas</span>, <span>program</span>, <span>uniformData</span>, <span>uniformLocation</span> } = <span>this</span>
    <span>gl</span>.uniform2f(
      <span>gl</span>.getUniformLocation(<span>program</span>, <span>'u_resolution'</span>),
      <span>canvas</span>.width,
      <span>canvas</span>.height
    )
    <span>// &#36825;&#37324;&#26159;&#29992;&#26469;&#32465;&#23450;&#19968;&#20010; framebuffer &#30340;</span>
    <span>uniformLocation</span>.brightness = <span>gl</span>.getUniformLocation(<span>program</span>, <span>'u_brightness'</span>)<span>!</span>
    <span>uniformLocation</span>.btnSize = <span>gl</span>.getUniformLocation(<span>program</span>, <span>'u_btnSize'</span>)<span>!</span>
    <span>uniformLocation</span>.btnStrokeWidth = <span>gl</span>.getUniformLocation(
      <span>program</span>,
      <span>'u_btnStrokeWidth'</span>
    )<span>!</span>
    <span>uniformLocation</span>.lineStrokeWidth = <span>gl</span>.getUniformLocation(
      <span>program</span>,
      <span>'u_lineStrokeWidth'</span>
    )<span>!</span>
    <span>// &#36825;&#37324;&#35843;&#29992;&#30340; uniform1f &#21644;&#29255;&#27573;&#30528;&#33394;&#22120;&#37027;&#37324;&#23450;&#20041;&#30340;&#31867;&#22411;&#26377;&#20851;,&#20855;&#20307;&#30340;&#30475;&#25991;&#26723;</span>
    <span>gl</span>.uniform1f(<span>uniformLocation</span>.brightness, <span>uniformData</span>.brightness)
    <span>gl</span>.uniform1f(<span>uniformLocation</span>.btnSize, <span>uniformData</span>.btnSize)
    <span>gl</span>.uniform1f(<span>uniformLocation</span>.btnStrokeWidth, <span>uniformData</span>.btnStrokeWidth)
    <span>gl</span>.uniform1f(<span>uniformLocation</span>.lineStrokeWidth, <span>uniformData</span>.lineStrokeWidth)
  }
}
</code></pre>
</div>

<p>
我们来管理一下事件的回调和动画的播放等等&hellip;<br />
</p>

<div>
<pre><code><span>class</span> <span>ColorPicker</span> {
  <span>private</span> <span>requestAnimationFrameHandle</span><span>!</span>: <span>number</span> <span>// &#21160;&#30011;&#30340; handle</span>
  <span>private</span> <span>eventManger</span>: [<span>string</span>, <span>EventListenerOrEventListenerObject</span>][]
  <span>constructor</span>() {
    <span>this</span>.eventManger = []
  }
  <span>// &#32472;&#21046;&#19968;&#24103;&#30340;&#39068;&#33394;&#30424;,&#21518;&#38754;&#36890;&#36807; requestAnimationFrame &#24490;&#29615;&#35843;&#29992;</span>
  <span>private</span> <span>drawOneFrame</span>() {
    <span>const</span> { <span>gl</span>, <span>program</span> } = <span>this</span>
    <span>gl</span>.bindBuffer(<span>gl</span>.ARRAY_BUFFER, <span>gl</span>.createBuffer())
    <span>gl</span>.bufferData(
      <span>gl</span>.ARRAY_BUFFER,
      <span>new</span> <span>Float32Array</span>([-<span>1.0</span>, -<span>1.0</span>, <span>1.0</span>, -<span>1.0</span>, -<span>1.0</span>, <span>1.0</span>, <span>1.0</span>, <span>1.0</span>]),
      <span>gl</span>.STATIC_DRAW
    )
    <span>const</span> <span>posAttributeLocation</span> = <span>gl</span>.getAttribLocation(<span>program</span>, <span>'a_position'</span>)
    <span>gl</span>.enableVertexAttribArray(<span>posAttributeLocation</span>)
    <span>gl</span>.vertexAttribPointer(<span>posAttributeLocation</span>, <span>2</span>, <span>gl</span>.FLOAT, <span>false</span>, <span>0</span>, <span>0</span>)
    <span>return</span> () =&gt; {
      <span>gl</span>.clearColor(<span>0.0</span>, <span>0.0</span>, <span>0.0</span>, <span>0.0</span>)
      <span>gl</span>.clear(<span>gl</span>.COLOR_BUFFER_BIT)
      <span>gl</span>.drawArrays(<span>gl</span>.TRIANGLE_STRIP, <span>0</span>, <span>4</span>)
    }
  }
  <span>private</span> <span>addEventListener</span>&lt;<span>K</span> <span>extends</span> <span>keyof</span> <span>HTMLElementEventMap</span>&gt;(
    <span>type</span>: <span>K</span>,
    <span>listener</span>: (<span>this</span>: <span>HTMLCanvasElement</span>, <span>ev</span>: <span>HTMLElementEventMap</span>[<span>K</span>]) =&gt; <span>unknown</span>,
    <span>options</span>?: <span>boolean</span> | <span>AddEventListenerOptions</span>
  ): <span>void</span>
  <span>private</span> <span>addEventListener</span>(
    <span>type</span>: <span>string</span>,
    <span>listener</span>: <span>EventListenerOrEventListenerObject</span>,
    <span>options</span>?: <span>boolean</span> | <span>AddEventListenerOptions</span>
  ) {
    <span>this</span>.eventManger.push([type, listener])
    <span>this</span>.canvas.addEventListener(<span>type</span>, <span>listener</span>, <span>options</span>)
  }
}
</code></pre>
</div>

<p>
接下来就是交互, 这里是比较麻烦的, 需要换算鼠标坐标到 webgl 里的坐标,<br />
通过 webgl 里的变量来计算出当前的颜色.<br />
</p>

<p>
所以需要定义一个 <code>toWebGlPos</code> 和一个 <code>isInCircle</code> 函数来计算坐标,在 <code>processEvent</code> 函数<br />
里绑定事件,并更新 <code>selectorPos</code> 变量.<br />
</p>

<div>
<pre><code><span>private</span> toWebGlPos(x: <span>number</span>, y: <span>number</span>) {
  <span>return</span> [
    ((x - <span>this</span>.canvas.width * <span>0.5</span>) / <span>this</span>.size) * <span>2</span>,
    ((<span>this</span>.canvas.height * <span>0.5</span> - y) / <span>this</span>.size) * <span>2</span>
  ]
}
<span>private</span> isInCircle(x: <span>number</span>, y: <span>number</span>) {
  <span>const</span> { <span>canvas</span>, <span>size</span> } = <span>this</span>
  <span>const</span> <span>offset</span> = (<span>Math</span>.max(<span>canvas</span>.width, <span>canvas</span>.height) - size) * <span>0.5</span>
  <span>const</span> <span>radius</span> = size * <span>0.5</span>
  <span>const</span> <span>tmp</span> = offset + radius
  <span>return</span> (x - tmp) ** <span>2</span> + (y - tmp) ** <span>2</span> &lt;= radius ** <span>2</span>
}
<span>private</span> processEvent() {
  <span>const</span> <span>uPosition</span> = <span>this</span>.gl.getUniformLocation(<span>this</span>.program, <span>'u_position'</span>)
  <span>let</span> <span>mousedown</span> = <span>false</span>
  <span>this</span>.addEventListener(<span>'mousedown'</span>, <span>e</span> =&gt; {
    <span>const</span> <span>rect</span> = <span>this</span>.canvas.getBoundingClientRect()
    <span>const</span> <span>x</span> = <span>e</span>.clientX - <span>rect</span>.left
    <span>const</span> <span>y</span> = <span>e</span>.clientY - <span>rect</span>.top
    <span>if</span> (<span>!this</span>.isInCircle(<span>x</span>, <span>y</span>)) {
      <span>return</span>
    }
    <span>e</span>.stopPropagation()
    <span>const</span> <span>pos</span> = <span>this</span>.toWebGlPos(<span>x</span>, <span>y</span>)
    <span>this</span>.gl.uniform2f(<span>uPosition</span>, pos[<span>0</span>], pos[<span>1</span>])
    mousedown = <span>true</span>
  })
  <span>this</span>.addEventListener(<span>'mousemove'</span>, <span>e</span> =&gt; {
    <span>const</span> <span>rect</span> = <span>this</span>.canvas.getBoundingClientRect()
    <span>const</span> <span>x</span> = <span>e</span>.clientX - <span>rect</span>.left
    <span>const</span> <span>y</span> = <span>e</span>.clientY - <span>rect</span>.top
    <span>if</span> (<span>!this</span>.isInCircle(<span>x</span>, <span>y</span>)) {
      <span>this</span>.canvas.style.cursor = <span>'default'</span>
      <span>return</span>
    }
    <span>e</span>.stopPropagation()
    <span>// &#36825;&#37324;&#26159;&#20026;&#20102;&#40736;&#26631;&#31227;&#21160;&#30340;&#26102;&#20505;,&#40736;&#26631;&#26679;&#24335;&#21464;&#25104;&#21313;&#23383;&#26550;</span>
    <span>if</span> (<span>this</span>.canvas.style.cursor !== <span>'crosshair'</span>)
      <span>this</span>.canvas.style.cursor = <span>'crosshair'</span>
    <span>if</span> (mousedown) {
      <span>const</span> <span>pos</span> = <span>this</span>.toWebGlPos(<span>x</span>, <span>y</span>)
      <span>this</span>.gl.uniform2f(<span>uPosition</span>, pos[<span>0</span>], pos[<span>1</span>])
      <span>this</span>.selectorPos.set(<span>pos</span>)
    }
  })
  <span>this</span>.addEventListener(<span>'mouseup'</span>, <span>e</span> =&gt; {
    <span>e</span>.stopPropagation()
    mousedown = <span>false</span>
  })
}
</code></pre>
</div>
</div>
</div>
</div>
<div>
<h2><span>4.</span> 最后</h2>
<div>
<p>
我们来暴露一下公共方法,供外部调用:<br />
</p>

<div>
<pre><code><span>public</span> setUniformData(key: <span>keyof</span> <span>UniformData</span>, value: <span>number</span>) {
  <span>this</span>.uniformData[key] = value
  <span>const</span> <span>tmp</span> = <span>this</span>.uniformLocation[key]
  <span>if</span> (tmp) {
    <span>this</span>.gl.uniform1f(<span>tmp</span>, <span>value</span>)
  }
}
<span>public</span> install(parent: HTMLElement = <span>document</span>.body) {
  <span>parent</span>.appendChild(<span>this</span>.canvas)
  <span>const</span> <span>draw</span> = <span>this</span>.drawOneFrame()
  <span>const</span> <span>drawLoop</span> = () =&gt; {
    draw()
    <span>this</span>.requestAnimationFrameHandle = requestAnimationFrame(<span>drawLoop</span>)
  }
  drawLoop()
  <span>this</span>.processEvent()
}
<span>public</span> uninstall() {
  cancelAnimationFrame(<span>this</span>.requestAnimationFrameHandle)
  <span>this</span>.eventManger.forEach(<span>event</span> =&gt; {
    <span>this</span>.canvas.removeEventListener(event[<span>0</span>], event[<span>1</span>])
  })
}
</code></pre>
</div>

<p>
以上就是所有的内容了, 希望能帮到你.<br />
</p>
</div>
</div>]]></description>
    </item>
    <item>
      <title>Emacs for Windows的IME输入法字体大小修复</title>
      <link>https://zdn.codeberg.page/public/Emacs-W32-IME-Font.html</link>
      <guid isPermaLink="true">https://zdn.codeberg.page/public/Emacs-W32-IME-Font.html</guid>
      <pubDate>Mon, 31 Aug 2026 00:00:00 +0800</pubDate>
      <description><![CDATA[<h1>Emacs for Windows的IME输入法字体大小修复</h1>
<div role="doc-toc">
<h2>&#30446;&#24405;</h2>
<div role="doc-toc">
<ul>
<li><a href="#orga92bde0">1. 问题描述</a></li>
<li><a href="#org5f141e3">2. 前置知识</a></li>
<li><a href="#orgcb68cc7">3. 修复过程</a></li>
<li><a href="#orgb1c3024">4. 后日谈</a></li>
</ul>
</div>
</div>
<div>
<h2><span>1.</span> 问题描述</h2>
<div>
<p>
相关的 <a href="https://debbugs.gnu.org/cgi/bugreport.cgi?bug=68339">bug报告</a>.<br />
</p>

<p>
GNU Emacs在Windows方面人手不足, 这个问题年久失修, 是长期问题.<br />
表现为IME输入法preedit窗口的字体大小不会跟随着emacs字体的缩放<br />
而缩放, 导致视觉上特别小.<br />
</p>
</div>
</div>
<div>
<h2><span>2.</span> 前置知识</h2>
<div>
<p>
首先要了解Windows上的输入法api, 微软提供了两套api, GNU Emacs使用<br />
IME api, 这是Windows老牌api. 还要了解Emacs相关代码和Windows窗口<br />
事件循坏且有基础Windows桌面开发经验.<br />
</p>

<p>
GNU Emacs政策禁止AI编写代码, 要遵循相关政策知道贡献指南.<br />
开发流程通过邮件列表和linux一模一样.<br />
</p>

<p>
参考链接<br />
</p>
<ul>
<li><a href="https://www.gnu.org/software/emacs/manual/html_node/emacs/Contributing.html">https://www.gnu.org/software/emacs/manual/html_node/emacs/Contributing.html</a><br /></li>
<li><a href="https://en.wikipedia.org/wiki/Event_loop">https://en.wikipedia.org/wiki/Event_loop</a><br /></li>
<li><a href="https://en.wikipedia.org/wiki/Input_method">https://en.wikipedia.org/wiki/Input_method</a><br /></li>
<li><a href="https://github.com/microsoft/Windows-classic-samples">https://github.com/microsoft/Windows-classic-samples</a><br /></li>
<li><a href="https://learn.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-getobjectw">https://learn.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-getobjectw</a><br /></li>
<li><a href="https://learn.microsoft.com/en-us/windows/win32/api/imm/nf-imm-immsetcompositionfontw">https://learn.microsoft.com/en-us/windows/win32/api/imm/nf-imm-immsetcompositionfontw</a><br /></li>
</ul>
</div>
</div>
<div>
<h2><span>3.</span> 修复过程</h2>
<div>
<p>
<a href="https://github.com/zHaOdANiuu/emacs-w32-ime-font.patch">PATCH</a><br />
</p>

<p>
Emacs原先代码只调用了 <b>ImmSetCompositionWindow</b>,<br />
没有设置相关结构体, 准备工作要做的比较多且要注意线程安全.<br />
</p>

<p>
修复路径<br />
</p>
<ul>
<li>调用 <b>ImmSetCompositionFont</b> 传入 <b>LOGFONTW</b> 设置字体大小.<br />
<ul>
<li>通过全局 <b>w32_system_remap_font</b> 使用 <b>GetObjectW</b> 填充 <b>LOGFONTW</b> 结构体.<br />
<ul>
<li>在 <b>lisp</b> 线程里面找到有调用 <b>PostMessage</b> 通知IME相关事件调用栈,<br />
通过 <b>faceremp</b> 机制访问Emacs动态映射的字体并用 <b>w32_system_remap_font</b> 保存.<br /></li>
</ul></li>
</ul></li>
</ul>

<p>
相关技术细节bug报告会更详细.<br />
</p>
</div>
</div>
<div>
<h2><span>4.</span> 后日谈</h2>
<div>
<p>
经过我的坚持和努力最终修复了这个问题, PATCH改动不大, 但浓缩了大量知识<br />
中间遇到了线程安全, 不了解Emacs代码架构等问题. 需要感谢GNU Emacs的<br />
核心成员 <b>Eli Zaretskii</b> 指导和review, 还要感谢 <a href="https://emacs-china.org/t/windows-emacs-patch-vim/31835/130">Emacs China</a> 的坛友测试.<br />
</p>

<p>
在LLM流行的当下, 解决这个问题所需要的知识可以轻松获取, 我不知道以后AI Coding<br />
会发展成什么样, 它提升代码编写效率实打实且聪明模型写代码往往正确. 但LLM的代码也<br />
确实存在版权问题, 即AI会在你不知情下使用别人仓库的代码, 有大量相关报道和案列,<br />
这是GNU项目当下所遇到的版权问题.<br />
</p>

<p>
这是我第一次给GNU项目贡献代码, 我签署了相关fsf版权协议, 这个过程完全电子化,<br />
非常有趣的一次贡献, 让我更加了解GNU项目的思想和历史和熟悉邮件列表交流.<br />
</p>

<p>
提交合并的那一刻, 正式成为了GNU Emacs的贡献者我很高兴, 用 <b>RMS</b> 的话来说<br />
我也成为了追求自由软件的人且不是搞开源软件的而是自由软件, 这点很重要😉.<br />
</p>
</div>
</div>]]></description>
    </item>
    <item>
      <title>Emacs for Windows的输入法事件修复</title>
      <link>https://zdn.codeberg.page/public/Emacs-W32-IME-Notify.html</link>
      <guid isPermaLink="true">https://zdn.codeberg.page/public/Emacs-W32-IME-Notify.html</guid>
      <pubDate>Thu, 15 Oct 2026 00:00:00 +0800</pubDate>
      <description><![CDATA[<h1>Emacs for Windows的输入法事件修复</h1>
<div role="doc-toc">
<h2>&#30446;&#24405;</h2>
<div role="doc-toc">
<ul>
<li><a href="#org101c257">1. 前因后果</a></li>
<li><a href="#org1980c52">2. 问题描述</a></li>
<li><a href="#org9c8b401">3. 修复过程</a></li>
<li><a href="#org202018b">4. 后日谈</a></li>
</ul>
</div>
</div>
<div>
<h2><span>1.</span> 前因后果</h2>
<div>
<p>
Windows11 22h2以后, 把输入法的实现默认设置为TSF了, 导致相关IME api出现了兼容问题.<br />
<a href="https://learn.microsoft.com/en-us/answers/questions/5612690/detecting-the-ime-status-of-other-windows">相关的链接</a><br />
</p>

<p>
在修复这个问题的时, 发现的这个问题.  排查这个问题我根据Windows开发的直觉, 发现了具体代码的<br />
问题并修复.<br />
<a href="https://cgit.git.savannah.gnu.org/cgit/emacs.git/commit/?id=a8e727e4add08b586e41820acb1f2bef1fa26aa9">commit</a><br />
</p>
</div>
</div>
<div>
<h2><span>2.</span> 问题描述</h2>
<div>
<p>
相关的<a href="https://debbugs.gnu.org/cgi/bugreport.cgi?bug=71711">bug报告</a>.<br />
</p>

<p>
具体表现为:<br />
</p>
<ol>
<li>忽略语音输入法<br /></li>
<li>聚焦emacs时, 切换中英文输入法任务栏的输入法图标不会更新<br /></li>
</ol>
</div>
</div>
<div>
<h2><span>3.</span> 修复过程</h2>
<div>
<p>
在事件循环里面给默认分支添加 <code>DispatchMessage</code> 调用就可以.<br />
具体的代码可以看我的仓库或者上面的提交链接.<br />
<a href="https://github.com/zHaOdANiuu/w32-ime-status/tree/main">我的仓库</a><br />
</p>
</div>
</div>
<div>
<h2><span>4.</span> 后日谈</h2>
<div>
<p>
这个问题很复杂,需要比较了解窗口的事件循环处理逻辑, 在这之前得知道emacs的窗口事件分发是怎么样<br />
的, emacs的窗口事件有2个线程, 一个是工作线程, 一个是输入线程.<br />
</p>

<p>
首先在emacs的主循环代码里面, emacs是有区分msg的消息来源是工作线程还是输入线程, 通过判断<br />
msg的hwnd是不是等于NULL, 这个很容易理解, 工作线程的事件是没有窗口句柄的. 不是NULL就会调用<br />
DispatchMessage, 从而分发到输入线程的窗口过程事件函数.<br />
</p>

<p>
问题就出现在这里了, 当在emacs里, 键盘上按下shift键的时候, 按照我的Windows开发经验来说,<br />
系统分发了2个事件, 一个是键盘按下和通知IME状态的事件, 区别就在这里.<br />
</p>

<p>
通知IME状态的事件它是没有窗口句柄的, 被误以为是工作线程事件, 给拦截住了, 导致没有把事件分发<br />
到输入线程的窗口过程函数, 也就没有去调用默认的窗口事件处理函数.  这个问题是新版本的TSF兼容<br />
问题.<br />
</p>

<p>
这对于emacs来说是bug吗？ 我感觉不算, 但处理的也有点问题, 光靠判断hwnd还不够, 应该要有专门<br />
的flag去判断, 旧版本的微软拼音没有这个问题, 小狼毫等三方输入法不兼容IME也会导致这个问题.<br />
</p>
</div>
</div>]]></description>
    </item>
    <item>
      <title>Emacs for Windows的TUI IME支持</title>
      <link>https://zdn.codeberg.page/public/Emacs-W32-TUI-IME.html</link>
      <guid isPermaLink="true">https://zdn.codeberg.page/public/Emacs-W32-TUI-IME.html</guid>
      <pubDate>Thu, 03 Sep 2026 00:00:00 +0800</pubDate>
      <description><![CDATA[<h1>Emacs for Windows的TUI IME支持</h1>
<div role="doc-toc">
<h2>&#30446;&#24405;</h2>
<div role="doc-toc">
<ul>
<li><a href="#org1c11c23">1. 问题描述</a></li>
<li><a href="#org7342e5a">2. 前置知识</a></li>
<li><a href="#org90d503e">3. 修复过程</a></li>
<li><a href="#org37db12c">4. 后日谈</a></li>
</ul>
</div>
</div>
<div>
<h2><span>1.</span> 问题描述</h2>
<div>
<p>
相关的<a href="https://debbugs.gnu.org/cgi/bugreport.cgi?bug=81495">bug报告</a>.<br />
</p>

<p>
在终端里面启动 <code>emacs (emacs -nw)</code>, 输入中文字符串完成按下回车或者候选数字<br />
emacs接收不到结果.<br />
</p>
</div>
</div>
<div>
<h2><span>2.</span> 前置知识</h2>
<div>
<p>
首先要了解Windows上的控制台方面api, 且了解Emacs相关代码.<br />
</p>

<p>
GNU Emacs政策禁止AI编写代码, 要遵循相关政策知道贡献指南.<br />
开发流程通过邮件列表和linux一模一样.<br />
</p>

<p>
参考链接<br />
</p>
<ul>
<li><a href="https://www.gnu.org/software/emacs/manual/html_node/emacs/Contributing.html">https://www.gnu.org/software/emacs/manual/html_node/emacs/Contributing.html</a><br /></li>
<li><a href="https://github.com/microsoft/Windows-classic-samples">https://github.com/microsoft/Windows-classic-samples</a><br /></li>
<li><a href="https://learn.microsoft.com/en-us/windows/console/readconsoleinput">https://learn.microsoft.com/en-us/windows/console/readconsoleinput</a><br /></li>
<li><a href="https://learn.microsoft.com/en-us/windows/console/key-event-record-str">https://learn.microsoft.com/en-us/windows/console/key-event-record-str</a><br /></li>
</ul>
</div>
</div>
<div>
<h2><span>3.</span> 修复过程</h2>
<div>
<p>
<a href="https://github.com/emacs-mirror/emacs/commit/9e0360d398a9574a9e399efd45dcd1c048cbac7e">commit</a><br />
</p>

<p>
原先代码有一个分支会忽略emacs自己的状态键.<br />
</p>

<div>
<pre><code><span>if</span> (faked_key == event-&gt;wVirtualKeyCode)
  {
    faked_key = 0;
    <span>return</span> 0
  }
</code></pre>
</div>

<p>
问题出现在这个分支上, IME消息发送时, event-&gt;wVirtualKeyCode的值为0,<br />
同时faked_key未赋值也是0, 就把IME消息给忽略了.<br />
</p>

<p>
修复这个地方的同时还要处理读取字符版本分支, emacs需要兼容Windows 9x版本,<br />
也就是上世纪的Windows 95等, 所用到的api会有大量ascii版本带A后缀的.<br />
</p>

<p>
所以emacs会有多个flag变量来判断支不支持Unicode, 当前代码需要用到的变量是<br />
<b>w32_console_unicode_input</b>.<br />
</p>

<p>
原先代码的判断逻辑.<br />
</p>

<div>
<pre><code><span>if</span> (event-&gt;uChar.AsciiChar == 0)
  {
    <span>// </span><span>...
</span>  }
<span>else</span> <span>if</span> (event-&gt;uChar.AsciiChar &gt; 0)
  {
  <span>// </span><span>...
</span>  }
</code></pre>
</div>

<p>
这个分支的判断可能在几十年前是对的, 但是现在是错误的. 根据微软文档 <b>KEY_EVENT_RECORD</b><br />
结构体的uChar类型是一个 <b>AsciiChar</b> 低8位, <b>UnicodeChar</b> 高16位的联合体.<br />
</p>

<div>
<pre><code><span>union</span> {
  <span>WCHAR</span> <span>UnicodeChar</span>;
  <span>CHAR</span>  <span>AsciiChar</span>;
} <span>uChar</span>;
</code></pre>
</div>

<p>
所以得先判断高位的UnicodeChar, 不然会混进AsciiChar的数据导致乱码.<br />
</p>

<div>
<pre><code><span>if</span> ((w32_console_unicode_input &amp;&amp; event-&gt;uChar.UnicodeChar &lt; 128)
    || (<span>!</span>w32_console_unicode_input &amp;&amp; event-&gt;uChar.AsciiChar &gt; 0))
</code></pre>
</div>

<p>
这样判断就能有效判断是ascii支持还是unicode, 同时还兼容了上世纪的代码.<br />
</p>

<p>
相关技术细节bug报告会更详细.<br />
</p>
</div>
</div>
<div>
<h2><span>4.</span> 后日谈</h2>
<div>
<p>
这是我第二次给GNU项目贡献代码, 非常有趣的一次贡献, 改动的代码虽然小, 但花费了我大量的<br />
时间和精力, 隐约有掉头发的感觉, 最后代码成功合并到emacs的master主线了.<br />
</p>
</div>
</div>]]></description>
    </item>
  </channel>
</rss>
