easyx官网例子初试

基础

easyx简介

在官网安装包里面有个easyx帮助文档:
在这里插入图片描述
项目中使用的也是静态链接的方式:
在这里插入图片描述

透明贴图和三元光栅操作

当我们想在窗口上显示一个人物的时候,我们当然是希望让他融入到背景中去,这个时候我们就要用到透明贴图或三元光栅操作了。
文章链接https://codeabc.cn/yangw/post/transparent-putimage

除了文章里面的方法,常用的方法还有XOR 运算,它有一个重要的特性:(a ^ b) ^ b = a。
假如 a 是背景图案,b 是将要绘制的图案,只要用 XOR 方式绘图,连续绘两次,那么背景是不变的。

#include <graphics.h>
#include <conio.h>

int main()
{
	initgraph(640, 480);				// 初始化 640 x 480 的绘图窗口

	setlinestyle(PS_SOLID, 10);			// 设置线宽为 10,这样效果明显
	setlinecolor(0x08BAFF);				// 设置画线颜色为黄色
	rectangle(100, 100, 200, 200);		// 画一个黄色矩形框,当做背景图案

	setwritemode(R2_XORPEN);			// 设置 XOR 绘图模式
	setlinecolor(0x2553F3);				// 设置画线颜色为红色

	line(50, 0, 200, 300);				// 画红色线
	_getch();							// 等待按任意键
	line(50, 0, 200, 300);				// 画红色线(XOR 方式重复画线会恢复背景图案)
	_getch();							// 等待按任意键

	closegraph();						// 关闭绘图窗口
	return 0;
}

在这里插入图片描述
在这里插入图片描述

精确延时

Sleep() 这个延时有个缺点,那就是无法统计代码执行的时间。
在这里插入图片描述
毫秒延时:

void HpSleep(int ms)
{
	static clock_t oldclock = clock();		// 静态变量,记录上一次 tick

	oldclock += ms * CLOCKS_PER_SEC / 1000;	// 更新 tick

	if (clock() > oldclock)					// 如果已经超时,无需延时
		oldclock = clock();
	else
		while(clock() < oldclock)			// 延时
			Sleep(1);						// 释放 CPU 控制权,降低 CPU 占用率
}

微妙延时:

#include <windows.h>

class MMTimer
{
private:
	static LARGE_INTEGER m_clk;			// 保存时钟信息
	static LONGLONG m_oldclk;			// 保存开始时钟和结束时钟
	static int m_freq;					// 时钟频率(时钟时间换算率),时间差

public:
	static void Sleep(int ms);
};

LARGE_INTEGER MMTimer::m_clk;
LONGLONG MMTimer::m_oldclk;
int MMTimer::m_freq = 0;

// 延时
void MMTimer::Sleep(int ms)
{
	if (m_oldclk == 0)
	{
		QueryPerformanceFrequency(&m_clk);
		m_freq = (int)m_clk.QuadPart / 1000;	// 获得计数器的时钟频率

		// 开始计时
		QueryPerformanceCounter(&m_clk);
		m_oldclk = m_clk.QuadPart;				// 获得开始时钟
	}

	unsigned int c = ms * m_freq;

	m_oldclk += c;

	QueryPerformanceCounter(&m_clk);

	if (m_clk.QuadPart > m_oldclk)
	{	
		m_oldclk = m_clk.QuadPart;
	}
	else
	{
		do
		{
			::Sleep(1);
			QueryPerformanceCounter(&m_clk);	// 获得终止时钟
		}
		while(m_clk.QuadPart < m_oldclk);
	}
}

程序

自由运动的点

官网例子链接:https://codebus.cn/yangw/a/free-dots

效果:
在这里插入图片描述
实现思路:随机初始化64个点(随机包括初始坐标、xy方向的速度和颜色),进入死循环,每次循环移动点(如果越界必定改变方向,没越界有百分之十几率改变方向,当然是随机方向)并且绘制移动前后的连线(因为比较短所以连起来看是曲线),最后有个全屏模糊并且延时33ms,进入下次循环。

主要代码:
全屏模糊:除了屏幕第一行和最后一行每个像素点都和上下左右四个像素点取均值,因为背景是黑色的,而且背景占了大部分,所以就有”曲线“慢慢变黑的效果。

