trip logs / gnuvola


Trip Log 2022-01-29 h08 -- Hooray! Part 5

The long-awaited foreground appears in part 5 of the Hooray! series.  It doesn't do much, but it lays the foundation for future changes.  Thus, patch 8 deserves its own episode. 

patch 8

     1  commit f2b85ea6892ddcf4d590efdbab9046e162a910cb
     2  Author: Thien-Thi Nguyen <ttn@gnuvola.org>
     3  Date:   2022-01-29 00:20:32 -0500
     4
     5      Add simple foreground
     6
     7      * hooray.scm: Import ‘(sdl misc-utils) exact-truncate’.
     8      Import ‘(sdl gfx)’ procs ‘blit-rgba’, ‘imfi-sub-c’.
     9      (FG-EDGE-LENGTH): New constant.
    10      (make-fg-rect): New proc.
    11      (hooray!): New proc.
    12      <top-level>: Add call to ‘hooray!’.
    13  ---
    14   hooray.scm | 119 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
    15   1 file changed, 117 insertions(+), 2 deletions(-)
    16
    17  diff --git a/hooray.scm b/hooray.scm
    18  index ef226b2..cd3da51 100644
    19  --- a/hooray.scm
    20  +++ b/hooray.scm
    21  @@ -15,10 +15,12 @@
    22
    23   ;;; Commentary:
    24
    25  -;; Usage: guile -s hooray.scm THEME
    26  +;; Usage: guile -s hooray.scm THEME [THRONG]
    27   ;;
    28   ;; This opens a window, loads THEME (a stylized PNG file),
    29   ;; displays some random tiles from it placed in random locations,
    30  +;; displays a THRONG (integer, default 9) of "hooray!" animations
    31  +;; (in parallel, repeating a number of times),
    32   ;; waits a moment, then exits.
    33   ;; The window has title "hooray!!!", maybe.
    34
    35  @@ -26,7 +28,10 @@
    36
    37   (use-modules
    38    ((srfi srfi-11) #:select (let-values))
    39  - ((sdl misc-utils) #:select (copy-surface))
    40  + ((sdl misc-utils) #:select (copy-surface
    41  +                             exact-truncate))
    42  + ((sdl gfx) #:select (blit-rgba
    43  +                      imfi-sub-c))
    44    ((sdl sdl) #:prefix SDL:))
    45
    46   (SDL:init 'video)
    47  @@ -133,9 +138,119 @@ (define smokin
    48           (SDL:delay millisecs))
    49         (newline))))
    50
    51  +(define FG-EDGE-LENGTH 32)
    52  +
    53  +(define (make-fg-rect)
    54  +  (SDL:make-rect 0 0
    55  +                 FG-EDGE-LENGTH
    56  +                 FG-EDGE-LENGTH))
    57  +
    58  +(define hooray!
    59  +  (let ((throng (cond ((pair? (cddr (command-line)))
    60  +                       (string->number (caddr (command-line))))
    61  +                      (else
    62  +                       9)))
    63  +        (step-count 42)
    64  +        (src (make-fg-rect)))
    65  +
    66  +    (define (svec)
    67  +      (make-vector step-count))
    68  +
    69  +    (define (make-put win)
    70  +      ;; Avoid ‘copy-surface’; it uses ‘blit-surface’ which disregards alpha.
    71  +      ;;- (copy-surface win src)
    72  +      (let ((surface (SDL:create-rgb-surface
    73  +                      '(src-alpha)
    74  +                      FG-EDGE-LENGTH    ; w
    75  +                      FG-EDGE-LENGTH    ; h
    76  +                      32                ; depth
    77  +                      #x000000FF
    78  +                      #x0000FF00
    79  +                      #x00FF0000
    80  +                      #xFF000000)))
    81  +        ;; We can use ‘src’ for dest rect only because its xy stays 0 0.
    82  +        ;; Some day ‘blit-rgba’ will accept ‘#f’ for 2nd, 4th args to mean
    83  +        ;; "full surface", and we will be able to change this accordingly.
    84  +        (blit-rgba win src surface src)
    85  +        surface))
    86  +
    87  +    (define (fg-step put fraction)
    88  +
    89  +      (define (dim! surface)
    90  +        (let ((c (exact-truncate (* 256 fraction))))
    91  +          ;; Ideally we would operate only on the alpha channel
    92  +          ;; by using ‘(ash c 24)’ instead of ‘c’ here.
    93  +          ;; Unfortunately Guile-SDL 0.5.3 ‘imfi-sub-c’ has a bug
    94  +          ;; where values ‘(ash 128 24)’ and up signal range error.
    95  +          ;; When next Guile-SDL is released, we will update this.
    96  +          (imfi-sub-c surface surface c)
    97  +          surface))
    98  +
    99  +      (dim! put))
   100  +
   101  +    (define (/2 n)
   102  +      (ash n -1))
   103  +
   104  +    (define (half aspect)
   105  +      ;; rv
   106  +      (lambda (surface)
   107  +        (/2 (aspect surface))))
   108  +
   109  +    (define (tvec)
   110  +      (make-vector throng))
   111  +
   112  +    (define (random-c offset)
   113  +      (let ((origin (+ (/2 FG-EDGE-LENGTH)
   114  +                       (offset (* TILE-EDGE-LENGTH 2)
   115  +                               (/2 FG-EDGE-LENGTH)))))
   116  +        ;; rv
   117  +        (lambda ()
   118  +          (+ origin (random 29) -14))))
   119  +
   120  +    (define (place-fg! fg w/2 h/2)
   121  +      ;; rv
   122  +      (lambda (dst x y)
   123  +        (SDL:rect:set-x! dst (- x w/2))
   124  +        (SDL:rect:set-y! dst (exact-truncate (- y h/2)))
   125  +        (SDL:blit-surface fg #f #f dst)))
   126  +
   127  +    (let ((fg (svec))
   128  +          (w/2 (svec))
   129  +          (h/2 (svec))
   130  +          (x (tvec))                    ; location
   131  +          (y (tvec))
   132  +          (dst (tvec)))
   133  +
   134  +      (array-index-map! fg (lambda (i)
   135  +                             (fg-step (make-put (surf 'win))
   136  +                                      (/ i step-count))))
   137  +      (array-map! w/2 (half SDL:surface:w) fg)
   138  +      (array-map! h/2 (half SDL:surface:h) fg)
   139  +      (array-map! dst make-fg-rect)
   140  +
   141  +      ;; hooray!
   142  +      (lambda ()
   143  +        (array-map! x (random-c +))
   144  +        (array-map! y (random-c -))
   145  +        (array-for-each (lambda (rect x y)
   146  +                          (SDL:rect:set-x! rect x)
   147  +                          (SDL:rect:set-y! rect y))
   148  +                        dst x y)
   149  +        (array-for-each
   150  +         (lambda (fg w/2 h/2)
   151  +           (refresh-scenery!)
   152  +           (array-for-each (place-fg! fg w/2 h/2)
   153  +                           dst x y)
   154  +           (SDL:flip)
   155  +           (SDL:delay 42))
   156  +         fg w/2 h/2)))))
   157  +
   158   (set! *random-state* (seed->random-state (let ((now (gettimeofday)))
   159                                              (* (car now) (cdr now)))))
   160   (smokin 9)
   161  +(do ((i 0 (1+ i)))
   162  +    ((= 5 i))
   163  +  (hooray!))
   164
   165   (SDL:delay 420)
   166   (exit (SDL:quit))

First off, patch 8 introduces another (optional) command-line argument: the ‘THRONG’ (line 26, lines 59-62).  The behavior description (lines 30-31) tells us the nature of this arg and points out the direction of development: towards parallelism.  We'll have more to say on the matter in a later Trip Log. 

Next, we notice a familiar pattern, the addition of constant ‘FG-EDGE-LENGTH’ (line 51) and the convenience procedure ‘make-fg-rect’ (an abstraction) that uses it (lines 53-56).  Remember how we noticed that the ‘win’ tile was special?  Well, that's now codified in ‘FG-EDGE-LENGTH’ and ‘make-fg-rect’. 

Now for the big ‘hooray!’ procedure.  It has many internal procedures (lines 66-125), which we will describe next.  Before doing that, a small explanation of the overall strategy that ‘hooray!’ takes is in order. 

There are two axes to think about for this animation.  The first is the parallelism — the ‘throng’ value; the second is the step count (line 63) — how fine-grained the animation is to be.  The strategy ‘hooray!’ takes is to present ‘throng’ fg images (some people call them “sprites”) in parallel, a ‘step-count’ number of times.  For each step, something varies in the presentation — that's how the illusion of motion is created from still frames. 

To organize the data on these axes, we use regular Scheme vectors.  That's what ‘svec’ (lines 66-67) and ‘tvec’ (lines 109-110) provide.  Thus, we use ‘svec’ for things that might change every step, such as ‘fg’, ‘w/2’, and ‘h/2’ (lines 127-129), and ‘tvec’ for thronging things, such as location (lines 130-132). 

Speaking of location, there is a difference between the coordinate system we use for the scenery and the one for the foreground.  Because scenery dimensions and placement are relatively static, it's enough to refer to their location by the tile images' regular (upper-left-hand corner) coordinates.  Not so for the foreground!  Because we anticipate that the foreground image will change (spoiler: that doesn't happen in patch 8 — sorry), it's better to refer to the foreground by its “center” coordinates.  Here's a picture that illustrates the difference: 

The empty circle marks the upper-left-hand corner coordinate of ‘win’, while the filled circle marks the center coordinate.  This helps explain the existence of ‘w/2’ and ‘h/2’ and the hair surrounding the computation of ‘random-c’ (lines 112-118) local variable ‘origin’ (line 113).  Note that ‘place-fg!’ (lines 120-125) must eventually translate the center coordinate when it sets up the ‘dst’ rect (lines 123-124), since SDL underestands only upper-left-hand corner coordinates. 

As for what comprises the set of foreground images, hopefully the comments in ‘make-put’ (lines 69-85) and ‘fg-step’ (lines 87-99) are self-explanatory.  If not, feel free to let me know (hint for the Italiano-phobes: “URL non è chiaro per niente!”).  The main thing to note there is that each step gets its own image, and that the steps are parameterized by their ‘fraction’ (line 87). 

So, what is the result of this parameterization?  Here's what I see when I run the command “guile -s hooray.scm Hellsfire.png”: 

It appears the only change is the transparency of the throng.  They aren't even moving!  Well, that, dear reader, is for a future Trip Log.  For now, we'll just check off the aspects of our emulation that we've achieved thus far: 

* PipeWalker fg behavior (to reproduce if possible)
  - [X] starts near top of center tile
  - [ ] mostly upward velocity
  - [ ] rotates clockwise slightly
  - [ ] starts smaller than 32x32
  - [ ] grows quickly
  - [ ] non synchronized (all aspects)
  - [x] fades to transparent

Actually, “fades to transparent” is debatable (hence the small “x”).  There is indeed fading, but the way we did it is not ideal.  In other words, it looks okay for Hellsfire, but not so nice for other themes (such as Network) where the fg image is lighter-colored.  Perhaps with the next release of Guile-SDL, we can fix things and change that small “x” to a big “X” in good conscience. 

See you next time! 


Copyright (C) 2022 Thien-Thi Nguyen