计算机图形学——基本图形生成算法


第一章 直线图形的生成

在光栅显示器上,直线是由一系列离散像素点逼近而成的。核心问题是:给定起点和终点,该点亮哪些像素?

1.1 DDA 算法(数值微分法)

原理

DDA(Digital Differential Analyzer)利用直线的微分方程逐点递推。从直线方程 y=kx+by = kx + by=kx+b 出发,根据斜率绝对值的不同,分两种情况处理。

情况一:∣k∣<1|k| < 1∣k∣<1(以 x 为步进方向)

x 每步加 1,y 每步加 k:

xi+1=xi+1,yi+1=yi+k(i=0,1,2,…, xe−xs−1)x_{i+1} = x_i + 1,\quad y_{i+1} = y_i + k \quad (i = 0,1,2,\dots,\ x_e-x_s-1)xi+1​=xi​+1,yi+1​=yi​+k(i=0,1,2,…, xe​−xs​−1)

写像素时对 y 四舍五入:SetPixel(x, y+0.5)。

情况二:∣k∣≥1|k| \ge 1∣k∣≥1(以 y 为步进方向)

y 每步加 1,x 每步加 1/k:

yi+1=yi+1,xi+1=xi+1k(i=0,1,2,…, ye−ys−1)y_{i+1} = y_i + 1,\quad x_{i+1} = x_i + \frac{1}{k} \quad (i = 0,1,2,\dots,\ y_e-y_s-1)yi+1​=yi​+1,xi+1​=xi​+k1​(i=0,1,2,…, ye​−ys​−1)

写像素时对 x 四舍五入:SetPixel(x+0.5, y)。

通俗理解:DDA 每次都沿着"变化快"的方向作为主步进方向(x 快就走 x,y 快就走 y),另一个方向按比例跟进。这保证了每一步最多跨越 1 个像素,不会出现断点。

八个方向的统一处理

如图,平面直线段有 8 个方向。交换起点和终点后,①和⑤归为一类、⑧和④归为一类、②和⑥归为一类、③和⑦归为一类,最终归并为两类基本情况:

方向斜率步进轴递推公式
① ⑧∣k∣<1|k|<1∣k∣<1xxi+1=xi+1, yi+1=yi+kx_{i+1}=x_i+1,\ y_{i+1}=y_i+kxi+1​=xi​+1, yi+1​=yi​+k
② ③∣k∣≥1|k|\ge 1∣k∣≥1yyi+1=yi+1, xi+1=xi+1/ky_{i+1}=y_i+1,\ x_{i+1}=x_i+1/kyi+1​=yi​+1, xi+1​=xi​+1/k

例题:已知直线段起点 (0, 0)、终点 (5, 2),用 DDA 法求出线段每个像素坐标。

解:k=(2−0)/(5−0)=0.4k = (2-0)/(5-0) = 0.4k=(2−0)/(5−0)=0.4,∣k∣<1|k| < 1∣k∣<1,以 x 为步进方向。

ixix_ixi​yiy_iyi​SetPixel(x, y+0.5)像素坐标
000.0(0, 0.5)→(0,0)(0, 0)
110.4(1, 0.9)→(1,0)(1, 0)
220.8(2, 1.3)→(2,1)(2, 1)
331.2(3, 1.7)→(3,1)(3, 1)
441.6(4, 2.1)→(4,2)(4, 2)
552.0(5, 2.5)→(5,2)(5, 2)

1.2 中点画线法

DDA 涉及浮点运算。中点画线法用整数递推完成同样的事,是硬件实现的主流方案。

1.2.1 基本原理

直线的隐式方程:

F(x,y)=y−kx−b=0F(x, y) = y - kx - b = 0F(x,y)=y−kx−b=0

性质:

  • F(x,y)=0F(x,y)=0F(x,y)=0:点在直线上
  • F(x,y)>0F(x,y)>0F(x,y)>0:点在直线上方
  • F(x,y)<0F(x,y)<0F(x,y)<0:点在直线下方

以 0<k<10 < k < 10<k<1(①方向)为例。假设当前像素为 P(xp,yp)P(x_p, y_p)P(xp​,yp​),下一步 x 必须加 1,y 有两种选择:

  • P₁:(xp+1, yp)(x_p+1,\ y_p)(xp​+1, yp​)——正右方(平走)
  • P₂:(xp+1, yp+1)(x_p+1,\ y_p+1)(xp​+1, yp​+1)——右上方(斜走)

取 P₁ 与 P₂ 的中点 M(xp+1, yp+0.5)M(x_p+1,\ y_p+0.5)M(xp​+1, yp​+0.5),代入 FFF:

di=F(M)=F(xi+1, yi+0.5)=(yi+0.5)−k(xi+1)−bd_i = F(M) = F(x_i+1,\ y_i+0.5) = (y_i+0.5) - k(x_i+1) - bdi​=F(M)=F(xi​+1, yi​+0.5)=(yi​+0.5)−k(xi​+1)−b

判定规则:

  • di≥0d_i \ge 0di​≥0:M 在直线上方或线上 → 取 P₁(正右)
  • di<0d_i < 0di​<0:M 在直线下方 → 取 P₂(右上)

