PreviewStrip

Snippet for displaying images from a catalog in a cover-like way. You can rotate them left or right and you’ve got easy access to the middle one via current() method.

You can also specify distribution of the images on the screen via {x,y,z}_dist functions.

Update: (thanks Nemesis)

To see how it works specify path to catalog with images (and only images unless you want to add some code for filtering out files which are not images). Path is relative to current working directory.


from direct.showbase.ShowBase import ShowBase
from panda3d.core import CardMaker
from direct.task import Task
from direct.interval.LerpInterval import LerpFunc
from direct.interval.IntervalGlobal import *
from os import sep, listdir




class PreviewStrip(object):
    def __init__(self, catalog, height = -0.5):
        self.height = height
        self.catalog = catalog

        self.preview_size = [-0.1,  0.1, -0.1, 0.1]
        
        self.generator = CardMaker("PreviewMaker") 
        self.generator.setFrame(*self.preview_size) 
        
        self.textures = []
        self.loadPreviewImages()
        #number of items visible on the screen
        self.visible = min(len(self.textures),5)
        #duration of an animation
        self.duration = 0.3
        
        self.positions = []
        self.preparePositions()
        
        self.head = 0
        self.tail = self.visible - 1
        
    def loadPreviewImages(self):
        files = listdir(self.catalog)
        files.sort()
        
        for filename in files:
            self.textures.append(loader.loadTexture(sep.join([self.catalog,filename])))

    # distribution functions they specify the shape in which
    # initially visible images are arranged
    def x_dist(self, i):
        return 0.5*(i - self.visible/2)
    
    def y_dist(self, i):
        return abs (i - self.visible /2 )

    def z_dist(self, i):
        return self.height
    
    # initials scaling of the visible images
    def scale(self, i):
        try:
            return 2.5 -  2*abs (float(i)/(self.visible-1) -  0.5)
        except:
            return 1.0
    
    def preparePositions(self):
        for i in range(0,self.visible):
            model = aspect2d.attachNewNode(self.generator.generate())
            model.setPos(self.x_dist(i), self.y_dist(i), self.z_dist(i))
            model.setScale(self.scale(i))
            # so that images are correctly displayed on top 
            # of each other
            model.setDepthTest(True)
            model.setDepthWrite(True)
            self.positions.append(model)
        
        # setting images    
        for i in range(len(self.positions)):
            self.positions[i].setTexture(self.textures[i])
        
    def _scaleItem(self, i, dir):
        # if dir is negative item is scaled right
        # if dir is positive item is scaled left
        next = (i+dir)%len(self.positions)
        return LerpScaleInterval (
                    self.positions[i], 
                    duration = self.duration,
                    startScale = self.positions[i].getScale(),
                    scale = self.positions[next].getScale()
        )
       
    def _positionItem(self, i, dir):
        # if dir is negative item is moved right
        # if dir is positive item is moved left 
        next = (i+dir) % len(self.positions)
        return LerpPosInterval (
                    self.positions[i], 
                    duration = self.duration,
                    startPos = self.positions[i].getPos()z,
                    pos = self.positions[next].getPos()
        )
        
    def _adjustLeft(self):
        # animation ends, we can re-eanble input
        last = self.positions.pop()
        self.head = (self.head - 1) % len(self.textures)
        self.tail = (self.tail - 1) % len(self.textures)
        last.setTexture(self.textures[self.head])
        self.positions.insert(0,last)
        base.acceptOnce("arrow_left",ps.rotateLeft)
        base.acceptOnce("arrow_right",ps.rotateRight)
        
    def _adjustRight(self):
        first = self.positions.pop(0)
        self.head = (self.head + 1) % len(self.textures)
        self.tail = (self.tail + 1) % len(self.textures)
        first.setTexture(self.textures[self.tail])
        self.positions.append(first)
        # animation ends, we can re-eanble input
        base.acceptOnce("arrow_left",ps.rotateLeft)
        base.acceptOnce("arrow_right",ps.rotateRight)
        
    def rotateRight(self):
        # prevent to starting another turnaround
        base.ignore("arrow_left")
        base.ignore("arrow_right")
        parallel = Parallel()
        
        for i in range(len(self.positions)-1):
            parallel.append( self._positionItem(i, 1))
            parallel.append( self._scaleItem(i, 1))

        # last item is moved symetrically so it has its scale preserved
        parallel.append(self._positionItem(-1,1))
        
        self.seq = Sequence(parallel, Func(self._adjustLeft))
        self.seq.start()

    def rotateLeft(self):
        # prevent to starting another turnaround
        base.ignore("arrow_left")
        base.ignore("arrow_right")
        parallel = Parallel()
        
        for i in range(len(self.positions)):
            parallel.append( self._positionItem(i, -1))
            parallel.append( self._scaleItem(i,  -1))
 
  
        parallel.append(self._positionItem(0,-1))
        self.seq = Sequence(parallel, Func(self._adjustRight))
        self.seq.start()

    def current(self):
        # list is being kept the way that the middle argument in the list is always current
        return self.positions[self.visible/2]
            
    def hide(self):
        for item in self.positions:
            item.hide()
            
    def show(self):
        for item in self.positions:
            item.show()


if __name__ == "__main__":
    base = ShowBase()
    # change directory to your own
    ps = PreviewStrip(".")
    base.acceptOnce("arrow_left",ps.rotateLeft)
    base.acceptOnce("arrow_right",ps.rotateRight)
    base.run()

Wrap a try…except around loader.loadTexture() for the files it can’t load. Even better might be checking file extension. By now it tries to load ALL files in the current directory. Using it as it is now in the same directory where you store the py file, it will fail because it tries to load itself (the python file) as a texture.

Also the path set up there seems to be relative to the current working directory, not relative to the python file, what i’d expect. This is still convenient to start the script from anywhere and make it load up the current dir.

Still it’s a nice snippet. Thanks for sharing :slight_smile: