用飞镖绘制像素

受OneLoneCoder视频的启发,我想玩简单的像素图形。 我想使用Dart,而不是使用C ++或Swift(因为我使用的是Mac)。 并且本着OLC的精神,我想创建可能可行的最基本的解决方案。 因此,我认为将SDL添加到Dart控制台应用程序可能是正确的方法。 我想从SDL使用的是一些用于绘制彩色像素的基本命令。

不幸的是,似乎以前没有人走过这条路,当我尝试为SDL创建本机Dart扩展时,我发现至少在macOS上,它真的不起作用,因为我不知道该怎么做。扩展使用主线程 ,这是唯一可以控制UI的线程。 GLFW(类似的技术)进行了扩展,但又不支持macOS。

因此,由于为Dart VM创建本机扩展并不是一件有趣的事情,因此我提出了以下计划:

为什么不使用C创建一个最小的控制台应用程序,该应用程序打开一个SDL窗口,从stdin读取绘图命令并显示它们,直到关闭stdin 。 然后,由我的Dart命令行应用程序启动此应用程序,并根据需要添加尽可能多的绘图命令。

使用SDL的第一步

通过Homebrew在系统上安装SDL之后,我创建了以下C代码,该代码来自示例。 在没有任何IDE支持的情况下编写此代码是一次有趣的经历,但是我认为C部分应该足够简单,可以在任何文本编辑器中编写该代码。

该应用程序初始化图形子系统,在屏幕上的某个地方打开一个小窗口,然后等待用户关闭该窗口。 然后它正确关闭。

  #include  
#include“ SDL2 / SDL.h”
  SDL_Window *窗口; 
  int main(int argc,char ** argv){ 
SDL_Init(SDL_INIT_VIDEO);

窗口= SDL_CreateWindow(
“ SDL测试”,
SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED,
320,
200,
0
);
如果(!window)返回1;
  SDL_Event e; 
而(SDL_WaitEvent(&e)){
如果(e.type == SDL_QUIT)中断;
}
  SDL_DestroyWindow(window); 
  SDL_Quit(); 
 返回0; 
}

我编译了该应用程序并成功对其进行了测试:

  $ cc -I / usr / local / include -L / usr / local / lib -lsdl2 -Wall console.c 
$ ./每年

绘画的东西

为了进行绘制,我直接使用了窗口的表面,而不是在每个帧上都重新创建了所有内容(使用渲染器)。 这样做似乎更容易。 但是,必须明确更新该表面。 意识到可以将绘制单个像素概括为填充矩形后,我将介绍下一个实验:

  ... 
如果(!window)返回1;
  SDL_Surface *屏幕= SDL_GetWindowSurface(窗口); 
如果(!屏幕)返回1;
  SDL_Rect rect = {20,40,60,80}; 
Uint32 rgb = SDL_MapRGB(screen-> format,240,120,0);
SDL_FillRect(screen,&rect,rgb);
SDL_UpdateWindowSurface(window);
  SDL_Event e; 
...

编译并运行它会发现一个小而美丽的橙色矩形。

把它放在一起

接下来,我决定要使用命令行参数指定窗口标题以及窗口位置和大小。 另外,我还想指定像素大小,以使5K显示器具有复古的像素外观。 事后看来,让SDL缩放表面可能更容易,更有效。

我提出了以下图形命令列表:

  • c rgb设置填充颜色
  • rxywh用当前颜色绘制一个填充的矩形
  • u更新窗口

我在下面显示的process_command()中实现了它们。 如果应用程序退出,该函数将返回1,否则返回0。 在main()我评估命令行参数,检索要绘制的SDL_Surface ,并使用fgets()stdin中读取行以对其进行处理。 该函数将在EOF上返回NULL

  #include  
#include“ SDL2 / SDL.h”
  SDL_Window *窗口; 
SDL_Surface *屏幕;
int pixelz = 1;
  int process_command(char * buf){ 
静态Uint32 rgb;
 如果(!buf)返回1; 
开关(buf [0]){
情况“ c”:{
int c;
sscanf(++ buf,“%d”,&c);
rgb = SDL_MapRGB(屏幕->格式,
(c >> 16)&255,(c >> 8)&255,c&255);
返回0;
}
大小写“ r”:{
SDL_Rect rect;
sscanf(++ buf,“%d%d%d%dd”,&rect.x,&rect.y,&rect.w,&rect.h);
rect.x * =像素z;
rect.y * = pixelzz;
rect.w * = pixelzz;
rect.h * = pixelzz;
SDL_FillRect(screen,&rect,rgb);
返回0;
}
情况'u':
SDL_UpdateWindowSurface(window);
返回0;
默认:
返回0;
}
}
  int _pos(int i){ 
返回i <0吗? SDL_WINDOWPOS_UNDEFINED:i;
}
  int main(int argc,char ** argv){ 
如果(argc <6)返回1;
 如果(argc> = 7)pixelzz = atoi(argv [6]); 
  SDL_Init(SDL_INIT_VIDEO); 

窗口= SDL_CreateWindow(
argv [1],
_pos(atoi(argv [2])),
_pos(atoi(argv [3])),
atoi(argv [4])* pixelz,
atoi(argv [5])* pixelz,
0
);
如果(!window)返回1;
 屏幕= SDL_GetWindowSurface(窗口); 
如果(!屏幕)返回1;
  SDL_Event e; 
而(SDL_WaitEvent(&e)){
如果(e.type == SDL_QUIT)中断;

char buf [64];
如果(process_command(fgets(buf,64,stdin)))中断;
}
  SDL_DestroyWindow(window); 
  SDL_Quit(); 
 返回0; 
}

