gpsanimdemo



#!/usr/bin/env perl
# gpsanimdemo - Animation demo that uses Gimp Perl Server
# License:  BSD-style [for this file only]
# Revision: 070913

# Note: The license indicated above applies to this  file.  It doesn't
# apply to the sample PNG input files provided. The PNG files that are
# being used  at this time are licensed under the Nethack General Pub-
# lic License.

#---------------------------------------------------------------------
#                              overview
#---------------------------------------------------------------------

                                # Single quotes must be used here
my $DOCUMENTATION = << 'END_OF_DOCUMENTATION';

"gpsanimdemo" is a  Gimp Perl Server  demo program.  This program uses
the server to create an animated GIF. The animated GIF displays a ser-
ies of images moving past a rotating globe.

This program  requires  one or more  PNG input files.  The input files
should contain  small square PNG images [85x85 to 300x300] with trans-
parent backgrounds.  Non-transparent backgrounds won't work correctly.
The sizes don't need to be consistent, as long as they're square.  The
images are scaled automatically.

The input files should be stored in the current directory. Their names
should be specified [without paths] in the source code  [specifically,
in @GPSDEMOINPUT].

Numerous  temporary files are created  [possibly several hundred files
or more].  They're stored in a subdirectory of  "/var/tmp".  The temp-
orary files are normally removed on exit.  However, if they're not re-
moved  [due to Control C, etc.],  the system will probably remove them
on the next "boot"  [assuming that it's  designed to  clean up  "/var/
tmp"].

One output file  is created  [not counting the  temporary files].  The
output file is an animated GIF named "gpsanimdemo.gif". It's stored in
the current directory. To view the animated GIF, use any Mozilla (tm)-
based browser.

Credits:  One subroutine in this file [PlanetFrame] uses an  algorithm
based on "daoo's" Gimp "planet" plugin. The rest of the code is new.

Requirements:  This script requires Perl 5.8.X, The Gimp 2.2.X,  Gimp-
Perl 2.2,  ImageMagick 6.3.X, gifsicle 1.48, and intergif 6.15.  Older
and/or newer versions may not work.

END_OF_DOCUMENTATION

#---------------------------------------------------------------------
#                        standard module setup
#---------------------------------------------------------------------

require 5.6.1;
use strict;
use Carp;
use warnings;
use Cwd;
                                # Trap warnings
$SIG {__WARN__} = sub { die @_; };

#---------------------------------------------------------------------
#                        Gimp-specific modules
#---------------------------------------------------------------------

use Gimp ":auto";
use Gimp::Fu;

#---------------------------------------------------------------------
#                           basic constants
#---------------------------------------------------------------------

use constant ZERO  => 0;        # Zero
use constant ONE   => 1;        # One
use constant TWO   => 2;        # Two

use constant FALSE => 0;        # Boolean FALSE
use constant TRUE  => 1;        # Boolean TRUE

                                # Standard geometry definitions
my $DEGREES_PER_CIRCLE      = 360;
my $DEGREES_PER_HALF_CIRCLE = 180;

#---------------------------------------------------------------------
#                         program parameters
#---------------------------------------------------------------------

my $PROGNAME = 'gpsanimdemo';   # Program name [must be one word]
                                # Internal-error message prefix
my $IE       = "Internal error: $PROGNAME";

my $TEMPGIF  = "temp$>-$$.gif"; # GIF  temporary-file name  [without a
                                # path component]

#---------------------------------------------------------------------

                                # Output-file name  [this  should be a
                                # single word plus ".gif"]
my $IMG_OUTPUT_BASENAME  = "$PROGNAME.gif";

                                # Output-image  width and  height [ex-
                                # pressed in pixels]
my $IMG_OUTPUT_WIDTH     = 100;
my $IMG_OUTPUT_HEIGHT    = 100;

#---------------------------------------------------------------------

# $IGDELAY specifies the time delay that  should  be inserted  between
# frames in the animated-GIF output file. The  delay  is  expressed in
# centiseconds [i.e., hundredths of a second].  The factory setting is
# 6.

# Note:  If you change $DIVIDE_BY_N,  you'll probably need  to  change
# $IGDELAY as well. If $DIVIDE_BY_N is equal to  one, try $IGDELAY = 6
# as a  starting point.  If it's equal  to  two,  try $IGDELAY = 7 in-
# stead.

my $IGDELAY = 6;                # "intergif" frame-delay setting

#---------------------------------------------------------------------

#  $DISTANCE is a "scale" factor. The suggested  range  is  0.5 to 6.0
# and the factory setting is 4.2. Larger values produce smaller globes
# and conversely.

my $DISTANCE = 4.2;             # Distance from planet [0.5 to 6.0]

#---------------------------------------------------------------------

#  $IGNUMCOLORS specifies the number of colors that should  be used in
# the animated-GIF output file. The maximum value allowed is 256. This
# is also the factory setting.

my $IGNUMCOLORS = 256;          # "intergif" color-count setting

#---------------------------------------------------------------------

# The $IMG_CYCLED_... parameters specify  the dimensions used when one
# of the @GPSDEMOINPUT images is displayed.  Note: The images in ques-
# tion don't need  to use  these dimensions  initially. They're scaled
# automatically.

my $IMG_CYCLED_WIDTH  = 70;     # Width  [in pixels]
my $IMG_CYCLED_HEIGHT = 70;     # Height [in pixels]

#---------------------------------------------------------------------

# $DIVIDE_BY_N is a  quality-related parameter.  This parameter can be
# used to skip frames. The following values are supported:
#
#     1   - This generates 100% of the possible frames [best  quality]
#     2   - This generates  50% of the possible frames [lower quality]
#     4   - This generates  25% of the possible frames [etc.         ]
#     8   - This generates  12% of the possible frames

# Normally, $DIVIDE_BY_N should be set to one or two. The other values
# are  intended for  debugging  purposes only.  The factory setting is
# one.

# Note:  If you change  $DIVIDE_BY_N,  you'll probably  need to change
# $IGDELAY as well.  For more information, see the notes preceding the
# definition of $IGDELAY.

my $DIVIDE_BY_N = ONE;

#---------------------------------------------------------------------

# The output animation displays a set of floating images  moving left-
# to-right  across a  rotating globe. $FRAME_MODE_SELECTED  determines
# the relationship between motion and frames. If this parameter is set
# to  $FRAME_MODE_DEGREES,  the globe rotates $DIVIDE_BY_N degrees per
# frame.  If it's set to  $FRAME_MODE_PIXELS, the floating images move
# forward $DIVIDE_BY_N pixels per frame.

# The factory setting for  $FRAME_MODE_SELECTED is $FRAME_MODE_PIXELS.
# This setting is recommended. In most cases, it should produce better
# results. However, there may be cases where $FRAME_MODE_DEGREES works
# better.

my $FRAME_MODE_DEGREES  = ONE;
my $FRAME_MODE_PIXELS   = TWO;
my $FRAME_MODE_SELECTED = $FRAME_MODE_PIXELS;

#---------------------------------------------------------------------

# $CYCLES_PER_RUN  specifies  the  number of  times  that the complete
# group of floating images will cycle per one rotation  of the  globe.
# The factory setting is one.

# Note:  The optimal value  depends on several factors, including  the
# floating images used.  For example,  if you  decrease the  number of
# floating images, you may need to increase $CYCLES_PER_RUN.

my $CYCLES_PER_RUN = ONE;

#---------------------------------------------------------------------

#  $TMPDIR should specify an absolute pathname for the temporary-files
# directory to be used.  Note: The pathname  should  include  both the
# $PROGNAME string  defined  previously  and  the  word  "tmp" as sub-
# strings. This rule is a safety measure [it makes a consistency check
# possible].

# The directory doesn't need to exist initially.  In fact, if the dir-
# ectory already exists, this script deletes it.  Warning: This script
# normally  deletes the  specified directory  both at  startup  and on
# exit.

my $TMPDIR = "/var/tmp/${PROGNAME}tmp-$>-$$";

#---------------------------------------------------------------------

#  @GPSDEMOINPUT should specify one or more PNG-file names. The speci-
# fied files should already exist.  They should  contain  small square
# PNG images [85x85 to  300x300]  with  transparent  backgrounds.  The
# names shouldn't include  path  components.  Presently,  the  program
# looks for these files in the current directory.

my @GPSDEMOINPUT = qw
(
    floatcat.png floatdog.png floatele.png
);

#---------------------------------------------------------------------
#                          global variables
#---------------------------------------------------------------------

my %ARGS = ();                  # Subroutine argument list

my $CWD;                        # Current  working  directory  [stored
                                # with a trailing forward slash]

my $FrameNumber = ONE;          # Frame number

my @IMG_CYCLED_IMAGE    = ();   # "image" pointers for cycled images
my @IMG_CYCLED_DRAWABLE = ();   # Associated "drawable" pointers

my $IMG_OUTPUT_PATHNAME;        # Absolute pathname for output file

my $IMG_CYCLED_TOTAL_WIDTH;     # Total width  [in pixels] of  all  of
                                # the cycled images [when they're laid
                                # out in a row]

#---------------------------------------------------------------------
#                         low-level routines
#---------------------------------------------------------------------

# Usage:    &net();

# The Gimp-Perl framework used  requires this subroutine  [even though
# it doesn't do anything].

#---------------------------------------------------------------------

sub net {}

#---------------------------------------------------------------------

# Usage:    &RemoveTempDir();

# RemoveTempDir performs some sanity checks related to the TMPDIR set-
# ting.  If the TMPDIR setting is valid, this routine deletes the dir-
# ectory specified by the setting.  Otherwise, it prints an error mes-
# sage and exits.

# Note:  In the latter case, RemoveTempDir  prints  an  error  message
# [and exits] directly.  It doesn't call ProgExit,  as this would lead
# to a recursive call; i.e., ProgExit calls *this* routine.

#---------------------------------------------------------------------

sub RemoveTempDir
{
    if (($PROGNAME =~ m@^\w([a-z0-9_\-]*\w|)\z@i) &&
        ($TMPDIR   =~ m@^/@) &&
        ($TMPDIR   =~ m@$PROGNAME@) &&
        ($TMPDIR   =~ m@tmp@))
    {
        system "/bin/rm -fr $TMPDIR";
    }
    else
    {
        print STDERR "$IE: Invalid TMPDIR setting\n";
        exit ONE;
    }

    undef;
}

#---------------------------------------------------------------------

# Usage:    &ProgExit();
#           &ProgExit ($errmsg);

# If no arguments  are specified,  "ProgExit"  terminates the program.
# The exit status is zero, in this case.

# If $errmsg is specified, ProgExit  prints $errmsg to STDERR  [with a
# trailing newline added] and exits with status one.

# ProgExit removes the program's temporary-files directory,  if possi-
# ble.

# Leading/trailing white space in $errmsg is ignored.  Additionally, a
# blank or empty string counts as no string.

#---------------------------------------------------------------------

sub ProgExit
{
    my ($errmsg) = @_;          # Argument list

    &RemoveTempDir();           # Remove temporary-files directory
                                # Adjust the specified string
    $errmsg =  "" unless defined $errmsg;
    $errmsg =~ s@^\s+@@s;
    $errmsg =~ s@\s+\z@@s;
                                # If no message, take "success" exit
    exit ZERO if !length $errmsg;
                                # Otherwise:
    print STDERR $errmsg, "\n"; # Print the adjusted string
    exit ONE;                   # Error exit
}

#---------------------------------------------------------------------

# Usage:    %ARGS = ...;
#           my $result = &GetArg ($switch);
# or        my $result = &GetArg ($switch, $default);
#
# GetArg returns the value of $ARGS{$switch}  [where %ARGS is a global
# hash table].  If the  value  is  undefined,  blank,  or  empty,  and
# $default is specified,  GetArg uses $default  instead.  If the final
# result [taking $default into account] is undefined, blank, or empty,
# GetArg prints an error message and terminates the program.

# Note: GetArg discards leading/trailing white space in $switch and/or
# $default.

#---------------------------------------------------------------------

sub GetArg
{
                                # Argument list
    my ($switch, $default) = @_;
    my $str;                    # Scratch

    $str =  $ARGS {$switch};
    $str =  ""       unless defined $str;
    $str =  $default unless $str =~ m@\S@;
    $str =  ""       unless defined $str;
    $str =~ s@^\s+@@;
    $str =~ s@\s+\z@@;

    if (!length ($str))
    {
        &ProgExit ("$IE: Missing or empty switch: $switch");
    }

    $str;                       # Final result
}

#---------------------------------------------------------------------
#                      "planet" frame generator
#---------------------------------------------------------------------

# This  routine  [PlanetFrame]  uses  the same  algorithm as  the Gimp
# "planet" plug-in written  by "daoo".  Four changes  worth mentioning
# have been made:
#
#     a. This code is written in Perl, as opposed to SCM.
#
#     b. The make-new-image flag used by "plug_in_map_object" has been
#        changed from FALSE to TRUE. Without this change, Perl doesn't
#        seem to be able to access the generated image [it sees one of
#        the intermediate images instead].
#
#     c. "PlanetFrame" takes a background-color argument  [in addition
#        to the original transparent-background flag].  If the  trans-
#        parent-background  flag  is TRUE,  the background-color value
#        isn't used.  Otherwise,  the generated image's  background is
#        set to the specified color.
#
#     d. A distance-from-planet argument has also been added.

#---------------------------------------------------------------------

sub PlanetFrame
{
    my ($filltype, $pattern, $width, $height, $randomseed, $seed,
        $detail, $xscale, $yscale, $gradient, $makeglobe, $xform,
        $lightcolor, $hilight, $rotationx, $rotationy, $rotationz,
        $transbg, $bgcolor, $distance) = @_;

    my $FULL_OPACITY = 100;

#---------------------------------------------------------------------

# Note:  To obtain  documentation for the "gimp_" and  "plug_in_" rou-
# tines used below,  run The Gimp and use the Procedure Browser, which
# can be accessed as follows:
#
#     Xtns -> Procedure Browser

#---------------------------------------------------------------------

    my $img       = gimp_image_new ($width, $height, RGB);
    my $display   = gimp_display_new ($img);

    my $layer_one = gimp_layer_new
        ($img, $width, $height, RGB_IMAGE,
            "bottom", $FULL_OPACITY, NORMAL_MODE);

    my $old_bg    = gimp_context_get_background();
    gimp_context_set_background ($bgcolor);

    gimp_image_undo_group_start ($img);
    gimp_image_add_layer ($img, $layer_one, ZERO);
    gimp_selection_all ($img);
    gimp_edit_clear ($layer_one);
    gimp_selection_none ($img);

    if (!$filltype)
    {
        if ($randomseed)
        {
            plug_in_solid_noise (ONE, $img,
                $layer_one, ONE, ZERO, rand (4294967295),
                $detail, $xscale, $yscale);
        }
        else
        {
            plug_in_solid_noise (ONE, $img,
                $layer_one, ONE, ZERO, $seed, $detail,
                $xscale, $yscale);
        }

        plug_in_c_astretch (ONE, $img, $layer_one);
        gimp_image_set_active_layer ($img, $layer_one);
        gimp_context_set_gradient ($gradient);
        plug_in_gradmap (ONE, $img, $layer_one);
    }
    else
    {
        gimp_context_set_pattern ($pattern);
        gimp_edit_bucket_fill
            ($layer_one, TWO, ZERO, 100, ZERO, FALSE, ZERO, ZERO);
    }

    if ($makeglobe)
    {
        plug_in_map_object (ONE, $img , $layer_one , $xform ,
            0.5  , 0.5  , $distance   ,
            0.5  , 0.5  , 0.5         ,
            ONE  , ZERO , ZERO , ZERO , ONE , ZERO    ,
            $rotationx  , $rotationy  , $rotationz    ,
            ZERO        , $lightcolor ,
            -0.5 , -0.5 , TWO  , -1   , -1  , 1 , 0.3 ,
            1    ,  0.5 , 0.5  , $hilight   ,
            TRUE , TRUE , TRUE , $transbg   ,
            0.25 ,  0.5 , 0.5  , 0.5  , 0.8 ,
            -1   , -1   , -1   , -1   , -1  , -1 , -1 , -1);
    }

    gimp_context_set_background ($old_bg);
    gimp_image_flatten ($img);
    gimp_image_undo_group_end ($img);
    undef;
}

#---------------------------------------------------------------------
#                        main frame generator
#---------------------------------------------------------------------

# Usage:
#
# &MakeFrame
# (
#     -x1_rotation => ... , # See "plug_in_map_object"
#     -y1_rotation => ... , # Ditto
#     -z1_rotation => ... , # Ditto
#     -x2_offset   => ... , # See the following notes
#     -y2_offset   => ... , # Vertical offset from top of output image
#                           # to top of cycled image[s]
#     -width       => ... , # Output-image width
#     -height      => ... , # Output-image height
#     -distance    => ... , # Distance from planet [0.5 to 6.0]
# );

# MakeFrame  generates one frame and  writes the frame  to disk in PNG
# format.  The pathname used is  based on several  global parameter[s]
# and variable[s], including the current frame number.

# Note:  The  frame number is  stored in the  global variable  $Frame-
# Number.  The caller [or the main program] should set $FrameNumber to
# one initially.  MakeFrame  increments the  variable  before  return-
# ing.

# This routine treats the set of  cycled images as a  contiguous panel
# [with the  images laid out  horizontally].  "-x2_offset" specifies a
# horizontal pixel offset from the left side of the panel to a pointer
# that's moving left-to-right across the panel. The pointer's position
# determines  which image[s] are added to the current frame.  For more
# information, see the MakeFrame source code.

#---------------------------------------------------------------------

sub MakeFrame
{
    %ARGS = @_;                 # Argument list
    my $n;                      # Scratch [integer]
    my $str;                    # Scratch [string ]

#---------------------------------------------------------------------
# Extract arguments.

    my $x1_rotation = &GetArg ('-x1_rotation' , 0.0     );
    my $y1_rotation = &GetArg ('-y1_rotation' , 0.0     );
    my $z1_rotation = &GetArg ('-z1_rotation' , 0.0     );

    my $x2_offset   = &GetArg ('-x2_offset'   , ZERO    );
    my $y2_offset   = &GetArg ('-y2_offset'   , ZERO    );

    my $width       = &GetArg ('-width'       , 100     );
    my $height      = &GetArg ('-height'      , 100     );
    my $distance    = &GetArg ('-distance'    , TWO     );

#---------------------------------------------------------------------
# Print a status message.

    print "Frame #$FrameNumber\n";

#---------------------------------------------------------------------
# Adjust the specified X-Y-Z "rotation" values.

    for my $ref_rotation
        (\$x1_rotation, \$y1_rotation, \$z1_rotation)
    {
        $$ref_rotation -= $DEGREES_PER_CIRCLE if
            $$ref_rotation > $DEGREES_PER_HALF_CIRCLE;
    }

#---------------------------------------------------------------------
# Generate an appropriate "planet" image.

    &PlanetFrame
    (
        ZERO                  , # Fill type
        "Maple Leaves"        , # Pattern name
        $width                , # Image width
        $height               , # Image height
        FALSE                 , # FALSE = Disable random
        456788                , # Seed
        4.0                   , # Detail
        4.0                   , # X_scale
        4.0                   , # Y_Scale
        "Land and Sea"        , # Gradient name
        TRUE                  , # TRUE  = Make a form
        ONE                   , # Form number (0+)
        [255,255,255]         , # Illumination color
        27.0                  , # Highlight
        $x1_rotation          , # X_rotation [ -180 to +180 ]
        $y1_rotation          , # Y_rotation [  ditto       ]
        $z1_rotation          , # Z_rotation [  ditto       ]
        FALSE                 , # TRUE  = Transparent background
        [0,0,0]               , # Background  color   [if  transparent
                                # mode is disabled]
        $distance               # Distance from planet [0.5 to 6.0]
    );

#---------------------------------------------------------------------
# Obtain the planet's Gimp "image" and "drawable" pointers.

    my $img      = gimp_image_list();
    my $drawable = gimp_image_active_drawable ($img);

#---------------------------------------------------------------------
# Compute some relevant parameters.

# $abc1 is  approximately equal to the  maximum  number  of cycled im-
# ages that can be visible [wholly or partially] in the  output  image
# at one time.  It may be slightly greater than the actual number, but
# it shouldn't be less.

# $abc2 is equal to two times $abc1.

    my $abc1 = int ($IMG_OUTPUT_WIDTH / $IMG_CYCLED_WIDTH) + TWO;
    my $abc2 = $abc1 * TWO;

#---------------------------------------------------------------------
# Add the cycled images.

    for my $ii (ZERO..$#IMG_CYCLED_DRAWABLE)
    {
        my $layer;
        gimp_edit_copy ($IMG_CYCLED_DRAWABLE [$ii]);

        for my $jj (ZERO..$abc2)
        {
            my $kk = ($jj - $abc1) * $IMG_CYCLED_TOTAL_WIDTH;

            if ((($n = $kk + $x2_offset - ($IMG_CYCLED_WIDTH * $ii))
                    > -$IMG_CYCLED_WIDTH) &&
                 ($n < $IMG_OUTPUT_WIDTH))
            {
                $layer = gimp_edit_paste ($drawable, ZERO);
                gimp_layer_set_offsets ($layer, $n, $y2_offset);
            }
        }
    }

#---------------------------------------------------------------------
# Flatten [or re-flatten] the result.

    gimp_image_flatten ($img);

#---------------------------------------------------------------------
# Save the completed frame to disk.

                                # Construct the appropriate pathname
    my $nn = sprintf ('%04d', $FrameNumber++);
    $str   = $IMG_OUTPUT_BASENAME;

                                # Consistency check
    &ProgExit ("$IE Invalid IMG_OUTPUT_BASENAME setting")
        unless $str =~ s@(\w)[_\-]*\d*\.gif\z@$1-$nn.png@;

                                # Absolute path for "frame" PNG file
    my $FramePath = "$TMPDIR/$str";

    $drawable = gimp_image_active_drawable ($img);

    file_png_save (RUN_NONINTERACTIVE, $img, $drawable,
        $FramePath, $FramePath,
        ZERO, 9, ZERO, ZERO, ZERO, ZERO, ZERO);

#---------------------------------------------------------------------
# Wrap it up.

    gimp_displays_flush();
    undef;
}

#---------------------------------------------------------------------
#                            main routine
#---------------------------------------------------------------------

sub Main
{
    my $FrameCount;             # Number of frames
    my $errmsg;                 # Error message
    my $n;                      # Scratch [integer]
    my $str;                    # Scratch [string ]

#---------------------------------------------------------------------
# Initial setup.

    select STDERR; $| = ONE;    # Set flush-on-write mode
    select STDOUT; $| = ONE;    # Ditto

                                # Consistency check
    if ($PROGNAME !~ m@^\w([a-z0-9_\-]*\w|)\z@i)
    {                           # Internal error
        $IE = 'Internal error';
        &ProgExit ("$IE: PROGNAME setting must be exactly one word")
    }

    $CWD =  getcwd();           # Current directory
    $CWD =~ s@/*\z@/@;          # Add a "/" [if necessary]

                                # Absolute pathname for output file
    $IMG_OUTPUT_PATHNAME = $CWD . $IMG_OUTPUT_BASENAME;

    &RemoveTempDir();           # Remove temporary directory

                                # Consistency check
    &ProgExit ("$IE: IMG_CYCLED_HEIGHT must be <= IMG_OUTPUT_HEIGHT")
        unless $IMG_CYCLED_HEIGHT <= $IMG_OUTPUT_HEIGHT;

#---------------------------------------------------------------------
# Connect to The Gimp.

                                # Current "process" information
    $str = `/bin/ps ax 2>&1`;
    $str = "" unless defined $str;
                                # Is a background [i.e., gimp -i] in-
                                # stance of The Gimp running?
    if ($str !~ m@\bgimp -i\s*(\n|\z)@)
    {                           # No  - Try to start one
        system ("gimp -i >& /dev/null &");
        sleep 3;                # This code needs some work
    }

    Gimp::init;                 # Code related to Gimp Perl Server
    Gimp::on_net (\&net);       # Ditto

#---------------------------------------------------------------------
# Create temporary-files directory.

# Note: If the directory  in question  existed previously,  it was de-
# leted by this program's initial-setup code.

    system "mkdir -p $TMPDIR";

#---------------------------------------------------------------------
# Set up image pathnames and related parameters.

    my @IMG_CYCLED_PATHNAME = ();

    for my $ii (ZERO..$#GPSDEMOINPUT)
    {                           # Process the next image
        my ($x_path, $x_image, $x_draw);

        if (($str = $GPSDEMOINPUT [$ii]) =~ m@/@)
        {
            $errmsg = << "END";
$IE: GPSDEMOINPUT should specify filenames without path
components
END
            &ProgExit ($errmsg);
        }

        $IMG_CYCLED_PATHNAME [$ii] = $x_path  = $CWD . $str;
        &ProgExit ("$IE: Missing input file:\n$x_path")
            unless -f $x_path;

        $IMG_CYCLED_IMAGE    [$ii] = $x_image = file_png_load
            (RUN_NONINTERACTIVE, $x_path, $str);

        $x_draw = gimp_image_active_drawable ($x_image);

        $x_draw = gimp_drawable_transform_scale ($x_draw,
            ZERO, ZERO, $IMG_CYCLED_WIDTH, $IMG_CYCLED_HEIGHT,
            ZERO, TWO , TRUE, 3, FALSE);

        $IMG_CYCLED_DRAWABLE [$ii] = $x_draw;
    }

#---------------------------------------------------------------------
# Consistency checks.

    my $drawable1 = $IMG_CYCLED_DRAWABLE [ZERO];
    my $width1    = gimp_drawable_width  ($drawable1);
    my $height1   = gimp_drawable_height ($drawable1);

    &ProgExit ("$IE: Scaling didn't work as expected")
        unless ($width1  == $IMG_CYCLED_WIDTH ) &&
               ($height1 == $IMG_CYCLED_HEIGHT);

#---------------------------------------------------------------------
# Compute misc. required values.

                                # Total width  [in pixels] of  all  of
                                # the cycled images [when they're laid
                                # out in a row]
    $IMG_CYCLED_TOTAL_WIDTH  =
        scalar (@GPSDEMOINPUT) * $IMG_CYCLED_WIDTH;

                                # Vertical offset to cycled images
    my $IMG_CYCLED_VOFFSET   = $IMG_OUTPUT_HEIGHT - $IMG_CYCLED_HEIGHT;

# When the  displayed globe rotates one degree, the cycled images move
# left-to-right  by a [possibly fractional] number of pixels.  $PIXEL_
# STEP_PER_DEGREE  is equal to the pixel step in question.  Note: This
# is a floating-point value.

    my $PIXEL_STEP_PER_DEGREE =
        ($CYCLES_PER_RUN / $DEGREES_PER_CIRCLE) *
            $IMG_CYCLED_TOTAL_WIDTH;

#---------------------------------------------------------------------
# Compute number of frames.

    if ($FRAME_MODE_SELECTED == $FRAME_MODE_DEGREES)
    {                           # See notes at start of file
        $FrameCount = $DEGREES_PER_CIRCLE;
    }
    elsif ($FRAME_MODE_SELECTED == $FRAME_MODE_PIXELS)
    {                           # Ditto
        $FrameCount = $IMG_CYCLED_TOTAL_WIDTH;
    }
    else
    {                           # Internal error
        &ProgExit ("$IE: FRAME_MODE_SELECTED is set incorrectly");
    }

#---------------------------------------------------------------------
# Main loop.

    for my $PossibleFrame (ONE..$FrameCount)
    {                           # Generate the next frame
        my $DeltaFrames = $PossibleFrame - ONE;
        my $DeltaDegrees;
        my $DeltaPixels;

        if (defined ($DIVIDE_BY_N) &&
            ($DIVIDE_BY_N > ONE) && ($DIVIDE_BY_N <= 8))
        {                       # See notes at start of file
            next unless ($DeltaFrames % $DIVIDE_BY_N) == ZERO;
        }

        if ($FRAME_MODE_SELECTED == $FRAME_MODE_DEGREES)
        {                       # See notes at start of file
            $DeltaDegrees =  $DeltaFrames;
            $n            =  int (($DeltaDegrees *
                                 $PIXEL_STEP_PER_DEGREE) + 0.5);
            $n            %= $IMG_CYCLED_TOTAL_WIDTH;
            $DeltaPixels  =  $n;
        }
        elsif ($FRAME_MODE_SELECTED == $FRAME_MODE_PIXELS)
        {                       # Ditto
            $DeltaPixels  = $DeltaFrames;
            $DeltaDegrees = $DeltaPixels / $PIXEL_STEP_PER_DEGREE;
        }
        else
        {                       # Internal error
            &ProgExit ("$IE: FRAME_MODE_SELECTED is set incorrectly");
        }

        &MakeFrame
        (                       # Call single-frame generator
            -x1_rotation => ZERO                ,
            -y1_rotation => $DeltaDegrees       ,
            -z1_rotation => ZERO                ,
            -x2_offset   => $DeltaPixels        ,
            -y2_offset   => $IMG_CYCLED_VOFFSET ,
            -width       => $IMG_OUTPUT_WIDTH   ,
            -height      => $IMG_OUTPUT_HEIGHT  ,
            -distance    => $DISTANCE
        );
    }

#---------------------------------------------------------------------
# Go to temporary-files directory.

    chdir ($TMPDIR) ||
        &ProgExit ("$IE: Couldn't go to temporary directory: $!");

#---------------------------------------------------------------------
# Convert PNG frames to GIF format [using ImageMagick].

# Note:  The input is the set of  PNG  files created  previously.  The
# output is a set of GIF files with names of the form "$PROGNAME-####.
# png.gif".

    print "\n";
    print << 'END';             # 'END' should be single-quoted here
Converting PNG frames to GIF format [using ImageMagick]. This may take
a while.
END
    $str = << "END";            # "END" must be double-quoted here
ls -1 $PROGNAME-[0-9][0-9][0-9][0-9].png |
    xargs -I "{}" convert "{}" "{}.gif"
END
    $str =~ s@\s+\z@@s;
    $str =~ s@\s*\n\s*@ @gs;
    system $str;

# Note:  The following  consistency check  assumes  that  output-frame
# numbers start at one.

                                # Consistency check
    &ProgExit ("$IE: The ImageMagick step failed")
        unless -f "$PROGNAME-0001.png.gif";

#---------------------------------------------------------------------
# Use "gifsicle" to combine the frames.

# Note:  The input is the set of GIF files  created in  the  preceding
# step [$PROGNAME-####.png.gif]. The output is a preliminary animated-
# GIF file [$TEMPGIF].

    print "\n";
    print << 'END';             # 'END' should be single-quoted here
Creating a preliminary animated-GIF file [using gifsicle].
END
    $str = << "END";            # "END" must be double-quoted here
gifsicle -i -O -l --colors 256
    $PROGNAME-[0-9][0-9][0-9][0-9].png.gif -o $TEMPGIF
END
    $str =~ s@\s+\z@@s;
    $str =~ s@\s*\n\s*@ @gs;
    system $str;
                                # Consistency check
    &ProgExit ("$IE: The \"gifsicle\" step failed")
        unless -f $TEMPGIF;

#---------------------------------------------------------------------
# Use "intergif" to create the final result.

# Note:  The input is the preliminary animated-GIF file  created prev-
# iousl [$TEMPGIF].  The output is  the final animated-GIF file [$IMG_
# OUTPUT_PATHNAME].

    print "\n";
    print << 'END';             # 'END' should be single-quoted here
Creating the final animated-GIF file [using intergif].
END
    $str = << "END";            # "END" must be double-quoted here
intergif -i -t -trim -d $IGDELAY -loop -diffuse -zigzag
    -best $IGNUMCOLORS $TEMPGIF -o $IMG_OUTPUT_PATHNAME
END
    $str =~ s@\s+\z@@s;
    $str =~ s@\s*\n\s*@ @gs;
    system $str;
                                # Consistency check
    &ProgExit ("$IE: The \"intergif\" step failed")
        unless -f $IMG_OUTPUT_PATHNAME;

#---------------------------------------------------------------------
# Note regarding temporary files.

# There's no need to clean up the temporary files that we've just cre-
# ated. ProgExit will normally take care of this.

#---------------------------------------------------------------------
# Return to original directory.

    chdir ($CWD) ||
        &ProgExit ("$IE: Couldn't return to original directory: $!");

#---------------------------------------------------------------------
# Print a status message.

    if (-f $IMG_OUTPUT_PATHNAME)
    {
        print "\nCreated $IMG_OUTPUT_PATHNAME\n";
    }
    else
    {                           # Normally shouldn't reach this point
        &ProgExit ("Error: The operation failed");
    }

    undef;
}

#---------------------------------------------------------------------
#                            main program
#---------------------------------------------------------------------

&Main();                        # Call the main routine
&ProgExit();                    # Normal exit



To continue, press the browser's Back button.