examples/animated-textures.py

This is one of the example scripts included with Shady. These scripts can be run conventionally like any normal Python script, or you can choose to run them as interactive tutorials, for example with python -m Shady demo animated-textures

#!/usr/bin/env python
# $BEGIN_SHADY_LICENSE$
# 
# This file is part of the Shady project, a Python framework for
# real-time manipulation of psychophysical stimuli for vision science.
# 
# Copyright (c) 2017-2022 Jeremy Hill, Scott Mooney
# 
# Shady is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# 
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with this program. If not, see http://www.gnu.org/licenses/ .
# 
# $END_SHADY_LICENSE$

#: How to switch between frames of a multi-frame image
"""
This file demonstrates two different ways of switching
between frames of a multi-frame image.

This demo requires third-party packages `numpy` and
`pillow`.
"""#.
if __name__ == '__main__':

	"""
	First deal with the demo's command-line arguments,
	if any:
	"""#:
	import Shady
	cmdline = Shady.WorldConstructorCommandLine()
	cmdline.Help().Finalize()
	Shady.Require( 'numpy', 'Image' ) # die with an informative error if either is missing
	
	"""
	Create a World:
	"""#:
	w = Shady.World( **cmdline.opts )
	
	"""
	Now we'll create an inhabitant.
	"""#:
	filename = Shady.PackagePath( 'examples/media/alien1.gif' )
	s = w.Stimulus( filename )

	"""
	Now we'll make him walk, by setting his `.frame`
	property to a function of time:
	"""#:
	s.frame = lambda t: t * 16

	"""
	The `.frame` property, like many World and Stimulus
	properties, supports dynamics.  That means that,
	instead of setting it to a constant numeric value,
	you can assign a function of time.
	
	If you ask to retrieve `s.frame`....
	"""#:
	print( s.frame )
	
	"""
	...then you get the instantaneous numeric value of
	the property. If we do it repeatedly, we will get
	different values:
	"""#:
	print( s.frame )
	import time;  time.sleep(0.5)
	print( s.frame )

	"""
	Stimuli can be created in this way from animated GIFs
	(or equivalently from lists of pixel arrays, each array
	specifying one frame). What actually happens is that the
	frames are concatenated horizontally to form one wide
	strip in the "carrier" texture:
	"""#:
	s.frame = 0
	s.scaling = Shady.Transition( s.scaling, w.width / float( s.textureSize[0] ) )
	s.WaitFor( 'scaling' )
	s.width = Shady.Transition( s.width, s.textureSize[0] )
	# now we're looking at the whole strip

	"""
	Normally, the first element of `s.envelopeSize` (a.k.a `s.width`)
	is set so that only one frame is visible.  A change of `s.frame`
	is actually realized by changing `s.carrierTranslation[0]` (a.k.a.
	`s.cx`), so the carrier moves one frame-width at a time under the
	envelope, like a zoetrope.
	
	Well that's all very nice, and it only uses one OpenGL texture,
	but there's a limit to the dimensions that an OpenGL texture can
	have.  So if the frame width multiplied by the number of frames
	were to exceed the limit (which is hardware-/driver-defined, but
	I've seen it be as low as 8192 pixels) then you will not be able to
	do things this way.  So there is a different animation mechanism,
	called the "page" mechanism, if you need it...
	"""#:
	
	"""
	First let's put our friend back the way he was:
	"""#:
	s.Set(
		frame = lambda t: t * 16,
		scaling = Shady.Transition( s.scaling, 1.0 ),
		width = Shady.Transition( s.width, s.frameWidth ),
	)
	
	"""
	Now let's load the frames from disk into RAM:
	"""#:
	frames = Shady.LoadImage( filename )
	
	"""
	It's a list of PIL Image objects. Type `frames` and press
	return if you don't believe me.  Go ahead, I'll wait.
	"""#:
	
	"""
	Now we'll create a new empty Stimulus:
	"""#:
	s2 = w.Stimulus( x=300 )
	
	"""
	It currently has no texture, and its .backgroundColor is set
	to the default mid-grey.  Let's load each frame of the image
	into a new "page".  A new page corresponds to a new allocated
	texture in OpenGL, and its associated dimension settings:
	"""#:
	for i, frame in enumerate( frames ):
		s2.NewPage( frame, key=i )
	
	"""
	You may have noticed that we could see the textures being
	loaded one by one. In practice you might want to create the
	Stimulus with `visible=False` and only make it visible after
	all the textures are in place. Alternatively, you can
	automate the process in one call:
	"""#:
	
	s2.LoadPages( frames )
	# you can also construct the Stimulus with the option `multipage=True`
	
	"""
	Either way, now we can use the `.page` property, which also
	supports dynamics in the same way as `.frame`. This time, for
	fun, let's use a special callable object from the `Shady.Dynamics`
	sub-module:
	"""#:
	s2.page = Shady.Integral(16)
	
	"""
	Are they marching out of step with each other?  They may or
	may not be, depending on exactly when you executed that last
	line, because the newly-constructed `Integral()` would have
	started from zero on the next frame after that.  If it bothers
	you, I can think of a few different ways of putting these two
	guys into lock-step. The first is to retrieve the instantaneous
	numeric value of `s.frame` and add it to a new `Integral()`: 
	"""#:
	s2.page = Shady.Integral(16) + s.frame
	
	"""
	That at least demonstrates how you can do arithmetic operations
	with `Shady.Function` objects.  But this approach is overkill
	when you could instead assign to `s2.page` a simple function of
	time that always returns the current value of `s.frame`:
	"""#:
	s2.page = lambda t: s.frame
	
	"""
	Note that, while `.page` and `.frame` support dynamic value
	assignment, they are not fully-fledged "managed properties".
	Often, you will want to share properties between stimuli,
	and can take advantage of "property sharing" which allows
	this kind of linkage *without* requiring additional Python
	instructions to run on each frame. The `sharing` demo has
	more details.  However, for indirect "unmanaged" properties
	like `.page` and `.frame`, adding a lambda function that
	executes on each frame is about the best we can do to
	synchronize them.
	"""#:
	
	"""
	You can even combine the `.page` and `.frame` concepts to
	select between different animations for the same stimulus.
	
	To illustrate this, let's gather some resources created
	by craftpix.net and released under the OpenGameArt.org
	license. Let's use a `glob` pattern to see what we've got:
	"""#:
	import os, glob
	patterns = {
		os.path.basename( d ) : d + '/*.png'
		for d in glob.glob( Shady.PackagePath( 'examples/media/alien2/*' ) )
		if os.path.isdir( d )
	}
	for key, pattern in sorted( patterns.items() ):
		print( '% 6s :  %s' % ( key, pattern ) )
	
	
	"""
	Now let's create a single stimulus that can switch between
	these collections of frames:
	"""#:
	alien2 = w.Stimulus( x=-300, frame=Shady.Integral( 5 ) )
	for key, pattern in sorted( patterns.items() ):
		alien2.NewPage( pattern, key=key )
		print( 'loaded %r' % key )
		
	"""
	Now we can switch between them:
	"""#:
	alien2.page = 'fire'
	
	"""
	We can address the pages by those string names we gave
	them, or numerically. The latter means we could even
	cycle through the different animations automatically: 
	"""#:
	alien2.page = Shady.Integral( 0.3 )
	
	""#>
	Shady.AutoFinish( w ) # tidy up, in case we're not running this with `python -m Shady`