Full Code of plaaosert/credits_public for AI

main b0f09e1116d7 cached
22 files
142.8 KB
39.2k tokens
179 symbols
1 requests
Download .txt
Repository: plaaosert/credits_public
Branch: main
Commit: b0f09e1116d7
Files: 22
Total size: 142.8 KB

Directory structure:
gitextract_b3wq7xz6/

├── .gitattributes
├── .gitignore
├── CLIRender/
│   ├── classes.py
│   └── dat.py
├── README.md
├── animation_classes.py
├── animation_functions.py
├── animation_scenes.py
├── animator.py
├── colorama/
│   ├── __init__.py
│   ├── ansi.py
│   ├── ansitowin32.py
│   ├── initialise.py
│   ├── win32.py
│   └── winterm.py
├── credits.py
├── credits_pynput.py
├── media/
│   ├── README.txt
│   └── credits.txt
├── ocean.py
├── requirements.txt
└── string_defs.py

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitattributes
================================================
# Auto detect text files and perform LF normalization
* text=auto


================================================
FILE: .gitignore
================================================
**/__pycache__
media/*.wav
media/*.WAV
media/*.mp3
media/*.MP3
venv

================================================
FILE: CLIRender/classes.py
================================================
import os
from platform import system as system_type
from bisect import insort_right, bisect_right

from CLIRender.dat import Vector2


class RenderSection:
    id_inc = 0

    def __init__(self, string, start, code):
        self.id = RenderSection.id_inc
        RenderSection.id_inc += 1

        self.string = string
        self.start = start
        self.length = len(self.string)
        self.end = self.start + self.length
        self.code = code

    def __lt__(self, other):
        return self.start < other

    def __gt__(self, other):
        return self.start > other

    def add_char(self, char):
        self.string += char
        self.end += 2
        self.length += 2

    def mod_char(self, char, loc):
        if loc == self.end:
            self.add_char(char)
        else:
            self.string = self.string[:loc] + char + self.string[loc + 2:]

    # The addition operation adds the other section to this section. The other section takes precedence.
    # This only works with text and discards the other section's code.
    def add_section(self, other):
        diff = other.start - self.start
        self.string = self.string[:diff] + other.string + self.string[diff + other.length:]
        self.length = len(self.string)
        self.start = min(other.start, self.start)
        self.end = self.start + self.length

    # The addition operation adds the other section to this section. The self section takes precedence.
    # This only works with text and discards the other section's code.
    def add_section_below(self, other):
        diff = self.start - other.start
        self.string = other.string[:diff] + self.string + other.string[diff + self.length:]
        self.length = len(self.string)
        self.start = min(other.start, self.start)
        self.end = self.start + self.length

    # Same as section subtraction but only one char.
    def subtract_char(self, loc):
        first = None
        last = None
        if loc > self.start:
            diff = loc - self.start
            first = RenderSection(self.string[:diff], self.start, self.code)

        if loc < self.end:
            diff = self.end - loc
            last = RenderSection(self.string[diff:], loc + 2, self.code)

        return first, last

    # Subtracts another section's length from this section. This returns a 2-tuple containing the two new subsections.
    def subtract_section(self, other):
        first = None
        last = None
        if other.start > self.start:
            # if the other string starts later, there's still a part of the victim string remaining
            diff = other.start - self.start
            first = RenderSection(self.string[:diff], self.start, self.code)

        if other.end < self.end:
            # if the other string ends before us, the latter part needs something too
            diff = self.end - other.end
            last = RenderSection(self.string[self.length - diff:], other.end, self.code)

        return first, last


class Canvas:
    # TODO write multi-layer support. shouldn't be too hard, just merge down layers as we need
    # but not right now. plaao tired

    def __init__(self, dimensions, num_layers, merge_rules):
        self.dimensions = Vector2(dimensions.x * 2, dimensions.y)
        self.num_layers = num_layers
        self.layers = [Layer(i, self.dimensions) for i in range(num_layers)]
        self.merge_rules = merge_rules
        self.edits_this_frame = 0

    def set_char(self, layer, location, char, code):
        self.layers[layer].set_char(int(location.x * 2) + (location.y * self.dimensions.x), char, code)
        self.edits_this_frame += 1

    def set_string(self, layer, location, string, code):
        self.layers[layer].set_string(int(location.x * 2) + (location.y * self.dimensions.x), string, code)
        self.edits_this_frame += 1

    def clear_layer(self, layer):
        self.layers[layer].set_string(0, " " * self.dimensions.x * self.dimensions.y, "")

    def render_blank(self):
        print("\033[1;1H\033[1;37;40m" + (("  " * self.dimensions.x + "\n") * self.dimensions.y))

    def render_all(self):
        # Merge layers from bottom to top then render the resulting group list.
        # Here is the only point we even think about line lengths.
        total_string = "\033[1;1H\033[1;39m"
        for group in self.layers[0].new_groups:
            lineno = group.start // self.dimensions.x
            add_string = ""
            add_string += "\033[{};{}H".format(
                lineno + 1, group.start % self.dimensions.x + 1
            )

            add_string += group.code

            offset = group.start % self.dimensions.x
            # If overrun is greater than x, then it breaks a line boundary.
            # So, for every x overrun, we add a \n between lines.
            split = 0
            stop = 0
            while stop <= group.length:
                if lineno >= self.dimensions.y:
                    break

                stop = self.dimensions.x - offset + split
                add_string += group.string[split:stop] + ("\n" if stop <= group.length else "")
                offset = 0
                lineno += 1
                split += stop - split

            # We're done! How simple ...
            total_string += add_string

        self.layers[0].new_groups.clear()
        self.edits_this_frame = 0
        print(total_string + "\033[27;0H", end="\n")


class Layer:
    def __init__(self, layer_id, dimensions):
        self.dimensions = Vector2(dimensions.x * 2, dimensions.y)
        self.max_loc = self.dimensions.x * self.dimensions.y

        self.id = layer_id
        # new_groups contains a list of groups of text that need to be rendered next time this layer is flattened.
        self.new_groups = []
        self.full_str = ["" for i in range(self.dimensions.x * self.dimensions.y)]

    def set_char(self, loc, char, code):
        # Set a single char in the layer.
        # Step 1: binary search through new_groups to see if we need to make a new group
        # Step 2: make a new group or mod the found group
        #
        # We want to find the rightmost value less than or equal to loc,
        # then check if it encompasses the given position.
        i = bisect_right(self.new_groups, loc) - 1
        if i >= len(self.new_groups) or i < 0:
            i = None
        else:
            # Also check backwards for things with the SAME start location.
            while i >= 1 and self.new_groups[i - 1].start == loc:
                i -= 1

        if i is not None:
            # If a group already exists, we might need to split it.
            # This happens when codes are not equal.
            # In that situation, split our insert group in two and add a new one as normal.
            insert_group = self.new_groups[i]

            # This is only ever relevant when our found group actually reaches loc.
            if insert_group.end >= loc:
                if insert_group.code != code:
                    sub_groups = insert_group.subtract_char(loc)
                    self.new_groups.pop(i)

                    if sub_groups[0]:
                        insort_right(self.new_groups, sub_groups[0])
                    if sub_groups[1]:
                        insort_right(self.new_groups, sub_groups[1])

                    i = None
                else:
                    insert_group.mod_char(char, loc)
            else:
                i = None

        if i is None:
            # Add this char as a new group.
            insort_right(self.new_groups, RenderSection(char, loc, code))

        # Then replace the full string at location with our new thing.
        # This is only as a contingency if our console crashes / burns / explodes / vanishes without a trace.
        # It should be disableable if necessary.

        # print("\n".join("".join(char for char in canvas.layers[0].full_str[line * 40:(line + 1) * 40]) for line in range(10)))
        self.full_str[loc] = code + char

    def has_duplicate_starts(self):
        non_dupe_list = []
        for group in self.new_groups:
            if group in non_dupe_list:
                return False

            non_dupe_list.append(group)

        return True

    def set_string(self, loc, string, code):
        # Find the two groups that are closest to the start and end points of the group.
        # Perform the add function on both if they exist, or the sub function if their code is different.
        # Remove all groups in between.
        # Find the rightmost section with start less than or equal to loc; so, the closest element that can intersect.
        # IT MAY NOT INTERSECT. CHECK. IF IT DOESN'T, THEN NOTHING INTERSECTS START.
        # If we are out of bounds, leave instantly
        if loc >= self.max_loc or loc < 0:
            return

        start_i = bisect_right(self.new_groups, loc) - 1
        if start_i < 0:
            start_i = 0

        # Also check backwards for things with the SAME start location.
        while start_i >= 1 and self.new_groups[start_i - 1].start == loc:
            start_i -= 1

        # Cut the string down to the maximum possible length it could be.
        cut_string = string[:self.max_loc - loc]
        add_group = RenderSection(cut_string, loc, code)
        add_groups = [None, add_group, None]
        remove = []

        for mid_i in range(start_i, len(self.new_groups)):
            # If the group is the same, add. If it's different, subtract.
            # Fill up add_groups with subtracted items.
            mid_group = self.new_groups[mid_i]

            # If it starts after our end, break. We're out.
            if mid_group.start >= add_group.end:
                break

            # If it ends before our start, discard. We don't need it.
            if mid_group.end > add_group.start:
                # If it is eclipsed, we don't need it at all.
                if mid_group.start >= add_group.start and mid_group.end <= add_group.end:
                    remove.append(mid_i)
                else:
                    # Otherwise, run either subtract or add on it.
                    if mid_group.code == add_group.code:
                        # Add the mid group to the code. Delete the mid group.
                        add_group.add_section_below(mid_group)
                        remove.append(mid_i)
                    else:
                        # Subtract the add group from the mid group. Remove previous mid group.
                        sub_groups = mid_group.subtract_section(add_group)
                        if sub_groups[0]:
                            add_groups[0] = sub_groups[0]
                        if sub_groups[1]:
                            add_groups[2] = sub_groups[1]

                        remove.append(mid_i)

        remove.reverse()
        for index in remove:
            self.new_groups.pop(index)

        # THERE'S A BETTER WAY OF DOING THIS; I JUST DONT KNOW WHAT RIGHT NOW :(
        add_index = bisect_right(self.new_groups, loc)
        for group in add_groups:
            if group:
                self.new_groups.insert(start_i + add_index, group)
                add_index += 1

        # For full string usage.
        for index, char in enumerate(string):
            self.full_str[index] = code + char


def enable_ansi():
    """
    Uses a hack to enable ANSI mode in the Windows console. Does nothing on Linux.
    """
    if system_type() == "Windows":
        # might fuck around
        os.system("")


"""
enable_ansi()
canvas = Canvas(Vector2(40, 26), 1, ())
canvas.render_blank()
import time
import random
ind = 0
for j in range(50):
    canvas.render_all()
    canvas.layers[0].set_char(
        ind % (40 * 26) * 2, "##", "\033[{}m".format(31 + (ind % 12) // 2)
    )
    ind += random.randint(0, 80 * 26)
    canvas.layers[0].set_char(
        ind % (40 * 26) * 2, "##", "\033[{}m".format(31 + (ind % 12) // 2)
    )
    ind += random.randint(0, 80 * 26)


input("")
ind = 0
while True:
    canvas.render_all()
    for yval in range(26):
        if ind % 14 == 13:
            canvas.layers[0].set_string(
                0 + (80 * yval), "---------|" if ind % 2 == 0 else "~~~~~~~~~/", "\033[{}m".format(32)
            )

            canvas.layers[0].set_string(
                20 + (80 * yval), "|---------" if ind % 2 == 0 else "\~~~~~~~~~", "\033[{}m".format(32)
            )

            canvas.layers[0].set_string(
                10 + (80 * yval), "          ", "\033[{}m".format(32)
            )
        else:
            canvas.layers[0].set_string(
                0 + (80 * yval), "---------|" if ind % 2 == 0 else "~~~~~~~~~/", "\033[{}m".format(32)
            )

            canvas.layers[0].set_string(
                20 + (80 * yval), "|---------" if ind % 2 == 0 else "\~~~~~~~~~", "\033[{}m".format(32)
            )

            canvas.layers[0].set_string(
                14 - (1 * (ind % 14)) + (80 * yval), "<{}>".format("--" * (ind % 14)), "\033[{}m".format((34, 32, 33, 31)[int((ind % 14) / 14 * 4)])
            )

    # canvas.layers[0].set_string(
    #     i % (40 * 26), "abcdefghijklmnopqrstuvwxyz", "\033[{}m".format(31 + (i % 12) // 2)
    # )
    ind += 1

# TODO write nicer functions and multilayer support
# multilayer entails searching upwards for groups that intersect; if so, adding them to the layer
# nicer functions means an actual set_char for the canvas that takes in x, y rather than absolute loc
# also add set_string which can take in a longer string, subtracting any intersecting groups
"""

================================================
FILE: CLIRender/dat.py
================================================
# contains:
#
#    x position (generic typing because python but should be an int in most cases)
#    y position (the same)
#
class Vector2:  # to be honest this is me showing off
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __str__(self):  # returns a formatted string for the vector2
        return "({v.x}, {v.y})".format(v=self)

    def __add__(self, other):  # allows adding of vector2s, and ints
        if isinstance(other, Vector2):
            return Vector2(self.x + other.x, self.y + other.y)
        elif isinstance(other, (int, float)):
            return Vector2(self.x + other, self.y + other)
        else:
            raise ValueError("Accepted data types are float, int and Vector2.")

    def __mul__(self, other):  # multiplies a vector2 by an int, or by another vector2
        if isinstance(other, Vector2):
            return Vector2(self.x * other.x, self.y * other.y)
        elif isinstance(other, (int, float)):
            return Vector2(self.x * other, self.y * other)
        else:
            raise ValueError("Accepted data types are float, int and Vector2.")

    def __truediv__(self, other):  # returns a FLOAT value (true div)
        if isinstance(other, Vector2):
            return Vector2(self.x / other.x, self.y / other.y)
        elif isinstance(other, (int, float)):
            return Vector2(self.x / other, self.y / other)
        else:
            raise ValueError("Accepted data types are float, int and Vector2.")

    def __floordiv__(self, other):  # returns an INT value (floor div)
        if isinstance(other, Vector2):
            return Vector2(self.x // other.x, self.y // other.y)
        elif isinstance(other, (int, float)):
            return Vector2(self.x // other, self.y // other)
        else:
            raise ValueError("Accepted data types are float, int and Vector2.")

    def __pos__(self):
        return Vector2(abs(self.x), abs(self.y))

    def __neg__(self):
        return Vector2(-self.x, -self.y)
        
    def __eq__(self, other):
        return self.x == other.x and self.y == other.y
    
    def Magnitude(self):  # returns the magnitude of the vector2. (length of the vector)
        return ((self.x ** 2) + (self.y ** 2)) ** (1/2) #sqrt


class Vector3:  # to be honest this is me showing off
    def __init__(self, x, y, z):
        self.x = x
        self.y = y
        self.z = z

    def __str__(self):  # returns a formatted string for the Vector3
        return "({v.x}, {v.y}, {v.z})".format(v=self)

    def __add__(self, other):  # allows adding of Vector3s, and ints
        if isinstance(other, Vector3):
            return Vector3(self.x + other.x, self.y + other.y, self.z + other.z)
        elif isinstance(other, (int, float)):
            return Vector3(self.x + other, self.y + other, self.z + other)
        else:
            raise ValueError("Accepted data types are float, int and Vector3.")

    def __mul__(self, other):  # multiplies a Vector3 by an int, or by another Vector3
        if isinstance(other, Vector3):
            return Vector3(self.x * other.x, self.y * other.y, self.z * other.z)
        elif isinstance(other, (int, float)):
            return Vector3(self.x * other, self.y * other, self.z * other)
        else:
            raise ValueError("Accepted data types are float, int and Vector3.")

    def __truediv__(self, other):  # returns a FLOAT value (true div)
        if isinstance(other, Vector3):
            return Vector3(self.x / other.x, self.y / other.y, self.z / other.z)
        elif isinstance(other, (int, float)):
            return Vector3(self.x / other, self.y / other, self.z / other)
        else:
            raise ValueError("Accepted data types are float, int and Vector3.")

    def __floordiv__(self, other):  # returns an INT value (floor div)
        if isinstance(other, Vector3):
            return Vector3(self.x // other.x, self.y // other.y, self.z // other.z)
        elif isinstance(other, (int, float)):
            return Vector3(self.x // other, self.y // other, self.z // other)
        else:
            raise ValueError("Accepted data types are float, int and Vector3.")

    def __pos__(self):
        return Vector3(abs(self.x), abs(self.y), abs(self.z))

    def __neg__(self):
        return Vector3(-self.x, -self.y, -self.z)

    def __eq__(self, other):
        return self.x == other.x and self.y == other.y and self.z == other.z

    def Magnitude(self):  # returns the magnitude of the Vector3. (length of the vector)
        return ((self.x ** 2) + (self.y ** 2) + (self.z ** 2)) ** (1 / 2)  # sqrt


================================================
FILE: README.md
================================================
# credits_public
- Public version of Frums - Credits animation repository.
- Should be multiplatform. Feel free to raise an issue if it isn't. Feel free to raise an issue for anything.
- Tested on clean Python 3.6 (Windows 10) with minimal modules installed.
 
 
# media credits
- All animation work done by me (plaaosert)
- Renderer used is an in-progress command line rendering library for which a separate repository will be created... eventually.
- Song credit: Frums - Credits EX https://soundcloud.com/frums/credits-ex
- Song is not included in this repository. Read /media/README.txt.

 
# how to see
- https://youtu.be/o3cKQzrtFgQ
 
 
# how to run
 Run `credits.py`. Required libraries:
 - just-playback https://github.com/cheofusi/just_playback
 - keyboard https://github.com/boppreh/keyboard
   - **important**: if you're running **linux** or **macos**, you might have trouble with this module. if so, try using `credits_pynput.py` instead (and install pynput -- https://pypi.org/project/pynput/)
 - colorama https://github.com/tartley/colorama -
 the version of colorama used is also included inside this repository.

 You can install all required libraries by running:
 ```
 pip install -r requirements.txt
 ```

# misc

## start-of-song skips
before the animation starts, a menu will appear with options to skip to specific parts of the song. i used this a lot while debugging and left it in because i ~forgot~ felt i should include it. input nothing to let the song start normally, else hold a digit key on the keyboard to skip:
- 1 | start _(equivalent to pressing no button but slightly faster)_
- 2 | title _(flashing square in center + artist and self credits)_ 
- 3 | funding _(start of first "Funding for this program was made possible..." section)_ 
- 4 | loading _(loading bar before fake error and second typography "searching for access point" segment)_
- 5 | break _(hell ocean, second weather forecast scene)_
- 6 | final _(return of the flashing square - ending sequence)_

## generic hotkeys
| hotkey              | effect                   |
| ------------------- | ------------------------ |
| `p`                 | play/pause toggle        |
| `,`                 | small fast forward (4x)  |
| `.`                 | medium fast forward (8x) |
| `/`                 | large fast forward (16x) |


================================================
FILE: animation_classes.py
================================================
# Contains all classes (just Weather)
import random
import math


class Weather:
    def __init__(self, precip, temp, wind, gust, wind_dir, humidity, days=2):
        self.precip = precip
        self.temp = temp
        self.wind = wind
        self.gust = gust
        self.wind_dir = wind_dir
        self.humidity = humidity
        self.weather_name = self.get_weather_name()
        self.days = days

    def __str__(self):
        return "{}% precipitation\n" \
               "{}F temperature\n" \
               "{}mph wind\n" \
               "{}mph gust\n" \
               "{} wind direction\n" \
               "{}% humidity\n" \
               "{}\n".format(
                    round(self.precip * 100, 2),
                    int(self.temp),
                    int(self.wind),
                    int(self.gust),
                    int(self.wind_dir),
                    round(self.humidity * 100, 2),
                    self.weather_name
               )

    def get_weather_name(self):
        # Clear/Sunny <> Cloudy/Rainy
        if self.humidity > 0.5:
            # Cloudy or rainy
            if self.precip > 0.4:
                if self.wind > 43:
                    return "Blizzard" if self.temp < 32 else "Hurricane"
                elif self.wind > 25:
                    return "Snowstorm" if self.temp < 32 else "Storm"
                else:
                    return "Snow" if self.temp < 32 else "Rain"
            elif self.precip > 0.25:
                return "Sleet" if self.temp < 32 else "Drizzle"

            if self.humidity > 0.8 or self.precip > 0.5:
                return "Overcast"
            elif self.humidity > 0.65 or self.precip > 0.3:
                return "Cloudy"
            else:
                return "Partly cloudy"
        else:
            if self.precip > 0.4:
                return "Snow" if self.temp < 32 else "Rain"
            elif self.precip > 0.2:
                return "Sleet" if self.temp < 32 else "Drizzle"

            return ("Sunny" if self.humidity < 0.1 else "Partly sunny") if self.humidity < 0.2 else "Clear"

    def mutate(self, steps=1):
        for i in range(steps):
            # Prefer 33% precip, move wind dir by +-3 each day, gust = speed x 2.1 - 2.6
            precip_move = random.randint(-100, 100) / 400.0
            precip_move += (random.randint(0, int(100 * abs(0.33 - self.precip))) / 200) * (-1 if 0.33 - self.precip < 0 else 1)
            self.precip = min(1, max(0, self.precip + precip_move))

            self.wind_dir = (self.wind_dir + random.randint(-3, 3)) % 8

            wind_move = random.randint(-100, 100) / 13
            wind_move += (random.randint(0, int(abs(15 - self.wind))) / 3) * (-1 if 15 - self.wind < 0 else 1)
            self.wind = max(0, self.wind + wind_move)

            adjusted_days = self.days - 282
            temp_target = 40 * (math.sin((2 * math.pi * adjusted_days) / 365 - math.pi / 3) + 1) + 20
            temp_move = random.randint(-100, 100) / 20
            temp_move += (random.randint(0, int(abs(temp_target - self.temp))) / 5) * (-1 if temp_target - self.temp < 0 else 1)
            self.temp = max(0, self.temp + temp_move)

            humidity_move = random.randint(-100, 100) / 500.0
            humidity_move += (random.randint(0, int(abs(0.2 - self.humidity))) / 200) * (-1 if 0.5 - self.humidity < 0 else 1)
            self.humidity = max(0, min(1, self.humidity + humidity_move))

            self.gust = self.wind * random.randint(210, 260) * 0.01
            self.days += 1

        self.weather_name = self.get_weather_name()


# 22.10.2009: Cloudy, precip 20%, temp 43, wind dir 6, 13 (25)
# 23.10.2009: Sunny, precip 4%, temp 52, wind dir 7, 12 (25)
# 24.10.2009: Partly sunny, precip 7%, temp 48, wind dir 1, 8 (20)
known_weathers = (
    Weather(0.203, 43, 13, 25, 6, 0.66),
    Weather(0.04, 52, 12, 25, 7, 0.1),
    Weather(0.07, 48, 8, 20, 1, 0.1, 200),
    Weather(0.07, 48, 8, 20, 1, 0.1, 528 + 200),
    Weather(-1, -1, -1, -1, -1, -1)
)

known_weathers[4].weather_name = "Connection lost...      "


================================================
FILE: animation_functions.py
================================================
# Contains functions used inside credits.py.
# Reduce clutter. Reuse clutter. Recycle clutter.
import random
from CLIRender.dat import Vector2
from colorama import Fore, Style


def generate_random_hex(length):
    return ('%0' + str(length) + 'x') % random.randrange(16 ** length)


def replace_text_with_spaces(string, chance):
    return "".join((" " if random.randint(0, 100) < chance else char) for char in string)


def render_weather(c, layer, x, y, weather, mutations_after=0, spc_chance=0):
    # .                      .
    if weather.precip != -1:
        c.set_string(
            layer, Vector2(x + 5, y - 2),
            replace_text_with_spaces("{:>14}".format(weather.weather_name), spc_chance),
            Fore.YELLOW + Style.NORMAL
        )
    else:
        c.set_string(
            layer, Vector2(x, y - 2),
            replace_text_with_spaces("{:>14}".format(weather.weather_name), spc_chance),
            Fore.RED + Style.BRIGHT
        )

    temp_colour = (Fore.WHITE, Fore.CYAN, Fore.YELLOW, Fore.RED)[min(3, int(weather.temp) // 23)]
    c.set_string(
        layer, Vector2(x, y),
        replace_text_with_spaces("{:2}°F  ".format(int(weather.temp)), spc_chance),
        temp_colour + Style.BRIGHT
    )

    wind_dirs = ("N ", "NE", "E ", "SE", "S ", "SW", "W ", "NW")
    c.set_string(
        layer, Vector2(x + 5, y),
        replace_text_with_spaces("Wind {:2} mph {}".format(int(weather.wind), wind_dirs[weather.wind_dir]), spc_chance),
        Fore.CYAN + Style.BRIGHT
    )

    c.set_string(
        layer, Vector2(x + 5, y + 2),
        replace_text_with_spaces("Precipitation ", spc_chance),
        Fore.BLUE + Style.BRIGHT
    )

    c.set_string(
        layer, Vector2(x + 5, y + 3),
        replace_text_with_spaces("{:^13} ".format(str(round(weather.precip * 100, 2)) + "%"), spc_chance),
        Fore.BLUE + Style.BRIGHT
    )

    weather.mutate(mutations_after)


def noise(c, layer, amount, chars, colours):
    for n in range(amount):
        location = Vector2(random.randint(0, c.dimensions.x - 1), random.randint(0, c.dimensions.y - 1))
        c.set_char(
            layer, location, random.choice(chars), random.choice(colours)
        )


def set_multiline_string(c, layer, x, y, string, col):
    for offset, line in enumerate(string.split("\n")):
        c.set_string(
            layer, Vector2(x, y + offset), line, col
        ),


def type_text(c, generator, layer, x, y, col, render=True):
    # pop a char off the manager's text if there is one
    text_get = generator.get_data("text")
    offset = generator.get_data("offset")
    total_chars = 0
    add_offset = 0
    if text_get:
        if text_get.startswith("[##CLEAR|"):
            clear_bounds = text_get.split("|")[1].split(";")
            for yclear in range(int(clear_bounds[1])):
                location = Vector2(x, y + yclear)

                if render:
                    c.set_string(
                        layer, location, " " * int(clear_bounds[0]), col
                    )
        else:
            for linecount, text_line in enumerate(text_get.split("\n")):
                local_offset = offset - total_chars
                if 0 <= local_offset < len(text_get):
                    place_typer = local_offset < len(text_line) - 1
                    string = text_line[:local_offset] + ("_" if place_typer else "")
                    if local_offset < len(text_line) and text_line[local_offset] == "@":
                        add_offset += 3

                    string = string.replace("~", "").replace("@", "")
                    total_chars += len(text_line)
                    location = Vector2(x, y + linecount)

                    if render:
                        c.set_string(
                            layer, location, string, col
                        )

            generator.oper_data("offset", lambda t: t + 1 + add_offset)


def fuck_up_text(string, chance, also_ignore=""):
    new_str = ""
    fucks = (
        ".", ".", ".", " ", " ", "`", "=", "/", "?", "-", "$", "%"
    )
    for char in string:
        if char == "\n":
            new_str += char
        else:
            if random.randint(1, 1000) < chance and char not in "@~\n" + also_ignore:
                new_str += random.choice(fucks)
            else:
                new_str += char

    return new_str


def debug_info(c, g, b, frames):
    c.set_string(
        0, Vector2(32, 1), "{:4} g | {:4} l".format(g.parent.cur_beat, g.parent.active_scene[0].internal_beat), Style.BRIGHT + Fore.YELLOW
    ),
    counted_scenes = 0
    for index, scene in enumerate(filter(lambda s: s.name != "debug_counter", g.parent.active_scene)):
        c.set_string(
            0, Vector2(32, index + 2), "{:^17}".format(
                scene.name + " ({})".format(len(list(filter(lambda g: g.start_beat <= b, scene.generators))))
            ), Style.NORMAL + Fore.GREEN
        ),

        counted_scenes += 1

    for index2 in range(counted_scenes, 6):
        c.set_string(
            0, Vector2(32, index2 + 2), "                 ", Style.NORMAL + Fore.GREEN
        ),

    c.set_string(
        0, Vector2(32, 8), "  {:4} e/s".format(c.edits_this_frame), Style.BRIGHT + Fore.YELLOW
    ),

    avg_differences = sum(frames[i] - frames[i - 1] for i in range(len(frames) - 1, 0, -1))
    if avg_differences:
        avg_differences /= 10
    else:
        avg_differences = 60

    c.set_string(
        0, Vector2(32, 9), " {:6} fps".format(round(1 / avg_differences, 1)), Style.BRIGHT + Fore.YELLOW
    ),

    cols = (
        Fore.BLACK,
        Fore.RED,
        Fore.GREEN,
        Fore.YELLOW,
        Fore.BLUE,
        Fore.MAGENTA,
        Fore.CYAN,
        Fore.WHITE,
    )

    styles = (
        Style.NORMAL,
        Style.BRIGHT
    )
    for index in range(16):
        c.set_char(
            0, Vector2(32 + (index % 8), 11 + (index // 8)), "##", cols[index % 8] + styles[index // 8]
        ),


def clear(c, layer):
    c.clear_layer(layer)


def beat_toggle(c, g, layer, x, x2, y, y2, char, col):
    tog = g.get_data("beat_toggle")
    chars = char if tog else ".."
    x_diff = x2 - x
    for yn in range(y, y2):
        c.set_string(
            layer, Vector2(x, yn), chars * x_diff, col
        ),

    g.set_data("beat_toggle", not tog)


def work_out_date(b, day_offset=0):
    # "22.10.2009"
    lengths = (
        31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31
    )

    # One day passes every 64 beats.
    days_needed = (b // 64) + day_offset

    month_loc = 9
    day_loc = 22
    year = 2009
    while days_needed > 0:
        leap_year = (1 if year % 4 == 0 and month_loc == 1 else 0)  # leap year
        until_end_month = lengths[month_loc] + 1 - day_loc + leap_year
        day_loc += min(days_needed, until_end_month)
        days_needed -= min(days_needed, until_end_month)

        if day_loc > lengths[month_loc] + leap_year:
            month_loc += 1
            day_loc = 1
            if month_loc >= 12:
                month_loc = 0
                year += 1

    return "{:02}.{:02}.{:04}".format(day_loc, month_loc + 1, year)


def split_word_template(string):
    return [sp.split("#") for sp in string.split("\n")]


def typewrite_by_word(c, generator, layer, x, y, col, render=True, history_var="history"):
    # Show the text up to offset words at line lineno
    text_get = generator.get_data("text")
    offset = generator.get_data("offset")
    lineno = generator.get_data("lineno")
    if text_get:
        if lineno < len(text_get):
            line_get = text_get[lineno]
            line_total = "".join(line for line in line_get[:offset])

            if line_get:
                if line_get[0].startswith("[~~CLEAR|"):
                    clear_bounds = line_get[0].split("|")[1]
                    location = Vector2(x, y)

                    if render:
                        c.set_string(
                            layer, location, " " * int(clear_bounds), col
                        )
                else:
                    # print line_total and increment offset by 1.
                    # if offset is > lineno, reset offset and increment lineno
                    # if line_total is empty (offset == 0), print a clear
                    if render:
                        if line_total:
                            c.set_string(
                                layer, Vector2(x, y), line_total.replace("~", ""), col
                            ),
                        else:
                            c.set_string(
                                layer, Vector2(x, y), " " * 60, col
                            ),

                    if offset >= len(line_get):
                        history = generator.parent.get_data(history_var)
                        is_fluff = line_total.startswith(" ") or not line_total
                        is_important = line_total.endswith("~")
                        colour_select = Fore.YELLOW + Style.NORMAL
                        prefix = "- "
                        if is_fluff:
                            prefix = "  "
                            colour_select = Fore.BLACK + Style.BRIGHT
                        elif is_important:
                            prefix = "> "
                            colour_select = Fore.GREEN + Style.NORMAL

                        if not history:
                            history = [(
                                prefix + line_total.strip(" ").replace("~", ""), colour_select
                            )]
                            generator.parent.set_data(history_var, history)
                        else:
                            history.append((
                                prefix + line_total.strip(" ").replace("~", ""), colour_select
                            ))

                        generator.parent.set_data("refresh", True)

                        generator.set_data("offset", 0)
                        generator.set_data("lineno", lineno + 1)
                    else:
                        generator.set_data("offset", offset + 1)

            else:
                generator.set_data("offset", offset + 1)


def write_history(c, generator, layer, x, y, col, stop, var="history"):
    # Write history from y position going upwards until 0
    history = generator.parent.get_data(var)
    need_refresh = generator.parent.get_data("refresh")

    if need_refresh:
        generator.parent.set_data("refresh", False)

        if history:
            lineid = 0
            for ypos in range(y, stop - 1, -1):
                line = history[len(history) - 1 - lineid] if lineid < len(history) else ("", Fore.BLACK + Style.BRIGHT)
                lineid += 1

                location = Vector2(x, ypos)

                c.set_string(
                    layer, location, "{:50}".format(line[0]), line[1]
                ),
        else:
            for ypos in range(y, stop - 1, -1):
                c.set_string(
                    layer, Vector2(x, ypos), " " * 50, Fore.BLACK + Style.NORMAL
                ),


def show_access_point_visual(c, generator, layer, x, y):
    # Increment counter. If counter > 8, reset it, increment the bigger counter
    counter = generator.get_data("counter")
    block_counter = generator.get_data("block")
    location_x = 5 * (block_counter % 6) + x
    location_y = 4 * (block_counter // 6) + y

    if counter < 8:
        set_multiline_string(
            c, layer, location_x, location_y,
            "  ###  \nPBS #{:02}\nPing  {}".format(block_counter + 1, counter + 1), Fore.YELLOW + Style.NORMAL
        )

        generator.oper_data("counter", lambda t: t + 1)
    else:
        set_multiline_string(
            c, layer, location_x, location_y,
            "  ...  \nPBS #{:02}\n-------".format(block_counter + 1), Fore.BLACK + Style.BRIGHT
        )

        location_x_next = 5 * ((block_counter + 1) % 6) + x
        location_y_next = 4 * ((block_counter + 1) // 6) + y

        set_multiline_string(
            c, layer, location_x_next, location_y_next,
            "  ###  \nPBS #{:02}\nPing  {}".format(block_counter + 2, 1), Fore.YELLOW + Style.NORMAL
        )

        generator.set_data("counter", 1)
        generator.oper_data("block", lambda t: t + 1)


def make_poweroff_bars(c, b, layer, col):
    # at b=1, full screen
    # then lerp height on both ends down to 0
    height = int(24 / (b ** 1.3))
    location = Vector2(0, 12 - (height // 2))

    c.set_string(
        layer, location, ("##" * 40) * height, col
    )

================================================
FILE: animation_scenes.py
================================================
# Contains all animation scenes except for debug_counter.
# Use variable "all_scenes" which contains all scenes defined here.
# This file also creates the canvas.

import animator as am
from animation_functions import *
from animation_classes import known_weathers
from ocean import begin_ocean, update_ocean_slices
from CLIRender.classes import Canvas

canvas = Canvas(Vector2(40, 24), 1, ())

wipe = am.Scene(
    "wipe",
    (
        am.Generator(
            0, am.Generator.always(),
            am.Generator.no_create(),
            lambda g, b: noise(
                canvas, 0, int(b ** 1.4), ("##", "@@", "  "),
                (
                    *((Style.BRIGHT + Fore.WHITE,) * b),
                    *((Style.NORMAL + Fore.WHITE,) * (70 - b)),
                    *((Style.BRIGHT + Fore.BLACK,) * 4 * (40 - b)),
                )
            ),
            am.Generator.no_request()
        ),
    )
)


clear_wipe = am.Scene(
    "clear_wipe",
    (
        am.Generator(
            0, am.Generator.always(),
            am.Generator.no_create(),
            lambda g, b: noise(
                canvas, 0, int(b ** 2.2), ("  ",), (Fore.WHITE + Style.BRIGHT,)
            ),
            am.Generator.no_request()
        ),
    )
)


clear_scene = am.Scene(
    "clear",
    (
        #am.Generator(
        #    0, am.Generator.at_beat(0),
        #    lambda g: noise(
        #        canvas, 0, 40 * 24, ("##",), (Style.BRIGHT + Fore.WHITE,)
        #    ),
        #    am.Generator.no_request(), am.Generator.no_request()
        #),

        am.Generator(
            0, am.Generator.always(),
            lambda g: clear(canvas, 0),
            am.Generator.no_request(), am.Generator.no_request()
        ),
    )
)


ocean = am.Scene(
    "ocean_b",
    (
        am.Generator(
            0, am.Generator.every_n_beats(2),
            lambda g: g.set_data("ocean", begin_ocean(), "ocean_glitch", 1, "ocean_col", Style.BRIGHT + Fore.BLUE),
            lambda g, b: canvas.set_string(
                0, Vector2(0, 14), update_ocean_slices(g.get_data("ocean"),
                                                       g.get_data("ocean_glitch")), g.get_data("ocean_col")
            ),
            am.Generator.no_request()
        ),

        am.Generator(
            0, am.Generator.always(),
            lambda g: g.set_data("text", "", "offset", 0),
            lambda g, b: type_text(canvas, g, 0, 1, 1, Style.BRIGHT + Fore.WHITE),
            am.Generator.no_request()
        ),
    )
)


ocean2 = am.Scene(
    "ocean_c",
    (
        am.Generator(
            0, am.Generator.every_n_beats(2),
            lambda g: g.set_data("ocean", begin_ocean(), "ocean_glitch", 16, "ocean_col", Style.BRIGHT + Fore.BLACK),
            lambda g, b: canvas.set_string(
                0, Vector2(0, 14), update_ocean_slices(g.get_data("ocean"),
                                                       g.get_data("ocean_glitch")), g.get_data("ocean_col")
            ),
            am.Generator.no_request()
        ),

        am.Generator(
            0, am.Generator.always(),
            lambda g: g.set_data("text", "", "offset", 0),
            lambda g, b: type_text(canvas, g, 0, 1, 1, Style.BRIGHT + Fore.RED, int(b ** 1.143) % 20 not in (0, 8, 17, 15)),
            am.Generator.no_request()
        ),

        am.Generator(
            0, am.Generator.always(),
            lambda g: g.set_data("text", "", "offset", 0),
            lambda g, b: type_text(canvas, g, 0, 1, 1, Style.BRIGHT + Fore.RED, int(b ** 1.2) % 20 in (0, 15)),
            am.Generator.no_request()
        ),

        am.Generator(
            0, am.Generator.always(),
            lambda g: g.set_data("text", "", "offset", 0),
            lambda g, b: type_text(canvas, g, 0, 1, 1, Style.BRIGHT + Fore.RED, int(b ** 1.2) % 20 == 8),
            am.Generator.no_request()
        ),

        am.Generator(
            0, am.Generator.always(),
            lambda g: g.set_data("text", "", "offset", 0),
            lambda g, b: type_text(canvas, g, 0, 1, 1, Style.BRIGHT + Fore.RED, int(b ** 1.2) % 20 == 17),
            am.Generator.no_request()
        ),
    )
)


ocean3 = am.Scene(
    "ocean_d",
    (
        am.Generator(
            0, am.Generator.always(),
            lambda g: g.set_data("ocean", begin_ocean(), "ocean_glitch", 100, "ocean_col", Style.NORMAL + Fore.MAGENTA),
            lambda g, b: canvas.set_string(
                0, Vector2(0, 14), update_ocean_slices(g.get_data("ocean"),
                                                       g.get_data("ocean_glitch")), g.get_data("ocean_col")
            ),
            am.Generator.no_request()
        ),

        am.Generator(
            0, am.Generator.always(),
            am.Generator.no_create(),
            lambda g, b: set_multiline_string(canvas, 0, 1, 1,
                                              "The system has encountered a fatal error. Please wait.\n\n"
                                              "[ERR: 801]\n\n" + "\n".join(
                                                  " ".join(generate_random_hex(4) for _ in range(8)) for _ in range(4)
                                              ),
                                              Style.BRIGHT + Fore.RED),
            am.Generator.no_request()
        ),
    )
)


text = am.Scene(
    "typewrite",
    (
        am.Generator(
            0, am.Generator.always(),
            lambda g: g.set_data("text", "", "offset", 0),
            lambda g, b: type_text(canvas, g, 0, 1, 1, Style.BRIGHT + Fore.WHITE),
            am.Generator.no_request()
        ),
    )
)


title = am.Scene(
    "title",
    (
        # We have 12 beat-ends to map to:
        # '  - plaaosert
        # '  - frums
        # '  - python 3.6
        # '  - command line
        # drums
        # '  -
        # '  -
        # '  -
        # '  -
        # small moog
        # '  -
        # '  -
        # '  -
        # '  -

        am.Generator(
            64, am.Generator.at_beat(64),
            am.Generator.no_create(),
            lambda g, b: canvas.set_string(
                0, Vector2(1, 1), "animation | plaaosert", Fore.CYAN + Style.BRIGHT
            ),
            am.Generator.no_request()
        ),

        am.Generator(
            128, am.Generator.at_beat(128),
            am.Generator.no_create(),
            lambda g, b: canvas.set_string(
                0, Vector2(1, 2), "bgm       | Frums - Credits", Fore.CYAN + Style.BRIGHT
            ),
            am.Generator.no_request()
        ),

        am.Generator(
            192, am.Generator.at_beat(192),
            am.Generator.no_create(),
            lambda g, b: canvas.set_string(
                0, Vector2(1, 4), "running pure Python 3.6", Fore.CYAN + Style.NORMAL
            ),
            am.Generator.no_request()
        ),

        am.Generator(
            256, am.Generator.at_beat(256),
            am.Generator.no_create(),
            lambda g, b: canvas.set_string(
                0, Vector2(1, 5), "in the command line", Fore.CYAN + Style.NORMAL
            ),
            am.Generator.no_request()
        ),

        am.Generator(
            320, am.Generator.at_beat(320),
            am.Generator.no_create(),
            lambda g, b: canvas.set_string(
                0, Vector2(0, 21), "--" * 40, Fore.WHITE + Style.BRIGHT
            ),
            am.Generator.no_request()
        ),

        am.Generator(
            384, am.Generator.at_beat(384),
            am.Generator.no_create(),
            lambda g, b: canvas.set_string(
                0, Vector2(1, 22), "> _ ", Fore.WHITE + Style.BRIGHT
            ),
            am.Generator.no_request()
        ),

        am.Generator(
            448, am.Generator.at_beat(448),
            am.Generator.no_create(),
            lambda g, b: set_multiline_string(
                canvas, 0, 26, 14, "----------------------------\n" +
                                   "|                          |\n" * 6, Fore.WHITE + Style.BRIGHT
            ),
            am.Generator.no_request()
        ),

        am.Generator(
            512, am.Generator.at_beat(512),
            am.Generator.no_create(),
            lambda g, b: set_multiline_string(
                canvas, 0, 27, 15, "22.10.2009", Fore.YELLOW + Style.BRIGHT
            ),
            am.Generator.no_request()
        ),

        am.Generator(
            576, am.Generator.at_beat(576),
            am.Generator.no_create(),
            lambda g, b: set_multiline_string(
                canvas, 0, 26, 16, "----------------------------", Fore.WHITE + Style.BRIGHT
            ),
            am.Generator.no_request()
        ),

        am.Generator(
            640, am.Generator.at_beat(640),
            am.Generator.no_create(),
            lambda g, b: set_multiline_string(
                canvas, 0, 27, 17, "43°F      Wind 13 mph W \n"
                                   "                        ", Fore.CYAN + Style.BRIGHT
            ),
            am.Generator.no_request()
        ),

        am.Generator(
            704, am.Generator.at_beat(704),
            am.Generator.no_create(),
            lambda g, b: set_multiline_string(
                canvas, 0, 32, 19, "Precipitation \n"
                                   "    20.3%     ", Fore.BLUE + Style.BRIGHT
            ),
            am.Generator.no_request()
        ),

        am.Generator(
            768, am.Generator.at_beat(768),
            am.Generator.no_create(),
            lambda g, b: set_multiline_string(
                canvas, 0, 36, 15, "Cloudy", Fore.YELLOW + Style.NORMAL
            ),
            am.Generator.no_request()
        ),
    )
)


beats = am.Scene(
    "beats",
    (
        # beat manager
        am.Generator(
            0, am.Generator.combine_conditions(am.Generator.every_on_off(48, 16), am.Generator.every_n_beats(4)),
            lambda g: g.set_data("beat_toggle", True),
            lambda g, b: beat_toggle(canvas, g, 0, 18, 22, 11, 15, "@@", Style.BRIGHT + Fore.YELLOW),
            am.Generator.no_request()
        ),
        am.Generator(
            0, am.Generator.combine_conditions(am.Generator.every_off_on(48, 16), am.Generator.every_n_beats(2)),
            lambda g: g.set_data("beat_toggle", True),
            lambda g, b: beat_toggle(canvas, g, 0, 18, 22, 11, 15, "##", Style.BRIGHT + Fore.GREEN),
            am.Generator.no_request()
        ),
    )
)

beats_lr = am.Scene(
    "beats_lr",
    (
        # beat manager
        am.Generator(
            0, am.Generator.combine_conditions(am.Generator.every_on_off(48, 16), am.Generator.every_n_beats(4)),
            lambda g: g.set_data("beat_toggle", True),
            lambda g, b: beat_toggle(canvas, g, 0, 18, 20, 11, 15, "@@", Style.BRIGHT + Fore.YELLOW),
            am.Generator.no_request()
        ),
        am.Generator(
            0, am.Generator.combine_conditions(am.Generator.every_off_on(48, 16), am.Generator.every_n_beats(2)),
            lambda g: g.set_data("beat_toggle", True),
            lambda g, b: beat_toggle(canvas, g, 0, 20, 22, 11, 15, "##", Style.BRIGHT + Fore.GREEN),
            am.Generator.no_request()
        ),
    )
)


beats_side = am.Scene(
    "beats_side",
    (
        # beat manager
        am.Generator(
            0, am.Generator.combine_conditions(am.Generator.every_on_off(48, 16), am.Generator.every_n_beats(4)),
            lambda g: g.set_data("beat_toggle", True),
            lambda g, b: beat_toggle(canvas, g, 0, 18, 20, 11, 15, "@@", Style.BRIGHT + Fore.YELLOW),
            am.Generator.no_request()
        ),
        am.Generator(
            0, am.Generator.combine_conditions(am.Generator.every_off_on(48, 16), am.Generator.every_n_beats(2)),
            lambda g: g.set_data("beat_toggle", True),
            lambda g, b: beat_toggle(canvas, g, 0, 20, 22, 11, 15, "##", Style.BRIGHT + Fore.GREEN),
            am.Generator.no_request()
        ),
    )
)


date_ticker = am.Scene(
    "dates",
    (
        # TODO change this shit to every n beats, use a data variable to store the current date index PLEASE
        #
        # Run at normal speed
        # Run at double speed
        # Run with slight randomness forwards
        # Skip forwards, go every step
        # At crazy part, switch off this and go to a glitchy display
        #
        am.Generator(
            0, am.Generator.before_n(64 * 8),
            am.Generator.no_create(),
            lambda g, b: canvas.set_string(
                0, Vector2(27, 15), work_out_date(b), Fore.YELLOW + Style.BRIGHT
            ),
            am.Generator.no_request()
        ),

        am.Generator(
            64 * 8, am.Generator.before_n(64 * 12 - 4),
            am.Generator.no_create(),
            lambda g, b: canvas.set_string(
                0, Vector2(27, 15), work_out_date((64 * 8) + (b - (64 * 8)) * 2), Fore.YELLOW + Style.BRIGHT
            ),
            am.Generator.no_request()
        ),

        am.Generator(
            64 * 12 - 4, am.Generator.at_beat(64 * 12 - 4),
            am.Generator.no_create(),
            lambda g, b: canvas.set_string(
                0, Vector2(27, 15), "??.??.????", Fore.YELLOW + Style.BRIGHT
            ),
            am.Generator.no_request()
        ),

        am.Generator(
            64 * 12 + 4, am.Generator.combine_conditions(am.Generator.every_n_beats(4), am.Generator.before_n(64 * 16)),
            am.Generator.no_create(),
            lambda g, b: canvas.set_string(
                0, Vector2(27, 15), work_out_date(random.randint(64 * 12 + b * 8, 64 * 12 + b * 24)), Fore.YELLOW + Style.BRIGHT
            ),
            am.Generator.no_request()
        ),

        am.Generator(
            64 * 16, am.Generator.always(),
            am.Generator.no_create(),
            lambda g, b: canvas.set_string(
                0, Vector2(27, 15), work_out_date(64 * 16 + b * 64 - 64 * 16 * 32), Fore.YELLOW + Style.BRIGHT
            ),
            am.Generator.no_request()
        ),
    )
)


redraw_ui = am.Scene(
    "redraw_ui",
    (
        am.Generator(
            0, am.Generator.at_beat(0),
            am.Generator.no_create(),
            lambda g, b: canvas.set_string(
                0, Vector2(0, 21), "--" * 40, Fore.WHITE + Style.BRIGHT
            ),
            am.Generator.no_request()
        ),

        am.Generator(
            0, am.Generator.at_beat(0),
            am.Generator.no_create(),
            lambda g, b: canvas.set_string(
                0, Vector2(1, 22), "> _ ", Fore.WHITE + Style.BRIGHT
            ),
            am.Generator.no_request()
        ),

        am.Generator(
            0, am.Generator.at_beat(2),
            am.Generator.no_create(),
            lambda g, b: set_multiline_string(
                canvas, 0, 26, 14, "----------------------------\n" +
                                   "|                          |\n" * 6, Fore.WHITE + Style.BRIGHT
            ),
            am.Generator.no_request()
        ),

        am.Generator(
            0, am.Generator.at_beat(3),
            am.Generator.no_create(),
            lambda g, b: set_multiline_string(
                canvas, 0, 27, 15, "22.10.2009", Fore.YELLOW + Style.BRIGHT
            ),
            am.Generator.no_request()
        ),

        am.Generator(
            0, am.Generator.at_beat(4),
            am.Generator.no_create(),
            lambda g, b: set_multiline_string(
                canvas, 0, 26, 16, "----------------------------", Fore.WHITE + Style.BRIGHT
            ),
            am.Generator.no_request()
        ),

        am.Generator(
            0, am.Generator.at_beat(5),
            am.Generator.no_create(),
            lambda g, b: set_multiline_string(
                canvas, 0, 27, 17, "43°F      Wind 13 mph W ", Fore.CYAN + Style.BRIGHT
            ),
            am.Generator.no_request()
        ),

        am.Generator(
            0, am.Generator.at_beat(6),
            am.Generator.no_create(),
            lambda g, b: set_multiline_string(
                canvas, 0, 32, 19, "Precipitation \n"
                                   "    20.3%     ", Fore.BLUE + Style.BRIGHT
            ),
            am.Generator.no_request()
        ),

        am.Generator(
            0, am.Generator.at_beat(7),
            am.Generator.no_create(),
            lambda g, b: set_multiline_string(
                canvas, 0, 36, 15, "Cloudy", Fore.YELLOW + Style.NORMAL
            ),
            am.Generator.no_request()
        ),
    )
)


weather = am.Scene(
    "weather",
    (
        am.Generator(
            0, am.Generator.combine_conditions(am.Generator.every_n_beats(64), am.Generator.before_n(64 * 8)),
            am.Generator.no_create(),
            lambda g, b: render_weather(canvas, 0, 27, 17, known_weathers[min(2, b // 64)], 0 if b < 128 else 1),
            am.Generator.no_request()
        ),
        am.Generator(
            64 * 8, am.Generator.combine_conditions(am.Generator.every_n_beats(32), am.Generator.before_n(64 * 12)),
            am.Generator.no_create(),
            lambda g, b: render_weather(canvas, 0, 27, 17, known_weathers[2], 1),
            am.Generator.no_request()
        ),

        am.Generator(
            64 * 12 - 4, am.Generator.at_beat(64 * 12 - 4),
            am.Generator.no_create(),
            lambda g, b: render_weather(canvas, 0, 27, 17, known_weathers[4], 1),
            am.Generator.no_request()
        ),

        am.Generator(
            64 * 12 + 4, am.Generator.combine_conditions(am.Generator.every_n_beats(4), am.Generator.before_n(64 * 16)),
            am.Generator.no_create(),
            lambda g, b: render_weather(canvas, 0, 27, 17, known_weathers[2], 14),
            am.Generator.no_request()
        ),
        am.Generator(
            64 * 16, am.Generator.always(),
            am.Generator.no_create(),
            lambda g, b: render_weather(canvas, 0, 27, 17, known_weathers[3], 1, max(0, (b - 1080) * 2.2)),
            am.Generator.no_request()
        ),
    )
)


funding = am.Scene(
    "funding",
    (
        am.Generator(
            0, am.Generator.always(),
            lambda g: g.set_data("text", [], "offset", 0, "lineno", 0, "type_col", Fore.WHITE + Style.BRIGHT),
            lambda g, b: typewrite_by_word(canvas, g, 0, 2, 22, g.get_data("type_col")),
            am.Generator.no_request()
        ),

        am.Generator(
            0, am.Generator.always(),
            am.Generator.no_create(),
            lambda g, b: write_history(canvas, g, 0, 1, 19, Fore.BLACK + Style.BRIGHT, 1),
            am.Generator.no_request()
        ),
    )
)


loading = am.Scene(
    "loadingbar",
    (
        am.Generator(
            0, am.Generator.at_beat(0),
            lambda g: g.scene.set_data("progress", 0),
            lambda g, b: set_multiline_string(canvas, 0, 2, 9,
                                              "----------------------------------\n"
                                              "|                                |\n"
                                              "----------------------------------",
                                              Fore.WHITE + Style.BRIGHT),
            am.Generator.no_request()
        ),

        am.Generator(
            1, am.Generator.at_beat(1),
            lambda g: g.scene.set_data("progress", 0),
            lambda g, b: set_multiline_string(canvas, 0, 3, 10,
                                              "          Loading...          \n",
                                              Fore.BLACK + Style.BRIGHT),
            am.Generator.no_request()
        ),

        am.Generator(
            0, am.Generator.combine_conditions(am.Generator.before_n(240), am.Generator.every_n_beats(8)),
            am.Generator.no_create(),
            lambda g, b: g.scene.oper_data("progress", lambda t: t + 1),
            am.Generator.no_request()
        ),

        am.Generator(
            240, am.Generator.always(),
            am.Generator.no_create(),
            lambda g, b: g.scene.oper_data("progress", lambda t: t + random.randint(40, 70)),
            am.Generator.no_request()
        ),

        am.Generator(
            0, am.Generator.before_n(240),
            am.Generator.no_create(),
            lambda g, b: canvas.set_string(0, Vector2(3, 10), "#" * g.scene.get_data("progress"),
                                           Fore.YELLOW + Style.BRIGHT),
            am.Generator.no_request()
        ),

        am.Generator(
            240, am.Generator.always(),
            am.Generator.no_create(),
            lambda g, b: canvas.set_string(0, Vector2(3, 10), fuck_up_text("#" * g.scene.get_data("progress"), 400),
                                           Fore.RED + Style.BRIGHT),
            am.Generator.no_request()
        ),

        am.Generator(
            0, am.Generator.before_n(240),
            lambda g: g.set_data("text", [], "offset", 0, "lineno", 0, "type_col", Fore.BLACK + Style.BRIGHT),
            lambda g, b: typewrite_by_word(canvas, g, 0, 3, 12, g.get_data("type_col")),
            am.Generator.no_request()
        ),
    )
)


quick_loading = am.Scene(
    "fastload",
    (
        am.Generator(
            0, am.Generator.at_beat(0),
            lambda g: g.scene.set_data("progress", 0),
            lambda g, b: set_multiline_string(canvas, 0, 2, 9,
                                              "----------------------------------\n"
                                              "|                                |\n"
                                              "----------------------------------",
                                              Fore.WHITE + Style.BRIGHT),
            am.Generator.no_request()
        ),

        am.Generator(
            1, am.Generator.at_beat(1),
            lambda g: g.scene.set_data("progress", 0),
            lambda g, b: set_multiline_string(canvas, 0, 3, 10,
                                              "        Please wait...        \n",
                                              Fore.BLACK + Style.BRIGHT),
            am.Generator.no_request()
        ),

        am.Generator(
            0, am.Generator.always(),
            am.Generator.no_create(),
            lambda g, b: g.scene.oper_data("progress", lambda t: t + random.randint(4, 6)),
            am.Generator.no_request()
        ),

        am.Generator(
            0, am.Generator.always(),
            am.Generator.no_create(),
            lambda g, b: canvas.set_string(0, Vector2(3, 10), "#" * g.scene.get_data("progress"),
                                           Fore.GREEN + Style.BRIGHT),
            am.Generator.no_request()
        ),
    )
)


error_screen = am.Scene(
    "error",
    (
        am.Generator(
            0, am.Generator.at_beat(0),
            lambda g: g.scene.set_data("progress", 0),
            lambda g, b: set_multiline_string(canvas, 0, 0, 14, "--" * 40 + "\n" * 8 + "  $ ", Fore.WHITE + Style.BRIGHT),
            am.Generator.no_request()
        ),

        am.Generator(
            0, am.Generator.at_beat(0),
            lambda g: g.scene.set_data("progress", 0),
            lambda g, b: set_multiline_string(canvas, 0, 1, 1,
                                              "Automatic diagnosis unsuccessful. Please wait.\n> ",
                                              Fore.BLACK + Style.BRIGHT),
            am.Generator.no_request()
        ),
    )
)


funding_double = am.Scene(
    "fundingx2",
    (
        am.Generator(
            0, am.Generator.always(),
            lambda g: g.set_data("text", [], "offset", 0, "lineno", 0, "type_col", Fore.RED + Style.NORMAL),
            lambda g, b: typewrite_by_word(canvas, g, 0, 2, 2, g.get_data("type_col")),
            am.Generator.no_request()
        ),

        # am.Generator(
        #     0, am.Generator.always(),
        #     am.Generator.no_create(),
        #     lambda g, b: write_history(canvas, g, 0, 1, 19, Fore.BLACK + Style.BRIGHT, 1),
        #     am.Generator.no_request()
        # ),

        am.Generator(
            0, am.Generator.always(),
            lambda g: g.set_data("text", [], "offset", 0, "lineno", 0, "type_col", Fore.CYAN + Style.BRIGHT),
            lambda g, b: typewrite_by_word(canvas, g, 0, 2, 22, g.get_data("type_col"), history_var="history2"),
            am.Generator.no_request()
        ),

        am.Generator(
            0, am.Generator.always(),
            am.Generator.no_create(),
            lambda g, b: write_history(canvas, g, 0, 1, 21, Fore.BLACK + Style.BRIGHT, 15, var="history2"),
            am.Generator.no_request()
        ),

        am.Generator(
            0, am.Generator.always(),
            lambda g: g.set_data("text", [], "offset", 0, "lineno", 0, "type_col", Fore.YELLOW + Style.BRIGHT),
            lambda g, b: typewrite_by_word(canvas, g, 0, 4, 5, g.get_data("type_col"), history_var="history3"),
            am.Generator.no_request()
        ),
    )
)

access_points = am.Scene(
    "accesspoints",
    (
        am.Generator(
            0, am.Generator.combine_conditions(am.Generator.before_n(20), am.Generator.every_n_beats(2)),
            am.Generator.no_create(),
            lambda g, b: set_multiline_string(
                canvas, 0, 1, 1,
                "\n\n".join(
                    "\n".join(
                        "   ".join(
                            ("PBS #{:02}".format((yblock * 6) + x + 1) if yline == 1 else ("  ###  ", "", "Unknown")[yline]) if random.randint(0, max(1, 32 - int(b ** 1.2))) < 4 else "       "
                            for x in range(6)
                        ) for yline in range(3)
                    ) for yblock in range(4)
                ),
                Fore.BLACK + Style.BRIGHT),
            am.Generator.no_request()
        ),

        am.Generator(
            20, am.Generator.at_beat(20),
            am.Generator.no_create(),
            lambda g, b: set_multiline_string(
                canvas, 0, 1, 1,
                "\n\n".join(
                    "\n".join(
                        "   ".join(
                            ("PBS #{:02}".format((yblock * 6) + x + 1) if yline == 1 else ("  ###  ", "", "Unknown")[yline])
                            for x in range(6)
                        ) for yline in range(3)
                    ) for yblock in range(4)
                ),
                Fore.RED + Style.NORMAL),
            am.Generator.no_request()
        ),

        am.Generator(
            0, am.Generator.at_beat(0),
            am.Generator.no_create(),
            lambda g, b: set_multiline_string(canvas, 0, 1, 17,
                                              "No access points are broadcasting.\n"
                                              "Manual search in progress.\n"
                                              "Last search 27.02.2019 (532 days ago)",
                                              Fore.BLACK + Style.BRIGHT),
            am.Generator.no_request()
        ),

        am.Generator(
            80, am.Generator.combine_conditions(am.Generator.every_n_beats(8), am.Generator.before_n(976)),
            lambda g: g.set_data("counter", 0, "block", 0),
            lambda g, b: show_access_point_visual(canvas, g, 0, 1, 1),
            am.Generator.no_request()
        ),

        am.Generator(
            968, am.Generator.at_beat(968),
            am.Generator.no_create(),
            lambda g, b: set_multiline_string(canvas, 0, 6, 9, "  @@@  \nPBS #14\n Active", Fore.GREEN + Style.BRIGHT),
            am.Generator.no_request()
        )
    )
)


funding_single = am.Scene(
    "fdg_single",
    (
        am.Generator(
            0, am.Generator.at_beat(0),
            lambda g: g.scene.set_data("progress", 0),
            lambda g, b: set_multiline_string(canvas, 0, 0, 20, "--" * 40 + "\n" + "  Sending > ",
                                              Fore.WHITE + Style.BRIGHT),
            am.Generator.no_request()
        ),

        am.Generator(
            0, am.Generator.always(),
            lambda g: g.set_data("text", [], "offset", 0, "lineno", 0, "type_col", Fore.YELLOW + Style.NORMAL),
            lambda g, b: typewrite_by_word(canvas, g, 0, 6, 21, g.get_data("type_col")),
            am.Generator.no_request()
        ),
    )
)


funding_down = am.Scene(
    "fdg_down",
    (
        am.Generator(
            0, am.Generator.always(),
            lambda g: g.set_data("text", [], "offset", 0, "lineno", 0, "type_col", Fore.YELLOW + Style.NORMAL),
            lambda g, b: typewrite_by_word(canvas, g, 0, 1, 1 + g.get_data("lineno"), g.get_data("type_col")),
            am.Generator.no_request()
        ),

        am.Generator(
            0, am.Generator.always(),
            lambda g: g.set_data("text", [], "offset", 0, "lineno", 0, "type_col", Fore.YELLOW + Style.NORMAL),
            lambda g, b: typewrite_by_word(canvas, g, 0, 1, 20 + g.get_data("lineno"), g.get_data("type_col")),
            am.Generator.no_request()
        ),
    )
)


poweroff = am.Scene(
    "poweroff",
    (
        am.Generator(
            3, am.Generator.always(),
            am.Generator.no_create(),
            lambda g, b: make_poweroff_bars(canvas, b - 2, 0, Fore.BLACK + Style.NORMAL),
            am.Generator.no_request()
        ),

        am.Generator(
            2, am.Generator.always(),
            am.Generator.no_create(),
            lambda g, b: make_poweroff_bars(canvas, b - 1, 0, Fore.BLACK + Style.BRIGHT),
            am.Generator.no_request()
        ),

        am.Generator(
            1, am.Generator.always(),
            am.Generator.no_create(),
            lambda g, b: make_poweroff_bars(canvas, b, 0, Fore.WHITE + Style.NORMAL),
            am.Generator.no_request()
        ),

        am.Generator(
            0, am.Generator.always(),
            am.Generator.no_create(),
            lambda g, b: make_poweroff_bars(canvas, b + 1, 0, Fore.WHITE + Style.BRIGHT),
            am.Generator.no_request()
        ),
    )
)

all_scenes = (
    wipe, clear_wipe, clear_scene, ocean, ocean2, ocean3, text, title, beats, beats_lr, funding, date_ticker,
    weather, redraw_ui, loading, quick_loading, error_screen, funding_double, access_points, funding_single,
    funding_down, poweroff
)


================================================
FILE: animator.py
================================================
class DataStoringObject:
    def __init__(self):
        self.data = {}

    def set_data(self, *ident):
        for i in range(0, len(ident), 2):
            self.data[ident[i]] = ident[i + 1]

    def get_data(self, ident):
        if ident in self.data:
            return self.data[ident]

    def oper_data(self, ident, oper):
        self.data[ident] = oper(self.data[ident])


class SceneManager(DataStoringObject):
    def __init__(self, scenes, events):
        super().__init__()

        self.scenes = {scene.name: scene for scene in scenes}
        self.events = {event.beat: [ev for ev in events if ev.beat == event.beat] for event in events}

        for scene in scenes:
            scene.set_parent(self)

        self.active_scene = []
        self.cur_beat = -1
        self.data = {}

    def start_scene(self, scene, at=0):
        if len(self.active_scene) == 0:
            self.active_scene.append(None)

        self.active_scene[0] = self.scenes[scene]
        self.active_scene[0].start(at)

    def add_scene(self, scene, at=0):
        self.active_scene.append(self.scenes[scene])
        self.active_scene[-1].start(at)

    def remove_scene(self, scene):
        if self.scenes[scene] in self.active_scene:
            self.active_scene.remove(self.scenes[scene])

    def request_next(self, render=True):
        for scene in self.active_scene:
            scene.request_frame(render)

        self.next_beat()

    def next_beat(self):
        self.cur_beat += 1
        if self.cur_beat in self.events:
            for event in self.events[self.cur_beat]:
                event.do(self)

    def set_scene_data(self, scene, *ident):
        self.scenes[scene].set_data(*ident)

    def set_generator_data(self, scene, generator, *ident):
        self.scenes[scene].generators[generator].set_data(*ident)


class Event:
    def __init__(self, beat, do):
        self.beat = beat
        self.do = do

    @staticmethod
    def swap_scene(sc, at=0):
        return lambda sm: sm.start_scene(sc, at)

    @staticmethod
    def layer_scene(sc, at=0):
        return lambda sm: sm.add_scene(sc, at)

    @staticmethod
    def remove_scene(sc):
        return lambda sm: sm.remove_scene(sc)


class Scene(DataStoringObject):
    def __init__(self, name, generators):
        super().__init__()

        self.parent = None
        self.name = name
        self.generators = generators
        self.start_beat = 0
        self.internal_beat = 0

    def set_parent(self, parent):
        self.parent = parent

    # Requests a frame. This basically calls the request() and request_clear() functions in every generator.
    def request_frame(self, render=True):
        beat = self.internal_beat
        if render:
            for generator in self.generators:
                if beat >= generator.start_beat and generator.condition(beat):
                    if beat != self.start_beat and beat != generator.start_beat:
                        # Clear previous beat
                        generator.request_clear(generator, beat - 1)

                    # Make current
                    generator.request(generator, beat)

        self.internal_beat += 1

    # Starts the scene at the beat given.
    def start(self, at):
        for generator in self.generators:
            generator.set_parent(self.parent)
            generator.set_scene(self)
            generator.on_create(generator)

        self.start_beat = at
        self.internal_beat = at
        self.request_frame()


class Generator(DataStoringObject):
    def __init__(self, start_beat, condition, on_create, request, request_clear):
        super().__init__()

        self.parent = None
        self.scene = None

        self.data = {}
        self.start_beat = start_beat
        self.condition = condition
        self.on_create = on_create
        self.request = request
        self.request_clear = request_clear

    def set_parent(self, parent):
        self.parent = parent

    def set_scene(self, scene):
        self.scene = scene

    @staticmethod
    def combine_conditions(*cond):
        return lambda b: all(c(b) for c in cond)

    @staticmethod
    def always():
        return lambda b: True

    @staticmethod
    def every_n_beats(beat):
        return lambda b: b % beat == 0

    @staticmethod
    def every_on_off(on, off):
        return lambda b: (b % (on + off)) < on

    @staticmethod
    def every_off_on(off, on):
        return lambda b: (b % (on + off)) >= off

    @staticmethod
    def before_n(beat):
        return lambda b: b < beat

    @staticmethod
    def at_beat(beat):
        return lambda b: b == beat

    @staticmethod
    def no_create():
        return lambda g: None

    @staticmethod
    def no_request():
        return lambda g, b: None


================================================
FILE: colorama/__init__.py
================================================
# Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file.
from .initialise import init, deinit, reinit, colorama_text
from .ansi import Fore, Back, Style, Cursor
from .ansitowin32 import AnsiToWin32

__version__ = '0.3.7'



================================================
FILE: colorama/ansi.py
================================================
# Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file.
'''
This module generates ANSI character codes to printing colors to terminals.
See: http://en.wikipedia.org/wiki/ANSI_escape_code
'''

CSI = '\033['
OSC = '\033]'
BEL = '\007'


def code_to_chars(code):
    return CSI + str(code) + 'm'

def set_title(title):
    return OSC + '2;' + title + BEL

def clear_screen(mode=2):
    return CSI + str(mode) + 'J'

def clear_line(mode=2):
    return CSI + str(mode) + 'K'


class AnsiCodes(object):
    def __init__(self):
        # the subclasses declare class attributes which are numbers.
        # Upon instantiation we define instance attributes, which are the same
        # as the class attributes but wrapped with the ANSI escape sequence
        for name in dir(self):
            if not name.startswith('_'):
                value = getattr(self, name)
                setattr(self, name, code_to_chars(value))


class AnsiCursor(object):
    def UP(self, n=1):
        return CSI + str(n) + 'A'
    def DOWN(self, n=1):
        return CSI + str(n) + 'B'
    def FORWARD(self, n=1):
        return CSI + str(n) + 'C'
    def BACK(self, n=1):
        return CSI + str(n) + 'D'
    def POS(self, x=1, y=1):
        return CSI + str(y) + ';' + str(x) + 'H'


class AnsiFore(AnsiCodes):
    BLACK           = 30
    RED             = 31
    GREEN           = 32
    YELLOW          = 33
    BLUE            = 34
    MAGENTA         = 35
    CYAN            = 36
    WHITE           = 37
    RESET           = 39

    # These are fairly well supported, but not part of the standard.
    LIGHTBLACK_EX   = 90
    LIGHTRED_EX     = 91
    LIGHTGREEN_EX   = 92
    LIGHTYELLOW_EX  = 93
    LIGHTBLUE_EX    = 94
    LIGHTMAGENTA_EX = 95
    LIGHTCYAN_EX    = 96
    LIGHTWHITE_EX   = 97


class AnsiBack(AnsiCodes):
    BLACK           = 40
    RED             = 41
    GREEN           = 42
    YELLOW          = 43
    BLUE            = 44
    MAGENTA         = 45
    CYAN            = 46
    WHITE           = 47
    RESET           = 49

    # These are fairly well supported, but not part of the standard.
    LIGHTBLACK_EX   = 100
    LIGHTRED_EX     = 101
    LIGHTGREEN_EX   = 102
    LIGHTYELLOW_EX  = 103
    LIGHTBLUE_EX    = 104
    LIGHTMAGENTA_EX = 105
    LIGHTCYAN_EX    = 106
    LIGHTWHITE_EX   = 107


class AnsiStyle(AnsiCodes):
    BRIGHT    = 1
    DIM       = 2
    NORMAL    = 22
    RESET_ALL = 0

Fore   = AnsiFore()
Back   = AnsiBack()
Style  = AnsiStyle()
Cursor = AnsiCursor()


================================================
FILE: colorama/ansitowin32.py
================================================
# Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file.
import re
import sys
import os

from .ansi import AnsiFore, AnsiBack, AnsiStyle, Style
from .winterm import WinTerm, WinColor, WinStyle
from .win32 import windll, winapi_test


winterm = None
if windll is not None:
    winterm = WinTerm()


def is_stream_closed(stream):
    return not hasattr(stream, 'closed') or stream.closed


def is_a_tty(stream):
    return hasattr(stream, 'isatty') and stream.isatty()


class StreamWrapper(object):
    '''
    Wraps a stream (such as stdout), acting as a transparent proxy for all
    attribute access apart from method 'write()', which is delegated to our
    Converter instance.
    '''
    def __init__(self, wrapped, converter):
        # double-underscore everything to prevent clashes with names of
        # attributes on the wrapped stream object.
        self.__wrapped = wrapped
        self.__convertor = converter

    def __getattr__(self, name):
        return getattr(self.__wrapped, name)

    def write(self, text):
        self.__convertor.write(text)


class AnsiToWin32(object):
    '''
    Implements a 'write()' method which, on Windows, will strip ANSI character
    sequences from the text, and if outputting to a tty, will convert them into
    win32 function calls.
    '''
    ANSI_CSI_RE = re.compile('\001?\033\[((?:\d|;)*)([a-zA-Z])\002?')     # Control Sequence Introducer
    ANSI_OSC_RE = re.compile('\001?\033\]((?:.|;)*?)(\x07)\002?')         # Operating System Command

    def __init__(self, wrapped, convert=None, strip=None, autoreset=False):
        # The wrapped stream (normally sys.stdout or sys.stderr)
        self.wrapped = wrapped

        # should we reset colors to defaults after every .write()
        self.autoreset = autoreset

        # create the proxy wrapping our output stream
        self.stream = StreamWrapper(wrapped, self)

        on_windows = os.name == 'nt'
        # We test if the WinAPI works, because even if we are on Windows
        # we may be using a terminal that doesn't support the WinAPI
        # (e.g. Cygwin Terminal). In this case it's up to the terminal
        # to support the ANSI codes.
        conversion_supported = on_windows and winapi_test()

        # should we strip ANSI sequences from our output?
        if strip is None:
            strip = conversion_supported or (not is_stream_closed(wrapped) and not is_a_tty(wrapped))
        self.strip = strip

        # should we should convert ANSI sequences into win32 calls?
        if convert is None:
            convert = conversion_supported and not is_stream_closed(wrapped) and is_a_tty(wrapped)
        self.convert = convert

        # dict of ansi codes to win32 functions and parameters
        self.win32_calls = self.get_win32_calls()

        # are we wrapping stderr?
        self.on_stderr = self.wrapped is sys.stderr

    def should_wrap(self):
        '''
        True if this class is actually needed. If false, then the output
        stream will not be affected, nor will win32 calls be issued, so
        wrapping stdout is not actually required. This will generally be
        False on non-Windows platforms, unless optional functionality like
        autoreset has been requested using kwargs to init()
        '''
        return self.convert or self.strip or self.autoreset

    def get_win32_calls(self):
        if self.convert and winterm:
            return {
                AnsiStyle.RESET_ALL: (winterm.reset_all, ),
                AnsiStyle.BRIGHT: (winterm.style, WinStyle.BRIGHT),
                AnsiStyle.DIM: (winterm.style, WinStyle.NORMAL),
                AnsiStyle.NORMAL: (winterm.style, WinStyle.NORMAL),
                AnsiFore.BLACK: (winterm.fore, WinColor.BLACK),
                AnsiFore.RED: (winterm.fore, WinColor.RED),
                AnsiFore.GREEN: (winterm.fore, WinColor.GREEN),
                AnsiFore.YELLOW: (winterm.fore, WinColor.YELLOW),
                AnsiFore.BLUE: (winterm.fore, WinColor.BLUE),
                AnsiFore.MAGENTA: (winterm.fore, WinColor.MAGENTA),
                AnsiFore.CYAN: (winterm.fore, WinColor.CYAN),
                AnsiFore.WHITE: (winterm.fore, WinColor.GREY),
                AnsiFore.RESET: (winterm.fore, ),
                AnsiFore.LIGHTBLACK_EX: (winterm.fore, WinColor.BLACK, True),
                AnsiFore.LIGHTRED_EX: (winterm.fore, WinColor.RED, True),
                AnsiFore.LIGHTGREEN_EX: (winterm.fore, WinColor.GREEN, True),
                AnsiFore.LIGHTYELLOW_EX: (winterm.fore, WinColor.YELLOW, True),
                AnsiFore.LIGHTBLUE_EX: (winterm.fore, WinColor.BLUE, True),
                AnsiFore.LIGHTMAGENTA_EX: (winterm.fore, WinColor.MAGENTA, True),
                AnsiFore.LIGHTCYAN_EX: (winterm.fore, WinColor.CYAN, True),
                AnsiFore.LIGHTWHITE_EX: (winterm.fore, WinColor.GREY, True),
                AnsiBack.BLACK: (winterm.back, WinColor.BLACK),
                AnsiBack.RED: (winterm.back, WinColor.RED),
                AnsiBack.GREEN: (winterm.back, WinColor.GREEN),
                AnsiBack.YELLOW: (winterm.back, WinColor.YELLOW),
                AnsiBack.BLUE: (winterm.back, WinColor.BLUE),
                AnsiBack.MAGENTA: (winterm.back, WinColor.MAGENTA),
                AnsiBack.CYAN: (winterm.back, WinColor.CYAN),
                AnsiBack.WHITE: (winterm.back, WinColor.GREY),
                AnsiBack.RESET: (winterm.back, ),
                AnsiBack.LIGHTBLACK_EX: (winterm.back, WinColor.BLACK, True),
                AnsiBack.LIGHTRED_EX: (winterm.back, WinColor.RED, True),
                AnsiBack.LIGHTGREEN_EX: (winterm.back, WinColor.GREEN, True),
                AnsiBack.LIGHTYELLOW_EX: (winterm.back, WinColor.YELLOW, True),
                AnsiBack.LIGHTBLUE_EX: (winterm.back, WinColor.BLUE, True),
                AnsiBack.LIGHTMAGENTA_EX: (winterm.back, WinColor.MAGENTA, True),
                AnsiBack.LIGHTCYAN_EX: (winterm.back, WinColor.CYAN, True),
                AnsiBack.LIGHTWHITE_EX: (winterm.back, WinColor.GREY, True),
            }
        return dict()

    def write(self, text):
        if self.strip or self.convert:
            self.write_and_convert(text)
        else:
            self.wrapped.write(text)
            self.wrapped.flush()
        if self.autoreset:
            self.reset_all()


    def reset_all(self):
        if self.convert:
            self.call_win32('m', (0,))
        elif not self.strip and not is_stream_closed(self.wrapped):
            self.wrapped.write(Style.RESET_ALL)


    def write_and_convert(self, text):
        '''
        Write the given text to our wrapped stream, stripping any ANSI
        sequences from the text, and optionally converting them into win32
        calls.
        '''
        cursor = 0
        text = self.convert_osc(text)
        for match in self.ANSI_CSI_RE.finditer(text):
            start, end = match.span()
            self.write_plain_text(text, cursor, start)
            self.convert_ansi(*match.groups())
            cursor = end
        self.write_plain_text(text, cursor, len(text))


    def write_plain_text(self, text, start, end):
        if start < end:
            self.wrapped.write(text[start:end])
            self.wrapped.flush()


    def convert_ansi(self, paramstring, command):
        if self.convert:
            params = self.extract_params(command, paramstring)
            self.call_win32(command, params)


    def extract_params(self, command, paramstring):
        if command in 'Hf':
            params = tuple(int(p) if len(p) != 0 else 1 for p in paramstring.split(';'))
            while len(params) < 2:
                # defaults:
                params = params + (1,)
        else:
            params = tuple(int(p) for p in paramstring.split(';') if len(p) != 0)
            if len(params) == 0:
                # defaults:
                if command in 'JKm':
                    params = (0,)
                elif command in 'ABCD':
                    params = (1,)

        return params


    def call_win32(self, command, params):
        if command == 'm':
            for param in params:
                if param in self.win32_calls:
                    func_args = self.win32_calls[param]
                    func = func_args[0]
                    args = func_args[1:]
                    kwargs = dict(on_stderr=self.on_stderr)
                    func(*args, **kwargs)
        elif command in 'J':
            winterm.erase_screen(params[0], on_stderr=self.on_stderr)
        elif command in 'K':
            winterm.erase_line(params[0], on_stderr=self.on_stderr)
        elif command in 'Hf':     # cursor position - absolute
            winterm.set_cursor_position(params, on_stderr=self.on_stderr)
        elif command in 'ABCD':   # cursor position - relative
            n = params[0]
            # A - up, B - down, C - forward, D - back
            x, y = {'A': (0, -n), 'B': (0, n), 'C': (n, 0), 'D': (-n, 0)}[command]
            winterm.cursor_adjust(x, y, on_stderr=self.on_stderr)


    def convert_osc(self, text):
        for match in self.ANSI_OSC_RE.finditer(text):
            start, end = match.span()
            text = text[:start] + text[end:]
            paramstring, command = match.groups()
            if command in '\x07':       # \x07 = BEL
                params = paramstring.split(";")
                # 0 - change title and icon (we will only change title)
                # 1 - change icon (we don't support this)
                # 2 - change title
                if params[0] in '02':
                    winterm.set_title(params[1])
        return text


================================================
FILE: colorama/initialise.py
================================================
# Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file.
import atexit
import contextlib
import sys

from .ansitowin32 import AnsiToWin32


orig_stdout = None
orig_stderr = None

wrapped_stdout = None
wrapped_stderr = None

atexit_done = False


def reset_all():
    if AnsiToWin32 is not None:    # Issue #74: objects might become None at exit
        AnsiToWin32(orig_stdout).reset_all()


def init(autoreset=False, convert=None, strip=None, wrap=True):

    if not wrap and any([autoreset, convert, strip]):
        raise ValueError('wrap=False conflicts with any other arg=True')

    global wrapped_stdout, wrapped_stderr
    global orig_stdout, orig_stderr

    orig_stdout = sys.stdout
    orig_stderr = sys.stderr

    if sys.stdout is None:
        wrapped_stdout = None
    else:
        sys.stdout = wrapped_stdout = \
            wrap_stream(orig_stdout, convert, strip, autoreset, wrap)
    if sys.stderr is None:
        wrapped_stderr = None
    else:
        sys.stderr = wrapped_stderr = \
            wrap_stream(orig_stderr, convert, strip, autoreset, wrap)

    global atexit_done
    if not atexit_done:
        atexit.register(reset_all)
        atexit_done = True


def deinit():
    if orig_stdout is not None:
        sys.stdout = orig_stdout
    if orig_stderr is not None:
        sys.stderr = orig_stderr


@contextlib.contextmanager
def colorama_text(*args, **kwargs):
    init(*args, **kwargs)
    try:
        yield
    finally:
        deinit()


def reinit():
    if wrapped_stdout is not None:
        sys.stdout = wrapped_stdout
    if wrapped_stderr is not None:
        sys.stderr = wrapped_stderr


def wrap_stream(stream, convert, strip, autoreset, wrap):
    if wrap:
        wrapper = AnsiToWin32(stream,
            convert=convert, strip=strip, autoreset=autoreset)
        if wrapper.should_wrap():
            stream = wrapper.stream
    return stream




================================================
FILE: colorama/win32.py
================================================
# Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file.

# from winbase.h
STDOUT = -11
STDERR = -12

try:
    import ctypes
    from ctypes import LibraryLoader
    windll = LibraryLoader(ctypes.WinDLL)
    from ctypes import wintypes
except (AttributeError, ImportError):
    windll = None
    SetConsoleTextAttribute = lambda *_: None
    winapi_test = lambda *_: None
else:
    from ctypes import byref, Structure, c_char, POINTER

    COORD = wintypes._COORD

    class CONSOLE_SCREEN_BUFFER_INFO(Structure):
        """struct in wincon.h."""
        _fields_ = [
            ("dwSize", COORD),
            ("dwCursorPosition", COORD),
            ("wAttributes", wintypes.WORD),
            ("srWindow", wintypes.SMALL_RECT),
            ("dwMaximumWindowSize", COORD),
        ]
        def __str__(self):
            return '(%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d)' % (
                self.dwSize.Y, self.dwSize.X
                , self.dwCursorPosition.Y, self.dwCursorPosition.X
                , self.wAttributes
                , self.srWindow.Top, self.srWindow.Left, self.srWindow.Bottom, self.srWindow.Right
                , self.dwMaximumWindowSize.Y, self.dwMaximumWindowSize.X
            )

    _GetStdHandle = windll.kernel32.GetStdHandle
    _GetStdHandle.argtypes = [
        wintypes.DWORD,
    ]
    _GetStdHandle.restype = wintypes.HANDLE

    _GetConsoleScreenBufferInfo = windll.kernel32.GetConsoleScreenBufferInfo
    _GetConsoleScreenBufferInfo.argtypes = [
        wintypes.HANDLE,
        POINTER(CONSOLE_SCREEN_BUFFER_INFO),
    ]
    _GetConsoleScreenBufferInfo.restype = wintypes.BOOL

    _SetConsoleTextAttribute = windll.kernel32.SetConsoleTextAttribute
    _SetConsoleTextAttribute.argtypes = [
        wintypes.HANDLE,
        wintypes.WORD,
    ]
    _SetConsoleTextAttribute.restype = wintypes.BOOL

    _SetConsoleCursorPosition = windll.kernel32.SetConsoleCursorPosition
    _SetConsoleCursorPosition.argtypes = [
        wintypes.HANDLE,
        COORD,
    ]
    _SetConsoleCursorPosition.restype = wintypes.BOOL

    _FillConsoleOutputCharacterA = windll.kernel32.FillConsoleOutputCharacterA
    _FillConsoleOutputCharacterA.argtypes = [
        wintypes.HANDLE,
        c_char,
        wintypes.DWORD,
        COORD,
        POINTER(wintypes.DWORD),
    ]
    _FillConsoleOutputCharacterA.restype = wintypes.BOOL

    _FillConsoleOutputAttribute = windll.kernel32.FillConsoleOutputAttribute
    _FillConsoleOutputAttribute.argtypes = [
        wintypes.HANDLE,
        wintypes.WORD,
        wintypes.DWORD,
        COORD,
        POINTER(wintypes.DWORD),
    ]
    _FillConsoleOutputAttribute.restype = wintypes.BOOL

    _SetConsoleTitleW = windll.kernel32.SetConsoleTitleA
    _SetConsoleTitleW.argtypes = [
        wintypes.LPCSTR
    ]
    _SetConsoleTitleW.restype = wintypes.BOOL

    handles = {
        STDOUT: _GetStdHandle(STDOUT),
        STDERR: _GetStdHandle(STDERR),
    }

    def winapi_test():
        handle = handles[STDOUT]
        csbi = CONSOLE_SCREEN_BUFFER_INFO()
        success = _GetConsoleScreenBufferInfo(
            handle, byref(csbi))
        return bool(success)

    def GetConsoleScreenBufferInfo(stream_id=STDOUT):
        handle = handles[stream_id]
        csbi = CONSOLE_SCREEN_BUFFER_INFO()
        success = _GetConsoleScreenBufferInfo(
            handle, byref(csbi))
        return csbi

    def SetConsoleTextAttribute(stream_id, attrs):
        handle = handles[stream_id]
        return _SetConsoleTextAttribute(handle, attrs)

    def SetConsoleCursorPosition(stream_id, position, adjust=True):
        position = COORD(*position)
        # If the position is out of range, do nothing.
        if position.Y <= 0 or position.X <= 0:
            return
        # Adjust for Windows' SetConsoleCursorPosition:
        #    1. being 0-based, while ANSI is 1-based.
        #    2. expecting (x,y), while ANSI uses (y,x).
        adjusted_position = COORD(position.Y - 1, position.X - 1)
        if adjust:
            # Adjust for viewport's scroll position
            sr = GetConsoleScreenBufferInfo(STDOUT).srWindow
            adjusted_position.Y += sr.Top
            adjusted_position.X += sr.Left
        # Resume normal processing
        handle = handles[stream_id]
        return _SetConsoleCursorPosition(handle, adjusted_position)

    def FillConsoleOutputCharacter(stream_id, char, length, start):
        handle = handles[stream_id]
        char = c_char(char.encode())
        length = wintypes.DWORD(length)
        num_written = wintypes.DWORD(0)
        # Note that this is hard-coded for ANSI (vs wide) bytes.
        success = _FillConsoleOutputCharacterA(
            handle, char, length, start, byref(num_written))
        return num_written.value

    def FillConsoleOutputAttribute(stream_id, attr, length, start):
        ''' FillConsoleOutputAttribute( hConsole, csbi.wAttributes, dwConSize, coordScreen, &cCharsWritten )'''
        handle = handles[stream_id]
        attribute = wintypes.WORD(attr)
        length = wintypes.DWORD(length)
        num_written = wintypes.DWORD(0)
        # Note that this is hard-coded for ANSI (vs wide) bytes.
        return _FillConsoleOutputAttribute(
            handle, attribute, length, start, byref(num_written))

    def SetConsoleTitle(title):
        return _SetConsoleTitleW(title)


================================================
FILE: colorama/winterm.py
================================================
# Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file.
from . import win32


# from wincon.h
class WinColor(object):
    BLACK   = 0
    BLUE    = 1
    GREEN   = 2
    CYAN    = 3
    RED     = 4
    MAGENTA = 5
    YELLOW  = 6
    GREY    = 7

# from wincon.h
class WinStyle(object):
    NORMAL              = 0x00 # dim text, dim background
    BRIGHT              = 0x08 # bright text, dim background
    BRIGHT_BACKGROUND   = 0x80 # dim text, bright background

class WinTerm(object):

    def __init__(self):
        self._default = win32.GetConsoleScreenBufferInfo(win32.STDOUT).wAttributes
        self.set_attrs(self._default)
        self._default_fore = self._fore
        self._default_back = self._back
        self._default_style = self._style
        # In order to emulate LIGHT_EX in windows, we borrow the BRIGHT style.
        # So that LIGHT_EX colors and BRIGHT style do not clobber each other,
        # we track them separately, since LIGHT_EX is overwritten by Fore/Back
        # and BRIGHT is overwritten by Style codes.
        self._light = 0

    def get_attrs(self):
        return self._fore + self._back * 16 + (self._style | self._light)

    def set_attrs(self, value):
        self._fore = value & 7
        self._back = (value >> 4) & 7
        self._style = value & (WinStyle.BRIGHT | WinStyle.BRIGHT_BACKGROUND)

    def reset_all(self, on_stderr=None):
        self.set_attrs(self._default)
        self.set_console(attrs=self._default)

    def fore(self, fore=None, light=False, on_stderr=False):
        if fore is None:
            fore = self._default_fore
        self._fore = fore
        # Emulate LIGHT_EX with BRIGHT Style
        if light:
            self._light |= WinStyle.BRIGHT
        else:
            self._light &= ~WinStyle.BRIGHT
        self.set_console(on_stderr=on_stderr)

    def back(self, back=None, light=False, on_stderr=False):
        if back is None:
            back = self._default_back
        self._back = back
        # Emulate LIGHT_EX with BRIGHT_BACKGROUND Style
        if light:
            self._light |= WinStyle.BRIGHT_BACKGROUND
        else:
            self._light &= ~WinStyle.BRIGHT_BACKGROUND
        self.set_console(on_stderr=on_stderr)

    def style(self, style=None, on_stderr=False):
        if style is None:
            style = self._default_style
        self._style = style
        self.set_console(on_stderr=on_stderr)

    def set_console(self, attrs=None, on_stderr=False):
        if attrs is None:
            attrs = self.get_attrs()
        handle = win32.STDOUT
        if on_stderr:
            handle = win32.STDERR
        win32.SetConsoleTextAttribute(handle, attrs)

    def get_position(self, handle):
        position = win32.GetConsoleScreenBufferInfo(handle).dwCursorPosition
        # Because Windows coordinates are 0-based,
        # and win32.SetConsoleCursorPosition expects 1-based.
        position.X += 1
        position.Y += 1
        return position

    def set_cursor_position(self, position=None, on_stderr=False):
        if position is None:
            # I'm not currently tracking the position, so there is no default.
            # position = self.get_position()
            return
        handle = win32.STDOUT
        if on_stderr:
            handle = win32.STDERR
        win32.SetConsoleCursorPosition(handle, position)

    def cursor_adjust(self, x, y, on_stderr=False):
        handle = win32.STDOUT
        if on_stderr:
            handle = win32.STDERR
        position = self.get_position(handle)
        adjusted_position = (position.Y + y, position.X + x)
        win32.SetConsoleCursorPosition(handle, adjusted_position, adjust=False)

    def erase_screen(self, mode=0, on_stderr=False):
        # 0 should clear from the cursor to the end of the screen.
        # 1 should clear from the cursor to the beginning of the screen.
        # 2 should clear the entire screen, and move cursor to (1,1)
        handle = win32.STDOUT
        if on_stderr:
            handle = win32.STDERR
        csbi = win32.GetConsoleScreenBufferInfo(handle)
        # get the number of character cells in the current buffer
        cells_in_screen = csbi.dwSize.X * csbi.dwSize.Y
        # get number of character cells before current cursor position
        cells_before_cursor = csbi.dwSize.X * csbi.dwCursorPosition.Y + csbi.dwCursorPosition.X
        if mode == 0:
            from_coord = csbi.dwCursorPosition
            cells_to_erase = cells_in_screen - cells_before_cursor
        if mode == 1:
            from_coord = win32.COORD(0, 0)
            cells_to_erase = cells_before_cursor
        elif mode == 2:
            from_coord = win32.COORD(0, 0)
            cells_to_erase = cells_in_screen
        # fill the entire screen with blanks
        win32.FillConsoleOutputCharacter(handle, ' ', cells_to_erase, from_coord)
        # now set the buffer's attributes accordingly
        win32.FillConsoleOutputAttribute(handle, self.get_attrs(), cells_to_erase, from_coord)
        if mode == 2:
            # put the cursor where needed
            win32.SetConsoleCursorPosition(handle, (1, 1))

    def erase_line(self, mode=0, on_stderr=False):
        # 0 should clear from the cursor to the end of the line.
        # 1 should clear from the cursor to the beginning of the line.
        # 2 should clear the entire line.
        handle = win32.STDOUT
        if on_stderr:
            handle = win32.STDERR
        csbi = win32.GetConsoleScreenBufferInfo(handle)
        if mode == 0:
            from_coord = csbi.dwCursorPosition
            cells_to_erase = csbi.dwSize.X - csbi.dwCursorPosition.X
        if mode == 1:
            from_coord = win32.COORD(0, csbi.dwCursorPosition.Y)
            cells_to_erase = csbi.dwCursorPosition.X
        elif mode == 2:
            from_coord = win32.COORD(0, csbi.dwCursorPosition.Y)
            cells_to_erase = csbi.dwSize.X
        # fill the entire screen with blanks
        win32.FillConsoleOutputCharacter(handle, ' ', cells_to_erase, from_coord)
        # now set the buffer's attributes accordingly
        win32.FillConsoleOutputAttribute(handle, self.get_attrs(), cells_to_erase, from_coord)

    def set_title(self, title):
        win32.SetConsoleTitle(title)


================================================
FILE: credits.py
================================================
from animation_functions import debug_info
from CLIRender.classes import enable_ansi
from colorama import Fore, Style

import keyboard
import time
import os
import random
from just_playback import Playback

from animation_scenes import all_scenes, canvas
from string_defs import data_strings

import animator as am

enable_ansi()
# canvas.render_blank()
delay = 60.0 / 179.0 / 8.0
beat = -60
offset = 5.492
# print((delay * 980) + offset)
skip_by = 0

debug = False
last_frames = []


def skip_beats(ctr, amount, next_debug):
    global beat
    global skip_by

    ctr.cur_beat += amount
    beat += amount

    if debug:
        controller.events[next_debug] = (am.Event(next_debug, am.Event.layer_scene("debug_counter")),)

    skip_by = offset + (delay * amount)


counter = am.Scene(
    "debug_counter",
    (
        am.Generator(
            0, am.Generator.always(),
            am.Generator.no_create(),
            lambda g, b: debug_info(canvas, g, b, last_frames),
            am.Generator.no_request()
        ),
    )
)


ocean_events = (
    am.Event(60, lambda c: c.set_generator_data(
        "ocean_b", 1, "text", data_strings["ocean_b_0"]
    )),
    am.Event(310, lambda c: c.set_generator_data(
        "ocean_b", 1,
        "text", "[##CLEAR|60;6"
    )),
    am.Event(310, lambda c: c.set_generator_data(
        "ocean_b", 0, "ocean_glitch", 3
    )),
    am.Event(310, lambda c: c.set_generator_data(
        "ocean_b", 0, "ocean_col", Style.NORMAL + Fore.BLUE
    )),
    am.Event(312, lambda c: c.set_generator_data(
        "ocean_b", 0, "ocean_col", Style.BRIGHT + Fore.BLUE
    )),
    am.Event(314, lambda c: c.set_generator_data(
        "ocean_b", 0, "ocean_col", Style.NORMAL + Fore.BLUE
    )),
    am.Event(320, lambda c: c.set_generator_data(
        "ocean_b", 1, "text", data_strings["ocean_b_1"], "offset", 0
    )),
    am.Event(590, lambda c: c.set_generator_data(
        "ocean_b", 0, "ocean_col", Style.BRIGHT + Fore.BLACK
    )),
    am.Event(646, lambda c: c.set_generator_data(
        "ocean_b", 1,
        "text", "[##CLEAR|60;8"
    )),
    am.Event(652, lambda c: c.set_generator_data(
        "ocean_b", 1,
        "text", data_strings["ocean_b_2"], "offset", 0
    )),
    am.Event(656, lambda c: c.set_generator_data(
        "ocean_b", 0, "ocean_col", Style.NORMAL + Fore.YELLOW
    )),
    am.Event(666, lambda c: c.set_generator_data(
        "ocean_b", 0, "ocean_col", Style.BRIGHT + Fore.YELLOW
    )),
    am.Event(666, lambda c: c.set_generator_data(
        "ocean_b", 0, "ocean_glitch", 6
    )),
    am.Event(850, lambda c: c.set_generator_data(
        "ocean_b", 0, "ocean_col", Style.NORMAL + Fore.YELLOW
    )),
    am.Event(999, lambda c: c.set_generator_data(
        "ocean_b", 1,
        "text", "[##CLEAR|60;10"
    )),
    am.Event(1000, lambda c: c.set_generator_data(
        "ocean_b", 0, "ocean_col", Style.BRIGHT + Fore.BLACK
    )),
    am.Event(1000, lambda c: c.set_generator_data(
        "ocean_b", 0, "ocean_glitch", 13
    )),
    am.Event(1000, lambda c: c.set_generator_data(
        "ocean_b", 1,
        "text", data_strings["ocean_b_3"],
        "offset", 0
    )),
    am.Event(1044, lambda c: c.set_generator_data(
        "ocean_b", 0, "ocean_glitch", 102
    )),
    am.Event(1048, lambda c: c.set_generator_data(
        "ocean_b", 0, "ocean_glitch", 230
    )),
    am.Event(1052, lambda c: c.set_generator_data(
        "ocean_b", 0, "ocean_glitch", 500
    )),
    am.Event(1056, lambda c: c.set_generator_data(
        "ocean_b", 0, "ocean_glitch", 760
    )),
    am.Event(1060, lambda c: c.set_generator_data(
        "ocean_b", 0, "ocean_glitch", 1600
    )),
    am.Event(1064, lambda c: c.set_generator_data(
        "ocean_b", 0, "ocean_glitch", 2500
    ))
)


ocean2_events = (
    am.Event(3896, lambda c: c.set_generator_data(
            "ocean_c", 1, "text", data_strings["ocean_c_0"]
        )
    ),
    am.Event(3896, lambda c: c.set_generator_data(
            "ocean_c", 2, "text", data_strings["ocean_c_1"]
        )
    ),

    am.Event(3896, lambda c: c.set_generator_data(
            "ocean_c", 3, "text", data_strings["ocean_c_2"]
        )
    ),

    am.Event(3896, lambda c: c.set_generator_data(
            "ocean_c", 4, "text", data_strings["ocean_c_3"]
        )
    ),

    am.Event(4400, lambda c: c.set_generator_data(
        "ocean_c", 1,
        "text", "[##CLEAR|60;10"
    )),

    *(
        am.Event(4401 + i * 2, lambda c: c.set_generator_data(
            "ocean_c", 0, "ocean_col", random.choice((Style.BRIGHT, Style.NORMAL)) + Fore.BLACK
        )) for i in range(5)
    ),

    am.Event(4411, lambda c: c.set_generator_data(
        "ocean_c", 0, "ocean_col", Style.NORMAL + Fore.BLACK
    )),
)


controller = am.SceneManager((*all_scenes, counter), (
    am.Event(0, am.Event.swap_scene("wipe")),
    # am.Event(1, am.Event.layer_scene("debug_counter")),
    am.Event(58, am.Event.swap_scene("clear")),
    am.Event(60, am.Event.swap_scene("ocean_b")),
    # am.Event(60, am.Event.layer_scene("typewrite")),
    *ocean_events,
    am.Event(1079, am.Event.swap_scene("clear")),
    am.Event(1080, am.Event.swap_scene("beats")),
    am.Event(1080, am.Event.layer_scene("title")),
    am.Event(1080, am.Event.swap_scene("beats")),
    am.Event(1336, am.Event.swap_scene("beats_lr")),
    am.Event(1844, am.Event.remove_scene("title")),
    am.Event(1848, am.Event.swap_scene("funding")),
    am.Event(1848, am.Event.layer_scene("dates")),
    am.Event(1848, am.Event.layer_scene("weather")),
    am.Event(1848, lambda c: c.set_data(
        "history", [], "refresh", True
    )),

    am.Event(1848, lambda c: c.set_generator_data(
        "funding", 0, "text", data_strings["funding_0"]
    )),

    am.Event(2348, lambda c: c.set_generator_data(
        "funding", 0, "text", (('',), ('',))
    )),

    am.Event(2348, lambda c: c.set_data(
        "history", [], "refresh", True
    )),

    am.Event(2352, lambda c: c.set_generator_data(
        "funding", 0, "text", data_strings["funding_1"], "offset", 0, "lineno", 0
    )),

    am.Event(2976, am.Event.remove_scene("dates")),
    am.Event(2976, am.Event.remove_scene("weather")),

    am.Event(2976, am.Event.swap_scene("clear_wipe")),
    am.Event(3007, am.Event.swap_scene("clear")),

    # idk what to do here. some sort of bootup sequence?
    am.Event(3132, am.Event.swap_scene("loadingbar")),
    am.Event(3132, lambda c: c.set_generator_data(
        "loadingbar", 6, "text", data_strings["funding_2"]
    )),

    am.Event(3376, am.Event.swap_scene("ocean_d")),
    am.Event(3380, am.Event.swap_scene("clear")),
    am.Event(3380, am.Event.swap_scene("fastload")),
    am.Event(3388, am.Event.swap_scene("clear")),

    # Crazy part. Go wild
    am.Event(3390, am.Event.swap_scene("error")),
    am.Event(3390, am.Event.layer_scene("fundingx2")),
    am.Event(3390, lambda c: c.set_data(
        "history", [], "refresh", True
    )),

    am.Event(3390, lambda c: c.set_generator_data(
        "fundingx2", 0,
        "text", data_strings["fundingx2_0"]
    )),

    am.Event(3390, lambda c: c.set_generator_data(
        "fundingx2", 1,
        "text", data_strings["fundingx2_1"]
    )),

    am.Event(3390, lambda c: c.set_generator_data(
        "fundingx2", 3,
        "text", data_strings["fundingx2_2"]
    )),

    am.Event(3895, am.Event.remove_scene("fundingx2")),
    am.Event(3895, am.Event.swap_scene("clear")),
    am.Event(3896, am.Event.swap_scene("ocean_c")),
    *ocean2_events,

    # remember to clean the ocean_c events up, or it will stay in the screen. At frame 4413.
    am.Event(4413, am.Event.swap_scene("clear")),
    am.Event(4460, am.Event.swap_scene("accesspoints")),
    am.Event(4460, am.Event.layer_scene("fdg_single")),
    am.Event(4534, lambda c: c.set_generator_data(
        "fdg_single", 1, "text", data_strings["fdg_single_0"]
    )),

    am.Event(5500, am.Event.remove_scene("fdg_single")),
    am.Event(5500, am.Event.swap_scene("clear")),
    am.Event(5500, am.Event.swap_scene("beats")),
    am.Event(5500, am.Event.layer_scene("fdg_down")),
    am.Event(5788, lambda c: c.set_generator_data(
        "fdg_down", 0, "text", data_strings["fdg_down_0"]
    )),

    am.Event(5916, lambda c: c.set_generator_data(
        "fdg_down", 0,
        "text", data_strings["fdg_down_1"], "lineno", 0, "offset", 0
    )),

    am.Event(5916, lambda c: c.set_generator_data(
        "fdg_down", 1,
        "text", data_strings["fdg_down_2"]
    )),

    am.Event(6270, am.Event.swap_scene("beats_lr")),

    am.Event(6508, am.Event.remove_scene("fdg_down")),
    # am.Event(6508, am.Event.remove_scene("debug_counter")),

    am.Event(6508, am.Event.swap_scene("clear"))
))


# class MixerWrapper:
#     def __init__(self):
#         self.is_paused = False
#
#     def toggle_music(self):
#         if self.is_paused:
#             pygame.mixer.music.unpause()
#             self.is_paused = False
#         else:
#             pygame.mixer.music.pause()
#             self.is_paused = True
#
#
# ffwing = MixerWrapper()

paused_this_frame = False
ff_this_frame = False

filename = "media/credits.wav"

playback = Playback()
playback.load_file(filename)

# Clear the console before showing the skip menu
if os.name == "nt":
    os.system("cls")
elif os.name == "posix":
    os.system("clear")
else:
    print("\033[2J")

print("\033[1;1Hskips\n\n1 | start\n2 | title\n3 | funding\n4 | loading\n5 | break\n6 | final")

# Skips forward to the title scene
time_menu = time.time()
while time.time() - 2 < time_menu:
    if keyboard.is_pressed("1"):
        time_menu = 99999999999999999999
        break
    elif keyboard.is_pressed("2"):
        skip_beats(controller, 1000, 1081)
        time_menu = 99999999999999999999
        break
    elif keyboard.is_pressed("3"):
        skip_beats(controller, 1770, 1851)
        controller.events[1849] = am.Event(1849, am.Event.layer_scene("redraw_ui")),
        controller.events[1860] = am.Event(1860, am.Event.remove_scene("redraw_ui")),
        time_menu = 99999999999999999999
        break
    elif keyboard.is_pressed("4"):
        skip_beats(controller, 3040, 3133)
        time_menu = 99999999999999999999
        break
    elif keyboard.is_pressed("5"):
        skip_beats(controller, 3780, 3898)
        time_menu = 99999999999999999999
        break
    elif keyboard.is_pressed("6"):
        skip_beats(controller, 5420, 5501)
        time_menu = 99999999999999999999
        break

    time.sleep(0.01)

if os.name == "nt":
    os.system("cls")
elif os.name == "posix":
    os.system("clear")
else:
    print("\033[2J")

# wave_obj = sa.WaveObject.from_wave_file(filename)
# play_obj = wave_obj.play()
playback.play()
playback.seek(skip_by)

time_start = time.time()
last_update = time.time()
prev_pos = 0

while playback.active:
    # (17.06.21) might have broken, i used a -1 beat offset here to try and sync up everything better
    # since i originally used 1-indexed beats
    #
    # (24.06.21) update chat it didnt break

    # next_beat = (time.time() - time_start - offset) > ((beat - 1) * delay)
    next_beat = (playback.curr_pos - offset) > ((beat - 1) * delay)
    need_update = time.time() - (1/30) > last_update
    # print(pygame.mixer.music.get_pos())

    if next_beat:
        controller.request_next()
        canvas.render_all()
        last_frames.append(time.time())
        if len(last_frames) > 10:
            last_frames.pop(0)

        beat += 1

    if need_update:
        if keyboard.is_pressed("p"):
            if not paused_this_frame:
                if playback.paused:
                    playback.resume()
                else:
                    playback.pause()

                paused_this_frame = True
        else:
            paused_this_frame = False

        if keyboard.is_pressed(","):
            if not ff_this_frame:
                ff_this_frame = True
            else:
                playback.seek(playback.curr_pos + delay * 3)

        if keyboard.is_pressed("."):
            if not ff_this_frame:
                ff_this_frame = True
            else:
                playback.seek(playback.curr_pos + delay * 7)

        if keyboard.is_pressed("/"):
            if not ff_this_frame:
                ff_this_frame = True
            else:
                playback.seek(playback.curr_pos + delay * 15)
        # else:
        #     # print("n", ff_this_frame, pygame.mixer.music.get_pos() + prev_pos, prev_pos)
        #     if ff_this_frame:
        #         # print("unpausing now")
        #         ff_this_frame = False
        #         ffwing.toggle_music()

        last_update = time.time()

# Add a final clear at the end to prevent the last frame from sticking around when the program ends.
if os.name == "nt":
    os.system("cls")
elif os.name == "posix":
    os.system("clear")
else:
    print("\033[2J")

================================================
FILE: credits_pynput.py
================================================
from animation_functions import debug_info
from CLIRender.classes import enable_ansi
from colorama import Fore, Style

from pynput import keyboard
import time
import os
import random
from just_playback import Playback

from animation_scenes import all_scenes, canvas
from string_defs import data_strings

import animator as am

enable_ansi()
# canvas.render_blank()
delay = 60.0 / 179.0 / 8.0
beat = -60
offset = 5.492
# print((delay * 980) + offset)
skip_by = 0

debug = False
last_frames = []


def skip_beats(ctr, amount, next_debug):
    global beat
    global skip_by

    ctr.cur_beat += amount
    beat += amount

    if debug:
        controller.events[next_debug] = (am.Event(next_debug, am.Event.layer_scene("debug_counter")),)

    skip_by = offset + (delay * amount)


counter = am.Scene(
    "debug_counter",
    (
        am.Generator(
            0, am.Generator.always(),
            am.Generator.no_create(),
            lambda g, b: debug_info(canvas, g, b, last_frames),
            am.Generator.no_request()
        ),
    )
)


ocean_events = (
    am.Event(60, lambda c: c.set_generator_data(
        "ocean_b", 1, "text", data_strings["ocean_b_0"]
    )),
    am.Event(310, lambda c: c.set_generator_data(
        "ocean_b", 1,
        "text", "[##CLEAR|60;6"
    )),
    am.Event(310, lambda c: c.set_generator_data(
        "ocean_b", 0, "ocean_glitch", 3
    )),
    am.Event(310, lambda c: c.set_generator_data(
        "ocean_b", 0, "ocean_col", Style.NORMAL + Fore.BLUE
    )),
    am.Event(312, lambda c: c.set_generator_data(
        "ocean_b", 0, "ocean_col", Style.BRIGHT + Fore.BLUE
    )),
    am.Event(314, lambda c: c.set_generator_data(
        "ocean_b", 0, "ocean_col", Style.NORMAL + Fore.BLUE
    )),
    am.Event(320, lambda c: c.set_generator_data(
        "ocean_b", 1, "text", data_strings["ocean_b_1"], "offset", 0
    )),
    am.Event(590, lambda c: c.set_generator_data(
        "ocean_b", 0, "ocean_col", Style.BRIGHT + Fore.BLACK
    )),
    am.Event(646, lambda c: c.set_generator_data(
        "ocean_b", 1,
        "text", "[##CLEAR|60;8"
    )),
    am.Event(652, lambda c: c.set_generator_data(
        "ocean_b", 1,
        "text", data_strings["ocean_b_2"], "offset", 0
    )),
    am.Event(656, lambda c: c.set_generator_data(
        "ocean_b", 0, "ocean_col", Style.NORMAL + Fore.YELLOW
    )),
    am.Event(666, lambda c: c.set_generator_data(
        "ocean_b", 0, "ocean_col", Style.BRIGHT + Fore.YELLOW
    )),
    am.Event(666, lambda c: c.set_generator_data(
        "ocean_b", 0, "ocean_glitch", 6
    )),
    am.Event(850, lambda c: c.set_generator_data(
        "ocean_b", 0, "ocean_col", Style.NORMAL + Fore.YELLOW
    )),
    am.Event(999, lambda c: c.set_generator_data(
        "ocean_b", 1,
        "text", "[##CLEAR|60;10"
    )),
    am.Event(1000, lambda c: c.set_generator_data(
        "ocean_b", 0, "ocean_col", Style.BRIGHT + Fore.BLACK
    )),
    am.Event(1000, lambda c: c.set_generator_data(
        "ocean_b", 0, "ocean_glitch", 13
    )),
    am.Event(1000, lambda c: c.set_generator_data(
        "ocean_b", 1,
        "text", data_strings["ocean_b_3"],
        "offset", 0
    )),
    am.Event(1044, lambda c: c.set_generator_data(
        "ocean_b", 0, "ocean_glitch", 102
    )),
    am.Event(1048, lambda c: c.set_generator_data(
        "ocean_b", 0, "ocean_glitch", 230
    )),
    am.Event(1052, lambda c: c.set_generator_data(
        "ocean_b", 0, "ocean_glitch", 500
    )),
    am.Event(1056, lambda c: c.set_generator_data(
        "ocean_b", 0, "ocean_glitch", 760
    )),
    am.Event(1060, lambda c: c.set_generator_data(
        "ocean_b", 0, "ocean_glitch", 1600
    )),
    am.Event(1064, lambda c: c.set_generator_data(
        "ocean_b", 0, "ocean_glitch", 2500
    ))
)


ocean2_events = (
    am.Event(3896, lambda c: c.set_generator_data(
            "ocean_c", 1, "text", data_strings["ocean_c_0"]
        )
    ),
    am.Event(3896, lambda c: c.set_generator_data(
            "ocean_c", 2, "text", data_strings["ocean_c_1"]
        )
    ),

    am.Event(3896, lambda c: c.set_generator_data(
            "ocean_c", 3, "text", data_strings["ocean_c_2"]
        )
    ),

    am.Event(3896, lambda c: c.set_generator_data(
            "ocean_c", 4, "text", data_strings["ocean_c_3"]
        )
    ),

    am.Event(4400, lambda c: c.set_generator_data(
        "ocean_c", 1,
        "text", "[##CLEAR|60;10"
    )),

    *(
        am.Event(4401 + i * 2, lambda c: c.set_generator_data(
            "ocean_c", 0, "ocean_col", random.choice((Style.BRIGHT, Style.NORMAL)) + Fore.BLACK
        )) for i in range(5)
    ),

    am.Event(4411, lambda c: c.set_generator_data(
        "ocean_c", 0, "ocean_col", Style.NORMAL + Fore.BLACK
    )),
)


controller = am.SceneManager((*all_scenes, counter), (
    am.Event(0, am.Event.swap_scene("wipe")),
    # am.Event(1, am.Event.layer_scene("debug_counter")),
    am.Event(58, am.Event.swap_scene("clear")),
    am.Event(60, am.Event.swap_scene("ocean_b")),
    # am.Event(60, am.Event.layer_scene("typewrite")),
    *ocean_events,
    am.Event(1079, am.Event.swap_scene("clear")),
    am.Event(1080, am.Event.swap_scene("beats")),
    am.Event(1080, am.Event.layer_scene("title")),
    am.Event(1080, am.Event.swap_scene("beats")),
    am.Event(1336, am.Event.swap_scene("beats_lr")),
    am.Event(1844, am.Event.remove_scene("title")),
    am.Event(1848, am.Event.swap_scene("funding")),
    am.Event(1848, am.Event.layer_scene("dates")),
    am.Event(1848, am.Event.layer_scene("weather")),
    am.Event(1848, lambda c: c.set_data(
        "history", [], "refresh", True
    )),

    am.Event(1848, lambda c: c.set_generator_data(
        "funding", 0, "text", data_strings["funding_0"]
    )),

    am.Event(2348, lambda c: c.set_generator_data(
        "funding", 0, "text", (('',), ('',))
    )),

    am.Event(2348, lambda c: c.set_data(
        "history", [], "refresh", True
    )),

    am.Event(2352, lambda c: c.set_generator_data(
        "funding", 0, "text", data_strings["funding_1"], "offset", 0, "lineno", 0
    )),

    am.Event(2976, am.Event.remove_scene("dates")),
    am.Event(2976, am.Event.remove_scene("weather")),

    am.Event(2976, am.Event.swap_scene("clear_wipe")),
    am.Event(3007, am.Event.swap_scene("clear")),

    # idk what to do here. some sort of bootup sequence?
    am.Event(3132, am.Event.swap_scene("loadingbar")),
    am.Event(3132, lambda c: c.set_generator_data(
        "loadingbar", 6, "text", data_strings["funding_2"]
    )),

    am.Event(3376, am.Event.swap_scene("ocean_d")),
    am.Event(3380, am.Event.swap_scene("clear")),
    am.Event(3380, am.Event.swap_scene("fastload")),
    am.Event(3388, am.Event.swap_scene("clear")),

    # Crazy part. Go wild
    am.Event(3390, am.Event.swap_scene("error")),
    am.Event(3390, am.Event.layer_scene("fundingx2")),
    am.Event(3390, lambda c: c.set_data(
        "history", [], "refresh", True
    )),

    am.Event(3390, lambda c: c.set_generator_data(
        "fundingx2", 0,
        "text", data_strings["fundingx2_0"]
    )),

    am.Event(3390, lambda c: c.set_generator_data(
        "fundingx2", 1,
        "text", data_strings["fundingx2_1"]
    )),

    am.Event(3390, lambda c: c.set_generator_data(
        "fundingx2", 3,
        "text", data_strings["fundingx2_2"]
    )),

    am.Event(3895, am.Event.remove_scene("fundingx2")),
    am.Event(3895, am.Event.swap_scene("clear")),
    am.Event(3896, am.Event.swap_scene("ocean_c")),
    *ocean2_events,
    
    # remember to clean the ocean_c events up, or it will stay in the screen. At frame 4413.
    am.Event(4413, am.Event.swap_scene("clear")),
    am.Event(4460, am.Event.swap_scene("accesspoints")),
    am.Event(4460, am.Event.layer_scene("fdg_single")),
    am.Event(4534, lambda c: c.set_generator_data(
        "fdg_single", 1, "text", data_strings["fdg_single_0"]
    )),

    am.Event(5500, am.Event.remove_scene("fdg_single")),
    am.Event(5500, am.Event.swap_scene("clear")),
    am.Event(5500, am.Event.swap_scene("beats")),
    am.Event(5500, am.Event.layer_scene("fdg_down")),
    am.Event(5788, lambda c: c.set_generator_data(
        "fdg_down", 0, "text", data_strings["fdg_down_0"]
    )),

    am.Event(5916, lambda c: c.set_generator_data(
        "fdg_down", 0,
        "text", data_strings["fdg_down_1"], "lineno", 0, "offset", 0
    )),

    am.Event(5916, lambda c: c.set_generator_data(
        "fdg_down", 1,
        "text", data_strings["fdg_down_2"]
    )),

    am.Event(6270, am.Event.swap_scene("beats_lr")),

    am.Event(6508, am.Event.remove_scene("fdg_down")),
    # am.Event(6508, am.Event.remove_scene("debug_counter")),

    am.Event(6508, am.Event.swap_scene("clear"))
))


# class MixerWrapper:
#     def __init__(self):
#         self.is_paused = False
#
#     def toggle_music(self):
#         if self.is_paused:
#             pygame.mixer.music.unpause()
#             self.is_paused = False
#         else:
#             pygame.mixer.music.pause()
#             self.is_paused = True
#
#
# ffwing = MixerWrapper()

paused_this_frame = False
ff_this_frame = False

filename = "media/credits.wav"

playback = Playback()
playback.load_file(filename)

# Clear the console before showing the skip menu
if os.name == "nt":
    os.system("cls")
elif os.name == "posix":
    os.system("clear")
else:
    print("\033[2J")

print("\033[1;1Hskips\n\n1 | start\n2 | title\n3 | funding\n4 | loading\n5 | break\n6 | final")

# Skips forward to the title scene
key_states = {
    '1': False,
    '2': False,
    '3': False,
    '4': False,
    '5': False,
    '6': False,
    'p': False,
    ',': False,
    '.': False,
    '/': False
}

def on_press(key):
    try:
        if key.char in key_states:
            key_states[key.char] = True
    except AttributeError:
        pass

def on_release(key):
    try:
        if key.char in key_states:
            key_states[key.char] = False
    except AttributeError:
        pass

listener = keyboard.Listener(on_press=on_press, on_release=on_release)
listener.start()

time_menu = time.time()
while time.time() - 2 < time_menu:
    if key_states['1']:
        time_menu = 99999999999999999999
        break
    elif key_states['2']:
        skip_beats(controller, 1000, 1081)
        time_menu = 99999999999999999999
        break
    elif key_states['3']:
        skip_beats(controller, 1770, 1851)
        controller.events[1849] = am.Event(1849, am.Event.layer_scene("redraw_ui")),
        controller.events[1860] = am.Event(1860, am.Event.remove_scene("redraw_ui")),
        time_menu = 99999999999999999999
        break
    elif key_states['4']:
        skip_beats(controller, 3040, 3133)
        time_menu = 99999999999999999999
        break
    elif key_states['5']:
        skip_beats(controller, 3780, 3898)
        time_menu = 99999999999999999999
        break
    elif key_states['6']:
        skip_beats(controller, 5420, 5501)
        time_menu = 99999999999999999999
        break

    time.sleep(0.01)


if os.name == "nt":
    os.system("cls")
elif os.name == "posix":
    os.system("clear")
else:
    print("\033[2J")

# wave_obj = sa.WaveObject.from_wave_file(filename)
# play_obj = wave_obj.play()
playback.play()
playback.seek(skip_by)

time_start = time.time()
last_update = time.time()
prev_pos = 0

while playback.active:
    # (17.06.21) might have broken, i used a -1 beat offset here to try and sync up everything better
    # since i originally used 1-indexed beats
    #
    # (24.06.21) update chat it didnt break

    # next_beat = (time.time() - time_start - offset) > ((beat - 1) * delay)
    next_beat = (playback.curr_pos - offset) > ((beat - 1) * delay)
    need_update = time.time() - (1/30) > last_update
    # print(pygame.mixer.music.get_pos())

    if next_beat:
        controller.request_next()
        canvas.render_all()
        last_frames.append(time.time())
        if len(last_frames) > 10:
            last_frames.pop(0)

        beat += 1

    if need_update:
        if key_states['p']:
            if not paused_this_frame:
                if playback.paused:
                    playback.resume()
                else:
                    playback.pause()
    
                paused_this_frame = True
        else:
            paused_this_frame = False
    
        if key_states[',']:
            if not ff_this_frame:
                ff_this_frame = True
            else:
                playback.seek(playback.curr_pos + delay * 3)
    
        if key_states['.']:
            if not ff_this_frame:
                ff_this_frame = True
            else:
                playback.seek(playback.curr_pos + delay * 7)
    
        if key_states['/']:
            if not ff_this_frame:
                ff_this_frame = True
            else:
                playback.seek(playback.curr_pos + delay * 15)
        # else:
        #     # print("n", ff_this_frame, pygame.mixer.music.get_pos() + prev_pos, prev_pos)
        #     if ff_this_frame:
        #         # print("unpausing now")
        #         ff_this_frame = False
        #         ffwing.toggle_music()

        last_update = time.time()

# Add a final clear at the end to prevent the last frame from sticking around when the program ends.
if os.name == "nt":
    os.system("cls")
elif os.name == "posix":
    os.system("clear")
else:
    print("\033[2J")

================================================
FILE: media/README.txt
================================================
I can't really distribute a full .wav file of an entire song that I don't own the rights to.
That would be disrespectful.

If you want to run this on your own device, you'll need to find yourself a copy of
Frums 「Credits LONG」

for yourself.
Sorry.

-----------
If you do find a copy, convert it to "credits.wav" and drop it in this /media folder.

================================================
FILE: media/credits.txt
================================================
Now for the official national weather service forecast for Eastern Massachusets inside of I-495,
including Boston, issued at 7:21 PM, Thursday, October 22nd.
Tonight: Mostly cloudy with isolated showers until midight,
then mostly clear after midnight.
Lows in the lower 40s.
West winds 10-15 mph with gusts up to 25 mph.
Chance of rain: 20 percent.

Friday: Sunny. Lush colour with highs in the lower 50s.
Northwest winds 10-15mph with gusts up to 25mph.
Friday night, mostly clear.
Lows in the mid-30s.
North winds 10-15mph.
Saturday: Partly sunny.
High-igh-igh-igh-igh-igh-igh-igh-igh-igh-igh-igh-igh-igh-igh-igh-igh-igh-igh...


22.10.2009: Cloudy, precip 20%, temp 43, wind dir 6, 13 (25)
23.10.2009: Sunny, precip 4%, temp 52, wind dir 7, 12 (25)
24.10.2009: Partly sunny, precip 7%, temp 48, wind dir 1, 8 (20)

25.10.2009 and beyond: randomly mutate value.
Prefer 33% precip, move wind dir by +-3 each day, gust = speed x 2.1 - 2.6


Funding for this program was made possible by by by by by
Fun-by-by-by-by-ding by-by-by-by-for thiiiii
Program. Program. Pro-pro-pro-pro-pro-gram.
Funding for-by-by-made possible by viewers like you.
like you. like you. like you. like you. like you. like you
...

...
Annual financial suppor-
Annual financial suppor-
Annual financial suppor-
Annual financial suppor-
Annual financial suppor-
Annual financial suppor-
Annual financial suppor-
Annual financial suppor-
Annu- Annu- Th-th-th-thank you.
By-ci-po-po-cor-cor-by-by-rrrrrrroooooaaaaaa-
-wers-ble-b-b-F-f-fi-i-naaaaaaaa-
Fun-funding, fu-fun-fu-fun-funding
Fu-fu-fun-funding, fu-fun-fu-funding
F-i-n-a-n--po-po-cor-by-por-portio-portio-portion-nnn
B-b-b-by b-by b-by b-by
>>>>>
By-by-finan-b-b-nc-nc-
Corrrrrr
Viewers like you. like you. like you.
Funding-Funding-Fun-fu-funding
Fu-fu-fu-fu-Fu-fu-fu-fu-
Corporationnnn
The corportation for public broadcasting and bi-annual fiii-
of-nan-for-financial su-for financial asssss-
in-viewers-you-
This is PBS.
...

...
Here are the 7 PM observations for the Boston metropolitan area.
At Logan Airport, it was cloudy.
The temperature as 68 degrees.
The dew point, 47 -
and the relative humidity, 46 percent.
The wind was southwest at 13 miles an hour.
The pressure was 29.99 inches and rising.
Elsewherrrrrrrrrrrrr
...

...
Funding for this program was made possible by viewers...

================================================
FILE: ocean.py
================================================
# adapted from the-sea.js by plaao (me)

# The ocean is stored as a list of strings which is combined into one string when rendered.
# It is rendered internally in 40x8 resolution.
import math
import random

ocean_time = math.floor(random.random() * 2000)
alphabet = "abcdefghijklmnopqrstuvwxyz"


def init_populate_ocean():
    global ocean_time

    # Create 12 lists of 80 chars each and get an ocean slice for every ocean time value needed.
    ocean_base = get_ocean_slices(ocean_time, ocean_time + 80)

    ocean_time += 80
    return ocean_base


def get_ocean_slice(xr, glitch):
    x = xr / 5
    c = math.cos(0.2 * x) + math.sin(0.3 * x) * math.sin(0.23 * x)
    y = -math.floor(2 * c * math.sin(x)) + 3

    # Returns a list of chars which can then be populated into the ocean.
    cont_list = []
    for i in range(10):
        if random.random() <= 0.002 * glitch:
            cont_list.append(alphabet[math.floor(random.random() * len(alphabet))])

        else:
            if i == y:
                cont_list.append("#")
            elif i > y:
                cont_list.append(".")
            else:
                cont_list.append(" ")

    return cont_list


def mutate_text(txt, glitch):
    for i in range(len(txt)):
        ch = txt[i]
        if ch in "#. " and random.random() <= (0.0002 + ((ocean_time % 1200) / 1200000)) * glitch:
            txt = txt[:i] + alphabet[math.floor(random.random() * len(alphabet))] + txt[i + 1:]

    return txt


def get_ocean_slices(x1, x2):
    cont_list = [
        "",
        "",
        "",
        "",
        "",
        "",
        "",
        "",
        "",
        ""
    ]

    for xr in range(x1, x2):
        x = xr / 5
        c = math.cos(0.2 * x) + math.sin(0.3 * x) * math.sin(0.23 * x)
        y = -math.floor(2 * c * math.sin(x)) + 3

        # Returns a list of chars which can then be populated into the ocean.
        for i in range(10):
            if random.random() <= 0.002:
                cont_list[i] += alphabet[math.floor(random.random() * len(alphabet))]

            else:
                if i == y:
                    cont_list[i] += "#"
                elif i > y:
                    cont_list[i] += "."
                else:
                    cont_list[i] += " "

    return cont_list


def update_ocean_slices(ocean_content, ocean_glitch):
    global ocean_time
    
    # edits in place
    # get the new slice
    ocean_slice = get_ocean_slice(ocean_time, ocean_glitch)
    ocean_time += 1
    
    # for every line in content, remove the first char and add the slice char
    for i in range(10):
        ocean_content[i] = ocean_content[i][1:] + ocean_slice[i]

    raw_txt = unpack_content_to_text(ocean_content)
    
    return mutate_text(raw_txt, ocean_glitch)


def unpack_content_to_text(content):
    # Join every line with \n
    return "".join(line for line in content)


def begin_ocean():
    return init_populate_ocean()


================================================
FILE: requirements.txt
================================================
just_playback
pynput; sys_platform == "darwin"
keyboard; sys_platform != "darwin"


================================================
FILE: string_defs.py
================================================
# Contains strings used inside credits.py.
# Kept inside this file to reduce clutter.
# Implemented as a dictionary of in-memory strings.
from animation_functions import split_word_template, fuck_up_text, work_out_date

data_strings = {}

data_strings["ocean_b_0"] = (
    "Now for the official national~ weather~ service~ forecast\n"
    "~~~~for~~ Eastern Massachusetts~ inside of~~ I-~~~~4~~~~9~~~~~~~5~~,\n"
    "~~~~~~~~    including Boston,\n\n"
    "~~~~~~~~issued at 7~~~~:2~~1~~~~ PM~~~~, ~~~~~~~~~~~~~~Thursday, October~~ 2~~2~~nd."
)

data_strings["ocean_b_1"] = (
    "Tonight:\n\n~~~~~Mostly cloudy with isolated~ showers~ until~~ mid~~~~night,\n"
    "~~~~~~~~~then mostly clear~~ after~~ mid~night.\n"
    "~~~~~~~~~Lows in the lower 4~~~~0~~~~s.\n"
    "~~~~~~~~~West winds 10~ to~~ 1~~5~~ miles~~ an~~ hour\n"
    "~~~~~with~ gusts~~ up~ to~ 2~~~~5~~~~ miles~~ an~~ hour~~.\n"
    "~~~~~~~~~~Chance of rain:~~~~ 2~~0~~~~ per~cent."
)

data_strings["ocean_b_2"] = (
    "Friday:\n\n~~Sunny.\n~~~~~~~~~~~~~~~~Lush colour with highs in the low~er 5~~~0~s.\n"
    "~~~~~~~~~~Northwest~~ winds~~ 10~~-~1~~5~~ miles~~ an~~ hour\n"
    "~~~~with gusts up to~~ 2~~~~5~~~~ miles~~ an~~ hour~~.\n"
    "~~~~~~~~~~~~~~~~Friday night,~~ mostly~ clear.\n"
    "~~~~~~~~~~~~~~Lows in the mid-3~~~~0~~s.\n"
    "~~~~~~~~~~~~~~~~North winds 10-1~~~5~~ miles~~ an~~ hour~~."
)

data_strings["ocean_b_3"] = (
    "Saturday:\n\n~~~~~~~~Partly sunny.\n"
    "~~~~~~~~High-@igh-@igh-@igh-@igh-@igh-@igh-@igh-@igh-@igh-@igh-@igh-@igh-@igh-@igh-@igh-@igh-@igh-@igh-i"
)

data_strings["ocean_c_0"] = (
    "Here~ are~ the 7~~~~ P~~~~M~~~~ ob~ser~va~tions for~~ the\n"
    "~~~Bos~ton~~ metro~po~li~tan~~~ ar~ea.\n\n"
    "~~~~~~~~~~~~~~~At Logan~~~~ Airport,~~~~~~ it was clou~~~~dy.\n"
    "~~~~~~~~~~~~~~~The tem~per~a~ture was 6~~~~8~~~~ de~grees,\n"
    "~~~~~~~~the dew point,~~ 4~~~~7~~ -\n"
    "~~~~~~and~ the~ re~la~tive~~ hu~mi~di~ty,~~~~ 4~~~~6~~~~ per~~cent.\n"
    "~~~~~~~~~~~~~~~~The~ wind~ was~ south~~west~ at 1~~~~3~ miles~~~~ an~~~~ ho~ur.\n"
    "~~~~~~~~~~~~The~ pres~~sure~~ was~~ 2~~~~9~~~~.~~~~~~~~9~~~~~~9~~~~~~ in~ches and ri~sing.\n"
    "~~~~~~~~Elsewher" + fuck_up_text(
        "rrrrrrrrr\n", 800
    )
)

data_strings["ocean_c_1"] = (
    fuck_up_text(
        "Here~ are~ the 7~~~~ P~~~~M~~~~ ob~ser~va~tions for~~ the\n"
        "~~~Bos~ton~~ metro~po~li~tan~~~ ar~ea.\n\n"
        "~~~~~~~~~~~~~~~At Logan~~~~ Airport,~~~~~~ it was clou~~~~dy.\n"
        "~~~~~~~~~~~~~~~The tem~per~a~ture was 6~~~~8~~~~ de~grees,\n"
        "~~~~~~~~the dew point,~~ 4~~~~7~~ -\n"
        "~~~~~~and~ the~ re~la~tive~~ hu~mi~di~ty,~~~~ 4~~~~6~~~~ per~~cent.\n"
        "~~~~~~~~~~~~~~~~The~ wind~ was~ south~~west~ at 1~~~~3~ miles~~~~ an~~~~ ho~ur.\n"
        "~~~~~~~~~~~~The~ pres~~sure~~ was~~ 2~~~~9~~~~.~~~~~~~~9~~~~~~9~~~~~~ in~ches and ri~sing.\n"
        "~~~~~~~~Elsewher", 100
    ) + fuck_up_text(
        "rrrrrrrrr\n", 900
    )
)

data_strings["ocean_c_2"] = (
    fuck_up_text(
        "Here~ are~ the 7~~~~ P~~~~M~~~~ ob~ser~va~tions for~~ the\n"
        "~~~Bos~ton~~ metro~po~li~tan~~~ ar~ea.\n\n"
        "~~~~~~~~~~~~~~~At Logan~~~~ Airport,~~~~~~ it was clou~~~~dy.\n"
        "~~~~~~~~~~~~~~~The tem~per~a~ture was 6~~~~8~~~~ de~grees,\n"
        "~~~~~~~~the dew point,~~ 4~~~~7~~ -\n"
        "~~~~~~and~ the~ re~la~tive~~ hu~mi~di~ty,~~~~ 4~~~~6~~~~ per~~cent.\n"
        "~~~~~~~~~~~~~~~~The~ wind~ was~ south~~west~ at 1~~~~3~ miles~~~~ an~~~~ ho~ur.\n"
        "~~~~~~~~~~~~The~ pres~~sure~~ was~~ 2~~~~9~~~~.~~~~~~~~9~~~~~~9~~~~~~ in~ches and ri~sing.\n"
        "~~~~~~~~Elsewher", 200
    ) + fuck_up_text(
        "rrrrrrrrr\n", 1000
    )
)

data_strings["ocean_c_3"] = (
    fuck_up_text(
        "Here~ are~ the 7~~~~ P~~~~M~~~~ ob~ser~va~tions for~~ the\n"
        "~~~Bos~ton~~ metro~po~li~tan~~~ ar~ea.\n\n"
        "~~~~~~~~~~~~~~~At Logan~~~~ Airport,~~~~~~ it was clou~~~~dy.\n"
        "~~~~~~~~~~~~~~~The tem~per~a~ture was 6~~~~8~~~~ de~grees,\n"
        "~~~~~~~~the dew point,~~ 4~~~~7~~ -\n"
        "~~~~~~and~ the~ re~la~tive~~ hu~mi~di~ty,~~~~ 4~~~~6~~~~ per~~cent.\n"
        "~~~~~~~~~~~~~~~~The~ wind~ was~ south~~west~ at 1~~~~3~ miles~~~~ an~~~~ ho~ur.\n"
        "~~~~~~~~~~~~The~ pres~~sure~~ was~~ 2~~~~9~~~~.~~~~~~~~9~~~~~~9~~~~~~ in~ches and ri~sing.\n"
        "~~~~~~~~Elsewher", 250
    ) + fuck_up_text(
        "rrrrrrrrr\n", 1000
    )
)

data_strings["funding_0"] = (
    split_word_template(
        "Fun####ding#### for#### this#### pro####gram#### was#### made#### pos####si####ble###~\n"
        "  by###\n"
        "    by###\n"
        "      by###\n"
        "        by###\n"
        "          by#\n"
        "Fun#\n"
        "   by\n"
        "     by\n"
        "       by\n"
        "         by\n"
        "Funding#\n"
        "       by\n"
        "         by\n"
        "           by\n"
        "             by\n"
        "Funding for\n"
        "Funding for thi#i#i#i#i#\n"
        "Funding for this pro####gram###\n"
        "Funding for this pro####gram###\n"
        "                   pro\n"
        "                     pro\n"
        "                       pro\n"
        "Funding for this pro#gram.#~\n"
        "Fun####ding#### for#\n"
        "           by#\n"
        "             by#\n"
        "Funding made#### pos####si####ble#### by#### view####ers#### like#### you.###~\n"
        "####like#### you.##\n"
        "####like#### you.##\n"
        "####like#### you.##\n"
        "####like#### you.##\n"
        "####like#### you.##\n"
        "####like#### you.\n"
        " Fu\n"
        "  Fu\n"
    ) * 2
)

data_strings["funding_1"] = (
    split_word_template(
        "Broad####cast####\n"
        "Broadcast Cor####por##a####tion.~#####\n"
        "Cor####po##ra####tion.#####\n"
        "Cor####po##ra####tion.#####\n"
        " Cor###po\n"
        "  Cor###po\n"
        "    Co\n"
        "      Co\n"
        "Cor####po##ra####tion.#####\n"
        "Cor####po##ra####tion.#####\n"
        "Cor####po##ra####tion.#####\n"
        " Cor###po\n"
        "  Cor###po\n"
        "    Co\n"
        "      Co\n"
        "Cor####po##ra###tion.#\n"
        " Co\n"
        "  Co\n"
        "Cor####po##ra###tion.#\n"
        " Co\n"
        "  Co\n"
        "Cor####po##ra###tion.#\n"
        " Co\n"
        "  Co\n"
        "Cor####po##ra###tion.#\n"
        " Co\n"
        "  Co\n"
        "Cor####po##ra###tion.#\n"
        " Co\n"
        "  Co\n"
        "Cor####po##ra###tion.#\n"
        " Co\n"
        "  Co\n"
        " Cor###po\n"
        "  Cor###po\n"
        "    Cor###po\n"
        "      Cor###po\n"
        "...#######\n"
        " Cor###po#\n"
        "     cor##\n"
        "  cor###po#\n"
        "    cor###po#\n"
        " cor##\n"
        "      cor###po#\n"
        "  cor###po#\n"
        "         cor##\n"
        "     cor###po#\n"
        " cor##\n"
        "   cor##\n"
        "     cor##\n"
        "       co#\n"
        "     co#\n"
        " cor###po#\n"
        "   cor##\n"
        " cor###po#\n"
        "   cor###po#\n"
        "     cor##\n"
        "       cor##\n"
        "         cor###po#\n" + fuck_up_text(

            " cor###po#\n"
            "   cor#\n"
            " cor###po#\n"
            "   cor#\n"
            "     cor#\n", 100, "# "
        ) + " ...#\n" + fuck_up_text(

            " co#\n"
            "  co#\n"
            " cor###po#\n"
            "   cor##\n"
            "     cor###po#\n"
            "   cor###po#\n"
            "            cor##\n"
            "      cor###po#\n", 280, "# "
        ) + fuck_up_text(

            "             cor###po#\n"
            "    cor##\n"
            "                  cor###po#\n"
            "  cor#\n"
            "          cor#\n"
            "                    cor#\n", 500, "# "
        ) + fuck_up_text(

            " co#\n"
            "                co#\n"
            "     cor###po#\n"
            "                        cor#\n"
            "  cor###po#\n"
            "           cor###po#\n"
            "             cor#\n"
            "  cor###po#\n", 650, "# "
        ) +

        " ???###p?#\n"
        "   ??r#\n"
        "           ???###??#\n"
        "                       co?#\n"
        "                                       ???#\n"
    
        "....#....#....#....#....#....#....#....#...#...#...#...#..#..\n"
        "              <##C##O##N##N##E##C##T##I##O##N## ##L##O##S##T##>"
        "##########################################################################################################"
        "##########################################################################################################"
        "##########################################################################################################"
        "##########################################################################################################"
    )
)

data_strings["funding_2"] = (
    split_word_template(
        "--####--####-- ####-i####-a-####---- ####-up####-o-#\n" +
        "-n####nu####-l ####fi####nan####cial ####sup####por#\n" +
        "An####nu####al ####fi####nan####cial ####sup####por#\n" * 5 +
        "An####nu###\nAn####nu###\n"
    )
)

data_strings["fundingx2_0"] = (
    split_word_template(
        " By\n"
        "  ci\n"
        "    po\n"
        "      po\n"
        "  cor#\n"
        "    cor#\n"
        "        by#\n"
        "          by#\n"
        "rr#rr#rr#ro#oo#oo#aa#aa#aa#\n##"
        "  wers#\n"
        " ble#\n"
        "       b\n"
        "         b\n"
        "           F#\n"
        "         f\n"
        "       fi\n"
        "     i\n"
        "na#aa#aa#aa#aa#aa#aa#aa#\n"
        " Fun\n"
        "Fun####ding#\n"
        " Fu#\n"
        "  Fun#\n"
        " fu#\n"
        "Fun####ding#\n"
        " Fu#\n"
        "  Fun#\n"
        " fu#\n"
        "Fun####ding#\n"
        " Fu#\n"
        "  Fun#\n"
        " fu#\n"
        "Fun####ding#\n"
        "F\n"
        "    Fi\n"
        "   Fin\n"
        "          Fina\n"
        "      Finan\n"
        "po\n"
        "po\n"
        "cor#\n"
        "by#\n"
        "  por\n"
        "Por#tio##\n"
        "Por#tio##\n"
        "Por#tion# nn#nn#nn\n"
        "b\n"
        " b\n"
        "by###\n"
        "---- by###\n"
        "-------- by###\n"
        "------------ by###\n"
        "---------------- by#######\n"
        ">>#>>###\n"
        "By#\n"
        "  by#\n"
        "fi#nan#\n"
        " b#\n"
        "  b#\n"
        "   nc#\n"
        "    nc#\n"
        "Corr#rr#rr#\n"
        "View####ers#### like#### you.#\n"
        "      like#### you.#\n"
        "    like## you.#\n"
        "Fun####ding###\n"
        "Fun####ding###\n"
        "  Fun###\n"
        "    Fu#\n"
        "Fun####ding###\n"
        "  fu#\n"
        "    fu#\n"
        "      fu#\n"
        "        fu#\n"
        "          fu#\n"
        "Cor####por####a####tion\n"
        "The#### cor####por####a####tion#### for#### pub####lic####"
        " broad####cast####ing#### and#### bi####-an####nual#### fii#ii#i\n"
        "of#\n"
        "nan#\n"
        "for#\n"
        "fin####an####cial#### su#\n"
        "for#### fin####an####cial#### ass#ss#ss\n"
        "in#\n"
        "view####ers###\n"
        "you#####\n"
        "| ########This######## is######## P####B####S!############~\n"
    )
)

data_strings["fundingx2_1"] = (
    split_word_template("\n".join(
        "{}#### Unknown###".format(
            work_out_date((64 * 3390) + i * 64)).replace(".", ".####") for i in range(26)
    ) + "\n| ########This######## is######## P####B####S!############~\n")
)

data_strings["fundingx2_2"] = (
    split_word_template(
        "S#e#a#r#c#h#i#n#g# #f#o#r# #a#c#c#e#s##s## ##p##o##i##n##t"
        "########.########.########.########\n" +
        "Searching for access point########.########.########.########\n" * 11 +
        "Found:################"
        " P#B#S# #O#f#f#i#c#i#a#l# #1#1#.#### #R#e#s#t#a#r#t#i#n#g#.####.####.####"
    )
)

data_strings["fdg_single_0"] = (
    split_word_template(
        "########.########.########.########.########.########.########.#######\n"
        "########.########.########.########.########.########.########.#######\n"
        "########.########.########.########.########.########.########.#######\n"
        "########.########.########.########.########.########.########.#######\n"
        "########.########.########.########.########.########.########.#######\n"
        "########.########.########.########.########.########.########.#######\n"
        "Fun####ding#### for#### this#### pro####gram#### was#### made#### pos####sible#\n"
        " 7 > > > by###\n"
        " 7 > > > > by###\n"
        " 8 > > > by###\n"
        " 8 > > > > by###\n"
        " 9 > > > by###\n"
        "Fun##\n"
        "     by\n"
        "       by\n"
        "         by\n"
        "           by\n"
        "Funding#\n"
        "     by\n"
        "       by\n"
        "         by\n"
        "           by\n"
        "for\n"
        "thi#i#i#i\n"
        "Pro####gram.#\n"
        "Pro####gram.#\n"
        "Pro\n"
        "  pro\n"
        "    pro\n"
        "      pro\n"
        "Pro####gram.###\n"
        "Fun####ding#### for####\n"
        "            by#\n"
        "              by#\n"
        "Funding for made#### pos####sible#### by#### view####ers#### like#### you.###\n"
        "like#### you.#####\n"
        "##like#### you.#####\n"
        "##like#### you.#####\n"
        "##like#### you.#####\n"
        "##like#### you.#####\n"
        "##like#### you.###\n"
        "Fu\n"
        "  Fu\n"
        "Fun####ding#### for#### this#### pro####gram#### was#### made#### pos####sible#### by#\n"
        "                                         by###\n"
        "                                       by###\n"
        "                                     by###\n"
        "                                   by#\n"
        "Fun###\n"
        "     by\n"
        "       by\n"
        "         by\n"
        "           by\n"
        "Funding#\n"
        "     by\n"
        "       by\n"
        "         by\n"
        "           by\n"
        "for\n"
        "thi#i#i#i\n"
        "Pro####gram.###\n"
        "Pro####gram.###\n"
        "Pro\n"
        "  pro\n"
        "    pro\n"
        "      pro\n"
        "Pro####gram.#\n"
        "Fun####ding#### for####\n"
        "            by#\n"
        "              by#\n"
        "Funding for made#### pos####sible#### by#### view####ers#### like#### you.###\n"
        "like#### you.#####\n"
        "##like#### you.#####\n"
        "##like#### you.#####\n"
        "##like#### you.#####\n"
        "##like#### you.#####\n"
        "####< RET 200################################################################\n"
    )
)

data_strings["fdg_down_0"] = (
    split_word_template(
        "Fun####ding#### for#### this#### pro####gram#### was#\n"
        "made#### made#### made#### made#### made#### made#### made#### made#### made###\n"
        "pos####sible#### by#### view####ers#### like#### you.#######"
    )
)

data_strings["fdg_down_1"] = (
    split_word_template(
        "---####----#### ---#### ----#### ---####----#### ---#\n"
        "----#### ----#### ----#### ----#### ----#### ----#### ----#### ----#### ----###\n"
        "---####-----#### --#### ----####---###### ----######## -##-##-##.#######"
    )
)

data_strings["fdg_down_2"] = (
    split_word_template(
        "Fun####ding#### for#### this#### pro####gram#### was#\n"
        "made#### made#### made#### made#### made#### made#### made#### made#### made###\n"
        "pos####sible#### by#### view####ers###### like######## y##o##u##.#######"
    )
)
Download .txt
gitextract_b3wq7xz6/

├── .gitattributes
├── .gitignore
├── CLIRender/
│   ├── classes.py
│   └── dat.py
├── README.md
├── animation_classes.py
├── animation_functions.py
├── animation_scenes.py
├── animator.py
├── colorama/
│   ├── __init__.py
│   ├── ansi.py
│   ├── ansitowin32.py
│   ├── initialise.py
│   ├── win32.py
│   └── winterm.py
├── credits.py
├── credits_pynput.py
├── media/
│   ├── README.txt
│   └── credits.txt
├── ocean.py
├── requirements.txt
└── string_defs.py
Download .txt
SYMBOL INDEX (179 symbols across 13 files)

FILE: CLIRender/classes.py
  class RenderSection (line 8) | class RenderSection:
    method __init__ (line 11) | def __init__(self, string, start, code):
    method __lt__ (line 21) | def __lt__(self, other):
    method __gt__ (line 24) | def __gt__(self, other):
    method add_char (line 27) | def add_char(self, char):
    method mod_char (line 32) | def mod_char(self, char, loc):
    method add_section (line 40) | def add_section(self, other):
    method add_section_below (line 49) | def add_section_below(self, other):
    method subtract_char (line 57) | def subtract_char(self, loc):
    method subtract_section (line 71) | def subtract_section(self, other):
  class Canvas (line 87) | class Canvas:
    method __init__ (line 91) | def __init__(self, dimensions, num_layers, merge_rules):
    method set_char (line 98) | def set_char(self, layer, location, char, code):
    method set_string (line 102) | def set_string(self, layer, location, string, code):
    method clear_layer (line 106) | def clear_layer(self, layer):
    method render_blank (line 109) | def render_blank(self):
    method render_all (line 112) | def render_all(self):
  class Layer (line 148) | class Layer:
    method __init__ (line 149) | def __init__(self, layer_id, dimensions):
    method set_char (line 158) | def set_char(self, loc, char, code):
    method has_duplicate_starts (line 207) | def has_duplicate_starts(self):
    method set_string (line 217) | def set_string(self, loc, string, code):
  function enable_ansi (line 287) | def enable_ansi():

FILE: CLIRender/dat.py
  class Vector2 (line 6) | class Vector2:  # to be honest this is me showing off
    method __init__ (line 7) | def __init__(self, x, y):
    method __str__ (line 11) | def __str__(self):  # returns a formatted string for the vector2
    method __add__ (line 14) | def __add__(self, other):  # allows adding of vector2s, and ints
    method __mul__ (line 22) | def __mul__(self, other):  # multiplies a vector2 by an int, or by ano...
    method __truediv__ (line 30) | def __truediv__(self, other):  # returns a FLOAT value (true div)
    method __floordiv__ (line 38) | def __floordiv__(self, other):  # returns an INT value (floor div)
    method __pos__ (line 46) | def __pos__(self):
    method __neg__ (line 49) | def __neg__(self):
    method __eq__ (line 52) | def __eq__(self, other):
    method Magnitude (line 55) | def Magnitude(self):  # returns the magnitude of the vector2. (length ...
  class Vector3 (line 59) | class Vector3:  # to be honest this is me showing off
    method __init__ (line 60) | def __init__(self, x, y, z):
    method __str__ (line 65) | def __str__(self):  # returns a formatted string for the Vector3
    method __add__ (line 68) | def __add__(self, other):  # allows adding of Vector3s, and ints
    method __mul__ (line 76) | def __mul__(self, other):  # multiplies a Vector3 by an int, or by ano...
    method __truediv__ (line 84) | def __truediv__(self, other):  # returns a FLOAT value (true div)
    method __floordiv__ (line 92) | def __floordiv__(self, other):  # returns an INT value (floor div)
    method __pos__ (line 100) | def __pos__(self):
    method __neg__ (line 103) | def __neg__(self):
    method __eq__ (line 106) | def __eq__(self, other):
    method Magnitude (line 109) | def Magnitude(self):  # returns the magnitude of the Vector3. (length ...

FILE: animation_classes.py
  class Weather (line 6) | class Weather:
    method __init__ (line 7) | def __init__(self, precip, temp, wind, gust, wind_dir, humidity, days=2):
    method __str__ (line 17) | def __str__(self):
    method get_weather_name (line 34) | def get_weather_name(self):
    method mutate (line 62) | def mutate(self, steps=1):

FILE: animation_functions.py
  function generate_random_hex (line 8) | def generate_random_hex(length):
  function replace_text_with_spaces (line 12) | def replace_text_with_spaces(string, chance):
  function render_weather (line 16) | def render_weather(c, layer, x, y, weather, mutations_after=0, spc_chanc...
  function noise (line 60) | def noise(c, layer, amount, chars, colours):
  function set_multiline_string (line 68) | def set_multiline_string(c, layer, x, y, string, col):
  function type_text (line 75) | def type_text(c, generator, layer, x, y, col, render=True):
  function fuck_up_text (line 112) | def fuck_up_text(string, chance, also_ignore=""):
  function debug_info (line 129) | def debug_info(c, g, b, frames):
  function clear (line 183) | def clear(c, layer):
  function beat_toggle (line 187) | def beat_toggle(c, g, layer, x, x2, y, y2, char, col):
  function work_out_date (line 199) | def work_out_date(b, day_offset=0):
  function split_word_template (line 227) | def split_word_template(string):
  function typewrite_by_word (line 231) | def typewrite_by_word(c, generator, layer, x, y, col, render=True, histo...
  function write_history (line 298) | def write_history(c, generator, layer, x, y, col, stop, var="history"):
  function show_access_point_visual (line 324) | def show_access_point_visual(c, generator, layer, x, y):
  function make_poweroff_bars (line 356) | def make_poweroff_bars(c, b, layer, col):

FILE: animator.py
  class DataStoringObject (line 1) | class DataStoringObject:
    method __init__ (line 2) | def __init__(self):
    method set_data (line 5) | def set_data(self, *ident):
    method get_data (line 9) | def get_data(self, ident):
    method oper_data (line 13) | def oper_data(self, ident, oper):
  class SceneManager (line 17) | class SceneManager(DataStoringObject):
    method __init__ (line 18) | def __init__(self, scenes, events):
    method start_scene (line 31) | def start_scene(self, scene, at=0):
    method add_scene (line 38) | def add_scene(self, scene, at=0):
    method remove_scene (line 42) | def remove_scene(self, scene):
    method request_next (line 46) | def request_next(self, render=True):
    method next_beat (line 52) | def next_beat(self):
    method set_scene_data (line 58) | def set_scene_data(self, scene, *ident):
    method set_generator_data (line 61) | def set_generator_data(self, scene, generator, *ident):
  class Event (line 65) | class Event:
    method __init__ (line 66) | def __init__(self, beat, do):
    method swap_scene (line 71) | def swap_scene(sc, at=0):
    method layer_scene (line 75) | def layer_scene(sc, at=0):
    method remove_scene (line 79) | def remove_scene(sc):
  class Scene (line 83) | class Scene(DataStoringObject):
    method __init__ (line 84) | def __init__(self, name, generators):
    method set_parent (line 93) | def set_parent(self, parent):
    method request_frame (line 97) | def request_frame(self, render=True):
    method start (line 112) | def start(self, at):
  class Generator (line 123) | class Generator(DataStoringObject):
    method __init__ (line 124) | def __init__(self, start_beat, condition, on_create, request, request_...
    method set_parent (line 137) | def set_parent(self, parent):
    method set_scene (line 140) | def set_scene(self, scene):
    method combine_conditions (line 144) | def combine_conditions(*cond):
    method always (line 148) | def always():
    method every_n_beats (line 152) | def every_n_beats(beat):
    method every_on_off (line 156) | def every_on_off(on, off):
    method every_off_on (line 160) | def every_off_on(off, on):
    method before_n (line 164) | def before_n(beat):
    method at_beat (line 168) | def at_beat(beat):
    method no_create (line 172) | def no_create():
    method no_request (line 176) | def no_request():

FILE: colorama/ansi.py
  function code_to_chars (line 12) | def code_to_chars(code):
  function set_title (line 15) | def set_title(title):
  function clear_screen (line 18) | def clear_screen(mode=2):
  function clear_line (line 21) | def clear_line(mode=2):
  class AnsiCodes (line 25) | class AnsiCodes(object):
    method __init__ (line 26) | def __init__(self):
  class AnsiCursor (line 36) | class AnsiCursor(object):
    method UP (line 37) | def UP(self, n=1):
    method DOWN (line 39) | def DOWN(self, n=1):
    method FORWARD (line 41) | def FORWARD(self, n=1):
    method BACK (line 43) | def BACK(self, n=1):
    method POS (line 45) | def POS(self, x=1, y=1):
  class AnsiFore (line 49) | class AnsiFore(AnsiCodes):
  class AnsiBack (line 71) | class AnsiBack(AnsiCodes):
  class AnsiStyle (line 93) | class AnsiStyle(AnsiCodes):

FILE: colorama/ansitowin32.py
  function is_stream_closed (line 16) | def is_stream_closed(stream):
  function is_a_tty (line 20) | def is_a_tty(stream):
  class StreamWrapper (line 24) | class StreamWrapper(object):
    method __init__ (line 30) | def __init__(self, wrapped, converter):
    method __getattr__ (line 36) | def __getattr__(self, name):
    method write (line 39) | def write(self, text):
  class AnsiToWin32 (line 43) | class AnsiToWin32(object):
    method __init__ (line 52) | def __init__(self, wrapped, convert=None, strip=None, autoreset=False):
    method should_wrap (line 85) | def should_wrap(self):
    method get_win32_calls (line 95) | def get_win32_calls(self):
    method write (line 139) | def write(self, text):
    method reset_all (line 149) | def reset_all(self):
    method write_and_convert (line 156) | def write_and_convert(self, text):
    method write_plain_text (line 172) | def write_plain_text(self, text, start, end):
    method convert_ansi (line 178) | def convert_ansi(self, paramstring, command):
    method extract_params (line 184) | def extract_params(self, command, paramstring):
    method call_win32 (line 202) | def call_win32(self, command, params):
    method convert_osc (line 224) | def convert_osc(self, text):

FILE: colorama/initialise.py
  function reset_all (line 18) | def reset_all():
  function init (line 23) | def init(autoreset=False, convert=None, strip=None, wrap=True):
  function deinit (line 51) | def deinit():
  function colorama_text (line 59) | def colorama_text(*args, **kwargs):
  function reinit (line 67) | def reinit():
  function wrap_stream (line 74) | def wrap_stream(stream, convert, strip, autoreset, wrap):

FILE: colorama/win32.py
  class CONSOLE_SCREEN_BUFFER_INFO (line 21) | class CONSOLE_SCREEN_BUFFER_INFO(Structure):
    method __str__ (line 30) | def __str__(self):
  function winapi_test (line 97) | def winapi_test():
  function GetConsoleScreenBufferInfo (line 104) | def GetConsoleScreenBufferInfo(stream_id=STDOUT):
  function SetConsoleTextAttribute (line 111) | def SetConsoleTextAttribute(stream_id, attrs):
  function SetConsoleCursorPosition (line 115) | def SetConsoleCursorPosition(stream_id, position, adjust=True):
  function FillConsoleOutputCharacter (line 133) | def FillConsoleOutputCharacter(stream_id, char, length, start):
  function FillConsoleOutputAttribute (line 143) | def FillConsoleOutputAttribute(stream_id, attr, length, start):
  function SetConsoleTitle (line 153) | def SetConsoleTitle(title):

FILE: colorama/winterm.py
  class WinColor (line 6) | class WinColor(object):
  class WinStyle (line 17) | class WinStyle(object):
  class WinTerm (line 22) | class WinTerm(object):
    method __init__ (line 24) | def __init__(self):
    method get_attrs (line 36) | def get_attrs(self):
    method set_attrs (line 39) | def set_attrs(self, value):
    method reset_all (line 44) | def reset_all(self, on_stderr=None):
    method fore (line 48) | def fore(self, fore=None, light=False, on_stderr=False):
    method back (line 59) | def back(self, back=None, light=False, on_stderr=False):
    method style (line 70) | def style(self, style=None, on_stderr=False):
    method set_console (line 76) | def set_console(self, attrs=None, on_stderr=False):
    method get_position (line 84) | def get_position(self, handle):
    method set_cursor_position (line 92) | def set_cursor_position(self, position=None, on_stderr=False):
    method cursor_adjust (line 102) | def cursor_adjust(self, x, y, on_stderr=False):
    method erase_screen (line 110) | def erase_screen(self, mode=0, on_stderr=False):
    method erase_line (line 139) | def erase_line(self, mode=0, on_stderr=False):
    method set_title (line 161) | def set_title(self, title):

FILE: credits.py
  function skip_beats (line 28) | def skip_beats(ctr, amount, next_debug):

FILE: credits_pynput.py
  function skip_beats (line 28) | def skip_beats(ctr, amount, next_debug):
  function on_press (line 335) | def on_press(key):
  function on_release (line 342) | def on_release(key):

FILE: ocean.py
  function init_populate_ocean (line 12) | def init_populate_ocean():
  function get_ocean_slice (line 22) | def get_ocean_slice(xr, glitch):
  function mutate_text (line 44) | def mutate_text(txt, glitch):
  function get_ocean_slices (line 53) | def get_ocean_slices(x1, x2):
  function update_ocean_slices (line 88) | def update_ocean_slices(ocean_content, ocean_glitch):
  function unpack_content_to_text (line 105) | def unpack_content_to_text(content):
  function begin_ocean (line 110) | def begin_ocean():
Condensed preview — 22 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (154K chars).
[
  {
    "path": ".gitattributes",
    "chars": 66,
    "preview": "# Auto detect text files and perform LF normalization\n* text=auto\n"
  },
  {
    "path": ".gitignore",
    "chars": 67,
    "preview": "**/__pycache__\nmedia/*.wav\nmedia/*.WAV\nmedia/*.mp3\nmedia/*.MP3\nvenv"
  },
  {
    "path": "CLIRender/classes.py",
    "chars": 13597,
    "preview": "import os\nfrom platform import system as system_type\nfrom bisect import insort_right, bisect_right\n\nfrom CLIRender.dat i"
  },
  {
    "path": "CLIRender/dat.py",
    "chars": 4617,
    "preview": "# contains:\n#\n#    x position (generic typing because python but should be an int in most cases)\n#    y position (the sa"
  },
  {
    "path": "README.md",
    "chars": 2326,
    "preview": "# credits_public\n- Public version of Frums - Credits animation repository.\n- Should be multiplatform. Feel free to raise"
  },
  {
    "path": "animation_classes.py",
    "chars": 4087,
    "preview": "# Contains all classes (just Weather)\nimport random\nimport math\n\n\nclass Weather:\n    def __init__(self, precip, temp, wi"
  },
  {
    "path": "animation_functions.py",
    "chars": 12547,
    "preview": "# Contains functions used inside credits.py.\n# Reduce clutter. Reuse clutter. Recycle clutter.\nimport random\nfrom CLIRen"
  },
  {
    "path": "animation_scenes.py",
    "chars": 30380,
    "preview": "# Contains all animation scenes except for debug_counter.\n# Use variable \"all_scenes\" which contains all scenes defined "
  },
  {
    "path": "animator.py",
    "chars": 4793,
    "preview": "class DataStoringObject:\n    def __init__(self):\n        self.data = {}\n\n    def set_data(self, *ident):\n        for i i"
  },
  {
    "path": "colorama/__init__.py",
    "chars": 240,
    "preview": "# Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file.\nfrom .initialise import init, deinit, reinit,"
  },
  {
    "path": "colorama/ansi.py",
    "chars": 2524,
    "preview": "# Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file.\n'''\nThis module generates ANSI character code"
  },
  {
    "path": "colorama/ansitowin32.py",
    "chars": 9668,
    "preview": "# Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file.\nimport re\nimport sys\nimport os\n\nfrom .ansi im"
  },
  {
    "path": "colorama/initialise.py",
    "chars": 1917,
    "preview": "# Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file.\nimport atexit\nimport contextlib\nimport sys\n\nf"
  },
  {
    "path": "colorama/win32.py",
    "chars": 5365,
    "preview": "# Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file.\n\n# from winbase.h\nSTDOUT = -11\nSTDERR = -12\n\n"
  },
  {
    "path": "colorama/winterm.py",
    "chars": 6290,
    "preview": "# Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file.\nfrom . import win32\n\n\n# from wincon.h\nclass W"
  },
  {
    "path": "credits.py",
    "chars": 12880,
    "preview": "from animation_functions import debug_info\nfrom CLIRender.classes import enable_ansi\nfrom colorama import Fore, Style\n\ni"
  },
  {
    "path": "credits_pynput.py",
    "chars": 13380,
    "preview": "from animation_functions import debug_info\nfrom CLIRender.classes import enable_ansi\nfrom colorama import Fore, Style\n\nf"
  },
  {
    "path": "media/README.txt",
    "chars": 347,
    "preview": "I can't really distribute a full .wav file of an entire song that I don't own the rights to.\nThat would be disrespectful"
  },
  {
    "path": "media/credits.txt",
    "chars": 2323,
    "preview": "Now for the official national weather service forecast for Eastern Massachusets inside of I-495,\nincluding Boston, issue"
  },
  {
    "path": "ocean.py",
    "chars": 2939,
    "preview": "# adapted from the-sea.js by plaao (me)\n\n# The ocean is stored as a list of strings which is combined into one string wh"
  },
  {
    "path": "requirements.txt",
    "chars": 82,
    "preview": "just_playback\npynput; sys_platform == \"darwin\"\nkeyboard; sys_platform != \"darwin\"\n"
  },
  {
    "path": "string_defs.py",
    "chars": 15758,
    "preview": "# Contains strings used inside credits.py.\n# Kept inside this file to reduce clutter.\n# Implemented as a dictionary of i"
  }
]

About this extraction

This page contains the full source code of the plaaosert/credits_public GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 22 files (142.8 KB), approximately 39.2k tokens, and a symbol index with 179 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!