// 全屏模糊处理
// (为了简化范例,该函数略去了屏幕第一行和最后一行的处理)
void Blur(DWORD* pMem)
{
	for (int i = 640; i < 640 * 479; ++i)
	{
		pMem[i] = RGB(
			(GetRValue(pMem[i]) + GetRValue(pMem[i - 640]) + GetRValue(pMem[i - 1]) + GetRValue(pMem[i + 1]) + GetRValue(pMem[i + 640])) / 5,
			(GetGValue(pMem[i]) + GetGValue(pMem[i - 640]) + GetGValue(pMem[i - 1]) + GetGValue(pMem[i + 1]) + GetGValue(pMem[i + 640])) / 5,
			(GetBValue(pMem[i]) + GetBValue(pMem[i - 640]) + GetBValue(pMem[i - 1]) + GetBValue(pMem[i + 1]) + GetBValue(pMem[i + 640])) / 5);
	}
}

冰封的EasyX

官网例子链接:https://codebus.cn/yangw/a/word-art-freeze

效果:
在这里插入图片描述
实现思路:首先在IMAGE对象上绘制一个白色的EasyX,遍历图片所有点,统计白色点个数记录在g_nCount,再次遍历图片所有点copy白色点记录在g_pDst(动态分配内存new POINT[g_nCount]),随机获取g_nCount个坐标点记录在g_pSrc(动态分配内存new POINT[g_nCount]),进入循环绘制点(一共绘制128次,每次都做和自由运动的点一样的模糊处理,g_nCount个点都是从g_pSrc位置慢慢朝着g_pDst靠拢的)。

主要代码:
循环绘制点

// 运算
int x, y;
for (int i = 2; i <= 256; i += 2)
{
	COLORREF c = RGB(i - 1, i - 1, i - 1);
	Blur(pBuf);						// 全屏模糊处理

	for (int d = 0; d < g_nCount; ++d)
	{
		x = g_pSrc[d].x + (g_pDst[d].x - g_pSrc[d].x) * i / 256;
		y = g_pSrc[d].y + (g_pDst[d].y - g_pSrc[d].y) * i / 256;
		pBuf[y * 640 + x] = c;		// 直接操作显存画点
	}

	FlushBatchDraw();				// 使显存操作生效
	Sleep(20);						// 延时
}

火焰效果

官网例子链接:https://codebus.cn/yangw/a/fire

效果:
在这里插入图片描述
实现思路:首先右边和下边的渐变绘制是通过裁剪区域实现,燃烧的火焰实现主要为设置火源g_Fire,火焰燃烧上升(和上面程序Blur思想类似,把取平均值换成减去随机值,直到等于0消失)。

主要代码:
CreatePolygonRgn创建一个长方形裁剪区,CreateEllipticRgn创建一个椭圆裁剪区,CombineRgn合并裁剪区。然后从左上角朝着各个方向画线。
取消裁剪区运行效果,对比一下就能看出裁剪区形状了:
在这里插入图片描述

// 画色系
void DrawColorScheme()
{
	POINT s[8] = { {0, 450}, {580, 450}, {580, 420}, {610, 420},
				{610, 0}, {639, 0},	{639, 479}, {0, 479} };

	HRGN rgn1 = CreatePolygonRgn(s, 8, WINDING);
	HRGN rgn2 = CreateEllipticRgn(550, 390, 611, 451);
	CombineRgn(rgn1, rgn1, rgn2, RGN_DIFF);

	// 定义裁剪区域
	setcliprgn(rgn1);			// 设置区域 rgn 为裁剪区
	DeleteObject(rgn1);
	DeleteObject(rgn2);

	// 画色系
	int c, x, y;
	for (int i = 0; i < 1120; ++i)
	{
		c = int(i / 5.8);
		x = (i <= 479 ? 639 : 639 - i + 479);
		y = (i <= 479 ? i : 479);
		setcolor(BGR(g_Colors[c]));
		line(0, 0, x, y);
	}

	// 取消裁剪区域
	setcliprgn(NULL);
}

设置火源
因为一开始设置火源全为0,所以通过m_nFlammability越小越小机会设置火源,所以算控制了火焰的易燃性。

int m_nFlammability = 385;	// 易燃性,范围 [1, 399],默认 385
void InitFire()
{
	。。。
	memset(g_Fire, 0, FIREWIDTH);
	。。。
}
void RenderFlame()
{
	。。。
	// 火焰燃烧速度控制
	if (rand() % (400 - m_nFlammability) == 0)
	{
		int off = m_nSize - 5;
		off = rand() % off;
		off += xStart;
	
		// 设置热源
		for (int x = off; x < off + 5; ++x)
		{
			g_Fire[x] = 239;
		}
	}
	。。。
}

上面设置火源g_Fire[x] = 239;其实已经超过最高色系了,所以控制火焰的温度在m_nMaxHeat内,并且通过火焰混沌(模糊程度)和传播速率控制火源的色系。

int m_nMaxHeat = 192;		// 最高热度,范围 [0, 192],默认 192
int m_nChaos = 50;			// 混沌,范围 [1, 100],默认 50
int m_nSpreadRate = 20;		// 传播速率,范围 [1, 100],默认 20

void RenderFlame()
{
	。。。
	for (int x = xStart; x < xEnd; ++x)
	{
		if (g_Fire[x] < m_nMaxHeat)
		{
			int val = rand() % m_nChaos + 1;
			val -= m_nChaos / 2;
			val += m_nSpreadRate;
			val += g_Fire[x];

			if (val > m_nMaxHeat)
			{
				g_Fire[x] = m_nMaxHeat;
			}
			else if (val < 0)
			{
				g_Fire[x] = 0;
			}
			else
			{
				g_Fire[x] = val;
			}
		}
		else
		{
			g_Fire[x] = m_nMaxHeat;
		}
	}
	。。。
}

通过上面的混沌和传播速率的计算火源已经有点差异了,通过m_nSmoothness值来滑度火焰,也就是所有的火源点都与周围的点(一共2 * m_nSmoothness + 1个点)取均值。

int m_nSmoothness = 1;		// 平滑度,范围 [0, 5],默认 1

void RenderFlame()
{
	。。。
	// 平滑火焰
	if (m_nSmoothness > 0)
	{
		xStart += m_nSmoothness;
		xEnd -= m_nSmoothness;
		for (int x = xStart; x < xEnd; ++x)
		{
			int val = 0;
			for (int y = x - m_nSmoothness; y < x + 1 + m_nSmoothness; ++y)
			{
				val += g_Fire[y];
			}

			// 和周围取均值
			g_Fire[x] = val / (2 * m_nSmoothness + 1);
		}
	}
}

火焰燃烧
首先通过m_nDistribution来控制火焰的范围,超过范围的火源直接设置为0,并且燃烧的最下面一行直接就是火源的温度。

int m_nDistribution = 1;	// 分布,范围 [0, 10],默认 1

void RenderFlame()
{
	int xStart = (FIREWIDTH - m_nSize) / 2,
		xEnd = xStart + m_nSize + 1;	// 火焰开始位置(最左边)和结束位置(最右边)

	BYTE *pRow = g_Bits;
	BYTE *pNextRow;

	// 在火焰分布外的地方都设置黑色
	for (int x = 0; x < FIREWIDTH; ++x)
	{
		if (x < (xStart + m_nDistribution) || x >= (xEnd - m_nDistribution))
		{
			g_Fire[x] = 0;
		}
		*pRow++ = g_Fire[x];
	}
	。。。
}

通过m_nDecay来控制火焰变小的速率,如果为1能烧很高很高,BurnPoint在计算火焰燃烧的时候会左右偏移一点,然后适当衰弱下面一行pNextRow的火焰给这一行pRow,所以火焰从下面烧到上面最终会烧没。
关于火焰的燃烧方向:假如从左往右遍历燃烧,那么左边的随机小幅度往右偏移最大的点就不会再改变了,而右边的随机小幅度往左偏移最大的点还可能因为更加右边没有火源的点影响而消失,从下往上累计下来往左边的火越来越多,往右边的火越来越少。
在这里插入图片描述

int m_nDecay = 5;			// 衰减度,范围 [1, 100],默认 5

// 计算火焰的每个点
inline void BurnPoint(BYTE* pRow, BYTE* pNextRow)
{
	// 随机小幅度往左右偏移
	int off = rand() % (m_nDistribution + 1);
	BYTE *pTarget = (rand() & 1) ? pRow + off : pRow - off;

	// 火焰衰减(必须大于等于0)
	int val = m_nDecay + 1;
	val = rand() % val;
	val = *pNextRow - val;
	*pTarget = (val > 0) ? (BYTE)val : 0;
}

void RenderFlame()
{
	。。。
	for (int y = FIREHEIGHT - 1; y > 0; --y)	// y = 0 火焰最大;y++ 火焰变小
	{
		pRow = (g_Bits + FIREWIDTH * y);
		pNextRow = (g_Bits + FIREWIDTH * (y - 1));

		if (rand() & 1)	// 火焰大体从右往左燃烧
		{
			for (int x = 0; x < FIREWIDTH; ++x)
			{
				BurnPoint(pRow, pNextRow);
				++pRow;
				++pNextRow;
			}
		}
		else	// 火焰大体从左往右燃烧
		{
			pRow += FIREWIDTH - 1;
			pNextRow += FIREWIDTH - 1;
			for (int x = 0; x < FIREWIDTH; ++x)
			{
				BurnPoint(pRow, pNextRow);
				--pRow;
				--pNextRow;
			}
		}
	}
	。。。
}

烟花

官网例子链接:https://codebus.cn/xiongfj/post/firework

效果:
在这里插入图片描述
实现思路:这个烟花是一张静态的图片,不过显示的时候是一圈一圈从里到外慢慢显示的,而这个慢慢消失的效果是直接对窗口显存进行操作,每次迭代随机选择4000个像素点进行擦除,也就是赋值成黑色。

主要代码:
随机选择 4000 个像素点擦除

for (int clr = 0; clr < 1000; clr++)
{
	for (int j = 0; j < 2; j++)
	{
		int px1 = rand() % 1200;
		int py1 = rand() % 800;

		if (py1 < 799)				// 防止越界
		{
			pMem[py1 * 1200 + px1] = pMem[py1 * 1200 + px1 + 1] = BLACK;	// 对显存赋值擦出像素点
		}
	}
}

第一个putimage可以去除上一次烟花弹显示的痕迹, 使用了XOR 方式绘图,而且被擦去的像素点又会显示出来,形成了流星尾的痕迹。

putimage(Jet[i].x, Jet[i].y, &Jet[i].img[Jet[i].n], SRCINVERT);

if (Jet[i].y > Jet[i].hy)
{
	Jet[i].n++;
	Jet[i].y -= 5;
}

putimage(Jet[i].x, Jet[i].y, &Jet[i].img[Jet[i].n], SRCINVERT);

如果该号炮花可爆炸,根据当前爆炸半径画烟花,颜色值接近黑色的不输出。

if (Fire[i].draw)
{
	for (double a = 0; a <= 6.28; a += 0.01)
	{
		int x1 = (int)(Fire[i].cen_x + Fire[i].r * cos(a));				// 相对于图片左上角的坐标
		int y1 = (int)(Fire[i].cen_y - Fire[i].r * sin(a));

		if (x1 > 0 && x1 < Fire[i].width && y1 > 0 && y1 < Fire[i].height)	// 只输出图片内的像素点
		{
			int b = Fire[i].xy[x1][y1] & 0xff;
			int g = (Fire[i].xy[x1][y1] >> 8) & 0xff;
			int r = (Fire[i].xy[x1][y1] >> 16);

			// 烟花像素点在窗口上的坐标
			int xx = (int)(Fire[i].x + Fire[i].r * cos(a));
			int yy = (int)(Fire[i].y - Fire[i].r * sin(a));

			// 较暗的像素点不输出、防止越界
			if (r > 0x20 && g > 0x20 && b > 0x20 && xx > 0 && xx < 1200 && yy > 0 && yy < 800)
			{
				pMem[yy * 1200 + xx] = BGR(Fire[i].xy[x1][y1]);	// 显存操作绘制烟花
			}
		}
	}
	Fire[i].draw = false;
}

