G'MIC Tutorial Fragments


“I juni måned” (In the Month of June) Laurits Andersen Ring

James Russell Lowell’s paean “And what is so rare as a day in June?” reveals this poet, literary critic and Atlantic editor, as a native of the upper latitudes of North America, for June on this part of the seaboard, my home, takes one’s breath away. Lowell spent most his life here. In the latter decades of the nineteenth century, he circulated almost exclusively in the environs between New York City and Boston.

Here above the horse latitudes, on the American eastern seaboard, these rare June days of crystalline air and low dew points straddle the summer solstice, and soon give way to a conveyor belt of Gulf Stream sogginess that persists, with intermittent relief, for the balance of the summer, propelled by an anti-cyclonic high pressure system known formally the North Atlantic (Subtropical) Anticyclone, and informally as the Bermuda High. I hope that it leads to pleasantness in Bermuda; its effects are imperfectly welcome here.

June sped by quickly, but a few tutorial pieces came to the fore. The effort to bring antique program control tutorials to a 2.9x setting has come to past:

  1. The “iffis” lead the way on May 31:-if…-fi…-elif…-else…-endif. I think the tutorial was one of the earliest written of the 1.6x series (2013) and all of the old examples utilized file or directory existence conditions, a scheme no longer supported after 2.6.; it was this that set off radioactivity alarms in the control flow realm.
  2. -repeat…-done was pulled into th 2.9x realm on Sunday June 6 and was largely re-written. The 1.6x examples were no longer workable, so the example was re-tooled.
  3. -local…-endlocal was similarly dragged into the 2.9x on June 12, much of it also re-written from scratch. The old tutorial had no working examples and was not complete; methinks it was published before it was finished. The most missing discussion was, to my mind, what happens when G’MIC merges local and global numeration.
  4. -rbf a new tutorial on Radial Basis Functions, rounds out June. Longish, but somehow just scratching the surface. How could I have left out generating warping vector fields on-the-fly, à la Interactive deformation and morphing. Well, -warp_rbf awaits in the wings for a future tutorial play, as does morph_rbf, the animated version… a great deal in store for extrapolators along a curve.

July, a steamy month in this locale, threatens anyone’s desire to Get Things Done: It’s Too Darn Hot. That said, there are still many basic, fundamental control flow commands that have never had benefit of any tutorial. Expect work along that course.

Along the lines of addressing the extreme basics, I have been given much thought to how rank beginners get through the very basic learning curve of the G’MIC command tool. Part of this is motivated by the collaboration with @myselfhimself in conveying to pythonistas what to feed the run() command. Part of this is also knowing that those who get initial success in obtaining what they set out to do have a rewarding sense of accomplishment — and so they set out to do more. Conversely, those who hit brick walls are just motivated to use something else. The game is to improve the success rate on those very early, rank-beginner explorations, but I don’t wish to duck the issue by presenting a portfolio of easy commands, as done here as they bypass the heart of the matter. Rather, I want to get to the heart of the matter, which is — I think — coping with the broad (and difficult) image manipulation freedom that the command line tool offers. It overwhelms. So I have been going though many old discuss.pixl.us and gimpchat.com posts in a forensic frame of mind, wondering how best to ease people into this. Not sure where something like this will wind up in the tutorial tree. Stay tuned.

2 Likes

Hi Gary,
that was pure pleasure to read your intro of new tutorial fragments.

I think I have to learn better English and to look into Lowell. Still I had recently some similar experience reading “Levines Mühle” from Johannes Bobrowski in German. Possibly, you can manage that. J. B. was a baltic Lithuanian German in the last century, my parents generation.

@grosgood : Now I have another idea for you. I have made a sample code that actually performs the same operation on multiple threads at once, and this beats apply_tiles, apply_parallel as you’re not restricted to a image, and you can do things like dynamic array, and insert into a image.

Reptorian's Sample Code which allows a very much non-standard multithreaded operation
rep_new_dla:

n_threads={$_cpus}
mt={$n_threads-1}

l. 
 $n_threads,1,1,1
 +s. x
 r[^0] 100%,100%,100%,2
 store[^0] dla_ref
endl

length_const_line=""
xp_line=""

length_const_line.=begin(

repeat $n_threads
 
 insert_dar_pos={$>+2}
 insert_ref_pos={2+$n_threads+$>}

 length_const_line.="const v"
 length_const_line.=$>=
 length_const_line.=h#$insert_ref_pos
 length_const_line.=;
 
 xp_line.=x==
 xp_line.=$>
 xp_line.=?(
 xp_line.=repeat(v$>,refpos,
 xp_line.=dar_insert(#$insert_dar_pos,I(#$insert_ref_pos,0,refpos));
 xp_line.=);
 xp_line.=)
 
 if ($>!=$mt) 
  xp_line.=:
 else
  xp_line.=;
 fi
 
done

length_const_line.=);

echo $length_const_line
echo $xp_line

set 1,50%,50%,0

repeat $! l[$>]
 +rep_noise_poissondisk_to_coordinates 8
 l[0] $dla_ref endl
 s. y,$n_threads
 eval[1] :${-math_lib}$length_const_line$xp_line
endl done

The reason this works is that first, you generate the string which is dependent the number of threads that you have. And then, it will compile based on the string given.

This is the generated 12-threads code

Code
begin(
 const v0=h#14;
 const v1=h#15;
 const v2=h#16;
 const v3=h#17;
 const v4=h#18;
 const v5=h#19;
 const v6=h#20;
 const v7=h#21;
 const v8=h#22;
 const v9=h#23;
 const v10=h#24;
 const v11=h#25;
);
x==0?(
 repeat(v0,refpos,
  dar_insert(#2,I(#14,0,refpos));
 );
):x==1?(
 repeat(v1,refpos,
  dar_insert(#3,I(#15,0,refpos));
 );
):x==2?(
 repeat(v2,refpos,
  dar_insert(#4,I(#16,0,refpos));
 );
):x==3?(
 repeat(v3,refpos,
  dar_insert(#5,I(#17,0,refpos));
 );
):x==4?(
 repeat(v4,refpos,
  dar_insert(#6,I(#18,0,refpos));
 );
):x==5?(
 repeat(v5,refpos,
  dar_insert(#7,I(#19,0,refpos));
 );
):x==6?(
 repeat(v6,refpos,
  dar_insert(#8,I(#20,0,refpos));
 );)
:x==7?(
 repeat(v7,refpos,
  dar_insert(#9,I(#21,0,refpos));
 );
):x==8?(
 repeat(v8,refpos,
  dar_insert(#10,I(#22,0,refpos));
 );
):x==9?(
 repeat(v9,refpos,
  dar_insert(#11,I(#23,0,refpos));
 );
):x==10?(
 repeat(v10,refpos,
  dar_insert(#12,I(#24,0,refpos));
 );)
:x==11?(
 repeat(v11,refpos,
  dar_insert(#13,I(#25,0,refpos));
 );
);
1 Like

Some more awesome stuffs by @grosgood ! :beers: :+1:

https://gmic.eu/tutorial/dowhile.html

do_elecamp

Busy boxes presage a tutorial:

busybox

gmic -m busybox.gmic -busyboxes 512,0.008,98,-0.75,20 

This on the -for-done conditional — I believe such a construct is in the busybox script somewhere… I am sure of it!

busybox.gmic
busyboxes: check "isint(${1=128}) && ${1}>=128 && \ # Square image sz, pixels
                  isnum(${2=0.02})             && \ # Seeding
                  0.001<${2}&&${2}<=0.5        && \ # • Larger → more boxes
                  isnum(${3=96.5})             && \ # Limit spread
                  0<${3}&&${3}<100             && \ # • Smaller → more limited
                  isnum(${4=0})                && \ # Color
                  -1<=${4}&&${4}<=1            && \ # • Pink(-1) to Green(1)
                  isnum(${5=20})  && ${5} >0"       # Sharpness: Larger → +edginess
    $=busybx_
    lbound={exp(($busybx_3/100)-1)}
    ccolor={0.5*pi*$busybx_4}
    # Render image
    -input $busybx_1,$busybx_1,1,3
    -name. boxplots
    -noise $busybx_2,2
    -abs.
    # Convolution kernels
    -input 3,3,1,1,-0.125
    -set. 1,1,1
    -name. neg
    -input 3,3,1,1,0.125
    -set. 1,1,1
    -name. pos
    cnt=0
    # Convolve and spread
    -for abs(im#-3)<=$lbound&&$cnt<1000
        -if $cnt%2
           -convolve[boxplots] [pos],2,1
        -else
           -convolve[boxplots] [neg],2,1
        -fi
        cnt+=1
    -done
    -keep...
    # Colorize
    +luminance.
    -normalize 0,1
    -fill[boxplots] ">begin(rv=[1.0,0.23,0.1157]);
                      rot(rv,($ccolor+pi*(2*i(#-1)-1)))*I"
    -keep[boxplots]
    -sharpen $busybx_5
    -normalize[boxplots] 0,255
1 Like

@grosgood

Two three things.

  1. You don’t need to use ‘-’. If you note my commands, I never use - next to their commands.

  2. input isn’t necessary. Just 3,3,1,1,-.125 would do for example.

Also I find this interesting:

$=busybx_
    lbound={exp(($busybx_3/100)-1)}

What was the point of $=…? I only use it to access a specified argument. For example:

#@cli rep_mn: eq. to 'rep_multinormalize' : (+)
rep_mn: rep_multinormalize $*
#@cli rep_multinormalize: values
#@cli : Normalize based on channels using values.
#@cli : (eq. to 'rep_mn').\n
#@cli : Author: Reptorian.
rep_multinormalize:
repeat $! l[$>]
 repeat s
  $=a
  $=b
  val_a=${a{$>*2+1}}
  val_b=${b{$>*2+2}}
  sh $>
  normalize. $val_a,$val_b
  rm.
 done
endl done
1 Like

Voynich Manuscript
[Detail of the nymphs on folio 78r of the Voynich Manuscript Beinecke Rare Book & Manuscript Library, Yale University, New Haven, Connecticut, USA.

And so July is done, and the dog days of August are upon us, here on the Eastern Seaboard of North America. July was not as steamy as she could be, the latter two weeks being rather mild, and for some days with dew points in the mid to low 60°s Fahrenheit, ‘Fahrenheit,’ children, one of the many measures from the days of yore that we Americans still treasure.
I have been more sketching and planning than writing: getting grist for the mill. Thus, little came out through the door in July:

*do…while was unearthed from the 1.6x antiquities; I retained, but updated, its extended example on lightning bolts.
*for…done slipped past the door in the waning hours of 31 July. It freights a sidebar on Variance. One might notice a pattern in these workaday control flow commands: they are a good setting to slip in a little image theory. Not much, but a little. G’MIC does not operate in a vacuum; where possible, making home for an interesting or vital concept is intent on connecting the language with its world. Hopefully, this is not too painful.

@KaRo made an interesting remark on documenting when a document comes into being. Still uncomfortably aware of how much 1.6x documentation is still being taken seriously, I felt it responsible for me to begin dating these tutorials, now that Git so nicely records their development histories. Going forward, I will date the new and retrodate the older in the 2.9x series. There’s little to be done about the 1.6x series but rewrite them out of existence, for they are pure HTML with no backing originals.

It may not have been his intention, but in post 60 of this thread @Reptorian bought to the fore a responsibility where I have been remiss, so it seems worth my while to address that shortcoming now in these lackadaisical dog days of August, before the quickening press of September is upon us: by what principles am I guided to write these tutorials, in particular, a script coding style that I term “Tutorial writing?”

In fact, I have quite strong opinions on the matter, ones that favor redundancy over reduction, and I don’t want the vigour to which I hold these principles to translate in anyway to a seeming rebuff of the good @Reptorian, who is going about around here as he often does by being helpful. First and foremost, I want to make clear that I appreciate his helpfulness, hope to benefit more from it, and generally celebrate his being around.

That said, I should like to express my intent that explicit -input notation, hyphen-and-all, will be part of G’MIC tutorials so long as I have any part in the endeavor. The reasons, I think, are unassailable — but I have made no effort to advance what those reasons are, for which I have been remiss. So thank you @Reptorian, for bringing the matter to the fore, and what better place to do it than here in Tutorial Fragments?

To the rest of you developing twitches in the face of exciting discussions on Documentation Standards, I bid you a fair adieu, but before you drop entirely from the thread allow me to leave you with this: Clarity matters. In writing code, it is bearing in mind that someone will be reading it. Presuming you’ve done anything useful at all, it will be read, and if it is to serve as an agent to advance this particular craft, then it should be as readable as your skills can make it. That may entail coding in a style not quite so reduced as personal habit leads. That’s it. Drop now, and fair thee well.

One or two still here. Gee. Must be a slow day at the Krusty Krab. Very well then. Last March I went to some length about the process of tutorial writing Post 28, Instructions to help writing reference documentation, somewhat centered on how hard it is to get out of bed and find the bathroom, let alone write anything useful. What I didn’t address over there, but will here, is G’MIC’s recondite coding style, at once its strength and encroaching weakness. Fortunately, G’MIC’s design has a great deal of influence on coding style, such that one can, with deliberation and foresight, code in a way that avoids the allure of reduction in favor of redundancy. The trick, I think, turns on the realization that code can be read. Realizing that, one can then proceed to have her code deliberately read. It is a sociable thing to do, which is more than being pleasant. As I noted last month, those who have an initial success in steering G’MIC to do something useful enjoy a rewarding sense of accomplishment — and so they set out to do more. Conversely, those who hit brick walls are just motivated to use something else. G’MIC’s survival in the Darwinist world of open source depends on lots of little successes just like that. The key then, is engineering successes just like that.

Some time ago, @David_Tschumperle made clear his design goals for the G’MIC command line interpreter. See Looking back at 10 years of development. The two design goals were conciseness and coherence. By implementing highly consistent parsing rules, the G’MIC team in the days of yore achieved coherence, which furnishes a foundation for conciseness. With conciseness, we may routinely omit -input, say, and, given the coherent parsing rules that underlie command line parsing, nothing untoward happens: the command line parser quietly infers -input where it could have been and, if the pipeline is not too infested with typos, misconstructions or malapropisms, proceeds on its merry way. Through its substitution rules, G’MIC consolidates at levels approaching Forth: normalizen, set=, input → , among others, and if the out-of-the-box syncopations do not suffice, one may write <shortname> : <long form> substitutions — Adding Custom Commands in a nutshell — reducing, say, turbulence to mxup via a mxup : turbulence $* customization or some such.

In very short order, many sense the Game: consolidating code as a kind of solitaire, one that engenders enormous satisfaction in puzzling out how little one may write to do a lot.

This Game is not peculiar to G’MIC. It may be played with any language, computer or natural, and to serve pragmatic ends. I am sure many of you have heard the writers’ lament: “I am sorry I wrote you such a long letter; I did not have time to write a short one.” There is much time consumed in clarifying ideas so that words don’t get in the way: pouring through dictionaries to find that one nugget which dissolves a phrase, crafting apt transitions to burn down bridging paragraphs, sifting through similes and metaphores for just those that crystalize ideas shimmering on the edge of perception. It is a labor writers must do in the service of their readers, who, like all souls, live with time-and-space constraints and cannot spend forever reading a ramble. But it is also a labor in which writers take a great deal of pleasure, for it is again just the Game, and the Game’s condense-and-consolidate puzzles exercise our intellects and provide the thrill of the chase. Truth be told, I wouldn’t have it any other way. It would be churlish of me, therefore, to lodge any sort of complaint against the reduced-form of G’MIC coding, especially since I practice it myself, with all the childish delight of a chimpanzee in a poo fight. But — Alas! — The road to Hell is paved with good intentions. For all the good intentions behind the sophisticated and varied substitution rules underlying the most abbreviated, yet effective, G’MIC command, these very rules raise not an inconsiderable barrier to newcomers, whose oft-time response is this:

[quote=“punchcard, post:29, topic:9966”]
Did you ever create a filter or other GMic method to create these tilings? I’m having trouble figuring out the GMic commands because everyone uses single letter commands and no comments.[/quote]

The flip side of Reduction is Redundancy — pehaps the Anti-Game. Those who train themselves on writing a little to do a lot look upon Redundancy with not a little displeasure. Among cognoscenti conversing in the cloister there is indeed an ingrained resistance to perceived excess. However, the Hubble Space Telescope is still useful as of this writing because of triply-redundant systems. Commercial twin-engine aircraft can still get to an airport with just one engine. No one is suggesting, however, that this one engine suffices, because getting to an airport with no engines is hard.

The triply-redunant regimen taught to me is in pyramid form: “Tell 'em what you’re going to tell them. Tell 'em. Then, tell 'em what you told 'em.” The first part, the pinnacle, is a very concise précis about ‘Foo’, calibrated at just below the TL;DR tripwire. For semi-seasoned readers needing reminders, the first part triggers: “Ah! Foo! I remember Foo. I just need to get the Bar piece and then I’m good to go.” So the semi-seasoned peel away, leaving those still at sea with the middle part of the pyramid. In the middle part, we unpack Foo, present its relation to Bar, go over why Foo needs Bar while Bar may not need Foo, but heaven help you if you try to go through life without Foo, only Bar. At this juncture we hope to have cleared the hall but there are still stragglers in the cheap seats, way up in God’s Country, and the blank expressions on their faces tell you they Just Don’t Get It. Knowledge has a chiral character and a right-handed unpacking of Foo only works for some. So in the bottom part of the pyramid we unpack Foo again, only left-handedly: different words, new similes, slower unwindings and at a finer grain, and we leave Determinants to the last: the folk with left-handed brains always seem to struggle with Determinants.

There are more than a few Darwinists around perfectly willing to tell those at the bottom of the pyramid to just bugger off; pay people to give you the basics and come back here when you know how to ask a question. There is an economic side to how much time one can invest in walking people up a curve and it is a question with which I constantly struggle, for it has no clean resolution. My judgements in this matter tend to be provisional and are case-specific. For G’MIC, from which I took about a five year hiatus, my provisional judgement is grounded upon sobering observations when I came back: a community that has grown not that much in the intervening years — maybe it has shrunk! — and that withall suffers from the stress assailing many open-source projects: the participants are highly skilled and, for the talented, the commercial world has plenty of money. That, and the tendency of more experienced participants for acquiring greater responsibilities as their careers progress. So @David_Tschumperle’s cautionaries in Some news from the G’MIC developers’ corner! are unsettling but not surprising. These, too, are Darwinist pressures: a project either takes up new people to assume the load from those being drawn away — or it doesn’t. Full stop.

These issues are far larger than I am, so it comes to the piece that I can manage. It is this: technical communication, harnessed to draw in newcomers. It is what I do best and I can be at it as long as my health holds up and the City of New York doesn’t roll off the Eastern Seaboard. I find it quite remarkable, and very much to the credit of G’MIC development, that the language can be written at a variety of compression levels, from verbose to terse. Its curse is its salvation, the twain in ways depends upon intent. Any mature and intelligent practitioner of a craft knows when it is time to pick the right kit for the job.

For private experimentation, and perhaps communication with fellow cognoscenti, terse is fine, and, in fact, is the proper and polite way to go. One does not bore friends with redundancy. But in a more public sphere, with the aim to establish and advance acceptance for a craft, then clarity matters, and clarity is necessary for any active intent to communicate. Lacking that, the work becomes a latter day Voynich Manuscript, its somber mystery of little practical use in the advance of craft. So in the Tutorial sphere, I shall be the curmudgeonly guardian for the verbose Tutorial style, insistent on hyphens, full command spellings, named images and descriptive variable names, for all these offer clues of intent and engender readability. In time, perhaps, the student discovers the allure of syncopation, senses the Game, sees how little she can get away in typing and still pull off the trick. I will pretend not to notice. And if I think no one is looking, may hazard a smile.

2 Likes

Verbosity makes my eyes sting dry.
input is indispensable here:

gmic sample tiger input[0] 100%,50%,1,1

vs

gmic sample tiger 100%,50%,1,1 move[-1] 0

Certainly. You’ve read the Post 61 rant on curmudgeonly opinions, so leave it at that.

Ditto here, no? We are both using the same $-expression mechanism. See Adding Custom Commands, ninth and last sub-bullet. Perhaps this is not immediately obvious because our notations superficially differ: you are harnessing a double-substitution mechanism, because your choice of argument index depends on the channel to which you have iterated. Say, at the second iteration with $> == 1 (‘G’ channel):

rep_mn.  40,100,5,200,0,255 # De-emphasize selected’s red channel
$=a # $-expression generates an assignment sequence with the base name `a`
    # for however many elements on the command line there may be
…
val_a=${a{$>*2+1}} → val_a=${a3} → val_a=5

At the second stage of this double-substitution, the inner expression resolves to a3, referencing a variable created by the $=a $-expression and assigned 3, Substituting again sets val_a to the third command line argument: 5. That selects the lower boundary of the normalization to be applied to the green channel of the currently selected image.

Here:

-busyboxes 512,0.008,98,-0.75,20
…
$=busybx_  # $-expression generates an assignment sequence with the base name
           # `busybx_` for however many elements on the command line there may be.
…
lbound={exp(($busybx_3/100)-1)} → lbound={exp((98/100)-1)} → lbound=0.9801987…

selects the third argument 98 directly, establishing a lower boundary with which the user is content. I see no fundamental difference in how we both harness $-expressions; we are both selecting elements from a (virtual, pseudo) argument ‘vector’.
§
For readers dropping in from search engines, G`MIC $-expressions permit pseudo-array actions with the G’MIC command line processor, which has no intrinsic array construct. Consider ssub.gmic:

ssub :
 $=a
 -repeat $#+1
    -echo[] "Argument No. "$>" and its value: "${a{$>}}"."
 -done
 -quit

To observe how the G’MIC manages $-expressions, run this command with a boosted verbosity:

gosgood@bertha ~ $ gmic -verbose 3 -command ssub.gmic ssub 45,192,dog,800A,1-800-555-1212,"Now is the time for every good boy to eat fudge!"
[gmic]-0./ Start G'MIC interpreter.
[gmic]-0./ Set verbosity level to 3.
[gmic]-0./ Import commands from file 'ssub.gmic' (2 new, 2 replaced, total: 4379).
[gmic]-0./ssub/ Set local variable 'a0=ssub'.
[gmic]-0./ssub/ Set local variable 'a1=45'.
[gmic]-0./ssub/ Set local variable 'a2=192'.
[gmic]-0./ssub/ Set local variable 'a3=dog'.
[gmic]-0./ssub/ Set local variable 'a4=800A'.
[gmic]-0./ssub/ Set local variable 'a5=1-800-555-1212'.
[gmic]-0./ssub/ Set local variable 'a6=Now is the time for every good boy to eat fudge!'.
[gmic]-0./ssub/*repeat/ Start 'repeat...done' block (7 iterations).
Argument No. 0 and its value: ssub.
Argument No. 1 and its value: 45.
Argument No. 2 and its value: 192.
Argument No. 3 and its value: dog.
Argument No. 4 and its value: 800A.
Argument No. 5 and its value: 1-800-555-1212.
Argument No. 6 and its value: Now is the time for every good boy to eat fudge!.
[gmic]-0./ssub/*repeat/ End 'repeat...done' block.
[gmic]-0./ssub/ Quit G'MIC interpreter.

The G’MIC command line parser replaces the pseudo-assignment $=var with a series of local variable assignments, var0=<first argument> var1=<second argument>… , equal to the number of elements on the command line — seven, in the example, enumerated from [0 – 6]. The first local assignment, enumerated zero, contains the name of the invoked command. The remaining assignments contain the command’s arguments in the order written.

This is a pretty little thing — normalizing channels independently — as in colorshifting footage:

Raw cauldron footage:

 $ gmic -input 256,256,600,3 -noise. 3,2 -bandpass. 0.01,0.02 o. bpass.cimg

Blue version:

$ gmic -command ssub.gmic -input bpass.cimg -rep_mn. 40,100,5,200,0,255 -split. z o bpass_bluish.mp4

Red version:

$ gmic -command ssub.gmic -input bpass.cimg -rep_mn. 0,255,5,200,40,120 -split. z o bpass_reddish.mp4

Though, perhaps, you might consider:

gtutor_mn: gtutor_multinormalize $*
#@cli gtutor_multinormalize: values
#@cli : Normalize based on channels using values.
#@cli : (eq. to 'gtutor_mn').\n
#@cli : Author: Reptorian.
gtutor_multinormalize:
$=a
repeat $! l[$>]
 repeat s
  val_a=${a{$>*2+1}}
  val_b=${a{$>*2+2}}
  sh $>
  normalize. $val_a,$val_b
  rm.
 done
endl done

because in the original:

  $=a
  $=b

the $-expressions (1) expand to an identical series of assignments, as they each necessarily operate on the same command line (there is only one), and (2) being in the innermost loop, these $-expression expansions are invoked 2 ($=a and $=b) × 3 (number of channels in each image) = 6 times per image. A bit of a shuffle. However, moving the $-expression $=a out of both loops and reusing the same $-expression series assignment for both val_a and val_b cuts down on the back-and-forth, no?

A quibble perhaps, as in the larger pipeline, ffmpeg encoding consumes the majority of time. In any case, a nice utility. Thank you!

2 Likes

It is not well known, but when writing out the -input command explicitly, the selector acquires a special meaning: it indicates what position in the image list an incoming image should go. See the Selection section, -input tutorial. With care, an incoming image can even be placed in more than one location.

  gmic -input[0] amp_y.png -input[0,-1] amp_c.png

Yes, I do that sometimes. My example was incomplete. That is why you are the tutorial writer. :stuck_out_tongue:

gmic seekbetter.gmic seekbetter 1024,576,256,1024,25,1.0625,50,40,50
 $1 → width                    →      1024
 $2 → height                   →       576
 $3 → slices                   →       256
 $4 → sprite count             →      1024
 $5 → acceleration attenuation →        25 (0: no attenuation (frantic animation) 100: attenuated to zero (languid animation))
 $6 → Velocity dampening       →    1.0625 (0: no dampening 100: dampens rapidly to zero)
 $7 → Microtiks per tik        →        50 (Lower, faster background paging. 50: microtik (50÷100) × slice count before advancing to next slice).
 $8 → Spectral turbulence      →        40 (0: Low frequency background gradient. 100: High frequency background gradient).
 $9 → Blend background         →        50 (0: Spectral field; 100: solid blue-gray. 50: 50-50 blend).
$10 → Recording mode           →  False(0) (True: start in recording mode. 's' on keyboard stops recording. Otherwise 'r' starts recording).

Concept

Sprites seek better homes. Bottom gradients with just teeny-tiny acceleration vectors, A place where a weary sprite can nestle down and get some sleep. Bliss! But, Alas! Water-shedding down an acceleration gradient develops velocity, maybe too much, and too often the poor sprite shoots past an inviting bottom gradient, only to fly up on the opposite slope — maybe even past it! With luck, with time, a sprite may slide into a bottom gradient without a lot of velocity, and with a bit of oscillation, settles in. Maybe other sprites can slide in on low delta too so that one and all become a part of a nice, squiggly pile of sprites. Then too, sometimes stability comes from being on the go: the nudging acceleration vectors do so just periodically enough to settle the sprites into circular paths, or along latitude/longitude lines of the torus.

And over the eons, plate tectonics happen. Mountains fall. Oceans rise. Bottom gradients shift and become inhospitable, sometimes cataclysmically, sending sprites off, yet again, to seek better homes.

Sprite Life

On a microtik:

  1. Dampen velocity a smidgen (90% - 95%).
  2. Sample the background pixel upon which the sprite sits, obtaining raw accelerations in x and y.
  3. attenuate the acceleration by a constant factor
  4. Add acceleration components to velocity, a delta-V.
  5. Orient the sprite along the velocity vector
  6. step the sprite

On a tik:

  • Step the background to the next torroidal slice - it’s almost, but not quite, the same as the slice before. This skews all the acceleration samples slightly.
  • On hitting ESC: Animation shuts down.

The outline for this appears in Post 54. Here is how the implementation turned out.

seekbetter.gmic
#@cli seekbetter : _width>64,_height>64,_depth>64,_sprite_count>0,0<=_acceleration_decay<=100,0<=_velocity_decay<=100,_microtik_rate>=0,_turbulence>=5,_backblend>=0
#@cli : Generate a `Seeking a Better Home` animation.
seekbetter :  -check "isnum(${1=512}) && ${1}>=64 && "\
	             "isnum(${2=512}) && ${2}>=64 && "\
	             "isnum(${3=128}) && ${3}>=64 && "\
		     "isint(${4=128}) && ${4}>0   && "\
		     "isnum(${5=50})  && ${5}>=1  && ${5}<=100 && "\
		     "isnum(${6=1})   && ${6}>=0  && ${6}<=100 && "\
		     "isnum(${7=50})  && ${7}>=0  && ${7}<=100 && "\
		     "isnum(${8=50})  && ${8}>=5  && ${8}<=100 && "\
		     "isnum(${9=0})   && ${9}>=0  && ${9}<=100 && "\
                     "isbool(${10=0})"
   # Set up parameters
   wid=$1                       # Field width: pixels 128 minimum
   hgh=$2                       # Field height: pixels 128 minimum
   dph=$3                       # field depth/tiktok slices: pixels: 64 minimum
   cnt=$4                       # Sprite count: count of swimming thingies — whatever they are. Positive integral count
   mul={exp(-$5/10)}            # Acceleration decay 0→100 (0 → 10 common) unitless multiplier
                                # 100 decays a lot, 0 not at all
   dcy={exp(-$6/100)}           # Velocity decay 0→100 (0→10 common): unitless real 100 decays a lot, 0 not at all 
                                # i. e., high decay: little velocity carry-over to the next microtik.
   tsz={round($dph*$7/100,1,0)} # Microticks per tiktok. lower microtick count evolves background at a faster pace.
   tub={$8/100}                 # Turbulence: 5 → 100. 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 in a "$wid"×"$hgh"×"$dph" field "\
              "Sprite count (>0): "$cnt"; Relative Acceleration Attenuation (0→1): "$mul"; "\
              "Relative Velocity Decay (0→1): "$dcy"; Rel. Microtiks per Ticktok (frames) : "$tsz"; "\
              "Background Turbulence (>0.05): "$tub"; Background Fade (0→1): "$bkb
 
   # Generate data structures and viewport
   tiktok=0
   oc=0
   -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
   -do
       # Set the viewport background
       -fill[viewport] ">[i(#-2,x,y,"$tiktok",0),i(#-2,x,y,"$tiktok",1),0]"
       -norm[viewport]
       -normalize[viewport] 0,255
       -map[viewport] tarn
       -fill[viewport] "*lerp(I(#-1),[5,30,50],"$bkb")"
       +store[viewport] topography
       -window[viewport] $wid,$hgh,0,0,0,0,"Seeking a Better Land"
       -echo[] "Ticktok is: "$tiktok

       # Microtik loop. tsz → tick size → number of microticks before
       # advancing one frame to the next parameter space torus.
       -repeat $tsz

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

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

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

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

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

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

          # Refresh background graphic
          -input $topography
          -name. topography
          -fill[viewport] I(#-1)
          -remove.
       -done
       -if $tiktok<$dph-1
          tiktok+=1
       -else
          tiktok=0
       -fi
   -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
   -else
      -error "Channel count="{s}" is unexpected. 4 channels needed for sprites."
   -fi

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   && ${5}<=1"

   # 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*$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,u([2,2,2])-[1,1,1]
   -name. snoise

   # Shape snoise into an approximate Gaussian distribution
   -input 100%,100%,100%,1
   -set. {whd},{(w/2)-1},{(h/2)-1},{(d/2)-1},0
   -blur. $tub,1,1
   -normalize. 0,1
   [-1]
   -append[-2,-1] c
   -name. smask
   -mul[snoise,smask]
   -name. specspace
   -mul[specspace] {whd}

   # 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

   # zero-value dummy for imaginary component of each channel
   [-1]
   -fill. 0

   # Inverse Fourier Transform into temporal space
   -ifft[-2,-1]

   # Heuristic: Choose the most nearly zero mean. (No basis in math)
   -if ia#-2>ia#-1
      -rm..
   -else
      -rm. 
   -fi
   -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"
   stp=$2
   mul=$3
   dcy=$4
   -pass$1 1
   -fill.. ">
              dyn=I;
              acc=I(#1,dyn[0],dyn[1],"$stp");
	      dyn[2]*="$dcy";
	      dyn[3]*="$dcy";
              dyn[2]-="$mul"*acc[0];
              dyn[3]-="$mul"*acc[1];
              dyn[0]+=dyn[2];
              dyn[1]+=dyn[3];
              dyn
           "
   -remove.

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. ">
            dI=[
                  i(x,0,0,0)<"$lx"?"$dx":i(x,0,0,0)>("$lx"+"$dx")?-"$dx":0,
                  i(x,0,0,1)<"$ly"?"$dy":i(x,0,0,1)>("$ly"+"$dy")?-"$dy":0,
                  0,
                  0
               ];
            I+dI
         "

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. ">
              ang=atan2(i(x,0,0,3),i(x,0,0,2));
              ellipse(#0,i(x,0,0,0),i(x,0,0,1),"$ra","$rb",ang,"$op",[80,180,255]);
              ellipse(#0,i(x,0,0,0),i(x,0,0,1),-"$ra",-"$rb",ang,1,0xffffffff,[255,255,255]);I"
   rm.

That’s it for now. Looks like a Cookbook, but a write-up won’t be 'til fallish. Thoughts, suggestions welcome. Probably drop this into garryosgood.gmic, gtutor_seekbetter, in the sweet fullness of time.

2 Likes

That is really impressive. Shows what is possible with G’MIC. Does remind me a bit of Processing works I have seen, but never bothered getting around doing animated things like that.

Me as well… Processing life

Hello Garry,
sorry I did a G’MIC summer break these last months.
I have read a bit of your last comments here and am very humbled :slight_smile:
I can dedicate a bit of time for making gmic-py releases go forward, but would be glad that this happen in some kind of remote sessions with other open source graphics related people, with some online appointment…

@grosgood By the way, have you checked this thread? - How to write clean G'MIC code ?

The reason I pointed to that thread is because of the use of tabs. It isn’t recommended to use it. I use KDE Kate, other might use Notepad or something, etc.

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.

seekbetter.gmic
#@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 && "\
                     "isbool(${10=0})"

    # 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
   tiktok=0
   oc=0
   clr=0
   -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
   -do
       # 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
          -remove.
          -hsv2rgb[viewport]
       -else
          -fill[viewport] "[i(#-2,x,y,$tiktok,0),i(#-2,x,y,$tiktok,1),0]"
          -norm[viewport]
          -normalize[viewport] 0,255
          -map[viewport] tarn
       -fi
       -if $bkb
          -fill[viewport] "*lerp(I(#-1),[5,30,50],"$bkb")"
       -fi
       +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..."
             -quit

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

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

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

          # 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."
                   rec=0
                   oc=0
                -fi
             -fi
             -outputn[viewport] "seek/seekbetter.png",$oc
             oc+=1
          -fi

          # Refresh background graphic
          -input $topography
          -name. topography
          -fill[viewport] I(#-1)
          -remove.
       -done
       -if $tiktok<$dph-1
          tiktok+=1
       -else
          tiktok=0
       -fi
   -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
   -else
      -error "Channel count="{s}" is unexpected. 4 channels needed for sprites."
   -fi

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
   -ifft[-2,-1]
   -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"
   stp=$2
   mul=$3
   dcy=$4
   -pass$1 1
   -fill.. ">
              dyn=I;
              acc=I(#1,dyn[0],dyn[1],"$stp");
              dyn[2]*="$dcy";
              dyn[3]*="$dcy";
              dyn[2]-="$mul"*acc[0];
              dyn[3]-="$mul"*acc[1];
              dyn[0]+=dyn[2];
              dyn[1]+=dyn[3];
              dyn
           "
   -remove.

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. ">
            dI=[
                  i(x,0,0,0)<"$lx"?"$dx":i(x,0,0,0)>=("$lx"+"$dx")?-"$dx":0,
                  i(x,0,0,1)<"$ly"?"$dy":i(x,0,0,1)>=("$ly"+"$dy")?-"$dy":0,
                  0,
                  0
               ];
            I+dI
         "

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. ">
              ang=atan2(i(x,0,0,3),i(x,0,0,2));
              ellipse(#0,i(x,0,0,0),i(x,0,0,1),"$ra","$rb",ang,"$op",[80,180,255]);
              ellipse(#0,i(x,0,0,0),i(x,0,0,1),-"$ra",-"$rb",ang,1,0xffffffff,[255,255,255]);I"
   rm.
1 Like

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


a probably earlier than that.