随波逐流的空间

希望每天黎明有着希望的光茫,照耀着并希望着,我们每天思考一步,就要想到做第三步。

EasyX 绘图库实现一个缓冲区半透明绘图

0.前言

  1. 本文章中,提供半透明矩形、半透明画点、半透明画圆函数供参考。
  2. 未来,可将半透明算法。加入到直线绘图、多边形绘图、扇形绘图等一系列绘图函数中。
  3. 可以整合一个头文件或扩展库,做一个完整几何图形与半透明绘图函数合集。

1.缓冲区半透明绘制随机运动圆球效果

这是本文章,最后一个代码截图效果。

2.半透明理论结合算法缓冲区画矩形说明

1.半透明绘图理论:

  • 与普通绘图不同,半透明绘图,是在普通绘图基础上,增加了半透明度的计算。
  • 使用半透明算法,计算出两个颜色之间的透明度。
  • 将绘图下方的背景颜色,与当前绘制的图形填充色,进行数学计算,混合在一起。形成透明通道(alpha),效果看起来就是透明的。
  • 通常情况下。背景颜色位于,当前绘制图形的颜色下方所有颜色。前景颜色位于,当前绘制图形的填充颜色。
  • 背景颜色不等于绘图背景色,而是指位于,当前图形填充色下方的填充颜色。
  • 获取背景颜色的方法,可以使用 EasyX 绘图库,自带读取缓冲区像素点颜色功能。
  • 将混合后的颜色写入缓冲区,就形成了半透明颜色,再应用于绘图程序上。

2.半透明理论结合缓冲区画半透明矩形说明:

  • 对于构成矩形的每个缓冲点区像素点,都会向下读取该缓冲区,像素点下层,图形的颜色(即背景颜色)。
  • 并将其与自身颜色(前景颜色)算法混合,形成半透明效果。
  • 后面绘制的半透明矩形,会叠加在之前绘制的半透明矩形上,叠加的次数越多,最上面的矩形效果越不透明。
  • 透明度可以设置在 0~256 之间。数值越低则越透明,数值越高则越不透明。‘

3.半透明核心代码公式与结果准确性

半透明算法使用位移算法(使用颜色值范围 0~256 使用位移运行符操作二进制来提高算法效率)。

半透明核心代码:

// RGB 颜色变量等于透明颜色混合计算值。
COLORREF rgb1 = RGB
	(
		(r1 * alp + r2 * (256 - alp)) >> 8, // 混合颜色 R 得出半透明颜色,r1 是前景色,alp 是颜色透明度,r2 背景色。
		(g1 * alp + g2 * (256 - alp)) >> 8, // 混合颜色 G 得出半透明颜色,g1 是前景色,alp 是颜色透明度,g2 背景色。
		(b1 * alp + b2 * (256 - alp)) >> 8 // 混合颜色 B 得出半透明颜色,b1 是前景色,alp 是颜色透明度,b2 背景色。
	);

半透明位移式算法公式(颜色值使用范围 0~256,在万位数上位移 8 在除法上相当于除以 256,也就是转换为百分比模式。高效率):

  • 混合颜色 R 色=(R 前景色 X 透明度 APLHA + R 背景色 X(256 -透明度 APLHA))>> 8。
  • 混合颜色 G 色=(G 前景色 X 透明度 APLHA + G 背景色 X(256 -透明度 APLHA))>> 8。
  • 混合颜色 B 色=(B 前景色 X 透明度 APLHA + B 背景色 X(256 -透明度 APLHA))>> 8。
  • 或者整体通用简化为:(前景色 X 透明度 + 背景色 X(256 - 透明度))>> 8

百分比式,半透明算法公式(颜色值使用范围 0~100,效率一般):

  • 混合颜色 R 色=(R 前景色 X 透明度 APLHA)+((100 - 透明度 APLHA)X 背景色 R)X 0.01。
  • 混合颜色 G 色=(G 前景色 X 透明度 APLHA)+((100 - 透明度 APLHA)X 背景色 G)X 0.01。
  • 混合颜色 B 色=(B 前景色 X 透明度 APLHA)+((100 - 透明度 APLHA)X 背景色 B)X 0.01。
  •  
  • 或者整体通用简化为:(前景色 X 透明度)+((100 - 透明度)X 背景色)X 0.01

