G'MIC Tutorial Fragments

I cited it in Post 61 of this thread: the July end-of-month rant, with its reference to the etherial Voynich Manuscript, an illuminated parchment now housed in Yale’s Beinecke Library. The work hints of a deep and wonderful intelligence, but factually conveys nothing — because no one can read it. “Clarity matters” I thundered from the bully pulpit, forefinger circling above my head. Hopefully the spittle didn’t too much dampen occupants of the front-row cheap seats. Showers aside, allusions to certain gmic-community artifacts which may score high on some notional Voynich scale shimmer in the wings. One may take that as far as one will; G’MIC’s acceptance on wider stages stems from how well its code may be read by unprompted strangers.

Those of us who habitually ascend bully pulpits are regularly confronted with examples of preaching that falls short of practice. Lapses in Quality Assurance I consciously avoid but unconsciously commit may be found as recently as in the Post 66 seekbetter.gmic inclusion, where tab characters riotously played hob with indentation. There is no telling how a local text rendering facility will expand TAB characters. Four spaces? Eight? Some number in between? In any case, the author’s intent is obscured.

In the sweet fullness of time, How to write clean G’MIC code? ought to be elevated to some station in the tutorial universe. It’s a good read, but more than a good read, it is a good practice. I’d even go so far as to advance Python’s PEP-8 as auxiliary reading to that post, sacrilege though that may be. The low PEP number is evidence of that community’s early commitment to readability, weaved into their ethic for over twenty one years now, and still it endures — and in a most practical way. Perhaps the maintenance of contributions from now-absent members could be a bit more tractable had an appreciation of the needs of unprompted strangers been a more ingrained part of our culture back in similar, antediluvian times.

That is something debatable.
As you know, the G’MIC scripting language has been created with both the ideas of capabilities (in image processing) and brevity. Readability was definitely not a target property.
(I may heard some voices telling me that a non-readable language is probably useless, but as a former developer in assembly language, it’s something I can’t agree with :slight_smile: ).

One of the goal of G’MIC also is to provide a lot of image processing operators and filters that people can apply on their images. That is also (I think) a reached goal.

Attracting new developers to the G’MIC scripting language was actually not one of my primary goal. I’m happy to see some of you are appreciating it at the end, and I honestly admire what you (@grosgood , @Reptorian , @afre , and others) have managed to do with this obscure language.
Saying that, I’d be happy to know new script developers want to join, but I also understand why it could not happen.

1 Like

Still looking…

Ah! September! Once again, the days are brisk, as Western Atlantic high pressure anticyclones sputter, shrink and shift eastward, lessening Gulf stream moisture and letting the dew points drop. Still, those ill-tempered anticyclones may yet lob a hurricane or two at the Eastern North American Seaboard, my home.

Hesitantly, I opened the documentation gates on a new sort of tutorial: a cheat sheet. As I noted in Post 55, I have been given much thought to how rank beginners get through the very basic learning curve of the G’MIC command tool. Trusting that they are inquisitive, they cut-and-paste something from somewhere, often a fx_ <something_or_other> command that underlies a gmic_qt filter and try and make it go. They are Mickey Mouse in Fantasia’s “The Sorcerer’s Apprentice”. They got a command. Maybe they don’t entirely know how to read it, but — by golly! — if the spell makes the broom carry the water, go for it! You know the rest.

In time, I decided that G’MIC is very sensitive to typing errors and beginners are insensitive to G’MIC’s sensitivities. There are really, really simple maladies which plague the beginner. Spaces after commas in argument lists, An unquestioning acceptance of whatever -display shows, the troublesome discovery that -output is on their watch; one has to know how to feed it. So cheat sheets are problem-oriented – perhaps catastrophe-oriented – shorts with a TL;DR for the “Too long; didn’t read.” clientele and a more lengthly précis for those not minding a held hand. I try to be brief; from my point of view, a cheat sheet is a dispatch tool to more lengthly, more developed material. So there are a lot of links. TL;DR is the quick fix; the précis for a little bit of theory; throughout, links to more developed material. I’ve still not quite got the form in hand. The cheat sheet on debugging pipelines, finished in this hour, was unsatisfactory in its length, but it ventured into realms where there is little backing material to which one may link.

That gives me three forms now: catastrophe-oriented cheats, the first of which saw the light of day on August 22nd:

  1. The cheatsheet index
  2. The custom command cheat
  3. The display command lies cheat
  4. The fourth aforementioned debugging cheat is just now rattling down the distribution pipeline.

The second form are command-oriented tutorials, which are rather a bottom-up way to the topic at hand, starting with a tool and ending with what may done with that tool. As always, there is still much to be done in this realm, but not much was done with command tutorials this month, preoccupied as I was with the mechanics of cheat sheets.

The third form are the situation-oriented Cookbooks, which are rather a top-down way to the topic at hand, starting with a visual and ending with an G’MIC implementation which serves the design. They tend to go to length and generally spring from an interest I already have. The motile sprites which infested Post 66 stem from a heart-felt fascination with how elementary, grade-school mathematical relations can give rise to complex behaviors. Part of the behavior arises from the particular origin of the acceleration field, which the sprites sample for a velocity change and direction: a spectral space consisting of a tiny population of low-frequency coefficients is transformed temporally, giving rise to gradual sinusoidial-like fields. That has a signal effect on sprite behavior, which would be significantly different had the acceleration field been generated through -plasma or -turbulence. Freighting Fourier Transforms over the transom may be the most difficult pitch, but past that, the sampling behavior of the sprites is easy to explain. To better elucidate the connection between the sprite’s behavior and the point-wise acceleration fields, I color-coded the vectors comprising the point-wise acceleration field in the usual way: red → 0°, cyan → 180° and so forth, so that as a sprite traverses a red region, its orientation shifts from left to right — but not instantaneously, of course. It’s latent velocity, and the orientation of that vector, is still manifest and only dissipates over time from artifically introduced friction. For those who like toys, an updated seekbetter script, listed following, may be swiped. The corresponding cookbook article is brewing now.