水波

官网例子链接:https://codebus.cn/contributor/post/hao-water-ripple-effect
效果:
在这里插入图片描述
实现思路:没有任何波纹的平面水面,则我们透过一个平面的玻璃去看水下的景物,将产生一副任何偏移的水底景物的画面。而较小的水波产生较轻微扭曲的画面,较大的水波产生扭曲更大的画面。因此,我们可以通过像素的偏移大小来模拟水波视觉。

代码实现:
定义两个与水池图象一样大小的数组用来保存水面上每一个点的前一时刻和后一时刻波幅数据,因为波幅也就代表了波的能量,所以称这两个数组为波能缓冲区。

// 以下两个 buf 为每一个点的波幅,前者为当前波幅,后者为下一个时刻的波幅。
short *buf = new short[PIC_HEIGHT*PIC_WIDTH + PIC_WIDTH];
short *buf2 = new short[PIC_HEIGHT*PIC_WIDTH + PIC_WIDTH];

水面在初始状态时是一个平面,各点的波幅都为0,所以,这两个数组的初始值都等于0。

// 初始化波幅数组
memset(buf, 0, (PIC_HEIGHT*PIC_WIDTH + PIC_WIDTH) * sizeof(short));
memset(buf2, 0, (PIC_HEIGHT*PIC_WIDTH + PIC_WIDTH) * sizeof(short));

我们假设存在这样一个一次公式,可以在任意时刻根据某一个点周围前、后、左、右四个点以及该点自身的振幅来推算出下一时刻该点的振幅,那么,我们就有可能用归纳法求出任意时刻这个水面上任意一点的振幅。
某一时刻,X0点的振幅除了受X0点自身振幅的影响外,同时受来自它周围前、后、左、右四个点(X1、X2、X3、X4)的影响(为了简化,我们忽略了其它所有点),而且,这四个点对a0点的影响力可以说是机会均等的。那么我们可以假设这个一次公式为:X0’=a(X1+X2+X3+X4)+bX0 (公式1)
a、b为待定系数,X0’为0点下一时刻的振幅
X0、X1、X2、X3、X4为当前时刻的振幅

假设水的阻尼为0。在这种理想条件下,水的总势能将保持不变。也就是说在任何时刻,所有点的振幅的和保持不变。那么可以得到下面这个公式:
X0’+X1’+…+Xn’ = X0+X1+…+Xn

将每一个点都象公式1那样计算,然后代入上式,得到:
(4a+b)X0+(4a+b)X1+…(4a+b)Xn = X0+X1+…+Xn =>4a+b=1

找出一个最简解:a = 1/2、b = -1
因为1/2可以用移位运算符“>>”来进行,不用进行乘除法,所以,这组解是最适用的而且是最快的。那么最后得到的公式就是:
X0’=(X1+X2+X3+X4)/ 2- X0

水在实际中是存在阻尼的,否则,用上面这个公式,一旦你在水中增加一个波源,水面将永不停止的震荡下去。所以,还需要对波幅数据进行衰减处理,让每一个点在经过一次计算后,波幅都比理想值按一定的比例降低。这个衰减率经过测试,用1/32比较合适,也就是1/2^5。可以通过移位运算很快的获得。

// 计算出下一个时刻所有点的波幅
void nextFrame()
{
	for (int i = PIC_WIDTH; i < PIC_HEIGHT*(PIC_WIDTH - 1); i++)
	{
		// 公式:X0'= (X1+X2+X3+X4) / 2 - X0
		buf2[i] = ((buf[i - PIC_WIDTH] + buf[i + PIC_WIDTH] + buf[i - 1] + buf[i + 1]) >> 1) - buf2[i];

		// 波能衰减
		buf2[i] -= buf2[i] >> 5;
	}

	// 交换两个指针所指的空间
	short *ptmp = buf;
	buf = buf2;
	buf2 = ptmp;
}

因为水面越倾斜,所看到的水下景物偏移量就越大,所以,波幅越大像素点偏移也就越大。

