adding pink trombone, rfid arduino code, camera views rotater, !go and !turn commands master
authorfrank <420@shampoo.ooo>
Wed, 24 Mar 2021 01:25:21 +0000 (21:25 -0400)
committerfrank <420@shampoo.ooo>
Wed, 24 Mar 2021 01:25:21 +0000 (21:25 -0400)
51 files changed:
ReadUidMultiReader_modified/ReadUidMultiReader_modified.ino [moved from ReadUidMultiReader_modified.ino with 100% similarity]
TV_border.png [new file with mode: 0644]
camera_views/.gitmodules [new file with mode: 0644]
camera_views/camera_views.py [new file with mode: 0644]
camera_views/config [new file with mode: 0644]
camera_views/lib/__init__.py [new file with mode: 0644]
camera_views/lib/pgfw/.gitignore [new file with mode: 0644]
camera_views/lib/pgfw/MANIFEST.in [new file with mode: 0644]
camera_views/lib/pgfw/README [new file with mode: 0644]
camera_views/lib/pgfw/__init__.py [new file with mode: 0644]
camera_views/lib/pgfw/pgfw/Animation.py [new file with mode: 0644]
camera_views/lib/pgfw/pgfw/Audio.py [new file with mode: 0644]
camera_views/lib/pgfw/pgfw/Configuration.py [new file with mode: 0644]
camera_views/lib/pgfw/pgfw/Delegate.py [new file with mode: 0644]
camera_views/lib/pgfw/pgfw/Display.py [new file with mode: 0644]
camera_views/lib/pgfw/pgfw/Game.py [new file with mode: 0644]
camera_views/lib/pgfw/pgfw/GameChild.py [new file with mode: 0644]
camera_views/lib/pgfw/pgfw/Gradient.py [new file with mode: 0644]
camera_views/lib/pgfw/pgfw/Input.py [new file with mode: 0644]
camera_views/lib/pgfw/pgfw/Interpolator.py [new file with mode: 0644]
camera_views/lib/pgfw/pgfw/Mainloop.py [new file with mode: 0644]
camera_views/lib/pgfw/pgfw/Note.py [new file with mode: 0644]
camera_views/lib/pgfw/pgfw/Profile.py [new file with mode: 0644]
camera_views/lib/pgfw/pgfw/ScreenGrabber.py [new file with mode: 0644]
camera_views/lib/pgfw/pgfw/Setup.py [new file with mode: 0644]
camera_views/lib/pgfw/pgfw/SetupOSX.py [new file with mode: 0644]
camera_views/lib/pgfw/pgfw/SetupWin.py [new file with mode: 0644]
camera_views/lib/pgfw/pgfw/Sprite.py [new file with mode: 0644]
camera_views/lib/pgfw/pgfw/TimeFilter.py [new file with mode: 0644]
camera_views/lib/pgfw/pgfw/Vector.py [new file with mode: 0644]
camera_views/lib/pgfw/pgfw/VideoRecorder.py [new file with mode: 0644]
camera_views/lib/pgfw/pgfw/__init__.py [new file with mode: 0644]
camera_views/lib/pgfw/pgfw/confirm_quit_message.py [new file with mode: 0644]
camera_views/lib/pgfw/pgfw/extension.py [new file with mode: 0644]
camera_views/lib/pgfw/pgfw/gfx_extension.py [new file with mode: 0644]
camera_views/lib/pgfw/sample.py [new file with mode: 0644]
camera_views/lib/pgfw/setup.py [new file with mode: 0644]
camera_views_mask.png [new file with mode: 0644]
chatbot/bot.py
mou_visual_novel/game/custom_atl.rpy [new file with mode: 0644]
mouscreenshot1.png [new file with mode: 0644]
read_rfid_stations.py
rfid_status.json
send_command.py
ticker_fade_mask.png [new file with mode: 0644]
tromboner/Pink Trombone.html [new file with mode: 0644]
tromboner/auto-clicker.py [new file with mode: 0644]
tromboner/coods.txt [new file with mode: 0644]
tromboner/string-to-trombone.py [new file with mode: 0644]
vn_mask.png [new file with mode: 0644]
winch_status.json [new file with mode: 0644]