简约 50% 半透明度公式(简单,相对高效率):

  • alphaR = (r1 + r2) / 2。
  • 或者:50% 透明度 R 色 =(前景色 + 背景色)/ 2。
  • aplhaG = (g1 + g2) / 2。
  • 或者:50% 透明度 G 色 =(前景色 + 背景色)/ 2。
  •  
  • aplhaB = (b1 + b2) / 2。
  • 或者:50% 透明度 B 色 =(前景色 + 背景色)/ 2。

公式算法结果准确性:

  • 经 Photoshop 软件,对比透明度验证。EasyX 缓冲区绘图加半透明算法,结果准确性。与 Photoshop 软件上的半透明数值,没有误差。数值完全一致,达到完美半透明效果。加上位移操作本身无小数,精准误差等于零。
  • 验证方法是将背景与前景,两图层进行叠加,前景图层与背景图层不同颜色,把前景图层透明度,调成各种不同的百分比数值,与不透明的背景图层,做前景透明度于背景试验。再使用颜色拾取器,获取叠加的半透明那部分颜色值,与缓冲区半透明算法绘图透明部分颜色值,判断对比并校准。

4.实现一个缓冲区半透明画点函数与半透明矩形函数

该程序效果。是一个鼠标,跟随着四个色方块。有不同的颜色,分别是红、蓝、绿、黑。它们移动到别的透明色方块上方,会有透明的效果展示。

实例代码:

// EasyX 简单缓冲区半透明画点,让你的绘图支持任意的透明度,达到让大家推波推润的效果。
// 日期:2023-4-7
// 作者:随波逐流
// 联系:2963787923@qq.com
//

#include <stdio.h>
#include <conio.h>
#include <graphics.h>
#include <math.h>
// 初始换透明度百分比为 50 也就是 128。
int alpha = 128;
// 设置半透明度函数。
void setalpha(int al) { alpha = al; }
// 打开缓冲区。
DWORD* g_pBuf11;
// 进行前景色和背景色半透明计算。
COLORREF Alphargb1(int r1, int g1, int b1, int r2, int g2, int b2, int alp)
{	// 替换颜色。
	alp = alpha;
	// 半透明颜色混合计算算法。(村长修改的特殊算法,使用位移算法,能提高效率)
	COLORREF rgb_alpha = RGB
	(
		(r1 * alp + r2 * (256 - alp)) >> 8,
		(g1 * alp + g2 * (256 - alp)) >> 8,
		(b1 * alp + b2 * (256 - alp)) >> 8
	);
	// 返回颜色。
	return rgb_alpha;
}
// 显示缓冲区指针,并读取坐标上的颜色值。
COLORREF fast_getpixelcolor(int x, int y, int WIDTH) { COLORREF c = g_pBuf11[y * WIDTH + x]; return BGR(c); }
// 快速缓冲区半透明画点函数 2
void putpixealpha(DWORD* g_pBuf, int x, int y, COLORREF c)
{
	int WIDTH = getwidth(), HEIGHT = getheight(), puti = y * WIDTH + x;
	// 如果 puti 超出缓冲区范围,直接返回。
	if (puti < 0 || puti >= (WIDTH * HEIGHT)) { return; }
	COLORREF backdropcolor, bkcolor = getbkcolor();
	// 读取背景上的颜色点。
	backdropcolor = fast_getpixelcolor(x, y, WIDTH);
	// 声明定义背景颜色和绘图的颜色。
	int r2 = GetRValue(c), g2 = GetGValue(c), b2 = GetBValue(c), r1 = GetRValue(backdropcolor), g1 = GetGValue(backdropcolor), b1 = GetBValue(backdropcolor);

	// 如果获取的颜色等于背景色不进行透明计算。
	if (bkcolor == backdropcolor) { g_pBuf[puti] = BGR(c); }
	// 透明颜色混合并输出。
	if (backdropcolor != bkcolor) { g_pBuf[puti] = BGR(Alphargb1(r2, g2, b2, r1, g1, b1, 128)); }
}

// 画支持透明的矩形。
void rec(int x1, int y1, int x2, int y2, COLORREF c)
{	// 获取绘图区宽度。
	int WIDTH = getwidth();	g_pBuf11 = GetImageBuffer();
	for (int i = y1; i < y1 + (y2 - y1); i++)
	for (int j = x1; j < x1 + (x2 - x1); j++)
	putpixealpha(g_pBuf11, j, i, c);
}