编译后,这种工作方式如下:

  $ ./a.out'Hello World'-1 -1 160 100 4 
15 15759360
r 20 30 40 50
ü
^ D

不幸的是,从stdin读取会阻塞事件循环,并使窗口无响应。 我没有找到某种方法来侦听SDL事件循环中的文件描述符(类似于select )。 然后,我尝试使stdin成为非阻塞状态,并切换到SDL_PollEvent ,该方法可以SDL_PollEvent工作,但消耗了100%的CPU。

因此,我最终使用了另一个线程和用户定义的SDL事件,如下所示:

  Uint32命令;  //自定义SDL事件类型 
  int command_loop(void * data){ 
SDL_Event e = {};
e.type =命令;
做{
char buf [64];
如果(fgets(buf,64,stdin)){
e.user.data1 = strdup(buf);
}其他{
e.user.data1 = NULL;
}
SDL_PushEvent(&e);
} while(e.user.data1);
返回0;
}
  ... 
  int main(){ 
...
  if((命令= SDL_RegisterEvents(1))==(Uint32)-1)返回2; 
如果(!SDL_CreateThread(command_loop,NULL,NULL))返回2;
  SDL_Event e; 
而(SDL_WaitEvent(&e)){
如果(e.type == SDL_QUIT)中断;
if(e.type ==命令){
如果(process_command(e.user.data1))中断;
免费(e.user.data1);
}
}
  ... 
}

现在,在关闭stdin时,应用程序会自动退出,并保持响应状态,因此也可以使用⌘Q或单击关闭按钮来关闭窗口。

最后但并非最不重要,Dart

让我们从RGB值的抽象开始。 现在,它只包装32位整数,但是我可以想象以后再添加一些方法来导出较浅或较深的颜色阴影或支持不同的颜色空间,例如ieHSL。

 类别颜色{ 
最终的int值;
  const Color(this.value); 

const Color.rgb(int红色,int绿色,int蓝色)
:值=(红色<< 16)| (绿色<< 8)| 蓝色;
  int得到红色=>(值>> 16)&255; 
int得到绿色=>(值>> 8)&255;
int得到蓝色=>值&255;
}

然后,我创建了一些与Flutter使用的名称相同的常量,这仅仅是因为我知道并且经常使用它们。 我花了五分钟左右的时间来复制材料设计规范中的所有十六进制值。

 颜色类{ 
静态const black = Color(0x000000);
静态const white = Color(0xFFFFFF);
静态const red = Color(0xF44336);
静态const pink = Color(0xE91E63);
静态const Purple = Color(0x9C27B0);
静态const deepPurple = Color(0x673AB7);
静态const靛蓝= Color(0x3F51B5);
静态const blue = Color(0x2196F3);
静态常量lightBlue = Color(0x03A9F4);
静态const cyan = Color(0x00BCD4);
静态const teal = Color(0x009688);
静态const green = Color(0x4CAF50);
静态常量lightGreen = Color(0x8BC34A);
静态常量石灰=颜色(0xCDDC39);
静态const yellow = Color(0xFFEB3B);
静态const琥珀色= Color(0xFFC107);
静态常量橙色=颜色(0xFF9800);
静态const deepOrange = Color(0xFF5722);
静态常量棕色=颜色(0x795548);
static const gray = Color(0x9E9E9E);
静态const blueGray = Color(0x607D8B);
颜色._();
}

稍后,我也想抽象点和矩形,但是现在,不费吹灰之力,让我们实现Console类,该类包装并驱动上面创建的SDL命令行应用程序(仍称为a.out )。

 类控制台{ 
最终过程_process;
最终的int宽度;
最终int高度;
  Console ._(this._process,this.width,this.height){ 
明确();
}
  void close(){ 
_process.stdin.close();
}
 颜色_color = Colors.white; 
颜色获取颜色=> _color;
设置颜色(颜色){
_color =颜色;
_process.stdin.writeln('c $ {color.value}');
}
  void clear([[Color color = Colors.black]){ 
_process.stdin.writeln('c $ {color.value}');
_process.stdin.writeln('r0 0 $ width $ height');
_process.stdin.writeln('c $ {_ color.value}');
}
 无效点(int x,int y){ 
_process.stdin.writeln('r $ x $ y 1 1');
}
  void rect(int x,int y,int width,int height){ 
_process.stdin.writeln('r $ x $ y $ width $ height');
}
  void update(){ 
_process.stdin.writeln('u');
}
 静态Future  create( 
int宽度,int高度,[int比例= 1]
)异步{
返回Console ._(
等待Process.start(
'./a.out',
['Dart','-1','-1','$ width','$ height','$ scale'],
),
宽度,
高度,
);
}
}

创建Console实例的唯一方法是静态方法Console.create(...) 。 并且由于Process.start()使用期货,因此该方法也是异步的。

只是为了好玩,我还使用布雷森纳姆line() Bresenham)算法实现了line() ,它花了比预期更长的时间,因为我无法正确地从Wikipedia复制代码,并且没有像我所希望的那样快找到错误。 可能还需要实现一种算法来填充圆形或三角形(或多边形),这可能是一个有趣的练习。 但是我离题了。

