注意,图中由于录制原因,中心部分的像素太小,导致GIF中看不到了
typedef struct
{
int x; //创建粒子时的初始坐标
int y;
double direc; //粒子的移动方向,弧度制
float speed; //粒子的移动速度,或者逃逸速度
double curDis; //当前粒子距离初始坐标的距离
bool live; //粒子是否是存活状态
double curR; //当前粒子的半径
double addR; //半径的递增大小
int r; //粒子的RGB颜色值
int b;
int g;
}Light;
1.怎么根据中心点,距离,方向,计算出新坐标
2.新方向怎么计算
//Light mLight[200];
//用一个数组来保存粒子,这样我们的最大显示粒子数量就是200,
//在显示的时候,检查粒子的存活状态进行显示
//在满足死亡条件的时候设置为死亡状态,不在进行显示
int px = mLight[i].x + mLight[i].curDis*cos(mLight[i].direc);
int py = mLight[i].y + mLight[i].curDis*sin(mLight[i].direc);
//以上,就是通过初始坐标,半径,方向,计算出目标坐标
//简单来说:新X=源X+距离*cos(方向); 也就是:新X=源X+X增量
//类似的,新的Y坐标也是一样的计算,不过注意,使用的是sin()计算
const double PI=3.141592653549;
mLight[i].direc = (((rand() % 30) / 180.0*PI) + mLight[i].direc)*0.3 + mLight[i].direc*0.7;
//简单来说:
//新方向=源方向+偏转方向
//看起来不像对吧,这样就像了:
//新方向=(源方向+偏转方向)*新方向比例+源方向*源方向比例
//1.0=新方向比例+源方向比例
//偏转方向=偏转角/180.0*PI; 其实就是等比例计算,偏转方向:PI=偏转角:180
//偏转角=rand()%偏转范围; 使用了随机范围内的一个偏转角度
if (mLight[i].direc > 3.141592653549*2)
mLight[i].direc -= 3.141592653549*2;
//另外再处理一下,角度保持在2*PI范围内,
//虽然大于2*PI也能进行正常计算,但是总感觉...
//程序中,使用的都是弧度制
//弧度=角度/180.0*PI;
//角度=弧度/PI*180.0;
代码完整,如果需要,可以直接复制,创建控制台程序即可(前提是安装了EasyX图形库),使用Win32或者MFC的话,直接在合适的地方初始化,在绘制(WM_PAINT/WM_TIMER/OnDraw())里面调用即可
#pragma once
#include
#include
#include
#include
class StarVortex
{
public:
//这个结构前面介绍过了,就不重复了
typedef struct
{
int x;
int y;
double direc;
float speed;
double curDis;
bool live;
double curR;//其实这两个域是没用的,因为后面绘制时是直接计算出来的
double addR;//
int r;
int b;
int g;
}Light;
//三个构造,使用本类进行绘制,必须指定绘图的宽度,高度,粒子数量
//但是,默认宽高度和粒子数量也是有的,但是那样的话,绘制将可能得不到你想要的结果
StarVortex();
StarVortex(int width, int height, int count);
~StarVortex();
//设置完宽高度粒子数量之后,一定要调用的方法
//这个方法必须在绘制之前调用,它负责初始化粒子数组的内容
void InitLight();
//调用绘制,给定HDC即可
//每次绘制会自动进行粒子的移动和死亡处理等
//详细使用步骤请看主函数
void DrawBackground(HDC hdc);
//参数设置,和构造一样
void SetMaxCount(int count);
void SetWindowSize(int width, int height);
private:
//前面说了,是有默认参数的,因此通过它统一设置
void SetDefaultArgument();
//创建出新的粒子,由于是存放在数组中的,也就是进行了对死亡粒子的重新初始化和复活
void CreateLight();
//这个函数是辅助上述的初始化函数和复活函数使用的,负责初始化一个指定下标的粒子
void SetLightToDefault(int index);
int mMaxCount; //定义粒子数量,数量越多,占用CPU运算越高,越少则屏幕上将会没什么东西
Light * mLight;//粒子数组,后面通过malloc动态申请
int mWinWidth;//绘制的窗口的宽高
int mWinHeight;
};
#include //EasyX图形库头文件,Win32或者MFC就不用包含了
#include"StarVortex.h"
int main(int argc, char * argv[])
{
//获取屏幕的大小,作为我们窗口的大小
int winWid = GetSystemMetrics(SM_CXSCREEN);
int winHei = GetSystemMetrics(SM_CYSCREEN);
//使用EasyX图形库初始化我们的窗口
HWND hwnd=initgraph(winWid,winHei);
//移动窗口到0,0位置,这样我们的窗口就铺满了屏幕了
//参数说明:窗口句柄,Zorder窗口,坐标X,Y,宽高wid,hei,操作掩码
//由于我们只是移动,因此不排序SWP_NOZORDER ,不调整尺寸SWP_NOSIZE
//这个时候,Zorder窗口,宽高的值任意值都行
SetWindowPos(hwnd, NULL, 0, 0, 0, 0, SWP_NOZORDER | SWP_NOSIZE);
//创建我们的类对象
StarVortex vortext;
//设置参数并初始化
vortext.SetWindowSize(winWid,winHei);
vortext.SetMaxCount(2000);
//注意,一定要初始化
vortext.InitLight();
while (1)
{
//EasyX的缓冲绘制开始
BeginBatchDraw();
//EasyX的清空屏幕
cleardevice();
//调用我们的类方法进行绘制
//在EasyX中,获取默认窗口的HDC,调用GetImageHDC()方法获取
//如果是获取EasyX的某个IMAGE对象的HDC,给定参数即可
//比如:
//IMAGE img;
//HDC hdc=GetImageHDC(&img);
vortext.DrawBackground(GetImageHDC());
//EasyX的缓冲绘制结束
EndBatchDraw();
//睡眠一下,控制一下帧率=1000/24
Sleep(24);
}
return 0;
}
//#include "stdafx.h"
#include "StarVortex.h"
//构造函数就不说了,你可以看到默认参数的值
StarVortex::StarVortex()
{
SetDefaultArgument();
SetWindowSize(720, 480);
SetMaxCount(2000);
}
StarVortex::StarVortex(int width, int height, int count)
{
SetDefaultArgument();
SetWindowSize(width,height);
SetMaxCount(count);
}
void StarVortex::SetDefaultArgument()
{
mMaxCount = 2000;
mLight = NULL;
mWinWidth = 720;
mWinHeight=480;
}
//析构函数中,需要释放申请的内存
StarVortex::~StarVortex()
{
if (mLight != NULL)
{
free(mLight);
}
}
//设置粒子数量的时候,检查是否已经申请过
void StarVortex::SetMaxCount(int count)
{
//如果已经申请过了,那么就先释放,再重新申请
if (mLight != NULL)
{
free(mLight);
mLight = NULL;
}
//重新申请
mMaxCount = count;
mLight = (Light*)malloc(sizeof(Light)*mMaxCount);
}
void StarVortex::SetWindowSize(int width, int height)
{
mWinWidth = width;
mWinHeight = height;
}
void StarVortex::SetLightToDefault(int index)
{
//上一次生成粒子的方向
/*
思路:
初始化新粒子的时候,根据上一次的角度,加上一个随机角度,
那么就实现了一周一周的生成,就能够实现螺旋感
因此使用静态变量
*/
static double lastDirect = 0;
//获取屏幕鼠标的坐标,因为我们的窗口和屏幕一样大
//因此客户区和屏幕没有什么差别
//不用调用ScreenToClient函数进行坐标转换
//我们生成的粒子就从鼠标的坐标下座位出发点
//就实现了跟随鼠标的效果
POINT cursor = { 0 };
GetCursorPos(&cursor);
//设置粒子初始坐标为当前鼠标坐标
mLight[index].x = cursor.x;
mLight[index].y = cursor.y;
//初始状态粒子时死亡状态
mLight[index].live = false;
//你可以通过下面的注释语句,实现固定在屏幕中间散开
/*mLight[index].x = mWinWidth / 2;
mLight[index].y = mWinHeight / 2;*/
//在上一次的角度基础上加上一个随机的偏转角度
//注意/180*PI做的是角度转弧度,毕竟人还是比较喜欢角度的
mLight[index].direc = lastDirect + ((rand() % 15) / 180.0*3.141592653549);
if (mLight[index].direc > 2 * 3.141592653549)
{
mLight[index].direc -= 2 * 3.141592653549;
}
//重新保存为上一次角度
lastDirect = mLight[index].direc;
//粒子逃逸速度设置为屏幕的1/150-2的大小,这样的1/150就实现了动态根据窗口大小,调整速度
mLight[index].speed = rand() % (mWinWidth * 1 / 150) + 1;
if (mLight[index].speed == 1) mLight[index].speed = 2;
//随机粒子的颜色,采用一个亮色调,背景默认是黑色
mLight[index].r = rand() % 200 + 54;
mLight[index].b = rand() % 200 + 54;
mLight[index].g = rand() % 200 + 54;
//前面说了,下面这两个域是无效的,所以可以不用管
mLight[index].curR =1;
mLight[index].addR = (rand() % 100 + 1)%100;
//初始的逃逸距离为0,对吧,就在x,y上
mLight[index].curDis = 0;
}
void StarVortex::InitLight()
{
//这就是,为什么一定要调用初始化,看到了吧,需要将一些粒子设置为存活状态
//还有参数设置
for (int i = 0; i < mMaxCount; i++)
{
SetLightToDefault(i);
if (3 > rand() % 100) //这里就是97%的粒子都是存活的
mLight[i].live = true;
else
mLight[i].live = false;
}
}
void StarVortex::CreateLight()
{
//这里控制一下,每次调用此函数,最多复活多少个粒子
//如果不进行控制,那么一次会激活一批,导致会产生一圈一圈的效果
//和预想的有差异
int maxCount = mMaxCount / 100.0;
if (maxCount <= 0)
maxCount = 1;
int curCount = 0;
for (int i = 0; i < mMaxCount; i++)
{
if (mLight[i].live == false)
{
if (rand() % 100 < 21)
{
SetLightToDefault(i);
mLight[i].live = true;
curCount++;
}
}
if (curCount >= maxCount)
break;
}
}
void StarVortex::DrawBackground(HDC hdc)
{
//我们获取一下,允许粒子走的最大距离,为我们窗口的对角线的距离的0.6
//距离计算:开方(x^2+y^2) 开方:sqrt,次幂=pow
double winMaxDis = sqrt(pow(mWinWidth*0.6,2.0)+pow(mWinHeight*0.6,2.0));
for (int i = 0; i < mMaxCount; i++)
{
if (mLight[i].live)//如果粒子存活就进行绘制
{
//根据初始坐标,移动距离,方向,计算出绘制的坐标
int px = mLight[i].x + mLight[i].curDis*cos(mLight[i].direc);
int py = mLight[i].y + mLight[i].curDis*sin(mLight[i].direc);
//这部分是GDI的内容,设置和粒子颜色一样的画笔和画刷
//这样就能绘制一个纯净粒子颜色的球形了
COLORREF cor = RGB(mLight[i].r,mLight[i].g,mLight[i].b);
HPEN pen = CreatePen(0, 1, cor);
HGDIOBJ op=SelectObject(hdc, pen);
HBRUSH brush = CreateSolidBrush(cor);
HGDIOBJ ob = SelectObject(hdc, brush);
//计算在当前距离下,球应该有的半径,距离越大,半径越大
//这也是一个比例运算,应该不难理解,最大半径就是6
mLight[i].curR = 6.0*mLight[i].curDis / winMaxDis;
if (mLight[i].curR <= 1)
mLight[i].curR = 1;
//绘制圆形,由于GDI是没有绘制圆形的函数的,因此使用坐标点作为圆心,用半径来计算坐标点,使用绘制椭圆的函数进行绘制
//参数:HDC,左上角X,Y,右下角X,Y
Ellipse(hdc, px - mLight[i].curR, py - mLight[i].curR, px + mLight[i].curR, py + mLight[i].curR);
//GDI的内容,画笔句柄是需要释放的
//否则,一旦句柄被用完了,没有释放,那就没墨水绘制了
//常见的就是,绘制的都是黑白的
DeleteObject(op);
DeleteObject(pen);
DeleteObject(ob);
DeleteObject(brush);
/
//移动,在当前距离的基础上,加上移动速度,就实现了距离的增长
mLight[i].curDis += mLight[i].speed;
//如果每次的移动距离都一样,那么距离远近都一个速度,不太好
//给他加上一个增量,这样,距离越远,速度越快
mLight[i].speed += 0.1;
//死亡判定,如果移动距离已经大于最大允许距离,那么就该死了
//但是直接判定死亡,就死的成为一个圆形,没有过度,不好看
//因此,还要被选中12%确定处以死刑,才能死,
//否则就能够侥幸存活一段时间,这样就实现了边界模糊
if (mLight[i].curDis > winMaxDis && rand() % 100 < 12)
mLight[i].live = false;
//if (rand() % 100 < 85)
{
//看起来有点复杂,简单说一下
//就是计算偏转角度的,但是是放大了10倍计算的
//最大偏转30度,距离越远,偏转角度越小
//这样就明白了吧
//rfac=(30-28)*距离比例 说了放大了10倍
//距离比例=粒子移动距离/最大允许距离
//但是前面说了,粒子可能侥幸逃脱死亡判定,那么也就是说
//粒子移动距离不一定小于最大距离
//因此三目运算符处理
int rfac = (300 - (280.0*(mLight[i].curDis>winMaxDis ? winMaxDis : mLight[i].curDis) / winMaxDis));
//防止后面取余时发生异常,限定不能小于2
rfac = rfac<2 ? 2 : rfac;
//前面说了,放大了10倍,这里还回去(rand() % rfac)/10.0
//这里除以10
//这个公式一开始就介绍了,不重复讲了
// /180.0*3.141592653549 转弧度
mLight[i].direc = ((((rand() % rfac)/10.0) / 180.0*3.141592653549) + mLight[i].direc)*0.3 + mLight[i].direc*0.7;
if (mLight[i].direc > 3.141592653549*2)
mLight[i].direc -= 3.141592653549*2;
}
}
}
//每次绘制结束,都去复活一些粒子,这样就能够感觉源源不断的粒子产生和死亡
//相比较于用链表实现,好多了吧
CreateLight();
}