// 处理当前时刻波幅影响之后的位图,保存在 dest_img 中
void RenderRipple()
{
	int i = 0;
	for (int y = 0; y < PIC_HEIGHT; y++)
	{
		for (int x = 0; x < PIC_WIDTH; x++)
		{
			short data = 1024 - buf[i];

			// 偏移
			int a = ((x - PIC_WIDTH / 2) * data / 1024) + PIC_WIDTH / 2;
			int b = ((y - PIC_HEIGHT / 2) * data / 1024) + PIC_HEIGHT / 2;

			// 边界处理
			if (a >= PIC_WIDTH)		a = PIC_WIDTH - 1;
			if (a < 0)				a = 0;
			if (b >= PIC_HEIGHT)	b = PIC_HEIGHT - 1;
			if (b < 0)				b = 0;

			// 处理偏移 
			img_ptr2[i] = img_ptr1[a + (b * PIC_WIDTH)];
			i++;
		}
	}
}

最后就是产生水波了,以石头入水中心点为圆心,画一个以石头半径为半径的圆,圆内所以的点都产生一个波幅。

// 鼠标模拟投石头
// 参数说明:
// (x, y): 鼠标坐标
// stonesize: “石头”的大小
// stoneweight: 投“石头”的力度
// Ps: 如果产生错误,一般就是数组越界所致,请酌情调整“石头”的大小和“石头”的力度
void disturb(int x, int y, int stonesize, int stoneweight)
{
	// 突破边界不处理
	if ((x >= PIC_WIDTH - stonesize) ||
		(x < stonesize) ||
		(y >= PIC_HEIGHT - stonesize) ||
		(y < stonesize))
	{
		return;
	}

	for (int posx = x - stonesize; posx < x + stonesize; posx++)
	{
		for (int posy = y - stonesize; posy < y + stonesize; posy++)
		{
			if ((posx - x)*(posx - x) + (posy - y)*(posy - y) < stonesize*stonesize)
			{
				buf[PIC_WIDTH*posy + posx] += stoneweight;
			}
		}
	}
}

迷宫

官网例子链接:https://codebus.cn/zhaoh/maze-game
效果:
在这里插入图片描述
生成迷宫算法:

// 生成迷宫:初始化(注:宽高必须是奇数)
void MakeMaze(int width, int height)
{
	if (width % 2 != 1 || height % 2 != 1)
	{
		return;
	}

	// 定义迷宫尺寸,并分配迷宫内存
	g_aryMap = new BYTE*[width + 2];
	for (int x = 0; x < width + 2; ++x)
	{
		g_aryMap[x] = new BYTE[height + 2];
		memset(g_aryMap[x], MAP_WALL, height + 2);
	}

	// 定义边界
	for (int x = 0; x <= width + 1; ++x)
	{
		g_aryMap[x][0] = g_aryMap[x][height + 1] = MAP_GROUND;
	}

	for (int y = 1; y <= height; ++y)
	{
		g_aryMap[0][y] = g_aryMap[width + 1][y] = MAP_GROUND;
	}

	// 定义入口和出口
	g_aryMap[1][2] = MAP_ENTRANCE;
	g_aryMap[width][height - 1] = MAP_EXIT;

	// 从任意点开始遍历生成迷宫
	TravelMaze(((rand() % (width - 1)) & 0xfffe) + 2, ((rand() % (height - 1)) & 0xfffe) + 2);

	// 将边界标记为迷宫外
	for (int x = 0; x <= width + 1; ++x)
	{
		g_aryMap[x][0] = g_aryMap[x][height + 1] = MAP_OUTSIDE;
	}

	for (int y = 1; y <= height; ++y)
	{
		g_aryMap[0][y] = g_aryMap[width + 1][y] = MAP_OUTSIDE;
	}
}


// 生成迷宫:遍历 (x, y) 四周
void TravelMaze(int x, int y)
{
	// 定义遍历方向
	int d[4][2] = { 0, 1, 1, 0, 0, -1, -1, 0 };

	// 将遍历方向乱序
	int n, t;
	for (int i = 0; i < 4; ++i)
	{
		n = rand() % 4;
		t = d[i][0], d[i][0] = d[n][0], d[n][0] = t;
		t = d[i][1], d[i][1] = d[n][1], d[n][1] = t;
	}

	// 尝试周围四个方向
	g_aryMap[x][y] = MAP_GROUND;
	for (int i = 0; i < 4; ++i)
	{
		if (g_aryMap[x + 2 * d[i][0]][y + 2 * d[i][1]] == MAP_WALL)
		{
			g_aryMap[x + d[i][0]][y + d[i][1]] = MAP_GROUND;
			TravelMaze(x + d[i][0] * 2, y + d[i][1] * 2);		// 递归
		}
	}
}

宽高为什么必须要是基数呢,因为网上大部分迷宫生成算法生成的迷宫墙壁都是线条,这里生成的迷宫墙壁是正方形,为了保证一行墙壁一行通路,大小只能是奇数。
一开始所有地图都是墙,随机选择一点开始生成路,先乱序生成若干个方向(数组d大小是4为什么不说是四个方向,因为有重复的方向),然后各个方向尝试开路,每次走出去2个单位,直到所有的方向都走到了边界。

纪念披头士摇滚乐队

官网例子链接:https://codebus.cn/zhaoh/miss-the-beatles
效果:
在这里插入图片描述

歌词一共520(凑得真好)个字符,先计算每个字符的初始位置(按照字符宽度算的),然后解码字符的终止坐标:

// 定义字符元素结构体
struct ELEMENT
{
	TCHAR c;
	int  x, y;		// 初始坐标(会改变)
	int  xo, yo;	// 初始坐标(不改变)
	int  xd, yd;	// 终止坐标(不改变)
};

char	g_data[]					// 字符坐标数据
= "Q;PQD0R1F1CMF=;I73F5=QP2<OQUN0=@49CA4KK54BOQM?BW;46G1CETWSVGB;QC0EOISUKVRF3EHL44C2T?RESL17KP:GA6@@MM6P@L59I1MA;ME?O58MKLT3CSNS6PD<:KRSCORFU8WLAURN=P:03CFP3Q>WUHCWNAOP:8LF77E43BL4S?UEHNW?27UGBFJ2CC0WNI9;G?P1;G0GL35WRTMSBVB:@LJI>I03=J:RF1>>V3U?M3G=U7BF;GQ=E4=8RFBU23IMQ>GH1PINK:7D:JWC0UEPM9L>MR<SS7BL3B2LOSGW1;UI4MN:U>A6QRF?;>C7TLCU89CP?92A2U7UQCOEK<9A1S9W@7>AL562GTI7OF9?IE2UQPNSVNFK=PML1LNKFB8A32T4QOJ9W=QK?CEP6VA;UMQTA5K35W64MO25V3E5G7QC3BC=S0SB=A2GAO7VBTF8JF?BKLGQ8WPD>RECRPWSP9BPVKA2O56N?Q7DOO=3RDW;W53;QORWIF1:L22KV5SKV4=;@3=6W@0ECMJTBODP3HL=8TUMD2N1CNFSFR71D=PE>O?IN3EA5IM6HPSP@>U2S@OBG81NR551533394<K5TMTD076=;@N<C9MV>VATGMVBCR>TSKU5NGIQVES?;UD:URR7T?1E8GEAIUGS0O>PVS1GSWTP@SLLKKC?S3@34EUK<9HQ9J90:VGGI<Q6JIAG37UF32874DW3WC7>C82GGMI=?UOOOPL99:8H;>IBMLC?N3:W>4RRTR6MVA0NNEHSIFN6S?CTHGBA8A25Q;=R5@E6IT=;MADSN55909<UL>94MOWR4TIU59NGM?L4AJVN4LJG@1RRLO0CR>33<94>KE6Q5AI8V==9;T5CNU2F;O0O3GEKNISQQER0?U8@X8O1RUM7U@M8N9TQ=>UL=TQ=<ICG;JF6K5GH3<U4874M6D2QGP=PMQ>G26GESGGFC=GF3POOMNONJ3QCGT<WTRI6766;7F>H4=0ULE6;A@DRNK17D1RTD4FVTPO5WA6DVK1CVI7Q@Q?PPCQCD>QEH;FQVQ9OBQLHI?4R9P>QM3KJSDPN<N7FME6I:5T;7TE5889<RM02VMCIISGMG;0AQD:8A@MDFOCQMOC;G@;OWKK5CN66Q=GE=BTDTNIIR;M16QE3OOEW>2M87>RTL@2P:=JU6RPP@5T?>GMGASRQSQVVGS6>2=GB?RQ=JJ7AHFV@FD:NNVDPGE?<CTVCHIGP=IGKQM77H<HQ@A?2<U85FWOEN3I<1S3N1BI5P1HIQEHDHJ3QVNPMIMG@S:GVADRN5W<@JEDDHD3<KJUO28UQW>T4@J15PLURMQSER><G8GA;WGW<H75;40222J1L1W57EJ7QO5WD4D16:85DHNQN7:LFMD=>UB=K>PP3S8HJ7HVPP>;SANMTHVO0O0HBOP5M1ABQ73GE@O5@6P6PU9WRHQQ3W;F6A@ECVOWI1S5E35@<?M728A<<EPB:H:TODGAEJ41IUCUNTATBFU5TGE6=762>=P2KS97O89U2:U>OLB91J>FF>2MWV8P;1F<R1";