diff --git a/TV_border.png b/TV_border.png
new file mode 100644 (file)
index 0000000..ad22839
Binary files /dev/null and b/TV_border.png differ
diff --git a/camera_views/.gitmodules b/camera_views/.gitmodules
new file mode 100644 (file)
index 0000000..b9cf2a1
--- /dev/null
@@ -0,0 +1,3 @@
+[submodule "lib/pgfw"]
+       path = lib/pgfw
+       url = http://git.shampoo.ooo/pgfw
diff --git a/camera_views/camera_views.py b/camera_views/camera_views.py
new file mode 100644 (file)
index 0000000..795e176
--- /dev/null
@@ -0,0 +1,40 @@
+from time import sleep
+from random import randint
+import pygame, pygame.camera
+
+from lib.pgfw.pgfw.Game import Game
+from lib.pgfw.pgfw.Animation import Animation
+
+# inheriting from Game allows you to customize your project
+class CameraViews(Game):
+
+    CAMERA_IDS = 4, 1, 2
+
+    def __init__(self):
+        super().__init__()
+        pygame.camera.init()
+        self.cams = []
+        for cam_id in self.CAMERA_IDS:
+            self.cams.append(pygame.camera.Camera(cam_id))
+            self.cams[-1].start()
+        self.active_cam_index = 0
+        self.register(self.rotate, interval=20000)
+        self.play(self.rotate)
+        self.camera_frame = pygame.surface.Surface(self.get_display_surface().get_size())
+        
+    def rotate(self):
+        self.active_cam_index = (self.active_cam_index + 1) % len(self.cams)
+
+    # update runs every frame, you can think of it as the mainloop
+    def update(self):
+        Animation.update(self)
+        cam = self.cams[self.active_cam_index]
+        original = cam.get_image()
+        self.camera_frame = pygame.transform.smoothscale(original, self.get_display_surface().get_size())
+        self.get_display_surface().blit(self.camera_frame, (0, 0))
+        pygame.display.update()
+
+
+if __name__ == '__main__':
+    # the play method begins the project's animation
+    CameraViews().run()
diff --git a/camera_views/config b/camera_views/config
new file mode 100644 (file)
index 0000000..a3bb17b
--- /dev/null
@@ -0,0 +1,2 @@
+[display]
+caption = camera views
\ No newline at end of file
diff --git a/camera_views/lib/__init__.py b/camera_views/lib/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/camera_views/lib/pgfw/.gitignore b/camera_views/lib/pgfw/.gitignore
new file mode 100644 (file)
index 0000000..5b3f7b7
--- /dev/null
@@ -0,0 +1,4 @@
+*.pyc
+build/
+MANIFEST
+*~
diff --git a/camera_views/lib/pgfw/MANIFEST.in b/camera_views/lib/pgfw/MANIFEST.in
new file mode 100644 (file)
index 0000000..57834c9
--- /dev/null
@@ -0,0 +1 @@
+include sample.py
diff --git a/camera_views/lib/pgfw/README b/camera_views/lib/pgfw/README
new file mode 100644 (file)
index 0000000..91c0774
--- /dev/null
@@ -0,0 +1,55 @@
+----------------
+Pygame Framework
+----------------
+
+Classes to facilitate creation of Pygame projects
+
+
+Example
+-------
+
+Save and run to create a project that redraws a square at a random location
+every second.  This script is also written in the `sample.py` file.
+
+from time import sleep
+from random import randint
+
+from pgfw.Game import Game
+
+class SampleGame(Game):
+
+    square_width = 30
+
+    # instructions in the update method automatically run once every frame
+    def update(self):
+        sleep(1)
+        screen = self.get_screen()
+        bounds = screen.get_size()
+        screen.fill((0, 0, 0))
+        screen.fill((255, 255, 255),
+                    (randint(0, bounds[0]), randint(0, bounds[1]),
+                     self.square_width, self.square_width))
+
+
+if __name__ == '__main__':
+    SampleGame().run()
+
+
+License
+-------
+
+This software is dedicated to the public domain.  See
+http://creativecommons.org/publicdomain/zero/1.0/ for details.
+
+
+Todo
+----
+
+- Remove unecessary python libraries from windows build
+- Debug levels
+
+
+Credit
+------
+
+Frank DeMarco <if.self.end@a-o.in>
diff --git a/camera_views/lib/pgfw/__init__.py b/camera_views/lib/pgfw/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/camera_views/lib/pgfw/pgfw/Animation.py b/camera_views/lib/pgfw/pgfw/Animation.py
new file mode 100644 (file)
index 0000000..f0a1c78
--- /dev/null
@@ -0,0 +1,154 @@
+import collections
+
+from .GameChild import GameChild
+
+class Animation(GameChild):
+
+    def __init__(self, parent, method=None, interval=None, unfiltered=False):
+        GameChild.__init__(self, parent)
+        self.unfiltered = unfiltered
+        self.default_method = method or self.build_frame
+        self.accounts = collections.OrderedDict()
+        self.register(self.default_method, interval=interval)
+        self.last_update = 0
+
+    def build_frame(self):
+        pass
+
+    def register(self, *args, **kwargs):
+        interval = None
+        if "interval" in kwargs:
+            interval = kwargs["interval"]
+        for method in args:
+            if method not in self.accounts:
+                self.accounts[method] = Account(interval, self)
+            else:
+                self.accounts[method].set_interval(interval)
+
+    def play(self, method=None, interval=None, delay=0, play_once=False,
+             **kwargs):
+        account = self.accounts[self.get_default(method)]
+        account.set_delay(delay)
+        account.set_args(kwargs)
+        account.set_play_once(play_once)
+        if interval:
+            account.set_interval(interval)
+        account.play()
+
+    def get_default(self, method=None):
+        if not method:
+            method = self.default_method
+        elif isinstance(method, str):
+            return getattr(self, method)
+        return method
+
+    def halt(self, method=None):
+        if not method:
+            for account in self.accounts.values():
+                account.halt()
+        else:
+            if method in self.accounts:
+                self.accounts[method].halt()
+
+    def is_playing(self, method=None, check_all=False, include_delay=False):
+        if check_all:
+            return any(self.is_account_playing(account, include_delay) for \
+                       method, account in self.accounts.items())
+        return self.is_account_playing(self.accounts[self.get_default(method)],
+                                       include_delay)
+
+    def is_account_playing(self, account, include_delay):
+        return account.playing and (include_delay or not account.delay)
+
+    def reset_timer(self, method=None):
+        if not method:
+            for account in self.accounts.values():
+                account.reset_timer()
+        else:
+            self.accounts[method].reset_timer()
+
+    def update(self):
+        for method, account in self.accounts.items():
+            if account.update():
+                method(**account.args)
+
+
+class Account:
+
+    def __init__(self, interval, animation):
+        self.animation = animation
+        self.time_filter = animation.get_game().time_filter
+        self.set_interval(interval)
+        self.set_delay(0)
+        self.set_play_once(False)
+        self.interval_index = 0
+        self.last_frame = 0
+        self.halt()
+
+    def set_interval(self, interval):
+        if isinstance(interval, int) or isinstance(interval, str):
+            interval = [interval]
+        self.interval = interval
+
+    def set_delay(self, delay):
+        self.delay = delay
+
+    def set_play_once(self, play_once):
+        self.play_once = play_once
+
+    def set_args(self, args):
+        self.args = args
+
+    def play(self):
+        self.playing = True
+
+    def halt(self):
+        self.last_update = None
+        self.playing = False
+
+    def update(self):
+        if self.playing:
+            if self.animation.unfiltered:
+                ticks = self.time_filter.get_unfiltered_ticks()
+            else:
+                ticks = self.time_filter.get_ticks()
+            self.update_delay(ticks)
+            if not self.delay:
+                interval = self.interval
+                if interval:
+                    if ticks - self.last_frame < self.get_current_interval():
+                        return False
+                    self.last_frame = ticks
+                    self.increment_interval_index()
+                if self.play_once:
+                    self.halt()
+                return True
+
+    def get_current_interval(self):
+        return self.interval[self.interval_index]
+
+    def increment_interval_index(self, increment=1):
+        index = self.interval_index + increment
+        while index >= len(self.interval):
+            index -= len(self.interval)
+        self.interval_index = index
+
+    def reset_interval(self):
+        self.interval_index = 0
+
+    def reset_timer(self):
+        if self.animation.unfiltered:
+            ticks = self.time_filter.get_unfiltered_ticks()
+        else:
+            ticks = self.time_filter.get_ticks()
+        self.last_frame = ticks
+
+    def update_delay(self, ticks):
+        delay = self.delay
+        if delay > 0:
+            last_update = self.last_update or ticks
+            delay -= ticks - last_update
+            if delay < 0:
+                delay = 0
+        self.last_update = ticks
+        self.delay = delay
diff --git a/camera_views/lib/pgfw/pgfw/Audio.py b/camera_views/lib/pgfw/pgfw/Audio.py
new file mode 100644 (file)
index 0000000..92ffb78
--- /dev/null
@@ -0,0 +1,878 @@
+# -*- coding: utf-8 -*-
+
+import os, re, shutil, pygame, sys, collections
+
+from .GameChild import *
+from .Sprite import *
+from .Input import *
+from .Animation import *
+from .extension import *
+
+class Audio(Animation):
+
+    UP, DOWN = .1, -.1
+    CONFIG_SEPARATOR = ","
+
+    def __init__(self, game):
+        Animation.__init__(self, game)
+        # self.original_volumes = {}
+        self.current_bgm = None
+        self.volume = 1.0
+        self.pre_muted_volume = None
+        if self.check_command_line("-mute"):
+            self.get_configuration().set("audio", "volume", 0)
+        self.register(self.play_sfx)
+        if self.get_configuration("audio", "panel-enabled"):
+            self.audio_panel = AudioPanel(self)
+        else:
+            self.audio_panel = None
+        self.subscribe(self.respond)
+        self.sfx = {}
+        self.load_sfx()
+        self.bgm = {}
+        self.load_bgm()
+        self.set_volume(self.get_configuration("audio", "volume"))
+
+    def set_volume(self, volume=None, increment=None, mute=False, unmute=False):
+        if mute:
+            self.pre_muted_volume = self.volume
+            self.volume = 0
+        elif unmute and self.pre_muted_volume is not None:
+            self.volume = self.pre_muted_volume
+            self.pre_muted_volume = None
+        elif increment:
+            self.volume = clamp(self.volume + increment, 0, 1.0)
+        else:
+            self.volume = volume
+        self.get_configuration().set("audio", "volume", self.volume)
+        if pygame.mixer.music.get_busy():
+            pygame.mixer.music.set_volume(self.current_bgm.volume * self.volume)
+        for ii in range(pygame.mixer.get_num_channels()):
+            channel = pygame.mixer.Channel(ii)
+            if channel.get_busy():
+                channel.set_volume(channel.get_sound().get_volume() * self.volume)
+
+    def respond(self, event):
+        compare = self.get_game().delegate.compare
+        if compare(event, "volume-mute"):
+            if self.volume > 0:
+                self.set_volume(mute=True)
+            else:
+                self.set_volume(unmute=True)
+        elif compare(event, "volume-up"):
+            self.set_volume(increment=self.UP)
+        elif compare(event, "volume-down"):
+            self.set_volume(increment=self.DOWN)
+
+    #
+    # Loading SFX procedure
+    #
+    # - load config file name/path definitions at init
+    # - check project specific sfx paths at init, load any that don't conflict
+    # - check default sfx paths at init, load any that don't conflict
+    # - repository paths are not loaded at init but can replace loaded paths
+    #   and get written to config file
+    #
+    def load_sfx(self, sfx_location=None):
+        for name, sfx_definition in self.get_configuration("sfx").items():
+            sfx_definition_members = sfx_definition.split(self.CONFIG_SEPARATOR)
+            path, volume, fade_out, loops, maxtime = sfx_definition_members[0], 1.0, 0, 0, 0
+            for ii, member in enumerate(sfx_definition_members[1:]):
+                if ii == 0:
+                    volume = float(member)
+                elif ii == 1:
+                    fade_out = float(member)
+                elif ii == 2:
+                    loops = int(member)
+                elif ii == 3:
+                    maxtime = float(member)
+            self.load_sfx_file(
+                path, name, True, volume=volume, fade_out=fade_out, loops=loops,
+                maxtime=maxtime)
+        if sfx_location is None:
+            sfx_location = self.get_configuration("audio", "sfx-project-path") + \
+                self.get_configuration("audio", "sfx-default-path")
+        if isinstance(sfx_location, str):
+            sfx_location = [sfx_location]
+        for root in sfx_location:
+            prefix = ""
+            root = self.get_resource(root)
+            print("checking {} for sound effects".format(root))
+            if root:
+                if os.path.isfile(root):
+                    self.load_sfx_file(root)
+                else:
+                    for node, branches, leaves in os.walk(root, followlinks=True):
+                        for leaf in leaves:
+                            prefix = re.sub(r"{}".format(root), r"", r"{}".format(node))
+                            prefix = re.sub(r"^{}".format(os.path.sep), r"", prefix)
+                            if prefix:
+                                prefix = re.sub(r"{}".format(os.path.sep), r"_", prefix) + "_"
+                            self.load_sfx_file(os.path.join(node, leaf,), prefix=prefix)
+
+    def load_sfx_file(self, path, name=None, replace=False, prefix="",
+                      volume=1.0, fade_out=0, loops=0, maxtime=0):
+        path = self.get_resource(path)
+        if path and self.is_loadable(path):
+            if name is None:
+                name = prefix + re.sub("\.[^.]*$", "", os.path.basename(path))
+            if not replace and name in self.sfx:
+                print("skipping existing sound effect for {}: {}".format(name, path))
+            else:
+                print("loading sound effect {} into {}".format(path, name))
+                self.sfx[name] = SoundEffect(
+                    self, path, volume, loops, fade_out, maxtime=maxtime)
+                return True
+        return False
+
+    def play_sfx(self, name, loops=None, maxtime=None, fade_ms=None, position=None,
+                 x=None):
+        return self.sfx[name].play(loops, maxtime, fade_ms, position, x)
+
+    #
+    # Loading BGM procedure
+    #
+    # - load config file name/path definitions
+    # - check project specific bgm paths, load any that don't conflict
+    # - other paths can replace loaded paths through the audio panel and get
+    #   written to config file
+    #
+    def load_bgm(self):
+        for name, bgm_definition in self.get_configuration("bgm").items():
+            bgm_definition_members = bgm_definition.split(self.CONFIG_SEPARATOR)
+            path, volume = bgm_definition_members[0], 1.0
+            for ii, member in enumerate(bgm_definition_members[1:]):
+                if ii == 0:
+                    volume = float(member)
+            self.set_bgm(path, name, volume=volume)
+        for root in self.get_configuration("audio", "bgm-project-path"):
+            if os.path.exists(root):
+                print("checking {} for music".format(root))
+                if os.path.isfile(root):
+                    self.set_bgm(root)
+                else:
+                    for node, branches, leaves in os.walk(root, followlinks=True):
+                        for leaf in leaves:
+                            prefix = re.sub(root, "", node)
+                            prefix = re.sub("^/", "", prefix)
+                            if prefix:
+                                prefix = re.sub("/", "_", prefix) + "_"
+                            self.set_bgm(os.path.join(node, leaf), prefix=prefix)
+
+    def set_bgm(self, path, name=None, prefix="", volume=1.0):
+        print("setting {} music to {}".format(name, path))
+        try:
+            pygame.mixer.music.load(path)
+        except:
+            print("can't load {} as music".format(path))
+            return False
+        if name is None:
+            name = os.path.basename(path).split(".")[0]
+        self.bgm[prefix + name] = BGM(self, path, volume)
+        return True
+
+    def play_bgm(self, name=None, store_as_current=True, start=0):
+        if name is None:
+            bgm = self.current_bgm
+        else:
+            bgm = self.bgm[name]
+        pygame.mixer.music.load(bgm.get_path())
+        try:
+            pygame.mixer.music.play(-1, start)
+        except pygame.error:
+            pygame.mixer.music.play(-1)
+        pygame.mixer.music.set_volume(bgm.get_volume() * self.get_configuration("audio", "volume"))
+        if store_as_current:
+            self.current_bgm = bgm
+
+    def is_sound_file(self, path):
+        return path.split(".")[-1] in self.get_configuration("audio", "sfx-extensions")
+
+    def is_loadable(self, path):
+        try:
+            pygame.mixer.Sound(path)
+        except:
+            return False
+        return True
+
+    def is_streamable(self, path):
+        try:
+            pygame.mixer.music.load(path)
+        except:
+            return False
+        return True
+
+    def is_audio_panel_active(self):
+        return self.audio_panel and self.audio_panel.active
+
+    def update(self):
+        Animation.update(self)
+        if self.audio_panel:
+            self.audio_panel.update()
+
+
+class BGM(GameChild):
+
+    def __init__(self, parent, path, volume=1.0):
+        GameChild.__init__(self, parent)
+        self.path = path
+        self.volume = volume
+
+    def get_path(self):
+        return self.path
+
+    def adjust_volume(self, increment):
+        self.volume = clamp(self.volume + increment, 0, 1.0)
+        if self.parent.current_bgm == self:
+            pygame.mixer.music.set_volume(self.volume)
+        return self.volume
+
+    def get_volume(self):
+        return self.volume
+
+    def __eq__(self, other):
+        return self.path == other.path 
+
+
+class SoundEffect(GameChild, pygame.mixer.Sound):
+
+    def __init__(self, parent, path, volume=1.0, loops=0, fade_out_length=0,
+                 fade_in_length=0, maxtime=0):
+        self.path = path
+        GameChild.__init__(self, parent)
+        pygame.mixer.Sound.__init__(self, path)
+        self.display_surface = self.get_display_surface()
+        self.local_volume = volume
+        self.loops = loops
+        self.fade_out_length = fade_out_length
+        self.fade_in_length = fade_in_length
+        self.maxtime = maxtime
+
+    def play(self, loops=None, maxtime=None, fade_ms=None, position=None,
+             x=None):
+        self.set_volume(
+            self.local_volume * self.get_configuration("audio", "volume"))
+        if loops is None:
+            loops = self.loops
+        if maxtime is None:
+            maxtime = int(self.maxtime * 1000)
+        if fade_ms is None:
+            fade_ms = int(self.fade_in_length * 1000)
+        channel = pygame.mixer.Sound.play(self, loops, maxtime, fade_ms)
+        if x is not None:
+            position = float(x) / self.display_surface.get_width()
+        if position is not None and channel is not None:
+            channel.set_volume(*self.get_panning(position))
+        if self.fade_out_length > 0:
+            self.fadeout(int(self.fade_out_length * 1000))
+        return channel
+
+    def get_panning(self, position):
+        return 1 - max(0, ((position - .5) * 2)), \
+               1 + min(0, ((position - .5) * 2))
+
+    def adjust_volume(self, increment):
+        self.local_volume += increment
+        if self.local_volume > 1.0:
+            self.local_volume = 1.0
+        elif self.local_volume < 0:
+            self.local_volume = 0
+        return self.local_volume
+
+    def adjust_loop_count(self, increment):
+        self.loops += increment
+        if self.loops < -1:
+            self.loops = -1
+        return self.loops
+
+    def adjust_fade_in_length(self, increment):
+        self.fade_in_length += increment
+        limit = self.get_length() * (self.loops + 1)
+        if self.fade_in_length < 0:
+            self.fade_in_length = 0
+        elif self.loops > -1 and self.fade_in_length > limit:
+            self.fade_in_length = limit
+        return self.fade_in_length
+
+    def adjust_fade_out_length(self, increment):
+        self.fade_out_length += increment
+        limit = self.get_length() * (self.loops + 1)
+        if self.fade_out_length < 0:
+            self.fade_out_length = 0
+        elif self.loops > -1 and self.fade_out_length > limit:
+            self.fade_out_length = limit
+        return self.fade_out_length
+
+    def adjust_maxtime(self, increment):
+        self.maxtime += increment
+        limit = self.get_length() * (self.loops + 1)
+        if self.maxtime < 0:
+            self.maxtime = 0
+        elif self.loops > -1 and self.maxtime > limit:
+            self.maxtime = limit
+        return self.maxtime
+
+
+class AudioPanel(Animation):
+
+    MARGIN = 6
+
+    def __init__(self, parent):
+        Animation.__init__(self, parent)
+        self.rows = []
+        self.bgm_elapsed = None
+        font_path = self.get_resource(self.get_configuration("audio", "panel-font"))
+        self.font_large = pygame.font.Font(font_path, 15)
+        self.font_medium = pygame.font.Font(font_path, 12)
+        self.font_small = pygame.font.Font(font_path, 8)
+        self.file_browser = AudioPanelFileBrowser(self)
+        self.subscribe(self.respond)
+        self.subscribe(self.respond, pygame.MOUSEBUTTONDOWN)
+        self.reset()
+
+    def reset(self):
+        self.row_offset = 0
+        self.deactivate()
+
+    def get_selected(self):
+        for row in self.rows:
+            if row.selected:
+                return row
+
+    def activate(self):
+        pygame.mouse.set_visible(True)
+        self.active = True
+        if pygame.mixer.music.get_busy():
+            self.bgm_elapsed = pygame.mixer.music.get_pos() / 1000
+            pygame.mixer.music.stop()
+        pygame.mixer.stop()
+        # self.build()
+
+    def deactivate(self):
+        pygame.mouse.set_visible(self.get_configuration("mouse", "visible"))
+        self.active = False
+        if self.bgm_elapsed is not None:
+            self.get_audio().play_bgm(start=self.bgm_elapsed)
+        self.file_browser.hide()
+
+    def respond(self, event):
+        if self.get_delegate().compare(event, "toggle-audio-panel") and self.get_audio().sfx:
+            if self.active:
+                self.deactivate()
+            else:
+                self.activate()
+                if not self.rows:
+                    self.build()
+        elif self.active:
+            if event.type == pygame.MOUSEBUTTONDOWN and self.file_browser.is_hidden():
+                if event.button == 5:
+                    self.row_offset += 1
+                elif event.button == 4:
+                    self.row_offset -= 1
+                elif event.button == 3:
+                    self.deactivate()
+
+    def build(self):
+        for row in self.rows:
+            row.unsubscribe()
+            del row
+        self.rows = []
+        for key in sorted(self.parent.bgm):
+            self.rows.append(AudioPanelRow(self, key, True))
+        for key in sorted(self.parent.sfx):
+            self.rows.append(AudioPanelRow(self, key))
+
+    def update(self):
+        if self.active:
+            Animation.update(self)
+            ds = self.get_display_surface()
+            dsr = ds.get_rect()
+            ds.fill((0, 0, 0))
+            corner = Vector(self.MARGIN, self.MARGIN)
+            index = self.row_offset
+            for row in self.rows:
+                row.location.bottom = 0
+                row.update()
+            while corner.y < dsr.height - self.MARGIN:
+                row = self.rows[index % len(self.rows)]
+                row.location.topleft = corner.copy()
+                row.update()
+                corner.y += row.location.height + self.MARGIN
+                index += 1
+            self.file_browser.update()
+
+
+class AudioPanelRow(BlinkingSprite):
+
+    BACKGROUND = pygame.Color(128, 192, 255, 255)
+    FOREGROUND = pygame.Color(0, 0, 0, 255)
+    WIDTH = .5
+    HEIGHT = 30
+    INDENT = 4
+    MAX_NAME_WIDTH = .7
+    SLIDER_W = 60
+    BUTTON_W = 30
+
+    def __init__(self, parent, key, is_bgm=False):
+        BlinkingSprite.__init__(self, parent, 500)
+        self.key = key
+        self.selected = False
+        self.font = self.parent.font_large
+        self.is_bgm = is_bgm
+        self.build()
+        font_medium = self.parent.font_medium
+        font_small = self.parent.font_small
+        if self.is_bgm:
+            volume = self.get_bgm().volume
+            volume_function = self.get_bgm().adjust_volume
+        else:
+            volume = self.get_sound_effect().local_volume
+            volume_function = self.get_sound_effect().adjust_volume
+        self.volume_spinner = AudioPanelSpinner(
+            self, font_medium, font_small, self.SLIDER_W, self.location.h, .05,
+            volume, volume_function, self.FOREGROUND, self.BACKGROUND, 2, "vol")
+        if not self.is_bgm:
+            self.fade_out_spinner = AudioPanelSpinner(
+                self, font_medium, font_small, self.SLIDER_W, self.location.h, .1,
+                self.get_sound_effect().fade_out_length,
+                self.get_sound_effect().adjust_fade_out_length, self.FOREGROUND,
+                self.BACKGROUND, 1, "fade")
+            self.loops_spinner = AudioPanelSpinner(
+                self, font_medium, font_small, self.SLIDER_W, self.location.h, 1,
+                self.get_sound_effect().loops,
+                self.get_sound_effect().adjust_loop_count, self.FOREGROUND,
+                self.BACKGROUND, 0, "loops")
+            self.maxtime_spinner = AudioPanelSpinner(
+                self, font_medium, font_small, self.SLIDER_W, self.location.h, .1,
+                self.get_sound_effect().maxtime,
+                self.get_sound_effect().adjust_maxtime, self.FOREGROUND,
+                self.BACKGROUND, 1, "cutoff")
+        if self.is_bgm:
+            callback, kwargs = self.get_game().get_audio().play_bgm, {"name": self.key, "store_as_current": False}
+        else:
+            callback, kwargs = self.get_sound_effect().play, {}
+        self.play_button = AudioPanelButton(self, callback, kwargs)
+        frame = pygame.Surface((self.BUTTON_W, self.location.h), SRCALPHA)
+        frame.fill(self.BACKGROUND)
+        stop_button_frame = frame.copy()
+        w, h = frame.get_size()
+        pygame.draw.polygon(frame, self.FOREGROUND, ((w * .25, h * .25), (w * .25, h * .75), (w * .75, h * .5)))
+        self.play_button.add_frame(frame)
+        if self.is_bgm:
+            callback = pygame.mixer.music.stop
+        else:
+            callback = self.get_sound_effect().stop
+        self.stop_button = AudioPanelButton(self, callback)
+        stop_button_frame.fill(self.FOREGROUND, (w * .25, h * .25, w * .5, h * .5))
+        self.stop_button.add_frame(stop_button_frame)
+        self.stop_blinking()
+        self.subscribe(self.respond, pygame.MOUSEBUTTONDOWN)
+
+    def respond(self, event):
+        if self.parent.active and event.button == 1:
+            if self.parent.file_browser.is_hidden() and self.location.collidepoint(event.pos):
+                if not self.selected:
+                    self.parent.file_browser.visit(self.parent.file_browser.HOME)
+                    self.selected = True
+                    self.start_blinking()
+                self.parent.file_browser.unhide()
+            elif self.parent.file_browser.is_hidden():
+                if self.selected:
+                    self.selected = False
+                self.stop_blinking()
+
+    def unsubscribe(self, callback=None, kind=None):
+        if callback is None:
+            callback = self.respond
+            kind = pygame.MOUSEBUTTONDOWN
+        GameChild.unsubscribe(self, self.respond, pygame.MOUSEBUTTONDOWN)
+        self.play_button.unsubscribe()
+        self.stop_button.unsubscribe()
+        if not self.is_bgm:
+            for spinner in self.volume_spinner, self.fade_out_spinner, self.loops_spinner, self.maxtime_spinner:
+                spinner.unsubscribe()
+
+    def build(self):
+        ds = self.get_display_surface()
+        dsr = ds.get_rect()
+        surface = pygame.Surface((dsr.w * self.WIDTH, self.HEIGHT), pygame.SRCALPHA)
+        surface.fill(self.BACKGROUND)
+        self.add_frame(surface)
+        name_sprite = Sprite(self)
+        name = self.font.render(self.key + ":", True, self.FOREGROUND)
+        if name.get_width() > int(self.location.w * self.MAX_NAME_WIDTH):
+            crop = pygame.Rect(0, 0, int(self.location.w * self.MAX_NAME_WIDTH), name.get_height())
+            crop.right = name.get_rect().right
+            name = name.subsurface(crop)
+        name_sprite.add_frame(name)
+        name_sprite.display_surface = surface
+        name_sprite.location.midleft = self.INDENT, self.location.centery
+        name_sprite.update()
+        file_sprite = Sprite(self)
+        box = get_boxed_surface(
+            pygame.Surface((self.location.w - name_sprite.location.w - self.INDENT * 3,
+                            self.location.height - 4), pygame.SRCALPHA),
+            border=self.FOREGROUND)
+        file_sprite.add_frame(box)
+        file_sprite.location.midright = self.location.right - self.INDENT, self.location.centery
+        file_sprite.display_surface = surface
+        file_name_sprite = Sprite(self)
+        if self.is_bgm:
+            file_name = self.get_bgm().path
+        else:
+            file_name = self.get_sound_effect().path
+        file_name_text = self.font.render(file_name, True, self.FOREGROUND)
+        file_name_sprite.add_frame(file_name_text)
+        file_name_sprite.display_surface = box
+        file_name_sprite.location.midright = file_sprite.location.w - self.INDENT, file_sprite.location.h / 2
+        file_name_sprite.update()
+        file_sprite.update()
+
+    def get_sound_effect(self):
+        return self.get_game().get_audio().sfx[self.key]
+
+    def get_bgm(self):
+        return self.get_game().get_audio().bgm[self.key]
+
+    def update_config(self):
+        if self.is_bgm:
+            section_name = "bgm"
+        else:
+            section_name = "sfx"
+        if not self.get_configuration().has_section(section_name):
+            self.get_configuration().add_section(section_name)
+        if self.is_bgm:
+            bgm = self.get_bgm()
+            config_value = "{}, {:.2f}".format(bgm.path, bgm.volume)
+        else:
+            sound_effect = self.get_sound_effect()
+            config_value = "{}, {:.2f}, {:.2f}, {}, {:.2f}".format(
+                sound_effect.path, sound_effect.local_volume, sound_effect.fade_out_length,
+                sound_effect.loops, sound_effect.maxtime)
+        self.get_configuration().set(section_name, self.key, config_value)
+        config_path = self.get_configuration().locate_project_config_file()
+        backup_path = config_path + ".backup"
+        shutil.copyfile(config_path, backup_path)
+        self.get_configuration().write(open(config_path, "w"))
+
+    def set_clickable(self, clickable=True):
+        self.play_button.set_clickable(clickable)
+        self.stop_button.set_clickable(clickable)
+        self.volume_spinner.set_clickable(clickable)
+        if not self.is_bgm:
+            self.fade_out_spinner.set_clickable(clickable)
+            self.loops_spinner.set_clickable(clickable)
+            self.maxtime_spinner.set_clickable(clickable)
+
+    def update(self):
+        self.play_button.location.midleft = self.location.move(5, 0).midright
+        self.stop_button.location.midleft = self.play_button.location.midright
+        self.volume_spinner.location.midleft = self.stop_button.location.move(5, 0).midright
+        if not self.is_bgm:
+            self.fade_out_spinner.location.midleft = self.volume_spinner.location.midright
+            self.loops_spinner.location.midleft = self.fade_out_spinner.location.midright
+            self.maxtime_spinner.location.midleft = self.loops_spinner.location.midright
+        Sprite.update(self)
+        self.volume_spinner.update()
+        if not self.is_bgm:
+            self.fade_out_spinner.update()
+            self.loops_spinner.update()
+            self.maxtime_spinner.update()
+        self.play_button.update()
+        self.stop_button.update()
+
+
+class AudioPanelFileBrowser(Sprite):
+
+    WIDTH = .75
+    HEIGHT = .75
+    COLORS = pygame.Color(255, 255, 255), pygame.Color(0, 0, 0)
+    HOME, UP = "[HOME]", "[UP]"
+
+    def __init__(self, parent):
+        Sprite.__init__(self, parent)
+        self.rows = []
+        self.font = self.parent.font_large
+        self.previewing_sound = None
+        self.previewing_sound_row = None
+        ds = self.get_display_surface()
+        dsr = ds.get_rect()
+        surface = pygame.Surface((dsr.w * self.WIDTH - 2, dsr.h * self.HEIGHT - 2), SRCALPHA)
+        surface.fill(self.COLORS[0])
+        self.background = get_boxed_surface(surface, self.COLORS[0], self.COLORS[1])
+        self.add_frame(self.background.copy())
+        self.location.center = dsr.center
+        self.reset()
+        self.subscribe(self.respond, pygame.MOUSEBUTTONDOWN)
+
+    def reset(self):
+        if self.previewing_sound is not None:
+            self.previewing_sound.stop()
+        # self.visit(self.HOME)
+        self.hide()
+
+    def respond(self, event):
+        if not self.is_hidden():
+            if event.button == 1:
+                if self.collide(event.pos):
+                    for row in self.rows:
+                        pos = Vector(*event.pos).get_moved(-self.location.left, -self.location.top)
+                        if (not row.has_child("button") or pos.x < row.get_child("button").location.left) and row.collide(pos):
+                            full_path = os.path.join(os.path.sep.join(self.trail), row.path)
+                            if row.path == self.HOME or row.path == self.UP or \
+                               os.path.isdir(full_path) and os.access(full_path, os.R_OK):
+                                self.visit(row.path)
+                            elif os.path.isfile(full_path) and os.access(full_path, os.R_OK):
+                                loaded = False
+                                selected = self.parent.get_selected()
+                                if selected.is_bgm:
+                                    loaded = self.get_audio().set_bgm(full_path, selected.key)
+                                else:
+                                    loaded = self.get_audio().load_sfx_file(full_path, selected.key, True)
+                                if loaded:
+                                    selected.update_config()
+                                    self.hide()
+                                    self.get_delegate().cancel_propagation()
+                                    self.parent.build()
+                else:
+                    self.hide()
+                    self.get_delegate().cancel_propagation()
+            elif event.button == 4:
+                self.row_offset -= 1
+            elif event.button == 5:
+                self.row_offset += 1
+
+    def hide(self):
+        for row in self.parent.rows:
+            row.selected = False
+            row.stop_blinking()
+            row.set_clickable(True)
+        for row in self.rows:
+            if row.has_child("button"):
+                row.get_child("button").set_clickable(False)
+        if self.previewing_sound is not None:
+            self.previewing_sound.stop()
+        Sprite.hide(self)
+
+    def unhide(self):
+        for row in self.parent.rows:
+            row.set_clickable(False)
+        for row in self.rows:
+            if row.has_child("button"):
+                row.get_child("button").set_clickable()
+        Sprite.unhide(self)
+
+    def visit(self, path):
+        if path == self.UP and len(self.trail) > 1:
+            path = self.trail[-2]
+            self.trail = self.trail[:-2]
+            self.visit(path)
+        elif path != self.UP:
+            self.row_offset = 0
+            if path == self.HOME:
+                self.trail = []
+                self.paths = ["/"]
+                for option in "sfx-repository-path", "sfx-default-path", "sfx-project-path", \
+                    "bgm-repository-path", "bgm-project-path":
+                    for sfx_location in self.get_configuration("audio", option):
+                        if self.get_resource(sfx_location):
+                            self.paths.append(self.get_resource(sfx_location))
+            else:
+                self.paths = [self.HOME]
+                self.trail.append(path)
+                if len(self.trail) > 1:
+                    self.paths.append(self.UP)
+                self.paths.extend(sorted(os.listdir(os.path.sep.join(self.trail))))
+        self.build()
+
+    def build(self):
+        for row in self.rows:
+            if row.has_child("button"):
+                row.get_child("button").unsubscribe()
+            del row
+        self.rows = []
+        for path in self.paths:
+            row = Sprite(self)
+            row.path = path
+            text = self.font.render(path, True, self.COLORS[1])
+            surface = pygame.Surface((self.location.w, text.get_height()), SRCALPHA)
+            surface.blit(text, (8, 0))
+            surface.fill(self.COLORS[1], (0, surface.get_height() - 1, self.location.w, 1))
+            row.add_frame(surface)
+            row.display_surface = self.get_current_frame()
+            row.location.bottom = 0
+            self.rows.append(row)
+            full_path = os.path.join(os.path.sep.join(self.trail), path)
+            if self.get_audio().is_sound_file(full_path):
+                button = AudioPanelButton(self, self.preview, {"path": full_path, "row": row}, [row, self])
+                row.set_child("button", button)
+                frame = pygame.Surface([text.get_height()] * 2, SRCALPHA)
+                w, h = frame.get_size()
+                pygame.draw.polygon(
+                    frame, self.COLORS[1], ((w * .25, h * .25), (w * .25, h * .75), (w * .75, h * .5)))
+                button.add_frame(frame)
+                button.display_surface = row.get_current_frame()
+                button.location.right = self.location.w - 10
+
+    def preview(self, path, row):
+        is_bgm = self.parent.get_selected().is_bgm
+        audio = self.get_audio()
+        if is_bgm and audio.is_streamable(path) or not is_bgm and audio.is_loadable(path):
+            if self.previewing_sound is not None:
+                self.previewing_sound.stop()
+            pygame.mixer.music.stop()
+            if is_bgm:
+                pygame.mixer.music.load(path)
+                pygame.mixer.music.play(-1)
+            else:
+                self.previewing_sound = SoundEffect(self, path)
+                self.previewing_sound.play()
+            self.previewing_sound_row = row
+
+    def update(self):
+        self.get_current_frame().blit(self.background, (0, 0))
+        if not self.is_hidden():
+            corner = Vector(1, 1)
+            index = self.row_offset
+            for row in self.rows:
+                row.remove_locations()
+                row.location.bottom = 0
+            while corner.y < self.location.h:
+                row = self.rows[index % len(self.rows)]
+                if index - self.row_offset >= len(self.rows):
+                    row.add_location(corner.copy())
+                else:
+                    row.location.topleft = corner.copy()
+                corner.y += row.location.height
+                index += 1
+            for row in self.rows:
+                row.update()
+                for location in row.locations:
+                    if location.collidepoint(*Vector(*pygame.mouse.get_pos()).get_moved(
+                            -self.location.left, -self.location.top)) or \
+                            row == self.previewing_sound_row:
+                        self.get_current_frame().fill(self.COLORS[1], (
+                            location.topleft, (6, location.h)))
+                        self.get_current_frame().fill(self.COLORS[1], (
+                            location.move(-8, 0).topright, (6, location.h)))
+        Sprite.update(self)
+
+
+class AudioPanelSpinner(Sprite):
+
+    def __init__(self, parent, font, label_font, width=80, height=48,
+                 magnitude=1, value=0, callback=None,
+                 foreground=pygame.Color(0, 0, 0),
+                 background=pygame.Color(255, 255, 255), precision=0,
+                 label_text=""):
+        Sprite.__init__(self, parent)
+        self.magnitude, self.value = magnitude, value
+        self.background, self.foreground = background, foreground
+        self.precision = precision
+        self.callback = callback
+        self.font = font
+        self.label_font = label_font
+        surface = pygame.Surface((width, height), SRCALPHA)
+        surface.fill(background)
+        self.add_frame(surface)
+        self.label = Sprite(self)
+        self.label.add_frame(self.label_font.render(label_text, True, foreground))
+        self.label.display_surface = self.get_current_frame()
+        self.display = Sprite(self)
+        self.display.display_surface = self.get_current_frame()
+        self.update_display()
+        self.up_button = Sprite(self)
+        self.up_button.add_frame(render_box(
+            width=width - self.display.location.w - 2, height=int(height * .5) - 2,
+            color=foreground, border=foreground, font=self.font, text="+"))
+        self.up_button.location.left = self.display.location.right - 1
+        self.up_button.display_surface = self.get_current_frame()
+        self.down_button = Sprite(self)
+        self.down_button.add_frame(render_box(
+            width=self.up_button.location.w - 2, height=self.up_button.location.h - 1,
+            border=foreground, font=self.font, text="-"))
+        self.down_button.location.topleft = self.display.location.right - 1, \
+            self.up_button.location.bottom - 1
+        self.down_button.display_surface = self.get_current_frame()
+        self.set_clickable()
+        self.subscribe(self.respond, pygame.MOUSEBUTTONDOWN)
+
+    def unsubscribe(self, callback=None, kind=None):
+        if callback is None:
+            callback = self.respond
+            kind = pygame.MOUSEBUTTONDOWN
+        GameChild.unsubscribe(self, callback, kind)
+
+    def update_display(self):
+        self.display.clear_frames()
+        self.display.add_frame(render_box(
+            width=int(self.location.w * .7) - 2,
+            border=self.foreground, font=self.font, text="{:.{precision}f}".format(
+                self.value, precision=self.precision)))
+        self.display.location.bottomleft = 0, self.location.h
+
+    def increment(self, up=True):
+        step = self.magnitude * [-1, 1][up]
+        self.value += step
+        if self.callback is not None:
+            response = self.callback(step)
+            if response is not None:
+                self.value = response
+        self.update_display()
+
+    def respond(self, event):
+        if self.clickable and event.button == 1:
+            relative_position = Vector(*event.pos).get_moved(
+                -self.location.left, -self.location.top)
+            up_collides = self.up_button.collide(relative_position)
+            down_collides = self.down_button.collide(relative_position)
+            if up_collides or down_collides:
+                if up_collides:
+                    self.increment()
+                else:
+                    self.increment(False)
+                self.parent.update_config()
+
+    def set_clickable(self, clickable=True):
+        self.clickable = clickable
+
+    def update(self):
+        self.get_current_frame().fill(self.background)
+        self.label.update()
+        self.up_button.update()
+        self.down_button.update()
+        self.display.update()
+        Sprite.update(self)
+
+
+class AudioPanelButton(Sprite):
+
+    def __init__(self, parent, callback, callback_kwargs={}, containers=[], pass_mods=False):
+        Sprite.__init__(self, parent)
+        self.callback = callback
+        self.callback_kwargs = callback_kwargs
+        self.containers = containers
+        self.pass_mods = pass_mods
+        self.set_clickable()
+        self.subscribe(self.respond, pygame.MOUSEBUTTONDOWN)
+
+    def unsubscribe(self, callback=None, kind=None):
+        if callback is None:
+            callback = self.respond
+            kind = pygame.MOUSEBUTTONDOWN
+        Sprite.unsubscribe(self, callback, kind)
+
+    def respond(self, event):
+        if self.get_audio().audio_panel.active and self.clickable and event.button == 1:
+            pos = Vector(*event.pos)
+            for container in self.containers:
+                pos.move(-container.location.left, -container.location.top)
+            if self.collide(pos):
+                if self.pass_mods:
+                    kwargs = collections.ChainMap(self.callback_kwargs, {"mods": pygame.key.get_mods()})
+                else:
+                    kwargs = self.callback_kwargs
+                self.callback(**kwargs)
+
+    def set_clickable(self, clickable=True):
+        self.clickable = clickable
diff --git a/camera_views/lib/pgfw/pgfw/Configuration.py b/camera_views/lib/pgfw/pgfw/Configuration.py
new file mode 100644 (file)
index 0000000..10cf2cb
--- /dev/null
@@ -0,0 +1,496 @@
+from os import sep, getcwd
+from os.path import join, exists, basename, dirname, expanduser
+from sys import argv, version_info
+from re import match
+from pprint import pformat
+
+if version_info[0] >= 3:
+    from configparser import RawConfigParser
+else:
+    from ConfigParser import RawConfigParser
+
+class Configuration(RawConfigParser):
+
+    default_project_file_rel_path = "config"
+    default_resource_paths = [".", "resource"]
+
+    def __init__(self, project_file_rel_path=None, resource_path=None,
+                 type_declarations=None):
+        RawConfigParser.__init__(self)
+        self.project_file_rel_path = project_file_rel_path
+        self.resource_path = resource_path
+        self.modifiable = {}
+        self.order = []
+        self.set_type_declarations(type_declarations)
+        self.set_defaults()
+        self.read_project_config_file()
+        self.modify_defaults()
+        self.print_debug(self)
+
+    def set_type_declarations(self, type_declarations):
+        if type_declarations is None:
+            type_declarations = TypeDeclarations()
+        self.type_declarations = type_declarations
+
+    def translate_path(self, path):
+        new = ""
+        if path and path[0] == sep:
+            new += sep
+        return expanduser("{0}{1}".format(new, join(*path.split(sep))))
+
+    def set_defaults(self):
+        add_section = self.add_section
+        set_option = self.set
+        section = "setup"
+        add_section(section)
+        set_option(section, "package-root", basename(getcwd()), False)
+        set_option(section, "additional-packages", "", False)
+        set_option(section, "title", "", False)
+        set_option(section, "classifiers", "", False)
+        set_option(section, "resource-search-path", "./, resource/", False)
+        set_option(section, "installation-dir", "/usr/local/share/games/",
+                   False)
+        set_option(section, "changelog", "changelog", False)
+        set_option(section, "description-file", "", False)
+        set_option(section, "init-script", "", False)
+        set_option(section, "version", "", False)
+        set_option(section, "summary", "", False)
+        set_option(section, "license", "", False)
+        set_option(section, "platforms", "", False)
+        set_option(section, "contact-name", "", False)
+        set_option(section, "contact-email", "", False)
+        set_option(section, "url", "", False)
+        set_option(section, "requirements", "", False)
+        set_option(section, "main-object", "pgfw/Game.py", False)
+        set_option(section, "resource-path-identifier", "resource_path", False)
+        set_option(section, "special-char-placeholder", "_", False)
+        set_option(section, "whitespace-placeholder", "-", False)
+        set_option(section, "windows-dist-path", "dist/win/", False)
+        set_option(section, "windows-icon-path", "", False)
+        set_option(section, "boolean-true-lowercase", "yes, true, t, 1", False)
+        set_option(section, "osx-includes", "", False)
+        section = "display"
+        add_section(section)
+        set_option(section, "dimensions", "480, 360", False)
+        set_option(section, "frame-duration", "40", False)
+        set_option(section, "wait-duration", "2", False)
+        set_option(section, "caption", "", False)
+        set_option(section, "centered", "yes", False)
+        set_option(section, "icon-path", "", False)
+        set_option(section, "skip-frames", "no", False)
+        set_option(section, "fullscreen", "no", False)
+        set_option(section, "windowed-flag", "wi", False)
+        set_option(section, "show-framerate", "no", False)
+        set_option(section, "framerate-display-flag", "fr", False)
+        set_option(section, "framerate-text-size", "16", False)
+        set_option(section, "framerate-text-color", "0, 0, 0", False)
+        set_option(section, "framerate-text-background", "255, 255, 255", False)
+        set_option(section, "use-framebuffer", "no", False)
+        section = "input"
+        add_section(section)
+        set_option(section, "release-suffix", "-release", False)
+        set_option(section, "confirm-quit", "no", False)
+        section = "sprite"
+        add_section(section)
+        set_option(section, "transparent-color", "magenta", False)
+        section = "screen-captures"
+        add_section(section)
+        set_option(section, "rel-path", "caps", False)
+        set_option(section, "file-name-format", "%Y%m%d%H%M%S", False)
+        set_option(section, "file-extension", "png", False)
+        section = "video-recordings"
+        add_section(section)
+        set_option(section, "enable", "no", False)
+        set_option(section, "rel-path", "vids", False)
+        set_option(section, "directory-name-format", "%Y%m%d%H%M%S", False)
+        set_option(section, "file-extension", "png", False)
+        set_option(section, "frame-format", "RGB", False)
+        set_option(section, "framerate", "40", False)
+        set_option(section, "temp-directory", "", False)
+        set_option(section, "record-audio", "yes", False)
+        set_option(section, "filename-digits", "6", False)
+        section = "mouse"
+        add_section(section)
+        set_option(section, "visible", "yes", False)
+        set_option(section, "double-click-time-limit", ".5", False)
+        section = "keys"
+        add_section(section)
+        set_option(section, "up", "K_UP", False)
+        set_option(section, "right", "K_RIGHT", False)
+        set_option(section, "down", "K_DOWN", False)
+        set_option(section, "left", "K_LEFT", False)
+        set_option(section, "capture-screen", "K_F9", False)
+        set_option(section, "toggle-fullscreen", "K_F11", False)
+        set_option(section, "reset-game", "K_F8", False)
+        set_option(section, "record-video", "K_F10", False)
+        set_option(section, "volume-down", "K_F1", False)
+        set_option(section, "volume-up", "K_F2", False)
+        set_option(section, "volume-mute", "K_F3", False)
+        set_option(section, "toggle-interpolator", "K_F7", False)
+        set_option(section, "toggle-audio-panel", "K_F6", False)
+        section = "joy"
+        add_section(section)
+        set_option(section, "advance", "7", False)
+        set_option(section, "pause", "7", False)
+        set_option(section, "select", "6", False)
+        set_option(section, "single-xy", "no", False)
+        set_option(section, "delay-axis", "0", False)
+        set_option(section, "vertical-axis", "1", False)
+        set_option(section, "horizontal-axis", "0", False)
+        section = "event"
+        add_section(section)
+        set_option(section, "user-event-id", "USEREVENT", False)
+        set_option(section, "command-id-offset", "1", False)
+        set_option(section, "command-key", "command", False)
+        set_option(section, "cancel-flag-key", "cancel", False)
+        section = "audio"
+        add_section(section)
+        set_option(section, "sfx-default-path", "~/storage/audio/sfx/default", False)
+        set_option(section, "sfx-repository-path", "~/storage/audio/sfx/all", False)
+        set_option(section, "sfx-project-path", "sfx", False)
+        set_option(section, "sfx-extensions", "wav, ogg, mp3", False)
+        set_option(section, "bgm-repository-path", "~/storage/audio/bgm", False)
+        set_option(section, "bgm-project-path", "bgm", False)
+        set_option(section, "sfx-volume", "1.0", False)
+        set_option(section, "bgm-volume", "1.0", False)
+        set_option(section, "volume", "1.0", False)
+        set_option(section, "panel-enabled", "no", False)
+        set_option(section, "panel-font", None, False)
+        section = "interpolator-gui"
+        add_section(section)
+        set_option(section, "margin", "80", False)
+        set_option(section, "marker-color", "255, 0, 0", False)
+        set_option(section, "marker-size", "11", False)
+        set_option(section, "curve-color", "0, 255, 0", False)
+        set_option(section, "label-size", "16", False)
+        set_option(section, "label-precision", "2", False)
+        set_option(section, "axis-label-count", "8", False)
+        set_option(section, "prompt-size", "380, 60", False)
+        set_option(section, "prompt-border-color", "255, 0, 0", False)
+        set_option(section, "prompt-border-width", "3", False)
+        set_option(section, "prompt-character-limit", "21", False)
+        set_option(section, "prompt-text-size", "42", False)
+        set_option(section, "template-nodeset", "L 0 0, 1000 1", False)
+        set_option(section, "template-nodeset-name", "template", False)
+        set_option(section, "flat-y-range", "1", False)
+
+    def add_section(self, name):
+        if name not in self.order:
+            self.order.append(name)
+        RawConfigParser.add_section(self, name)
+
+    def set(self, section, option, value, modifiable=True):
+        if modifiable:
+            if section not in self.order:
+                self.order.append(section)
+            if section not in self.modifiable:
+                self.modifiable[section] = []
+            if option not in self.modifiable[section]:
+                self.modifiable[section].append(option)
+        RawConfigParser.set(self, section, option, value)
+
+    def read_project_config_file(self):
+        path = self.locate_project_config_file()
+        if path:
+            fp = open(path)
+            self.set_modifiable(fp)
+            fp.seek(0)
+            self.readfp(fp)
+            fp.seek(0)
+            self.set_order(fp)
+            fp.close()
+        else:
+            self.print_debug("No configuration file found")
+
+    def locate_project_config_file(self):
+        rel_path = self.project_file_rel_path
+        if not rel_path:
+            rel_path = self.default_project_file_rel_path
+        if exists(rel_path) and not self.is_shared_mode():
+            return rel_path
+        if self.resource_path:
+            installed_path = join(self.resource_path, rel_path)
+            if exists(installed_path):
+                return installed_path
+
+    def set_order(self, fp):
+        self.order = order = []
+        for line in open(self.locate_project_config_file()):
+            result = match("^\s*\[(.*)\]\s*$", line)
+            if result:
+                order.append(result.group(1))
+
+    def set_modifiable(self, fp):
+        config = RawConfigParser()
+        config.readfp(fp)
+        modifiable = self.modifiable
+        for section in config._sections:
+            if section not in modifiable:
+                modifiable[section] = []
+            for option in config._sections[section]:
+                if option != "__name__" and option not in modifiable[section]:
+                    modifiable[section].append(option)
+
+    def is_shared_mode(self):
+        return "-s" in argv
+
+    def print_debug(self, statement):
+        if self.is_debug_mode():
+            print(statement)
+
+    def is_debug_mode(self):
+        return "-d" in argv
+
+    def modify_defaults(self):
+        self.set_installation_path()
+        self.set_resource_search_path()
+        self.set_screen_captures_path()
+        self.set_video_recordings_path()
+        self.set_data_exclusion_list()
+        self.set_requirements()
+
+    def set_installation_path(self):
+        self.set("setup", "installation-path",
+                 join(self.get("setup", "installation-dir"),
+                      self.get("setup", "package-root")), False)
+
+    def set_resource_search_path(self):
+        section, option = "setup", "resource-search-path"
+        search_path = self.get(section, option)
+        if self.resource_path:
+            search_path.append(self.resource_path)
+        else:
+            search_path.append(self.get("setup", "installation-path"))
+        self.set(section, option, search_path, False)
+
+    def get(self, section, option):
+        value = RawConfigParser.get(self, section, option)
+        if value is None:
+            value = self.get_substitute(section, option)
+        return self.cast_value(section, option, value)
+
+    def get_substitute(self, section, option):
+        if section == "display":
+            if option == "caption":
+                return self.get("setup", "title")
+
+    def cast_value(self, section, option, value):
+        pair = section, option
+        types = self.type_declarations
+        # if type(value) == str or type(value) == unicode:
+        if type(value) == str:
+            if pair in types["bool"]:
+                return value.lower() in self.get("setup", "boolean-true-lowercase")
+            elif pair in types["int"]:
+                return int(value)
+            elif pair in types["float"]:
+                return float(value)
+            elif pair in types["path"]:
+                return self.translate_path(value)
+            elif pair in types["list"]:
+                if value == "":
+                    return []
+                else:
+                    return [member.strip() for member in value.split(types.list_member_sep)]
+            elif pair in types["int-list"]:
+                return [int(member) for member in value.split(types.list_member_sep)]
+            elif pair in types["float-list"]:
+                return [float(member) for member in value.split(types.list_member_sep)]
+            elif pair in types["path-list"]:
+                return [self.translate_path(member.strip()) for member in value.split(types.list_member_sep)]
+        return value
+
+    def set_screen_captures_path(self):
+        section, option = "screen-captures", "path"
+        if not self.has_option(section, option):
+            self.set(section, option, join(self.build_home_path(),
+                                           self.get(section, "rel-path")),
+                     False)
+
+    def build_home_path(self):
+        return join("~", "." + self.get("setup", "package-root"))
+
+    def set_video_recordings_path(self):
+        section, option = "video-recordings", "path"
+        if not self.has_option(section, option):
+            self.set(section, option, join(self.build_home_path(),
+                                           self.get(section, "rel-path")),
+                     False)
+
+    def set_data_exclusion_list(self):
+        section, option = "setup", "data-exclude"
+        exclude = []
+        if self.has_option(section, option):
+            exclude = self.get(section, option)
+        exclude += [".git*", "README", "build/", "dist/", "*.egg-info",
+                    "*.py", "MANIFEST*", "PKG-INFO", "*.pyc", "*.swp", "*~",
+                    self.get("setup", "changelog"),
+                    self.get("setup", "package-root"),
+                   self.get("setup", "init-script")]
+        for location in self.get("setup", "additional-packages"):
+            exclude.append(location)
+        self.set(section, option, exclude, False)
+
+    def set_requirements(self):
+        section, option = "setup", "requirements"
+        requirements = []
+        if self.has_option(section, option):
+            requirements = self.get(section, option)
+        if "pygame" not in requirements:
+            requirements.append("pygame")
+        self.set(section, option, requirements, False)
+
+    def get_section(self, section):
+        assignments = {}
+        for option in self.options(section):
+            assignments[option] = self.get(section, option)
+        return assignments
+
+    def __repr__(self):
+        config = {}
+        for section in self.sections():
+            config[section] = self.get_section(section)
+        return pformat(config, 2, 1)
+
+    def items(self, section):
+        items = []
+        for option in self.options(section):
+            items.append((option, self.get(section, option)))
+        return items
+
+    def write(self, fp=None):
+        modifiable = self.modifiable
+        use_main = fp is None
+        if use_main:
+            path = self.locate_project_config_file()
+            if not path:
+                path = join(self.resource_path or "",
+                            self.default_project_file_rel_path)
+            fp = open(path, "w")
+        break_line = False
+        for section in self.order:
+            if section in modifiable:
+                break_line and fp.write("\n")
+                fp.write("[%s]\n" % section)
+                for option in modifiable[section]:
+                    if self.has_option(section, option):
+                        value = self.get(section, option)
+                        fp.write("%s = %s\n" % (option,
+                                                self.get_raw_value(value)))
+                break_line = True
+        if use_main:
+            fp.close()
+
+    def get_raw_value(self, value):
+        if isinstance(value, list):
+            raw = ""
+            for ii, value in enumerate(value):
+                if ii:
+                    raw += ", "
+                raw += str(value)
+        else:
+            raw = str(value)
+        return raw
+
+    def clear_section(self, section):
+        if self.has_section(section):
+            for option in self.options(section):
+                self.remove_option(section, option)
+
+
+class TypeDeclarations(dict):
+
+    list_member_sep = ','
+
+    defaults = {
+
+        "display": {"int": ["frame-duration", "wait-duration",
+                            "framerate-text-size"],
+
+                    "bool": ["centered", "skip-frames", "fullscreen",
+                             "show-framerate", "use-framebuffer"],
+
+                    "int-list": ["dimensions", "framerate-text-color",
+                                 "framerate-text-background"]},
+
+        "input": {"bool": "confirm-quit"},
+
+        "screen-captures": {"path": ["rel-path", "path"]},
+
+        "video-recordings": {"path": ["rel-path", "path"],
+
+                             "int": ["framerate", "filename-digits"],
+
+                             "bool": ["enable", "record-audio"]},
+
+        "setup": {"list": ["classifiers", "resource-search-path",
+                           "requirements", "data-exclude",
+                           "additional-packages", "osx-includes", "boolean-true-lowercase"],
+
+                  "path": ["installation-dir", "changelog", "description-file",
+                           "main-object", "icon-path", "windows-dist-path",
+                           "package-root"]},
+
+        "mouse": {"float": "double-click-time-limit",
+
+                  "bool": "visible"},
+
+        "keys": {"list": ["up", "right", "down", "left"]},
+
+        "joy": {"int": ["advance", "pause", "select", "vertical-axis",
+                        "horizontal-axis"],
+
+                "float": "delay-axis",
+
+                "bool": "single-xy"},
+
+        "audio": {
+
+            "list": "sfx-extensions",
+
+            "path": "panel-font",
+
+            "path-list": [
+                "sfx-default-path", "sfx-repository-path", "sfx-project-path",
+                "bgm-repository-path", "bgm-project-path"
+            ],
+
+            "float": ["sfx-volume", "bgm-volume", "volume"],
+
+            "bool": "panel-enabled"
+
+        },
+
+        "event": {"int": "command-id-offset"},
+
+        "interpolator-gui": {"int": ["margin", "marker-size", "label-size",
+                                     "axis-label-count", "label-precision",
+                                     "prompt-border-width",
+                                     "prompt-character-limit",
+                                     "prompt-text-size", "flat-y-range"],
+
+                             "int-list": ["marker-color", "curve-color",
+                                          "prompt-size",
+                                          "prompt-border-color"]},
+
+        }
+
+    additional_defaults = {}
+
+    def __init__(self):
+        dict.__init__(self, {"bool": [], "int": [], "float": [], "path": [],
+                             "list": [], "int-list": [], "float-list": [],
+                             "path-list": []})
+        self.add_chart(self.defaults)
+        self.add_chart(self.additional_defaults)
+
+    def add(self, cast, section, option):
+        self[cast].append((section, option))
+
+    def add_chart(self, chart):
+        for section, declarations in chart.items():
+            for cast, options in declarations.items():
+                if type(options) != list:
+                    options = [options]
+                for option in options:
+                    self.add(cast, section, option)
diff --git a/camera_views/lib/pgfw/pgfw/Delegate.py b/camera_views/lib/pgfw/pgfw/Delegate.py
new file mode 100644 (file)
index 0000000..ca516ca
--- /dev/null
@@ -0,0 +1,97 @@
+import collections
+
+from pygame.event import get, pump, Event, post
+from pygame.locals import *
+
+from .GameChild import GameChild
+from .Input import Input
+
+class Delegate(GameChild):
+
+    def __init__(self, game):
+        GameChild.__init__(self, game)
+        self.subscribers = collections.OrderedDict()
+        self.load_configuration()
+        self.disable()
+        self.cancelling_propagation = False
+
+    def load_configuration(self):
+        config = self.get_configuration("event")
+        self.cancel_flag_key = config["cancel-flag-key"]
+        self.command_key = config["command-key"]
+        self.command_event_id = config["command-id-offset"] + \
+                                globals()[config["user-event-id"]]
+
+    def disable(self):
+        self.enabled = False
+
+    def enable(self):
+        self.enabled = True
+        self.interpolator = self.get_game().interpolator
+
+    def dispatch(self):
+        if self.enabled:
+            subscribers = self.subscribers
+            for evt in get():
+                kind = evt.type
+                if kind in subscribers:
+                    for subscriber in subscribers[kind]:
+                        if not self.interpolator.is_gui_active() or \
+                               hasattr(subscriber, "im_class") and \
+                               (subscriber.im_class == Input or \
+                                subscriber.im_class == \
+                                self.interpolator.gui.__class__):
+                            self.print_debug("Passing %s to %s" % (evt,
+                                                                   subscriber))
+                            if not self.cancelling_propagation:
+                                subscriber(evt)
+                            else:
+                                self.cancelling_propagation = False
+                                break
+        else:
+            pump()
+
+    def cancel_propagation(self):
+        self.cancelling_propagation = True
+
+    def add_subscriber(self, callback, kind=None):
+        self.print_debug("Subscribing %s to %s" % (callback, kind))
+        if kind is None:
+            kind = self.command_event_id
+        subscribers = self.subscribers
+        if kind not in subscribers:
+            subscribers[kind] = list()
+        subscribers[kind].append(callback)
+
+    def is_command(self, event):
+        return event.type == self.command_event_id
+
+    def remove_subscriber(self, callback, kind=None):
+        if kind is None:
+            kind = self.command_event_id
+        self.subscribers[kind].remove(callback)
+
+    def compare(self, evt, commands=None, cancel=False, **attributes):
+        if self.is_command(evt):
+            self.add_cancel_flag_to_attributes(attributes, cancel)
+        if commands is not None and not isinstance(commands, list):
+            commands = [commands]
+        if commands is not None:
+            if not self.command_in_list(evt, commands):
+                return False
+        return all(key in evt.dict and evt.dict[key] == value for \
+                   key, value in attributes.items())
+
+    def add_cancel_flag_to_attributes(self, attributes, cancel):
+        attributes[self.cancel_flag_key] = cancel
+
+    def command_in_list(self, evt, commands):
+        return self.get_command_attribute(evt) in commands
+
+    def get_command_attribute(self, evt):
+        return (self.command_key in evt.dict) and evt.dict[self.command_key]
+
+    def post(self, command=None, cancel=False, **attributes):
+        attributes[self.command_key] = command
+        self.add_cancel_flag_to_attributes(attributes, cancel)
+        post(Event(self.command_event_id, attributes))
diff --git a/camera_views/lib/pgfw/pgfw/Display.py b/camera_views/lib/pgfw/pgfw/Display.py
new file mode 100644 (file)
index 0000000..d92300c
--- /dev/null
@@ -0,0 +1,95 @@
+from os import environ
+from sys import maxsize, platform 
+
+from pygame import display, image, mouse
+from pygame.locals import *
+
+from .GameChild import *
+
+class Display(GameChild):
+
+    def __init__(self, game):
+        GameChild.__init__(self, game)
+        self.delegate = self.get_delegate()
+        self.load_configuration()
+        self.align_window()
+        self.init_screen()
+        self.set_caption()
+        self.set_icon()
+        self.set_mouse_visibility()
+        self.subscribe(self.toggle_fullscreen)
+
+    def load_configuration(self):
+        config = self.get_configuration("display")
+        self.centered = config["centered"]
+        self.fullscreen_enabled = config["fullscreen"]
+        self.caption = config["caption"]
+        self.windowed_flag = config["windowed-flag"]
+        self.icon_path = self.get_resource("display", "icon-path")
+        self.mouse_visibility = self.get_configuration("mouse", "visible")
+
+    def align_window(self):
+        if self.centered:
+            environ["SDL_VIDEO_CENTERED"] = "1"
+
+    def init_screen(self):
+        # flags = 0
+        full = False
+        if self.fullscreen_requested():
+            # flags = -abs(FULLSCREEN)
+            full = True
+        self.set_screen(0x0, fs=full)
+
+    def fullscreen_requested(self):
+        return not self.check_command_line(self.windowed_flag) and \
+               self.fullscreen_enabled
+
+    def set_screen(self, flags=0x0, dimensions=None, fs=False):
+        self.dimensions_changed = dimensions is not None
+        if dimensions is None:
+            if display.get_surface():
+                dimensions = display.get_surface().get_size()
+            else:
+                dimensions = self.get_configuration("display", "dimensions")
+        if fs:
+            self.screen = display.set_mode(dimensions, flags | -0x80000000)
+        else:
+            self.screen = display.set_mode(dimensions, flags)
+        if self.dimensions_changed:
+            interpolator = self.get_game().interpolator
+            if interpolator.gui_enabled:
+                interpolator.gui.rearrange()
+
+    def set_caption(self):
+        display.set_caption(self.caption)
+
+    def set_icon(self):
+        if self.icon_path:
+            display.set_icon(image.load(self.icon_path).convert_alpha())
+
+    def set_mouse_visibility(self, visibility=None):
+        if visibility is None:
+            visibility = self.mouse_visibility
+        return mouse.set_visible(visibility)
+
+    def get_screen(self):
+        return self.screen
+
+    def get_size(self):
+        return self.screen.get_size()
+
+    def toggle_fullscreen(self, event):
+        if self.delegate.compare(event, "toggle-fullscreen"):
+            screen = self.screen
+            cpy = screen.convert()
+            flags = self.screen.get_flags()
+            if flags & 0x80000000:
+                full = False
+                if not platform == "win32" and maxsize >> 33:
+                    flags ^= 0x80000000
+                else:
+                    flags ^= -0x80000000
+            else:
+                full = True
+            self.set_screen(flags, fs=full)
+            screen.blit(cpy, (0, 0))
diff --git a/camera_views/lib/pgfw/pgfw/Game.py b/camera_views/lib/pgfw/pgfw/Game.py
new file mode 100644 (file)
index 0000000..4231fb0
--- /dev/null
@@ -0,0 +1,124 @@
+import os, pygame, sys
+from pygame.locals import *
+
+from .Animation import Animation
+from .Mainloop import Mainloop
+from .Audio import Audio
+from .Display import Display
+from .Configuration import Configuration
+from .Delegate import Delegate
+from .Input import Input
+from .ScreenGrabber import ScreenGrabber
+from .Profile import Profile
+from .VideoRecorder import VideoRecorder
+from .Interpolator import Interpolator
+from .TimeFilter import TimeFilter
+from .Sprite import Sprite
+from .confirm_quit_message import CONFIRM_QUIT_MESSAGE
+from .extension import *
+
+class Game(Animation):
+
+    resource_path = None
+
+    def __init__(self, config_rel_path=None, type_declarations=None):
+        self.confirming_quit = False
+        self.profile = Profile(self)
+        self.time_filter = TimeFilter(self)
+        Animation.__init__(self, self)
+        self.print_debug(pygame.version.ver)
+        print(sys.version_info)
+        self.config_rel_path = config_rel_path
+        self.type_declarations = type_declarations
+        self.set_configuration()
+        if self.get_configuration("display", "use-framebuffer"):
+            os.putenv("SDL_FBDEV", "/dev/fb0")
+            os.putenv("SDL_VIDEODRIVER", "fbcon")
+            os.putenv("SDL_NOMOUSE", "1")
+        pygame.init()
+        self.set_children()
+        self.register(self.get_input().unsuppress)
+        self.delegate.enable()
+
+    def set_configuration(self):
+        self.configuration = Configuration(self.config_rel_path,
+                                           self.resource_path,
+                                           self.type_declarations)
+
+    def set_children(self):
+        self.delegate = Delegate(self)
+        self.subscribe(self.end, QUIT)
+        self.subscribe(self.end)
+        self.display = Display(self)
+        self.mainloop = Mainloop(self)
+        self.input = Input(self)
+        self.audio = Audio(self)
+        self.screen_grabber = ScreenGrabber(self)
+        self.video_recorder = VideoRecorder(self)
+        self.interpolator = Interpolator(self)
+
+    def frame(self):
+        self.time_filter.update()
+        self.delegate.dispatch()
+        if not self.interpolator.is_gui_active() and not self.get_audio().is_audio_panel_active():
+            Animation.update(self)
+            if self.confirming_quit:
+                self.time_filter.close()
+                self.draw_confirm_quit_dialog()
+            else:
+                self.time_filter.open()
+                self.update()
+        elif self.interpolator.is_gui_active():
+            self.interpolator.gui.update()
+        self.audio.update()
+        if self.video_recorder.requested:
+            self.video_recorder.update()
+
+    def draw_confirm_quit_dialog(self):
+        dialog = Sprite(self)
+        dialog.add_frame(self.get_confirm_quit_surface())
+        dialog.location.center = self.get_display_surface().get_rect().center
+        dialog.update()
+
+    def get_confirm_quit_surface(self):
+        lines = CONFIRM_QUIT_MESSAGE.strip().split("\n")
+        w, h = len(lines[0]), len(lines)
+        text = pygame.Surface((w, h), SRCALPHA)
+        for y, line in enumerate(lines):
+            for x, char in enumerate(line.strip()):
+                if char == "#":
+                    text.set_at((x, y), (0, 0, 0))
+        text = pygame.transform.scale2x(text)
+        text = pygame.transform.scale2x(text)
+        return get_boxed_surface(
+            text, background=(255, 255, 255), border=(0, 0, 0), border_width=24, padding=12)
+
+    def run(self):
+        self.mainloop.run()
+
+    def update(self):
+        pass
+
+    def blit(self, source, destination, area=None, special_flags=0):
+        self.get_screen().blit(source, destination, area, special_flags)
+
+    def get_rect(self):
+        return self.get_screen().get_rect()
+
+    def suppress_input_temporarily(self, length=700):
+        self.get_input().suppress()
+        self.play(self.get_input().unsuppress, delay=length, play_once=True)
+
+    def end(self, event):
+        if event.type == QUIT or self.delegate.compare(event, "quit"):
+            if self.confirming_quit or not self.get_configuration("input", "confirm-quit"):
+                self.mainloop.stop()
+                self.profile.end()
+            else:
+                print("press quit again to confirm or press any other button to cancel...")
+                self.confirming_quit = True
+        elif self.delegate.compare(event, "any"):
+            if self.confirming_quit and self.delegate.compare(event, "any"):
+                print("cancelled quit...")
+                self.confirming_quit = False
+                self.delegate.cancel_propagation()
diff --git a/camera_views/lib/pgfw/pgfw/GameChild.py b/camera_views/lib/pgfw/pgfw/GameChild.py
new file mode 100644 (file)
index 0000000..8043919
--- /dev/null
@@ -0,0 +1,96 @@
+from os.path import exists, join, basename, normpath, abspath, commonprefix, sep
+from sys import argv, version_info
+
+from pygame import mixer, event, time
+from pygame.locals import *
+
+if version_info[0] >= 3:
+    from . import Game
+else:
+    import Game
+
+class GameChild:
+
+    def __init__(self, parent=None):
+        self.parent = parent
+        self.game = self.get_game()
+
+    def get_game(self):
+        current = self
+        while not isinstance(current, Game.Game):
+            current = current.parent
+        return current
+
+    def get_configuration(self, section=None, option=None, linebreaks=True):
+        config = self.game.configuration
+        if option is None and section is None:
+            return config
+        elif option and section:
+            if config.has_option(section, option):
+                rvalue = config.get(section, option)
+                if not linebreaks and isinstance(rvalue, str):
+                    rvalue = rvalue.replace("\n", " ")
+                return rvalue
+        elif option is None:
+            if config.has_section(section):
+                return config.get_section(section)
+            else:
+                return {}
+
+    def get_input(self):
+        return self.game.input
+
+    def get_screen(self):
+        return self.game.display.get_screen()
+
+    def get_display_surface(self):
+        current = self
+        attribute = "display_surface"
+        while not isinstance(current, Game.Game):
+            if hasattr(current, attribute):
+                return getattr(current, attribute)
+            current = current.parent
+        return current.display.get_screen()
+
+    def get_audio(self):
+        return self.game.audio
+
+    def get_delegate(self):
+        return self.game.delegate
+
+    def get_resource(self, path_or_section, option=None):
+        config = self.get_configuration()
+        rel_path = path_or_section
+        if option is not None:
+            rel_path = config.get(path_or_section, option)
+        if rel_path:
+            for root in config.get("setup", "resource-search-path"):
+                if self.is_shared_mode() and not self.is_absolute_path(root):
+                    continue
+                path = join(root, rel_path)
+                if exists(path):
+                    return normpath(path)
+        self.print_debug("Couldn't find resource: {0} {1}".format(
+            path_or_section, option))
+
+    def is_shared_mode(self):
+        return self.check_command_line("s")
+
+    def check_command_line(self, flag):
+        return "-" + flag in argv
+
+    def print_debug(self, statement):
+        if self.is_debug_mode():
+            print(statement)
+
+    def is_debug_mode(self):
+        return self.check_command_line("d")
+
+    def is_absolute_path(self, path):
+        return normpath(path) == abspath(path)
+
+    def subscribe(self, callback, kind=None):
+        self.game.delegate.add_subscriber(callback, kind)
+
+    def unsubscribe(self, callback, kind=None):
+        self.game.delegate.remove_subscriber(callback, kind)
diff --git a/camera_views/lib/pgfw/pgfw/Gradient.py b/camera_views/lib/pgfw/pgfw/Gradient.py
new file mode 100644 (file)
index 0000000..4ad1e9c
--- /dev/null
@@ -0,0 +1,30 @@
+from .GameChild import GameChild
+from .extension import get_random_hsla_color, get_range_steps, get_hsla_color
+
+class Gradient(GameChild):
+
+    def __init__(self, parent, rect, granularity, start=None, end=None):
+        GameChild.__init__(self, parent)
+        if start is None:
+            start = get_random_hsla_color()
+        self.start = start
+        if end is None:
+            end = get_random_hsla_color()
+        self.end = end
+        self.rect = rect
+        self.granularity = granularity
+
+    def update(self):
+        ds = self.get_display_surface()
+        y = self.rect.top
+        hues = list(get_range_steps(self.start.hsla[0], self.end.hsla[0],
+                                    self.granularity))
+        saturations = list(get_range_steps(self.start.hsla[1], self.end.hsla[1],
+                                           self.granularity))
+        lightnesses = list(get_range_steps(self.start.hsla[2], self.end.hsla[2],
+                                           self.granularity))
+        for index in range(len(hues)):
+            h = self.rect.h // self.granularity
+            ds.fill(get_hsla_color(int(hues[index]), int(saturations[index]), int(lightnesses[index])),
+                    (self.rect.left, y, self.rect.w, h))
+            y += h
diff --git a/camera_views/lib/pgfw/pgfw/Input.py b/camera_views/lib/pgfw/pgfw/Input.py
new file mode 100644 (file)
index 0000000..48e4d09
--- /dev/null
@@ -0,0 +1,244 @@
+from time import time as get_secs
+
+from pygame import joystick as joy
+from pygame.key import get_pressed, get_mods
+from pygame.locals import *
+
+from .GameChild import *
+
+class Input(GameChild):
+
+    def __init__(self, game):
+        GameChild.__init__(self, game)
+        self.last_mouse_down_left = None
+        self.axes_cancelled = {"up": True, "right": True, "down": True, "left": True}
+        self.last_command_post_time = get_secs()
+        self.joystick = Joystick()
+        self.delegate = self.get_delegate()
+        self.load_configuration()
+        self.set_any_press_ignore_list()
+        self.unsuppress()
+        self.unsuppress_any_on_mods()
+        self.subscribe_to_events()
+        self.build_key_map()
+        self.build_joy_button_map()
+
+    def load_configuration(self):
+        self.release_suffix = self.get_configuration("input", "release-suffix")
+        self.key_commands = self.get_configuration().items("keys")
+        self.double_click_time_limit = self.get_configuration(
+            "mouse", "double-click-time-limit")
+
+    def set_any_press_ignore_list(self):
+        self.any_press_ignored = set(["capture-screen", "toggle-fullscreen",
+                                      "reset-game", "record-video", "quit",
+                                      "toggle-interpolator", "toggle-audio-panel",
+                                      "volume-down", "volume-up", "volume-mute"])
+        self.any_press_ignored_keys = set()
+
+    def unsuppress(self):
+        self.suppressed = False
+
+    def unsuppress_any_on_mods(self):
+        self.suppressed_any_key_on_mods = False
+
+    def suppress_any_key_on_mods(self):
+        self.suppressed_any_key_on_mods = True
+
+    def subscribe_to_events(self):
+        self.subscribe(self.translate_key, KEYDOWN)
+        self.subscribe(self.translate_key, KEYUP)
+        self.subscribe(self.translate_joy_button, JOYBUTTONDOWN)
+        self.subscribe(self.translate_joy_button, JOYBUTTONUP)
+        self.subscribe(self.translate_axis_motion, JOYAXISMOTION)
+        self.subscribe(self.translate_mouse_input, MOUSEBUTTONDOWN)
+        self.subscribe(self.translate_mouse_input, MOUSEBUTTONUP)
+
+    def build_key_map(self):
+        key_map = {}
+        for command, keys in self.key_commands:
+            key_map[command] = []
+            if type(keys) == str:
+                keys = [keys]
+            for key in keys:
+                key_map[command].append(globals()[key])
+        self.key_map = key_map
+
+    def build_joy_button_map(self):
+        self.joy_button_map = self.get_configuration("joy")
+
+    def suppress(self):
+        self.suppressed = True
+
+    def translate_key(self, event):
+        if not self.suppressed:
+            cancel = event.type == KEYUP
+            posted = None
+            key = event.key
+            for cmd, keys in self.key_map.items():
+                if key in keys:
+                    self.post_command(cmd, cancel=cancel)
+                    posted = cmd
+            if (not posted or posted not in self.any_press_ignored) and \
+                   key not in self.any_press_ignored_keys and \
+                   (not self.suppressed_any_key_on_mods or \
+                    (not get_mods() & KMOD_CTRL and not get_mods() & KMOD_ALT)):
+                self.post_any_command(key, cancel)
+
+    def post_command(self, cmd, **attributes):
+        self.delegate.post(cmd, **attributes)
+
+    def post_any_command(self, id, cancel=False):
+        self.post_command("any", id=id, cancel=cancel)
+
+    def translate_joy_button(self, event):
+        if not self.suppressed:
+            cancel = event.type == JOYBUTTONUP
+            posted = None
+            for command, button in self.joy_button_map.items():
+                if int(button) == event.button:
+                    self.post_command(command, cancel=cancel)
+                    posted = command
+            if not posted or posted not in self.any_press_ignored:
+                self.post_any_command(event.button, cancel)
+
+    def translate_axis_motion(self, event):
+        if not self.suppressed and not self.check_command_line("-disable-joy-axis"):
+            axis = event.axis
+            value = event.value
+            single_xy = self.get_configuration("joy", "single-xy")
+            command = None
+            if -.01 < value < .01:
+                for direction in "up", "right", "down", "left":
+                    if not self.axes_cancelled[direction]:
+                        self.post_command(direction, cancel=True)
+                        if direction not in self.any_press_ignored:
+                            self.post_any_command(direction, True)
+                        self.axes_cancelled[direction] = True
+                    if single_xy:
+                        if direction == "up" and self.joystick.is_direction_pressed(Joystick.down):
+                            command = "down"
+                        elif direction == "down" and self.joystick.is_direction_pressed(Joystick.up):
+                            command = "up"
+                        elif direction == "right" and self.joystick.is_direction_pressed(Joystick.left):
+                            command = "left"
+                        elif direction == "left" and self.joystick.is_direction_pressed(Joystick.right):
+                            command = "right"
+            else:
+                if axis == self.get_configuration("joy", "vertical-axis"):
+                    if value < 0:
+                        if not single_xy or not self.joystick.is_direction_pressed(Joystick.down):
+                            command = "up"
+                    elif value > 0:
+                        if not single_xy or not self.joystick.is_direction_pressed(Joystick.up):
+                            command = "down"
+                elif axis == self.get_configuration("joy", "horizontal-axis"):
+                    if value > 0:
+                        if not single_xy or not self.joystick.is_direction_pressed(Joystick.left):
+                            command = "right"
+                    elif value < 0:
+                        if not single_xy or not self.joystick.is_direction_pressed(Joystick.right):
+                            command = "left"
+            if command is not None:
+                delay = self.get_configuration("joy", "delay-axis")
+                secs = get_secs()
+                if not delay or secs - self.last_command_post_time > delay:
+                    self.post_command(command)
+                    if command not in self.any_press_ignored:
+                        self.post_any_command(command)
+                    self.axes_cancelled[command] = False
+                    self.last_command_post_time = secs
+
+    def is_command_active(self, command):
+        if not self.suppressed:
+            if self.is_key_pressed(command):
+                return True
+            joystick = self.joystick
+            joy_map = self.joy_button_map
+            if command in joy_map and joystick.get_button(joy_map[command]):
+                return True
+            if command == "up":
+                return joystick.is_direction_pressed(Joystick.up)
+            elif command == "right":
+                return joystick.is_direction_pressed(Joystick.right)
+            elif command == "down":
+                return joystick.is_direction_pressed(Joystick.down)
+            elif command == "left":
+                return joystick.is_direction_pressed(Joystick.left)
+
+    def is_key_pressed(self, command):
+        poll = get_pressed()
+        for key in self.key_map[command]:
+            if poll[key]:
+                return True
+
+    def translate_mouse_input(self, event):
+        button = event.button
+        pos = event.pos
+        post = self.post_command
+        if event.type == MOUSEBUTTONDOWN:
+            if button == 1:
+                last = self.last_mouse_down_left
+                if last:
+                    limit = self.double_click_time_limit
+                    if get_secs() - last < limit:
+                        post("mouse-double-click-left", pos=pos)
+                last = get_secs()
+                self.last_mouse_down_left = last
+            if "mouse" not in self.any_press_ignored:
+                self.post_any_command(event.button)
+        if event.type == MOUSEBUTTONUP:
+            if "mouse" not in self.any_press_ignored:
+                self.post_any_command(event.button, True)
+
+    def get_axes(self):
+        axes = {}
+        for direction in "up", "right", "down", "left":
+            axes[direction] = self.is_command_active(direction)
+        return axes
+
+    def register_any_press_ignore(self, *args, **attributes):
+        self.any_press_ignored.update(args)
+        self.any_press_ignored_keys.update(self.extract_keys(attributes))
+
+    def extract_keys(self, attributes):
+        keys = []
+        if "keys" in attributes:
+            keys = attributes["keys"]
+            if type(keys) == int:
+                keys = [keys]
+        return keys
+
+    def unregister_any_press_ignore(self, *args, **attributes):
+        self.any_press_ignored.difference_update(args)
+        self.any_press_ignored_keys.difference_update(
+            self.extract_keys(attributes))
+
+
+class Joystick:
+
+    (up, right, down, left) = range(4)
+
+    def __init__(self):
+        js = None
+        if joy.get_count() > 0:
+            js = joy.Joystick(0)
+            js.init()
+        self.js = js
+
+    def is_direction_pressed(self, direction):
+        js = self.js
+        if not js or direction > 4:
+            return False
+        if direction == 0:
+            return js.get_axis(1) < 0
+        elif direction == 1:
+            return js.get_axis(0) > 0
+        elif direction == 2:
+            return js.get_axis(1) > 0
+        elif direction == 3:
+            return js.get_axis(0) < 0
+
+    def get_button(self, id):
+        if self.js:
+            return self.js.get_button(id)
diff --git a/camera_views/lib/pgfw/pgfw/Interpolator.py b/camera_views/lib/pgfw/pgfw/Interpolator.py
new file mode 100644 (file)
index 0000000..0ce39e9
--- /dev/null
@@ -0,0 +1,734 @@
+from re import match
+from os.path import join
+from tempfile import gettempdir
+
+from pygame import Surface
+from pygame.font import Font
+from pygame.draw import aaline
+from pygame.locals import *
+
+from .GameChild import GameChild
+from .Sprite import Sprite
+from .Animation import Animation
+
+class Interpolator(list, GameChild):
+
+    def __init__(self, parent):
+        GameChild.__init__(self, parent)
+        self.set_nodesets()
+        self.gui_enabled = self.check_command_line("-interpolator")
+        if self.gui_enabled:
+            self.gui = GUI(self)
+
+    def set_nodesets(self):
+        config = self.get_configuration()
+        if config.has_section("interpolate"):
+            for name, value in config.get_section("interpolate").items():
+                self.add_nodeset(name, value)
+
+    def add_nodeset(self, name, value, method=None):
+        self.append(Nodeset(name, value, method))
+        return len(self) - 1
+
+    def is_gui_active(self):
+        return self.gui_enabled and self.gui.active
+
+    def get_nodeset(self, name):
+        for nodeset in self:
+            if nodeset.name == name:
+                return nodeset
+
+    def remove(self, outgoing):
+        for ii, nodeset in enumerate(self):
+            if nodeset.name == outgoing.name:
+                self.pop(ii)
+                break
+
+
+class Nodeset(list):
+
+    LINEAR, CUBIC = range(2)
+
+    def __init__(self, name, nodes, method=None):
+        list.__init__(self, [])
+        self.name = name
+        if isinstance(nodes, str):
+            self.parse_raw(nodes)
+        else:
+            self.interpolation_method = method
+            self.parse_list(nodes)
+        self.set_splines()
+
+    def parse_raw(self, raw):
+        raw = raw.strip()
+        if raw[0].upper() == "L":
+            self.set_interpolation_method(self.LINEAR, False)
+        else:
+            self.set_interpolation_method(self.CUBIC, False)
+        for node in raw[1:].strip().split(","):
+            self.add_node(list(map(float, node.strip().split())), False)
+
+    def set_interpolation_method(self, method, refresh=True):
+        self.interpolation_method = method
+        if refresh:
+            self.set_splines()
+
+    def add_node(self, coordinates, refresh=True):
+        x = coordinates[0]
+        inserted = False
+        index = 0
+        for ii, node in enumerate(self):
+            if x < node.x:
+                self.insert(ii, Node(coordinates))
+                inserted = True
+                index = ii
+                break
+            elif x == node.x:
+                return None
+        if not inserted:
+            self.append(Node(coordinates))
+            index = len(self) - 1
+        if refresh:
+            self.set_splines()
+        return index
+
+    def parse_list(self, nodes):
+        for node in nodes:
+            self.add_node(node)
+
+    def set_splines(self):
+        if self.interpolation_method == self.LINEAR:
+            self.set_linear_splines()
+        else:
+            self.set_cubic_splines()
+
+    def set_linear_splines(self):
+        self.splines = splines = []
+        for ii in range(len(self) - 1):
+            x1, y1, x2, y2 = self[ii] + self[ii + 1]
+            m = float(y2 - y1) / (x2 - x1)
+            splines.append(LinearSpline(x1, y1, m))
+
+    def set_cubic_splines(self):
+        n = len(self) - 1
+        a = [node.y for node in self]
+        b = [None] * n
+        d = [None] * n
+        h = [self[ii + 1].x - self[ii].x for ii in range(n)]
+        alpha = [None] + [(3.0 / h[ii]) * (a[ii + 1] - a[ii]) - \
+                          (3.0 / h[ii - 1]) * (a[ii] - a[ii - 1]) \
+                          for ii in range(1, n)]
+        c = [None] * (n + 1)
+        l = [None] * (n + 1)
+        u = [None] * (n + 1)
+        z = [None] * (n + 1)
+        l[0] = 1
+        u[0] = z[0] = 0
+        for ii in range(1, n):
+            l[ii] = 2 * (self[ii + 1].x - self[ii - 1].x) - \
+                    h[ii - 1] * u[ii - 1]
+            u[ii] = h[ii] / l[ii]
+            z[ii] = (alpha[ii] - h[ii - 1] * z[ii - 1]) / l[ii]
+        l[n] = 1
+        z[n] = c[n] = 0
+        for jj in range(n - 1, -1, -1):
+            c[jj] = z[jj] - u[jj] * c[jj + 1]
+            b[jj] = (a[jj + 1] - a[jj]) / h[jj] - \
+                    (h[jj] * (c[jj + 1] + 2 * c[jj])) / 3
+            d[jj] = (c[jj + 1] - c[jj]) / (3 * h[jj])
+        self.splines = [CubicSpline(self[ii].x, a[ii], b[ii], c[ii],
+                                    d[ii]) for ii in range(n)]
+
+    def get_y(self, t, loop=False, reverse=False, natural=False):
+        if loop or reverse:
+            if reverse and int(t) / int(self[-1].x) % 2:
+                t = self[-1].x - t
+            t %= self[-1].x
+        elif not natural:
+            if t < self[0].x:
+                t = self[0].x
+            elif t > self[-1].x:
+                t = self[-1].x
+        splines = self.splines
+        for ii in range(len(splines) - 1):
+            if t < splines[ii + 1].x:
+                return splines[ii].get_y(t)
+        return splines[-1].get_y(t)
+
+    def remove(self, node, refresh=True):
+        list.remove(self, node)
+        if refresh:
+            self.set_splines()
+
+    def resize(self, left, length, refresh=True):
+        old_left = self[0].x
+        old_length = self.get_length()
+        for node in self:
+            node.x = left + length * (node.x - old_left) / old_length
+        if refresh:
+            self.set_splines()
+
+    def get_length(self):
+        return self[-1].x - self[0].x
+
+
+class Node(list):
+
+    def __init__(self, coordinates):
+        list.__init__(self, coordinates)
+
+    def __getattr__(self, name):
+        if name == "x":
+            return self[0]
+        elif name == "y":
+            return self[1]
+        return list.__get__(self, name)
+
+    def __setattr__(self, name, value):
+        if name == "x":
+            list.__setitem__(self, 0, value)
+        elif name == "y":
+            list.__setitem__(self, 1, value)
+        else:
+            list.__setattr__(self, name, value)
+
+
+class Spline:
+
+    def __init__(self, x):
+        self.x = x
+
+
+class CubicSpline(Spline):
+
+    def __init__(self, x, a, b, c, d):
+        Spline.__init__(self, x)
+        self.a = a
+        self.b = b
+        self.c = c
+        self.d = d
+
+    def get_y(self, t):
+        x = self.x
+        return self.a + self.b * (t - x) + self.c * (t - x) ** 2 + self.d * \
+               (t - x) ** 3
+
+
+class LinearSpline(Spline):
+
+    def __init__(self, x, y, m):
+        Spline.__init__(self, x)
+        self.y = y
+        self.m = m
+
+    def get_y(self, t):
+        return self.m * (t - self.x) + self.y
+
+
+class GUI(Animation):
+
+    B_DUPLICATE, B_WRITE, B_DELETE, B_LINEAR, B_CUBIC, B_SPLIT = range(6)
+    S_NONE, S_LEFT, S_RIGHT = range(3)
+
+    def __init__(self, parent):
+        Animation.__init__(self, parent, unfiltered=True)
+        self.audio = self.get_audio()
+        self.display = self.get_game().display
+        self.display_surface = self.get_display_surface()
+        self.time_filter = self.get_game().time_filter
+        self.delegate = self.get_delegate()
+        self.split = self.S_NONE
+        self.success_indicator_active = True
+        self.success_indicator_blink_count = 0
+        self.load_configuration()
+        self.font = Font(None, self.label_size)
+        self.prompt = Prompt(self)
+        self.set_temporary_file()
+        self.set_background()
+        self.set_success_indicator()
+        self.set_plot_rect()
+        self.set_marker_frame()
+        self.set_buttons()
+        self.active = False
+        self.set_nodeset_index()
+        self.set_y_range()
+        self.set_markers()
+        self.subscribe(self.respond_to_command)
+        self.subscribe(self.respond_to_mouse_down, MOUSEBUTTONDOWN)
+        self.subscribe(self.respond_to_key, KEYDOWN)
+        self.register(self.show_success_indicator, interval=100)
+        self.register(self.save_temporary_file, interval=10000)
+        self.play(self.save_temporary_file)
+
+    def load_configuration(self):
+        config = self.get_configuration("interpolator-gui")
+        self.label_size = config["label-size"]
+        self.axis_label_count = config["axis-label-count"]
+        self.margin = config["margin"]
+        self.curve_color = config["curve-color"]
+        self.marker_size = config["marker-size"]
+        self.marker_color = config["marker-color"]
+        self.label_precision = config["label-precision"]
+        self.template_nodeset = config["template-nodeset"]
+        self.template_nodeset_name = config["template-nodeset-name"]
+        self.flat_y_range = config["flat-y-range"]
+
+    def set_temporary_file(self):
+        self.temporary_file = open(join(gettempdir(), "pgfw-config"), "w")
+
+    def set_background(self):
+        surface = Surface(self.display_surface.get_size())
+        surface.fill((0, 0, 0))
+        self.background = surface
+
+    def set_success_indicator(self):
+        surface = Surface((10, 10))
+        surface.fill((0, 255, 0))
+        rect = surface.get_rect()
+        rect.topleft = self.display_surface.get_rect().topleft
+        self.success_indicator, self.success_indicator_rect = surface, rect
+
+    def set_plot_rect(self):
+        margin = self.margin
+        self.plot_rect = self.display_surface.get_rect().inflate(-margin,
+                                                                 -margin)
+
+    def set_marker_frame(self):
+        size = self.marker_size
+        surface = Surface((size, size))
+        transparent_color = (255, 0, 255)
+        surface.fill(transparent_color)
+        surface.set_colorkey(transparent_color)
+        line_color = self.marker_color
+        aaline(surface, line_color, (0, 0), (size - 1, size - 1))
+        aaline(surface, line_color, (0, size - 1), (size - 1, 0))
+        self.marker_frame = surface
+
+    def set_buttons(self):
+        self.buttons = buttons = []
+        text = "Duplicate", "Write", "Delete", "Linear", "Cubic", "Split: No"
+        x = 0
+        for instruction in text:
+            buttons.append(Button(self, instruction, x))
+            x += buttons[-1].location.w + 10
+
+    def set_nodeset_index(self, increment=None, index=None):
+        parent = self.parent
+        if index is None:
+            if not increment:
+                index = 0
+            else:
+                index = self.nodeset_index + increment
+                limit = len(parent) - 1
+                if index > limit:
+                    index = 0
+                elif index < 0:
+                    index = limit
+        self.nodeset_index = index
+        self.set_nodeset_label()
+
+    def set_nodeset_label(self):
+        surface = self.font.render(self.get_nodeset().name, True, (0, 0, 0),
+                                   (255, 255, 255))
+        rect = surface.get_rect()
+        rect.bottomright = self.display_surface.get_rect().bottomright
+        self.nodeset_label, self.nodeset_label_rect = surface, rect
+
+    def get_nodeset(self):
+        if not len(self.parent):
+            self.parent.add_nodeset(self.template_nodeset_name,
+                                    self.template_nodeset)
+            self.set_nodeset_index(0)
+        return self.parent[self.nodeset_index]
+
+    def set_y_range(self):
+        width = self.plot_rect.w
+        nodeset = self.get_nodeset()
+        self.y_range = y_range = [nodeset[0].y, nodeset[-1].y]
+        x = 0
+        while x < width:
+            y = nodeset.get_y(self.get_function_coordinates(x)[0])
+            if y < y_range[0]:
+                y_range[0] = y
+            elif y > y_range[1]:
+                y_range[1] = y
+            x += width * .01
+        if y_range[1] - y_range[0] == 0:
+            y_range[1] += self.flat_y_range
+        if self.split:
+            self.adjust_for_split(y_range, nodeset)
+        self.set_axis_labels()
+
+    def get_function_coordinates(self, xp=0, yp=0):
+        nodeset = self.get_nodeset()
+        x_min, x_max, (y_min, y_max) = nodeset[0].x, nodeset[-1].x, self.y_range
+        rect = self.plot_rect
+        x = float(xp) / (rect.right - rect.left) * (x_max - x_min) + x_min
+        y = float(yp) / (rect.bottom - rect.top) * (y_min - y_max) + y_max
+        return x, y
+
+    def adjust_for_split(self, y_range, nodeset):
+        middle = nodeset[0].y if self.split == self.S_LEFT else nodeset[-1].y
+        below, above = middle - y_range[0], y_range[1] - middle
+        if below > above:
+            y_range[1] += below - above
+        else:
+            y_range[0] -= above - below
+
+    def set_axis_labels(self):
+        self.axis_labels = labels = []
+        nodeset, formatted, render, rect, yr = (self.get_nodeset(),
+                                                self.get_formatted_measure,
+                                                self.font.render,
+                                                self.plot_rect, self.y_range)
+        for ii, node in enumerate(nodeset[0::len(nodeset) - 1]):
+            xs = render(formatted(node.x), True, (0, 0, 0), (255, 255, 255))
+            xsr = xs.get_rect()
+            xsr.top = rect.bottom
+            if not ii:
+                xsr.left = rect.left
+            else:
+                xsr.right = rect.right
+            ys = render(formatted(yr[ii]), True, (0, 0, 0), (255, 255, 255))
+            ysr = ys.get_rect()
+            ysr.right = rect.left
+            if not ii:
+                ysr.bottom = rect.bottom
+            else:
+                ysr.top = rect.top
+            labels.append(((xs, xsr), (ys, ysr)))
+
+    def get_formatted_measure(self, measure):
+        return "%s" % float(("%." + str(self.label_precision) + "g") % measure)
+
+    def deactivate(self):
+        self.active = False
+        self.time_filter.open()
+        # self.audio.muted = self.saved_mute_state
+        self.display.set_mouse_visibility(self.saved_mouse_state)
+
+    def respond_to_command(self, event):
+        compare = self.delegate.compare
+        if compare(event, "toggle-interpolator"):
+            self.toggle()
+        elif self.active:
+            if compare(event, "reset-game"):
+                self.deactivate()
+            elif compare(event, "quit"):
+                self.get_game().end(event)
+
+    def toggle(self):
+        if self.active:
+            self.deactivate()
+            self.get_game().delegate.post("refresh-nodesets")
+        else:
+            self.activate()
+
+    def activate(self):
+        self.active = True
+        self.time_filter.close()
+        # self.saved_mute_state = self.audio.muted
+        # self.audio.mute()
+        self.draw()
+        self.saved_mouse_state = self.display.set_mouse_visibility(True)
+
+    def respond_to_mouse_down(self, event):
+        redraw = False
+        if self.active and not self.prompt.active:
+            nodeset_rect = self.nodeset_label_rect
+            plot_rect = self.plot_rect
+            if event.button == 1:
+                pos = event.pos
+                if nodeset_rect.collidepoint(pos):
+                    self.set_nodeset_index(1)
+                    redraw = True
+                elif self.axis_labels[0][0][1].collidepoint(pos):
+                    text = "{0} {1}".format(*map(self.get_formatted_measure,
+                                                 self.get_nodeset()[0]))
+                    self.prompt.activate(text, self.resize_nodeset, 0)
+                elif self.axis_labels[1][0][1].collidepoint(pos):
+                    text = "{0} {1}".format(*map(self.get_formatted_measure,
+                                                 self.get_nodeset()[-1]))
+                    self.prompt.activate(text, self.resize_nodeset, -1)
+                else:
+                    bi = self.collide_buttons(pos)
+                    if bi is not None:
+                        if bi == self.B_WRITE:
+                            self.get_configuration().write()
+                            self.play(self.show_success_indicator)
+                        elif bi in (self.B_LINEAR, self.B_CUBIC):
+                            nodeset = self.get_nodeset()
+                            if bi == self.B_LINEAR:
+                                nodeset.set_interpolation_method(Nodeset.LINEAR)
+                            else:
+                                nodeset.set_interpolation_method(Nodeset.CUBIC)
+                            self.store_in_configuration()
+                            redraw = True
+                        elif bi == self.B_DUPLICATE:
+                            self.prompt.activate("", self.add_nodeset)
+                        elif bi == self.B_DELETE and len(self.parent) > 1:
+                            self.parent.remove(self.get_nodeset())
+                            self.set_nodeset_index(1)
+                            self.store_in_configuration()
+                            redraw = True
+                        elif bi == self.B_SPLIT:
+                            self.toggle_split()
+                            redraw = True
+                    elif plot_rect.collidepoint(pos) and \
+                             not self.collide_markers(pos):
+                        xp, yp = pos[0] - plot_rect.left, pos[1] - plot_rect.top
+                        self.get_nodeset().add_node(
+                            self.get_function_coordinates(xp, yp))
+                        self.store_in_configuration()
+                        redraw = True
+            elif event.button == 3:
+                pos = event.pos
+                if nodeset_rect.collidepoint(pos):
+                    self.set_nodeset_index(-1)
+                    redraw = True
+                elif plot_rect.collidepoint(pos):
+                    marker = self.collide_markers(pos)
+                    if marker:
+                        self.get_nodeset().remove(marker.node)
+                        self.store_in_configuration()
+                        redraw = True
+        elif self.active and self.prompt.active and \
+                 not self.prompt.rect.collidepoint(event.pos):
+            self.prompt.deactivate()
+            redraw = True
+        if redraw:
+            self.set_y_range()
+            self.set_markers()
+            self.draw()
+
+    def resize_nodeset(self, text, index):
+        result = match("^\s*(-{,1}\d*\.{,1}\d*)\s+(-{,1}\d*\.{,1}\d*)\s*$",
+                       text)
+        if result:
+            try:
+                nodeset = self.get_nodeset()
+                x, y = map(float, result.group(1, 2))
+                if (index == -1 and x > nodeset[0].x) or \
+                       (index == 0 and x < nodeset[-1].x):
+                    nodeset[index].y = y
+                    if index == -1:
+                        nodeset.resize(nodeset[0].x, x - nodeset[0].x)
+                    else:
+                        nodeset.resize(x, nodeset[-1].x - x)
+                    self.store_in_configuration()
+                    self.set_y_range()
+                    self.set_axis_labels()
+                    self.set_markers()
+                    self.draw()
+                    return True
+            except ValueError:
+                return False
+
+    def collide_buttons(self, pos):
+        for ii, button in enumerate(self.buttons):
+            if button.location.collidepoint(pos):
+                return ii
+
+    def store_in_configuration(self):
+        config = self.get_configuration()
+        section = "interpolate"
+        config.clear_section(section)
+        for nodeset in self.parent:
+            code = "L" if nodeset.interpolation_method == Nodeset.LINEAR else \
+                   "C"
+            for ii, node in enumerate(nodeset):
+                if ii > 0:
+                    code += ","
+                code += " {0} {1}".format(*map(self.get_formatted_measure,
+                                               node))
+            if not config.has_section(section):
+                config.add_section(section)
+            config.set(section, nodeset.name, code)
+
+    def toggle_split(self):
+        self.split += 1
+        if self.split > self.S_RIGHT:
+            self.split = self.S_NONE
+        self.buttons[self.B_SPLIT].set_frame(["Split: No", "Split: L",
+                                              "Split: R"][self.split])
+
+    def add_nodeset(self, name):
+        nodeset = self.get_nodeset()
+        self.set_nodeset_index(index=self.parent.add_nodeset(\
+            name, nodeset, nodeset.interpolation_method))
+        self.store_in_configuration()
+        self.draw()
+        return True
+
+    def collide_markers(self, pos):
+        for marker in self.markers:
+            if marker.location.collidepoint(pos):
+                return marker
+
+    def set_markers(self):
+        self.markers = markers = []
+        for node in self.get_nodeset()[1:-1]:
+            markers.append(Marker(self, node))
+            markers[-1].location.center = self.get_plot_coordinates(*node)
+
+    def get_plot_coordinates(self, x=0, y=0):
+        nodeset = self.get_nodeset()
+        x_min, x_max, (y_min, y_max) = nodeset[0].x, nodeset[-1].x, self.y_range
+        x_ratio = float(x - x_min) / (x_max - x_min)
+        rect = self.plot_rect
+        xp = x_ratio * (rect.right - rect.left) + rect.left
+        y_ratio = float(y - y_min) / (y_max - y_min)
+        yp = rect.bottom - y_ratio * (rect.bottom - rect.top)
+        return xp, yp
+
+    def draw(self):
+        display_surface = self.display_surface
+        display_surface.blit(self.background, (0, 0))
+        display_surface.blit(self.nodeset_label, self.nodeset_label_rect)
+        self.draw_axes()
+        self.draw_function()
+        self.draw_markers()
+        self.draw_buttons()
+
+    def draw_axes(self):
+        display_surface = self.display_surface
+        for xl, yl in self.axis_labels:
+            display_surface.blit(*xl)
+            display_surface.blit(*yl)
+
+    def draw_function(self):
+        rect = self.plot_rect
+        surface = self.display_surface
+        nodeset = self.get_nodeset()
+        step = 1
+        for x in range(rect.left, rect.right + step, step):
+            ii = x - rect.left
+            fx = nodeset.get_y(self.get_function_coordinates(ii)[0])
+            y = self.get_plot_coordinates(y=fx)[1]
+            if ii > 0:
+                aaline(surface, self.curve_color, (x - step, last_y), (x, y))
+            last_y = y
+
+    def draw_markers(self):
+        for marker in self.markers:
+            marker.update()
+
+    def draw_buttons(self):
+        for button in self.buttons:
+            button.update()
+
+    def respond_to_key(self, event):
+        if self.prompt.active:
+            prompt = self.prompt
+            if event.key == K_RETURN:
+                if prompt.callback[0](prompt.text, *prompt.callback[1]):
+                    prompt.deactivate()
+            elif event.key == K_BACKSPACE:
+                prompt.text = prompt.text[:-1]
+                prompt.update()
+                prompt.draw_text()
+            elif (event.unicode.isalnum() or event.unicode.isspace() or \
+                  event.unicode in (".", "-", "_")) and len(prompt.text) < \
+                  prompt.character_limit:
+                prompt.text += event.unicode
+                prompt.update()
+                prompt.draw_text()
+
+    def show_success_indicator(self):
+        self.draw()
+        if self.success_indicator_blink_count > 1:
+            self.success_indicator_blink_count = 0
+            self.halt(self.show_success_indicator)
+        else:
+            if self.success_indicator_active:
+                self.display_surface.blit(self.success_indicator,
+                                          self.success_indicator_rect)
+            if self.success_indicator_active:
+                self.success_indicator_blink_count += 1
+            self.success_indicator_active = not self.success_indicator_active
+
+    def save_temporary_file(self):
+        fp = self.temporary_file
+        fp.seek(0)
+        fp.truncate()
+        self.get_configuration().write(fp)
+
+    def rearrange(self):
+        self.set_background()
+        self.set_success_indicator()
+        self.set_plot_rect()
+        self.set_markers()
+        self.set_nodeset_label()
+        self.set_axis_labels()
+        self.set_buttons()
+        self.prompt.reset()
+
+class Marker(Sprite):
+
+    def __init__(self, parent, node):
+        Sprite.__init__(self, parent)
+        self.add_frame(parent.marker_frame)
+        self.node = node
+
+
+class Button(Sprite):
+
+    def __init__(self, parent, text, left):
+        Sprite.__init__(self, parent)
+        self.set_frame(text)
+        self.location.bottomleft = left, \
+                                   self.get_display_surface().get_rect().bottom
+
+    def set_frame(self, text):
+        self.clear_frames()
+        self.add_frame(self.parent.font.render(text, True, (0, 0, 0),
+                                               (255, 255, 255)))
+
+
+class Prompt(Sprite):
+
+    def __init__(self, parent):
+        Sprite.__init__(self, parent)
+        self.load_configuration()
+        self.font = Font(None, self.text_size)
+        self.reset()
+        self.deactivate()
+
+    def deactivate(self):
+        self.active = False
+
+    def load_configuration(self):
+        config = self.get_configuration("interpolator-gui")
+        self.size = config["prompt-size"]
+        self.border_color = config["prompt-border-color"]
+        self.border_width = config["prompt-border-width"]
+        self.character_limit = config["prompt-character-limit"]
+        self.text_size = config["prompt-text-size"]
+
+    def reset(self):
+        self.set_frame()
+        self.place()
+
+    def set_frame(self):
+        self.clear_frames()
+        surface = Surface(self.size)
+        self.add_frame(surface)
+        surface.fill(self.border_color)
+        width = self.border_width * 2
+        surface.fill((0, 0, 0), surface.get_rect().inflate(-width, -width))
+
+    def place(self):
+        self.location.center = self.display_surface.get_rect().center
+
+    def activate(self, text, callback, *args):
+        self.active = True
+        self.text = str(text)
+        self.callback = callback, args
+        self.update()
+        self.draw_text()
+
+    def draw_text(self):
+        surface = self.font.render(self.text, True, (255, 255, 255), (0, 0, 0))
+        rect = surface.get_rect()
+        rect.center = self.location.center
+        self.display_surface.blit(surface, rect)
diff --git a/camera_views/lib/pgfw/pgfw/Mainloop.py b/camera_views/lib/pgfw/pgfw/Mainloop.py
new file mode 100644 (file)
index 0000000..f21cc24
--- /dev/null
@@ -0,0 +1,108 @@
+from pygame import display
+from pygame.font import Font
+from pygame.time import get_ticks, wait
+
+from .GameChild import GameChild
+
+class Mainloop(GameChild):
+
+    def __init__(self, parent):
+        GameChild.__init__(self, parent)
+        self.overflow = 0
+        self.frame_count = 1
+        self.actual_frame_duration = 0
+        self.frames_this_second = 0
+        self.last_framerate_display = 0
+        self.load_configuration()
+        self.init_framerate_display()
+        self.last_ticks = get_ticks()
+        self.stopping = False
+
+    def load_configuration(self):
+        config = self.get_configuration("display")
+        self.target_frame_duration = config["frame-duration"]
+        self.wait_duration = config["wait-duration"]
+        self.skip_frames = config["skip-frames"]
+        self.show_framerate = config["show-framerate"]
+        self.framerate_text_size = config["framerate-text-size"]
+        self.framerate_text_color = config["framerate-text-color"]
+        self.framerate_text_background = config["framerate-text-background"]
+        self.framerate_display_flag = config["framerate-display-flag"]
+
+    def init_framerate_display(self):
+        if self.framerate_display_active():
+            screen = self.get_screen()
+            self.last_framerate_count = 0
+            self.framerate_topright = screen.get_rect().topright
+            self.display_surface = screen
+            self.font = Font(None, self.framerate_text_size)
+            self.font.set_bold(True)
+            self.render_framerate()
+
+    def framerate_display_active(self):
+        return self.check_command_line(self.framerate_display_flag) or \
+               self.show_framerate
+
+    def render_framerate(self):
+        text = self.font.render(str(self.last_framerate_count), False,
+                                self.framerate_text_color,
+                                self.framerate_text_background)
+        rect = text.get_rect()
+        rect.topright = self.framerate_topright
+        self.framerate_text = text
+        self.framerate_text_rect = rect
+
+    def run(self):
+        while not self.stopping:
+            self.advance_frame()
+            self.update_frame_duration()
+            self.update_overflow()
+        self.stopping = False
+
+    def advance_frame(self):
+        refresh = False
+        while self.frame_count > 0:
+            refresh = True
+            self.parent.frame()
+            if self.framerate_display_active():
+                self.update_framerate()
+            self.frame_count -= 1
+            if not self.skip_frames:
+                break
+        if refresh:
+            display.update()
+
+    def update_frame_duration(self):
+        last_ticks = self.last_ticks
+        actual_frame_duration = get_ticks() - last_ticks
+        last_ticks = get_ticks()
+        while actual_frame_duration < self.target_frame_duration:
+            wait(self.wait_duration)
+            actual_frame_duration += get_ticks() - last_ticks
+            last_ticks = get_ticks()
+        self.actual_frame_duration = actual_frame_duration
+        self.last_ticks = last_ticks
+
+    def update_overflow(self):
+        self.frame_count = 1
+        target_frame_duration = self.target_frame_duration
+        overflow = self.overflow
+        overflow += self.actual_frame_duration - target_frame_duration
+        while overflow > target_frame_duration:
+            self.frame_count += 1
+            overflow -= target_frame_duration
+        overflow = self.overflow
+
+    def update_framerate(self):
+        count = self.frames_this_second + 1
+        if get_ticks() - self.last_framerate_display > 1000:
+            if count != self.last_framerate_count:
+                self.last_framerate_count = count
+                self.render_framerate()
+            self.last_framerate_display = get_ticks()
+            count = 0
+        self.display_surface.blit(self.framerate_text, self.framerate_text_rect)
+        self.frames_this_second = count
+
+    def stop(self):
+        self.stopping = True
diff --git a/camera_views/lib/pgfw/pgfw/Note.py b/camera_views/lib/pgfw/pgfw/Note.py
new file mode 100644 (file)
index 0000000..355a55d
--- /dev/null
@@ -0,0 +1,153 @@
+# adapted from Pythoven
+# https://github.com/phiresky/pythoven
+
+from random import randint
+from math import sin, log, pi
+from array import array
+
+from pygame import version
+from pygame.mixer import Sound, get_init
+
+class Samples(Sound):
+
+    def __init__(self):
+        self.set_amplitude()
+        if version.vernum < (1, 9, 2):
+            Sound.__init__(self, self.build())
+        else:
+            Sound.__init__(self, buffer=self.build())
+
+    def set_amplitude(self):
+        self.amplitude = (1 << (self.get_sample_width() * 8 - 1)) - 1
+
+    def get_sample_width(self):
+        return abs(int(get_init()[1] / 8))
+
+    def build(self):
+        pass
+
+    def get_empty_array(self, length):
+        return array(self.get_array_typecode(), [0] * length)
+
+    def get_array_typecode(self):
+        return [None, "b", "h"][self.get_sample_width()]
+
+
+class Note(Samples):
+
+    base_frequency = 440.0
+    base_octave = 4
+    base_name = "A"
+    names = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]
+    SQUARE, TRIANGLE, SAW, SINE, DIRTY = range(5)
+
+    def __init__(self, name=None, octave=4, frequency=None, shape=SQUARE,
+                 volume=1.0):
+        names = self.names
+        self.shape = shape
+        if frequency is None:
+            self.name = name
+            self.octave = octave
+            self.set_frequency()
+        elif name is None:
+            self.frequency = float(frequency)
+            self.set_name_and_octave()
+        Samples.__init__(self)
+        self.set_volume(volume)
+
+    def set_frequency(self):
+        name, octave = self.name, self.octave
+        names = self.names
+        octave_length = len(names)
+        offset = (octave - self.base_octave) * octave_length + \
+                 names.index(name) - names.index(self.base_name)
+        self.frequency = self.base_frequency * 2 ** \
+                         (offset / float(octave_length))
+
+    def set_name_and_octave(self):
+        names = self.names
+        octave_length = len(names)
+        offset = int(round(log(self.frequency / self.base_frequency, 2) * \
+                           octave_length)) + names.index(self.base_name)
+        self.octave = self.base_octave + offset / octave_length
+        self.name = names[offset % octave_length]
+
+    def __repr__(self):
+        return "%s%i %.2f" % (self.name, self.octave, self.frequency)
+
+    def build(self):
+        period = int(round(get_init()[0] / self.frequency))
+        samples = self.get_empty_array(period)
+        shape = self.shape
+        if shape == self.TRIANGLE:
+            self.store_triangle_wave(samples, period)
+        elif shape == self.SAW:
+            self.store_saw_wave(samples, period)
+        elif shape == self.SINE:
+            self.store_sine_wave(samples, period)
+        elif shape == self.DIRTY:
+            self.store_dirty_wave(samples)
+        else:
+            self.store_square_wave(samples, period)
+        return samples
+
+    def store_triangle_wave(self, samples, period):
+        amplitude = self.amplitude
+        if period > 1:
+            coefficient = 4 * amplitude / float(period - 1)
+            for time in range(int(round(period / 2.0))):
+                y = int((coefficient * time) - amplitude)
+                samples[time] = y
+                samples[-time - 1] = y
+
+    def store_saw_wave(self, samples, period):
+        amplitude = self.amplitude
+        if period > 1:
+            for time in range(period):
+                samples[time] = int(2 * amplitude / float(period - 1) * time - \
+                                    amplitude)
+
+    def store_sine_wave(self, samples, period):
+        amplitude = self.amplitude
+        for time in range(period):
+            samples[time] = int(round(sin(time / (period / pi / 2)) * \
+                                      amplitude))
+
+    def store_dirty_wave(self, samples):
+        amplitude = self.amplitude
+        for time in range(len(samples)):
+            samples[time] = randint(-amplitude, amplitude)
+
+    def store_square_wave(self, samples, period):
+        amplitude = self.amplitude
+        for time in range(period):
+            if time < period / 2:
+                samples[time] = amplitude
+            else:
+                samples[time] = -amplitude
+
+    def play(self, maxtime=0, fadeout=None, panning=None, fade_in=0):
+        channel = Samples.play(self, -1, maxtime, fade_in)
+        if fadeout:
+            self.fadeout(fadeout)
+        if channel and panning:
+            channel.set_volume(*panning)
+        return channel
+
+
+class Chord:
+
+    def __init__(self, *args):
+        self.notes = args
+
+    def play(self, maxtime=0, fadeout=[None], panning=None, fade_in=[0]):
+        if isinstance(fadeout, int):
+            fadeout = [fadeout]
+        if isinstance(fade_in, int):
+            fade_in = [fade_in]
+        for ii, note in enumerate(self.notes):
+            note.play(maxtime, fadeout[ii % len(fadeout)], panning, fade_in[ii % len(fade_in)])
+
+    def stop(self):
+        for note in self.notes:
+            note.stop()
diff --git a/camera_views/lib/pgfw/pgfw/Profile.py b/camera_views/lib/pgfw/pgfw/Profile.py
new file mode 100644 (file)
index 0000000..b22a983
--- /dev/null
@@ -0,0 +1,26 @@
+import cProfile
+from time import strftime
+from os import mkdir
+from os.path import join, exists
+
+from .GameChild import GameChild
+
+class Profile(cProfile.Profile, GameChild):
+
+    def __init__(self, parent):
+        GameChild.__init__(self, parent)
+        cProfile.Profile.__init__(self)
+        if self.requested():
+            self.enable()
+
+    def requested(self):
+        return self.check_command_line("p")
+
+    def end(self):
+        if self.requested():
+            root = "stat/"
+            if not exists(root):
+                mkdir(root)
+            self.disable()
+            self.create_stats()
+            self.dump_stats(join(root, strftime("%Y%m%d-%H%M_%S.stat")))
diff --git a/camera_views/lib/pgfw/pgfw/ScreenGrabber.py b/camera_views/lib/pgfw/pgfw/ScreenGrabber.py
new file mode 100644 (file)
index 0000000..2413a8b
--- /dev/null
@@ -0,0 +1,41 @@
+from os import makedirs
+from os.path import exists, join
+from sys import exc_info
+from time import strftime
+
+from pygame import image
+
+from .GameChild import *
+from .Input import *
+
+class ScreenGrabber(GameChild):
+
+    def __init__(self, game):
+        GameChild.__init__(self, game)
+        self.delegate = self.get_delegate()
+        self.load_configuration()
+        self.subscribe(self.save_display)
+
+    def load_configuration(self):
+        config = self.get_configuration("screen-captures")
+        self.save_path = config["path"]
+        self.file_name_format = config["file-name-format"]
+        self.file_extension = config["file-extension"]
+
+    def save_display(self, event):
+        if self.delegate.compare(event, "capture-screen"):
+            directory = self.save_path
+            try:
+                if not exists(directory):
+                    makedirs(directory)
+                name = self.build_name()
+                path = join(directory, name)
+                capture = image.save(self.get_screen(), path)
+                self.print_debug("Saved screen capture to %s" % (path))
+            except:
+                self.print_debug("Couldn't save screen capture to %s, %s" %\
+                                 (directory, exc_info()[1]))
+
+    def build_name(self):
+        return "{0}.{1}".format(strftime(self.file_name_format),
+                                self.file_extension)
diff --git a/camera_views/lib/pgfw/pgfw/Setup.py b/camera_views/lib/pgfw/pgfw/Setup.py
new file mode 100644 (file)
index 0000000..06dd399
--- /dev/null
@@ -0,0 +1,137 @@
+from os import walk, remove
+from os.path import sep, join, exists, normpath, basename
+from re import findall, sub
+from distutils.core import setup
+from distutils.command.install import install
+from pprint import pprint
+from fileinput import FileInput
+from re import sub, match
+from fnmatch import fnmatch
+
+from .Configuration import *
+
+class Setup:
+
+    config = Configuration()
+    manifest_path = "MANIFEST"
+
+    def __init__(self):
+        pass
+
+    def remove_old_mainfest(self):
+        path = self.manifest_path
+        if exists(path):
+            remove(path)
+
+    def build_package_list(self):
+        packages = []
+        config = self.config.get_section("setup")
+        locations = [config["package-root"]] + config["additional-packages"]
+        for location in locations:
+            if exists(location):
+                for root, dirs, files in walk(location, followlinks=True):
+                    if exists(join(root, "__init__.py")):
+                        packages.append(root.replace(sep, "."))
+        return packages
+
+    def build_data_map(self):
+        include = []
+        config = self.config.get_section("setup")
+        exclude = list(map(normpath, config["data-exclude"]))
+        for root, dirs, files in walk("."):
+            dirs = self.remove_excluded(dirs, root, exclude)
+            files = [join(root, f) for f in self.remove_excluded(files, root,
+                                                                 exclude)]
+            if files:
+                include.append((normpath(join(config["installation-path"],
+                                              root)), files))
+        return include
+
+    def remove_excluded(self, paths, root, exclude):
+        removal = []
+        for path in paths:
+            if self.contains_path(join(root, path), exclude):
+                removal.append(path)
+        for path in removal:
+            if path in paths:
+               paths.remove(path)
+        return paths
+
+    def contains_path(self, path, container):
+        return any(fnmatch(path, rule) or fnmatch(basename(path), rule) for rule in container) 
+
+    def translate_title(self):
+        config = self.config.get_section("setup")
+        title = config["title"].replace(" ", config["whitespace-placeholder"])
+        return sub("[^\w-]", config["special-char-placeholder"], title)
+
+    def build_description(self):
+        description = ""
+        path = self.config.get("setup", "description-file")
+        if exists(path):
+            description = "\n%s\n%s\n%s" % (file(path).read(),
+                                            "Changelog\n=========",
+                                            self.translate_changelog())
+        return description
+
+    def translate_changelog(self):
+        translation = ""
+        path = self.config.get("setup", "changelog")
+        if exists(path):
+            lines = file(path).readlines()
+            package_name = lines[0].split()[0]
+            for line in lines:
+                line = line.strip()
+                if line.startswith(package_name):
+                    version = findall("\((.*)\)", line)[0]
+                    translation += "\n%s\n%s\n" % (version, "-" * len(version))
+                elif line and not line.startswith("--"):
+                    if line.startswith("*"):
+                        translation += line + "\n"
+                    else:
+                        translation += "  " + line + "\n"
+        return translation
+
+    def setup(self, windows=[], options={}):
+        print("running setup...")
+        self.remove_old_mainfest()
+        config = self.config.get_section("setup")
+        scripts = []
+        if config["init-script"]:
+            scripts.append(config["init-script"])
+        setup(cmdclass={"install": insert_resource_path},
+              name=self.translate_title(),
+              packages=self.build_package_list(),
+              scripts=scripts,
+              data_files=self.build_data_map(),
+              requires=config["requirements"],
+              version=config["version"],
+              description=config["summary"],
+              classifiers=config["classifiers"],
+              long_description=self.build_description(),
+              license=config["license"],
+              platforms=config["platforms"],
+              author=config["contact-name"],
+              author_email=config["contact-email"],
+              url=config["url"],
+              windows=windows,
+              options=options)
+
+
+class insert_resource_path(install):
+
+    def run(self):
+        install.run(self)
+        self.edit_game_object_file()
+
+    def edit_game_object_file(self):
+        config = Configuration().get_section("setup")
+        for path in self.get_outputs():
+            if path.endswith(config["main-object"]):
+                for line in FileInput(path, inplace=True):
+                    pattern = "^ *{0} *=.*".\
+                              format(config["resource-path-identifier"])
+                    if match(pattern, line):
+                        line = sub("=.*$", "= \"{0}\"".\
+                                   format(config["installation-path"]), line)
+                    print(line.strip("\n"))
diff --git a/camera_views/lib/pgfw/pgfw/SetupOSX.py b/camera_views/lib/pgfw/pgfw/SetupOSX.py
new file mode 100644 (file)
index 0000000..265c0e2
--- /dev/null
@@ -0,0 +1,44 @@
+from os.path import exists
+from re import match
+from setuptools import setup, find_packages
+
+from Configuration import Configuration
+from Setup import Setup
+
+class SetupOSX(Setup):
+
+    def __init__(self, launcher_path, data_file_paths,
+                 config_file_path="config"):
+        Setup.__init__(self)
+        self.launcher_path = launcher_path
+        self.data_file_paths = data_file_paths
+        self.config_file_path = config_file_path
+
+    def setup(self):
+        config = Configuration()
+        setup_obj = Setup()
+        version = config.get_section("setup")["version"]
+        name = setup_obj.translate_title()
+        plist = dict(
+            CFBundleIconFile=name,
+            CFBundleName=name,
+            CFBundleShortVersionString=version,
+            CFBundleGetInfoString=' '.join([name, version]),
+            CFBundleExecutable=name,
+            CFBundleIdentifier='org.' + name.lower())
+        setup(name=name,
+              version=version,
+              app=[dict(script=self.launcher_path, plist=plist)],
+              setup_requires=["py2app"],
+              options=dict(py2app=dict(arch="i386",)),
+              data_files=self.data_file_paths)
+        config_path = "dist/%s.app/Contents/Resources/%s" % \
+            (name, self.config_file_path)
+        if exists(config_path):
+            lines = open(config_path).readlines()
+            fp = open(config_path, "w")
+            for line in lines:
+                if match("^\W*fullscreen\W*=\W*yes\W*", line):
+                    fp.write("fullscreen = no\n")
+                else:
+                    fp.write(line)
diff --git a/camera_views/lib/pgfw/pgfw/SetupWin.py b/camera_views/lib/pgfw/pgfw/SetupWin.py
new file mode 100644 (file)
index 0000000..d66a62e
--- /dev/null
@@ -0,0 +1,85 @@
+import sys
+from os import makedirs, walk, sep, remove
+from os.path import join, dirname, basename, exists
+from shutil import rmtree, copy, rmtree
+from itertools import chain
+from zipfile import ZipFile
+
+import py2exe
+
+from .Setup import Setup
+
+class SetupWin(Setup):
+
+    def __init__(self):
+        Setup.__init__(self)
+        if sys.version_info[0] < 3:
+            self.replace_isSystemDLL()
+
+    def replace_isSystemDLL(self):
+        origIsSystemDLL = py2exe.build_exe.isSystemDLL
+        def isSystemDLL(pathname):
+            if basename(pathname).lower() in ("libogg-0.dll", "sdl_ttf.dll"):
+                return 0
+            return origIsSystemDLL(pathname)
+        py2exe.build_exe.isSystemDLL = isSystemDLL
+
+    def setup(self):
+        if sys.version_info[0] < 3 or sys.version_info[1] >= 5:
+            self.numpy_dll_paths_fix()
+        config = self.config.get_section("setup")
+        windows = [{}]
+        if config["init-script"]:
+            windows[0]["script"] = config["init-script"]
+        if config["windows-icon-path"]:
+            windows[0]["icon-resources"] = [(1, config["windows-icon-path"])]
+        Setup.setup(self, windows,
+                    {"py2exe": {"packages": self.build_package_list(),
+                                "dist_dir": config["windows-dist-path"]}})
+        rmtree("build")
+        self.copy_data_files()
+        self.create_archive()
+
+    # https://stackoverflow.com/questions/36191770/py2exe-errno-2-no-such-file-or-directory-numpy-atlas-dll
+    def numpy_dll_paths_fix(self):
+        import numpy
+        import sys
+        paths = set()
+        np_path = numpy.__path__[0]
+        for dirpath, _, filenames in walk(np_path):
+            for item in filenames:
+                if item.endswith('.dll'):
+                    paths.add(dirpath)
+        sys.path.append(*list(paths))
+
+    def copy_data_files(self):
+        root = self.config.get("setup", "windows-dist-path")
+        for path in chain(*list(zip(*self.build_data_map()))[1]):
+            dest = join(root, dirname(path))
+            if not exists(dest):
+                makedirs(dest)
+            copy(path, dest)
+        self.include_readme(root)
+
+    def include_readme(self, root):
+        name = "README"
+        if exists(name):
+            readme = open(name, "r")
+            reformatted = open(join(root, name + ".txt"), "w")
+            for line in open(name, "r"):
+                reformatted.write(line.rstrip() + "\r\n")
+
+    def create_archive(self):
+        config = self.config.get_section("setup")
+        title = self.translate_title() + "-" + config["version"] + "-win"
+        archive_name = title + ".zip"
+        archive = ZipFile(archive_name, "w")
+        destination = config["windows-dist-path"]
+        for root, dirs, names in walk(destination):
+            for name in names:
+                path = join(root, name)
+                archive.write(path, path.replace(destination, title + sep))
+        archive.close()
+        copy(archive_name, "dist")
+        remove(archive_name)
+        rmtree(destination)
diff --git a/camera_views/lib/pgfw/pgfw/Sprite.py b/camera_views/lib/pgfw/pgfw/Sprite.py
new file mode 100644 (file)
index 0000000..e9a09e8
--- /dev/null
@@ -0,0 +1,581 @@
+import collections
+from os import listdir
+from os.path import isfile, join
+from glob import glob
+
+from pygame import Color, Rect, Surface, PixelArray, mask
+from pygame.image import load
+from pygame.transform import flip
+from pygame.locals import *
+
+from .Animation import Animation
+from .Vector import Vector, EVector
+from .extension import get_hue_shifted_surface, get_step, load_frames, get_blinds_rects
+
+class Sprite(Animation):
+
+    def __init__(self, parent, framerate=None, draw_children_on_frame=True):
+        Animation.__init__(self, parent, self.shift_frame, framerate)
+        self.frames = []
+        self.mirrored = False
+        self.alpha = 255
+        self.locations = []
+        self.framesets = [Frameset(self, framerate=framerate)]
+        self.frameset_index = 0
+        self._step = EVector()
+        self.children = collections.OrderedDict()
+        self.draw_children_on_frame = draw_children_on_frame
+        self.set_frameset(0)
+        self.locations.append(Location(self))
+        self.motion_overflow = Vector()
+        self.display_surface = self.get_display_surface()
+        self.wipe_blinds = []
+        self.current_wipe_index = 0
+        self.areas = None
+        self.register(self.hide, self.unhide, self.toggle_hidden, self.wipe_out, self.set_frameset)
+
+    def reset(self):
+        self.halt(self.wipe_out)
+        self.halt(self.set_frameset)
+        self.current_wipe_index = 0
+
+    def __getattr__(self, name):
+        if name in ("location", "rect"):
+            return self.locations[0]
+        if hasattr(Animation, "__getattr__"):
+            return Animation.__getattr__(self, name)
+        raise AttributeError(name)
+
+    def add_frameset(self, order=[], framerate=None, name=None, switch=False):
+        if framerate is None:
+            framerate = self.accounts[self.default_method].interval
+        frameset = Frameset(self, order, framerate, name)
+        self.framesets.append(frameset)
+        if switch:
+            self.set_frameset(len(self.framesets) - 1)
+        return frameset
+
+    def set_frameset(self, identifier):
+        if isinstance(identifier, str):
+            for ii, frameset in enumerate(self.framesets):
+                if frameset.name == identifier:
+                    identifier = ii
+                    break
+        if self.frameset_index != identifier:
+            self.frameset_index = identifier
+            self.register_interval()
+            self.update_location_size()
+            if self.get_current_frameset().length() > 1:
+                self.play()
+            self.get_current_frameset().reset()
+        return self.get_current_frameset()
+
+    def register_interval(self):
+        self.register(self.shift_frame,
+                      interval=self.get_current_frameset().framerate)
+
+    def get_current_frameset(self):
+        return self.framesets[self.frameset_index]
+
+    def update_location_size(self):
+        size = self.get_current_frameset().rect.size
+        for location in self.locations:
+            location.size = size
+            location.fader.init_surface()
+
+    def set_framerate(self, framerate):
+        self.get_current_frameset().set_framerate(framerate)
+        self.register_interval()
+
+    def load_from_path(self, path, transparency=False, ppa=True, key=None,
+                       query=None, omit=False):
+        for frame in load_frames(self.get_resource(path), transparency, ppa, key, query):
+            self.add_frame(frame, omit)
+
+    def fill_colorkey(img, key=None):
+        if not key:
+            key = (255, 0, 255)
+        img = img.convert_alpha()
+        frame = Surface(img.get_size())
+        frame.fill(key)
+        frame.set_colorkey(key)
+        frame.blit(img, (0, 0))
+        return frame
+
+    def add_frame(self, frame, omit=False):
+        self.frames.append(frame)
+        frame.set_alpha(self.alpha)
+        if not omit:
+            frameset = self.get_current_frameset()
+            frameset.add_index(self.frames.index(frame))
+            self.update_location_size()
+            self.wipe_blinds = get_blinds_rects(*self.location.size)
+            self.wipe_blinds.reverse()
+            if frameset.length() > 1:
+                self.play()
+
+    def add_frames(self, frames):
+        for frame in frames:
+            self.add_frame(frame)
+
+    def shift_frame(self):
+        self.get_current_frameset().shift()
+
+    def get_current_frame(self):
+        return self.frames[self.get_current_frameset().get_current_id()]
+
+    def move(self, dx=0, dy=0):
+        for location in self.locations:
+            location.move_ip(dx, dy)
+
+    def reset_motion_overflow(self):
+        for location in self.locations:
+            location.reset_motion_overflow()
+
+    def collide(self, other):
+        if not isinstance(other, Rect):
+            if hasattr(other, "rect"):
+                other = other.rect
+            else:
+                other = Rect(other[0], other[1], 1, 1)
+        for location in self.locations:
+            if location.colliderect(other):
+                return location
+
+    def collide_mask(self, other):
+        location = self.collide(other)
+        if location:
+            current_mask = mask.from_surface(self.get_current_frame())
+            other_mask = mask.from_surface(other.get_current_frame())
+            offset = other.location.left - location.left, \
+                other.location.top - location.top
+            return current_mask.overlap(other_mask, offset) is not None
+
+    def mirror(self):
+        frames = self.frames
+        for ii, frame in enumerate(frames):
+             frames[ii] = flip(frame, True, False)
+        self.mirrored = not self.mirrored
+
+    def clear_frames(self):
+        self.frames = []
+        for frameset in self.framesets:
+            frameset.order = []
+            frameset.reset()
+            frameset.measure_rect()
+
+    def add_location(self, topleft=None, offset=(0, 0), count=1, base=0):
+        if topleft is not None:
+            for ii in range(count):
+                self.locations.append(Location(
+                    self, Rect(topleft, self.locations[0].size)))
+        else:
+            base = self.locations[base]
+            current_offset = list(offset)
+            for ii in range(count):
+                self.locations.append(Location(self,
+                                               base.move(*current_offset)))
+                current_offset[0] += offset[0]
+                current_offset[1] += offset[1]
+        return self.locations[-1]
+
+    def fade(self, length=0, out=None, index=None):
+        if index is None:
+            for location in self.locations:
+                fader = location.fader
+                fader.reset()
+                fader.start(length, out)
+        else:
+            fader = self.locations[index].fader
+            fader.reset()
+            fader.start(length, out)
+
+    def set_alpha(self, alpha):
+        self.alpha = alpha
+        for frame in self.frames:
+            frame.set_alpha(alpha)
+        for location in self.locations:
+            location.fader.set_alpha()
+
+    def hide(self):
+        for location in self.locations:
+            location.hide()
+        for child in self.children.values():
+            child.hide()
+
+    def unhide(self):
+        for location in self.locations:
+            location.unhide()
+        for child in self.children.values():
+            child.unhide()
+
+    def toggle_hidden(self):
+        for location in self.locations:
+            location.toggle_hidden()
+        for child in self.children.values():
+            child.toggle_hidden()
+
+    def is_hidden(self):
+        return all(location.is_hidden() for location in self.locations)
+
+    def remove_locations(self, location=None):
+        if location:
+            self.locations.remove(location)
+        else:
+            self.locations = self.locations[:1]
+
+    def reverse(self, frameset=None):
+        if frameset:
+            frameset.reverse()
+        else:
+            for frameset in self.framesets:
+                frameset.reverse()
+
+    def set_step(self, dx=0, dy=0, magnitude=None, angle=0):
+        self._step.set_step(dx, dy, magnitude, angle)
+
+    def cancel_step(self):
+        self._step.set_step(0, 0)
+
+    def is_stepping(self):
+        return bool(self._step)
+
+    def set_child(self, name, child):
+        self.children[name] = child
+
+    def get_child(self, name):
+        return self.children[name]
+
+    def has_child(self, name):
+        return name in self.children
+
+    def remove_child(self, name):
+        if self.has_child(name):
+            self.children.pop(name)
+
+    def wipe_out(self):
+        for child in self.children.values():
+            if not child.is_playing(child.wipe_out):
+                child.play(child.wipe_out)
+        self.current_wipe_index += 1
+        if self.current_wipe_index == len(self.wipe_blinds):
+            self.current_wipe_index = 0
+            self.halt(self.wipe_out)
+            self.hide()
+
+    def get_current_blinds(self):
+        return self.wipe_blinds[self.current_wipe_index]
+
+    def set_areas(self, areas):
+        self.areas = areas
+
+    def update(self, areas=None, substitute=None, flags=0):
+        Animation.update(self)
+        if self.is_stepping():
+            self.move(self._step.dx, self._step.dy)
+        if self.get_current_frameset().length():
+            if self.is_playing(self.wipe_out):
+                areas = self.get_current_blinds()
+            elif not areas and self.areas:
+                areas = self.areas
+            self.draw(areas, substitute, flags)
+        for child in self.children.values():
+            if self.draw_children_on_frame:
+                child.display_surface = self.get_current_frame()
+            else:
+                child.display_surface = self.get_display_surface()
+                save_child_topleft = child.location.topleft
+                child.move(*self.location.topleft)
+            child.update()
+            if not self.draw_children_on_frame:
+                child.location.topleft = save_child_topleft
+
+    def draw(self, areas=None, substitute=None, flags=0):
+        for location in self.locations:
+            location.fader.draw(areas, substitute, flags)
+
+
+class Location(Rect):
+
+    def __init__(self, sprite, rect=(0, 0, 0, 0)):
+        self.sprite = sprite
+        Rect.__init__(self, rect)
+        self.motion_overflow = Vector()
+        self.fader = Fader(self)
+        self.unhide()
+
+    def move_ip(self, dx, dy):
+        if isinstance(dx, float) or isinstance(dy, float):
+            excess = self.update_motion_overflow(dx, dy)
+            Rect.move_ip(self, int(dx) + excess[0], int(dy) + excess[1])
+        else:
+            Rect.move_ip(self, dx, dy)
+
+    def update_motion_overflow(self, dx, dy):
+        overflow = self.motion_overflow
+        overflow.move(dx - int(dx), dy - int(dy))
+        excess = [int(value) for value in overflow]
+        # excess = map(int, overflow)
+        overflow[0] -= int(overflow[0])
+        overflow[1] -= int(overflow[1])
+        return excess
+
+    def move_to(self, x, y, base=None):
+        ox, oy = self.apply_motion_overflow(base)
+        self.move_ip(x - ox, y - oy)
+
+    def reset_motion_overflow(self):
+        self.motion_overflow.place_at_origin()
+
+    def apply_motion_overflow(self, coordinates=None):
+        if coordinates is None:
+            coordinates = self.topleft
+        return self.motion_overflow + coordinates
+
+    def hide(self):
+        self.hidden = True
+
+    def unhide(self):
+        self.hidden = False
+
+    def toggle_hidden(self):
+        self.hidden = not self.hidden
+
+    def is_hidden(self):
+        return self.hidden
+
+
+class Fader(Surface):
+
+    def __init__(self, location):
+        self.location = location
+        self.time_filter = location.sprite.get_game().time_filter
+        self.reset()
+
+    def reset(self):
+        self.init_surface()
+        self.fade_remaining = None
+
+    def init_surface(self):
+        Surface.__init__(self, self.location.size)
+        if self.location.sprite.get_current_frameset().length():
+            background = Surface(self.get_size())
+            sprite = self.location.sprite
+            key = sprite.get_current_frame().get_colorkey() or (255, 0, 255)
+            self.set_colorkey(key)
+            background.fill(key)
+            self.background = background
+            self.set_alpha()
+
+    def set_alpha(self, alpha=None):
+        if alpha is None:
+            alpha = self.location.sprite.alpha
+        Surface.set_alpha(self, alpha)
+
+    def start(self, length, out=None):
+        if self.fade_remaining <= 0:
+            alpha = self.get_alpha()
+            maximum = self.location.sprite.alpha
+            if out is None:
+                out = alpha == maximum
+            if out and alpha > 0 or not out and alpha < maximum:
+                self.fade_length = self.fade_remaining = length
+                self.start_time = self.time_filter.get_ticks()
+                self.fading_out = out
+
+    def draw(self, areas=None, substitute=None, flags=0):
+        sprite = self.location.sprite
+        if substitute is None:
+            frame = sprite.get_current_frame()
+        else:
+            frame = substitute
+        if self.fade_remaining is not None and self.fade_remaining >= 0:
+            self.update_alpha()
+            self.clear()
+            frame.set_alpha(255)
+            self.blit(frame, (0, 0))
+            frame.set_alpha(sprite.alpha)
+            if not self.location.is_hidden():
+                if frame.get_colorkey() is None:
+                    ratio = self.get_alpha() / 255.0
+                    pixels = PixelArray(frame.copy())
+                    color = Color(0, 0, 0)
+                    for x in range(len(pixels)):
+                        for y in range(len(pixels[0])):
+                            h, s, l, a = Color(*frame.unmap_rgb(pixels[x][y])).hsla
+                            if a:
+                                color.hsla = h, s, l, int(a * ratio)
+                                pixels[x][y] = color
+                    self.blit_to_display(pixels.make_surface(), areas, flags)
+                    del pixels
+                else:
+                    self.blit_to_display(self, areas, flags)
+        elif self.fade_remaining is None or self.get_alpha() >= sprite.alpha:
+            if self.fade_remaining is not None and self.fade_remaining >= 0:
+                self.update_alpha()
+            if not self.location.is_hidden():
+                self.blit_to_display(frame, areas, flags)
+
+    def blit_to_display(self, frame, areas=None, flags=0):
+        if not isinstance(areas, list):
+            areas = [areas]
+        for area in areas:
+            if area:
+                dest = area.left + self.location.left, \
+                       area.top + self.location.top
+            else:
+                dest = self.location
+            self.location.sprite.display_surface.blit(frame, dest, area, flags)
+
+    def update_alpha(self):
+        remaining = self.fade_remaining = self.fade_length - \
+                    (self.time_filter.get_ticks() - self.start_time)
+        ratio = self.fade_length and float(remaining) / self.fade_length
+        if not self.fading_out:
+            ratio = 1 - ratio
+        maximum = self.location.sprite.alpha
+        alpha = int(ratio * maximum)
+        if alpha > maximum:
+            alpha = maximum
+        elif alpha < 0:
+            alpha = 0
+        self.set_alpha(alpha)
+
+    def clear(self):
+        self.blit(self.background, (0, 0))
+
+
+class Frameset:
+
+    def __init__(self, sprite, order=[], framerate=None, name=None):
+        self.sprite = sprite
+        self.name = name
+        self.reversed = False
+        self.order = []
+        self.rect = Rect(0, 0, 0, 0)
+        self.add_index(order)
+        self.set_framerate(framerate)
+        self.reset()
+
+    def add_index(self, order):
+        if isinstance(order, int):
+            order = [order]
+        self.order += order
+        self.measure_rect()
+
+    def set_framerate(self, framerate):
+        self.framerate = framerate
+
+    def reset(self):
+        self.current_index = 0
+        self.sprite.accounts[self.sprite.shift_frame].reset_interval()
+
+    def get_current_id(self):
+        return self.order[self.current_index]
+
+    def measure_rect(self):
+        max_width, max_height = 0, 0
+        frames = self.sprite.frames
+        for index in self.order:
+            frame = frames[index]
+            width, height = frame.get_size()
+            max_width = max(width, max_width)
+            max_height = max(height, max_height)
+        self.rect.size = max_width, max_height
+
+    def shift(self):
+        if len(self.order) > 1:
+            self.increment_index()
+
+    def increment_index(self, increment=None):
+        if increment is None:
+            increment = 1 if not self.reversed else -1
+        index = self.current_index + increment
+        while index < 0:
+            index += self.length()
+        while index >= self.length():
+            index -= self.length()
+        self.current_index = index
+
+    def length(self):
+        return len(self.order)
+
+    def reverse(self):
+        self.reversed = not self.reversed
+
+
+class BlinkingSprite(Sprite):
+
+    def __init__(self, parent, blink_rate, framerate=None):
+        Sprite.__init__(self, parent, framerate)
+        self.register(self.blink, interval=blink_rate)
+        self.register(self.start_blinking, self.stop_blinking)
+        self.play(self.blink)
+
+    def reset(self):
+        self.unhide()
+
+    def blink(self):
+        self.toggle_hidden()
+
+    def start_blinking(self):
+        self.play(self.blink)
+
+    def stop_blinking(self):
+        self.halt(self.blink)
+        self.unhide()
+
+
+class RainbowSprite(Sprite):
+
+    def __init__(self, parent, image=None, hue_shift=8, framerate=None):
+        Sprite.__init__(self, parent, framerate)
+        self.hue_shift = hue_shift
+        if image is not None:
+            self.set_frames(image)
+
+    def set_frames(self, image):
+        for hue in range(0, 360, self.hue_shift):
+            self.add_frame(get_hue_shifted_surface(image, hue))
+
+
+class ShadowedSprite(Sprite):
+
+    def __init__(self, parent, offset=Vector(3, 3), opacity=1, color=(0, 0, 0), framerate=None):
+        Sprite.__init__(self, parent, framerate)
+        if not isinstance(offset, Vector):
+            offset = Vector(*offset)
+        if not isinstance(color, Color):
+            color = Color(*color)
+        self.shadow_color = color
+        self.shadow_offset = offset
+        self.shadow_opacity = opacity
+        self.shadow_color = color
+        self.shadow = Sprite(self)
+
+    def add_frame(self, frame, omit=False):
+        Sprite.add_frame(self, frame, omit)
+        fr = frame.get_rect()
+        shadow = Surface(fr.size, SRCALPHA)
+        pixels = PixelArray(frame)
+        shadow_pixels = PixelArray(shadow)
+        r, g, b = self.shadow_color.r, self.shadow_color.g, self.shadow_color.b
+        for y in range(len(pixels)):
+            for x in range(len(pixels[0])):
+                frame_pixel_color = frame.unmap_rgb(pixels[y][x])
+                if frame_pixel_color == frame.get_colorkey():
+                    shadow_pixel_color = 0, 0, 0, 0
+                else:
+                    shadow_pixel_color = r, g, b, frame_pixel_color.a * self.shadow_opacity
+                shadow_pixels[y][x] = shadow_pixel_color
+        self.shadow.add_frame(shadow)
+
+    def clear_frames(self):
+        self.shadow.clear_frames()
+        Sprite.clear_frames(self)
+
+    def update(self):
+        self.shadow.location.topleft = Vector(*self.location.topleft).get_moved(
+            *self.shadow_offset)
+        self.shadow.update()
+        Sprite.update(self)
diff --git a/camera_views/lib/pgfw/pgfw/TimeFilter.py b/camera_views/lib/pgfw/pgfw/TimeFilter.py
new file mode 100644 (file)
index 0000000..770b84c
--- /dev/null
@@ -0,0 +1,39 @@
+from pygame.time import get_ticks
+
+from .GameChild import GameChild
+
+class TimeFilter(GameChild):
+
+    def __init__(self, parent):
+        GameChild.__init__(self, parent)
+        self.reset_ticks()
+        self.open()
+
+    def reset_ticks(self):
+        self.ticks = self.unfiltered_ticks = self.last_ticks = get_ticks()
+
+    def close(self):
+        self.closed = True
+
+    def open(self):
+        self.closed = False
+
+    def get_ticks(self):
+        return self.ticks
+
+    def get_unfiltered_ticks(self):
+        return self.unfiltered_ticks
+
+    def get_last_ticks(self):
+        return self.last_ticks
+
+    def get_last_frame_duration(self):
+        return self.last_frame_duration
+
+    def update(self):
+        ticks = get_ticks()
+        self.last_frame_duration = duration = ticks - self.last_ticks
+        if not self.closed:
+            self.ticks += duration
+        self.unfiltered_ticks += duration
+        self.last_ticks = ticks
diff --git a/camera_views/lib/pgfw/pgfw/Vector.py b/camera_views/lib/pgfw/pgfw/Vector.py
new file mode 100644 (file)
index 0000000..d98ce62
--- /dev/null
@@ -0,0 +1,148 @@
+from math import pi, degrees
+
+class Vector(list):
+
+    def __init__(self, x=0, y=0):
+        list.__init__(self, (x, y))
+
+    def __repr__(self):
+        message = "<%f" % self[0]
+        for value in self[1:]:
+            message += ", %f" % value
+        return message + ">"
+
+    def __getattr__(self, name):
+        if name == "x":
+            return self[0]
+        elif name == "y":
+            return self[1]
+        else:
+            raise AttributeError
+
+    def __setattr__(self, name, value):
+        if name == "x":
+            self[0] = value
+        elif name == "y":
+            self[1] = value
+        else:
+            list.__setattr__(self, name, value)
+
+    def __add__(self, other):
+        return Vector(self.x + other[0], self.y + other[1])
+
+    __radd__ = __add__
+
+    def __iadd__(self, other):
+        self.x += other[0]
+        self.y += other[1]
+        return self
+
+    def __sub__(self, other):
+        return Vector(self.x - other[0], self.y - other[1])
+
+    def __rsub__(self, other):
+        return Vector(other[0] - self.x, other[1] - self.y)
+
+    def __isub__(self, other):
+        self.x -= other[0]
+        self.y -= other[1]
+        return self
+
+    def __mul__(self, other):
+        return Vector(self.x * other, self.y * other)
+
+    __rmul__ = __mul__
+
+    def __imul__(self, other):
+        self.x *= other
+        self.y *= other
+        return self
+
+    def __eq__(self, other):
+        for sv, ov in zip(self, other):
+            if sv != ov:
+                return False
+        return True
+
+    def __ne__(self, other):
+        for sv, ov in zip(self, other):
+            if sv == ov:
+                return False
+        return True
+
+    def __nonzero__(self):
+        for value in self:
+            if bool(value):
+                return True
+        return False
+
+    def apply_to_components(self, function):
+        self.x = function(self.x)
+        self.y = function(self.y)
+
+    def place(self, x=None, y=None):
+        if x is not None:
+            self.x = x
+        if y is not None:
+            self.y = y
+
+    def move(self, dx=0, dy=0):
+        if dx:
+            self.x += dx
+        if dy:
+            self.y += dy
+
+    def get_moved(self, dx=0, dy=0):
+        return Vector(self.x + dx, self.y + dy)
+
+    def place_at_origin(self):
+        self.place(0, 0)
+
+    def vol(self):
+        return self.x * self.y
+
+    def copy(self):
+        return Vector(self.x, self.y)
+
+
+class EVector(Vector):
+
+    def __init__(self, x=0, y=0, dx=0, dy=0, magnitude=None, angle=0):
+        Vector.__init__(self, x, y)
+        self.angle = None
+        self.set_step(dx, dy, magnitude, angle)
+
+    def set_step(self, dx=0, dy=0, magnitude=None, angle=0):
+        """specify angle in radians, counter-clockwise, 0 is up"""
+        if magnitude is not None:
+            from .extension import get_delta
+            self.magnitude = magnitude
+            self.angle = angle
+            self.dx, self.dy = get_delta(angle, magnitude, False)
+        else:
+            self.dx = dx
+            self.dy = dy
+            if dx == 0 and dy == 0:
+                self.magnitude = 0
+                self.angle = 0
+            else:
+                from .extension import get_angle, get_distance
+                end = self.x + dx, self.y + dy
+                self.angle = get_angle(self, end, True)
+                self.magnitude = get_distance(self, end)
+
+    def __repr__(self):
+        return "<dx=%.2f, dy=%.2f, m=%.2f, ang=%.2f>" % \
+            (self.dx, self.dy, self.magnitude, self.angle)
+
+    def __nonzero__(self):
+        return bool(self.magnitude)
+
+    def __setattr__(self, name, value):
+        list.__setattr__(self, name, value)
+        if name == "magnitude" and self.angle is not None:
+            from .extension import get_delta
+            self.dx, self.dy = get_delta(self.angle, value, False)
+
+    def move(self):
+        self += self.dx, self.dy
diff --git a/camera_views/lib/pgfw/pgfw/VideoRecorder.py b/camera_views/lib/pgfw/pgfw/VideoRecorder.py
new file mode 100644 (file)
index 0000000..8c9deff
--- /dev/null
@@ -0,0 +1,113 @@
+from os import makedirs
+from os.path import exists, join
+from tempfile import TemporaryFile
+from time import strftime
+
+from pygame.image import tostring, frombuffer, save
+from pygame.time import get_ticks
+
+from .GameChild import GameChild
+
+class VideoRecorder(GameChild):
+
+    def __init__(self, parent):
+        GameChild.__init__(self, parent)
+        self.set_requested()
+        if self.requested:
+            self.display_surface = self.get_display_surface()
+            self.delegate = self.get_delegate()
+            self.load_configuration()
+            self.reset()
+            self.subscribe(self.respond)
+
+    def set_requested(self):
+        self.requested = self.get_configuration("video-recordings")["enable"] \
+                         or self.check_command_line("-enable-video")
+
+    def load_configuration(self):
+        config = self.get_configuration("video-recordings")
+        self.root = config["path"]
+        self.directory_name_format = config["directory-name-format"]
+        self.file_extension = config["file-extension"]
+        self.frame_format = config["frame-format"]
+        self.framerate = config["framerate"]
+
+    def reset(self):
+        self.recording = False
+        self.frame_length = None
+        self.frames = None
+        self.last_frame = 0
+
+    def respond(self, event):
+        compare = self.delegate.compare
+        if compare(event, "record-video"):
+            self.toggle_record()
+        elif compare(event, "reset-game"):
+            self.reset()
+
+    def is_audio_recording_enabled(self):
+        return self.get_configuration("video-recordings", "record-audio") or\
+            self.check_command_line("-enable-sound-recording")
+
+    def toggle_record(self):
+        recording = not self.recording
+        if recording:
+            self.frame_length = len(self.get_string())
+            temp_dir = self.get_configuration("video-recordings", "temp-directory")
+            if temp_dir == "":
+                temp_dir = None
+            self.frames = TemporaryFile(dir=temp_dir)
+            print("writing video frames to {}".format(self.frames.name))
+            if self.is_audio_recording_enabled():
+                import pyaudio
+                import wave
+                self.audio_frames = []
+                self.audio_recorder = pyaudio.PyAudio()
+                def audio_callback(in_data, frame_count, time_info, status):
+                    self.audio_frames.append(in_data)
+                    return (in_data, pyaudio.paContinue)
+                for ii in range(self.audio_recorder.get_device_count()):
+                    print("device {}: {}".format(ii, self.audio_recorder.get_device_info_by_index(ii)))
+                self.audio_stream = self.audio_recorder.open(format=pyaudio.paInt32, channels=2, rate=44100,
+                                                             frames_per_buffer=1024,
+                                                             input=True, stream_callback=audio_callback)
+                print("writing audio data to {}".format(self.audio_stream))
+        else:
+            self.write_frames()
+        self.recording = recording
+
+    def get_string(self):
+        return tostring(self.display_surface, self.frame_format)
+
+    def write_frames(self):
+        root = join(self.root, strftime(self.directory_name_format))
+        if not exists(root):
+            makedirs(root)
+        if self.is_audio_recording_enabled():
+            import pyaudio
+            import wave
+            self.audio_stream.stop_stream()
+            self.audio_stream.close()
+            self.audio_recorder.terminate()
+            wf = wave.open(join(root, "audio.wav"), "wb")
+            wf.setnchannels(2)
+            wf.setsampwidth(self.audio_recorder.get_sample_size(pyaudio.paInt32))
+            wf.setframerate(44100)
+            wf.writeframes(b"".join(self.audio_frames))
+            wf.close()
+            print("saved audio in {}".format(root))
+        size = self.display_surface.get_size()
+        frames = self.frames
+        frames.seek(0)
+        for ii, frame in enumerate(
+                iter(lambda: frames.read(self.frame_length), b'')):
+            path = join(root, "{:0{digits}}.png".format(
+                ii, digits=self.get_configuration("video-recordings", "filename-digits")))
+            save(frombuffer(frame, size, self.frame_format), path)
+        print("saved video frames in {}".format(root))
+
+    def update(self):
+        ticks = get_ticks()
+        if self.recording and ticks - self.last_frame >= self.framerate:
+            self.frames.write(self.get_string())
+            self.last_frame = ticks
diff --git a/camera_views/lib/pgfw/pgfw/__init__.py b/camera_views/lib/pgfw/pgfw/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/camera_views/lib/pgfw/pgfw/confirm_quit_message.py b/camera_views/lib/pgfw/pgfw/confirm_quit_message.py
new file mode 100644 (file)
index 0000000..c696381
--- /dev/null
@@ -0,0 +1,10 @@
+CONFIRM_QUIT_MESSAGE = '''
+............................................................
+..###..###..#....#.####.#.####..#...#....###..#...#.#.#####.
+.#....#...#.##...#.#....#.#...#.##.##...#...#.#...#.#...#...
+.#....#...#.#.#..#.####.#.#...#.#.#.#...#...#.#...#.#...#...
+.#....#...#.#..#.#.#....#.####..#...#...#.#.#.#...#.#...#...
+.#....#...#.#...##.#....#.#...#.#...#...#..#..#...#.#...#...
+..###..###..#....#.#....#.#...#.#...#....##.#..###..#...#...
+............................................................
+'''
diff --git a/camera_views/lib/pgfw/pgfw/extension.py b/camera_views/lib/pgfw/pgfw/extension.py
new file mode 100644 (file)
index 0000000..69c864e
--- /dev/null
@@ -0,0 +1,493 @@
+import itertools, random, os, glob
+from math import sin, cos, atan2, radians, sqrt, pi
+
+import pygame
+from pygame import Surface, PixelArray, Color, Rect, draw, gfxdraw
+from pygame.mixer import get_num_channels, Channel
+from pygame.locals import *
+
+from .Vector import Vector
+
+def clamp(n, min_n, max_n):
+    if n < min_n:
+        return min_n
+    elif n > max_n:
+        return max_n
+    else:
+        return n
+
+def get_step(start, end, speed):
+    x0, y0 = start
+    x1, y1 = end
+    angle = atan2(x1 - x0, y1 - y0)
+    return Vector(speed * sin(angle), speed * cos(angle))
+
+def get_step_relative(start, end, step):
+    return get_step(start, end, get_distance(start, end) * step)
+
+def get_segments(start, end, count):
+    rel_step = get_step_relative(start, end, 1 / float(count))
+    segs = [[Vector(start[0], start[1])]]
+    for ii in range(count):
+        seg_end = Vector(segs[-1][0].x + rel_step.x, segs[-1][0].y + rel_step.y)
+        segs[-1].append(seg_end)
+        if ii < count - 1:
+            segs.append([seg_end])
+    return segs
+
+def get_points_on_line(start, end, count):
+    rel_step = get_step_relative(start, end, 1 / float(count - 1))
+    points = [Vector(start[0], start[1])]
+    for ii in range(count - 2):
+        points.append(Vector(points[-1][0] + rel_step[0], points[-1][1] + rel_step[1]))
+    points.append(Vector(end[0], end[1]))
+    return points
+
+def get_angle(start, end, transpose=False):
+    """counterclockwise, 0 is down"""
+    angle = atan2(end[0] - start[0], end[1] - start[1])
+    if transpose:
+        angle = -angle - pi
+    return angle
+
+def get_endpoint(start, angle, magnitude, translate_angle=True):
+    """clockwise, 0 is up"""
+    x0, y0 = start
+    dx, dy = get_delta(angle, magnitude, translate_angle)
+    return Vector(x0 + dx, y0 + dy)
+
+def get_delta(angle, magnitude, translate_angle=True):
+    if translate_angle:
+        angle = radians(angle)
+    return Vector(sin(angle) * magnitude, -cos(angle) * magnitude)
+
+def reflect_angle(angle, wall):
+    return wall - angle
+
+def rotate_2d(point, center, angle, translate_angle=True):
+    if translate_angle:
+        angle = radians(angle)
+    x, y = point
+    cx, cy = center
+    return cos(angle) * (x - cx) - sin(angle) * (y - cy) + cx, \
+           sin(angle) * (x - cx) + cos(angle) * (y - cy) + cy
+
+def get_points_on_circle(center, radius, count, offset=0):
+    angle_step = 360.0 / count
+    points = []
+    current_angle = 0
+    for _ in range(count):
+        points.append(get_point_on_circle(center, radius,
+                                          current_angle + offset))
+        current_angle += angle_step
+    return points
+
+def get_point_on_circle(center, radius, angle, translate_angle=True):
+    if translate_angle:
+        angle = radians(angle)
+    return Vector(center[0] + sin(angle) * radius,
+                  center[1] - cos(angle) * radius)
+
+def get_range_steps(start, end, count):
+    for ii in range(count):
+        yield start + (end - start) * ii / float(count - 1)
+
+def get_percent_way(iterable):
+    for ii in range(len(iterable)):
+        yield iterable[ii], float(ii) / (len(iterable) - 1)
+
+def mirrored(iterable, full=False, tail=True):
+    for ii, item in enumerate(itertools.chain(iterable, reversed(iterable))):
+        if not full and ii == len(iterable):
+            continue
+        elif not tail and ii == len(iterable) * 2 - 1:
+            continue
+        else:
+            yield item
+
+def get_distance(p0, p1):
+    return sqrt((p0[0] - p1[0]) ** 2 + (p0[1] - p1[1]) ** 2)
+
+def place_in_rect(rect, incoming, contain=True, *args):
+    while True:
+        incoming.center = random.randint(0, rect.w), random.randint(0, rect.h)
+        if not contain or rect.contains(incoming):
+            collides = False
+            for inner in args:
+                if inner.colliderect(incoming):
+                    collides = True
+                    break
+            if not collides:
+                break
+
+def constrain_dimensions_2d(vector, container):
+    dw = vector[0] - container[0]
+    dh = vector[1] - container[1]
+    if dw > 0 or dh > 0:
+        if dw > dh:
+            size = Vector(container[0], int(round(container[0] / vector[0] * vector[1])))
+        else:
+            size = Vector(int(round(container[1] / vector[1] * vector[0])), container[1])
+    else:
+        size = Vector(vector[0], vector[1])
+    return size
+
+# from http://www.realtimerendering.com/resources/GraphicsGems/gemsii/xlines.c
+def get_intersection(p0, p1, p2, p3):
+    x0, y0 = p0
+    x1, y1 = p1
+    x2, y2 = p2
+    x3, y3 = p3
+    a0 = y1 - y0
+    b0 = x0 - x1
+    c0 = x1 * y0 - x0 * y1
+    r2 = a0 * x2 + b0 * y2 + c0
+    r3 = a0 * x3 + b0 * y3 + c0
+    if r2 != 0 and r3 != 0 and r2 * r3 > 0:
+        return None
+    a1 = y3 - y2
+    b1 = x2 - x3
+    c1 = x3 * y2 - x2 * y3
+    r0 = a1 * x0 + b1 * y0 + c1
+    r1 = a1 * x1 + b1 * y1 + c1
+    if r0 != 0 and r1 != 0 and r0 * r1 > 0:
+        return None
+    denominator = a0 * b1 - a1 * b0
+    if denominator == 0:
+        return (x0 + x1 + x2 + x3) / 4, (y0 + y1 + y2 + y3) / 4
+    if denominator < 0:
+        offset = -denominator / 2
+    else:
+        offset = denominator / 2
+    numerator = b0 * c1 - b1 * c0
+    x = ((-1, 1)[numerator < 0] * offset + numerator) / denominator
+    numerator = a1 * c0 - a0 * c1
+    y = ((-1, 1)[numerator < 0] * offset + numerator) / denominator
+    return x, y
+
+def collide_line_with_rect(rect, p0, p1):
+    for line in ((rect.topleft, rect.topright),
+                 (rect.topright, rect.bottomright),
+                 (rect.bottomright, rect.bottomleft),
+                 (rect.bottomleft, rect.topleft)):
+        if get_intersection(p0, p1, *line):
+            return True
+
+def get_random_number_in_range(start, end):
+    return random.random() * (end - start) + start
+
+def get_value_in_range(start, end, position, reverse=False):
+    if reverse:
+        position = 1 - position
+    return (end - start) * position + start
+
+def get_boxed_surface(surface, background=None, border=None, border_width=1,
+                      padding=0):
+    if padding:
+        if isinstance(padding, int):
+            padding = [padding] * 2
+        padding = [x * 2 for x in padding]
+        rect = surface.get_rect()
+        padded_surface = Surface(rect.inflate(padding).size, SRCALPHA)
+        if background is not None:
+            padded_surface.fill(background)
+        rect.center = padded_surface.get_rect().center
+        padded_surface.blit(surface, rect)
+        surface = padded_surface
+    if border is not None:
+        if isinstance(border_width, int):
+            border_width = [border_width] * 2
+        border_width = [x * 2 for x in border_width]
+        rect = surface.get_rect()
+        bordered_surface = Surface(rect.inflate(border_width).size, SRCALPHA)
+        bordered_surface.fill(border)
+        rect.center = bordered_surface.get_rect().center
+        bordered_surface.fill((255, 255, 255, 255), rect)
+        bordered_surface.blit(surface, rect, None, BLEND_RGBA_MIN)
+        surface = bordered_surface
+    return surface
+
+def render_box(font=None, text=None, antialias=True, color=(0, 0, 0), background=None, border=None,
+               border_width=1, padding=0, width=None, height=None):
+    if font is not None:
+        surface = font.render(text, antialias, color, background)
+        if width is not None or height is not None:
+            if width is None:
+                width = surface.get_width()
+            if height is None:
+                height = surface.get_height()
+            container = Surface((width, height), SRCALPHA)
+            if background is not None:
+                container.fill(background)
+            text_rect = surface.get_rect()
+            text_rect.center = container.get_rect().center
+            container.blit(surface, text_rect)
+            surface = container
+    else:
+        surface = pygame.Surface((width, height), SRCALPHA)
+        surface.fill(background)
+    return get_boxed_surface(surface, background, border, border_width, padding)
+
+def get_wrapped_text_surface(font, text, width, antialias=True, color=(0, 0, 0),
+                             background=None, border=None, border_width=1,
+                             padding=0, align="left"):
+    lines = []
+    height = 0
+    for chunk in text.split("\n"):
+        line_text = ""
+        ii = 0
+        finished = False
+        if chunk.startswith("\*") and chunk.endswith("\*"):
+            chunk = chunk.replace("\*", "*")
+        elif chunk.startswith("*") and chunk.endswith("*"):
+            chunk = chunk[1:-1]
+            font.set_italic(True)
+        words = chunk.split(" ")
+        while not finished:
+            line_width = font.size(line_text + " " + words[ii])[0]
+            if line_width > width or ii == len(words) - 1:
+                if ii == len(words) - 1 and line_width <= width:
+                    if line_text != "":
+                        line_text += " "
+                    line_text += words[ii]
+                    finished = True
+                line = font.render(line_text, antialias, color)
+                height += line.get_height()
+                lines.append(line)
+                line_text = ""
+            else:
+                if ii > 0:
+                    line_text += " "
+                line_text += words[ii]
+                ii += 1
+        font.set_italic(False)
+    top = 0
+    surface = Surface((width, height), pygame.SRCALPHA)
+    # if background:
+    #     surface.fill(background)
+    rect = surface.get_rect()
+    for line in lines:
+        line_rect = line.get_rect()
+        line_rect.top = top
+        if align == "center":
+            line_rect.centerx = rect.centerx
+        surface.blit(line, line_rect)
+        top += line_rect.h
+    return get_boxed_surface(surface, background, border, border_width, padding)
+
+def replace_color(surface, color, replacement):
+    pixels = PixelArray(surface)
+    pixels.replace(color, replacement)
+    del pixels
+
+def get_color_swapped_surface(surface, color, replacement):
+    swapped = surface.copy()
+    replace_color(swapped, color, replacement)
+    return swapped
+
+def get_busy_channel_count():
+    count = 0
+    for index in range(get_num_channels()):
+        count += Channel(index).get_busy()
+    return count
+
+def get_hue_shifted_surface(base, offset):
+    surface = base.copy()
+    pixels = PixelArray(surface)
+    color = Color(0, 0, 0)
+    for x in range(surface.get_width()):
+        for y in range(surface.get_height()):
+            rgb = surface.unmap_rgb(pixels[x][y])
+            h, s, l, a = Color(*rgb).hsla
+            if a and surface.get_colorkey() != rgb:
+                color.hsla = (int(h) + offset) % 360, int(s), int(l), int(a)
+                pixels[x][y] = color
+    del pixels
+    return surface
+
+def get_inverted_color(color):
+    return Color(255 - color[0], 255 - color[1], 255 - color[2])
+
+def get_inverted_surface(base):
+    surface = base.copy()
+    pixels = PixelArray(surface)
+    for x in range(surface.get_width()):
+        for y in range(surface.get_height()):
+            color = Color(*surface.unmap_rgb(pixels[x][y]))
+            if color.hsla[3]:
+                pixels[x][y] = get_inverted_color(color)
+    del pixels
+    return surface
+
+def fill_tile(surface, tile, rect=None, flags=0):
+    w, h = tile.get_size()
+    surface.set_clip(rect)
+    for x in range(0, surface.get_width(), w):
+        for y in range(0, surface.get_height(), h):
+            surface.blit(tile, (x, y), None, flags)
+    surface.set_clip(None)
+
+def load_frames(path, transparency=False, ppa=True, key=None, query=None):
+    if os.path.isfile(path):
+        paths = [path]
+    else:
+        if query:
+            paths = sorted(glob.glob(os.path.join(path, query)))
+        else:
+            paths = [os.path.join(path, name) for name in sorted(os.listdir(path))]
+    frames = []
+    for path in paths:
+        img = pygame.image.load(path)
+        if transparency:
+            if ppa:
+                frame = img.convert_alpha()
+            else:
+                frame = fill_colorkey(img, key)
+        else:
+            frame = img.convert()
+        frames.append(frame)
+    return frames
+
+def fill_colorkey(img, key=None):
+    if key is None:
+        key = 255, 0, 255
+    img = img.convert_alpha()
+    frame = Surface(img.get_size())
+    frame.fill(key)
+    frame.set_colorkey(key)
+    frame.blit(img, (0, 0))
+    return frame
+
+def get_shadowed_text(text, font, offset, color, antialias=True, shadow_color=(0, 0, 0),
+                      colorkey=(255, 0, 255)):
+    foreground = font.render(text, antialias, color)
+    background = font.render(text, antialias, shadow_color)
+    alpha = SRCALPHA if antialias else 0
+    surface = Surface((foreground.get_width() + abs(offset[0]),
+                       foreground.get_height() + abs(offset[1])), alpha)
+    if not antialias:
+        surface.set_colorkey(colorkey)
+        surface.fill(colorkey)
+    surface.blit(background, ((abs(offset[0]) + offset[0]) / 2,
+                              (abs(offset[1]) + offset[1]) / 2))
+    surface.blit(foreground, ((abs(offset[0]) - offset[0]) / 2,
+                              (abs(offset[1]) - offset[1]) / 2))
+    return surface
+
+def get_blinds_rects(w, h, step=.05, count=4):
+    blinds = []
+    blind_h = int(round(h / float(count)))
+    for ii in range(1, count + 1):
+        blinds.append(Rect(0, blind_h * ii, w, 0))
+    inflate_h = int(round(blind_h * step))
+    if inflate_h < 1:
+        inflate_h = 1
+    rects = []
+    while blinds[0].h < blind_h:
+        rects.append([])
+        for blind in blinds:
+            bottom = blind.bottom
+            blind.inflate_ip(0, inflate_h)
+            blind.bottom = bottom
+            rects[-1].append(blind.copy())
+    return rects
+
+def get_blinds_frames(surface, step=.05, count=4, fill=(0, 0, 0, 0)):
+    frames = []
+    rects = []
+    h = int(round(surface.get_height() / float(count)))
+    for ii in range(1, count + 1):
+        rects.append(Rect(0, h * ii, surface.get_width(), 0))
+    bar_h = int(round(h * step))
+    if bar_h < 1:
+        bar_h = 1
+    while rects[0].h < h:
+        frame = surface.copy()
+        for rect in rects:
+            bottom = rect.bottom
+            rect.inflate_ip(0, bar_h)
+            rect.bottom = bottom
+            frame.fill(fill, rect)
+        frames.append(frame)
+    return frames
+
+def get_hsla_color(hue, saturation=100, lightness=50, alpha=100):
+    color = Color(0, 0, 0, 0)
+    color.hsla = hue % 360, saturation, lightness, alpha
+    return color
+
+def get_random_hsla_color(hue_range=(0, 359), saturation_range=(0, 100),
+                          lightness_range=(0, 100), alpha_range=(100, 100)):
+    return get_hsla_color(
+        random.randint(*hue_range), random.randint(*saturation_range), random.randint(*lightness_range),
+        random.randint(*alpha_range))
+
+def get_hsva_color(hue, saturation=100, value=100, alpha=100):
+    color = Color(0, 0, 0, 0)
+    color.hsva = hue % 360, saturation, value, alpha
+    return color
+
+def get_lightened_color(color, lightness):
+    h, s, _, a = color.hsla
+    return get_hsla_color(h, s, lightness, a)
+
+def get_glow_frames(radius, segments, colors=[(0, 0, 0), (255, 255, 255)], minsize=4, transparency=True):
+    frames = []
+    radius = int(round(radius))
+    sizes = [int(round(minsize + float(ii) / (segments - 1) * (radius - minsize))) for ii in range(segments)]
+    if transparency:
+        alpha_step = 255.0 / segments
+        alpha = alpha_step
+    else:
+        alpha = 255
+    for color_offset in range(len(colors)):
+        frame = Surface([radius * 2] * 2, SRCALPHA if transparency else None)
+        if transparency:
+            alpha = alpha_step
+        for segment_ii, segment_radius in enumerate(reversed(sizes)):
+            color = Color(*(colors[(color_offset + segment_ii) % len(colors)] + (int(round(alpha)),)))
+            gfxdraw.filled_circle(frame, radius, radius, int(round(segment_radius)), color)
+            if transparency:
+                alpha += alpha_step
+        frames.append(frame)
+    return frames
+
+# http://www.pygame.org/wiki/BezierCurve
+def compute_bezier_points(vertices, numPoints=60):
+    points = []
+    b0x = vertices[0][0]
+    b0y = vertices[0][1]
+    b1x = vertices[1][0]
+    b1y = vertices[1][1]
+    b2x = vertices[2][0]
+    b2y = vertices[2][1]
+    b3x = vertices[3][0]
+    b3y = vertices[3][1]
+    ax = -b0x + 3 * b1x + -3 * b2x + b3x
+    ay = -b0y + 3 * b1y + -3 * b2y + b3y
+    bx = 3 * b0x + -6 * b1x + 3 * b2x
+    by = 3 * b0y + -6 * b1y + 3 * b2y
+    cx = -3 * b0x + 3 * b1x
+    cy = -3 * b0y + 3 * b1y
+    dx = b0x
+    dy = b0y
+    numSteps = numPoints - 1
+    h = 1.0 / numSteps
+    pointX = dx
+    pointY = dy
+    firstFDX = ax * h ** 3 + bx * h ** 2 + cx * h
+    firstFDY = ay * h ** 3 + by * h ** 2 + cy * h
+    secondFDX = 6 * ax * h ** 3 + 2 * bx * h ** 2
+    secondFDY = 6 * ay * h ** 3 + 2 * by * h ** 2
+    thirdFDX = 6 * ax * h ** 3
+    thirdFDY = 6 * ay * h ** 3
+    points.append(Vector(pointX, pointY))
+    for i in range(numSteps):
+        pointX += firstFDX
+        pointY += firstFDY
+        firstFDX += secondFDX
+        firstFDY += secondFDY
+        secondFDX += thirdFDX
+        secondFDY += thirdFDY
+        points.append(Vector(pointX, pointY))
+    return points
diff --git a/camera_views/lib/pgfw/pgfw/gfx_extension.py b/camera_views/lib/pgfw/pgfw/gfx_extension.py
new file mode 100644 (file)
index 0000000..9e5aba2
--- /dev/null
@@ -0,0 +1,14 @@
+from pygame.gfxdraw import (aacircle, filled_circle, aatrigon, filled_trigon,
+                            aapolygon, filled_polygon)
+
+def aa_filled_circle(surface, cx, cy, radius, color):
+    aacircle(surface, cx, cy, radius, color)
+    filled_circle(surface, cx, cy, radius, color)
+
+def aa_filled_trigon(surface, x1, y1, x2, y2, x3, y3, color):
+    aatrigon(surface, x1, y1, x2, y2, x3, y3, color)
+    filled_trigon(surface, x1, y1, x2, y2, x3, y3, color)
+
+def aa_filled_polygon(surface, points, color):
+    aapolygon(surface, points, color)
+    filled_polygon(surface, points, color)
diff --git a/camera_views/lib/pgfw/sample.py b/camera_views/lib/pgfw/sample.py
new file mode 100644 (file)
index 0000000..e22d237
--- /dev/null
@@ -0,0 +1,24 @@
+from time import sleep
+from random import randint
+
+from pgfw.Game import Game
+
+# inheriting from Game allows you to customize your project
+class SampleGame(Game):
+
+    square_width = 30
+
+    # update runs every frame, you can think of it as the mainloop
+    def update(self):
+        sleep(1)
+        screen = self.get_screen()
+        bounds = screen.get_size()
+        screen.fill((0, 0, 0))
+        screen.fill((255, 255, 255),
+                    (randint(0, bounds[0]), randint(0, bounds[1]),
+                     self.square_width, self.square_width))
+
+
+if __name__ == '__main__':
+    # the play method begins the project's animation
+    SampleGame().run()
diff --git a/camera_views/lib/pgfw/setup.py b/camera_views/lib/pgfw/setup.py
new file mode 100644 (file)
index 0000000..b42da4b
--- /dev/null
@@ -0,0 +1,14 @@
+from distutils.core import setup
+
+setup(name="Pygame Framework",
+      version="0.1",
+      description="Classes to facilitate the creation of pygame projects",
+      author="Frank DeMarco",
+      author_email="frank.s.demarco@gmail.com",
+      url="http://usethematrixze.us",
+      packages=["pgfw"],
+      classifiers=["Development Status :: 2 - Pre-Alpha",
+                  "Environment :: Plugins",
+                  "Intended Audience :: Developers",
+                  "License :: Public Domain",
+                  "Programming Language :: Python :: 2.7"])
diff --git a/camera_views_mask.png b/camera_views_mask.png
new file mode 100644 (file)
index 0000000..695b9ed
Binary files /dev/null and b/camera_views_mask.png differ
index 91ca983..7b1730c 100644 (file)
@@ -8,35 +8,36 @@
 #     +--------+
 #
 #     LOLCAM📸 Twitch API chatbot listens for commands in the LOLCAM chat. Commands can be 
