Spaces:
Paused
Paused
| from kivy.app import App | |
| from kivy.uix.widget import Widget | |
| from kivy.uix.boxlayout import BoxLayout | |
| from kivy.uix.button import Button | |
| from kivy.uix.scatter import Scatter | |
| from kivy.uix.label import Label | |
| from kivy.uix.scrollview import ScrollView | |
| from kivy.properties import BooleanProperty, StringProperty, NumericProperty, ListProperty, ObjectProperty | |
| from kivy.uix.behaviors import ButtonBehavior | |
| from kivy.vector import Vector | |
| from kivy.metrics import dp | |
| from kivy.clock import Clock | |
| from kivy.uix.popup import Popup | |
| from kivy.config import Config | |
| Config.set('graphics', 'width', '1400') | |
| Config.set('graphics', 'height', '800') | |
| from kivy.core.window import Window | |
| import os | |
| from typing import List | |
| from deepsvg.svglib.geom import Point | |
| from deepsvg.svglib.svg_command import SVGCommandMove, SVGCommandLine, SVGCommandBezier | |
| from deepsvg.svgtensor_dataset import SVGTensorDataset | |
| from .layout.aligned_textinput import AlignedTextInput | |
| from .state.state import State, ToolMode, DrawMode, LoopMode, PlaybackMode | |
| from .state.project import Frame | |
| from .config import ROOT_DIR | |
| from .interpolate import compute_interpolation | |
| from .utils import * | |
| if not os.path.exists(ROOT_DIR): | |
| os.makedirs(ROOT_DIR) | |
| state = State() | |
| state.load_state() | |
| state.load_project() | |
| class HeaderIcon(Button): | |
| index = NumericProperty(0) | |
| source = StringProperty("") | |
| def on_press(self): | |
| state.header.selected_tool = self.index | |
| class Header(BoxLayout): | |
| selected_tool = NumericProperty(0) | |
| title = StringProperty(state.project.name) | |
| is_playing = BooleanProperty(False) | |
| delay = NumericProperty(state.delay) | |
| def on_selected_tool(self, *args): | |
| if self.selected_tool in [ToolMode.MOVE, ToolMode.PEN, ToolMode.PENCIL] and state.header.is_playing: | |
| state.header.pause_animation() | |
| def on_done(self, *args): | |
| if self.selected_tool == ToolMode.PEN and state.draw_mode == DrawMode.DRAW: | |
| path = state.current_path | |
| last_segment = path.children[-1] | |
| path.remove_widget(last_segment) | |
| state.draw_viewbox.on_path_done(state.current_path) | |
| state.draw_mode = DrawMode.STILL | |
| state.current_path = None | |
| self.selected_tool = ToolMode.MOVE | |
| def on_erase(self): | |
| state.modified = True | |
| state.draw_viewbox.clear() | |
| state.timeline.make_keyframe(False) | |
| def add_frame(self, keyframe=False): | |
| frame_idx = state.timeline._add_frame(keyframe=keyframe) | |
| state.project.frames.append(Frame(frame_idx, keyframe)) | |
| self.load_next_frame(frame_idx=frame_idx) | |
| def play_animation(self): | |
| self.is_playing = True | |
| state.sidebar.selected_path_idx = -1 | |
| self.clock = Clock.schedule_once(self.load_next_frame) | |
| def load_next_frame(self, dt=0, frame_idx=None, *args): | |
| if state.timeline.nb_frames > 0: | |
| if frame_idx is None: | |
| frame_idx_tmp = state.timeline.selected_frame + state.loop_orientation | |
| if frame_idx_tmp < 0 or frame_idx_tmp >= state.timeline.nb_frames: | |
| if state.loop_mode in [LoopMode.NORMAL, LoopMode.REVERSE]: | |
| frame_idx = frame_idx_tmp % state.timeline.nb_frames | |
| else: # LoopMode.PINGPONG | |
| state.loop_orientation *= -1 | |
| frame_idx = (state.timeline.selected_frame + state.loop_orientation) % state.timeline.nb_frames | |
| else: | |
| frame_idx = frame_idx_tmp | |
| state.timeline.selected_frame = frame_idx | |
| if self.is_playing: | |
| if state.playback_mode == PlaybackMode.EASE: | |
| t = frame_idx / state.timeline.nb_frames | |
| delay = 2 * state.delay / (1 + d_easein_easeout(t)) | |
| else: | |
| delay = state.delay | |
| self.clock = Clock.schedule_once(self.load_next_frame, delay) | |
| def pause_animation(self): | |
| self.clock.cancel() | |
| state.sidebar.selected_path_idx = -1 | |
| self.is_playing = False | |
| state.timeline.on_selected_frame() # re-render frame to display sidebar layers | |
| def on_title(self, title): | |
| state.project.name = title | |
| def interpolate(self): | |
| state.draw_viewbox.save_frame() | |
| compute_interpolation(state.project) | |
| class PathLayerView(ButtonBehavior, BoxLayout): | |
| index = NumericProperty(0) | |
| source = StringProperty("") | |
| def __init__(self, index, **kwargs): | |
| super().__init__(**kwargs) | |
| self.index = index | |
| self.source = os.path.join(state.project.cache_dir, f"{state.timeline.selected_frame}_{index}.png") | |
| def on_press(self): | |
| state.sidebar.selected_path_idx = self.index | |
| def move_up(self): | |
| if self.index > 0: | |
| state.sidebar.swap_paths(self.index, self.index - 1) | |
| def move_down(self): | |
| if self.index < state.sidebar.nb_paths - 1: | |
| state.sidebar.swap_paths(self.index, self.index + 1) | |
| def reverse(self): | |
| state.sidebar.reverse_path(self.index) | |
| class Sidebar(ScrollView): | |
| selected_path_idx = NumericProperty(-1) | |
| def sidebar(self): | |
| return self.ids.sidebar | |
| def nb_paths(self): | |
| return len(self.sidebar.children) | |
| def on_selected_path_idx(self, *args): | |
| state.draw_viewbox.unselect_all() | |
| if self.selected_path_idx >= 0: | |
| state.draw_viewbox.get_path(self.selected_path_idx).selected = True | |
| def _add_path(self, idx=None): | |
| if idx is None: | |
| idx = self.nb_paths | |
| new_pathlayer = PathLayerView(idx) | |
| self.sidebar.add_widget(new_pathlayer) | |
| return idx | |
| def get_path(self, path_idx): | |
| index = self.nb_paths - 1 - path_idx | |
| return self.sidebar.children[index] | |
| def erase(self): | |
| self.sidebar.clear_widgets() | |
| self.selected_path_idx = -1 | |
| def swap_paths(self, idx1, idx2): | |
| path_layer1, path_layer2 = self.get_path(idx1), self.get_path(idx2) | |
| path1, path2 = state.draw_viewbox.get_path(idx1), state.draw_viewbox.get_path(idx2) | |
| path_layer1.index, path_layer2.index = idx2, idx1 | |
| path1.color, path2.color = path2.color, path1.color | |
| path1.index, path2.index = path2.index, path1.index | |
| id1, id2 = self.nb_paths - 1 - idx1, self.nb_paths - 1 - idx2 | |
| self.sidebar.children[id1], self.sidebar.children[id2] = path_layer2, path_layer1 | |
| state.draw_viewbox.children[id1], state.draw_viewbox.children[id2] = path2, path1 | |
| self.selected_path_idx = idx2 | |
| state.modified = True | |
| def reverse_path(self, idx): | |
| path = state.draw_viewbox.get_path(idx) | |
| svg_path = path.to_svg_path().reverse() | |
| new_path = BezierPath.from_svg_path(svg_path, color=path.color, index=path.index, selected=path.selected) | |
| id = self.nb_paths - 1 - idx | |
| state.draw_viewbox.remove_widget(path) | |
| state.draw_viewbox.add_widget(new_path, index=id) | |
| self.selected_path_idx = idx | |
| state.modified = True | |
| def select(self, path_idx): | |
| if self.selected_path_idx >= 0: | |
| state.draw_viewbox.get_path(state.sidebar.selected_path_idx).selected = False | |
| self.selected_path_idx = path_idx | |
| class BezierSegment(Widget): | |
| is_curved = BooleanProperty(True) | |
| is_finished = BooleanProperty(True) | |
| select_dist = NumericProperty(3) | |
| p1 = ListProperty([0, 0]) | |
| q1 = ListProperty([0, 0]) | |
| q2 = ListProperty([0, 0]) | |
| p2 = ListProperty([0, 0]) | |
| def clone(self): | |
| segment = BezierSegment() | |
| segment.is_curved = self.is_curved | |
| segment.p1 = self.p1 # shallow copy | |
| segment.q1 = self.q1 | |
| segment.q2 = self.q2 | |
| segment.p2 = self.p2 | |
| return segment | |
| def line(p1, p2): | |
| segment = BezierSegment() | |
| segment.is_curved = False | |
| segment.p1 = segment.q1 = p1 | |
| segment.p2 = segment.q2 = p2 | |
| return segment | |
| def bezier(p1, q1, q2, p2): | |
| segment = BezierSegment() | |
| segment.is_curved = True | |
| segment.q1, segment.q2 = q1, q2 | |
| segment.p1, segment.p2 = p1, p2 | |
| return segment | |
| def get_point(self, key): | |
| return getattr(self, key) | |
| def on_touch_down(self, touch): | |
| max_dist = dp(self.select_dist) | |
| if not self.parent.selected: | |
| return super().on_touch_down(touch) | |
| keys_to_test = ["p1", "q1", "q2", "p2"] if self.is_curved else ["p1", "p2"] | |
| for key in keys_to_test: | |
| if dist(touch.pos, getattr(self, key)) < max_dist: | |
| touch.ud['selected'] = key | |
| touch.grab(self) | |
| state.modified = True | |
| return True | |
| def on_touch_move(self, touch): | |
| if touch.grab_current is not self: | |
| return super().on_touch_move(touch) | |
| key = touch.ud['selected'] | |
| setattr(self, key, touch.pos) | |
| if state.header.selected_tool == ToolMode.PEN: | |
| self.is_curved = True | |
| self.is_finished = False | |
| state.draw_mode = DrawMode.HOLDING_DOWN | |
| setattr(self, "p2", touch.pos) | |
| if key in ["p1", "p2"]: | |
| self.parent.move(self, key, touch.pos) | |
| def on_touch_up(self, touch): | |
| if touch.grab_current is not self: | |
| return super().on_touch_up(touch) | |
| touch.ungrab(self) | |
| if state.header.selected_tool == ToolMode.PEN: | |
| self.is_finished = True | |
| state.draw_mode = DrawMode.DRAW | |
| class BezierPath(Widget): | |
| color = ListProperty([1, 1, 1]) | |
| index = NumericProperty(0) | |
| selected = BooleanProperty(False) | |
| def __init__(self, segments: List[BezierSegment], color=None, index=None, selected=False, **kwargs): | |
| super().__init__(**kwargs) | |
| if color is not None: | |
| self.color = color | |
| if index is not None: | |
| self.index = index | |
| self.selected = selected | |
| for segment in segments: | |
| self.add_segment(segment) | |
| def clone(self): | |
| segments = [segment.clone() for segment in self.children] | |
| return BezierPath(segments, self.color, self.index, self.selected) | |
| def add_segment(self, segment: BezierSegment): | |
| self.add_widget(segment, index=len(self.children)) | |
| def move(self, segment, key, pos): | |
| idx = self.children.index(segment) | |
| if not (idx == 0 and key == "p1") and not (idx == len(self.children) - 1 and key == "p2"): | |
| idx2, key2 = (idx-1, "p2") if key == "p1" else (idx+1, "p1") | |
| setattr(self.children[idx2], key2, pos) | |
| def add_widget(self, widget, index=0, canvas=None): | |
| super().add_widget(widget, index=index, canvas=canvas) | |
| def remove_widget(self, widget): | |
| super().remove_widget(widget) | |
| def from_svg_path(svg_path: SVGPath, *args, **kwargs): | |
| segments = [] | |
| for command in svg_path.path_commands: | |
| if isinstance(command, SVGCommandBezier): | |
| segment = BezierSegment.bezier(flip_vertical(command.p1.tolist()), flip_vertical(command.q1.tolist()), | |
| flip_vertical(command.q2.tolist()), flip_vertical(command.p2.tolist())) | |
| segments.append(segment) | |
| elif isinstance(command, SVGCommandLine): | |
| segment = BezierSegment.line(flip_vertical(command.start_pos.tolist()), | |
| flip_vertical(command.end_pos.tolist())) | |
| segments.append(segment) | |
| path = BezierPath(segments, *args, **kwargs) | |
| return path | |
| def to_svg_path(self): | |
| path_commands = [] | |
| for segment in self.children: | |
| if segment.is_curved: | |
| command = SVGCommandBezier(Point(*flip_vertical(segment.p1)), Point(*flip_vertical(segment.q1)), | |
| Point(*flip_vertical(segment.q2)), Point(*flip_vertical(segment.p2))) | |
| else: | |
| command = SVGCommandLine(Point(*flip_vertical(segment.p1)), Point(*flip_vertical(segment.p2))) | |
| path_commands.append(command) | |
| svg_path = SVGPath(path_commands) | |
| return svg_path | |
| class Sketch(Widget): | |
| color = ListProperty([1, 1, 1]) | |
| points = ListProperty([]) | |
| def __init__(self, points, color=None, **kwargs): | |
| super().__init__(**kwargs) | |
| if color is not None: | |
| self.color = color | |
| self.points = points | |
| def on_touch_move(self, touch): | |
| if touch.grab_current is not self: | |
| return super().on_touch_move(touch) | |
| self.points.extend(touch.pos) | |
| def on_touch_up(self, touch): | |
| if touch.grab_current is not self: | |
| return super().on_touch_up(touch) | |
| touch.ungrab(self) | |
| self.parent.on_sketch_done(self) | |
| def to_svg_path(self): | |
| points = [Point(x, 255 - y) for x, y in zip(self.points[::2], self.points[1::2])] | |
| commands = [SVGCommandMove(points[0])] + [SVGCommandLine(p1, p2) for p1, p2 in zip(points[:-1], points[1:])] | |
| svg_path = SVGPath.from_commands(commands).path | |
| return svg_path | |
| class EditorView(Scatter): | |
| def on_touch_down(self, touch): | |
| if self.collide_point(*touch.pos) and touch.is_mouse_scrolling: | |
| if touch.button == 'scrolldown': | |
| if self.scale < 10: | |
| self.scale = self.scale * 1.1 | |
| elif touch.button == 'scrollup': | |
| if self.scale > 1: | |
| self.scale = self.scale * 0.8 | |
| return True | |
| return super().on_touch_down(touch) | |
| class DrawViewbox(Widget): | |
| def __init__(self, **kwargs): | |
| super().__init__(**kwargs) | |
| Window.bind(mouse_pos=self.on_mouse_pos) | |
| def nb_paths(self): | |
| return len(self.children) | |
| def _get_color(self, idx): | |
| color = color_dict[colors[idx % len(colors)]] | |
| return color | |
| def on_mouse_pos(self, _, abs_pos): | |
| pos = (Vector(abs_pos) - Vector(self.parent.pos)) / self.parent.scale | |
| if state.header.selected_tool == ToolMode.PEN and state.draw_mode == DrawMode.DRAW: | |
| segment = state.current_path.children[-1] | |
| segment.p2 = segment.q2 = pos | |
| def on_sketch_done(self, sketch: Sketch): | |
| # Digitalize points to Bézier path | |
| svg_path = preprocess_svg_path(sketch.to_svg_path(), force_smooth=True) | |
| path_idx = state.sidebar.nb_paths | |
| path = BezierPath.from_svg_path(svg_path, color=sketch.color, index=path_idx, selected=True) | |
| self.remove_widget(sketch) | |
| self.add_new_path(path, svg_path) | |
| def on_path_done(self, path: BezierPath): | |
| svg_path = preprocess_svg_path(path.to_svg_path()) | |
| path_idx = state.sidebar.nb_paths | |
| new_path = BezierPath.from_svg_path(svg_path, color=path.color, index=path_idx, selected=True) | |
| self.remove_widget(path) | |
| self.add_new_path(new_path, svg_path) | |
| def paste(self, path: BezierPath): | |
| path = path.clone() | |
| path_idx = state.sidebar.nb_paths | |
| path.color = self._get_color(path_idx) | |
| path.selected = True | |
| svg_path = path.to_svg_path() | |
| self.add_new_path(path, svg_path) | |
| def unselect_all(self): | |
| for path in self.children: | |
| path.selected = False | |
| def get_path(self, path_idx): | |
| index = self.nb_paths - 1 - path_idx | |
| return self.children[index] | |
| def add_new_path(self, path: BezierSegment, svg_path: SVGPath): | |
| self.add_path(path, svg_path, force_rerender_miniature=True) | |
| state.modified = True | |
| state.timeline.make_keyframe(True) | |
| state.sidebar.select(path.index) | |
| def add_path(self, path: BezierPath, svg_path: SVGPath, force_rerender_miniature=False): | |
| path_idx = state.sidebar.nb_paths | |
| self.add_widget(path) | |
| miniature_path = os.path.join(state.project.cache_dir, f"{state.timeline.selected_frame}_{path_idx}.png") | |
| if not os.path.exists(miniature_path) or force_rerender_miniature: | |
| svg_path = normalized_path(svg_path) | |
| svg_path.draw(viewbox=svg_path.bbox().make_square(min_size=12), | |
| file_path=os.path.join(state.project.cache_dir, f"{state.timeline.selected_frame}_{path_idx}.png"), | |
| do_display=False) | |
| if not state.header.is_playing: | |
| state.sidebar._add_path() | |
| def on_touch_down(self, touch): | |
| if state.header.selected_tool == ToolMode.PLAY: | |
| return False | |
| if state.header.selected_tool == ToolMode.PEN and self.collide_point(*touch.pos): | |
| state.draw_mode = DrawMode.DRAW | |
| if state.current_path is None: | |
| path = BezierPath([], color=self._get_color(len(self.children)), selected=True) | |
| self.add_widget(path) | |
| state.current_path = path | |
| l = BezierSegment.line(touch.pos, touch.pos) | |
| touch.ud["selected"] = "q1" | |
| touch.grab(l) | |
| state.current_path.add_segment(l) | |
| state.modified = True | |
| return True | |
| if state.header.selected_tool == ToolMode.PENCIL and self.collide_point(*touch.pos): | |
| l = Sketch([*touch.pos], color=self._get_color(len(self.children))) | |
| self.add_widget(l) | |
| touch.grab(l) | |
| state.modified = True | |
| return True | |
| if super().on_touch_down(touch): | |
| return True | |
| def clear(self): | |
| state.draw_viewbox.clear_widgets() | |
| state.sidebar.erase() | |
| def add_widget(self, widget, index=0, canvas=None): | |
| super().add_widget(widget, index=index, canvas=canvas) | |
| def remove_widget(self, widget): | |
| super().remove_widget(widget) | |
| def to_svg(self): | |
| svg_path_groups = [] | |
| for path in reversed(self.children): | |
| svg_path_groups.append(path.to_svg_path().to_group()) | |
| svg = SVG(svg_path_groups, viewbox=Bbox(256)) | |
| return svg | |
| def load_svg(self, svg: SVG, frame_idx): | |
| kivy_bezierpaths = [] | |
| for idx, svg_path in enumerate(svg.paths): | |
| path = BezierPath.from_svg_path(svg_path, color=self._get_color(idx), index=idx, selected=False) | |
| kivy_bezierpaths.append(path) | |
| self.add_path(path, svg_path, force_rerender_miniature=True) | |
| state.project.frames[frame_idx].svg = svg | |
| state.project.frames[frame_idx].kivy_bezierpaths = kivy_bezierpaths | |
| def load_cached(self, svg: SVG, kivy_bezierpaths: List[BezierPath]): | |
| for path, svg_path in zip(kivy_bezierpaths, svg.paths): | |
| self.add_path(path, svg_path) | |
| def load_frame(self, frame_idx): | |
| svg = state.project.frames[frame_idx].svg | |
| kivy_bezierpaths = state.project.frames[frame_idx].kivy_bezierpaths | |
| if kivy_bezierpaths is None: | |
| self.load_svg(svg, frame_idx) | |
| else: | |
| self.load_cached(svg, kivy_bezierpaths) | |
| self.unselect_all() | |
| def save_frame(self): | |
| svg = self.to_svg() | |
| state.project.frames[state.current_frame].svg = svg | |
| state.project.frames[state.current_frame].kivy_bezierpaths = [child for child in reversed(self.children) if isinstance(child, BezierPath)] | |
| class HeaderButton(Button): | |
| pass | |
| class UpButton(Button): | |
| def on_press(self): | |
| self.parent.move_up() | |
| class DownButton(Button): | |
| def on_press(self): | |
| self.parent.move_down() | |
| class ReverseButton(Button): | |
| def on_press(self): | |
| self.parent.reverse() | |
| class FrameView(Button): | |
| index = NumericProperty(0) | |
| keyframe = BooleanProperty(False) | |
| def __init__(self, index, keyframe=False, **kwargs): | |
| super().__init__(**kwargs) | |
| self.index = index | |
| self.keyframe = keyframe | |
| def on_press(self): | |
| state.timeline.selected_frame = self.index | |
| class TimeLine(ScrollView): | |
| selected_frame = NumericProperty(-1) | |
| def timeline(self): | |
| return self.ids.timeline | |
| def nb_frames(self): | |
| return len(self.timeline.children) | |
| def on_selected_frame(self, *args): | |
| self._update_frame(self.selected_frame) | |
| def _update_frame(self, new_frame_idx): | |
| if state.current_frame >= 0 and state.modified: | |
| state.draw_viewbox.save_frame() | |
| state.current_frame = new_frame_idx | |
| state.draw_viewbox.clear() | |
| state.modified = False | |
| state.draw_viewbox.load_frame(new_frame_idx) | |
| def _add_frame(self, keyframe=False): | |
| idx = self.nb_frames | |
| new_frame = FrameView(idx, keyframe=keyframe) | |
| self.timeline.add_widget(new_frame) | |
| return idx | |
| def get_frame(self, frame_idx): | |
| index = self.nb_frames - 1 - frame_idx | |
| return self.timeline.children[index] | |
| def make_keyframe(self, is_keyframe=None): | |
| if is_keyframe is None: | |
| is_keyframe = not self.get_frame(state.timeline.selected_frame).keyframe | |
| self.get_frame(state.timeline.selected_frame).keyframe = is_keyframe | |
| state.project.frames[state.timeline.selected_frame].keyframe = is_keyframe | |
| class TitleWidget(AlignedTextInput): | |
| pass | |
| class Padding(Label): | |
| pass | |
| class FileChoosePopup(Popup): | |
| load = ObjectProperty() | |
| path = StringProperty(".") | |
| class DeepSVGWidget(BoxLayout): | |
| def __init__(self, **kwargs): | |
| super().__init__(**kwargs) | |
| state.main_widget = self | |
| state.header = self.ids.header | |
| state.sidebar = self.ids.sidebar_scroll | |
| state.draw_viewbox = self.ids.editor.ids.draw_viewbox | |
| state.timeline = self.ids.timeline_scroll | |
| self._load_project() | |
| def _load_project(self): | |
| for frame in state.project.frames: | |
| state.timeline._add_frame(keyframe=frame.keyframe) | |
| state.timeline.selected_frame = 0 | |
| class DeepSVGApp(App): | |
| def build(self): | |
| self.title = 'DeepSVG Editor' | |
| Window.bind(on_request_close=self.on_request_close) | |
| Window.bind(on_keyboard=self.on_keyboard) | |
| return DeepSVGWidget() | |
| def save(self): | |
| state.draw_viewbox.save_frame() | |
| state.save_state() | |
| state.project.save_project() | |
| def on_request_close(self, *args, **kwargs): | |
| self.save() | |
| self.stop() | |
| def on_keyboard(self, window, key, scancode, codepoint, modifier): | |
| CTRL_PRESSED = (modifier == ['ctrl'] or modifier == ['meta']) | |
| if codepoint == "h" and not CTRL_PRESSED: | |
| # Hand tool | |
| state.header.selected_tool = ToolMode.MOVE | |
| elif codepoint == "p" and not CTRL_PRESSED: | |
| # Pen tool | |
| state.header.selected_tool = ToolMode.PEN | |
| elif CTRL_PRESSED and codepoint == "p": | |
| # Pencil tool | |
| state.header.selected_tool = ToolMode.PENCIL | |
| elif codepoint == "k" and not CTRL_PRESSED: | |
| # Make keypoint | |
| state.timeline.make_keyframe() | |
| elif CTRL_PRESSED and codepoint == 'q': | |
| # Quit | |
| self.on_request_close() | |
| elif CTRL_PRESSED and codepoint == 'i': | |
| # Import | |
| self.file_chooser = FileChoosePopup(load=self.on_file_chosen) | |
| self.file_chooser.open() | |
| elif CTRL_PRESSED and codepoint == "e": | |
| # Export | |
| state.project.export_to_gif(loop_mode=state.loop_mode) | |
| elif CTRL_PRESSED and codepoint == 'c': | |
| # Copy | |
| if state.sidebar.selected_path_idx >= 0: | |
| state.clipboard = state.draw_viewbox.get_path(state.sidebar.selected_path_idx).clone() | |
| elif CTRL_PRESSED and codepoint == 'v': | |
| # Paste | |
| if isinstance(state.clipboard, BezierPath): | |
| state.draw_viewbox.paste(state.clipboard) | |
| elif CTRL_PRESSED and codepoint == 's': | |
| # Save | |
| self.save() | |
| elif key == Keys.SPACEBAR: | |
| # Play/Pause | |
| state.header.selected_tool = ToolMode.PLAY | |
| if state.header.is_playing: | |
| state.header.pause_animation() | |
| else: | |
| state.header.play_animation() | |
| elif key == Keys.LEFT: | |
| # Previous frame | |
| if state.current_frame > 0: | |
| state.timeline.selected_frame = state.current_frame - 1 | |
| elif key == Keys.RIGHT: | |
| # Next frame | |
| if state.current_frame < state.timeline.nb_frames - 1: | |
| state.timeline.selected_frame = state.current_frame + 1 | |
| def on_file_chosen(self, selection): | |
| file_path = str(selection[0]) | |
| self.file_chooser.dismiss() | |
| if file_path: | |
| if not file_path.endswith(".svg"): | |
| return | |
| svg = SVG.load_svg(file_path) | |
| svg = SVGTensorDataset.simplify(svg) | |
| svg = SVGTensorDataset.preprocess(svg, mean=True) | |
| state.draw_viewbox.load_svg(svg, frame_idx=state.timeline.selected_frame) | |
| state.modified = True | |
| state.timeline.make_keyframe(True) | |
| if __name__ == "__main__": | |
| DeepSVGApp().run() | |