通俗理解:想象一条上坡路,你站在当前像素 P 上。前方一个岔路口——平走(P₁)和上坡(P₂)。中点 M 就是岔路口的中间位置。如果 M 比路面高(d≥0),说明路在下面,平走就够了;如果 M 比路面低(d<0),说明路在上面,得往上坡走。

1.2.2 递推公式(①方向:0 < k < 1)

选 P₁ 时(di≥0d_i \ge 0di​≥0):下一步中点为 M′(xi+2, yi+0.5)M'(x_i+2,\ y_i+0.5)M′(xi​+2, yi​+0.5)

di+1=(yi+0.5)−k(xi+2)−b=di−kd_{i+1} = (y_i+0.5) - k(x_i+2) - b = d_i - kdi+1​=(yi​+0.5)−k(xi​+2)−b=di​−k

选 P₂ 时(di<0d_i < 0di​<0):下一步中点为 M′(xi+2, yi+1.5)M'(x_i+2,\ y_i+1.5)M′(xi​+2, yi​+1.5)

di+1=(yi+1.5)−k(xi+2)−b=di+1−kd_{i+1} = (y_i+1.5) - k(x_i+2) - b = d_i + 1 - kdi+1​=(yi​+1.5)−k(xi​+2)−b=di​+1−k

初始值:d0=F(xs+1, ys+0.5)=0.5−kd_0 = F(x_s+1,\ y_s+0.5) = 0.5 - kd0​=F(xs​+1, ys​+0.5)=0.5−k

1.2.3 消除小数——整数版本

d0=0.5−kd_0 = 0.5 - kd0​=0.5−k 含小数。两边同乘 2Δx2\Delta x2Δx(Δx=xe−xs\Delta x = x_e-x_sΔx=xe​−xs​,Δy=ye−ys\Delta y = y_e-y_sΔy=ye​−ys​):

项目浮点版整数版
初始 d0d_0d0​0.5−k0.5 - k0.5−kΔx−2Δy\Delta x - 2\Delta yΔx−2Δy
di≥0d_i \ge 0di​≥0 递推di+1=di−kd_{i+1} = d_i - kdi+1​=di​−kdi+1=di−2Δyd_{i+1} = d_i - 2\Delta ydi+1​=di​−2Δy
di<0d_i < 0di​<0 递推di+1=di+1−kd_{i+1} = d_i + 1 - kdi+1​=di​+1−kdi+1=di+2Δx−2Δyd_{i+1} = d_i + 2\Delta x - 2\Delta ydi+1​=di​+2Δx−2Δy

①方向中点画线法(整数版)汇总:

  • 初始:x0=xs, y0=ys, d0=Δx−2Δyx_0 = x_s,\ y_0 = y_s,\ d_0 = \Delta x - 2\Delta yx0​=xs​, y0​=ys​, d0​=Δx−2Δy
  • di≥0d_i \ge 0di​≥0:xi+1=xi+1, yi+1=yi, di+1=di−2Δyx_{i+1}=x_i+1,\ y_{i+1}=y_i,\ d_{i+1}=d_i-2\Delta yxi+1​=xi​+1, yi+1​=yi​, di+1​=di​−2Δy
  • di<0d_i < 0di​<0:xi+1=xi+1, yi+1=yi+1, di+1=di+2Δx−2Δyx_{i+1}=x_i+1,\ y_{i+1}=y_i+1,\ d_{i+1}=d_i+2\Delta x-2\Delta yxi+1​=xi​+1, yi+1​=yi​+1, di+1​=di​+2Δx−2Δy
  • 循环 Δx\Delta xΔx 步
1.2.4 四个方向完整递推表
方向斜率初始 d0d_0d0​di≥0d_i \ge 0di​≥0di<0d_i < 0di​<0
①0<k<10<k<10<k<1Δx−2Δy\Delta x - 2\Delta yΔx−2Δyx+1, yx{+}1,\ yx+1, y 不变, d−2Δyd{-}2\Delta yd−2Δyx+1, y+1, d+2Δx−2Δyx{+}1,\ y{+}1,\ d{+}2\Delta x{-}2\Delta yx+1, y+1, d+2Δx−2Δy
②k>1k>1k>12Δx−Δy2\Delta x - \Delta y2Δx−Δyx+1, y+1, d+2Δx−2Δyx{+}1,\ y{+}1,\ d{+}2\Delta x{-}2\Delta yx+1, y+1, d+2Δx−2Δyxxx 不变, y+1, d+2Δxy{+}1,\ d{+}2\Delta xy+1, d+2Δx
③k<−1k<-1k<−12Δx+Δy2\Delta x + \Delta y2Δx+Δyxxx 不变, y+1, d+2Δxy{+}1,\ d{+}2\Delta xy+1, d+2Δxx−1, y+1, d+2Δx+2Δyx{-}1,\ y{+}1,\ d{+}2\Delta x{+}2\Delta yx−1, y+1, d+2Δx+2Δy
⑧−1<k<0-1<k<0−1<k<0−Δx−2Δy-\Delta x - 2\Delta y−Δx−2Δyx+1, y−1, d−2Δx−2Δyx{+}1,\ y{-}1,\ d{-}2\Delta x{-}2\Delta yx+1, y−1, d−2Δx−2Δyx+1, yx{+}1,\ yx+1, y 不变, d−2Δyd{-}2\Delta yd−2Δy
中点画线法例题