-#     submitted to control parts of the room seen on camera, for example to drive the robots
-#     in the room.
+#     submitted to control parts of the room on camera, for example to drive the robots
+#
 #
 #     +----------+
 #     | COMMANDS |
 #     +----------+
 #
-#     !halp ....... halp bot is here to halp
-#     !goodboy .... become goodboy
-#     !badboy ..... become badboy
-#     !flip ....... change window scene
-#     !party ...... a light show
-#     !ham ........ flash the ham sign
-#     !lamp ....... turn lamp on/off
-#     !fish ....... activate fish
-#     !fire ....... fireplace
-#     !dance ...... make halpbot dance
-#     !coom ....... keanu's wish
-#     !who ........ who is driving
+#     !halp ........ halp bot
+#     !goodboy ..... become goodboy robot
+#     !badboy ...... become badboy robot
+#     !ema ......... become emamouse robot
+#     !who ......... who is driving
+#     !vote [A/B] .. vote for A or B
+#     !flip ........ change window
+#     !party ....... light show
+#     !ham ......... flash the ham sign
+#     !lamp ........ lamp on/off
+#     !fish ........ fish
+#     !fire ........ fireplace
+#     !dance ....... halpbot dance
 #
 #     +------------------+
 #     | !goodboy/!badboy |
 #     +------------------+
 #
