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:
- Dampen velocity a smidgen (90% - 95%).
- Sample the background pixel upon which the sprite sits, obtaining raw accelerations in x and y.
- attenuate the acceleration by a constant factor
- Add acceleration components to velocity, a delta-V.
- Orient the sprite along the velocity vector
- 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.