// 主函数
int main()
{
	// 创建绘图窗口
	initgraph(1000, 1000, 1);
	ExMessage m;
	// 全局缓冲区
	g_pBuf11 = GetImageBuffer();
	// 缓冲区获取。
	DWORD* g_pBuf = GetImageBuffer();
	// 设置透明度百分比为 50 也就是 128。
	setalpha(128);
	BeginBatchDraw();
	while (true)
	{	// 获取一条鼠标或按键消息
		peekmessage(&m, EX_MOUSE | EX_KEY, true);
		rec(100, 100, 200, 200, RED);
		rec(150, 150, 150 + 100, 150 + 100, GREEN);
		rec(200, 200, 200 + 100, 200 + 100, BLUE);
		rec(200 + 50, 200 + 50, 250 + 100, 250 + 100, WHITE);
		rec(300, 300, 300 + 100, 300 + 100, LIGHTMAGENTA);
		rec(250 + 100, 250 + 100, 350 + 100, 350 + 100, YELLOW);
		switch (m.message)
		{
		case WM_MOUSEMOVE:
			rec(m.x - 100, m.y - 100, m.x, m.y, WHITE);
			rec(m.x, m.y - 100, m.x + 100, m.y + 100, BLACK);
			rec(m.x, m.y, m.x + 100, m.y + 100, RED);
			rec(m.x - 100, m.y, m.x, m.y + 100, BLUE);
			break;
		}
		FlushBatchDraw();
		cleardevice();
	}
	EndBatchDraw();
	getmessage(EX_CHAR);
	closegraph();
	return 0;
}

5.缓冲区半透明绘制随机运动圆球源代码

最后一个程序,是半透明度为百分比为 50 的随机运动圆球(百分比 50 透明度,相当于 0~256 的一半也就是透明度为 128)。运动到最后,球会越来越少直到数量重置,球的运动轨迹是组成多边线的线上的所有坐标。
实例代码:

// EasyX 简单缓冲区半透明画随机运动圆,让你的绘图支持任意的透明度,达到让大家推波推润的效果。
// 日期:2023-4-7
// 作者:随波逐流
// 联系:2963787923@qq.com

#include <graphics.h>
#include <stdio.h>
#include <conio.h>
#include <math.h>
#include<stdlib.h>
#include<time.h>

// 初始换透明度为百分比 50 也就是 128。
int alpha = 128;
// 设置半透明度函数。
void setalpha(int al) { alpha = al; }
// 打开缓冲区。
DWORD* g_pBuf11;
// 进行前景色和背景色半透明计算。
COLORREF Alphargb1(int r1, int g1, int b1, int r2, int g2, int b2, int alp)
{	// 替换颜色。
	alp = alpha;
	// 半透明颜色混合计算算法。(村长修改的特殊算法,使用位移算法,能提高效率)。
	COLORREF rgb_alpha = RGB
	(
		(r1 * alp + r2 * (256 - alp)) >> 8,
		(g1 * alp + g2 * (256 - alp)) >> 8,
		(b1 * alp + b2 * (256 - alp)) >> 8
	);
	// 返回颜色。
	return rgb_alpha;
}
// 显示缓冲区指针,并读取坐标上的颜色值。
COLORREF fast_getpixelcolor(int x, int y, int WIDTH)
{
	COLORREF c = g_pBuf11[y * WIDTH + x]; return BGR(c);
}
// 快速缓冲区半透明画点函数 2
void putpixealpha(DWORD* g_pBuf, int x, int y, COLORREF c)
{
	int WIDTH = getwidth(), HEIGHT = getheight(), puti = y * WIDTH + x;
	// 如果 puti 超出缓冲区范围,直接返回。
	if (puti < 0 || puti >= (WIDTH * HEIGHT)) { return; }
	COLORREF backdropcolor, bkcolor = getbkcolor();
	// 读取背景上的颜色点。
	backdropcolor = fast_getpixelcolor(x, y, WIDTH);
	// 声明定义背景颜色和绘图的颜色。
	int r2 = GetRValue(c), g2 = GetGValue(c), b2 = GetBValue(c), r1 = GetRValue(backdropcolor), g1 = GetGValue(backdropcolor), b1 = GetBValue(backdropcolor);

	// 如果获取的颜色等于背景色不进行透明计算。
	if (bkcolor == backdropcolor)
	{
		g_pBuf[puti] = BGR(c);
	}
	// 透明颜色混合并输出。
	if (backdropcolor != bkcolor)
	{
		g_pBuf[puti] = BGR(Alphargb1(r2, g2, b2, r1, g1, b1, 10));
	}
}

