# examples/dots3.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 dots3

# $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-2020 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$

#: Need to guarantee rounded dots?  Animate multiple polygons!
"""
This demo shows how it is very easy, and still fairly
computationally inexpensive, to animate a dot stimulus
in which each "dot" is a polygon.  It uses the POLYGON
draw mode, and our setup will be aided by the
ComplexPolygonBase utility function.

Using polygons with a high number of sides (say, 20)
is the only way to guarantee round-shaped dots. The
simpler approach of the dots1 demo, using the POINTS
draw mode and smoothing=True, is not guaranteed to
produce round dots---it depends on your graphics
hardware and drivers.
"""#.
if __name__ == '__main__':
"""
First let's parse the command-line arguments for
this demo:
"""#:
# All the usual World-construction ones we use in most demos:
cmdline = Shady.WorldConstructorCommandLine( canvas=False )
# And some that are specific to this demo:
ndots  = cmdline.Option( 'ndots', 300,   type=int, container=None, doc="Number of shapes.  Ensure (nsides+1)*ndots <= %d" % Shady.Rendering.MAX_POINTS )
nsides = cmdline.Option( 'nsides',  5,   type=int, min=3, container=None, doc="Number of sides.  Ensure (nsides+1)*ndots <= %d" % Shady.Rendering.MAX_POINTS )
radius = cmdline.Option( 'radius', 25.0, type=( int, float ), container=None, doc="Half-width of each shape, in pixels." )
spin   = cmdline.Option( 'spin',    0.2, type=( int, float ), container=None, doc="Spin the shapes at this speed (revolutions per second)." )
gauge  = cmdline.Option( 'gauge', False, type=bool,  container=None, doc="Whether or not to show a FrameIntervalGauge." )
cmdline.Help().Finalize()
Shady.Require( 'numpy', 'Image' ) # die with an informative error if either is missing

if ( nsides + 1 ) * ndots > Shady.Rendering.MAX_POINTS: raise ValueError( '(nsides + 1) * ndots cannot exceed %d' % Shady.Rendering.MAX_POINTS )

"""
Create a World:
"""#:
w = Shady.World( **cmdline.opts )
if gauge: Shady.FrameIntervalGauge( w )

"""
Create a Stimulus to act as a container for the shapes. For fun, we're
going to give this one a background image:
"""#:
field = w.Stimulus( Shady.PackagePath( 'examples/media/waves.jpg' ), size=w.size, visible=1 )
# And now, a slightly awkward manipulation of carrier parameters, to make the image fill
# the screen without stretching the coordinate system in which the shapes are defined (that's
# what would happen if we were to change .envelopeScaling)
field.carrierTranslation = ( field.envelopeSize - field.textureSize ) // 2
field.carrierScaling = max( field.envelopeSize.astype( float ) / field.textureSize )

"""
Set up the dynamic that draws the shapes:
"""#:
shape = Shady.ComplexPolygonBase( nsides )
import numpy
location = numpy.random.uniform( low=[   0,   0 ], high=field.size,    size=[ ndots, 2 ] )
velocity = numpy.random.uniform( low=[ -30, -30 ], high=[ +30, -150 ], size=[ ndots, 2 ] )
func = Shady.Integral( lambda t: velocity ) # we could say just Shady.Integral( velocity )
# but then we wouldn't be able to change the
# velocity on-the-fly
func += location
func %= field.size # wrap around the field, pacman-style
func.Transform( Shady.Real2DToComplex )      # ndots-by-1 complex
func += lambda t: radius * shape * 1j ** ( 4.0 * spin * t )  # 1-by-(sides+1) complex
# numpy broadcasting in the + operator does the rest.
# It is possible to assign a sequence of complex numbers to the .points property,
# so we will leave the func output in complex form.

"""
Apply it:
"""#:
field.Set( points=func, drawMode=Shady.DRAWMODE.POLYGON, visible=1 )

"""
Note that the lambda function we defined in the last line of our
setup is continually accessing, on every frame, three variables in
the current namespace, called radius, shape and spin. This
means that any manipulations you make to these variables, at the
current command-line, will immediately affect the stimulus.

Try playing with them.  Examples of some things to try::

Shady.AutoFinish( w ) # Finish up the demo (in case we're not running with python -m Shady)