#@cli seekbetter : _width(512)>64,_height(512)>64,_depth(128)>64,_sprite_count(128)>0,0<=_acceleration_attenuation(0.1)<=100,0<=_velocity_dampening(0.95)<=100,0<_slice_per_ticktok(50)<=100,1<=_spectral_turbulence(50)<=100,0<=_backblend(0)<=100,start_in_recording_mode:0
#@cli : Generate a "Seeking Better Home" animation. All arguments are optional
#@cli : and default to reasonable values. § Sprites sample acceleration from
#@cli : fields of pointwise acceleration vectors – see
#@cli : _acceleration_attenuation. Per frame, acceleration → delta velocity,
#@cli : which is vectorially added to sprite's current velocity after that has
#@cli : been dampened by _velocity_decay, a kind generalized friction. § The
#@cli : 'Better Homes' which sprites seek arise from two circumstances. (1)
#@cli : Sprites find 'positional stability' in wells of low acceleration. If
#@cli : they move into such environs at low velocity and the acceleration
#@cli : vectors available from the environment are nil or nearly so, then
#@cli : sprites settle in, their velocities dampen to zero, and, thus, might
#@cli : never leave. (2) Sprites find a dynamic stability from periodic
#@cli : accelerations just so timed and directed to keep the sprites
#@cli : travelling along specific paths. These may be loops around wells of
#@cli : low acceleration, where the surrounding acceleration vectors happen to
#@cli : array themselves in circular patterns. These may also be paths aligned
#@cli : along torii latitudes and longitudes, where acceleration vectors on
#@cli : both sides of the line point more-or-less in the same direction. §
#@cli : Both flavors of stability are transient. The field of pointwise
#@cli : acceleration vectors changes once per tiktok. Each field slice is
#@cli : drawn from a volume generated from spectral noise; the inverse Fourier
#@cli : transform of that noise generates repeating temporal acceleration
#@cli : vector fields that are periodic in x, y and z. Paging through this
#@cli : volume along z gives rise to gradual, periodically changing
#@cli : acceleration vector fields. Whatever form they may take, the
#@cli : acceleration vector fields underlying both forms of stability change,
#@cli : inducing the sprites to find newer, more congenial configurations.
#@cli :
#@cli :  1 → acceleration vector field width (pixels)
#@cli :  2 →       "        "      "   height(pixels)
#@cli :  3 →       "        "      "   depth (number of slices in field)
#@cli :  4 → sprite count:             [ 1 → ∞ ]
#@cli :  5 → acceler. attenuation:     0.1:    little attenuation (frantic animation)
#@cli :                                → 100:  substantial attenuation (languid animation)
#@cli :  6 → Velocity dampening:       0.1:    little dampening
#@cli :                                → 100:  completely dampened velocity
#@cli :  7 → frames per tikok:         frame paging rate, relative to field depth ($3).
#@cli :                                50 → 50% of depth used as frames per tiktok
#@cli :  8 → Spectral turbulence:      1: Low frequency background gradient.
#@cli :                                ∞: High frequency background gradient.
#@cli :  9 → Blend background:           0: Spectral field;
#@cli :                                100: Solid blue-gray;
#@cli :                                 50: 50-50 blend
#@cli : 10 → Recording mode:           False(0). If true, animation starts
#@cli :                                in recording mode. 's' on keyboard
#@cli :                                stops recording.
#@cli :                                Otherwise 'r' starts recording.
#@cli : Keyboard:                      ESC: stop animation.
#@cli :                                c: Toggle color mode.
#@cli :                                r: start recording.
#@cli :                                s: stop recording
#@cli : $1 seekbetter 256,256,512,2048 - 256×256×512 pixel field populated with 2,048 sprites.