// 基于 Bresenham 算法画半透明填充圆。
void FillCircle_alpha(int x, int y, int r, COLORREF color)
{
	int tx = 0, ty = r, d = 3 - 2 * r, i;

	while (tx < ty)
	{
		// 画水平两点连线(< 45 度)。
		for (i = x - ty; i <= x + ty; i++)
		{
			putpixealpha(g_pBuf11, i, y - tx, color);
			if (tx != 0)	// 防止水平线重复绘制
				putpixealpha(g_pBuf11, i, y + tx, color);
		}
		if (d < 0)			// 取上面的点。
			d += 4 * tx + 6;
		else				// 取下面的点。
		{
			// 画水平两点连线(> 45 度)。
			for (i = x - tx; i <= x + tx; i++)
			{
				putpixealpha(g_pBuf11, i, y - ty, color);
				putpixealpha(g_pBuf11, i, y + ty, color);
			}
			d += 4 * (tx - ty) + 10, ty--;
		}
		tx++;
	}
	if (tx == ty)			// 画水平两点连线(= 45 度)。
		for (i = x - ty; i <= x + ty; i++)
		{
			putpixealpha(g_pBuf11, i, y - tx, color);
			putpixealpha(g_pBuf11, i, y + tx, color);
		}
}
// 缓冲区画点。
void putpixelqBuffer(DWORD* pibu, int x, int y, int w, int h, COLORREF color)
{
	long zb = (h * y), zb1 = (w - x), ab2 = zb - zb1;
	pibu[abs(-ab2)] = BGR(WHITE);
}

// 使用中点算法画任意斜率的开放式直线(包括起始点,不包括终止点,同时返回该直线的所有 X_Y 坐标与线长度)。
int line_s1(int x1, int y1, int x2, int y2, int len[][2])
{
	int w1 = getwidth(), h1 = getheight(), len1 = 0;
	if (x1 >= w1 || x2 >= w1 || x1 <= 0 || x2 <= 0 || y1 >= h1 || y2 >= h1 || y1 <= 0 || y2 <= 0)return 0;

	DWORD* p = GetImageBuffer();
	int x = x1, y = y1;
	int a = y1 - y2, b = x2 - x1;
	int cx = (b >= 0 ? 1 : (b = -b, -1));
	int cy = (a <= 0 ? 1 : (a = -a, -1));
	int ii = 0, iii = 0;
	int d, d1, d2;

	if (-a <= b)
	{	// 斜率绝对值 <= 1。
		d = 2 * a + b;
		d1 = 2 * a;
		d2 = 2 * (a + b);

		while (x != x2)
		{
			putpixelqBuffer(p, len[ii][0] = x, len[ii][1] = y, w1, h1, RED);
			ii++;
			if (d < 0)
				y += cy, d += d2;
			else
				d += d1;
			x += cx;
		}
		return ii;
	}
	else {	// 斜率绝对值 > 1。
		d = 2 * b + a;
		d1 = 2 * b;
		d2 = 2 * (a + b);

		while (y != y2)
		{
			putpixelqBuffer(p, len[iii][0] = x, len[iii][1] = y, w1, h1, RED);
			iii++;
			if (d < 0)
				d += d1;
			else
				x += cx, d += d2;
			y += cy;
		}
		return iii;
	}
}
// 多做线 X_Y 容器。
int line_xy[6][3000][2] = { 0 };

// 开放式多段线绘图函数,通过指定 5 点坐标,返回 5 点坐标上的所有 X_Y 坐标与多段线长度。
int pl(int l, int p[], int line_xy1[][2])
{
	for (int g = 0; g < 6; g++)
		for (int i = 0; i < 3000; i++)
		{
			for (int j = 0; j < 2; j++)
			{
				line_xy[g][i][j] = 0;
			}
		}
	// tr 是奇数偶数开关
	int tr = 0;
	// 是五段线分别每一段直线的长度容器。
	int s[10][1] = { 0 };
	// 累加
	int iii = 0;
	// 累加

	// 特殊法配合奇偶开关绘制多段线
	for (int i = 0; i < l * 2; i++)
	{	// 奇偶开关
		if ((tr % 2) == 0)
		{
			// 核心绘制坐标点的线段并依次返回坐标与线长度
			s[iii][1] = line_s1(p[i], p[i + 1], p[i + 2], p[i + 3], line_xy[iii]);
			iii++;
			tr++;
		}
		else if ((tr % 2) != 0)
		{
			tr++;
		}
	}
	// 绘制末段线。
	s[4][1] = line_s1(p[l * 2 - 2], p[l * 2 - 1], p[0], p[1], line_xy[4]);

	// 把三维数组里的五段线 X_Y 坐标存入二级数组。
	int ss = 0;
	for (int g = 0; g < iii + 1; g++)
		for (int i = 0; i < s[g][1]; i++)
		{
			for (int j = 0; j < 2; j++)
			{
				line_xy1[ss][j] = line_xy[g][i][j];
			}
			ss++;
		}
	// 把五段线长度统一成完整的多线线长度。
	int ssg = 0;
	for (int i = 0; i < iii + 1; i++)
	{
		ssg = ssg + s[i][1];
	}
	// 返回多段线长度。
	return ssg;
}
struct linexy
{
	// 存放一条直线的绘制过程的 x_y 坐标二次使用。
	int line_xy2[3000][2] = { 0 }, line_xy3[3000][2] = { 0 }, line_xy4[3000][2] = { 0 };
	int line_xy5[3000][2] = { 0 }, line_xy6[3000][2] = { 0 }, line_xy7[3000][2] = { 0 };
	int line_xy8[3000][2] = { 0 }, line_xy9[3000][2] = { 0 }, line_xy10[3000][2] = { 0 };

}lixy;