// 初始化每个字符元素
void init()
{
	。。。
	// 解码每个字符元素的终止坐标
	for (int n = 0; n < 520; n++)
	{
		int p = g_data[n * 3] * 1600 + g_data[n * 3 + 1] * 40 + g_data[n * 3 + 2] - 78768;
		g_Element[n].yd = p % 284;
		g_Element[n].xd = (p - g_Element[n].yd) / 284;
		g_Element[n].yd += 74 + (HEIGHT - 470) / 2;
		g_Element[n].xd += 111 + (WIDTH - 450) / 2;
	}
	。。。
}

这边的最终坐标g_data不是乱码,每三个字符解码出来是一个(x,y)坐标
g_Element[n].yd += 74 + (HEIGHT - 470) / 2;g_Element[n].xd += 111 + (WIDTH - 450) / 2;这两行是设置外边距让最终图像居中的
前面三行对应关系p = 284 * x + yp = g_data[n * 3] * 1600 + g_data[n * 3 + 1] * 40 + g_data[n * 3 + 2] - 78768
我们先代个试试Q=81;=59P=80,所以p = 1600 * 81 + 40 * 59 + 80 - 78768,所以算出来x=187y=164
正着算可以算出来,那这个式子又为什么要这么写呢,通过观察发现g_data都是可显示字符,区间为下图红色部分(用程序算了一下):
在这里插入图片描述
也就是说编码区间在[48,88],所以当我们知道了xy坐标,就能算出p的值,通过式子p = d1 * 1600 + d2 * 40 + d3 - 78768和取值约束就能算出d1d2d3的值,也就得到了编码。

动画部分:

void ani()
{
	int i, m, n;

	// 计算需要运动的字符元素
	for (n = 0; n < 130; n++)
	{
		m = 519 - n;
		if ((an - n >= 0) && (an - n <= 30))
		{
			double b = (cos((an - n) * PI / 30) + 1) / 2;
			double a = 1 - b;
			for (i = 0; i <= 130; i += 130)
			{
				g_Element[n + i].x = int((g_Element[n + i].xd) * a + g_Element[n + i].xo * b + 0.5);
				g_Element[n + i].y = int((g_Element[n + i].yd) * a + g_Element[n + i].yo * b + 0.5);
				g_Element[m - i].x = int((g_Element[m - i].xd) * a + g_Element[m - i].xo * b + 0.5);
				g_Element[m - i].y = int((g_Element[m - i].yd) * a + g_Element[m - i].yo * b + 0.5);
			}
		}
	}

	// 显示全部字符元素
	for (i = 0; i < 520; i++)
	{
		outtextxy(g_Element[i].x, g_Element[i].y, g_Element[i].c);
	}

	an += dir;

	if ((an < 0) || (an > 160))
	{
		bAni = false;
	}
}

每刷新一次,n计算的是前面260个字符,m计算的是后面260个字符,一次动画字符移动三十次,从起始位置到终止位置,速度不是线性的,因为有三角函数,求导(类似sin函数第一区间[0,π])算出来速度应该是慢–快--慢变化的。

  • 10
    点赞
  • 53
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值