-#     forward ..... !advance !charge !inch
-#     backward .... !shrink !retreat !scooch
-#     turning ..... !turn !hang !swerve !180 !90 !45
-#     moves ....... !walk !spin !worm !pose !donut !ring !drill !bash !wheels !star !hand
-#     control ..... !swap
+#     !go [-10 - +10] .... move forward/backward
+#     !turn [-10 - +10] .. rotate
+#     !hand .............. open/close hand
+#     !swap [username] ... switch player
+#     moves .............. !walk !spin !worm !pose !donut !ring !drill !bash !wheels !star
 #
 
 import os, asyncio, threading, subprocess, time, twitchio, json
@@ -56,6 +57,7 @@ badboy=None
 greendriver=None
 yellowdriver=None
 timeLimit=180.0
+goodboy_name = "ema"
 
 @bot.event
 async def event_ready():
@@ -83,7 +85,7 @@ async def test(ctx):
 async def who(ctx):
     message = ""
     if goodboy is None:
-        message += "goodboy is free"
+        message += f"{goodboy_name} is free"
     else:
         message += f"{goodboy} is goodboy"
     message += " and "
@@ -93,32 +95,40 @@ async def who(ctx):
         message += f"{badboy} is badboy"
     await ctx.send(message)
 
+@bot.command(name="ema")
+async def ema(ctx):
+    await be_cum_goodboy(ctx, "ema")
+
 @bot.command(name="goodboy")
-async def be_cum_goodboy(ctx):
+async def be_the_goodboy(ctx):
+    await be_cum_goodboy(ctx)
+
+async def be_cum_goodboy(ctx, name="goodboy"):
+    goodboy_name = name
     if ctx.author.name == badboy:
-        await ctx.send(f"you are already badboy")
+        await ctx.send(f"[┐∵]┘ excuse me {ctx.author.name}, you are already badboy")
     else:
-        await you_can_be_cum(ctx, "goodboy")
+        await you_can_be_cum(ctx, name)
     
 @bot.command(name="badboy")
 async def be_cum_badboy(ctx):
     if ctx.author.name == goodboy:
-        await ctx.send(f"you are already goodboy")
+        await ctx.send(f"[┐∵]┘ excuse me {ctx.author.name}, you are already {goodboy_name}")
     else:
         await you_can_be_cum(ctx, "badboy")
-
+        
 async def you_can_be_cum(ctx, requested):
     global goodboy
     global badboy
     print(f"{ctx.author.name} is requesting {requested}")
-    if requested == "goodboy":
+    if requested in ("goodboy", "ema"):
         current_user = goodboy
     elif requested == "badboy":
         current_user = badboy
     if current_user == ctx.author.name:
         await ctx.send(f"you are already {requested}")
     elif current_user == None:
-        if requested == "goodboy":
+        if requested in ("goodboy", "ema"):
             goodboy = ctx.author.name
         elif requested == "badboy":
             badboy = ctx.author.name
@@ -129,13 +139,13 @@ async def you_can_be_cum(ctx, requested):
         print(f"wait for it again...")
         await asyncio.sleep(timeLimit)
         asyncio.create_task(ctx.send(f"{ctx.author.name} is no longer {requested}"))
-        if requested == "goodboy":
+        if requested in ("goodboy", "ema"):
             goodboy = None
         elif requested == "badboy":
             badboy = None
         print(f"{requested} reset")
     else:
-        await ctx.send(f"{current_user} is already {requested}")
+        await ctx.send(f"[┐∵]┘ excuse me {ctx.author.name}, {current_user} is already {requested}")
         
 @bot.command(name='greenbot')
 async def greenbot(ctx):
@@ -293,7 +303,7 @@ async def halp(ctx, command_name=""):
                 "!walk !spin !worm !pose !donut !ring !drill !bash !wheels !star !hand !swap ðŸ¤– "
                 "try submitting commands together (like !90 !advance !drill) ðŸ“ !halp [command] for more info"))
         else:
-            await ctx.send("use !goodboy !badboy !flip !party !ham !dance !who !vote ðŸ“ !halp [command] for more info")
+            await ctx.send("use !ema !badboy !flip !party !ham !dance !who !vote ðŸ“ !halp [command] for more info")
     else:
         await ctx.send(messages[command_name])
 
@@ -301,9 +311,9 @@ async def halp(ctx, command_name=""):
 async def swap(ctx, replacement=None):
     global goodboy, badboy
     if ctx.author.name not in (goodboy, badboy):
-        await ctx.send(f"only driver can swap out")
+        await ctx.send(f"[┐∵]┘ excuse me {ctx.author.name}, only driver can swap out")
     elif replacement is None:
-        await ctx.send(f"please specify replacement name")
+        await ctx.send(f"[┐∵]┘ excuse me {ctx.author.name}, please specify replacement")
     else:
         replacement = replacement.strip().lower()
         chatters = await bot.get_chatters(str(ctx.channel))
@@ -311,12 +321,12 @@ async def swap(ctx, replacement=None):
         if replacement == bot.nick:
             await ctx.send("lol")
         elif replacement == ctx.author.name:
-            await ctx.send(f"aren't you {replacement}?")
+            await ctx.send(f"[┐∵]┘ excuse me {ctx.author.name}, aren't you {replacement}?")
         elif replacement not in chatters.all:
-            await ctx.send(f"couldn't find {replacement}")
+            await ctx.send(f"[┐∵]┘ excuse me {ctx.author.name}, couldn't find {replacement}")
         elif goodboy == ctx.author.name:
             goodboy = replacement
-            await ctx.send(f"{replacement} swapped in for goodboy")
+            await ctx.send(f"{replacement} swapped in for {goodboy_name}")
         elif badboy == ctx.author.name:
             badboy = replacement
             await ctx.send(f"{replacement} swapped in for badboy")
@@ -339,12 +349,16 @@ async def flip(ctx):
 async def party(ctx):
     await timed_switch(ctx, 5)
     #await ctx.send(f"party lights are disabled during sports events")
+    
+@bot.command(name='lamp')
+async def lamp(ctx):
+    await toggle_switch(ctx, 4)
 
 @bot.command(name='ham')
 async def ham(ctx):
     await timed_switch(ctx, 2)
-
-async def timed_switch(ctx, switch_id):
+    
+async def toggle_switch(ctx, switch_id, timed=False):
     proc = await asyncio.create_subprocess_shell(
         "wemo status", stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
     stdout, stderr = await proc.communicate()
@@ -357,12 +371,16 @@ async def timed_switch(ctx, switch_id):
                 if status == "0":
                     print(f"turning on {switch_name}")
                     await asyncio.create_subprocess_shell(f"wemo switch {switch_name} on")
-                    await asyncio.sleep(30)
-                    print(f"turning off {switch_name}")
-                    await asyncio.create_subprocess_shell(f"wemo switch {switch_name} off")
+                    if timed:
+                        await asyncio.sleep(30)
+                        print(f"turning off {switch_name}")
+                        await asyncio.create_subprocess_shell(f"wemo switch {switch_name} off")
                 else:
                     print(f"turning off {switch_name}")
                     await asyncio.create_subprocess_shell(f"wemo switch {switch_name} off")
+
+async def timed_switch(ctx, switch_id):
+    await toggle_switch(ctx, switch_id, timed=True)
     
 #
 # BEAR COMMANDS:
@@ -371,127 +389,177 @@ async def timed_switch(ctx, switch_id):
 #
 
 @bot.command(name='star')
-async def star(ctx):
-    await robot(ctx, "star")
+async def star(ctx, *args):
+    await robot(ctx, ctx.command.name, *args)
 
 @bot.command(name='wheels')
-async def wheels(ctx):
-    await robot(ctx, "wheels")
+async def wheels(ctx, *args):
+    await robot(ctx, ctx.command.name, *args)
 
 @bot.command(name='bash')
-async def bash(ctx):
-    await robot(ctx, "bash")
+async def bash(ctx, *args):
+    await robot(ctx, ctx.command.name, *args)
 
 @bot.command(name='drill')
-async def drill(ctx):
-    await robot(ctx, "drill")
+async def drill(ctx, *args):
+    await robot(ctx, ctx.command.name, *args)
 
 @bot.command(name='ring')
-async def ring(ctx):
-    await robot(ctx, "ring")
+async def ring(ctx, *args):
+    await robot(ctx, ctx.command.name, *args)
 
 @bot.command(name='donut')
-async def donut(ctx):
-    await robot(ctx, "donut")
+async def donut(ctx, *args):
+    await robot(ctx, ctx.command.name, *args)
 
 @bot.command(name='pose')
-async def pose(ctx):
-    await robot(ctx, "pose")
+async def pose(ctx, *args):
+    await robot(ctx, ctx.command.name, *args)
 
 @bot.command(name='worm')
-async def worm(ctx):
-    await robot(ctx, "worm")
+async def worm(ctx, *args):
+    await robot(ctx, ctx.command.name, *args)
 
 @bot.command(name='spin')
-async def spin(ctx):
-    await robot(ctx, "spin")
+async def spin(ctx, *args):
+    await robot(ctx, ctx.command.name, *args)
 
 @bot.command(name='walk')
-async def walk(ctx):
-    await robot(ctx, "walk")
+async def walk(ctx, *args):
+    await robot(ctx, ctx.command.name, *args)
 
 @bot.command(name='swerve')
-async def swerve(ctx):
-    await robot(ctx, "swerve")
+async def swerve(ctx, *args):
+    await robot(ctx, ctx.command.name, *args)
 
 @bot.command(name='hang')
-async def hang(ctx):
-    await robot(ctx, "hang")
+async def hang(ctx, *args):
+    await robot(ctx, ctx.command.name, *args)
 
 @bot.command(name='turn')
-async def turn(ctx):
-    await robot(ctx, "turn")
+async def turn(ctx, *args):
+    await robot(ctx, ctx.command.name, *args)
 
 @bot.command(name='advance')
-async def advance(ctx):
-    await robot(ctx, "advance")
+async def advance(ctx, *args):
+    await robot(ctx, ctx.command.name, *args)
     
 @bot.command(name='retreat')
-async def retreat(ctx):
-    await robot(ctx, "retreat")
+async def retreat(ctx, *args):
+    await robot(ctx, ctx.command.name, *args)
     
 @bot.command(name='hand')
-async def hand(ctx):
-    await robot(ctx, "hand")
+async def hand(ctx, *args):
+    await robot(ctx, ctx.command.name, *args)
     
 @bot.command(name='charge')
-async def charge(ctx):
-    await robot(ctx, "charge")
+async def charge(ctx, *args):
+    await robot(ctx, ctx.command.name, *args)
     
 @bot.command(name='shrink')
-async def shrink(ctx):
-    await robot(ctx, "shrink")
+async def shrink(ctx, *args):
+    await robot(ctx, ctx.command.name, *args)
     
 @bot.command(name='inch')
-async def inch(ctx):
-    await robot(ctx, "inch")
+async def inch(ctx, *args):
+    await robot(ctx, ctx.command.name, *args)
     
 @bot.command(name='scooch')
-async def scooch(ctx):
-    await robot(ctx, "scooch")
+async def scooch(ctx, *args):
+    await robot(ctx, ctx.command.name, *args)
     
 @bot.command(name='45')
-async def rot_45(ctx):
-    await robot(ctx, "45")
+async def rot_45(ctx, *args):
+    await robot(ctx, ctx.command.name, *args)
     
 @bot.command(name='90')
-async def rot_90(ctx):
-    await robot(ctx, "90")
+async def rot_90(ctx, *args):
+    await robot(ctx, ctx.command.name, *args)
     
 @bot.command(name='180')
-async def rot_180(ctx):
-    await robot(ctx, "180")
+async def rot_180(ctx, *args):
+    await robot(ctx, ctx.command.name, *args)
+
+@bot.command(name="go")
+async def go(ctx, *args):
+    await robot(ctx, ctx.command.name, *args)
 
-async def robot(ctx, first_command):
+async def robot(ctx, *args):
     if ctx.author.name not in (goodboy, badboy):
         if goodboy is None:
-            await ctx.send(f"use !goodboy to become goodboy first")
+            await ctx.send(f"[┐∵]┘ excuse me {ctx.author.name}, use !{goodboy_name} to become {goodboy_name} first")
         elif badboy is None:
-            await ctx.send(f"use !badboy to become badboy first")
+            await ctx.send(f"[┐∵]┘ excuse me {ctx.author.name}, use !badboy to become badboy first")
         else:
-            await ctx.send(f"{goodboy} is goodboy and {badboy} is badboy")
+            await ctx.send(f"[┐∵]┘ excuse me {ctx.author.name}, {goodboy} is {goodboy_name} and {badboy} is badboy")
     else:
+        args = list(args)
+        # trim "!" part
+        if args[0].startswith("!"):
+            args[0] = args[1:]
         if ctx.author.name == goodboy:
             robot_id = 0
         else:
             robot_id = 1
-        commands = ctx.content.lower().split()
-        for ii, command in enumerate(commands):
-            if command.startswith("!"):
-                move = command[1:]
-                if move in ("advance", "retreat", "turn", "hang", "swerve", "walk", "spin", "worm", "pose", "donut", 
-                            "ring", "drill", "bash", "wheels", "star", "hand", "charge", "shrink", "inch", "scooch", "45", "90", "180"):
-                    subprocess_command = ["python", os.path.join("..", "send_command.py"), move, "--robot", str(robot_id)]
-                    if ii < len(commands) - 1:
-                        subprocess_command.append("--chain")
-                    print(f"sending {subprocess_command}...")
-                    subprocess.run(subprocess_command)
+        ii = 0
+        while ii < len(args):
+            arg = args[ii]
+            move = None
+            if arg in ("go", "turn"):
+                if ii + 1 < len(args) and not args[ii + 1].startswith("!"):
+                    amount = int(args[ii + 1])
+                    ii += 1
                 else:
-                    await ctx.send(f"{command} is not a supported robot move")
-                    if len(commands) > 1 and ii == len(commands) - 1:
-                        subprocess_command = ["python", os.path.join("..", "send_command.py"), "advance", "--robot", str(robot_id)]
-                        print(f"sending dummy {subprocess_command}...")
-                        subprocess.run(subprocess_command)
+                    amount = 5
+                if amount != 0:
+                    if arg == "go":
+                        if amount <= -8:
+                            move = f"{11 + 2 * (abs(amount) - 8)} 15 15"
+                        elif amount <= -4:
+                            move = f"{7 + 2 * (abs(amount) - 4)} 11 11"
+                        elif amount <= -1:
+                            move = f"{7 + 2 * (abs(amount) - 1)} 6 6"
+                        elif amount <= 3:
+                            move = f"{4 + 2 * (amount - 1)} 8 8"
+                        elif amount <= 7:
+                            move = f"{7 + 2 * (amount - 4)} 11 11"
+                        else:
+                            move = f"{11 + 2 * (amount - 8)} 15 15"
+                        if amount < 0:
+                            move += f" 0 1 {robot_id}"
+                        else:
+                            move += f" 1 0 {robot_id}"
+                    else:
+                        if abs(amount) < 4:
+                            move = f"{1 + 2 * (abs(amount) - 1)} 6 6"
+                        elif abs(amount) < 8:
+                            move = f"{3 + 2 * (abs(amount) - 4)} 9 9"
+                        else:
+                            move = f"{3 + 2 * (abs(amount) - 8)} 11 11"
+                        if amount < 0:
+                            move += f" 0 0 {robot_id}"
+                        else:
+                            move += f" 1 1 {robot_id}"
+                        if abs(amount) > 7:
+                            move += f" 0 4"
+                        elif abs(amount) > 3:
+                            move += f" 0 1"
+            elif arg in ("advance", "retreat", "turn", "hang", "swerve", "walk", "spin", "worm", "pose", "donut", 
+                        "ring", "drill", "bash", "wheels", "star", "hand", "charge", "shrink", "inch", "scooch", "45", "90", "180"):
+                move = arg
+            if move is not None:
+                subprocess_command = ["python", os.path.join("..", "send_command.py")] + move.split() + ["--robot", str(robot_id)]
+                if ii < len(args) - 1:
+                    subprocess_command.append("--chain")
+                print(f"sending {subprocess_command}...")
+                subprocess.run(subprocess_command)
+            else:
+                await ctx.send(f"[┐∵]┘ excuse me {ctx.author.name}, I don't know how to {arg}")
+                if len(args) > 1 and ii == len(args) - 1:
+                    subprocess_command = ["python", os.path.join("..", "send_command.py"), "advance", "--robot", str(robot_id)]
+                    print(f"sending dummy {subprocess_command}...")
+                    subprocess.run(subprocess_command)
+            ii += 1
 
 #stuff for boxing robots
     
diff --git a/mou_visual_novel/game/custom_atl.rpy b/mou_visual_novel/game/custom_atl.rpy
new file mode 100644 (file)
index 0000000..cafe9d5
--- /dev/null
@@ -0,0 +1,7 @@
+transform leave_left:
+    xalign .5 yalign 1.0
+    linear .5 xalign 0.0 yalign 1.0
+
+transform move_right:
+    xalign 0.0 yalign 1.0
+    linear .5 xalign .3 yalign 1.0
diff --git a/mouscreenshot1.png b/mouscreenshot1.png
new file mode 100644 (file)
index 0000000..200349a
Binary files /dev/null and b/mouscreenshot1.png differ
index 1980483..5a8c726 100755 (executable)
@@ -1,6 +1,6 @@
 #!/usr/bin/env python3
 
-import SerialDevice, argparse, pathlib, json
+import SerialDevice, argparse, pathlib, json, re
 
 class RFIDStations(SerialDevice.SerialDevice):
 
@@ -17,10 +17,13 @@ class RFIDStations(SerialDevice.SerialDevice):
             self.serial.reset_input_buffer()
             # waits until a line of data is received
             line = self.serial.readline().strip().decode()
-            fields = line.split()
-            rfid_id = int(fields[1])
-            card_id = "".join(fields[2:])
-            self.update_rfid_status_file(rfid_id, card_id)
+            if re.match(r"Reader [0-9] .{2} .{2} .{2} .{2}", line):
+                fields = line.split()
+                rfid_id = int(fields[1])
+                card_id = "".join(fields[2:])
+                self.update_rfid_status_file(rfid_id, card_id)
+            else:
+                print(f"unable to parse {line}")
 
     def get_status(self):
         with self.output_file_path.open("rt") as rfid_status_file:
index 8613981..d7f25c7 100644 (file)
@@ -3,27 +3,29 @@
         {
             "is_active": true,
             "cards": {
+                "F281F635": 5
             }
         },
         {
-            "is_active": false,
+            "is_active": true,
             "cards": {
+                "DA36B980": 22
             }
         },
         {
-            "is_active": false,
+            "is_active": true,
             "cards": {
+                "DA36B980": 42,
+                "F281F635": 11
             }
         },
         {
             "is_active": false,
-            "cards": {
-            }
+            "cards": {}
         },
         {
             "is_active": false,
-            "cards": {
-            }
+            "cards": {}
         }
     ]
-}
+}
\ No newline at end of file
index ad5fc7d..c78b3fb 100755 (executable)
@@ -1,9 +1,9 @@
 #!/usr/bin/env python3
 
-#          _      
-#     |   | | |   
-#     |   | | |   [ @ m ðŸ“¸
-#     '-- '-' '-- ````````
+#      __     ____   __   
+#     |  |   |  _ | |  |   
+#     |  :_  | '-'| |  :_   [ @ m ðŸ“¸
+#     '----' '----' '----'  ````````
 #
 #     This script launches a daemon process that waits for robot driving commands to appear in a text file
 #     and also sends commands to that text file when run separately. Those commands are forwarded by the daemon