// 主函数
int main()
{	// 随机种子。
	srand((unsigned)time(0));

	// 创建绘图窗口
	initgraph(800, 800);
	// 全局缓冲区
	g_pBuf11 = GetImageBuffer();
	// 缓冲区获取。
	DWORD* g_pBuf = GetImageBuffer();
	int siz = 85;

	// 容纳透明方格背景图片变量。
	IMAGE IM(800, 800);
	// 使用奇偶开关来绘制透明背景。
	for (int i = 0; i <= 100; i++)
	{
		for (int j = 0; j <= 100; j++)
		{
			// 奇偶开关,偶数绘制灰格了。
			if (((i + j) % 2) == 0)
			{
				// 绘制灰格。
				setfillcolor(RGB(204, 204, 204));
				fillrectangle(i * 8, j * 8, i * 8 + 8, j * 8 + 8);
			}
			// 奇偶开关,奇数绘制白格了。
			else if (((i + j) % 2) != 0)
			{
				// 绘制白格。
				setfillcolor(WHITE);
				fillrectangle(i * 8, j * 8, i * 8 + 8, j * 8 + 8);
			}
		}
	}
	getimage(&IM, 0, 0, 800, 800);

	// 设置透明度为百分比 50 也就是 128。
	setalpha(128);
	BeginBatchDraw();

	while (true)
	{
		// 设置半透明圆的随机运动轨迹坐标。
		int pts[10] = { 0 };
		for (int i = 0; i < 10; i++)
		{
			pts[i] = rand() % 800 + 1;
		}
		int s1 = pl(5, pts, lixy.line_xy2);

		int pts2[10] = { 0 };
		for (int i = 0; i < 10; i++)
		{
			pts2[i] = rand() % 800 + 1;
		}
		int s12 = pl(5, pts2, lixy.line_xy3);

		int pts3[10] = { 0 };
		for (int i = 0; i < 10; i++)
		{
			pts3[i] = rand() % 800 + 1;
		}
		int s13 = pl(5, pts3, lixy.line_xy4);

		int pts4[10] = { 0 };
		for (int i = 0; i < 10; i++)
		{
			pts4[i] = rand() % 800 + 1;
		}
		int s14 = pl(5, pts4, lixy.line_xy5);

		int pts5[10] = { 0 };
		for (int i = 0; i < 10; i++)
		{
			pts5[i] = rand() % 800 + 1;
		}
		int s15 = pl(5, pts5, lixy.line_xy6);

		int pts6[10] = { 0 };
		for (int i = 0; i < 10; i++)
		{
			pts6[i] = rand() % 800 + 1;
		}
		int s16 = pl(5, pts6, lixy.line_xy7);

		int pts7[10] = { 0 };
		for (int i = 0; i < 10; i++)
		{
			pts7[i] = rand() % 800 + 1;
		}
		int s17 = pl(5, pts7, lixy.line_xy8);

		int pts8[10] = { 0 };
		for (int i = 0; i < 10; i++)
		{
			pts8[i] = rand() % 800 + 1;
		}
		int s18 = pl(5, pts8, lixy.line_xy9);

		int pts9[10] = { 0 };
		for (int i = 0; i < 10; i++)
		{
			pts9[i] = rand() % 800 + 1;
		}
		int s19 = pl(5, pts9, lixy.line_xy10);

		// 绘制随机运动的半透明圆。同时也是正反交叠。
		for (int i = 0; i < 2000; i++)
		{
			putimage(0, 0, &IM);
			FillCircle_alpha(lixy.line_xy3[i][0], lixy.line_xy3[i][1], siz, WHITE);
			FillCircle_alpha(lixy.line_xy3[i][1], lixy.line_xy3[i][1], siz, LIGHTMAGENTA);

			FillCircle_alpha(lixy.line_xy2[i][1], lixy.line_xy2[i][1], siz, LIGHTBLUE);
			FillCircle_alpha(lixy.line_xy2[i][0], lixy.line_xy2[i][1], siz, RED);

			FillCircle_alpha(lixy.line_xy4[i][1], lixy.line_xy5[i][0], siz, LIGHTMAGENTA);
			FillCircle_alpha(lixy.line_xy4[i][0], lixy.line_xy4[i][1], siz, BLUE);

			FillCircle_alpha(lixy.line_xy4[i][1], lixy.line_xy4[i][0], siz, LIGHTRED);
			FillCircle_alpha(lixy.line_xy5[i][0], lixy.line_xy5[i][1], siz, GREEN);

			FillCircle_alpha(lixy.line_xy6[i][0], lixy.line_xy6[i][1], siz, BROWN);
			FillCircle_alpha(lixy.line_xy5[i][1], lixy.line_xy5[i][0], siz, CYAN);

			FillCircle_alpha(lixy.line_xy6[i][1], lixy.line_xy6[i][0], siz, BLACK);
			FillCircle_alpha(lixy.line_xy7[i][0], lixy.line_xy7[i][1], siz, LIGHTCYAN);

			FillCircle_alpha(lixy.line_xy7[i][1], lixy.line_xy7[i][0], siz, 0x2fb194);
			FillCircle_alpha(lixy.line_xy8[i][1], lixy.line_xy8[i][0], siz, 0x844cc7);

			FillCircle_alpha(lixy.line_xy9[i][1], lixy.line_xy9[i][0], siz, 0xffc81c);
			FillCircle_alpha(lixy.line_xy8[i][0], lixy.line_xy8[i][1], siz, YELLOW);

			FillCircle_alpha(lixy.line_xy10[i][1], lixy.line_xy10[i][0], siz, 0xff01c7);
			FillCircle_alpha(lixy.line_xy10[i][0], lixy.line_xy10[i][1], siz, MAGENTA);
			FillCircle_alpha(lixy.line_xy9[i][0], lixy.line_xy9[i][1], siz, DARKGRAY);

			FlushBatchDraw();

		}
	}

	EndBatchDraw();

	getmessage(EX_CHAR);
	closegraph();
	return 0;
}

6.参考资料

  1. 缓冲区画点参考技术文章:村长的缓冲区画点教程。
  2. 圆的是 Bresenham 算法。原技术文章:村长的画圆 Bresenham 算法。
  3. 开源 X_Y 坐标,直线算法为:村长的中心点直线算法修改而来。
  4. 半透明提高效率算法,技术指导来源于:村长的位移操作半透算法(0~256 半透明色值范围)。

7.指导老师:

  • 慢羊羊

添加评论