随机矩形

让我们将Console与此示例应用程序一起使用:

  void main()异步{ 
最后的r = Random();
最后的c =等待Console.create(160,160,8);
 而(true){ 
c.color = Color.rgb(
r.nextInt(256),
r.nextInt(256),
r.nextInt(256),
);
最终w = r.nextInt(40)+ 10;
最后的h = r.nextInt(40)+ 10;
最终x = r.nextInt(c.width-w);
最终y = r.nextInt(c.height-h);
c.rect(x,y,w,h);
c.update();
}
}

在与Dart源代码相同的目录中使用C a.out二进制文件运行应用程序将导致一个彩色窗口:

太糟糕了,我的方法无法按预期工作。

几秒钟后,窗口停止绘制矩形。 我试图调试Dart应用程序,并且按预期方式工作。 C应用程序也可以按预期工作。 我的猜测是,由于Dart VM通常在单个线程上运行,因此无尽的while循环使I / O子系统无法将更多数据从Dart VM发送到C应用程序。

这是我发现的一种(有些令人不满意的)解决方法:我没有使用无限循环,而是使用了每秒触发60次的定期计时器。 这样,Dart运行时似乎可以按预期工作。

  void main()异步{ 
最后的r = Random();
最后的c =等待Console.create(160,160,8);
  Timer.periodic(Duration(毫秒(毫秒):1000〜/ 60),(_){ 
c.color = Color.rgb(
r.nextInt(256),
r.nextInt(256),
r.nextInt(256),
);
c.line(
r.nextInt(c.width),
r.nextInt(c.height),
r.nextInt(c.width),
r.nextInt(c.height),
);
c.update();
});
}

现在,我的“引擎”可以按预期工作,绘制由微小的小矩形组成的块状线,并将所有命令发送到C进程。

我只能看到的动画

因为我使用的是周期计时器,所以我可以创建一个简单的动画来演示运动的圆(我使用Bresenham中点算法实现了该动画)。 因为圆圈有些复杂,所以我不能显示超过20个或更多的Dart进程无法将所有内容发送到C应用程序。

 圈子{ 
双x,y,vx,vy
诠释
颜色c;
 圈(this.x,this.y,this.r,this.vx,this.vy,this.c); 
 静态常量颜色= [ 
红色,蓝色,绿色,
Colors.brown,Colors.yellow];
}
  void main()异步{ 
最后的r = Random();
最后的c =等待Console.create(160,160,8);
 最后一圈= List.generate(15,(index){ 
最终ra = r.nextInt(28)+ 3;
返回圆(
(r.nextInt(c.width-ra * 2)+ ra).toDouble(),
(r.nextInt(c.height-ra * 2)+ ra).toDouble(),

(r.nextInt(11)-5).toDouble()/ 2
(r.nextInt(11)-5).toDouble()/ 2
Circle.colors [索引%5],
);
});
  Timer.periodic(Duration(毫秒(毫秒):1000〜/ 60),(_){ 
c.clear();
为(以圆圈为单位){
circle.x + = circle.vx;
circle.y + = circle.vy;
if(circle.x = c.width-circle.r)
circle.vx * = -1;
if(circle.y = c.height-circle.r)
circle.vy * = -1;
c.color = circle.c;
c.drawCircle(circle.x.toInt(),circle.y.toInt(),circle.r);
}
c.update();
});
}

不幸的是,下面的屏幕截图也显示了我从Wikipedia复制的算法在绘制小圆圈时存在一些问题(红色的圆圈看起来更像是矩形而不是圆圈)。

当(或如果)发现为什么通过文件流进行通信的想法有一些局限性时,我将更新本文。

更新(05.01.2019)

经过一些调试后,我找到了几秒钟后窗口停止绘制的原因。 我用来将Dart进程发送的命令传递到C应用程序的主线程的SDL事件队列最多只能容纳65535个事件。 这是command_loop()的新版本,该版本检查SDL_PostEvent是否会在片刻后重试失败:

  int command_loop(void * data){ 
SDL_Event e = {};
e.type =命令;
做{
char buf [64];
如果(fgets(buf,64,stdin)){
e.user.data1 = strdup(buf);
}其他{
e.user.data1 = NULL;
}
Uint32毫秒= 1;
而(SDL_PushEvent(&e)<0 && ms <1 << 10){
SDL_Delay(ms);
ms << = 1;
}
} while(e.user.data1);
返回0;
}

现在,一切正常。