examples/dots1.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 dots1

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

#: A simple random-dot stimulus
"""
This demo shows the simplest way of drawing dot stimuli,
which is to set the `.drawMode` property to
`Shady.DRAWMODE.POINTS` and then manipulate the `.points`
property.   The visual quality of the results will depend
on your graphics drivers.  To achieve greater control over
dot appearance, see the other dots demos (`dots2`, `dots3`
and `dots4`).
"""#.

if __name__ == '__main__':
	"""
	First let's wrangle the command-line options:
	"""#:
	
	import Shady
	cmdline = Shady.WorldConstructorCommandLine( canvas=False )
	ndots     = cmdline.Option( 'ndots',    300, type=int,  container=None, doc="Number of dots (may not exceed %d)." % Shady.Rendering.MAX_POINTS )
	bounce    = cmdline.Option( 'bounce', False, type=bool, container=None, doc="If True, implement some rudimentary physics to make the dots bounce off each other." )
	gauge     = cmdline.Option( 'gauge',   True, type=bool, container=None, doc="Whether or not to show a `FrameIntervalGauge`." )
	thickness = cmdline.Option( 'thickness', 20, type=( int, float ), container=None, doc="Value for the `.penThickness` property, dictating the size of the dots." )
	smooth    = cmdline.Option( 'smooth',  True, type=bool, container=None, doc="""If True, flag the dots as "smooth". This may make them round instead of square, or it may have no effect - unfortunately this is driver-dependent. If you want to guarantee round dots you'll have to use small polygons instead (see dots3 and dots4 demos) or separate `Stimulus` instances (dots2).""" )
	cmdline.Help().Finalize()
	Shady.Require( 'numpy' ) # die with an informative error if this is missing

	"""
	Create a World and, if requested, a frame interval gauge:
	"""#:
	w = Shady.World( **cmdline.opts )
	if gauge: Shady.FrameIntervalGauge( w )
	
	"""
	Now a single Stimulus that will host our random dots.
	We'll use the `POINTS` drawing mode.
	"""#:
	field = w.Stimulus( size=w.size, color=1, drawMode=Shady.DRAWMODE.POINTS, penThickness=thickness, smoothing=smooth )
	
	"""
	To draw the points, we'll have to set the `.points`
	property.
	"""#:
	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 ] )
	field.points = ( Shady.Integral( velocity ) + location ) % field.size

	
	"""
	Now, for a bit of fun, we'll define a couple of functions
	that allow the dots to bounce off each other.
	"""#:
	physics = dict( exponent=10, closest=10, coefficient=800 )
	def RepulsionForces( t=None ):
		positions = field.pointsComplex
		vectors = positions[ None, : ] - positions[ :, None ]
		magnitudes = numpy.abs( vectors )
		degenerate = magnitudes < 1e-4
		nondegenerate = ~degenerate
		vectors[ nondegenerate ] /= magnitudes[ nondegenerate ] # vectors are now unit vectors
		magnitudes[ degenerate ] = numpy.inf
		magnitudes = numpy.clip( magnitudes - field.penThickness / 2.0, physics[ 'closest' ], numpy.inf )
		magnitudes /= physics[ 'closest' ]
		magnitudes **= -physics[ 'exponent' ] # now we have inverse square (or whatever power) distances, with 0s on diagonal
		forces = magnitudes * vectors * physics[ 'coefficient' ]
		forces = forces.sum( axis=0 )
		forces = Shady.ComplexToReal2D( forces )  # n-by-2 real-valued output
		return forces
	def Bounce( **kwargs ):
		physics.update( kwargs )
		field.points = ( Shady.Integral( Shady.Integral( RepulsionForces ) + velocity ) + field.points ) % field.size

	if bounce: w.Defer( Bounce )
	# .Defer() will ensure that the function gets called at the end of
	# the next frame (this ensures that field.pointsComplex, which
	# RepulsionForces() relies on, has already had a value assigned to it

	"""
	If you requested this with the `--bounce` command-line option
	then the points should already be bouncing off each other.
	If not, you can manually trigger it if you want, by calling
	`Bounce()`.  Either way, our dots demo is done.
		
	Note that `drawMode=POINTS` is the simplest way of creating
	dot patterns, but not necessarily the most powerful. Setting
	`smoothing=True` should in principle make them round, but
	whether it successfully does so is dependent on your graphics
	driver: they may stubbornly remain square.  The best way of
	guaranteeing the shape of your dots is to draw them as
	tiny polygons (with a high number of sides, if you want them
	to look round).  This can be explored in the `dots3` and
	`dots4` demos.  Another option is to make each "dot" an
	independent Stimulus - but as the `dots2` demo shows, you
	have to worry much more about timing performance in that case.
	"""#:
	
	"""
	By the way: if you're not pleased with the frame rate of your
	colliding dots, and you're reading this console in the foreground,
	try switching to the World as your main window. This will put
	Shady in charge of synchronizing frame buffer swaps and likely
	lead to significant improvements in performance.
	"""#>
		
	Shady.AutoFinish( w )