1

I am trying to use Shady to present a sequence of image frames. In the past I've done this by assigning to the stimulus.page attribute an animation callback. In the callback, which is called by Shady once for each monitor frame, I first check a global variable that keeps track of which stimulus frame should be displayed. If it is positive, I set the visibility of the stimulus to True and return the frame number; otherwise I set it to False and return 0. Works like a charm.

Now I need to extend this, because I need to select one among a number of sequences to display. Since loading a sequence can take some time, I load them all at the beginning of my program and associate each with a Stimulus object. Now, here is my question. Do I have to create a different animation callback function for each of the sequences (whose number is variable, and can be high). Or is there a more elegant way to have a single animation callback (or something similar) being called, and in there I determine which frame of which sequence to show (again, based on the current value of global variables)?

jez
  • 14,867
  • 5
  • 37
  • 64
cq70
  • 21
  • 6

1 Answers1

0

First I want to make sure of the distinction in terminology between "animation callbacks" and "dynamic property values". The Shady docs, and this answer, do not refer to the thing you're using as an "animation callback". But the best answer to your problem, I think, might be to use actual animation callbacks.

  • Dynamics property values, or "dynamics": certain properties of World and Stimulus instances can have callables assigned to them. If so, they are called on every frame. Your stimulus.page is an example of this (even though page it is not a full-fledged "managed property", i.e. one with the capability of sharing memory with another instance's corresponding property, it does support "dynamic assignment" in this way). Dynamic properties are good if you want to change only a few aspects of a few stimuli at a time, and/or you want the changes in one property to be completely independent of changes in another. A dynamically-assigned function must take exactly one argument—time t in seconds—but (per your comment) that does allow you to use an already-bound method of s (or indeed any other instance) that has the prototype blah(self, t), since the self will be baked-in when you access it. The function must also return a value—the static value that should be assigned to the property on this frame.

  • Animation callbacks: each Stimulus instance s (and additionally the World instance) has a single "animation callback" which occupies the attribute slot s.Animate, and which can be changed by the s.SetAnimationCallback method or its equivalent function decorator @s.AnimationCallback. On every frame, every World or Stimulus instance checks to see if it has an .Animate property, and if so, calls it. If you want the changes to multiple properties (say, s.visible and s.page) to be coordinated with each other, then the animation callback attached to s is probably the best place to do it. Unlike dynamics, the animation callback can be an (unbound) method, which receives a reference to s as self, so the attributes of self can be used as a "global" (but stimulus-instance-specific) scratchpad. (Actually, with animation callbacks you have the choice whether to define them with a method-like prototype blah(self, t) or a single-argument prototype blah(t), similar to a dynamic.) Where dynamics are functional, animation callbacks are procedural: any return argument of an animation callback is ignored.

In either case, it is true (as you feared) that all stimuli to which you attach a callback will call it, and all stimuli will call their dynamics, on every frame, without checking to see if the same callables are also being used by other instances. If you want to make sure code is not called unnecessarily, your options include:

  1. If only one Stimulus is supposed to be visible at a time: have each Stimulus follow its own logic, in its own animation callback and/or in its dynamic properties (whichever makes most sense to you). But when it's supposed to be off-screen, don't just set s.visible = False: instead, actually have the Stimulus instance s.Leave() the stage, and s.Enter() again when necessary. When you're on-stage, even if you're invisible, your animation callback and your dynamics get called on every frame. When you're off-stage, they are not called. This is demonstrated towards the end of python -m Shady demo sharing

  2. Alternatively, you could just choose to perform most or all of your animation logic in one place. The best place for that is probably the World animation callback, which is definitely only called once per frame because there is only one World. References to individual stimuli can always be retrieved as global variables, or by name from the self.stimuli container:

    import Shady
    cmdline = Shady.WorldConstructorCommandLine()
    cmdline.Help().Finalize()
    
    w = Shady.World( **cmdline.opts )   
    w.Stimulus( name='bill', x=-100, size=100, color=[1,0,0] )
    w.Stimulus( name='ben',  x=+100, size=100, color=[0,0,1] )
    
    @w.AnimationCallback
    def Animate( self, t ):
        # top-down control of multiple stimuli:
    
        if self.framesCompleted % 2:
            # self.stimuli is a dict, with all the expected dict capabilities...
            self.stimuli['bill'].visible = True 
            self.stimuli['ben'].visible = False
        else:
            # ...but it supports lazier syntax too:
            self.stimuli.bill.visible = False 
            self.stimuli.ben.visible = True
    
    Shady.AutoFinish( w )
    
jez
  • 14,867
  • 5
  • 37
  • 64
  • Thanks. One clarification. You say that animation calls are methods and so self is passed to them, but what I assign to stimulus.page is also a method, and has (self, t) as its signature, and works fine (I'm using self within it, and it works). In terms of the answer, I think I'll go with the first approach, since it seems well suited to my requirements (I have a set of trials, and in each trial only a single stimulus will be used, so it makes sense to manage who's on stage accordingly). If only one stimulus is on stage at any one time, I can use the same callable for all of them. – cq70 Feb 11 '20 at 19:49
  • That was actually the suggestion you had made here: https://stackoverflow.com/questions/56382543/controlling-dynamic-properties-in-shady-according-to-video-frames-not-time – cq70 Feb 11 '20 at 19:55
  • 1
    Dynamics can only be one-argument callables—I'm guessing you must be assigning an *already-bound* method, `s.page = some_instance.method` because that is effectively a one-argument callable (the `self` argument is already baked into the instance *of* the instance method). That is of course a perfectly valid, and potentially very useful, way to do things. By contrast, animation callbacks can be defined as one- **or** two-argument callables, as explained in the edit. – jez Feb 11 '20 at 20:38
  • That's exactly what I'm doing, I hadn't appreciated the subtlety. – cq70 Feb 11 '20 at 22:40