diff --git a/ticker_fade_mask.png b/ticker_fade_mask.png
new file mode 100644 (file)
index 0000000..1ed6314
Binary files /dev/null and b/ticker_fade_mask.png differ
diff --git a/tromboner/Pink Trombone.html b/tromboner/Pink Trombone.html
new file mode 100644 (file)
index 0000000..ab47ceb
--- /dev/null
@@ -0,0 +1,1887 @@
+
+<!-- saved from url=(0029)https://dood.al/pinktrombone/ -->
+<html><link type="text/css" rel="stylesheet" id="dark-mode-custom-link"><link type="text/css" rel="stylesheet" id="dark-mode-general-link"><style lang="en" type="text/css" id="dark-mode-custom-style"></style><style lang="en" type="text/css" id="dark-mode-native-style"></style><!--
+
+P I N K   T R O M B O N E
+
+Bare-handed procedural speech synthesis
+
+version 1.1, March 2017
+by Neil Thapen
+venuspatrol.nfshost.com
+
+
+Bibliography
+
+Julius O. Smith III, "Physical audio signal processing for virtual musical instruments and audio effects."
+https://ccrma.stanford.edu/~jos/pasp/
+
+Story, Brad H. "A parametric model of the vocal tract area function for vowel and consonant simulation." 
+The Journal of the Acoustical Society of America 117.5 (2005): 3231-3254.
+
+Lu, Hui-Ling, and J. O. Smith. "Glottal source modeling for singing voice synthesis." 
+Proceedings of the 2000 International Computer Music Conference. 2000.
+
+Mullen, Jack. Physical modelling of the vocal tract with the 2D digital waveguide mesh. 
+PhD thesis, University of York, 2006.
+
+
+Copyright 2017 Neil Thapen 
+
+Permission is hereby granted, free of charge, to any person obtaining a 
+copy of this software and associated documentation files (the "Software"), 
+to deal in the Software without restriction, including without limitation 
+the rights to use, copy, modify, merge, publish, distribute, sublicense, 
+and/or sell copies of the Software, and to permit persons to whom the 
+Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in 
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 
+IN THE SOFTWARE.
+
+--><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+<title>Pink Trombone</title>
+<meta name="apple-mobile-web-app-capable" content="yes">
+<meta name="mobile-web-app-capable" content="yes">
+</head>
+
+<body style="margin: 5px 0px 0px 509px; padding: 0px; background-color: white; cursor: pointer;">
+
+<canvas id="tractCanvas" width="600" height="600" style="position: absolute; z-index: 1; background-color: transparent; margin: 0px; padding: 0px; width: 902px;">
+</canvas>
+<canvas id="backCanvas" width="600" height="600" style="position: absolute; z-index: 0; background-color: transparent; margin: 0px; padding: 0px; width: 902px;">
+</canvas>
+
+
+<script>
+
+Math.clamp = function(number, min, max) {
+    if (number<min) return min;
+    else if (number>max) return max;
+    else return number;
+}
+
+Math.moveTowards = function(current, target, amount)
+{
+    if (current<target) return Math.min(current+amount, target);
+    else return Math.max(current-amount, target);
+}
+
+Math.moveTowards = function(current, target, amountUp, amountDown)
+{
+    if (current<target) return Math.min(current+amountUp, target);
+    else return Math.max(current-amountDown, target);
+}
+
+Math.gaussian = function()
+{
+    var s = 0;
+    for (var c=0; c<16; c++) s+=Math.random();
+    return (s-8)/4;
+}
+
+
+var backCanvas = document.getElementById("backCanvas");
+var backCtx = backCanvas.getContext("2d");
+var tractCanvas = document.getElementById("tractCanvas");
+var tractCtx = tractCanvas.getContext("2d");
+
+var sampleRate;
+var time = 0;
+var temp = {a:0, b:0};
+var alwaysVoice = true;
+var autoWobble = true;
+var noiseFreq = 500;
+var noiseQ = 0.7;
+var palePink = "#FFEEF5";
+var isFirefox = false;
+var browser=navigator.userAgent.toLowerCase();
+if (browser.indexOf('firefox') > -1) isFirefox = true;
+
+var UI = 
+{ 
+    width : 600,
+    top_margin : 5,
+    left_margin : 5,
+    inAboutScreen : true,
+    inInstructionsScreen : false,
+    instructionsLine : 0,
+    debugText : "",
+    
+    init : function()
+    {
+        this.touchesWithMouse = [];
+        this.mouseTouch = {alive: false, endTime: 0};
+        this.mouseDown = false;
+        
+        this.aboutButton = makeButton(460, 392, 140, 30, "about...", true);         
+        this.alwaysVoiceButton = makeButton(460, 428, 140, 30, "always voice", true);
+        this.autoWobbleButton = makeButton(460, 464, 140, 30, "pitch wobble", true); 
+
+        tractCanvas.addEventListener('touchstart', UI.startTouches);
+        tractCanvas.addEventListener('touchmove', UI.moveTouches);
+        tractCanvas.addEventListener('touchend', UI.endTouches);     
+        tractCanvas.addEventListener('touchcancel', UI.endTouches);  
+        
+        document.addEventListener('touchstart', (function(event) {event.preventDefault();}) );
+        
+        document.addEventListener('mousedown', (function(event)
+            {UI.mouseDown = true; event.preventDefault(); UI.startMouse(event);}));
+        document.addEventListener('mouseup', (function(event)
+            {UI.mouseDown = false; UI.endMouse(event);}));
+        document.addEventListener('mousemove', UI.moveMouse);      
+    },
+    
+    draw : function()
+    {
+        this.alwaysVoiceButton.draw(tractCtx);
+        this.autoWobbleButton.draw(tractCtx);
+        this.aboutButton.draw(tractCtx);
+        if (this.inAboutScreen) this.drawAboutScreen();
+        else if (this.inInstructionsScreen) this.drawInstructionsScreen();
+    },
+    
+    drawAboutScreen :  function()
+    {
+        var ctx = tractCtx;
+        ctx.globalAlpha = 0.8;
+        ctx.fillStyle = "white";
+        ctx.rect(0,0,600,600);
+        ctx.fill();   
+    
+        this.drawAboutText();
+    },
+    
+    drawAboutText : function()
+    {
+        var ctx = tractCtx;
+        ctx.globalAlpha = 1.0;
+        ctx.fillStyle = "#C070C6";
+        ctx.strokeStyle = "#C070C6";
+        ctx.font="50px Arial";
+        ctx.lineWidth = 3;
+        ctx.textAlign = "center";
+        ctx.strokeText("P i n k   T r o m b o n e", 300, 230);
+        ctx.fillText("P i n k   T r o m b o n e", 300, 230);
+        
+        ctx.font="28px Arial";
+        ctx.fillText("bare-handed  speech synthesis", 300, 330);
+
+        ctx.font="20px Arial";        
+        //ctx.fillText("(tap to start)", 300, 380);   
+
+        if (isFirefox) 
+        {
+            ctx.font="20px Arial";        
+            ctx.fillText("(sorry - may work poorly with the Firefox browser)", 300, 430);  
+        }
+    },
+
+    
+    drawInstructionsScreen :  function()
+    {
+        AudioSystem.mute();
+        var ctx = tractCtx;
+        ctx.globalAlpha = 0.85;
+        ctx.fillStyle = "white";
+        ctx.rect(0,0,600,600);
+        ctx.fill();   
+        
+        ctx.globalAlpha = 1.0;
+        ctx.fillStyle = "#C070C6";
+        ctx.strokeStyle = "#C070C6";
+        ctx.font="24px Arial";
+        ctx.lineWidth = 2;
+        ctx.textAlign = "center";
+        
+        ctx.font = "19px Arial";
+        ctx.textAlign = "left";
+        this.instructionsLine = 0;
+        this.write("Sound is generated in the glottis (at the bottom left) then ");
+        this.write("filtered by the shape of the vocal tract. The voicebox ");
+        this.write("controls the pitch and intensity of the initial sound.");
+        this.write("");
+        this.write("Then, to talk:");
+        this.write("");
+        this.write("- move the body of the tongue to shape vowels");
+        this.write("");
+        this.write("- touch the oral cavity to narrow it, for fricative consonants");
+        this.write("");
+        this.write("- touch above the oral cavity to close it, for stop consonants");
+        this.write("");
+        this.write("- touch the nasal cavity to open the velum and let sound ");
+        this.write("   flow through the nose.");
+        this.write("");
+        this.write("");
+        this.write("(tap anywhere to continue)");
+        
+        ctx.textAlign = "center";
+        ctx.fillText("[tap here to RESET]", 470, 535);
+        
+        this.instructionsLine = 18.8;
+        ctx.textAlign = "left";
+        this.write("Pink Trombone v1.1");
+        this.write("by Neil Thapen");
+        ctx.fillStyle = "blue";
+        ctx.globalAlpha = 0.6;
+        this.write("venuspatrol.nfshost.com");
+        
+        /*ctx.beginPath();
+        ctx.rect(35, 535, 230, 35);
+        ctx.rect(370, 505, 200, 50);
+        ctx.fill();*/
+        
+        ctx.globalAlpha = 1.0;
+    },
+
+    
+    instructionsScreenHandleTouch : function(x,y)
+    {
+        if ((x >=35 && x<=265) && (y>=535 && y<=570)) window.location.href = "http://venuspatrol.nfshost.com";
+        else if ((x>=370 && x<=570) && (y>=505 && y<=555)) location.reload(false);
+        else
+        {
+            UI.inInstructionsScreen = false;
+            UI.aboutButton.switchedOn = true;
+            AudioSystem.unmute();
+        }
+    },
+    
+    write : function(text)
+    {
+        tractCtx.fillText(text, 50, 100 + this.instructionsLine*22);
+        this.instructionsLine += 1;
+        if (text == "") this.instructionsLine -= 0.3;
+    },
+    
+    buttonsHandleTouchStart : function(touch)
+    {
+        this.alwaysVoiceButton.handleTouchStart(touch);
+        alwaysVoice = this.alwaysVoiceButton.switchedOn;
+        this.autoWobbleButton.handleTouchStart(touch);
+        autoWobble = this.autoWobbleButton.switchedOn;
+        this.aboutButton.handleTouchStart(touch);
+
+    },
+    
+    startTouches : function(event)
+    {
+        event.preventDefault();    
+        if (!AudioSystem.started)
+        {
+            AudioSystem.started = true;
+            AudioSystem.startSound();
+        }
+        
+        if (UI.inAboutScreen)
+        {
+            UI.inAboutScreen = false;
+            return;
+        }
+        
+        if (UI.inInstructionsScreen)
+        {
+            var touches = event.changedTouches;
+            for (var j=0; j<touches.length; j++)        
+            {
+                var x = (touches[j].pageX-UI.left_margin)/UI.width*600;
+                var y = (touches[j].pageY-UI.top_margin)/UI.width*600;
+            }
+            UI.instructionsScreenHandleTouch(x,y);
+            return;
+        }
+        
+
+        var touches = event.changedTouches;
+        for (var j=0; j<touches.length; j++)        
+        {
+            var touch = {};
+            touch.startTime = time;
+            touch.endTime = 0;
+            touch.fricative_intensity = 0;            
+            touch.alive = true;
+            touch.id = touches[j].identifier;
+            touch.x = (touches[j].pageX-UI.left_margin)/UI.width*600;
+            touch.y = (touches[j].pageY-UI.top_margin)/UI.width*600;
+            touch.index = TractUI.getIndex(touch.x, touch.y);
+            touch.diameter = TractUI.getDiameter(touch.x, touch.y);
+            UI.touchesWithMouse.push(touch);
+            UI.buttonsHandleTouchStart(touch);            
+        }    
+
+        UI.handleTouches();
+    },
+    
+    getTouchById : function(id)
+    {
+        for (var j=0; j<UI.touchesWithMouse.length; j++)
+        {
+            if (UI.touchesWithMouse[j].id == id && UI.touchesWithMouse[j].alive) return UI.touchesWithMouse[j];
+        }
+        return 0;
+    },
+    
+    moveTouches : function(event)
+    {
+        var touches = event.changedTouches;
+        for (var j=0; j<touches.length; j++)        
+        {
+            var touch = UI.getTouchById(touches[j].identifier);
+            if (touch != 0)
+            {
+                touch.x = (touches[j].pageX-UI.left_margin)/UI.width*600;
+                touch.y = (touches[j].pageY-UI.top_margin)/UI.width*600;
+                touch.index = TractUI.getIndex(touch.x, touch.y);
+                touch.diameter = TractUI.getDiameter(touch.x, touch.y);
+            }
+        }   
+        UI.handleTouches();
+    },
+    
+    endTouches : function(event)
+    {
+        var touches = event.changedTouches;
+        for (var j=0; j<touches.length; j++)        
+        {
+            var touch = UI.getTouchById(touches[j].identifier);
+            if (touch != 0)
+            {
+                touch.alive = false;
+                touch.endTime = time; 
+            }
+        }   
+        UI.handleTouches();
+        
+        if (!UI.aboutButton.switchedOn) 
+        {
+            UI.inInstructionsScreen = true;
+        }
+    },
+      
+    startMouse : function(event)
+    {
+        if (!AudioSystem.started)
+        {
+            AudioSystem.started = true;
+            AudioSystem.startSound();
+        }
+        if (UI.inAboutScreen)
+        {
+            UI.inAboutScreen = false;
+            return;
+        }
+        if (UI.inInstructionsScreen)
+        {
+            var x = (event.pageX-tractCanvas.offsetLeft)/UI.width*600;
+            var y = (event.pageY-tractCanvas.offsetTop)/UI.width*600;
+            UI.instructionsScreenHandleTouch(x,y);
+            return;
+        }
+        
+        var touch = {};
+        touch.startTime = time;
+        touch.fricative_intensity = 0;
+        touch.endTime = 0;
+        touch.alive = true;
+        touch.id = "mouse"+Math.random();
+        touch.x = (event.pageX-tractCanvas.offsetLeft)/UI.width*600;
+        touch.y = (event.pageY-tractCanvas.offsetTop)/UI.width*600;
+        touch.index = TractUI.getIndex(touch.x, touch.y);
+        touch.diameter = TractUI.getDiameter(touch.x, touch.y);
+        UI.mouseTouch = touch;
+        UI.touchesWithMouse.push(touch);   
+        UI.buttonsHandleTouchStart(touch);
+        UI.handleTouches();
+    },    
+
+    moveMouse : function(event)
+    {
+        var touch = UI.mouseTouch;
+        if (!touch.alive) return;
+        touch.x = (event.pageX-tractCanvas.offsetLeft)/UI.width*600;
+        touch.y = (event.pageY-tractCanvas.offsetTop)/UI.width*600;
+        touch.index = TractUI.getIndex(touch.x, touch.y);
+        touch.diameter = TractUI.getDiameter(touch.x, touch.y); 
+        UI.handleTouches();
+    },
+    
+    endMouse : function(event)
+    {
+        var touch = UI.mouseTouch;
+        if (!touch.alive) return;
+        touch.alive = false;
+        touch.endTime = time; 
+        UI.handleTouches();
+        
+        if (!UI.aboutButton.switchedOn) UI.inInstructionsScreen = true;  
+    },
+    
+    handleTouches : function(event)
+    {
+        TractUI.handleTouches();
+        Glottis.handleTouches();
+    },
+    
+    updateTouches : function()
+    {
+        var fricativeAttackTime = 0.1;
+        for (var j=UI.touchesWithMouse.length-1; j >=0; j--)
+        {
+            var touch = UI.touchesWithMouse[j];
+            if (!(touch.alive) && (time > touch.endTime + 1))
+            {
+                UI.touchesWithMouse.splice(j,1);
+            }
+            else if (touch.alive) 
+            {
+                touch.fricative_intensity = Math.clamp((time-touch.startTime)/fricativeAttackTime, 0, 1);
+            }
+            else
+            {
+                touch.fricative_intensity = Math.clamp(1-(time-touch.endTime)/fricativeAttackTime, 0, 1);
+            }
+        }
+    },
+        
+    shapeToFitScreen : function()
+    {
+        if (window.innerWidth <= window.innerHeight)
+        {
+            this.width = window.innerWidth-10;
+            this.left_margin = 5;
+            this.top_margin = 0.5*(window.innerHeight-this.width);
+        }
+        else
+        {
+            this.width = window.innerHeight-10;
+            this.left_margin = 0.5*(window.innerWidth-this.width);
+            this.top_margin = 5;
+        }
+        document.body.style.marginLeft = this.left_margin;
+        document.body.style.marginTop = this.top_margin;
+        tractCanvas.style.width = this.width;
+        backCanvas.style.width = this.width;  
+    }
+}
+
+
+var AudioSystem = 
+{   
+    blockLength : 512,
+    blockTime : 1,
+    started : false,
+    soundOn : false,
+
+    init : function ()
+    {
+        window.AudioContext = window.AudioContext||window.webkitAudioContext;
+        this.audioContext = new window.AudioContext();      
+        sampleRate = this.audioContext.sampleRate;
+        
+        this.blockTime = this.blockLength/sampleRate;
+    },
+    
+    startSound : function()
+    {
+        //scriptProcessor may need a dummy input channel on iOS
+        this.scriptProcessor = this.audioContext.createScriptProcessor(this.blockLength, 2, 1);
+        this.scriptProcessor.connect(this.audioContext.destination); 
+        this.scriptProcessor.onaudioprocess = AudioSystem.doScriptProcessor;
+    
+        var whiteNoise = this.createWhiteNoiseNode(2*sampleRate); // 2 seconds of noise
+        
+        var aspirateFilter = this.audioContext.createBiquadFilter();
+        aspirateFilter.type = "bandpass";
+        aspirateFilter.frequency.value = 500;
+        aspirateFilter.Q.value = 0.5;
+        whiteNoise.connect(aspirateFilter);
+        aspirateFilter.connect(this.scriptProcessor);  
+        
+        var fricativeFilter = this.audioContext.createBiquadFilter();
+        fricativeFilter.type = "bandpass";
+        fricativeFilter.frequency.value = 1000;
+        fricativeFilter.Q.value = 0.5;
+        whiteNoise.connect(fricativeFilter);
+        fricativeFilter.connect(this.scriptProcessor);        
+        
+        whiteNoise.start(0);
+    },
+    
+    createWhiteNoiseNode : function(frameCount)
+    {
+        var myArrayBuffer = this.audioContext.createBuffer(1, frameCount, sampleRate);
+
+        var nowBuffering = myArrayBuffer.getChannelData(0);
+        for (var i = 0; i < frameCount; i++) 
+        {
+            nowBuffering[i] = Math.random();// gaussian();
+        }
+
+        var source = this.audioContext.createBufferSource();
+        source.buffer = myArrayBuffer;
+        source.loop = true;
+
+        return source;
+    },
+    
+    
+    doScriptProcessor : function(event) 
+    {
+        var inputArray1 = event.inputBuffer.getChannelData(0);
+        var inputArray2 = event.inputBuffer.getChannelData(1);
+        var outArray = event.outputBuffer.getChannelData(0);
+        for (var j = 0, N = outArray.length; j < N; j++)
+        {
+            var lambda1 = j/N;
+            var lambda2 = (j+0.5)/N;
+            var glottalOutput = Glottis.runStep(lambda1, inputArray1[j]); 
+            
+            var vocalOutput = 0;
+            //Tract runs at twice the sample rate 
+            Tract.runStep(glottalOutput, inputArray2[j], lambda1);
+            vocalOutput += Tract.lipOutput + Tract.noseOutput;
+            Tract.runStep(glottalOutput, inputArray2[j], lambda2);
+            vocalOutput += Tract.lipOutput + Tract.noseOutput;
+            outArray[j] = vocalOutput * 0.125;
+        }
+        Glottis.finishBlock();
+        Tract.finishBlock();
+    },
+    
+    mute : function()
+    {
+        this.scriptProcessor.disconnect();
+    },
+    
+    unmute : function()
+    {
+        this.scriptProcessor.connect(this.audioContext.destination); 
+    }
+    
+}
+
+   
+var Glottis =
+{
+    timeInWaveform : 0,
+    oldFrequency : 140,
+    newFrequency : 140,
+    UIFrequency : 140,
+    smoothFrequency : 140,
+    oldTenseness : 0.6,
+    newTenseness : 0.6,
+    UITenseness : 0.6,
+    totalTime : 0,
+    vibratoAmount : 0.005,
+    vibratoFrequency : 6,
+    intensity : 0,
+    loudness : 1,
+    isTouched : false,
+    ctx : backCtx,
+    touch : 0,
+    x : 240,
+    y : 530,
+    
+    keyboardTop : 500,
+    keyboardLeft : 00,
+    keyboardWidth : 600,
+    keyboardHeight : 100,
+    semitones : 20,
+    marks : [0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0],
+    baseNote : 87.3071, //F
+    
+    init : function()
+    {
+        this.setupWaveform(0);
+        this.drawKeyboard();
+    },
+    
+    drawKeyboard : function()
+    {      
+        this.ctx.strokeStyle = palePink;
+        this.ctx.fillStyle = palePink;        
+        backCtx.globalAlpha = 1.0;     
+        backCtx.lineCap = 'round';        
+        backCtx.lineJoin = 'round';        
+    
+        var radius = 2;
+        
+        this.drawBar(0.0, 0.4, 8);
+        backCtx.globalAlpha = 0.7;         
+        this.drawBar(0.52, 0.72, 8);
+        
+        backCtx.strokeStyle = "orchid";   
+        backCtx.fillStyle = "orchid";
+        for (var i=0; i< this.semitones; i++)
+        {
+            var keyWidth = this.keyboardWidth/this.semitones;
+            var x = this.keyboardLeft+(i+1/2)*keyWidth;
+            var y = this.keyboardTop;
+            if (this.marks[(i+3)%12]==1)
+            {
+                backCtx.lineWidth = 4;
+                backCtx.globalAlpha = 0.4;  
+            }
+            else             
+            {
+                backCtx.lineWidth = 3;
+                backCtx.globalAlpha = 0.2;  
+            }
+            backCtx.beginPath();
+            backCtx.moveTo(x,y+9);
+            backCtx.lineTo(x, y+this.keyboardHeight*0.4-9);
+            backCtx.stroke();
+            
+            backCtx.lineWidth = 3;
+            backCtx.globalAlpha = 0.15;   
+            
+            backCtx.beginPath();
+            backCtx.moveTo(x,y+this.keyboardHeight*0.52+6);
+            backCtx.lineTo(x, y+this.keyboardHeight*0.72-6);
+            backCtx.stroke();  
+          
+        }
+        
+        backCtx.fillStyle = "orchid";
+        backCtx.font="17px Arial";
+        backCtx.textAlign = "center";
+        backCtx.globalAlpha = 0.7; 
+        backCtx.fillText("voicebox control", 300, 490);
+        backCtx.fillText("pitch", 300, 592);
+        backCtx.globalAlpha = 0.3; 
+        backCtx.strokeStyle = "orchid";
+        backCtx.fillStyle = "orchid";  
+        backCtx.save()
+        backCtx.translate(410, 587);
+        this.drawArrow(80, 2, 10);
+        backCtx.translate(-220, 0);
+        backCtx.rotate(Math.PI);
+        this.drawArrow(80, 2, 10);
+        backCtx.restore(); 
+        backCtx.globalAlpha=1.0;        
+    },
+    
+    drawBar : function(topFactor, bottomFactor, radius)
+    {
+        backCtx.lineWidth = radius*2; 
+        backCtx.beginPath();
+        backCtx.moveTo(this.keyboardLeft+radius, this.keyboardTop+topFactor*this.keyboardHeight+radius);
+        backCtx.lineTo(this.keyboardLeft+this.keyboardWidth-radius, this.keyboardTop+topFactor*this.keyboardHeight+radius);
+        backCtx.lineTo(this.keyboardLeft+this.keyboardWidth-radius, this.keyboardTop+bottomFactor*this.keyboardHeight-radius);
+        backCtx.lineTo(this.keyboardLeft+radius, this.keyboardTop+bottomFactor*this.keyboardHeight-radius);
+        backCtx.closePath();
+        backCtx.stroke();
+        backCtx.fill();
+    },
+    
+    drawArrow : function(l, ahw, ahl)
+    {
+        backCtx.lineWidth = 2;
+        backCtx.beginPath();
+        backCtx.moveTo(-l, 0);
+        backCtx.lineTo(0,0);
+        backCtx.lineTo(0, -ahw);
+        backCtx.lineTo(ahl, 0);
+        backCtx.lineTo(0, ahw);
+        backCtx.lineTo(0,0);
+        backCtx.closePath();
+        backCtx.stroke();
+        backCtx.fill();
+    },
+    
+    handleTouches :  function()
+    {
+        if (this.touch != 0 && !this.touch.alive) this.touch = 0;
+        
+        if (this.touch == 0)
+        {        
+            for (var j=0; j<UI.touchesWithMouse.length; j++)  
+            {
+                var touch = UI.touchesWithMouse[j];
+                if (!touch.alive) continue;
+                if (touch.y<this.keyboardTop) continue;
+                this.touch = touch;
+            }    
+        }
+        
+        if (this.touch != 0)
+        {
+            var local_y = this.touch.y -  this.keyboardTop-10;
+            var local_x = this.touch.x - this.keyboardLeft;
+            local_y = Math.clamp(local_y, 0, this.keyboardHeight-26);
+            var semitone = this.semitones * local_x / this.keyboardWidth + 0.5;
+            Glottis.UIFrequency = this.baseNote * Math.pow(2, semitone/12);
+            if (Glottis.intensity == 0) Glottis.smoothFrequency = Glottis.UIFrequency;
+            //Glottis.UIRd = 3*local_y / (this.keyboardHeight-20);
+            var t = Math.clamp(1-local_y / (this.keyboardHeight-28), 0, 1);
+            Glottis.UITenseness = 1-Math.cos(t*Math.PI*0.5);
+            Glottis.loudness = Math.pow(Glottis.UITenseness, 0.25);
+            this.x = this.touch.x;
+            this.y = local_y + this.keyboardTop+10;
+        }
+        Glottis.isTouched = (this.touch != 0);
+    },
+        
+    runStep : function(lambda, noiseSource)
+    {
+        var timeStep = 1.0 / sampleRate; 
+        this.timeInWaveform += timeStep;
+        this.totalTime += timeStep;
+        if (this.timeInWaveform>this.waveformLength) 
+        {
+            this.timeInWaveform -= this.waveformLength;
+            this.setupWaveform(lambda);
+        }
+        var out = this.normalizedLFWaveform(this.timeInWaveform/this.waveformLength);
+        var aspiration = this.intensity*(1-Math.sqrt(this.UITenseness))*this.getNoiseModulator()*noiseSource;
+        aspiration *= 0.2 + 0.02*noise.simplex1(this.totalTime * 1.99);
+        out += aspiration;
+        return out;
+    },
+    
+    getNoiseModulator : function()
+    {
+        var voiced = 0.1+0.2*Math.max(0,Math.sin(Math.PI*2*this.timeInWaveform/this.waveformLength));
+        //return 0.3;
+        return this.UITenseness* this.intensity * voiced + (1-this.UITenseness* this.intensity ) * 0.3;
+    },
+    
+    finishBlock : function()
+    {
+        var vibrato = 0;
+        vibrato += this.vibratoAmount * Math.sin(2*Math.PI * this.totalTime *this.vibratoFrequency);          
+        vibrato += 0.02 * noise.simplex1(this.totalTime * 4.07);
+        vibrato += 0.04 * noise.simplex1(this.totalTime * 2.15);
+        if (autoWobble)
+        {
+            vibrato += 0.2 * noise.simplex1(this.totalTime * 0.98);
+            vibrato += 0.4 * noise.simplex1(this.totalTime * 0.5);
+        }
+        if (this.UIFrequency>this.smoothFrequency) 
+            this.smoothFrequency = Math.min(this.smoothFrequency * 1.1, this.UIFrequency);
+        if (this.UIFrequency<this.smoothFrequency) 
+            this.smoothFrequency = Math.max(this.smoothFrequency / 1.1, this.UIFrequency);
+        this.oldFrequency = this.newFrequency;
+        this.newFrequency = this.smoothFrequency * (1+vibrato);
+        this.oldTenseness = this.newTenseness;
+        this.newTenseness = this.UITenseness
+            + 0.1*noise.simplex1(this.totalTime*0.46)+0.05*noise.simplex1(this.totalTime*0.36);
+        if (!this.isTouched && alwaysVoice) this.newTenseness += (3-this.UITenseness)*(1-this.intensity);
+        
+        if (this.isTouched || alwaysVoice) this.intensity += 0.13;
+        else this.intensity -= 0.05;
+        this.intensity = Math.clamp(this.intensity, 0, 1);
+    },    
+    
+    setupWaveform : function(lambda)
+    {
+        this.frequency = this.oldFrequency*(1-lambda) + this.newFrequency*lambda;
+        var tenseness = this.oldTenseness*(1-lambda) + this.newTenseness*lambda;
+        this.Rd = 3*(1-tenseness);
+        this.waveformLength = 1.0/this.frequency;
+        
+        var Rd = this.Rd;
+        if (Rd<0.5) Rd = 0.5;
+        if (Rd>2.7) Rd = 2.7;
+        var output;
+        // normalized to time = 1, Ee = 1
+        var Ra = -0.01 + 0.048*Rd;
+        var Rk = 0.224 + 0.118*Rd;
+        var Rg = (Rk/4)*(0.5+1.2*Rk)/(0.11*Rd-Ra*(0.5+1.2*Rk));
+        
+        var Ta = Ra;
+        var Tp = 1 / (2*Rg);
+        var Te = Tp + Tp*Rk; //
+        
+        var epsilon = 1/Ta;
+        var shift = Math.exp(-epsilon * (1-Te));
+        var Delta = 1 - shift; //divide by this to scale RHS
+           
+        var RHSIntegral = (1/epsilon)*(shift - 1) + (1-Te)*shift;
+        RHSIntegral = RHSIntegral/Delta;
+        
+        var totalLowerIntegral = - (Te-Tp)/2 + RHSIntegral;
+        var totalUpperIntegral = -totalLowerIntegral;
+        
+        var omega = Math.PI/Tp;
+        var s = Math.sin(omega*Te);
+        // need E0*e^(alpha*Te)*s = -1 (to meet the return at -1)
+        // and E0*e^(alpha*Tp/2) * Tp*2/pi = totalUpperIntegral 
+        //             (our approximation of the integral up to Tp)
+        // writing x for e^alpha,
+        // have E0*x^Te*s = -1 and E0 * x^(Tp/2) * Tp*2/pi = totalUpperIntegral
+        // dividing the second by the first,
+        // letting y = x^(Tp/2 - Te),
+        // y * Tp*2 / (pi*s) = -totalUpperIntegral;
+        var y = -Math.PI*s*totalUpperIntegral / (Tp*2);
+        var z = Math.log(y);
+        var alpha = z/(Tp/2 - Te);
+        var E0 = -1 / (s*Math.exp(alpha*Te));
+        this.alpha = alpha;
+        this.E0 = E0;
+        this.epsilon = epsilon;
+        this.shift = shift;
+        this.Delta = Delta;
+        this.Te=Te;
+        this.omega = omega;
+    },
+    
+    normalizedLFWaveform: function(t)
+    {     
+        if (t>this.Te) output = (-Math.exp(-this.epsilon * (t-this.Te)) + this.shift)/this.Delta;
+        else output = this.E0 * Math.exp(this.alpha*t) * Math.sin(this.omega * t);
+     
+        return output * this.intensity * this.loudness;
+    }
+}
+
+
+var Tract = 
+{
+    n : 44,
+    bladeStart : 10,
+    tipStart : 32,
+    lipStart : 39,
+    R : [], //component going right
+    L : [], //component going left
+    reflection : [],
+    junctionOutputR : [],
+    junctionOutputL : [],
+    maxAmplitude : [],
+    diameter : [],
+    restDiameter : [],
+    targetDiameter : [],
+    newDiameter : [],
+    A : [],
+    glottalReflection : 0.75,
+    lipReflection : -0.85,
+    lastObstruction : -1,
+    fade : 1.0, //0.9999,
+    movementSpeed : 15, //cm per second
+    transients : [],
+    lipOutput : 0,
+    noseOutput : 0,
+    velumTarget : 0.01,
+
+    init : function()
+    {
+        this.bladeStart = Math.floor(this.bladeStart*this.n/44);
+        this.tipStart = Math.floor(this.tipStart*this.n/44);
+        this.lipStart = Math.floor(this.lipStart*this.n/44);        
+        this.diameter = new Float64Array(this.n);
+        this.restDiameter = new Float64Array(this.n);
+        this.targetDiameter = new Float64Array(this.n);
+        this.newDiameter = new Float64Array(this.n);
+        for (var i=0; i<this.n; i++)
+        {
+            var diameter = 0;
+            if (i<7*this.n/44-0.5) diameter = 0.6;
+            else if (i<12*this.n/44) diameter = 1.1;
+            else diameter = 1.5;
+            this.diameter[i] = this.restDiameter[i] = this.targetDiameter[i] = this.newDiameter[i] = diameter;
+        }
+        this.R = new Float64Array(this.n);
+        this.L = new Float64Array(this.n);
+        this.reflection = new Float64Array(this.n+1);
+        this.newReflection = new Float64Array(this.n+1);
+        this.junctionOutputR = new Float64Array(this.n+1);
+        this.junctionOutputL = new Float64Array(this.n+1);
+        this.A =new Float64Array(this.n);
+        this.maxAmplitude = new Float64Array(this.n);
+        
+        this.noseLength = Math.floor(28*this.n/44)
+        this.noseStart = this.n-this.noseLength + 1;
+        this.noseR = new Float64Array(this.noseLength);
+        this.noseL = new Float64Array(this.noseLength);
+        this.noseJunctionOutputR = new Float64Array(this.noseLength+1);
+        this.noseJunctionOutputL = new Float64Array(this.noseLength+1);        
+        this.noseReflection = new Float64Array(this.noseLength+1);
+        this.noseDiameter = new Float64Array(this.noseLength);
+        this.noseA = new Float64Array(this.noseLength);
+        this.noseMaxAmplitude = new Float64Array(this.noseLength);
+        for (var i=0; i<this.noseLength; i++)
+        {
+            var diameter;
+            var d = 2*(i/this.noseLength);
+            if (d<1) diameter = 0.4+1.6*d;
+            else diameter = 0.5+1.5*(2-d);
+            diameter = Math.min(diameter, 1.9);
+            this.noseDiameter[i] = diameter;
+        }       
+        this.newReflectionLeft = this.newReflectionRight = this.newReflectionNose = 0;
+        this.calculateReflections();        
+        this.calculateNoseReflections();
+        this.noseDiameter[0] = this.velumTarget;
+    },     
+    
+    reshapeTract : function(deltaTime)
+    {
+        var amount = deltaTime * this.movementSpeed; ;    
+        var newLastObstruction = -1;
+        for (var i=0; i<this.n; i++)
+        {
+            var diameter = this.diameter[i];
+            var targetDiameter = this.targetDiameter[i];
+            if (diameter <= 0) newLastObstruction = i;
+            var slowReturn; 
+            if (i<this.noseStart) slowReturn = 0.6;
+            else if (i >= this.tipStart) slowReturn = 1.0; 
+            else slowReturn = 0.6+0.4*(i-this.noseStart)/(this.tipStart-this.noseStart);
+            this.diameter[i] = Math.moveTowards(diameter, targetDiameter, slowReturn*amount, 2*amount);
+        }
+        if (this.lastObstruction>-1 && newLastObstruction == -1 && this.noseA[0]<0.05)
+        {
+            this.addTransient(this.lastObstruction);
+        }
+        this.lastObstruction = newLastObstruction;
+        
+        amount = deltaTime * this.movementSpeed; 
+        this.noseDiameter[0] = Math.moveTowards(this.noseDiameter[0], this.velumTarget, 
+                amount*0.25, amount*0.1);
+        this.noseA[0] = this.noseDiameter[0]*this.noseDiameter[0];        
+    },
+    
+    calculateReflections : function()
+    {
+        for (var i=0; i<this.n; i++) 
+        {
+            this.A[i] = this.diameter[i]*this.diameter[i]; //ignoring PI etc.
+        }
+        for (var i=1; i<this.n; i++)
+        {
+            this.reflection[i] = this.newReflection[i];
+            if (this.A[i] == 0) this.newReflection[i] = 0.999; //to prevent some bad behaviour if 0
+            else this.newReflection[i] = (this.A[i-1]-this.A[i]) / (this.A[i-1]+this.A[i]); 
+        }
+        
+        //now at junction with nose
+
+        this.reflectionLeft = this.newReflectionLeft;
+        this.reflectionRight = this.newReflectionRight;
+        this.reflectionNose = this.newReflectionNose;
+        var sum = this.A[this.noseStart]+this.A[this.noseStart+1]+this.noseA[0];
+        this.newReflectionLeft = (2*this.A[this.noseStart]-sum)/sum;
+        this.newReflectionRight = (2*this.A[this.noseStart+1]-sum)/sum;   
+        this.newReflectionNose = (2*this.noseA[0]-sum)/sum;      
+    },
+
+    calculateNoseReflections : function()
+    {
+        for (var i=0; i<this.noseLength; i++) 
+        {
+            this.noseA[i] = this.noseDiameter[i]*this.noseDiameter[i]; 
+        }
+        for (var i=1; i<this.noseLength; i++)
+        {
+            this.noseReflection[i] = (this.noseA[i-1]-this.noseA[i]) / (this.noseA[i-1]+this.noseA[i]); 
+        }
+    },
+    
+    runStep : function(glottalOutput, turbulenceNoise, lambda)
+    {
+        var updateAmplitudes = (Math.random()<0.1);
+    
+        //mouth
+        this.processTransients();
+        this.addTurbulenceNoise(turbulenceNoise);
+        
+        //this.glottalReflection = -0.8 + 1.6 * Glottis.newTenseness;
+        this.junctionOutputR[0] = this.L[0] * this.glottalReflection + glottalOutput;
+        this.junctionOutputL[this.n] = this.R[this.n-1] * this.lipReflection; 
+        
+        for (var i=1; i<this.n; i++)
+        {
+            var r = this.reflection[i] * (1-lambda) + this.newReflection[i]*lambda;
+            var w = r * (this.R[i-1] + this.L[i]);
+            this.junctionOutputR[i] = this.R[i-1] - w;
+            this.junctionOutputL[i] = this.L[i] + w;
+        }    
+        
+        //now at junction with nose
+        var i = this.noseStart;
+        var r = this.newReflectionLeft * (1-lambda) + this.reflectionLeft*lambda;
+        this.junctionOutputL[i] = r*this.R[i-1]+(1+r)*(this.noseL[0]+this.L[i]);
+        r = this.newReflectionRight * (1-lambda) + this.reflectionRight*lambda;
+        this.junctionOutputR[i] = r*this.L[i]+(1+r)*(this.R[i-1]+this.noseL[0]);     
+        r = this.newReflectionNose * (1-lambda) + this.reflectionNose*lambda;
+        this.noseJunctionOutputR[0] = r*this.noseL[0]+(1+r)*(this.L[i]+this.R[i-1]);
+         
+        for (var i=0; i<this.n; i++)
+        {          
+            this.R[i] = this.junctionOutputR[i]*0.999;
+            this.L[i] = this.junctionOutputL[i+1]*0.999; 
+            
+            //this.R[i] = Math.clamp(this.junctionOutputR[i] * this.fade, -1, 1);
+            //this.L[i] = Math.clamp(this.junctionOutputL[i+1] * this.fade, -1, 1);    
+            
+            if (updateAmplitudes)
+            {   
+                var amplitude = Math.abs(this.R[i]+this.L[i]);
+                if (amplitude > this.maxAmplitude[i]) this.maxAmplitude[i] = amplitude;
+                else this.maxAmplitude[i] *= 0.999;
+            }
+        }
+
+        this.lipOutput = this.R[this.n-1];
+        
+        //nose     
+        this.noseJunctionOutputL[this.noseLength] = this.noseR[this.noseLength-1] * this.lipReflection; 
+        
+        for (var i=1; i<this.noseLength; i++)
+        {
+            var w = this.noseReflection[i] * (this.noseR[i-1] + this.noseL[i]);
+            this.noseJunctionOutputR[i] = this.noseR[i-1] - w;
+            this.noseJunctionOutputL[i] = this.noseL[i] + w;
+        }
+        
+        for (var i=0; i<this.noseLength; i++)
+        {
+            this.noseR[i] = this.noseJunctionOutputR[i] * this.fade;
+            this.noseL[i] = this.noseJunctionOutputL[i+1] * this.fade;   
+            
+            //this.noseR[i] = Math.clamp(this.noseJunctionOutputR[i] * this.fade, -1, 1);
+            //this.noseL[i] = Math.clamp(this.noseJunctionOutputL[i+1] * this.fade, -1, 1);    
+            
+            if (updateAmplitudes)
+            {
+                var amplitude = Math.abs(this.noseR[i]+this.noseL[i]);
+                if (amplitude > this.noseMaxAmplitude[i]) this.noseMaxAmplitude[i] = amplitude;
+                else this.noseMaxAmplitude[i] *= 0.999;
+            }
+        }
+
+        this.noseOutput = this.noseR[this.noseLength-1];
+       
+    },
+    
+    finishBlock : function()
+    {         
+        this.reshapeTract(AudioSystem.blockTime);
+        this.calculateReflections();
+    },
+    
+    addTransient : function(position)
+    {
+        var trans = {}
+        trans.position = position;
+        trans.timeAlive = 0;
+        trans.lifeTime = 0.2;
+        trans.strength = 0.3;
+        trans.exponent = 200;
+        this.transients.push(trans);
+    },
+    
+    processTransients : function()
+    {
+        for (var i = 0; i < this.transients.length; i++)  
+        {
+            var trans = this.transients[i];
+            var amplitude = trans.strength * Math.pow(2, -trans.exponent * trans.timeAlive);
+            this.R[trans.position] += amplitude/2;
+            this.L[trans.position] += amplitude/2;
+            trans.timeAlive += 1.0/(sampleRate*2);
+        }
+        for (var i=this.transients.length-1; i>=0; i--)
+        {
+            var trans = this.transients[i];
+            if (trans.timeAlive > trans.lifeTime)
+            {
+                this.transients.splice(i,1);
+            }
+        }
+    },
+    
+    addTurbulenceNoise : function(turbulenceNoise)
+    {
+        for (var j=0; j<UI.touchesWithMouse.length; j++)
+        {
+            var touch = UI.touchesWithMouse[j];
+            if (touch.index<2 || touch.index>Tract.n) continue;
+            if (touch.diameter<=0) continue;            
+            var intensity = touch.fricative_intensity;
+            if (intensity == 0) continue;
+            this.addTurbulenceNoiseAtIndex(0.66*turbulenceNoise*intensity, touch.index, touch.diameter);
+        }
+    },
+    
+    addTurbulenceNoiseAtIndex : function(turbulenceNoise, index, diameter)
+    {   
+        var i = Math.floor(index);
+        var delta = index - i;
+        turbulenceNoise *= Glottis.getNoiseModulator();
+        var thinness0 = Math.clamp(8*(0.7-diameter),0,1);
+        var openness = Math.clamp(30*(diameter-0.3), 0, 1);
+        var noise0 = turbulenceNoise*(1-delta)*thinness0*openness;
+        var noise1 = turbulenceNoise*delta*thinness0*openness;
+        this.R[i+1] += noise0/2;
+        this.L[i+1] += noise0/2;
+        this.R[i+2] += noise1/2;
+        this.L[i+2] += noise1/2;
+    }
+};
+
+
+var TractUI =
+{
+    originX : 340, 
+    originY : 449, 
+    radius : 298, 
+    scale : 60,
+    tongueIndex : 12.9,
+    tongueDiameter : 2.43,
+    innerTongueControlRadius : 2.05,
+    outerTongueControlRadius : 3.5,
+    tongueTouch : 0,
+    angleScale : 0.64,
+    angleOffset : -0.24,
+    noseOffset : 0.8,
+    gridOffset : 1.7,
+    fillColour : 'pink',
+    lineColour : '#C070C6',
+    
+    init : function()
+    {
+        this.ctx = tractCtx;
+        this.setRestDiameter();
+        for (var i=0; i<Tract.n; i++) 
+        {
+            Tract.diameter[i] = Tract.targetDiameter[i] = Tract.restDiameter[i];
+        }
+        this.drawBackground();
+        this.tongueLowerIndexBound = Tract.bladeStart+2;
+        this.tongueUpperIndexBound = Tract.tipStart-3;
+        this.tongueIndexCentre = 0.5*(this.tongueLowerIndexBound+this.tongueUpperIndexBound);
+    },
+    
+    moveTo : function(i,d) 
+    {
+        var angle = this.angleOffset + i * this.angleScale * Math.PI / (Tract.lipStart-1);
+        var wobble = (Tract.maxAmplitude[Tract.n-1]+Tract.noseMaxAmplitude[Tract.noseLength-1]);
+        wobble *= 0.03*Math.sin(2*i-50*time)*i/Tract.n;
+        angle += wobble;        
+        var r = this.radius - this.scale*d + 100*wobble;
+        this.ctx.moveTo(this.originX-r*Math.cos(angle), this.originY-r*Math.sin(angle));
+    },
+    
+    lineTo : function(i,d) 
+    {
+        var angle = this.angleOffset + i * this.angleScale * Math.PI / (Tract.lipStart-1);
+        var wobble = (Tract.maxAmplitude[Tract.n-1]+Tract.noseMaxAmplitude[Tract.noseLength-1]);
+        wobble *= 0.03*Math.sin(2*i-50*time)*i/Tract.n;
+        angle += wobble;       
+        var r = this.radius - this.scale*d + 100*wobble;
+        this.ctx.lineTo(this.originX-r*Math.cos(angle), this.originY-r*Math.sin(angle));
+    },
+    
+    drawText : function(i,d,text)
+    {
+        var angle = this.angleOffset + i * this.angleScale * Math.PI / (Tract.lipStart-1);
+        var r = this.radius - this.scale*d; 
+        this.ctx.save();
+        this.ctx.translate(this.originX-r*Math.cos(angle), this.originY-r*Math.sin(angle)+2); //+8);
+        this.ctx.rotate(angle-Math.PI/2);
+        this.ctx.fillText(text, 0, 0);
+        this.ctx.restore();
+    },
+    
+    drawTextStraight : function(i,d,text)
+    {
+        var angle = this.angleOffset + i * this.angleScale * Math.PI / (Tract.lipStart-1);
+        var r = this.radius - this.scale*d; 
+        this.ctx.save();
+        this.ctx.translate(this.originX-r*Math.cos(angle), this.originY-r*Math.sin(angle)+2); //+8);
+        //this.ctx.rotate(angle-Math.PI/2);
+        this.ctx.fillText(text, 0, 0);
+        this.ctx.restore();
+    },
+    
+    drawCircle : function(i,d,radius)
+    {
+        var angle = this.angleOffset + i * this.angleScale * Math.PI / (Tract.lipStart-1);
+        var r = this.radius - this.scale*d; 
+        this.ctx.beginPath();
+        this.ctx.arc(this.originX-r*Math.cos(angle), this.originY-r*Math.sin(angle), radius, 0, 2*Math.PI);
+        this.ctx.fill();
+    },
+        
+    getIndex : function(x,y)
+    {
+        var xx = x-this.originX; var yy = y-this.originY;
+        var angle = Math.atan2(yy, xx);
+        while (angle> 0) angle -= 2*Math.PI;
+        return (Math.PI + angle - this.angleOffset)*(Tract.lipStart-1) / (this.angleScale*Math.PI);
+    },
+    getDiameter : function(x,y)
+    {
+        var xx = x-this.originX; var yy = y-this.originY;
+        return (this.radius-Math.sqrt(xx*xx + yy*yy))/this.scale;
+    },
+    
+    draw : function()
+    {
+        this.ctx.clearRect(0, 0, tractCanvas.width, tractCanvas.height);
+        this.ctx.lineCap = 'round';        
+        this.ctx.lineJoin = 'round';  
+        
+        this.drawTongueControl();
+        this.drawPitchControl();
+        
+        var velum = Tract.noseDiameter[0];
+        var velumAngle = velum * 4;
+        
+        //first draw fill
+        this.ctx.beginPath();        
+        this.ctx.lineWidth = 2;
+        this.ctx.strokeStyle = this.fillColour;
+        this.ctx.fillStyle = this.fillColour;
+        this.moveTo(1,0);
+        for (var i = 1; i < Tract.n; i++) this.lineTo(i, Tract.diameter[i]);
+        for (var i = Tract.n-1; i >= 2; i--) this.lineTo(i, 0);  
+        this.ctx.closePath();
+        this.ctx.stroke();
+        this.ctx.fill();
+        
+        //for nose
+        this.ctx.beginPath();        
+        this.ctx.lineWidth = 2;
+        this.ctx.strokeStyle = this.fillColour;
+        this.ctx.fillStyle = this.fillColour;
+        this.moveTo(Tract.noseStart, -this.noseOffset);
+        for (var i = 1; i < Tract.noseLength; i++) this.lineTo(i+Tract.noseStart, -this.noseOffset - Tract.noseDiameter[i]*0.9);
+        for (var i = Tract.noseLength-1; i >= 1; i--) this.lineTo(i+Tract.noseStart, -this.noseOffset);  
+        this.ctx.closePath();
+        //this.ctx.stroke();
+        this.ctx.fill();
+        
+        //velum
+        this.ctx.beginPath();
+        this.ctx.lineWidth = 2;
+        this.ctx.strokeStyle = this.fillColour;
+        this.ctx.fillStyle = this.fillColour;
+        this.moveTo(Tract.noseStart-2, 0);
+        this.lineTo(Tract.noseStart, -this.noseOffset);
+        this.lineTo(Tract.noseStart+velumAngle, -this.noseOffset);
+        this.lineTo(Tract.noseStart+velumAngle-2, 0);
+        this.ctx.closePath();
+        this.ctx.stroke();
+        this.ctx.fill();
+        
+
+        
+        //white text
+        this.ctx.fillStyle = "white";
+        this.ctx.font="20px Arial";
+        this.ctx.textAlign = "center";
+        this.ctx.globalAlpha = 1.0;
+        this.drawText(Tract.n*0.10, 0.425, "throat");         
+        this.drawText(Tract.n*0.71, -1.8, "nasal");
+        this.drawText(Tract.n*0.71, -1.3, "cavity");
+        this.ctx.font="22px Arial";        
+        this.drawText(Tract.n*0.6, 0.9, "oral");    
+        this.drawText(Tract.n*0.7, 0.9, "cavity");        
+  
+       
+        this.drawAmplitudes(); 
+        
+        //then draw lines
+        this.ctx.beginPath();        
+        this.ctx.lineWidth = 5;
+        this.ctx.strokeStyle = this.lineColour;
+        this.ctx.lineJoin = 'round';
+        this.ctx.lineCap = 'round';          
+        this.moveTo(1, Tract.diameter[0]);
+        for (var i = 2; i < Tract.n; i++) this.lineTo(i, Tract.diameter[i]);
+        this.moveTo(1,0);
+        for (var i = 2; i <= Tract.noseStart-2; i++) this.lineTo(i, 0);
+        this.moveTo(Tract.noseStart+velumAngle-2,0);
+        for (var i = Tract.noseStart+Math.ceil(velumAngle)-2; i < Tract.n; i++) this.lineTo(i, 0);   
+        this.ctx.stroke();
+        
+        //for nose
+        this.ctx.beginPath();        
+        this.ctx.lineWidth = 5;
+        this.ctx.strokeStyle = this.lineColour;
+        this.ctx.lineJoin = 'round';  
+        this.moveTo(Tract.noseStart, -this.noseOffset);
+        for (var i = 1; i < Tract.noseLength; i++) this.lineTo(i+Tract.noseStart, -this.noseOffset - Tract.noseDiameter[i]*0.9);
+        this.moveTo(Tract.noseStart+velumAngle, -this.noseOffset);
+        for (var i = Math.ceil(velumAngle); i < Tract.noseLength; i++) this.lineTo(i+Tract.noseStart, -this.noseOffset);
+        this.ctx.stroke();
+        
+        
+        //velum
+        this.ctx.globalAlpha = velum*5;
+        this.ctx.beginPath();
+        this.moveTo(Tract.noseStart-2, 0);
+        this.lineTo(Tract.noseStart, -this.noseOffset);
+        this.moveTo(Tract.noseStart+velumAngle-2, 0);
+        this.lineTo(Tract.noseStart+velumAngle, -this.noseOffset);  
+        this.ctx.stroke();
+
+        
+        this.ctx.fillStyle = "orchid";
+        this.ctx.font="20px Arial";
+        this.ctx.textAlign = "center";
+        this.ctx.globalAlpha = 0.7;
+        this.drawText(Tract.n*0.95, 0.8+0.8*Tract.diameter[Tract.n-1], " lip"); 
+        
+        this.ctx.globalAlpha=1.0;        
+        this.ctx.fillStyle = "black";
+        this.ctx.textAlign = "left";
+        this.ctx.fillText(UI.debugText, 20, 20);
+        //this.drawPositions();
+    },
+    
+    drawBackground : function()
+    {
+        this.ctx = backCtx;
+        
+        
+        //text
+        this.ctx.fillStyle = "orchid";
+        this.ctx.font="20px Arial";
+        this.ctx.textAlign = "center";
+        this.ctx.globalAlpha = 0.7;
+        this.drawText(Tract.n*0.44, -0.28, "soft");
+        this.drawText(Tract.n*0.51, -0.28, "palate");
+        this.drawText(Tract.n*0.77, -0.28, "hard");
+        this.drawText(Tract.n*0.84, -0.28, "palate");
+        this.drawText(Tract.n*0.95, -0.28, " lip");
+        
+        this.ctx.font="17px Arial";        
+        this.drawTextStraight(Tract.n*0.18, 3, "  tongue control");   
+        this.ctx.textAlign = "left";
+        this.drawText(Tract.n*1.03, -1.07, "nasals");
+        this.drawText(Tract.n*1.03, -0.28, "stops");
+        this.drawText(Tract.n*1.03, 0.51, "fricatives");
+        //this.drawTextStraight(1.5, +0.8, "glottis")
+        this.ctx.strokeStyle = "orchid";
+        this.ctx.lineWidth = 2;
+        this.ctx.beginPath();
+        this.moveTo(Tract.n*1.03, 0); this.lineTo(Tract.n*1.07, 0); 
+        this.moveTo(Tract.n*1.03, -this.noseOffset); this.lineTo(Tract.n*1.07,  -this.noseOffset); 
+        this.ctx.stroke();
+        this.ctx.globalAlpha = 0.9;
+        this.ctx.globalAlpha = 1.0;
+        this.ctx = tractCtx;
+    },
+    
+    drawPositions : function()
+    {
+        this.ctx.fillStyle = "orchid";
+        this.ctx.font="24px Arial";
+        this.ctx.textAlign = "center";
+        this.ctx.globalAlpha = 0.6;
+        var a = 2;
+        var b = 1.5;
+        this.drawText(15, a+b*0.60, 'æ'); //pat
+        this.drawText(13, a+b*0.27, 'ɑ'); //part
+        this.drawText(12, a+b*0.00, 'ɒ'); //pot
+        this.drawText(17.7, a+b*0.05, '(ɔ)'); //port (rounded)
+        this.drawText(27, a+b*0.65, 'ɪ'); //pit
+        this.drawText(27.4, a+b*0.21, 'i'); //peat
+        this.drawText(20, a+b*1.00, 'e'); //pet
+        this.drawText(18.1, a+b*0.37, 'ʌ'); //putt   
+            //put ÊŠ
+        this.drawText(23, a+b*0.1, '(u)'); //poot (rounded)   
+        this.drawText(21, a+b*0.6, 'ə'); //pert [should be Éœ]
+        
+        var nasals = -1.1;
+        var stops = -0.4;
+        var fricatives = 0.3;
+        var approximants = 1.1;
+        this.ctx.globalAlpha = 0.8;
+        
+        //approximants
+        this.drawText(38, approximants, 'l');
+        this.drawText(41, approximants, 'w');
+        
+        //?
+        this.drawText(4.5, 0.37, 'h');
+        
+        if (Glottis.isTouched || alwaysVoice)
+        {
+            //voiced consonants
+            this.drawText(31.5, fricatives, 'ʒ');     
+            this.drawText(36, fricatives, 'z');
+            this.drawText(41, fricatives, 'v');
+            this.drawText(22, stops, 'g');
+            this.drawText(36, stops, 'd');
+            this.drawText(41, stops, 'b');
+            this.drawText(22, nasals, 'ŋ');
+            this.drawText(36, nasals, 'n');
+            this.drawText(41, nasals, 'm');  
+        }
+        else
+        {
+            //unvoiced consonants
+            this.drawText(31.5, fricatives, 'ʃ'); 
+            this.drawText(36, fricatives, 's');
+            this.drawText(41, fricatives, 'f');
+            this.drawText(22, stops, 'k');
+            this.drawText(36, stops, 't');
+            this.drawText(41, stops, 'p');
+            this.drawText(22, nasals, 'ŋ');
+            this.drawText(36, nasals, 'n');
+            this.drawText(41, nasals, 'm');  
+        }
+    },
+    
+    drawAmplitudes : function()
+    {
+        this.ctx.strokeStyle = "orchid";
+        this.ctx.lineCap = "butt";
+        this.ctx.globalAlpha = 0.3;
+        for (var i=2; i<Tract.n-1; i++)
+        {
+            this.ctx.beginPath();
+            this.ctx.lineWidth = Math.sqrt(Tract.maxAmplitude[i])*3;
+            this.moveTo(i, 0);
+            this.lineTo(i, Tract.diameter[i]);
+            this.ctx.stroke();
+        }
+        for (var i=1; i<Tract.noseLength-1; i++)
+        {
+            this.ctx.beginPath();
+            this.ctx.lineWidth = Math.sqrt(Tract.noseMaxAmplitude[i]) * 3;
+            this.moveTo(i+Tract.noseStart, -this.noseOffset);
+            this.lineTo(i+Tract.noseStart, -this.noseOffset - Tract.noseDiameter[i]*0.9);
+            this.ctx.stroke();
+        }
+        this.ctx.globalAlpha = 1;
+    },
+    
+    drawTongueControl : function()
+    {
+        this.ctx.lineCap = "round";
+        this.ctx.lineJoin = "round";
+        this.ctx.strokeStyle = palePink;
+        this.ctx.fillStyle = palePink;
+        this.ctx.globalAlpha = 1.0;
+        this.ctx.beginPath();
+        this.ctx.lineWidth = 45;
+        
+        //outline
+        this.moveTo(this.tongueLowerIndexBound, this.innerTongueControlRadius);
+        for (var i=this.tongueLowerIndexBound+1; i<=this.tongueUpperIndexBound; i++) this.lineTo(i, this.innerTongueControlRadius);
+        this.lineTo(this.tongueIndexCentre, this.outerTongueControlRadius);
+        this.ctx.closePath();
+        this.ctx.stroke();
+        this.ctx.fill();
+        
+        var a = this.innerTongueControlRadius;
+        var c = this.outerTongueControlRadius;
+        var b = 0.5*(a+c);
+        var r = 3;
+        this.ctx.fillStyle = "orchid";
+        this.ctx.globalAlpha = 0.3;        
+        this.drawCircle(this.tongueIndexCentre, a, r);
+        this.drawCircle(this.tongueIndexCentre-4.25, a, r);
+        this.drawCircle(this.tongueIndexCentre-8.5, a, r);
+        this.drawCircle(this.tongueIndexCentre+4.25, a, r);
+        this.drawCircle(this.tongueIndexCentre+8.5, a, r);
+        this.drawCircle(this.tongueIndexCentre-6.1, b, r);    
+        this.drawCircle(this.tongueIndexCentre+6.1, b, r);  
+        this.drawCircle(this.tongueIndexCentre, b, r);  
+        this.drawCircle(this.tongueIndexCentre, c, r);
+        
+        this.ctx.globalAlpha = 1.0;         
+
+        //circle for tongue position
+        var angle = this.angleOffset + this.tongueIndex * this.angleScale * Math.PI / (Tract.lipStart-1);
+        var r = this.radius - this.scale*(this.tongueDiameter);
+        var x = this.originX-r*Math.cos(angle);
+        var y = this.originY-r*Math.sin(angle);
+        this.ctx.lineWidth = 4;
+        this.ctx.strokeStyle = "orchid";
+        this.ctx.globalAlpha = 0.7;
+        this.ctx.beginPath();
+        this.ctx.arc(x,y, 18, 0, 2*Math.PI);
+        this.ctx.stroke();        
+        this.ctx.globalAlpha = 0.15;
+        this.ctx.fill();
+        this.ctx.globalAlpha = 1.0;
+        
+        this.ctx.fillStyle = "orchid";
+     },
+    
+    drawPitchControl : function()
+    {
+        var w=9;
+        var h=15;
+        if (Glottis.x)
+        {
+            this.ctx.lineWidth = 4;
+            this.ctx.strokeStyle = "orchid";
+            this.ctx.globalAlpha = 0.7;
+            this.ctx.beginPath();
+            this.ctx.moveTo(Glottis.x-w, Glottis.y-h);
+            this.ctx.lineTo(Glottis.x+w, Glottis.y-h);
+            this.ctx.lineTo(Glottis.x+w, Glottis.y+h);
+            this.ctx.lineTo(Glottis.x-w, Glottis.y+h);                    
+            this.ctx.closePath();            
+            this.ctx.stroke();    
+            this.ctx.globalAlpha = 0.15;
+            this.ctx.fill();            
+            this.ctx.globalAlpha = 1.0;
+        }
+    },
+    
+    setRestDiameter : function()
+    {
+        for (var i=Tract.bladeStart; i<Tract.lipStart; i++)
+        {
+            var t = 1.1 * Math.PI*(this.tongueIndex - i)/(Tract.tipStart - Tract.bladeStart);
+            var fixedTongueDiameter = 2+(this.tongueDiameter-2)/1.5;
+            var curve = (1.5-fixedTongueDiameter+this.gridOffset)*Math.cos(t);
+            if (i == Tract.bladeStart-2 || i == Tract.lipStart-1) curve *= 0.8;
+            if (i == Tract.bladeStart || i == Tract.lipStart-2) curve *= 0.94;               
+            Tract.restDiameter[i] = 1.5 - curve;
+        }
+    },
+    
+    handleTouches : function()
+    {           
+        if (this.tongueTouch != 0 && !this.tongueTouch.alive) this.tongueTouch = 0;
+        
+        if (this.tongueTouch == 0)
+        {        
+            for (var j=0; j<UI.touchesWithMouse.length; j++)  
+            {
+                var touch = UI.touchesWithMouse[j];
+                if (!touch.alive) continue;
+                if (touch.fricative_intensity == 1) continue; //only new touches will pass this
+                var x = touch.x;
+                var y = touch.y;        
+                var index = TractUI.getIndex(x,y);
+                var diameter = TractUI.getDiameter(x,y);
+                if (index >= this.tongueLowerIndexBound-4 && index<=this.tongueUpperIndexBound+4 
+                    && diameter >= this.innerTongueControlRadius-0.5 && diameter <= this.outerTongueControlRadius+0.5)
+                {
+                    this.tongueTouch = touch;
+                }
+            }    
+        }
+        
+        if (this.tongueTouch != 0)
+        {
+            var x = this.tongueTouch.x;
+            var y = this.tongueTouch.y;        
+            var index = TractUI.getIndex(x,y);
+            var diameter = TractUI.getDiameter(x,y);
+            var fromPoint = (this.outerTongueControlRadius-diameter)/(this.outerTongueControlRadius-this.innerTongueControlRadius);
+            fromPoint = Math.clamp(fromPoint, 0, 1);
+            fromPoint = Math.pow(fromPoint, 0.58) - 0.2*(fromPoint*fromPoint-fromPoint); //horrible kludge to fit curve to straight line
+            this.tongueDiameter = Math.clamp(diameter, this.innerTongueControlRadius, this.outerTongueControlRadius);
+            //this.tongueIndex = Math.clamp(index, this.tongueLowerIndexBound, this.tongueUpperIndexBound);
+            var out = fromPoint*0.5*(this.tongueUpperIndexBound-this.tongueLowerIndexBound);
+            this.tongueIndex = Math.clamp(index, this.tongueIndexCentre-out, this.tongueIndexCentre+out);
+        }
+        
+        this.setRestDiameter();   
+        for (var i=0; i<Tract.n; i++) Tract.targetDiameter[i] = Tract.restDiameter[i];        
+        
+        //other constrictions and nose
+        Tract.velumTarget = 0.01;
+        for (var j=0; j<UI.touchesWithMouse.length; j++) 
+        {
+            var touch = UI.touchesWithMouse[j];
+            if (!touch.alive) continue;            
+            var x = touch.x;
+            var y = touch.y;
+            var index = TractUI.getIndex(x,y);
+            var diameter = TractUI.getDiameter(x,y);
+            if (index > Tract.noseStart && diameter < -this.noseOffset)
+            {         
+                Tract.velumTarget = 0.4;
+            }            
+            temp.a = index;
+            temp.b = diameter;
+            if (diameter < -0.85-this.noseOffset) continue;
+            diameter -= 0.3;
+            if (diameter<0) diameter = 0;         
+            var width=2;
+            if (index<25) width = 10;
+            else if (index>=Tract.tipStart) width= 5;
+            else width = 10-5*(index-25)/(Tract.tipStart-25);
+            if (index >= 2 && index < Tract.n && y<tractCanvas.height && diameter < 3)
+            {
+                intIndex = Math.round(index);
+                for (var i=-Math.ceil(width)-1; i<width+1; i++) 
+                {   
+                    if (intIndex+i<0 || intIndex+i>=Tract.n) continue;
+                    var relpos = (intIndex+i) - index;
+                    relpos = Math.abs(relpos)-0.5;
+                    var shrink;
+                    if (relpos <= 0) shrink = 0;
+                    else if (relpos > width) shrink = 1;
+                    else shrink = 0.5*(1-Math.cos(Math.PI * relpos / width));
+                    if (diameter < Tract.targetDiameter[intIndex+i])
+                    {
+                        Tract.targetDiameter[intIndex+i] = diameter + (Tract.targetDiameter[intIndex+i]-diameter)*shrink;
+                    }
+                }
+            }
+        }      
+    },
+
+}
+
+
+function makeButton(x, y, width, height, text, switchedOn)
+{
+    button = {};
+    button.x = x;
+    button.y = y;
+    button.width = width;
+    button.height = height;
+    button.text = text;
+    button.switchedOn = switchedOn;
+    
+    button.draw = function(ctx)
+    {
+        var radius = 10;
+        ctx.strokeStyle = palePink;
+        ctx.fillStyle = palePink;        
+        ctx.globalAlpha = 1.0;     
+        ctx.lineCap = 'round';        
+        ctx.lineJoin = 'round';        
+        ctx.lineWidth = 2*radius;     
+        
+        ctx.beginPath();
+        ctx.moveTo(this.x+radius, this.y+radius);
+        ctx.lineTo(this.x+this.width-radius, this.y+radius);
+        ctx.lineTo(this.x+this.width-radius, this.y+this.height-radius);
+        ctx.lineTo(this.x+radius, this.y+this.height-radius);
+        ctx.closePath();
+        ctx.stroke();
+        ctx.fill();
+        
+        ctx.font="16px Arial";
+        ctx.textAlign = "center";
+        if (this.switchedOn) 
+        {
+            ctx.fillStyle = "orchid";        
+            ctx.globalAlpha = 0.6;
+        }
+        else        
+        {
+            ctx.fillStyle = "white";        
+            ctx.globalAlpha = 1.0;
+        }
+        this.drawText(ctx);
+    };
+    
+    button.drawText = function(ctx)
+    {
+        ctx.fillText(this.text, this.x+this.width/2, this.y+this.height/2+6);
+    };
+    
+    button.handleTouchStart = function(touch)
+    {
+        if (touch.x>=this.x && touch.x <= this.x + this.width  
+            && touch.y >= this.y && touch.y <= this.y + this.height)
+        {
+            this.switchedOn = !this.switchedOn;
+        }
+    };
+    
+    return button;
+}
+
+
+document.body.style.cursor = 'pointer';
+
+AudioSystem.init();
+UI.init();
+Glottis.init();
+Tract.init();
+TractUI.init();
+    
+requestAnimationFrame(redraw);
+function redraw(highResTimestamp)
+{
+    UI.shapeToFitScreen();
+    TractUI.draw();
+    UI.draw();
+    requestAnimationFrame(redraw);
+    time = Date.now()/1000;
+    UI.updateTouches();
+}
+
+
+
+/**********************************************************************************/
+/**********************************************************************************/
+
+/*
+ * A speed-improved perlin and simplex noise algorithms for 2D.
+ *
+ * Based on example code by Stefan Gustavson (stegu@itn.liu.se).
+ * Optimisations by Peter Eastman (peastman@drizzle.stanford.edu).
+ * Better rank ordering method by Stefan Gustavson in 2012.
+ * Converted to Javascript by Joseph Gentle.
+ *
+ * Version 2012-03-09
+ *
+ * This code was placed in the public domain by its original author,
+ * Stefan Gustavson. You may use it as you see fit, but
+ * attribution is appreciated.
+ *
+ */
+
+(function(global){
+  var module = global.noise = {};
+
+  function Grad(x, y, z) {
+    this.x = x; this.y = y; this.z = z;
+  }
+  
+  Grad.prototype.dot2 = function(x, y) {
+    return this.x*x + this.y*y;
+  };
+
+  Grad.prototype.dot3 = function(x, y, z) {
+    return this.x*x + this.y*y + this.z*z;
+  };
+
+  var grad3 = [new Grad(1,1,0),new Grad(-1,1,0),new Grad(1,-1,0),new Grad(-1,-1,0),
+               new Grad(1,0,1),new Grad(-1,0,1),new Grad(1,0,-1),new Grad(-1,0,-1),
+               new Grad(0,1,1),new Grad(0,-1,1),new Grad(0,1,-1),new Grad(0,-1,-1)];
+
+  var p = [151,160,137,91,90,15,
+  131,13,201,95,96,53,194,233,7,225,140,36,103,30,69,142,8,99,37,240,21,10,23,
+  190, 6,148,247,120,234,75,0,26,197,62,94,252,219,203,117,35,11,32,57,177,33,
+  88,237,149,56,87,174,20,125,136,171,168, 68,175,74,165,71,134,139,48,27,166,
+  77,146,158,231,83,111,229,122,60,211,133,230,220,105,92,41,55,46,245,40,244,
+  102,143,54, 65,25,63,161, 1,216,80,73,209,76,132,187,208, 89,18,169,200,196,
+  135,130,116,188,159,86,164,100,109,198,173,186, 3,64,52,217,226,250,124,123,
+  5,202,38,147,118,126,255,82,85,212,207,206,59,227,47,16,58,17,182,189,28,42,
+  223,183,170,213,119,248,152, 2,44,154,163, 70,221,153,101,155,167, 43,172,9,
+  129,22,39,253, 19,98,108,110,79,113,224,232,178,185, 112,104,218,246,97,228,
+  251,34,242,193,238,210,144,12,191,179,162,241, 81,51,145,235,249,14,239,107,
+  49,192,214, 31,181,199,106,157,184, 84,204,176,115,121,50,45,127, 4,150,254,
+  138,236,205,93,222,114,67,29,24,72,243,141,128,195,78,66,215,61,156,180];
+  // To remove the need for index wrapping, double the permutation table length
+  var perm = new Array(512);
+  var gradP = new Array(512);
+
+  // This isn't a very good seeding function, but it works ok. It supports 2^16
+  // different seed values. Write something better if you need more seeds.
+  module.seed = function(seed) {
+    if(seed > 0 && seed < 1) {
+      // Scale the seed out
+      seed *= 65536;
+    }
+
+    seed = Math.floor(seed);
+    if(seed < 256) {
+      seed |= seed << 8;
+    }
+
+    for(var i = 0; i < 256; i++) {
+      var v;
+      if (i & 1) {
+        v = p[i] ^ (seed & 255);
+      } else {
+        v = p[i] ^ ((seed>>8) & 255);
+      }
+
+      perm[i] = perm[i + 256] = v;
+      gradP[i] = gradP[i + 256] = grad3[v % 12];
+    }
+  };
+
+  module.seed(Date.now());
+
+  /*
+  for(var i=0; i<256; i++) {
+    perm[i] = perm[i + 256] = p[i];
+    gradP[i] = gradP[i + 256] = grad3[perm[i] % 12];
+  }*/
+
+  // Skewing and unskewing factors for 2, 3, and 4 dimensions
+  var F2 = 0.5*(Math.sqrt(3)-1);
+  var G2 = (3-Math.sqrt(3))/6;
+
+  var F3 = 1/3;
+  var G3 = 1/6;
+
+  // 2D simplex noise
+  module.simplex2 = function(xin, yin) {
+    var n0, n1, n2; // Noise contributions from the three corners
+    // Skew the input space to determine which simplex cell we're in
+    var s = (xin+yin)*F2; // Hairy factor for 2D
+    var i = Math.floor(xin+s);
+    var j = Math.floor(yin+s);
+    var t = (i+j)*G2;
+    var x0 = xin-i+t; // The x,y distances from the cell origin, unskewed.
+    var y0 = yin-j+t;
+    // For the 2D case, the simplex shape is an equilateral triangle.
+    // Determine which simplex we are in.
+    var i1, j1; // Offsets for second (middle) corner of simplex in (i,j) coords
+    if(x0>y0) { // lower triangle, XY order: (0,0)->(1,0)->(1,1)
+      i1=1; j1=0;
+    } else {    // upper triangle, YX order: (0,0)->(0,1)->(1,1)
+      i1=0; j1=1;
+    }
+    // A step of (1,0) in (i,j) means a step of (1-c,-c) in (x,y), and
+    // a step of (0,1) in (i,j) means a step of (-c,1-c) in (x,y), where
+    // c = (3-sqrt(3))/6
+    var x1 = x0 - i1 + G2; // Offsets for middle corner in (x,y) unskewed coords
+    var y1 = y0 - j1 + G2;
+    var x2 = x0 - 1 + 2 * G2; // Offsets for last corner in (x,y) unskewed coords
+    var y2 = y0 - 1 + 2 * G2;
+    // Work out the hashed gradient indices of the three simplex corners
+    i &= 255;
+    j &= 255;
+    var gi0 = gradP[i+perm[j]];
+    var gi1 = gradP[i+i1+perm[j+j1]];
+    var gi2 = gradP[i+1+perm[j+1]];
+    // Calculate the contribution from the three corners
+    var t0 = 0.5 - x0*x0-y0*y0;
+    if(t0<0) {
+      n0 = 0;
+    } else {
+      t0 *= t0;
+      n0 = t0 * t0 * gi0.dot2(x0, y0);  // (x,y) of grad3 used for 2D gradient
+    }
+    var t1 = 0.5 - x1*x1-y1*y1;
+    if(t1<0) {
+      n1 = 0;
+    } else {
+      t1 *= t1;
+      n1 = t1 * t1 * gi1.dot2(x1, y1);
+    }
+    var t2 = 0.5 - x2*x2-y2*y2;
+    if(t2<0) {
+      n2 = 0;
+    } else {
+      t2 *= t2;
+      n2 = t2 * t2 * gi2.dot2(x2, y2);
+    }
+    // Add contributions from each corner to get the final noise value.
+    // The result is scaled to return values in the interval [-1,1].
+    return 70 * (n0 + n1 + n2);
+  };
+  
+  module.simplex1 = function(x)
+  {
+    return module.simplex2(x*1.2, -x*0.7);
+  };
+
+  
+})(this);
+
+</script>
+
+</body></html>
\ No newline at end of file
diff --git a/tromboner/auto-clicker.py b/tromboner/auto-clicker.py
new file mode 100644 (file)
index 0000000..28710b4
--- /dev/null
@@ -0,0 +1,25 @@
+import pygetwindow
+from datetime import datetime
+import pyautogui, sys
+import time
+import random
+
+pt = pygetwindow.getWindowsWithTitle('Pink Trombone')[0]
+pt.resizeTo(900,900)
+pt.moveTo(0,0)
+
+i = 0
+topleft = pt.topleft
+bottomright=pt.bottomright
+
+while i < 50:
+    xpos1 = random.randrange(topleft[0]+30,bottomright[0]-30)
+    ypos1 = random.randrange(topleft[1]+30,bottomright[1]-30)
+    
+    pyautogui.mouseDown(xpos1,ypos1)
+    
+    pyautogui.mouseUp(xpos1,ypos1)
+    
+    
+    i+=1
+    print(i)
\ No newline at end of file
diff --git a/tromboner/coods.txt b/tromboner/coods.txt
new file mode 100644 (file)
index 0000000..ad9e34f
--- /dev/null
@@ -0,0 +1,28 @@
+topleft = 10,32
+bottomright= 873,871
+a= 328,566
+b=676, 242
+c= 227, 303
+d=551, 210
+e=414, 577
+f=636,318
+g=234,305
+h=59,680
+i=432,481
+j=420,295
+k=230,303
+l=573,342
+m=690,183
+n=558,151
+o=272,579
+p=678,245
+q=237,312
+r=251,409
+s=536,298
+t=552,209
+u=369,463
+v=641,309
+w=627,352
+x=363,308
+y=368,368
+z=544,297
diff --git a/tromboner/string-to-trombone.py b/tromboner/string-to-trombone.py
new file mode 100644 (file)
index 0000000..7d1c21c
--- /dev/null
@@ -0,0 +1,88 @@
+import pygetwindow
+import pyautogui
+import time
+import random
+import sys
+
+
+
+#define library
+
+#add coords for 'a' through 'z' to list
+letterCoords = []
+letterCoords.append((380,575))
+letterCoords.append((676,242))
+letterCoords.append((227,303))
+letterCoords.append((551,210))
+letterCoords.append((423,444))
+letterCoords.append((636,318))
+letterCoords.append((234,305))
+letterCoords.append((59,680))
+letterCoords.append((432,481))
+letterCoords.append((420,295))
+letterCoords.append((230,303))
+letterCoords.append((573,342))
+letterCoords.append((690,183))
+letterCoords.append((558,151))
+letterCoords.append((272,579))
+letterCoords.append((678,245))
+letterCoords.append((237,312))
+letterCoords.append((251,409))
+letterCoords.append((536,298))
+letterCoords.append((552,209))
+letterCoords.append((369,463))
+letterCoords.append((641,309))
+letterCoords.append((627,352))
+letterCoords.append((363,308))
+letterCoords.append((368,368))
+letterCoords.append((544,297))
+
+
+#find window (make sure no other windows are also called pink trombone - windows explorer, etc.)
+pt = pygetwindow.getWindowsWithTitle('Pink Trombone')[0]
+#focus window
+pt.activate()
+
+#re-size and move
+pt.resizeTo(900,900)
+pt.moveTo(0,0)
+
+#get window dimensions
+ptmin = pt.topleft
+ptmax = pt.bottomright
+
+
+
+#check if argument is string
+if isinstance(sys.argv[1],str):
+
+    
+    #populate a list with the alphanumeric characters in string
+    alphastring = []
+    for element in sys.argv[1]:
+        if element.isalpha():
+            alphastring.append(element.lower())
+            
+    #slice string so it's not too long.
+    maxLength = 30
+    
+    if len(alphastring) > maxLength:
+        alphastring = alphastring[0:maxLength-1]
+
+    #turn on voice
+    pyautogui.click(778,651)
+    count = 0
+    
+    print(alphastring)
+    for element in alphastring:
+        #click a random spot on the pitch slider
+        pyautogui.click(random.randrange(400,852),762)
+                
+        #click interface for random amount of time. "-97" in following line makes 'a' = 0, etc. with rest of letters.
+        pyautogui.mouseDown(letterCoords[ord(element)-97])
+        #time.sleep(random.uniform(0.0,0.05))
+        pyautogui.mouseUp(letterCoords[ord(element)-97])
+    
+    #turn off voice
+    time.sleep(.03)
+    pyautogui.click(778,651)
\ No newline at end of file
diff --git a/vn_mask.png b/vn_mask.png
new file mode 100644 (file)
index 0000000..bb1cff4
Binary files /dev/null and b/vn_mask.png differ
diff --git a/winch_status.json b/winch_status.json
new file mode 100644 (file)
index 0000000..49ff2ef
--- /dev/null
@@ -0,0 +1,3 @@
+{
+    length: 24
+}
\ No newline at end of file