seekbetter :  -check "isnum(${1=512})  && ${1}>=64 && "\
                     "isnum(${2=512})  && ${2}>=64 && "\
                     "isnum(${3=128})  && ${3}>=64 && "\
                     "isint(${4=128})  && ${4}>0   && "\
                     "isnum(${5=0.10}) && ${5}>=0  && ${5}<=100 && "\
                     "isnum(${6=0.95}) && ${6}>=0  && ${6}<=100 && "\
                     "isnum(${7=50})   && ${7}>0   && ${7}<=100 && "\
                     "isnum(${8=50})   && ${8}>=1  && "\
                     "isnum(${9=0})    && ${9}>=0  && ${9}<=100 && "\

    # Set up parameters
    wid=$1                       # Field width: pixels 128 minimum
    hgh=$2                       # Field height: pixels 128 minimum
    dph=$3                       # field depth: pixels: 64 minimum
    cnt=$4                       # Sprite count: count of swimming thingies. Positive integral count
    mul={exp(-$5)}               # Acceleration attenuation: 1 → 100. 1: fully attenuated. → 100: Unattenuated
    dcy={exp(-$6)}               # Velocity decay: 0 → 100 0: Fully decayed. → 100 No decay
    tsz={round($dph*$7/100,1,0)} # Relative frame count per slice: 32 → Page through frames 32% of field depth for each tiktok.
    tub={$8/100}                 # Turbulence: 1 → ∞. Higher: more turbulent landscape. Peaks and crannies galore!
    bkb={$9/100}                 # Background blend: 0 shows acceleration dynamics in background. 100: dark blue-gray
    rec=$10                      # If set (1), start in recording mode (Pre-presses 'r'). 's' stops recording.

    # Otherwise a linear interpolation, with 50 representing a 50-50 blend.

    # Announcement
   -echo[^-1]   "Starting a Seeking Better Home animation.\nField dimensions:                          "$wid"×"$hgh"×"$dph";\n"\
                "Sprite count (>0):                          "$cnt";\nRelative Acceleration Attenuation (0→10): "$mul";\n"\
                "Velocity Dampening (0→10):                "$dcy";\nFrames per Ticktok (rel. to depth):       "$tsz";\n"\
                "Background Turbulence (>=0.01):            "$tub";\nBackground Fade (0→1):                      "$bkb"\n"

   # Generate data structures and viewport
   -input $cnt,1,1,4
   -name. sprites
   -mksprites[sprites] $wid,$hgh
   -mktorii  $wid,$hgh,$dph,10,$tub
   -name. torii
   -input $wid,$hgh,1,3
   -name. viewport

   # Outer tiktok loop. Page through the torus slices
       # Set the viewport background
       -if {$clr==0}
          -fill[viewport] "[rad2deg(atan2(i(#-2,x,y,$tiktok,1),i(#-2,x,y,$tiktok,0)))+180,0.875,norm2(I(#-2,x,y,$tiktok))]"
          -shared[viewport] 100%
          -normalize. 0,1
          -fill[viewport] "[i(#-2,x,y,$tiktok,0),i(#-2,x,y,$tiktok,1),0]"
          -normalize[viewport] 0,255
          -map[viewport] tarn
       -if $bkb
          -fill[viewport] "*lerp(I(#-1),[5,30,50],"$bkb")"
       +store[viewport] topography

       # Intraslice loop. tsz → tick size → number of frames before
       # advancing one torroidal slice.
       -repeat $tsz

          # Events:
          #    Quit request?
          -if {*}" && "{*,ESC}
             -echo[] "Quitting..."

          #    Start recording frames?
          -elif {*}" && "{*,R}" && "$rec==0
             -echo[] "Starting Recording in seek directory..."

          #    Cease recording frames?
          -elif {*}" && "{*,S}" && "$rec==1
             -echo[] "Stopping recording at "$oc" frames."

          #    Color toggle?
          -elif {*}" && "{*,C}
             -echo[] "Viewport color toggled."

          # Paint the sprites
          -drawsprites. [-3],9,4,0.375

          # Step the dynamics
          -step[sprites] [-2],$tiktok,$mul,$dcy
          -toruswrap[sprites] 0,0,$wid,$hgh

          # Paint ticktok
          -text_outline. $tiktok,0.01~,0.01~,5%,1,1

          # Pause animation
          -window[viewport] $wid,$hgh,0,0,0,0,"Seeking a Better Home"
          -wait -10
          -if $rec
             -if !isdir('seek')
                -exec "mkdir seek"
                -if ${}
                   -error "Could not create output directory 'seek'; disabling recording."
             -outputn[viewport] "seek/seekbetter.png",$oc

          # Refresh background graphic
          -input $topography
          -name. topography
          -fill[viewport] I(#-1)
       -if $tiktok<$dph-1
   -while {*}" && "!{*,ESC}

mksprites : -check "isnum(${1=128}) && ${1}>=128 && isnum(${2=128}) && ${2}>=128 && isnum(${3=0}) && ${3}>=0"
   -if s==4
      sw=$1  # Range Width
      sh=$2  # Range Height
      sm=$3  # Acceleration, pixels/sec²
      -fill. u([$sw,$sh,$sm,$sm])-[0,0,$sm/2,$sm/2]
      -round. 1
      -error "Channel count="{s}" is unexpected. 4 channels needed for sprites."

mktorii :  -check "isnum(${1=128}) && ${1}>=64 && "\
                  "isnum(${2=128}) && ${2}>=64 && "\
                  "isnum(${3=128}) && ${3}>=64 && "\
                  "isnum(${4=1})   && ${4}>0   && "\
                  "isnum(${5=0.5}) && ${5}>0   && "\
                  "isint(${6=0})   && ${6}>=0"

   # Parameter filtering
   wid=$1     # Field width in pixels (x)
   hgh=$2     # Field height in pixels (y)
   dph=$3     # Field depth in slices/pixels (z)
   acc=$4     # Peak to average relative distance. Higher → more active sprites
   tub=$5     # Turbulence (literally, spread of Gaussian noise in spectral space.
              # Higher → more spread → more high frequency components → more temporal turbulence

   # Spectral noise

   -input $wid,$hgh,$dph,2,1
   -noise. 100,0
   -name. snoise

   # Shape snoise into an approximate Gaussian distribution
   -fill[snoise] whd*i*exp(-norm([x-(w/2)+1,y-(h/2)+1,z-(d/2)+1])/$tub)
   -name. specspace

   # Locate noise kernel in the center of spectral space
   # Set frequency 0 ('DC') spectral point to zero
   -shift[specspace] {-(($wid/2)-1)},{-(($hgh/2)-1)},{-(($dph/2)-1)},0,2,0
   -set. 0,0,0,0,0
   -set. 0,0,0,0,1

   -split. c

   # Inverse Fourier Transform into temporal space
   -append[-2,-1] c
   -mul. {abs(im)>iM?$acc/im:$acc/iM}

step : -check ${"is_image_arg $1"}" && isint(${2=0}) && ${2}>=0 && isnum(${3=1}) && ${3}>=0 && isnum(${4=1}) && ${4}>=0"
   -pass$1 1
   -fill.. ">

toruswrap : -check "isnum(${1}) && isnum(${2}) && isnum(${3}) && isnum(${4})"
   lx=$1 # Lower bounds, x
   ly=$2 # Lower bounds, y
   dx=$3 # Width, x
   dy=$4 # Height, y
   fill. ">

drawsprites : -check ${"is_image_arg $1"}" && ${2=4} && ${2}>=4 && ${3=6} && ${3}>=4 && isnum(${4=0.5}) && ${4}>=0 && ${4}<=1"
   -pass$1 1   # 1D image, vector of sprites. Selected image is the canvas
   ra=$2       # a radius of ellipse
   rb=$3       # b radius of ellipse
   op=$4       # opacity of gray fill. Outline fully opaque
   -eval. ">
1 Like

Making :nerd_face: :sunglasses: since '15a.

a probably earlier than that.

The first posting of Introduction and Basics to gmic.eu was around November 26, 2013, though earlier versions of those and some beginner cookbook and commands existed about six months before, posted to my own web site. Somewhere around October, 2013, @David_Tschumperle and I started corresponding when my website showed up in a Google search he did.

Been busy. But a tutorial seems imminent. Maybe tomorrow. Probably next week. This One liner provided the departure point, but also this animation, originally written in Python. Bringing it into G’MIC for the giggles and snorts…


gmic wheelie_anim.gmic wheelie_anim 512,0.63,0,1,0.36,0,3,0.125,0,-5,0.3,0,-9 o arabesque.mp4,30,H264

for starters.


   # wheelie: a circle of a particular (1) radius, (2) phase angle
   # (orientation) and (3) discrete angular velocity - a relative
   # integral number of revolutions per some unspecified base interval,
   # perhaps however long one revolution takes. Wheelies chain
   # together, the center of one situated at 0° on the rim of its
   # predecessor. The first wheelie of the chain centers at the origin
   # and the last has, instead of another wheelie, an attached marking
   # pen. Thus, the entire chain draws an "arabesque" as the
   # individual wheelies rotate at their respective angular velocities from
   # their initial orientations. This script animates such.

   # This script renders arabesques "traditionally": Each wheelie has
   # radial, phase and angular velocity components.  For each
   # increment, and, for each wheelie, apply the angular velocity
   # increment, rotating the wheelie, then, with all wheelies rotated,
   # plot the endpoint.

   # Fetch an arbitrarily long argument list to $a0, $a1, …, $an


   # Expect data triplets: radius, phase angle, angular velocity.

   -check {!(($#-1)%3)}

   # Initial argument: Spectral width (sw)

   -if $sw%2==0

   # Circles argument vector: For each wheelie, compose radius and
   # phase arguments into a homogeneous transform. In the animation
   # loop, we compose this with atk-dependent rotational transform to
   # plot the wheelie in an generic plotting space.

   -input $ccnt,1,1,12
   -name  args

   # For both methods: Iterate over argument vector. For each triplet
   # – representing a wheelie circle – Fetch radius (rad), phase angle
   # (ang) and angular velocity (sf) parameters.

   -repeat $#-1,j
      -if   $j%3==0 # get radius
         -check isnum($rad)" && "$rad>=0" && "$rad<=1
         -set[args] $rad,{round($j/3,1,-1)},0,0,0
      -elif $j%3==1 # get angle, degrees
         -check isnum($ang)" && "$ang>=0" && "$ang<=360
         -set[args] $ang,{round($j/3,1,-1)},0,0,1
      -else         # get ω and set spectral coefficient
         -check isnum($sf)" && "$sf%1==0" && "abs($sf)<=0.1*$sw
         -set[args] $sf,{round($j/3,1,-1)},0,0,2

   # Iterate over argument image, each 12 channel pixel representing
   # one wheelie. The first three channels contain wheelie radius,
   # phase angle and radial velocity. Compose a homogeneous matrix
   # transforming the origin to the tip of the wheelie radius, a
   # radial translation and phase rotation. Populate the nine
   # remaining channels with this matrix.

   -fill[args] ">
                  orot=rot([0,0,1],deg2rad(CPX[1])); # phase rotation
                  xlat[2]=CPX[0];                    # translation origin-to-rim
                  FIN=[CPX[0,3,1],mul(orot,xlat,3)]; # args+"RADX" i.e. 'rotation & xlation'
                  FIN                                # store above in 12-channel pixel

   -permute[args] cyzx
   -store[args] circles

   # Partial screenspace transform.
   -input {2*$sw},{2*$sw},1,4
   -name. backplate

   # Set the viewport backplate
   -fill[backplate] ">lerp([30,40,135,255],[10,100,175,255],y/(h-1))"

   # Arabesque arabesque
   -input 100%,100%,100%,100%
   -name. arabesque

   # Circle arabesque
   -input 100%,100%,100%,100%
   -name. circles

   # Stash backplate for duplication in loop
   -store[backplate] back

   # Animate arabesque
   -repeat 6*$sw,k
          -input (0.99^0.94^0.92^0.9985)
          -resize. [-2],[-2],[-2],[-2]
          -input $back

          # Plot wheelie indicators: rotate wheelies by their respective
          # angular velocity increments, compose this rotation into the
          # pre-computed phase, radial-displacement transform from the
          # argument image, compose with screenspace transform and plot
          # via polygon()

          -eval[circles]   ">

          -blend[backplate] [arabesque],alpha
          -blend[backplate] [circles],alpha
          -mul[circles] 0
          -mv[backplate] 0

In Post 393 of On the road to 3.0 @garagecoder sighted a line of query tangental to G’MIC Scope Fun!, My Light-Dawns-Over-Marblehead realization that, yes Virginia, command scope is indeed a contained environment: Of the parent scope’s context, the command line interpreter sees nothing save what it has been told about: images in the custom command’s selection decorator, if any, and passed arguments, if any. All else from the larger context is opaque to the command line interpreter as it walks down the custom command’s pipeline.

In particular -store. $hidyhole squirrels away the last image on the list, shortening it by one item, but should the next step on the pipeline bring the interpreter to a new custom command, when it enter’s the child’s scope the parent’s $hidyhole is hidden away and any reference to $hidyhole in the child custom command refers to a different (and new) variable in that context. So -input $hidyhole in the child context is not a clever way of sucking in some state from the parent context, but merely tells -input to open a file with an empty string name, and after the host operating system informs -input that no such file exists, it voices alarm by throwing an *** Error *** Unknown filename '' exception, which, in the absence of an -onfail block somewhere in an ancestral scope, pretty much makes all contexts go poof!

In light of selection decorators being a part of G’MIC since the antediluvian version 0, such use of store is neophyte’s work. Want to tell a child command about an image? Cite it in the child’s selection decorator; that’s what selection decorators are for. The command interpreter operating on the child pipeline will do what it will with selected images, including, — maybe! — hiding secret messages!!! in an image! Secret messages for any other command given the image in its selection, so it may also peek inside the carrying image’s guts for strictly confidential, undisclosed bits and blobs meant for the cognoscenti. Ooooooh!

Secret messages. Also known by the far more boring moniker: ‘global state’, that being the sight line along which @garagecoder’s post tangentially brushes G’MIC Scope Fun!. Is there (should there be) a (simple!) means to declare a variable ‘global’ and thereby add a bit to global state, visible to custom commands far and wide — or at least those implementing the secret access decoder ring?

Pythonistas may be pleased to write:

#! /usr/bin/python

HWORLD="Hello world"

def quiet() :
   print("Say {0:s}... in a whisper.".format(HWORLD))

def shout() :


where HWORLD, declared outside of any function definition, is visible to all functions. Slick and easy. And it won’t pass code review in my shop; probably not in yours as well, as many of us pray at the altar of separation of concerns. Global state fosters bugs of the most untraceable kind. By its nature, it is accessible to all, and All should be on their best behavior when they access it, and they are! — except when they aren’t. And with many clients using global state, inevitably someone abuses it, if only inadvertently, and it becomes devilishly complicated to figure out which of the uncounted clients is peeing in the pool. Recall too that the illustration above is childishly simple. In real life, simple global state like “Hello world!” transmogrifies into some ever-growing structure, to which various teams tack their pieces, because nobody wants to tackle the problem of how to pass around state, now that the project has seventy bazillion lines of code — but I digress.

As noted pre-digression, the place for global state in G’MIC is in a “non-image” image — where secret messages may be deposited and retrieved by any number of more-or-less cooperating commands. The antecedents for such antics have been around for a long time, the mathematical expression I[_#ind,k], addressing the kth pixel of image #ind, here linearly arranged in a vector from zero to w*h-1 and which returns an s (spectral) length vector from image #ind, which itself has s channels. On the right hand side of an assignment operator = I[_#ind,k] is a getter from the global storage image; on the left hand side it is a setter to it. This image, in practice, contains global state that any custom command can manipulate through math expressions, which notionally operate with isolated, local-only state. What I[_#ind,k] sets or gets, how the image (data structure) is organized, and how might complexity be managed, is up to the coder(s); G’MIC won’t offer protective insurance.

To this comes the newer da_*() native math functions, reflecting the earlier dar_*() macros from -math_lib in 2.9.9 and prior, coming soon as full-fledged native mathematical functions in 3.0.0; see Math evaluator: Native functions for managing Dynamic Arrays and the original discussion in Post 187 of G’MIC exercises. There is also newish examples in Post 424 of On the road to 3.0. Like their macro predecessors, the da_*() functions constitute access methods to an image and collectively project the illusion of a dynamic array, one that grows or shrinks as data are inserted or pushed, then removed. The first step is to create an image by any of the usual means. A sole 0, creating an Empty suffices: perhaps for discussion it is the first image on the list with a 0 index. Later, perhaps in a math expression in some custom command, a da_*(#0,…) inserts or pushes data-intended-for-global-visibility (née ‘pixels’) into the array; the empty image acquires pixels, but for programmatic purposes, that is a behind-the-scenes incidental. For this discussion, image [0] backing da_*(#0,…) constitutes global state that any custom command can access in a math expression through its own use of the da_*() functions, these referencing the backing store image through its index. The I[#ind,k] augments these, as it may get/set any element (née pixel) of the array, since the array is, in actuality, an image.

Now, perhaps such to-ing and fro-ing may seem finicky compared to the Python exemplar. Since I’m not a fan of global state, I don’t that much miss a slick and easy mechanism to global messes. From a tutorial writing perspective, however, a discussion on global state has many points of intersection with the discussion on scope, to wit: what is, and is not, visible to code at certain points of execution. Since posting G’MIC Scope Fun! I’ve had in mind a “Basic G’MIC Scope” article at the root of the tutorial tree, a peer of introductory topics on Images and Command Decorations. This post constitutes a first draft of that, all arms and legs and awkwardness — a tutorial fragment intended for such a thread as this.

Not sure when the final copy rattles down the ways; my Fair City called all of us civil service folk back into the trenches offices on the September Ides. My commute from Park Slope to the City Hall environs is much shorter than most, but it is still 40 minutes out of the day, to-and-fro, and that’s a bite out of recreational writing time. End-of-year vacation approaches, however. Maybe some time there to catch up.


I’ll need to re-read your post tomorrow, but just in case there was any doubt I’m most definitely not looking for global state by using “image variables” (which is already possible!); I’m thinking about passing them as arguments to other commands in the form of an image.

For example, some commands process string data held in an image. So if you were allowed to do mycommand $my_img_arg, I would expect the called command to receive it not as a variable, but as an image (as if we’d done i $my_img_arg mycommand [-1].

It’s really just a nice-to-have, because it’s a shorthand for what can already be done. The main advantage is having images outside the default stack - it’s surprising how much that can simplify the usual rigmarole of keeping track of stack positions, names, local scopes…

Edit: it seems as though part of what you’re mentioning is more to do with loose typing, e.g. we can’t define a class/struct to describe the “meaning” of a particular image. So yes, in some cases we end up using hidden or assumed type knowledge (e.g. see the various palette commands). But yes, variables as they are can often side-step local or even command scope.

I misunderstood your aim. This paragraph very nearly clarifies that. Thank you.

If I understand matters correctly, your proposal obliges the the command line interpreter to (conceptually) identify a pair of pipeline items that — first! — is a pass-empowered custom command, or a built-in with the same capabilities, such as -warp, and — second! — a follow-on special variable to which store assigns an off-list image object. Only when these two conditions prevail does the command line interpreter first invoke an implied -input command, passing the special variable as its argument. The reconstituted image is appended to the end of the list (stack), per usual for undecorated -input commands. Insofar as the pass-enabled custom command is concerned, it is also business-as-usual: it has as its argument an image at the end of the list. If one or both conditions fail to be, then the command line interpreter does not behave in this proposed fashion, no?

Your discussion could probably benefit as a dedicated thread. I suggest you put it all in one place.

Tutorial request:

I found this on cimg.h

#if cimg_display==1
    // Define keycodes for X11-based graphical systems.
    const unsigned int keyESC        = XK_Escape;
    const unsigned int keyF1         = XK_F1;
    const unsigned int keyF2         = XK_F2;
    const unsigned int keyF3         = XK_F3;
    const unsigned int keyF4         = XK_F4;
    const unsigned int keyF5         = XK_F5;
    const unsigned int keyF6         = XK_F6;
    const unsigned int keyF7         = XK_F7;
    const unsigned int keyF8         = XK_F8;
    const unsigned int keyF9         = XK_F9;
    const unsigned int keyF10        = XK_F10;
    const unsigned int keyF11        = XK_F11;
    const unsigned int keyF12        = XK_F12;
    const unsigned int keyPAUSE      = XK_Pause;
    const unsigned int key1          = XK_1;
    const unsigned int key2          = XK_2;
    const unsigned int key3          = XK_3;
    const unsigned int key4          = XK_4;
    const unsigned int key5          = XK_5;
    const unsigned int key6          = XK_6;
    const unsigned int key7          = XK_7;
    const unsigned int key8          = XK_8;
    const unsigned int key9          = XK_9;
    const unsigned int key0          = XK_0;
    const unsigned int keyBACKSPACE  = XK_BackSpace;
    const unsigned int keyINSERT     = XK_Insert;
    const unsigned int keyHOME       = XK_Home;
    const unsigned int keyPAGEUP     = XK_Page_Up;
    const unsigned int keyTAB        = XK_Tab;
    const unsigned int keyQ          = XK_q;
    const unsigned int keyW          = XK_w;
    const unsigned int keyE          = XK_e;
    const unsigned int keyR          = XK_r;
    const unsigned int keyT          = XK_t;
    const unsigned int keyY          = XK_y;
    const unsigned int keyU          = XK_u;
    const unsigned int keyI          = XK_i;
    const unsigned int keyO          = XK_o;
    const unsigned int keyP          = XK_p;
    const unsigned int keyDELETE     = XK_Delete;
    const unsigned int keyEND        = XK_End;
    const unsigned int keyPAGEDOWN   = XK_Page_Down;
    const unsigned int keyCAPSLOCK   = XK_Caps_Lock;
    const unsigned int keyA          = XK_a;
    const unsigned int keyS          = XK_s;
    const unsigned int keyD          = XK_d;
    const unsigned int keyF          = XK_f;
    const unsigned int keyG          = XK_g;
    const unsigned int keyH          = XK_h;
    const unsigned int keyJ          = XK_j;
    const unsigned int keyK          = XK_k;
    const unsigned int keyL          = XK_l;
    const unsigned int keyENTER      = XK_Return;
    const unsigned int keySHIFTLEFT  = XK_Shift_L;
    const unsigned int keyZ          = XK_z;
    const unsigned int keyX          = XK_x;
    const unsigned int keyC          = XK_c;
    const unsigned int keyV          = XK_v;
    const unsigned int keyB          = XK_b;
    const unsigned int keyN          = XK_n;
    const unsigned int keyM          = XK_m;
    const unsigned int keySHIFTRIGHT = XK_Shift_R;
    const unsigned int keyARROWUP    = XK_Up;
    const unsigned int keyCTRLLEFT   = XK_Control_L;
    const unsigned int keyAPPLEFT    = XK_Super_L;
    const unsigned int keyALT        = XK_Alt_L;
    const unsigned int keySPACE      = XK_space;
    const unsigned int keyALTGR      = XK_Alt_R;
    const unsigned int keyAPPRIGHT   = XK_Super_R;
    const unsigned int keyMENU       = XK_Menu;
    const unsigned int keyCTRLRIGHT  = XK_Control_R;
    const unsigned int keyARROWLEFT  = XK_Left;
    const unsigned int keyARROWDOWN  = XK_Down;
    const unsigned int keyARROWRIGHT = XK_Right;
    const unsigned int keyPAD0       = XK_KP_0;
    const unsigned int keyPAD1       = XK_KP_1;
    const unsigned int keyPAD2       = XK_KP_2;
    const unsigned int keyPAD3       = XK_KP_3;
    const unsigned int keyPAD4       = XK_KP_4;
    const unsigned int keyPAD5       = XK_KP_5;
    const unsigned int keyPAD6       = XK_KP_6;
    const unsigned int keyPAD7       = XK_KP_7;
    const unsigned int keyPAD8       = XK_KP_8;
    const unsigned int keyPAD9       = XK_KP_9;
    const unsigned int keyPADADD     = XK_KP_Add;
    const unsigned int keyPADSUB     = XK_KP_Subtract;
    const unsigned int keyPADMUL     = XK_KP_Multiply;
    const unsigned int keyPADDIV     = XK_KP_Divide;

#elif cimg_display==2
    // Define keycodes for Windows.
    const unsigned int keyESC        = VK_ESCAPE;
    const unsigned int keyF1         = VK_F1;
    const unsigned int keyF2         = VK_F2;
    const unsigned int keyF3         = VK_F3;
    const unsigned int keyF4         = VK_F4;
    const unsigned int keyF5         = VK_F5;
    const unsigned int keyF6         = VK_F6;
    const unsigned int keyF7         = VK_F7;
    const unsigned int keyF8         = VK_F8;
    const unsigned int keyF9         = VK_F9;
    const unsigned int keyF10        = VK_F10;
    const unsigned int keyF11        = VK_F11;
    const unsigned int keyF12        = VK_F12;
    const unsigned int keyPAUSE      = VK_PAUSE;
    const unsigned int key1          = '1';
    const unsigned int key2          = '2';
    const unsigned int key3          = '3';
    const unsigned int key4          = '4';
    const unsigned int key5          = '5';
    const unsigned int key6          = '6';
    const unsigned int key7          = '7';
    const unsigned int key8          = '8';
    const unsigned int key9          = '9';
    const unsigned int key0          = '0';
    const unsigned int keyBACKSPACE  = VK_BACK;
    const unsigned int keyINSERT     = VK_INSERT;
    const unsigned int keyHOME       = VK_HOME;
    const unsigned int keyPAGEUP     = VK_PRIOR;
    const unsigned int keyTAB        = VK_TAB;
    const unsigned int keyQ          = 'Q';
    const unsigned int keyW          = 'W';
    const unsigned int keyE          = 'E';
    const unsigned int keyR          = 'R';
    const unsigned int keyT          = 'T';
    const unsigned int keyY          = 'Y';
    const unsigned int keyU          = 'U';
    const unsigned int keyI          = 'I';
    const unsigned int keyO          = 'O';
    const unsigned int keyP          = 'P';
    const unsigned int keyDELETE     = VK_DELETE;
    const unsigned int keyEND        = VK_END;
    const unsigned int keyPAGEDOWN   = VK_NEXT;
    const unsigned int keyCAPSLOCK   = VK_CAPITAL;
    const unsigned int keyA          = 'A';
    const unsigned int keyS          = 'S';
    const unsigned int keyD          = 'D';
    const unsigned int keyF          = 'F';
    const unsigned int keyG          = 'G';
    const unsigned int keyH          = 'H';
    const unsigned int keyJ          = 'J';
    const unsigned int keyK          = 'K';
    const unsigned int keyL          = 'L';
    const unsigned int keyENTER      = VK_RETURN;
    const unsigned int keySHIFTLEFT  = VK_SHIFT;
    const unsigned int keyZ          = 'Z';
    const unsigned int keyX          = 'X';
    const unsigned int keyC          = 'C';
    const unsigned int keyV          = 'V';
    const unsigned int keyB          = 'B';
    const unsigned int keyN          = 'N';
    const unsigned int keyM          = 'M';
    const unsigned int keySHIFTRIGHT = VK_SHIFT;
    const unsigned int keyARROWUP    = VK_UP;
    const unsigned int keyCTRLLEFT   = VK_CONTROL;
    const unsigned int keyAPPLEFT    = VK_LWIN;
    const unsigned int keyALT        = VK_LMENU;
    const unsigned int keySPACE      = VK_SPACE;
    const unsigned int keyALTGR      = VK_CONTROL;
    const unsigned int keyAPPRIGHT   = VK_RWIN;
    const unsigned int keyMENU       = VK_APPS;
    const unsigned int keyCTRLRIGHT  = VK_CONTROL;
    const unsigned int keyARROWLEFT  = VK_LEFT;
    const unsigned int keyARROWDOWN  = VK_DOWN;
    const unsigned int keyARROWRIGHT = VK_RIGHT;
    const unsigned int keyPAD0       = 0x60;
    const unsigned int keyPAD1       = 0x61;
    const unsigned int keyPAD2       = 0x62;
    const unsigned int keyPAD3       = 0x63;
    const unsigned int keyPAD4       = 0x64;
    const unsigned int keyPAD5       = 0x65;
    const unsigned int keyPAD6       = 0x66;
    const unsigned int keyPAD7       = 0x67;
    const unsigned int keyPAD8       = 0x68;
    const unsigned int keyPAD9       = 0x69;
    const unsigned int keyPADADD     = VK_ADD;
    const unsigned int keyPADSUB     = VK_SUBTRACT;
    const unsigned int keyPADMUL     = VK_MULTIPLY;
    const unsigned int keyPADDIV     = VK_DIVIDE;

    // Define random keycodes when no display is available.
    // (should rarely be used then!).
    const unsigned int keyESC        = 1U;   //!< Keycode for the \c ESC key (architecture-dependent)
    const unsigned int keyF1         = 2U;   //!< Keycode for the \c F1 key (architecture-dependent)
    const unsigned int keyF2         = 3U;   //!< Keycode for the \c F2 key (architecture-dependent)
    const unsigned int keyF3         = 4U;   //!< Keycode for the \c F3 key (architecture-dependent)
    const unsigned int keyF4         = 5U;   //!< Keycode for the \c F4 key (architecture-dependent)
    const unsigned int keyF5         = 6U;   //!< Keycode for the \c F5 key (architecture-dependent)
    const unsigned int keyF6         = 7U;   //!< Keycode for the \c F6 key (architecture-dependent)
    const unsigned int keyF7         = 8U;   //!< Keycode for the \c F7 key (architecture-dependent)
    const unsigned int keyF8         = 9U;   //!< Keycode for the \c F8 key (architecture-dependent)
    const unsigned int keyF9         = 10U;  //!< Keycode for the \c F9 key (architecture-dependent)
    const unsigned int keyF10        = 11U;  //!< Keycode for the \c F10 key (architecture-dependent)
    const unsigned int keyF11        = 12U;  //!< Keycode for the \c F11 key (architecture-dependent)
    const unsigned int keyF12        = 13U;  //!< Keycode for the \c F12 key (architecture-dependent)
    const unsigned int keyPAUSE      = 14U;  //!< Keycode for the \c PAUSE key (architecture-dependent)
    const unsigned int key1          = 15U;  //!< Keycode for the \c 1 key (architecture-dependent)
    const unsigned int key2          = 16U;  //!< Keycode for the \c 2 key (architecture-dependent)
    const unsigned int key3          = 17U;  //!< Keycode for the \c 3 key (architecture-dependent)
    const unsigned int key4          = 18U;  //!< Keycode for the \c 4 key (architecture-dependent)
    const unsigned int key5          = 19U;  //!< Keycode for the \c 5 key (architecture-dependent)
    const unsigned int key6          = 20U;  //!< Keycode for the \c 6 key (architecture-dependent)
    const unsigned int key7          = 21U;  //!< Keycode for the \c 7 key (architecture-dependent)
    const unsigned int key8          = 22U;  //!< Keycode for the \c 8 key (architecture-dependent)
    const unsigned int key9          = 23U;  //!< Keycode for the \c 9 key (architecture-dependent)
    const unsigned int key0          = 24U;  //!< Keycode for the \c 0 key (architecture-dependent)
    const unsigned int keyBACKSPACE  = 25U;  //!< Keycode for the \c BACKSPACE key (architecture-dependent)
    const unsigned int keyINSERT     = 26U;  //!< Keycode for the \c INSERT key (architecture-dependent)
    const unsigned int keyHOME       = 27U;  //!< Keycode for the \c HOME key (architecture-dependent)
    const unsigned int keyPAGEUP     = 28U;  //!< Keycode for the \c PAGEUP key (architecture-dependent)
    const unsigned int keyTAB        = 29U;  //!< Keycode for the \c TAB key (architecture-dependent)
    const unsigned int keyQ          = 30U;  //!< Keycode for the \c Q key (architecture-dependent)
    const unsigned int keyW          = 31U;  //!< Keycode for the \c W key (architecture-dependent)
    const unsigned int keyE          = 32U;  //!< Keycode for the \c E key (architecture-dependent)
    const unsigned int keyR          = 33U;  //!< Keycode for the \c R key (architecture-dependent)
    const unsigned int keyT          = 34U;  //!< Keycode for the \c T key (architecture-dependent)
    const unsigned int keyY          = 35U;  //!< Keycode for the \c Y key (architecture-dependent)
    const unsigned int keyU          = 36U;  //!< Keycode for the \c U key (architecture-dependent)
    const unsigned int keyI          = 37U;  //!< Keycode for the \c I key (architecture-dependent)
    const unsigned int keyO          = 38U;  //!< Keycode for the \c O key (architecture-dependent)
    const unsigned int keyP          = 39U;  //!< Keycode for the \c P key (architecture-dependent)
    const unsigned int keyDELETE     = 40U;  //!< Keycode for the \c DELETE key (architecture-dependent)
    const unsigned int keyEND        = 41U;  //!< Keycode for the \c END key (architecture-dependent)
    const unsigned int keyPAGEDOWN   = 42U;  //!< Keycode for the \c PAGEDOWN key (architecture-dependent)
    const unsigned int keyCAPSLOCK   = 43U;  //!< Keycode for the \c CAPSLOCK key (architecture-dependent)
    const unsigned int keyA          = 44U;  //!< Keycode for the \c A key (architecture-dependent)
    const unsigned int keyS          = 45U;  //!< Keycode for the \c S key (architecture-dependent)
    const unsigned int keyD          = 46U;  //!< Keycode for the \c D key (architecture-dependent)
    const unsigned int keyF          = 47U;  //!< Keycode for the \c F key (architecture-dependent)
    const unsigned int keyG          = 48U;  //!< Keycode for the \c G key (architecture-dependent)
    const unsigned int keyH          = 49U;  //!< Keycode for the \c H key (architecture-dependent)
    const unsigned int keyJ          = 50U;  //!< Keycode for the \c J key (architecture-dependent)
    const unsigned int keyK          = 51U;  //!< Keycode for the \c K key (architecture-dependent)
    const unsigned int keyL          = 52U;  //!< Keycode for the \c L key (architecture-dependent)
    const unsigned int keyENTER      = 53U;  //!< Keycode for the \c ENTER key (architecture-dependent)
    const unsigned int keySHIFTLEFT  = 54U;  //!< Keycode for the \c SHIFTLEFT key (architecture-dependent)
    const unsigned int keyZ          = 55U;  //!< Keycode for the \c Z key (architecture-dependent)
    const unsigned int keyX          = 56U;  //!< Keycode for the \c X key (architecture-dependent)
    const unsigned int keyC          = 57U;  //!< Keycode for the \c C key (architecture-dependent)
    const unsigned int keyV          = 58U;  //!< Keycode for the \c V key (architecture-dependent)
    const unsigned int keyB          = 59U;  //!< Keycode for the \c B key (architecture-dependent)
    const unsigned int keyN          = 60U;  //!< Keycode for the \c N key (architecture-dependent)
    const unsigned int keyM          = 61U;  //!< Keycode for the \c M key (architecture-dependent)
    const unsigned int keySHIFTRIGHT = 62U;  //!< Keycode for the \c SHIFTRIGHT key (architecture-dependent)
    const unsigned int keyARROWUP    = 63U;  //!< Keycode for the \c ARROWUP key (architecture-dependent)
    const unsigned int keyCTRLLEFT   = 64U;  //!< Keycode for the \c CTRLLEFT key (architecture-dependent)
    const unsigned int keyAPPLEFT    = 65U;  //!< Keycode for the \c APPLEFT key (architecture-dependent)
    const unsigned int keyALT        = 66U;  //!< Keycode for the \c ALT key (architecture-dependent)
    const unsigned int keySPACE      = 67U;  //!< Keycode for the \c SPACE key (architecture-dependent)
    const unsigned int keyALTGR      = 68U;  //!< Keycode for the \c ALTGR key (architecture-dependent)
    const unsigned int keyAPPRIGHT   = 69U;  //!< Keycode for the \c APPRIGHT key (architecture-dependent)
    const unsigned int keyMENU       = 70U;  //!< Keycode for the \c MENU key (architecture-dependent)
    const unsigned int keyCTRLRIGHT  = 71U;  //!< Keycode for the \c CTRLRIGHT key (architecture-dependent)
    const unsigned int keyARROWLEFT  = 72U;  //!< Keycode for the \c ARROWLEFT key (architecture-dependent)
    const unsigned int keyARROWDOWN  = 73U;  //!< Keycode for the \c ARROWDOWN key (architecture-dependent)
    const unsigned int keyARROWRIGHT = 74U;  //!< Keycode for the \c ARROWRIGHT key (architecture-dependent)
    const unsigned int keyPAD0       = 75U;  //!< Keycode for the \c PAD0 key (architecture-dependent)
    const unsigned int keyPAD1       = 76U;  //!< Keycode for the \c PAD1 key (architecture-dependent)
    const unsigned int keyPAD2       = 77U;  //!< Keycode for the \c PAD2 key (architecture-dependent)
    const unsigned int keyPAD3       = 78U;  //!< Keycode for the \c PAD3 key (architecture-dependent)
    const unsigned int keyPAD4       = 79U;  //!< Keycode for the \c PAD4 key (architecture-dependent)
    const unsigned int keyPAD5       = 80U;  //!< Keycode for the \c PAD5 key (architecture-dependent)
    const unsigned int keyPAD6       = 81U;  //!< Keycode for the \c PAD6 key (architecture-dependent)
    const unsigned int keyPAD7       = 82U;  //!< Keycode for the \c PAD7 key (architecture-dependent)
    const unsigned int keyPAD8       = 83U;  //!< Keycode for the \c PAD8 key (architecture-dependent)
    const unsigned int keyPAD9       = 84U;  //!< Keycode for the \c PAD9 key (architecture-dependent)
    const unsigned int keyPADADD     = 85U;  //!< Keycode for the \c PADADD key (architecture-dependent)
    const unsigned int keyPADSUB     = 86U;  //!< Keycode for the \c PADSUB key (architecture-dependent)
    const unsigned int keyPADMUL     = 87U;  //!< Keycode for the \c PADMUL key (architecture-dependent)
    const unsigned int keyPADDIV     = 88U;  //!< Keycode for the \c PADDDIV key (architecture-dependent)

These points to all keyboard keys available for use in creation of interactive filter. I don’t recall a page where you can see all available ones.

Also, I note no period or comma there. No '[' or ']' either.

Lazy Arabesques
Related to Post 76. It’s great grandfather is the Roses one liner. And, there is an appearance of epicycles, but written in G’MIC this time, and not Python. Epicycles constitute the grande finale graphic of the upcoming Cookbook, and then there is this:

If I was a decent fellow, I’d post the listing for this, but the listing for this is a mess at the moment. It will be constituted, in some way or sideways, in the Arabesque Cookbook, coming next week in a tutorial set near you. For now, let it be known that the wheelies (epicycles) which draw arabesques live in the alternate reality of the frequency domain, where the analogues drift about in a lazy way. Back here in the time domain, a little z-blurring across frames renders the arabesques ever more languid. That’s it for now. Time to finish this thing. I said ‘next week’ last September, dammit.


Resurgence of the screen saver? At least in the mind of @grosgood… With some tweaks and a randomizer, it has the potential to be a fun G’MIC demo, among the existing ones like the Tower of Hanoi, if you are willing to put it there.

Take your time. There are a lot of TODOs on my side. Mostly optimization and better coding to existing script which is plenty of fun and sometimes boring to do.

For the first time in months and months! playtime with G’MIC. Topic: some illustrations for the upcoming arabesque plotting Cookbook recipe, an exercise in spectral play and using the math parser’s polygon() command. The demo code for all that lives in spectralarabesque.gmic.

First, basic use:

gmic -command spectralarabesque.gmic  \
     sw=512                           \
     -input '{$sw}','{$sw}',1,1       \
     -name. template                  \
     -repeat 360 k='{$>}'             \
         u='{$k/360}'                 \
         v='{2*$u*(1-$u)}'            \
         +gtutor_fwheelie[template] '{0.05+0.45*$v}',90,-1,'{0.125+0.5*$v}','{3*$k}',2,'{0.375*$v}','{-$k}',-5,'{0.25*$v}','{2*$k}',7 \
         -gtutor_specplot.            \
     -done                            \
     -remove[template]                \
     -output basic.mp4,24,H264        \

Next, an excuse to use thin plate splines in radial basis functions:

gmic -command spectralarabesque.gmic  \
     sw=1024                          \
     bi=15                            \
    -input '{$sw}','{$sw}',1,1        \
    -name. template                   \
    -repeat 360 k='{$>}'              \
        u='{$k/360}'                  \
        v='{3*$u*(1-$u)^2}'           \
        w='{3*($u^2)*(1-$u)}'         \
        +gtutor_fwheelie[template] '{0.5*$v}',90,-3,'{0.75*$w}','{3*$k}',2,'{0.5*$v}','{-$k}',-5,'{0.45+0.05*$w}','{2*$k}',-1 \
        -gtutor_specplot.             \
        -name. spiral_'{$k}'          \
    -done                             \
    -remove[template]                 \
    -append z                         \
    -deriche. '{$bi}',1,z,2           \
    -name. swirl                      \
    -normalize[swirl] 0,255           \
    -apply_curve[swirl] 1,0,0,63,40,127,160,200,240,255,255                          \
    -input '(0,63,127,189,255^255,250,220,0,0^127,230,220,50,255^20,80,180,100,255)' \
    -rbf. 255                         \
    -name. palette                    \
    -normalize[palette] 0,255         \
    -map[swirl] [palette],2           \
    -keep[swirl]                      \
    -split[swirl] z                   \
    -resize2dx '{0.5*$sw}',5,1        \
    -output rbfderiche.mp4,24,H264

Finally, let’s torment the cat.

gmic -command spectralarabesque.gmic  \
    bi=80                             \
    sw=512                            \
    -sample cat,'{$sw}'               \
    -name. cat                        \
    -input 100%,100%,1,1              \
    -name. template                   \
    -repeat 360 k='{$>}'              \
        u='{$k/360}'                  \
        v='{3*$u*(1-$u)^2}'           \
        w='{3*($u^2)*(1-$u)}'         \
        +gtutor_fwheelie[template]   '{0.5*$v}',90,1,'{0.5*$w}','{$k}',-2,'{0.5*(1-$v)}','{$k}',5 \
        -gtutor_specplot.             \
        -blur. '{$bi*$w}',1           \
        -resize. '{w#$template}','{h#$template}',100%,100%,5 \
        -normalize. 0,255             \
        +deriche. '{$bi*$w}',1,y,2    \
        -deriche.. '{$bi*$v}',1,x,2   \
        -append[-2,-1] c              \
        -normalize. -1,1              \
        -name. warper                 \
        -mul[warper] '{w#$cat*$w}'    \
        +warp[cat]  [warper],1,2,3    \
        -remove[warper]               \
        -name. warpedcat_'{$k}'       \
    -done                             \
    -remove[cat,template]             \
    -output warpedcat.mp4,24,H264

Here is where gtutor_fwheelie and gtutor_specplot live.

#@cli : gtutor_fwheelie : radius₀,angle₀,angular_velocity₀…
#@cli : Plot a two channel discrete frequency domain image corresponding to
#@cli : the supplied wheelie parameters: r, θ and ±ω triplet(s) on the command
#@cli : line, One triplet for each wheelie. Image suitable as a gtutor_specplot
#@cli : selection, which generates the arabesque.
#@cli : $ 1024,1024,1,1 gtutor_fwheelie. 0.5,67,1,0.25,0,-3 gtutor_specplot. name. circ_segment_triangle

gtutor_fwheelie :
   # Pseudo assignment expands to command line arguments

   # Expect data triplets $a1,$a2,$a3…

   -check {!($#%3)}
    -input $dwheelcnt,1,1,3
    -name. args
    -repeat $# j=$>
       if   $j%3==0
           -set[args] ${a{$j+1}},{int($j/3)},0,0,0
       elif $j%3==1
           -set[args] ${a{$j+1}},{int($j/3)},0,0,1
           -set[args] ${a{$j+1}},{int($j/3)},0,0,2
    -store[args] deltawheelies

   foreach {
      # For each selected image:
      # Fetch and check radius
      # velocity parameters. Aggregate Σf -> $accsf; Σθ → $acca
      # specw={k=int(min(w,h)/2);!(k%2)?k+1:k}

      #coeffcient image
      -input $specw,1,1,2
      -name carray
      -store[carray] coefficientarray

      # Iterate over arguments; populate carray and aggregate.
      -eval "const wc=$dwheelcnt;
             const sw=$specw;
      # Scale freq. dom. by dom. length - for ifft.
      # carray: frequency domain image generated from
      # the given wheelie chain.
      -input $coefficientarray
      -name. carray
      -mul[carray] $specw

#@cli gtutor_specplot : 
#@cli : Generate a phase plot from the selected frequency
#@cli : domain images.

gtutor_specplot :
   -foreach {
        # Frequency domain → time domain
        -name. carray
        -split[carray] c
        -append[-2,-1] c
        -name. temporal
        # Screenspace transform.
        -fill[temporal] ">
        -permute[temporal] cyzx
        -input {2*$sw},{2*$sw},1,1
        -name. canvas
        -eval[canvas] "begin(

Tomorrow looks like a good writing day. Might even push this Cookbook thing…


Garry, that’s beautiful. I couldn’t resist sharing your post on Twitter :slight_smile:

Glad to see you back with new stuffs!

1 Like

@grosgood The warping of the cat is fine but the shaking gives me an eye-/headache. :face_with_spiral_eyes:

It’s an outgrowth of the warp field eventually spanning the entire image, pulling all pixels, and these subject to rapidly changing gradient directions. That seemed in keeping with the feline abuse theme, but — while aesthetically consistent — I myself don’t find it pleasing; I’ve put it in the kit for whenever I need a cheap earthquake effect.

I have another idea for a tutorial, generating Aegean Number as images. To see what it looks like:

Base Symbol:

1234 in Aegean:

You could explore this so many ways.

1 Like

As in the single number: one thousand, two hundred and thirty four, I trust. On my first read, the glyphs came off the screen for me as ‘1,2,3,4’ (𐄇,𐄈,𐄉,𐄊). The mind is a funny place.

What do you have in mind? an exercise in structured drawing using math expression plotters, polygon() and ellipse()?

Perhaps a rough cheat sheet article can state the generality of putting plot point coordinates that fit in an origin-centered unit square in a math vector, an archetype entry in some sort of ‘glyph library’, then, in the specific plotting event, affine-transform the generic coordinates of a glyph library entry to serve the particular positioning of the glyph, the results of that being fed to polygon(). That cheat sheet may draw most of its content from ‘fun with affine transforms,’ to wit: scale, rotate, flip, shear, mirror and the like. However, your: ‘You could explore this so many ways’ suggests an interest in the Minoan/Mycenaean numbering system itself. That could be a fun off-topic jaunt, which I do from time to time in tutorials, but in the main, I don’t see that mapping into an expository on G’MIC technique.

Alas, but I have to throw that on the pile of TODOs. It has been 𐄛𐄗𐄇days since I’ve pushed to gmic-community, and the Arabesque article perpetually seems but 𐄇𐄋days to completion: it is a bit like nuclear fusion — the tutorial of the future! Always has been; always will be.