tests/interactive-ascii: add padshapes1.c adding 2d character shapes
authorMatthew Mondor <mmondor@pulsar-zone.net>
Thu, 13 Apr 2023 07:41:32 +0000 (07:41 +0000)
committerMatthew Mondor <mmondor@pulsar-zone.net>
Thu, 13 Apr 2023 07:41:32 +0000 (07:41 +0000)
tests/interactive-ascii/padshapes1.c [new file with mode: 0644]

diff --git a/tests/interactive-ascii/padshapes1.c b/tests/interactive-ascii/padshapes1.c
new file mode 100644 (file)
index 0000000..e7892f2
--- /dev/null
@@ -0,0 +1,416 @@
+/*
+ * Copyright (c) 2023, Matthew Mondor
+ * ALL RIGHTS RESERVED.
+ *
+ * Interactive curses user input and shape blitting test.
+ * Uses the input and clock method of test 8.
+ * Alters the animation to test using individual pads for complex 2d
+ * shapes.  Will then evaluate the performance impact of erasing those
+ * individually on a window or subwindow, versus erasing and redrawing.
+ * Another interesting test will be comparing the performance of small
+ * and larger pads.
+ *
+ * Notes:
+ * - If we wanted collision detection of a small projectile against a
+ *   complex text shape, it would be possible to first check the rectangle,
+ *   then if in, to check if it hits a non-space character in the pad.
+ * - The drawing and move routines could ensure not to draw large shapes
+ *   outside of the intended window.
+ * - We may want shape positions to be based on the center of a shape.
+ *
+ * $ cc -Wall -O0 -g -o padshapes1 padshapes1.c -lcurses
+ */
+
+#include <curses.h>
+#include <err.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+
+
+#define TERM_COLUMNS   79
+#define TERM_LINES     24
+#define ANIM_FPS       15
+#define NOBJECTS       56
+
+enum enum_shapes {
+       SHAPE_A = 0,
+       SHAPE_B,
+       SHAPE_SHIP,
+       SHAPE_MAX
+};
+
+/*
+ * These could be loaded from a text file.
+ * An animated shape could also consist of shape sequences.
+ * Good enough for an initial "blitting" test.
+ * When copied with copywin() into a window, the overlay parameter when TRUE
+ * causes the spaces of the shape to be transparent like with bitmasks.
+ */
+const char *shapes[SHAPE_MAX + 1] = {
+       " /\\\n"
+       "|__|\n"
+       "|  |"
+       ,
+       " __ \n"
+       "|__>\n"
+       "|__>"
+       ,
+       /* https://www.asciiart.eu/vehicles/boats */
+       "                 |~         \n"
+       "           |/    w          \n"
+       "          / (   (|   \\      \n"
+       "         /( (/   |)  |\\     \n"
+       "  ____  ( (/    (|   | )  , \n"
+       " |----\\ (/ |    /|   |'\\ /^;\n"
+       "\\---*---Y--+-----+---+--/(  \n"
+       " \\------*---*--*---*--/     \n"
+       "  '~~ ~~~~~~~~~~~~~~~       "
+       ,
+       NULL
+};
+
+WINDOW *spads[SHAPE_MAX];
+
+
+/* Animation object */
+
+enum aobject_type {
+       AOT_AVATAR = 0,
+       AOT_STAR,
+       AOT_BUG,
+       AOT_SHAPE
+};
+
+/* Could be a union but not needed for this test */
+typedef struct aobject {
+       int             t, x, y, col;
+       char            c;
+       int             s;
+} aobject_t;
+
+
+int                    main(void);
+static void            cleanup(void);
+static void            shape_init(int);
+static void            shape_draw(int, int, int);
+static void            anim_init(void);
+static void            anim_redraw(void);
+static void            anim_update(void);
+
+
+/* Curses window */
+static WINDOW          *w = NULL;
+
+/* Animation world */
+static aobject_t       objects[NOBJECTS];
+static aobject_t       avatar = { AOT_AVATAR,
+                           TERM_COLUMNS / 2, TERM_LINES / 2,
+                           0, '?', -1 };
+
+
+int
+main(void)
+{
+
+       /* Setup curses */
+       if ((w = initscr()) == NULL)
+               err(EXIT_FAILURE, "initscr()");
+       if (COLS < TERM_COLUMNS || LINES < TERM_LINES) {
+               cleanup();
+               (void)fprintf(stderr,
+                   "The terminal must support at least %d columns by %d"
+                   " lines for this program to work.\n",
+                   TERM_COLUMNS, TERM_LINES);
+               exit(EXIT_FAILURE);
+       }
+
+       /* Exit cleanup hook to restore normal terminal */
+       (void)atexit(cleanup);
+
+       /*
+        * XXX Useful but relies on an ncurses extension, also supported by
+        * NetBSD curses.  init_pair() cannot accept -1 according to X/Open.
+        * But this special value means to keep the default foreground or
+        * background color of the terminal configuration.  This also permits
+        * enforced color pairs to really only define the foreground or
+        * background color.
+        */
+       (void)use_default_colors();
+       if (start_color() == ERR)
+               err(EXIT_FAILURE, "start_color()");
+       (void)init_pair(0, -1, COLOR_BLACK);
+       (void)init_pair(1, -1, COLOR_RED);
+       (void)init_pair(2, -1, COLOR_GREEN);
+       (void)init_pair(3, -1, COLOR_YELLOW);
+       (void)init_pair(4, -1, COLOR_BLUE);
+       (void)init_pair(5, -1, COLOR_MAGENTA);
+       (void)init_pair(6, -1, COLOR_CYAN);
+       (void)init_pair(7, -1, COLOR_WHITE);
+       (void)init_pair(8, COLOR_BLUE, -1);
+
+       (void)cbreak(); /* Use raw() if not wanting SIGINT */
+       (void)noecho();
+       (void)keypad(w, TRUE);
+       (void)nonl();
+       (void)intrflush(stdscr, FALSE);
+       (void)timeout(0);
+
+       /* Convert strings to pads */
+       shape_init(COLOR_PAIR(8));
+
+       /* Enable hardware features if available */
+       /*
+       if (has_ic())
+               idcok(w, TRUE);
+       if (has_il())
+               idlok(w, TRUE);
+       */
+
+       /* Setup animation world */
+       anim_init();
+
+       /* Initialize screen */
+       (void)erase();
+       (void)doupdate();
+
+       /* Disable cursor */
+       (void)curs_set(0);
+       (void)leaveok(w, TRUE);
+
+       /* Animate until user requests to quit with q */
+       anim_redraw();
+       for (;;) {
+               int x = -1, y = -1, c = -1, uc = -1;
+
+               /* Sleep for most of the frame */
+               (void)usleep(1000000 / ANIM_FPS);
+
+               /* Non-blocking check for user input */
+               if ((c = wgetch(w)) != ERR) {
+                       uc = c;
+                       /*
+                        * If we don't flush and keyboard input/repeat is
+                        * faster than our refresh rate, the input queue grows
+                        * meaning that the avatar takes a while to react to
+                        * immediate user input.
+                        */
+                       (void)flushinp();
+               }
+
+               /* Process user input if any */
+               switch (uc) {
+               case -1:
+                       break;
+               case 'q':
+                       goto end;
+                       break;
+
+               case KEY_UP:
+               case '8': /* FALLTHROUGH */
+               case 'i': /* FALLTHROUGH */
+               case 'a': /* FALLTHROUGH */
+                       if (avatar.y > 0) {
+                               y = avatar.y - 1;
+                               x = avatar.x;
+                       }
+                       break;
+
+               case KEY_DOWN:
+               case '2': /* FALLTHROUGH */
+               case 'k': /* FALLTHROUGH */
+               case 'm': /* FALLTHROUGH */
+               case 'z': /* FALLTHROUGH */
+                       if (avatar.y < TERM_LINES - 1) {
+                               y = avatar.y + 1;
+                               x = avatar.x;
+                       }
+                       break;
+
+               case KEY_LEFT:
+               case '4': /* FALLTHROUGH */
+               case 'j': /* FALLTHROUGH */
+               case ',': /* FALLTHROUGH */
+                       if (avatar.x > 0) {
+                               x = avatar.x - 1;
+                               y = avatar.y;
+                       }
+                       break;
+
+               case KEY_RIGHT:
+               case '6': /* FALLTHROUGH */
+               case 'l': /* FALLTHROUGH */
+               case '.': /* FALLTHROUGH */
+                       if (avatar.x < TERM_COLUMNS - 1) {
+                               x = avatar.x + 1;
+                               y = avatar.y;
+                       }
+                       break;
+               }
+
+               /* Update avatar if necessary */
+               if (x != -1 && y != -1) {
+                       avatar.x = x;
+                       avatar.y = y;
+               }
+
+               /* Animate */
+               anim_update();
+               anim_redraw();
+               uc = -1;
+       }
+end:
+
+       exit(EXIT_SUCCESS);
+}
+
+static void
+cleanup(void)
+{
+
+       (void)curs_set(1);
+       (void)endwin();
+}
+
+static void
+shape_init(int attr)
+{
+       const char **shape;
+       int shapen;
+
+       /* Initialize shape pads */
+       for (shape = shapes, shapen = 0; *shape != NULL; shape++, shapen++) {
+               const char *cptr;
+               int xs = 0, ys = 0, x;
+               WINDOW *pad;
+
+               /* Evaluate shape size.  Leave an extra column. */
+               for (cptr = *shape, x = 0; *cptr != '\0'; cptr++, x++) {
+                       if (*cptr == '\n') {
+                               ys++;
+                               if (x > xs)
+                                       xs = x;
+                               x = 0;
+                       }
+               }
+               ys++;
+
+               /* Create the new pad and print the shape in it */
+               if ((pad = newpad(ys, xs)) == NULL)
+                       err(EXIT_FAILURE, "newpad()");
+               (void)werase(pad);
+               if (attr != -1)
+                       (void)wattrset(pad, attr);
+               (void)mvwaddstr(pad, 0, 0, *shape);
+               spads[shapen] = pad;
+       }
+}
+
+static void
+shape_draw(int shape, int y, int x)
+{
+       int xs, ys;
+       WINDOW *pad;
+
+       pad = spads[shape];
+       getmaxyx(pad, ys, xs);
+       (void)copywin(pad, w, 0, 0, y, x, y + ys - 1, x + xs - 1, TRUE);
+}
+
+static void
+anim_init(void)
+{
+       int i, col;
+
+       srandom(13);
+       col = 0;
+       for (i = 0; i < NOBJECTS; i++) {
+               aobject_t *o = &objects[i];
+
+               if (i <= 26) {
+                       o->t = AOT_BUG;
+                       o->c = 'A' + i;
+                       o->col = ++col % 7;
+                       o->s = -1;
+               } else if (i < 46) {
+                       o->t = AOT_STAR;
+                       o->c = '.';
+                       o->col = 0;
+                       o->s = -1;
+               } else {
+                       o->t = AOT_SHAPE;
+                       o->c = '\0';
+                       o->col = -1;
+                       o->s = random() % SHAPE_MAX;
+               }
+               o->x = random() % (TERM_COLUMNS - 1);
+               o->y = random() % (TERM_LINES - 1);
+       }
+}
+
+static void
+anim_redraw(void)
+{
+       int i;
+
+       (void)attrset(COLOR_PAIR(0));
+       (void)erase();
+       for (i = 0; i < NOBJECTS; i++) {
+               aobject_t *o = &objects[i];
+
+               if (o->t == AOT_SHAPE)
+                       shape_draw(o->s, o->y, o->x);
+               else {
+                       (void)attrset(COLOR_PAIR(o->col));
+                       (void)mvaddch(o->y, o->x, o->c);
+               }
+       }
+
+       /* Avatar */
+       (void)attr_on(A_REVERSE, NULL);
+       (void)mvaddch(avatar.y, avatar.x, avatar.c);
+       (void)attr_off(A_REVERSE, NULL);
+
+       (void)doupdate();
+}
+
+static void
+anim_update(void)
+{
+       int i;
+
+       for (i = 0; i < NOBJECTS; i++) {
+               aobject_t *o = &objects[i];
+               int x = o->x, y = o->y, d = random() % 4;
+
+               if (o->t == AOT_STAR) {
+                       if (x > 0)
+                               x--;
+                       else
+                               x = TERM_COLUMNS - 1;
+               } else {
+                       switch (d) {
+                       case 0: /* Up */
+                               if (y > 0)
+                                       y--;
+                               break;
+                       case 1: /* Down */
+                               if (y < TERM_LINES - 1)
+                                       y++;
+                               break;
+                       case 2: /* Left */
+                               if (x > 0)
+                                       x--;
+                               break;
+                       case 3: /* Right */
+                               if (x < TERM_COLUMNS - 1)
+                                       x++;
+                               break;
+                       }
+               }
+               o->x = x;
+               o->y = y;
+       }
+}