用中点画线法(整数版)绘制从 (0, 0) 到 (4, 3) 的直线段。

解:Δx=4\Delta x = 4Δx=4,Δy=3\Delta y = 3Δy=3,k=0.75k=0.75k=0.75(①方向)

d0=Δx−2Δy=4−6=−2d_0 = \Delta x - 2\Delta y = 4 - 6 = -2d0​=Δx−2Δy=4−6=−2

i当前像素did_idi​判定选择下一个像素di+1d_{i+1}di+1​ 计算
0(0, 0)−2< 0P₂(1, 1)−2+8−6=0
1(1, 1)0≥ 0P₁(2, 1)0−6=−6
2(2, 1)−6< 0P₂(3, 2)−6+8−6=−4
3(3, 2)−4< 0P₂(4, 3)—

像素序列:(0,0) → (1,1) → (2,1) → (3,2) → (4,3)


1.3 直线线宽处理(补充)

线刷子:沿垂直方向复制 w 行像素(w = 线宽)。

方刷子:以每个像素为中心,填充 w×ww\times ww×w 正方形。

多线段平行法:在原线段两侧平移出 w−1w-1w−1 条平行线段,分别绘制。


第二章 圆与椭圆图形的生成

2.1 参数方程产生圆弧

原理

圆的参数方程:

x=x0+rcos⁡θ,y=y0+rsin⁡θx = x_0 + r\cos\theta,\quad y = y_0 + r\sin\thetax=x0​+rcosθ,y=y0​+rsinθ

离散化:θ\thetaθ 从起点弧度逐步增加到终点弧度,每一步计算 (x,y)(x, y)(x,y)。

步长选择:Δθ=12πr\Delta\theta = \dfrac{1}{2\pi r}Δθ=2πr1​。圆周长 2πr2\pi r2πr,步长 Δθ\Delta\thetaΔθ 对应的弧长为 2πr⋅Δθ=12\pi r \cdot \Delta\theta = 12πr⋅Δθ=1,保证相邻像素间距不超过 1。

通俗理解:Δθ 的大小需要根据半径自动调节——半径越大,圆周长越大,需要更多的点来填充,步长就要越小。

只画一段圆弧

设置起始弧度 as 和终止弧度 ae,从 as 遍历到 ae 即可。 C 代码:

void Arc_Par(CDC* pDC, int x0, int y0, int r, float as, float ae, COLORREF color)
{
    int x, y; float d = 0.5 / r / 3.1415926;
    for(float t = as; t <= ae; t = t + d) {
        x = x0 + r * cos(t); y = y0 + r * sin(t);
        pDC->SetPixel(x, y, color);
    }
}

当 as=0, ae=6.28 时,绘制的就是完整圆。

例题

圆心 (5,5),半径 R=10,从 30° 到 150° 的圆弧,列出前 5 个离散点。

解:Δθ=1/(2π×10)≈0.0159\Delta\theta = 1/(2\pi \times 10) \approx 0.0159Δθ=1/(2π×10)≈0.0159 弧度 ≈ 0.91°

iθ (弧度)x = 5+10cosθy = 5+10sinθ像素
00.524 (30°)5+8.66=13.665+5.00=10.00(14,10)
10.5405+8.58=13.585+5.14=10.14(14,10)
20.5565+8.50=13.505+5.27=10.27(14,10)
30.5715+8.41=13.415+5.41=10.41(13,10)
40.5875+8.32=13.325+5.55=10.55(13,11)

2.2 中点画圆法

2.2.1 八分法画圆

利用圆的八对称性。已知圆上一点 (x,y)(x,y)(x,y),另外七个对称点可直接写出:

(y,x), (y,−x), (x,−y), (−x,−y), (−y,−x), (−y,x), (−x,y)(y,x),\ (y,-x),\ (x,-y),\ (-x,-y),\ (-y,-x),\ (-y,x),\ (-x,y)(y,x), (y,−x), (x,−y), (−x,−y), (−y,−x), (−y,x), (−x,y)

只需计算第一象限 xxx 从 0 到 R/2R/\sqrt{2}R/2​(即 x≤yx \le yx≤y)的 1/8 圆弧,其余七段对称映射。

2.2.2 判别函数

F(x,y)=x2+y2−R2=0F(x,y) = x^2 + y^2 - R^2 = 0F(x,y)=x2+y2−R2=0

  • F<0F<0F<0:点在圆内;F=0F=0F=0:点在圆上;F>0F>0F>0:点在圆外

