diff --git a/lib/Animator/Animation.h b/lib/Animator/Animation.h index 4adfa02..189877c 100644 --- a/lib/Animator/Animation.h +++ b/lib/Animator/Animation.h @@ -13,7 +13,7 @@ namespace AnimationHelpers{ V3D cyan{0,255,255}; V3D magenta{255,0,255}; - Cell CreateCell(float x_percent, float y_percent, float z_percent, V3D &color){ + Cell CreateCell(float x_percent, float y_percent, float z_percent, const V3D &color){ float continuousMaxValue{static_cast(std::numeric_limits::max())}; Cell cell{ .position = V3D{ @@ -211,11 +211,42 @@ namespace RisingCubes{ .delay = std::chrono::milliseconds(1) }; + AnimationFrame frame00{ + .frame = { + CreateCell(0.0,0.0,1.0,V3D(128.0,128.0,255.0)), + CreateCell(0.5,0.0,1.0,V3D(128.0,128.0,255.0)), + CreateCell(1.0,0.0,0.0,V3D(255.0,118.0,205.0)), + CreateCell(1.0,0.0,0.5,V3D(255.0,118.0,205.0)), + CreateCell(1.0,0.0,1.0,V3D(255.0,116.0,0.0)), + CreateCell(1.0,0.5,1.0,V3D(183.0,0.0,255.0)), + CreateCell(1.0,1.0,1.0,V3D(183.0,0.0,255.0)) + }, + .fillInterpolation = FillInterpolation::CLOSEST_COLOR, + .frameInterpolation = FrameInterpolation::FADE, + .delay = std::chrono::milliseconds(1000) + }; + + AnimationFrame frame01{ + .frame = { + CreateCell(0.0,0.0,1.0,V3D(255.0,255.0,171.0)), + CreateCell(0.5,0.0,1.0,V3D(255.0,255.0,171.0)), + CreateCell(1.0,0.0,0.0,V3D(0.0,195.0,88.0)), + CreateCell(1.0,0.0,0.5,V3D(0.0,195.0,88.0)), + CreateCell(1.0,0.0,1.0,V3D(0.0,195.0,88.0)), + CreateCell(1.0,0.5,1.0,V3D(112.0,222.0,255.0)), + CreateCell(1.0,1.0,1.0,V3D(112.0,222.0,255.0)) + }, + .fillInterpolation = FillInterpolation::CLOSEST_COLOR, + .frameInterpolation = FrameInterpolation::FADE, + .delay = std::chrono::milliseconds(1000) + }; + std::vector rising{ - frame1, // 0 - frame2, // 1 - frame3, // 2 - frame4, // 3 - frame5 + frame00, frame01, frame00 + // frame1, // 0 + // frame2, // 1 + // frame3, // 2 + // frame4, // 3 + // frame5 }; } \ No newline at end of file diff --git a/platformio.ini b/platformio.ini index 2b70b1f..4e4922f 100644 --- a/platformio.ini +++ b/platformio.ini @@ -7,34 +7,37 @@ ; ; Please visit documentation for the other options and examples ; https://docs.platformio.org/page/projectconf.html + [platformio] -default_envs = esp32s3_release ; this ensures that only this environment is built for anything but the debug build +default_envs = esp32s3_release extra_configs = platformio-local.ini [env] -; platform = espressif32 upload_protocol = esptool platform = https://github.com/platformio/platform-espressif32.git board = esp32-s3-devkitc-1 framework = arduino build_flags = -Iinclude - monitor_speed = 9600 monitor_filters = colorize, send_on_enter -upload_speed = 2000000 ;ESP32S3 USB-Serial Converter maximum 2000000bps -lib_deps = adafruit/Adafruit NeoPixel@^1.12.0 +upload_speed = 2000000 +lib_deps = + adafruit/Adafruit NeoPixel@^1.12.3 + fastled/FastLED@^3.7.3 [env:esp32s3_release] build_type = release +monitor_port = COM21 +upload_port = COM20 [env:esp32s3_debug] debug_init_break = tbreak setup debug_tool = esp-builtin build_type = debug debug_speed = 20000 -; debug_port = COM7 -; monitor_port = COM14 build_flags = - -D DEBUG = 1 - -I include \ No newline at end of file + -D DEBUG = 1 + -I include +monitor_port = COM21 +upload_port = COM20 diff --git a/src/main.cpp b/src/main.cpp index 737824f..91f810b 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -30,9 +30,10 @@ TaskHandle_t updateCommunicaitonTask; TaskHandle_t updateBoardTask; -std::array*, 2> animations = { +// WARNING! This array size should always be equal to the number of entries in it!! +std::array*, 1> animations = { &RisingCubes::rising, - &RotatingCubes::rotating, + // &RotatingCubes::rotating, }; // BluetoothSerial SerialBT; @@ -112,6 +113,7 @@ void parseData(Message &message){ } case Commands::PING:{ GlobalPrint::Println("!" + String(Commands::PING) + ";"); + boardManager.PrintColorState(); break; } case Commands::SetStackColors:{ diff --git a/tools/animation-tools/.gitignore b/tools/animation-tools/.gitignore new file mode 100644 index 0000000..77a7e05 --- /dev/null +++ b/tools/animation-tools/.gitignore @@ -0,0 +1,3 @@ +venv/ +__pycache__/ +output.txt \ No newline at end of file diff --git a/tools/animation-tools/AnimationExporter.py b/tools/animation-tools/AnimationExporter.py new file mode 100644 index 0000000..19246b1 --- /dev/null +++ b/tools/animation-tools/AnimationExporter.py @@ -0,0 +1,78 @@ +from Scene import Scene +from enum import StrEnum +from pygame.math import Vector3 +from Shapes import Mesh + +class FillInterpolation(StrEnum): + NO_FILL = "NO_FILL" + CLOSEST_COLOR = "CLOSEST_COLOR" + LINEAR_WEIGHTED_DISTANCE = "LINEAR_WEIGHTED_DISTANCE" + SQUARE_WEIGHTED_DISTANCE = "SQUARE_WEIGHTED_DISTANCE" + # options = [NO_FILL, CLOSEST_COLOR, LINEAR_WEIGHTED_DISTANCE, SQUARE_WEIGHTED_DISTANCE] + +class FrameInterpolation(StrEnum): + SNAP = "SNAP" + FADE = "FADE" + # options = [SNAP, FADE] + +class Cell: + def __init__(self, position: Vector3, color: Vector3): + self.position: Vector3 = position + self.color: Vector3 = color + + def to_string(self): + message = f"CreateCell({self.position.x},{self.position.y},{self.position.z}" + message += f",V3D({self.color.x},{self.color.y},{self.color.z}))," + return message + +class AnimationFrame: + def __init__(self, cells: list[Cell], fill_interpolation: FillInterpolation, frame_interpolation: FrameInterpolation, delay: int): + self.cells = cells + self.fill = fill_interpolation + self.frame_interp = frame_interpolation + self.delay = delay + + def to_string(self, frame_number: int): + cell_str = "" + for i, cell in enumerate(self.cells): + if cell.color.x != 0 or cell.color.y != 0 or cell.color.z != 0: + if i != 0: + cell_str += "\t\t" + cell_str += cell.to_string() + "\n\t" + cell_str = cell_str[:-3] + message = f""" +AnimationFrame frame{frame_number}{{ + .frame = {{ + {cell_str} + }}, + .fillInterpolation = FillInterpolation::{self.fill}, + .frameInterpolation = FrameInterpolation::{self.frame_interp}, + .delay = std::chrono::milliseconds({self.delay}) +}}; + """ + return message + + +#TODO: Impliment this +def generate_animation_frame_struct(animation_types_path: str): + with open(animation_types_path, 'r') as AnimationTypes: + with open("AnimationTypes.py", 'w') as output: + pass + +def mesh_to_cell(mesh: Mesh) -> Cell: + if mesh.face_color[0] != 0 or mesh.face_color[1] != 0 or mesh.face_color[2] != 0: + pass + pos = (mesh.get_average_position() + Vector3(1,1,1)) / 2 + # need to swap z and y for the coordinate system + z = pos.y + pos.y = pos.z + pos.z = z + testVector = Vector3(0.1,0.5,0.9) + + cell = Cell(pos, Vector3(mesh.face_color)) + return cell + +def scene_to_frame(scene: Scene, fill_interpolation: FillInterpolation, frame_interpolation: FrameInterpolation, delay: int, scene_number: int) -> str: + cells = [mesh_to_cell(cube) for cube in scene.meshes] + frame = AnimationFrame(cells, fill_interpolation, frame_interpolation, delay) + return frame.to_string(scene_number) \ No newline at end of file diff --git a/tools/animation-tools/MatrixMath.py b/tools/animation-tools/MatrixMath.py new file mode 100644 index 0000000..cd0d371 --- /dev/null +++ b/tools/animation-tools/MatrixMath.py @@ -0,0 +1,17 @@ +import math +from pygame.math import Vector3 + +def project(vector, w, h, fov, distance): + factor = math.atan(fov / 2 * math.pi / 180) / (distance + vector.z) + x = vector.x * factor * w + w / 2 + y = -vector.y * factor * w + h / 2 + return Vector3(x, y, vector.z) + +def rotate_vertices(vertices, angle, axis): + return [v.rotate(angle, axis) for v in vertices] +def scale_vertices(vertices, s): + return [Vector3(v[0]*s[0], v[1]*s[1], v[2]*s[2]) for v in vertices] +def translate_vertices(vertices, t): + return [v + Vector3(t) for v in vertices] +def project_vertices(vertices, w, h, fov, distance): + return [project(v, w, h, fov, distance) for v in vertices] \ No newline at end of file diff --git a/tools/animation-tools/README.md b/tools/animation-tools/README.md new file mode 100644 index 0000000..789b193 --- /dev/null +++ b/tools/animation-tools/README.md @@ -0,0 +1,31 @@ +# Setup +In order to run this tool follow these steps: + +1. ensure you're using python 3.12+. You can verify this by running +``` +python --version +``` +in a terminal. + +2. Inside of a terminal in the +``` +tools/animation-tools +``` +folder, run: +``` +python -m venv venv +``` + to create your virtual environment. + +3. Now activate the virtual environment. In powershell run +``` +venv\Scripts\activate +``` +You may need to fix permission issues if this is your first time using a virtual environment. For git bash just run: +``` +source venv/Scripts/activate +``` +4. Run +``` +pip install -r requirements.txt +``` \ No newline at end of file diff --git a/tools/animation-tools/Scene.py b/tools/animation-tools/Scene.py new file mode 100644 index 0000000..a5fe9db --- /dev/null +++ b/tools/animation-tools/Scene.py @@ -0,0 +1,70 @@ +from Shapes import Mesh +from MatrixMath import * +import pygame + +class Scene: + def __init__(self, meshes, fov, distance): + self.meshes: list[Mesh] = meshes + self.fov = fov + self.distance = distance + self.euler_angles = [0, 0, 0] + + def transform_vertices(self, vertices, width, height): + transformed_vertices = vertices + axis_list = [(1, 0, 0), (0, 1, 0), (0, 0, 1)] + for angle, axis in reversed(list(zip(list(self.euler_angles), axis_list))): + transformed_vertices = rotate_vertices(transformed_vertices, angle, axis) + transformed_vertices = project_vertices(transformed_vertices, width, height, self.fov, self.distance) + return transformed_vertices + + def point_in_polygon(self, point, polygon): + """ Determine if the point (x, y) is inside the polygon """ + x, y = point + n = len(polygon) + inside = False + p1x, p1y = polygon[0] + for i in range(n + 1): + p2x, p2y = polygon[i % n] + if y > min(p1y, p2y): + if y <= max(p1y, p2y): + if x <= max(p1x, p2x): + if p1y != p2y: + xints = (y - p1y) * (p2x - p1x) / (p2y - p1y) + p1x + if p1x == p2x or x <= xints: + inside = not inside + p1x, p1y = p2x, p2y + return inside + + def get_mesh_from_xy(self, pos: tuple[int]) -> Mesh: + x, y = pos + closest_mesh = None + closest_z = float('inf') + + for mesh in self.meshes: + transformed_vertices = self.transform_vertices(mesh.get_vertices(), *pygame.display.get_surface().get_size()) + avg_z = mesh.calculate_average_z(transformed_vertices) + for i, avg_z_value in avg_z: + polygon = mesh.create_polygon(mesh.get_face(i), transformed_vertices) + if self.point_in_polygon((x, y), polygon): + if avg_z_value < closest_z: + closest_z = avg_z_value + closest_mesh = mesh + + return closest_mesh + + def draw(self, surface): + + polygons = [] + for mesh in self.meshes: + transformed_vertices = self.transform_vertices(mesh.get_vertices(), *surface.get_size()) + avg_z = mesh.calculate_average_z(transformed_vertices) + for z in avg_z: + #for z in sorted(avg_z, key=lambda x: x[1], reverse=True): + pointlist = mesh.create_polygon(mesh.get_face(z[0]), transformed_vertices) + polygons.append((pointlist, z[1], mesh.face_color, mesh.edge_color)) + #pygame.draw.polygon(surface, (128, 128, 192), pointlist) + #pygame.draw.polygon(surface, (0, 0, 0), pointlist, 3) + + for poly in sorted(polygons, key=lambda x: x[1], reverse=True): + pygame.draw.polygon(surface, poly[2], poly[0]) + pygame.draw.polygon(surface, poly[3], poly[0], 3) \ No newline at end of file diff --git a/tools/animation-tools/Shapes.py b/tools/animation-tools/Shapes.py new file mode 100644 index 0000000..c72d6ca --- /dev/null +++ b/tools/animation-tools/Shapes.py @@ -0,0 +1,48 @@ +from MatrixMath import * +from pygame.math import Vector3 + +class Mesh(): + def __init__(self, vertices, faces): + self.__vertices = [Vector3(v) for v in vertices] + self.__faces = faces + self.face_color = (0, 0, 0) + self.edge_color = (0,0,0) + + def rotate(self, angle, axis): + self.__vertices = rotate_vertices(self.__vertices, angle, axis) + def scale(self, s): + self.__vertices = scale_vertices(self.__vertices, s) + def translate(self, t): + self.__vertices = translate_vertices(self.__vertices, t) + + def calculate_average_z(self, vertices): + return [(i, sum([vertices[j].z for j in f]) / len(f)) for i, f in enumerate(self.__faces)] + + def get_average_position(self): + vertex_sum: Vector3 = Vector3() + for vertex in self.__vertices: + vertex_sum += vertex + return vertex_sum / len(self.__vertices) + + def get_face(self, index): + return self.__faces[index] + def get_vertices(self): + return self.__vertices + + def create_polygon(self, face, vertices): + return [(vertices[i].x, vertices[i].y) for i in [*face, face[0]]] + + def set_face_color(self, color : tuple[int]): + self.face_color = color + + def set_edge_color(self, color : tuple[int]): + self.edge_color = color + +class Cube(Mesh): + def __init__(self): + vertices = [(-1,-1,1), (1,-1,1), (1,1,1), (-1,1,1), (-1,-1,-1), (1,-1,-1), (1,1,-1), (-1,1,-1)] + faces = [(0,1,2,3), (1,5,6,2), (5,4,7,6), (4,0,3,7), (3,2,6,7), (1,0,4,5)] + super().__init__(vertices, faces) + + def set_position(self, position : tuple): + super().translate(position) \ No newline at end of file diff --git a/tools/animation-tools/UI.py b/tools/animation-tools/UI.py new file mode 100644 index 0000000..8426aab --- /dev/null +++ b/tools/animation-tools/UI.py @@ -0,0 +1,210 @@ +from itertools import product +from pygame_widgets.button import Button +from pygame_widgets.slider import Slider +from pygame_widgets.dropdown import Dropdown +from pygame_widgets.textbox import TextBox +from Scene import Scene +from Shapes import * +import pygame +from AnimationExporter import * + +class ColorPicker: + def __init__(self, screen, x_pos: int, y_pos: int, width: int, height: int): + width = max(50, width) + slider_colors = ((255,0,0),(0,255,0),(0,0,255)) + self.sliders: list[Slider] = [ + Slider(screen, int(x_pos + i*(width/3)), y_pos, int((width-50)/3), height, min=0, max=255, step=1, vertical=True, handleColour=slider_colors[i]) for i in range(3) + ] + for slider in self.sliders: + slider.enable() + + def get_color(self) -> tuple[int]: + return tuple([slider.getValue() for slider in self.sliders]) + + def set_color(self, color: tuple[int]): + for i, slider in enumerate(self.sliders): + slider.setValue(color[i]) + +class SceneStore: + def __init__(self, scene: Scene, fill: FillInterpolation, fade: FrameInterpolation, delay: int): + self.scene: Scene = scene + self.fill: FillInterpolation = fill + self.fade: FrameInterpolation = fade + self.delay: int = delay + +class SceneManager: + def __init__(self, window, color_picker: ColorPicker): + self.file_data: str = "" + self._scenes: list[SceneStore] = [SceneStore(self.new_scene(), FillInterpolation.NO_FILL, FrameInterpolation.FADE, 1000)] + self._current_scene_idx: int = 0 + self.window = window + self.color_picker = color_picker + self._selected_meshes: list[Mesh] = [] + + def save_frame_to_file(self): + with open("tools/animation-tools/output.txt", 'w') as file: + for i, scene in enumerate(self._scenes): + file.write(scene_to_frame(scene.scene, scene.fill, scene.fade, scene.delay, i)) + + def generate_meshes(self) -> list[Mesh]: + # generate a list of cubes + meshes: list[Mesh] = [] + for origin in product([-1, 0, 1],[-1, 0, 1],[-1, 0, 1]): + cube = Cube() + cube.scale((0.35, 0.35, 0.35)) + cube.set_position(origin) + cube.set_face_color((0, 0, 0)) + meshes.append(cube) + return meshes + + def new_scene(self) -> Scene: + return Scene(self.generate_meshes(), 90, 5) + + def draw(self): + for mesh in self._selected_meshes: + mesh.set_face_color(self.color_picker.get_color()) + self.get_current_scene().scene.draw(self.window) + + def get_current_scene(self) -> SceneStore: + return self._scenes[self._current_scene_idx] + + def click_mesh(self, coordinates: tuple[int, int]): + mesh = self.get_current_scene().scene.get_mesh_from_xy(coordinates) + + if mesh == None: + return + + if mesh in self._selected_meshes: + mesh.set_edge_color((0,0,0)) + self._selected_meshes.remove(mesh) + else: + mesh.set_edge_color((255,0,0)) + self._selected_meshes.append(mesh) + + def deselect_all_mesh(self): + for mesh in self._selected_meshes: + mesh.set_edge_color((0,0,0)) + self._selected_meshes = [] + + def next_scene(self): + self.deselect_all_mesh() + current_angles = self.get_current_scene().scene.euler_angles + if len(self._scenes)-1 == self._current_scene_idx: + self._scenes.append(SceneStore(self.new_scene(), FillInterpolation.NO_FILL, FrameInterpolation.FADE, 1000)) + + self._current_scene_idx += 1 + + self.get_current_scene().scene.euler_angles = [angle for angle in current_angles] + + def last_scene(self): + if self._current_scene_idx > 0: + current_angles = self.get_current_scene().scene.euler_angles + self._current_scene_idx -= 1 + self.get_current_scene().scene.euler_angles = [angle for angle in current_angles] + self.deselect_all_mesh() + + def update_scene_options(self, fill: FillInterpolation, fade: FrameInterpolation, delay: int): + cur_scene: SceneStore = self.get_current_scene() + cur_scene.fill = fill + cur_scene.fade = fade + cur_scene.delay = delay + +class AnimatorUI: + def __init__(self, window): + scr_wdt, scr_hgt = window.get_size() + + self.window = window + self.colorPicker: ColorPicker = ColorPicker(self.window, *self.rel2abs(5, 5, 20, 60)) + self.sceneManager: SceneManager = SceneManager(window, self.colorPicker) + + self.save_button: Button = Button(self.window, *self.rel2abs(5, 90, 10, 10), text="Save",onClick=self.sceneManager.save_frame_to_file) + self.next_frame_button: Button = Button(self.window, *self.rel2abs(75, 90, 25, 5), text="Next Frame",onClick=self._next_scene) + self.last_frame_button: Button = Button(self.window, *self.rel2abs(50, 90, 25, 5), text="last Frame",onClick=self._last_scene) + self.trackMouseMotion: bool = False + + self.fill_dropdown: Dropdown = Dropdown(self.window, *self.rel2abs(30, 0, 40, 5), name="Fill Type", + choices=[option.value for option in FillInterpolation], onRelease=self.set_update_options_flag) + + self.fade_dropdown: Dropdown = Dropdown(self.window, *self.rel2abs(70, 0, 20, 5), name="Fade Type", + choices=[option.value for option in FrameInterpolation], onRelease=self.set_update_options_flag) + + self.updateOptions = False + self._set_dropdown_options() + + self.delay_text_entry: TextBox = TextBox(self.window, *self.rel2abs(30, 5, 60, 7), placeholderText="Transition Time (ms)", onSubmit=self.set_update_options_flag) + + # Make a frame counter as a button but make it not look like a button + default_color=(150,150,150) + self.frame_counter_text: Button = Button(self.window, *self.rel2abs(15, 90, 10, 10), text="0", inactiveColour=default_color, hoverColour=default_color, pressedColour=default_color) + + def rel2abs(self, x: float, y: float, width: float, height: float) -> tuple[int,int,int,int]: + scr_wdt, scr_hgt = self.window.get_size() + x_abs = int(x*scr_wdt/100) + y_abs = int(y*scr_hgt/100) + w_abs = int(width*scr_wdt/100) + h_abs = int(height*scr_hgt/100) + return (x_abs, y_abs, w_abs, h_abs) + + + def update_interaction(self, game_event): + if not self.trackMouseMotion: + pygame.mouse.get_rel() + + if game_event.type == pygame.MOUSEBUTTONDOWN: + if game_event.button == 2: # middle mouse click + self.trackMouseMotion = True + elif game_event.button == 1: # left click + self.sceneManager.click_mesh(pygame.mouse.get_pos()) + elif game_event.type == pygame.MOUSEBUTTONUP: + if game_event.button == 2: # middle mouse release + self.trackMouseMotion = False + elif self.trackMouseMotion and game_event.type == pygame.MOUSEMOTION: + mouseMovement = pygame.mouse.get_rel() + current_scene = self.sceneManager.get_current_scene() + current_scene.euler_angles[0] -= mouseMovement[1] + current_scene.euler_angles[1] -= mouseMovement[0] + + def draw(self): + if self.updateOptions: + self.set_scene_options() + # make the preview window for the color picker + pygame.draw.rect(self.window, self.colorPicker.get_color(), self.rel2abs(11, 70, 5, 5)) + pygame.draw.rect(self.window, (0,0,0), self.rel2abs(11, 70, 5, 5), 2) + + self.sceneManager.draw() + + def _next_scene(self): + self.sceneManager.next_scene() + self._set_dropdown_options() + self.frame_counter_text.setText(str(self.sceneManager._current_scene_idx)) + + + def _last_scene(self): + self.sceneManager.last_scene() + self._set_dropdown_options() + self.frame_counter_text.setText(str(self.sceneManager._current_scene_idx)) + + def _set_dropdown_options(self): + # cursed method to set dropdown options because for some reason pygame_widgets dropdown doesn't allow you to manually set the dropdown option + scene = self.sceneManager.get_current_scene() + for choice in self.fill_dropdown._Dropdown__choices: + if choice.text.find(scene.fill) != -1: + self.fill_dropdown.chosen = choice + break + + for choice in self.fade_dropdown._Dropdown__choices: + if choice.text.find(scene.fade) != -1: + self.fade_dropdown.chosen = choice + break + + def set_update_options_flag(self): + self.updateOptions = True + + def set_scene_options(self): + self.updateOptions = False + delay_time = 1000 + try: + delay_time = int(self.delay_text_entry.getText()) + except ValueError: + self.delay_text_entry.setText(str(delay_time)) + self.sceneManager.update_scene_options(self.fill_dropdown.getSelected(), self.fade_dropdown.getSelected(), delay_time) \ No newline at end of file diff --git a/tools/animation-tools/animation-tool.py b/tools/animation-tools/animation-tool.py new file mode 100644 index 0000000..b642227 --- /dev/null +++ b/tools/animation-tools/animation-tool.py @@ -0,0 +1,37 @@ +import pygame +import pygame_widgets +from Shapes import * +from UI import AnimatorUI + +WINDOW_W = 500 +WINDOW_H = 500 + + +file_data = "" + +pygame.init() +window = pygame.display.set_mode((WINDOW_W, WINDOW_H)) + +ui = AnimatorUI(window) + +clock = pygame.time.Clock() +run = True + +while run: + clock.tick(60) + window.fill((255, 255, 255)) + + events = pygame.event.get() + for event in events: + if event.type == pygame.QUIT: + run = False + # if the event isn't handled above as a global event, let the ui handle it + else: + ui.update_interaction(event) + + pygame_widgets.update(events) + + ui.draw() + pygame.display.flip() + +pygame.quit() \ No newline at end of file diff --git a/tools/animation-tools/requirements.txt b/tools/animation-tools/requirements.txt new file mode 100644 index 0000000..4455b2b --- /dev/null +++ b/tools/animation-tools/requirements.txt @@ -0,0 +1,2 @@ +pygame==2.6.0 +pygame-widgets==1.1.5