summaryrefslogtreecommitdiff
path: root/src/samples/slideshow.py
blob: 98704d004e28e8e70cc9a78805dd22977b486531 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
#!/usr/bin/env python
# -*- coding: utf-8 -*-

# libavg - Media Playback Engine.
# Copyright (C) 2003-2014 Ulrich von Zadow
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# Current versions can be found at www.libavg.de
#
# Original author of this file: Thomas Schott <scotty@c-base.org>
#

"""
Image slideshow example which shows all images found in the current working
directory (default) or the one provided via the "app_image_dir" setting.

Images are cross-faded and some random motion/scaling is applied while they're shown.
This example also shows how to use libavg's BitmapManager to asynchronously load images
in background.
"""

# TODO:
# add app settings for:
#  * show/transition intervals
#  * max. move distance
#  * sorted/shuffled show order (shuffled yet)

import sys
import os
from random import shuffle, randint
from libavg import avg, player, app


IMAGE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tga', '.tif', '.tiff']
SHOW_INTERVAL   = 6000 # [ms]
TRANS_INTERVAL  = 2000 # [ms]
CHANGE_INTERVAL = SHOW_INTERVAL + TRANS_INTERVAL
ANIM_INTERVAL   = CHANGE_INTERVAL + TRANS_INTERVAL
ANIM_MAX_MOVE   = 200 # [px]

BitmapMgr = avg.BitmapManager.get()


def scaleMax(srcSize, maxSize):
    """
    Returns scrSize aspect correct scaled to fit right into maxSize.
    """
    aspect = srcSize.x / srcSize.y
    if aspect < maxSize.x / maxSize.y:
        return avg.Point2D(maxSize.y * aspect, maxSize.y)
    return avg.Point2D(maxSize.x, maxSize.x / aspect)

def scaleMin(srcSize, minSize):
    """
    Returns scrSize aspect correct scaled to completely fill minSize.
    """
    aspect = srcSize.x / srcSize.y
    if aspect < minSize.x / minSize.y:
        return avg.Point2D(minSize.x, minSize.x / aspect)
    return avg.Point2D(minSize.y * aspect, minSize.y)


class Slide(avg.ImageNode):
    HIDE_DONE = avg.Publisher.genMessageID()

    def __init__(self, parent=None, **kwargs):
        super(Slide, self).__init__(opacity=0.0, **kwargs)
        self.registerInstance(self, parent)
        self.publish(Slide.HIDE_DONE)

    def show(self):
        s = self.getMediaSize()
        assert s.x and s.y
        # initial size and position (scaled to screen size and centered)
        self.size = scaleMax(s, self.parent.size)
        self.pos = (self.parent.size - self.size) * 0.5
        # random final size and position (center moved by (dx, dy) and scaled up accordingly)
        dx = float(randint(-ANIM_MAX_MOVE, ANIM_MAX_MOVE))
        dy = float(randint(-ANIM_MAX_MOVE, ANIM_MAX_MOVE))
        size = scaleMin(s, self.size + avg.Point2D(abs(dx), abs(dy)) * 2.0)
        pos = self.pos + avg.Point2D(dx, dy) + (self.size - size) * 0.5
        # start in-transition
        avg.fadeIn(self, TRANS_INTERVAL)
        # start move/scale animation
        avg.ParallelAnim([
                avg.LinearAnim(self, 'size', ANIM_INTERVAL, self.size, size),
                avg.LinearAnim(self, 'pos', ANIM_INTERVAL, self.pos, pos)]).start()

    def hide(self):
        # start out-transition, notify subscribers when finished
        avg.fadeOut(self, TRANS_INTERVAL,
                lambda: self.notifySubscribers(Slide.HIDE_DONE, [self]))


class Slideshow(app.MainDiv):
    def onArgvParserCreated(self, parser):
        parser.set_usage('%prog <images dir>')

    def onArgvParsed(self, options, args, parser):
        if len(args) != 1:
            parser.print_help()
            sys.exit(1)
        self._imagesDir = args[0]

    def onInit(self):
        if not os.path.isdir(self._imagesDir):
            avg.logger.error('Directory [%s] not found' % self._imagesDir)
            exit(1)
        avg.logger.info('Scanning directory [%s] ...' % self._imagesDir)

        imgExts = tuple(IMAGE_EXTENSIONS + [ext.upper() for ext in IMAGE_EXTENSIONS])
        self.__imgFiles = [os.path.join(self._imagesDir, imgFile) for imgFile in
                filter(lambda f: f.endswith(imgExts), os.listdir(self._imagesDir))]
        if not self.__imgFiles:
            avg.logger.error('No image files found, '
                    'scanned file extensions:\n%s' % (', '.join(imgExts)))
            exit(1)
        l = len(self.__imgFiles)
        avg.logger.info('%d image file%s found' % (l, 's' if l > 1 else ''))
        shuffle(self.__imgFiles)

        self.__slidesDiv = avg.DivNode(size=self.size, parent=self)
        # ping-pong two slides for cross-fade transition
        self.__newSlide = Slide(parent=self.__slidesDiv)
        self.__oldSlide = Slide(href=self.__imgFiles[0], parent=self.__slidesDiv)
        # HIDE_DONE notifications will trigger asynchronous pre-loading of the next image
        self.__newSlide.subscribe(Slide.HIDE_DONE, self.__asyncPreload)
        self.__oldSlide.subscribe(Slide.HIDE_DONE, self.__asyncPreload)
        self.__currentIdx = 0
        self.__changeSlide()
        player.setInterval(CHANGE_INTERVAL, self.__changeSlide)

    def __asyncPreload(self, slide):
        self.__currentIdx = (self.__currentIdx + 1) % len(self.__imgFiles)
        BitmapMgr.loadBitmap(self.__imgFiles[self.__currentIdx], slide.setBitmap)

    def __changeSlide(self):
        self.__oldSlide, self.__newSlide = self.__newSlide, self.__oldSlide
        self.__slidesDiv.reorderChild(self.__newSlide, 1) # move to top
        self.__newSlide.show()
        self.__oldSlide.hide()


if __name__ == '__main__':
    app.App().run(Slideshow())