从 (0,R)(0,R)(0,R) 顺时针走。当前像素 P(xi,yi)P(x_i,y_i)P(xi​,yi​),候选:

  • P₁:(xi+1, yi)(x_i+1,\ y_i)(xi​+1, yi​)——正右方
  • P₂:(xi+1, yi−1)(x_i+1,\ y_i-1)(xi​+1, yi​−1)——右下方

中点 M(xi+1, yi−0.5)M(x_i+1,\ y_i-0.5)M(xi​+1, yi​−0.5),判别式:

di=F(M)=(xi+1)2+(yi−0.5)2−R2d_i = F(M) = (x_i+1)^2 + (y_i-0.5)^2 - R^2di​=F(M)=(xi​+1)2+(yi​−0.5)2−R2

  • di<0d_i < 0di​<0:M 在圆内 → 取 P₁(正右)
  • di≥0d_i \ge 0di​≥0:M 在圆外或圆上 → 取 P₂(右下),约定 di=0d_i=0di​=0 时取 P₂
2.2.3 递推公式

di<0d_i < 0di​<0(选 P₁):

di+1=(xi+2)2+(yi−0.5)2−R2=di+2xi+3d_{i+1} = (x_i+2)^2 + (y_i-0.5)^2 - R^2 = d_i + 2x_i + 3di+1​=(xi​+2)2+(yi​−0.5)2−R2=di​+2xi​+3

di≥0d_i \ge 0di​≥0(选 P₂):

di+1=(xi+2)2+(yi−1.5)2−R2=di+2xi−2yi+5d_{i+1} = (x_i+2)^2 + (y_i-1.5)^2 - R^2 = d_i + 2x_i - 2y_i + 5di+1​=(xi​+2)2+(yi​−1.5)2−R2=di​+2xi​−2yi​+5

初始值:d0=F(1, R−0.5)=1.25−Rd_0 = F(1,\ R-0.5) = 1.25 - Rd0​=F(1, R−0.5)=1.25−R

2.2.4 消除小数——整数版本

令 e0=d0−0.25e_0 = d_0 - 0.25e0​=d0​−0.25,则 e0=1−Re_0 = 1 - Re0​=1−R(仍记为 ddd)。由于递推增量全为整数,ddd 始终为整数,di<0d_i < 0di​<0 等价于 di<0d_i < 0di​<0。最终公式不变。

项目公式
初始x0=0, y0=R, d0=1−Rx_0=0,\ y_0=R,\ d_0=1-Rx0​=0, y0​=R, d0​=1−R
di<0d_i < 0di​<0di+1=di+2xi+3, xi+1=xi+1, yi+1=yid_{i+1}=d_i+2x_i+3,\ x_{i+1}=x_i+1,\ y_{i+1}=y_idi+1​=di​+2xi​+3, xi+1​=xi​+1, yi+1​=yi​
di≥0d_i \ge 0di​≥0di+1=di+2xi−2yi+5, xi+1=xi+1, yi+1=yi−1d_{i+1}=d_i+2x_i-2y_i+5,\ x_{i+1}=x_i+1,\ y_{i+1}=y_i-1di+1​=di​+2xi​−2yi​+5, xi+1​=xi​+1, yi+1​=yi​−1
终止直到 x>yx > yx>y
中点画圆法例题

用中点画圆法(整数版)画圆心在原点、R=6 的圆。列出八分圆部分计算过程。

解:d0=1−6=−5d_0 = 1-6 = -5d0​=1−6=−5,x=0x=0x=0,y=6y=6y=6

i(x,y)did_idi​判定动作di+1d_{i+1}di+1​
0(0,6)−5< 0 → P₁x++, y 不变−5+0+3=−2
1(1,6)−2< 0 → P₁x++, y 不变−2+2+3=3
2(2,6)3≥ 0 → P₂x++, y−−3+4−12+5=0
3(3,5)0≥ 0 → P₂x++, y−−0+6−10+5=1
4(4,4)1≥ 0 → P₂x++, y−−—
—(5,3)—x>y 停止

八分圆像素:(0,6), (1,6), (2,6), (3,5), (4,4), (5,3),通过八对称映射得完整圆。


2.3 中点法绘制椭圆

中点画圆方法可推广到椭圆。圆心在原点的椭圆方程为:

F(x,y)=b2x2+a2y2−a2b2=0F(x,y) = b^2x^2 + a^2y^2 - a^2b^2 = 0F(x,y)=b2x2+a2y2−a2b2=0

其中 aaa 为横向半径、bbb 为纵向半径。利用四分对称性,只讨论第一象限椭圆弧。

关键:以法矢量两个分量相等的点——即 b2(xi+1)<a2(yi−0.5)b^2(x_i+1) < a^2(y_i-0.5)b2(xi​+1)<a2(yi​−0.5) 的边界——将第一象限弧分为上部分和下部分。

上部分(x 步进、y 判别)

中点 M(xi+1, yi−0.5)M(x_i+1,\ y_i-0.5)M(xi​+1, yi​−0.5),判别式:

di=b2(xi+1)2+a2(yi−0.5)2−a2b2d_i = b^2(x_i+1)^2 + a^2(y_i-0.5)^2 - a^2b^2di​=b2(xi​+1)2+a2(yi​−0.5)2−a2b2

  • di<0d_i < 0di​<0:取 P₁(正右)→ di+1=di+b2(2xi+3)d_{i+1} = d_i + b^2(2x_i+3)di+1​=di​+b2(2xi​+3)
  • di≥0d_i \ge 0di​≥0:取 P₂(右下)→ di+1=di+b2(2xi+3)+a2(−2yi+2)d_{i+1} = d_i + b^2(2x_i+3) + a^2(-2y_i+2)di+1​=di​+b2(2xi​+3)+a2(−2yi​+2)

初值:d0=b2+a2(0.25−b)d_0 = b^2 + a^2(0.25-b)d0​=b2+a2(0.25−b)

下部分(y 步进、x 判别)

中点 M(xi+0.5, yi−1)M(x_i+0.5,\ y_i-1)M(xi​+0.5, yi​−1),判别式:

di=b2(xi+0.5)2+a2(yi−1)2−a2b2d_i = b^2(x_i+0.5)^2 + a^2(y_i-1)^2 - a^2b^2di​=b2(xi​+0.5)2+a2(yi​−1)2−a2b2

  • di<0d_i < 0di​<0:取 P₂(右下)→ di+1=di+b2(2xi+2)+a2(−2yi+3)d_{i+1} = d_i + b^2(2x_i+2) + a^2(-2y_i+3)di+1​=di​+b2(2xi​+2)+a2(−2yi​+3)
  • di≥0d_i \ge 0di​≥0:取 P₁(正下)→ di+1=di+a2(−2yi+3)d_{i+1} = d_i + a^2(-2y_i+3)di+1​=di​+a2(−2yi​+3)
C 代码
void Elliptic_Mid(CDC* p, int x0, int y0, int a, int b, COLORREF color)
{
    int bb = b * b, aa = a * a, d = bb + aa * (0.25 - b), x = 0, y = b;
    while(bb * (x + 1) < aa * (y - 0.5)) {           // 上部分
        p->SetPixel( x+x0,  y+y0, color); p->SetPixel(-x+x0,  y+y0, color);
        p->SetPixel(-x+x0, -y+y0, color); p->SetPixel( x+x0, -y+y0, color);
        if(d < 0) d = d + bb * (2 * x + 3);
        else      d = d + bb * (2 * x + 3) + aa * (-2 * y + 2), y--;
        x++;
    }
    d = bb * (x+0.5)*(x+0.5) + aa * (y-1)*(y-1) - aa * bb;  // 下部分判别式
    while(y >= 0) {                                     // 下部分
        p->SetPixel( x+x0,  y+y0, color); p->SetPixel(-x+x0,  y+y0, color);
        p->SetPixel(-x+x0, -y+y0, color); p->SetPixel( x+x0, -y+y0, color);
        if(d < 0) d = d + bb * (2 * x + 2) + aa * (-2 * y + 3), x++;
        else      d = d + aa * (-2 * y + 3);
        y--;
    }
}

2.4 圆弧线宽处理(补充)

同心圆法:在原圆弧内外各偏移线宽一半,绘制多条同心圆弧并填充中间区域。

笔刷法:将圆弧上每个像素替换为一个填充区域,依据圆弧走势进行水平或垂直移动像素点来填充。


第三章 字符

计算机中每个字符由数字编码唯一标识。ASCII 码用 7 位二进制编码 128 个字符;汉字采用 GB/T 2312-1980 标准,区码和位码各占 1 字节,最高位为 1 区分于 ASCII。字符分为点阵字符和矢量字符。

3.1 点阵字符(重点)

存储方式

点阵字符将字符形状表示为一个矩形点阵:

  • 位 = 1:字符笔画经过,置字符颜色
  • 位 = 0:不经过,置背景颜色

常见规格:5×7、8×8(ASCII)、16×16(汉字,32 字节/字)。

教材示例:8×8 字符 “B”
点阵图案 (■=1, □=0):

行0: ■ ■ ■ ■ ■ ■ □ □    11111100    FC
行1: □ ■ ■ □ □ ■ ■ □    01100110    66
行2: □ ■ ■ □ □ ■ ■ □    01100110    66
行3: □ ■ ■ ■ ■ ■ □ □    01111100    7C
行4: □ ■ ■ □ □ ■ ■ □    01100110    66
行5: □ ■ ■ □ □ ■ ■ □    01100110    66
行6: ■ ■ ■ ■ ■ ■ □ □    11111100    FC
行7: ■ □ □ □ □ □ □ □    10000000    80

存储为一维数组:BYTE mark[8] = {0xFC, 0x66, 0x66, 0x7C, 0x66, 0x66, 0xFC, 0x80};

显示方法

逐行扫描:每行与 2i2^i2i 做按位与运算,提取第 i 位信息。C 代码:

void Show_Char1(CDC* p, int x0, int y0, BYTE mark[], int n, COLORREF color)
{
    long t[32]; t[n - 1] = 1;
    for(int i = n - 2; i >= 0; i--) t[i] = t[i + 1] * 2;
    for(int y = 0; y < n; y++)
        for(int x = 0; x < n; x++) {
            if((mark[y] & t[x]) == t[x])
                p->SetPixel(x + x0, y + y0, color);
        }
}

通俗理解:点阵字符就像 LED 广告屏——每个灯要么亮要么灭。把 8 行二进制(每行 1 字节)存下来,显示时逐行按位判断,1 就亮灯、0 就灭灯。


3.2 矢量字符

矢量字符记录字符的笔画信息而非整个位图。存储空间小,旋转缩放只需对笔画端点变换,不会出现锯齿。

端点存储方式

笔画用标志 + 坐标序列描述:

标志含义
0移动到(不绘制)
1画直线到
2画曲线到(过三点抛物线)
−1结束

示例——矢量字符 “B”:从原点开始,

移动→a(10,10) → 画直线→b(10,110) → 画直线→c(60,110)
→ 画曲线→d(60,60) [控制点(80,85)] → 画曲线→e(60,10) [控制点(80,35)]
→ 画直线→a(10,10) → 移动→f(10,60) → 画直线→d(60,60)

存储内容:0,10,10, 1,10,110, 1,60,110, 2,80,85,60,60, 2,80,35,60,10, 1,10,10, 0,10,60, 1,60,60, -1

曲线绘制

过三点 (x1,y1),(x2,y2),(x3,y3)(x_1,y_1),(x_2,y_2),(x_3,y_3)(x1​,y1​),(x2​,y2​),(x3​,y3​) 画抛物线(二次参数曲线):

x(t)=axt2+bxt+cx,y(t)=ayt2+byt+cyx(t)=a_x t^2+b_x t+c_x,\quad y(t)=a_y t^2+b_y t+c_yx(t)=ax​t2+bx​t+cx​,y(t)=ay​t2+by​t+cy​

系数公式:

{ax=2(x3−2x2+x1)bx=4x2−x1−3x3cx=x1,{ay=2(y3−2y2+y1)by=4y2−y1−3y3cy=y1\begin{cases}a_x=2(x_3-2x_2+x_1)\\b_x=4x_2-x_1-3x_3\\c_x=x_1\end{cases},\quad\begin{cases}a_y=2(y_3-2y_2+y_1)\\b_y=4y_2-y_1-3y_3\\c_y=y_1\end{cases}⎩⎨⎧​ax​=2(x3​−2x2​+x1​)bx​=4x2​−x1​−3x3​cx​=x1​​,⎩⎨⎧​ay​=2(y3​−2y2​+y1​)by​=4y2​−y1​−3y3​cy​=y1​​

用小直线段连接相邻点,ttt 以 0.05 为增量。

方向编码存储方式(AutoCAD 风格)

另一种存储方式是记录每笔画的方向编码和逻辑长度。16 个方向编码覆盖均匀分布的 16 个方向,每个笔画只需记录方向编号 + 长度,极大减少存储空间。

对比总结
特性点阵字符矢量字符
存储逐像素 0/1笔画端点 + 曲线参数
缩放放大出现锯齿任意缩放不失真
渲染快(逐行复制)慢(需计算曲线)
变换需对每个像素变换只对笔画端点变换
典型应用嵌入式、终端TrueType(.ttf)、AutoCAD

重点例题精讲

例题 A:DDA 法求像素坐标

已知直线段起点 A(5, 3)、终点 B(7, 9),用 DDA 算法求每个像素坐标,并画出示意图。

逐步计算

Δx=2\Delta x = 2Δx=2,Δy=6\Delta y = 6Δy=6,k=6/2=3k = 6/2 = 3k=6/2=3,∣k∣≥1|k| \ge 1∣k∣≥1,以 y 为步进方向。

1/k=1/3≈0.33331/k = 1/3 \approx 0.33331/k=1/3≈0.3333

iyiy_iyi​xix_ixi​SetPixel(x+0.5, y)像素
035.000(5.5, 3)→(5,3)(5, 3)
145.333(5.8, 4)→(5,4)(5, 4)
255.667(6.2, 5)→(6,5)(6, 5)
366.000(6.5, 6)→(6,6)(6, 6)
476.333(6.8, 7)→(6,7)(6, 7)
586.667(7.2, 8)→(7,8)(7, 8)
697.000(7.5, 9)→(7,9)(7, 9)
示意图
 y │
 9 │ · · · · · · ★(7,9)
 8 │ · · · · · ★(7,8) ·
 7 │ · · · · ★(6,7) · ·
 6 │ · · · ★(6,6) · · ·
 5 │ · · ★(6,5) · · · ·
 4 │ · ★(5,4) · · · · ·
 3 │ ★(5,3) · · · · · ·
   └──┴──┴──┴──┴──┴──┴──┴──
    x=5  x=6  x=7

因为 ∣k∣=3≥1|k|=3 \ge 1∣k∣=3≥1,以 y 为主步进方向。每一步 y 加 1,x 加 1/3 ≈ 0.33,稳步从 (5,3) 走到 (7,9)。


例题 B:16×16 点阵字符设计

设计汉字「明」的 16×16 点阵字符,写出完整十六进制编码。

点阵设计结果

在这里插入图片描述

完整十六进制编码(32字节)
00 00  01 FC  01 FC  7D 8C  7D FC  6D FC  7D 8C  6D FC
7D FC  7D 8C  01 8C  01 8C  03 9C  07 0C  00 00  00 00

结构说明:

  • 左"日":左竖(列7 持续亮,01→01→7D→…→7D),顶横(行1~2 01 FC),中间横(行6 7D 8C),底部(行9 7D 8C),内部以 7D/6D 交替填充,整体饱满方正
  • 右"月":右竖(列13 持续亮),左撇由左上向右下斜收(列8→列2 逐行内收),内部两短横在行4 和行8(7D FC中的 FC 对应列8~13 全亮与部分亮交替形成)

交互式点阵编辑器

以下为完整可运行的 16×16 点阵编辑器,读者可将代码保存为 .html 文件,双击在浏览器中打开,即可点击网格编辑点阵并实时查看十六进制编码。

<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>16x16 点阵编辑器</title>
<style>
* { margin:0; padding:0; box-sizing:border-box; }
body { display:flex; flex-direction:column; align-items:center; padding:30px 20px; font-family:'Microsoft YaHei',sans-serif; background:#f5f5f5; min-height:100vh; }
h2 { margin-bottom:8px; color:#333; }
.sub { color:#888; font-size:13px; margin-bottom:20px; }
.wrapper { display:flex; gap:30px; align-items:flex-start; flex-wrap:wrap; justify-content:center; }
.grid-container { background:#e0e0e0; padding:6px; border-radius:8px; box-shadow:0 2px 12px rgba(0,0,0,0.1); user-select:none; }
.grid { display:grid; grid-template-columns:repeat(16, 32px); grid-template-rows:repeat(16, 32px); gap:2px; }
.cell { width:32px; height:32px; background:#fff; border-radius:2px; cursor:pointer; transition:background 0.08s; }
.cell.on { background:#1a1a1a; }
.panel { display:flex; flex-direction:column; gap:16px; min-width:260px; }
.hex-box { background:#fff; padding:16px; border-radius:8px; box-shadow:0 2px 8px rgba(0,0,0,0.08); font-family:'Consolas','Courier New',monospace; font-size:14px; line-height:1.8; color:#333; white-space:pre; min-height:200px; }
.btn-row { display:flex; gap:10px; flex-wrap:wrap; }
.btn { padding:10px 18px; border:none; border-radius:6px; font-size:14px; cursor:pointer; transition:background 0.15s,transform 0.1s; font-family:inherit; }
.btn:active { transform:scale(0.96); }
.btn-clear { background:#eee; color:#555; }
.btn-clear:hover { background:#ddd; }
.btn-fill { background:#eee; color:#555; }
.btn-fill:hover { background:#ddd; }
.btn-copy { background:#4a90d9; color:#fff; }
.btn-copy:hover { background:#357abd; }
.btn-load { background:#eee; color:#555; }
.btn-load:hover { background:#ddd; }
.toast { position:fixed; top:20px; left:50%; transform:translateX(-50%); background:#333; color:#fff; padding:10px 24px; border-radius:6px; font-size:14px; opacity:0; transition:opacity 0.3s; pointer-events:none; z-index:999; }
.toast.show { opacity:1; }
.coords { font-size:12px; color:#aaa; text-align:center; }
</style>
</head>
<body>
<h2>16x16 点阵编辑器</h2>
<div class="sub">点击方块切换点亮/熄灭 · 支持拖拽连续绘制</div>
<div class="wrapper">
  <div class="grid-container">
    <div class="grid" id="grid"></div>
  </div>
  <div class="panel">
    <div class="hex-box" id="hexBox">点击网格开始编辑...</div>
    <div class="coords" id="coords">列: - 行: -</div>
    <div class="btn-row">
      <button class="btn btn-clear" onclick="clearAll()">清空</button>
      <button class="btn btn-fill" onclick="fillAll()">填满</button>
      <button class="btn btn-copy" onclick="copyHex()">复制编码</button>
    </div>
    <div style="margin-top:8px">
      <textarea id="loadInput" placeholder="粘贴32字节十六进制编码(如 00 20 20 7F...)" style="width:100%;height:70px;font-family:Consolas,monospace;font-size:12px;padding:8px;border:1px solid #ddd;border-radius:6px;resize:vertical;"></textarea>
      <button class="btn btn-load" style="margin-top:6px;width:100%" onclick="loadFromInput()">加载编码</button>
    </div>
  </div>
</div>
<div class="toast" id="toast"></div>
<script>
(function() {
  var ROWS = 16, COLS = 16;
  var initialHex = [0x00,0x00, 0x01,0xFC, 0x01,0xFC, 0x7D,0x8C, 0x7D,0xFC, 0x6D,0xFC, 0x7D,0x8C, 0x6D,0xFC, 0x7D,0xFC, 0x7D,0x8C, 0x01,0x8C, 0x01,0x8C, 0x03,0x9C, 0x07,0x0C, 0x00,0x00, 0x00,0x00];
  var grid = [];
  for (var r = 0; r < ROWS; r++) {
    grid[r] = [];
    var lo = initialHex[r * 2], hi = initialHex[r * 2 + 1];
    for (var c = 0; c < COLS; c++) {
      var mask = c < 8 ? (0x80 >> c) : (0x80 >> (c - 8));
      var val = c < 8 ? (lo & mask) : (hi & mask);
      grid[r][c] = !!val;
    }
  }
  var mouseDown = false, paintMode = null;
  var gridEl = document.getElementById('grid');
  var hexBox = document.getElementById('hexBox');
  var coordsEl = document.getElementById('coords');
  var toast = document.getElementById('toast');
  function updateHex() {
    var bytes = [];
    for (var r = 0; r < ROWS; r++) {
      var lo = 0, hi = 0;
      for (var c = 0; c < 8; c++) if (grid[r][c]) lo |= (0x80 >> c);
      for (var c = 8; c < 16; c++) if (grid[r][c]) hi |= (0x80 >> (c - 8));
      bytes.push(lo, hi);
    }
    var text = '';
    for (var i = 0; i < bytes.length; i += 2) {
      text += bytes[i].toString(16).padStart(2, '0').toUpperCase();
      text += ' ';
      text += bytes[i + 1].toString(16).padStart(2, '0').toUpperCase();
      text += (i % 4 === 2 && i < 30) ? '\n' : '  ';
    }
    hexBox.textContent = text.trim();
  }
  function setCell(r, c, val) {
    if (grid[r][c] === val) return;
    grid[r][c] = val;
    var idx = r * COLS + c;
    if (val) gridEl.children[idx].classList.add('on');
    else gridEl.children[idx].classList.remove('on');
    updateHex();
  }
  function buildGrid() {
    gridEl.innerHTML = '';
    for (var r = 0; r < ROWS; r++) {
      for (var c = 0; c < COLS; c++) {
        var cell = document.createElement('div');
        cell.className = 'cell' + (grid[r][c] ? ' on' : '');
        (function(row, col) {
          cell.addEventListener('mousedown', function(e) {
            e.preventDefault();
            mouseDown = true;
            paintMode = grid[row][col] ? 'off' : 'on';
            setCell(row, col, paintMode === 'on');
          });
          cell.addEventListener('mouseenter', function() {
            coordsEl.textContent = '列: ' + col + ' 行: ' + row;
            if (mouseDown) setCell(row, col, paintMode === 'on');
          });
        })(r, c);
        gridEl.appendChild(cell);
      }
    }
  }
  document.addEventListener('mouseup', function() { mouseDown = false; paintMode = null; });
  window.clearAll = function() { for (var r = 0; r < ROWS; r++) for (var c = 0; c < COLS; c++) setCell(r, c, false); };
  window.fillAll = function() { for (var r = 0; r < ROWS; r++) for (var c = 0; c < COLS; c++) setCell(r, c, true); };
  window.copyHex = function() {
    var bytes = [];
    for (var r = 0; r < ROWS; r++) {
      var lo = 0, hi = 0;
      for (var c = 0; c < 8; c++) if (grid[r][c]) lo |= (0x80 >> c);
      for (var c = 8; c < 16; c++) if (grid[r][c]) hi |= (0x80 >> (c - 8));
      bytes.push(lo, hi);
    }
    var text = bytes.map(function(b) { return '0x' + b.toString(16).padStart(2, '0').toUpperCase(); }).join(', ');
    navigator.clipboard.writeText(text).then(function() { showToast('已复制到剪贴板'); });
  };
  window.loadFromInput = function() {
    var raw = document.getElementById('loadInput').value.trim();
    var nums = raw.match(/[0-9a-fA-F]{2}/g);
    if (!nums || nums.length < 32) { showToast('需要至少32个十六进制字节'); return; }
    for (var r = 0; r < ROWS; r++) {
      var lo = parseInt(nums[r * 2], 16), hi = parseInt(nums[r * 2 + 1], 16);
      for (var c = 0; c < 8; c++) setCell(r, c, !!(lo & (0x80 >> c)));
      for (var c = 8; c < 16; c++) setCell(r, c, !!(hi & (0x80 >> (c - 8))));
    }
    showToast('已加载');
  };
  function showToast(msg) { toast.textContent = msg; toast.classList.add('show'); setTimeout(function() { toast.classList.remove('show'); }, 1500); }
  buildGrid();
  updateHex();
})();
</script>
</body>
</html>

算法对比总览

算法对象核心思想运算类型对称利用
DDA直线按k选择步进方向,另一方向按比例跟进
中点画线法直线F(x,y)=y−kx−b,中点 M 判别 + 整数递推整数无
参数方程圆/弧离散角度,cos/sin 逐点计算浮点无
中点画圆法圆F(x,y)=x²+y²−R²,中点 M 判别 + 整数递推整数八对称
中点画椭圆法椭圆分上下两部分,分别步进 x 和 y整数四对称
点阵字符逐位 0/1 矩阵存储——
矢量字符笔画端点 + 曲线参数——
Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