diff --git a/src/Classes/Project.gd b/src/Classes/Project.gd index 3016ad133..d4eb544cb 100644 --- a/src/Classes/Project.gd +++ b/src/Classes/Project.gd @@ -3,6 +3,7 @@ class_name Project extends RefCounted ## A class for project properties. +signal removed signal serialized(dict: Dictionary) signal about_to_deserialize(dict: Dictionary) signal resized @@ -137,6 +138,7 @@ func remove() -> void: # Prevents memory leak (due to the layers' project reference stopping ref counting from freeing) layers.clear() Global.projects.erase(self) + removed.emit() func remove_backup_file() -> void: diff --git a/src/UI/Recorder/Recorder.gd b/src/UI/Recorder/Recorder.gd index f2b611b9e..7b8e84f68 100644 --- a/src/UI/Recorder/Recorder.gd +++ b/src/UI/Recorder/Recorder.gd @@ -1,17 +1,17 @@ +class_name RecorderPanel extends PanelContainer -signal frame_saved - enum Mode { CANVAS, PIXELORAMA } var mode := Mode.CANVAS -var chosen_dir := "" +var chosen_dir := "": + set(value): + chosen_dir = value + if chosen_dir.ends_with("/"): # Remove end back-slashes if present + chosen_dir[-1] = "" +var recorded_projects := {} ## [Dictionary] of [Project] and [Recorder]. var save_dir := "" -var project: Project -var cache: Array[Image] = [] ## Images stored during recording -var frame_captured := 0 ## Used to visualize frames captured -var skip_amount := 1 ## Number of "do" actions after which a frame can be captured -var current_frame_no := 0 ## Used to compare with skip_amount to see if it can be captured +var skip_amount := 1 ## Number of "do" actions after which a frame can be captured. var resize_percent := 100 var _path_dialog: FileDialog: get: @@ -28,7 +28,6 @@ var _path_dialog: FileDialog: return _path_dialog @onready var captured_label := %CapturedLabel as Label -@onready var project_list := $"%TargetProjectOption" as OptionButton @onready var start_button := $"%Start" as Button @onready var size_label := $"%Size" as Label @onready var path_field := $"%Path" as LineEdit @@ -36,125 +35,96 @@ var _path_dialog: FileDialog: @onready var options_container := %OptionsContainer as VBoxContainer +class Recorder: + var project: Project + var recorder_panel: RecorderPanel + var actions_done := -1 + var frames_captured := 0 + var save_directory := "" + + func _init(_project: Project, _recorder_panel: RecorderPanel) -> void: + project = _project + recorder_panel = _recorder_panel + # Create a new directory based on time + var time_dict := Time.get_time_dict_from_system() + var folder := str( + project.name, time_dict.hour, "_", time_dict.minute, "_", time_dict.second + ) + var dir := DirAccess.open(recorder_panel.chosen_dir) + save_directory = recorder_panel.chosen_dir.path_join(folder) + dir.make_dir_recursive(save_directory) + project.removed.connect(recorder_panel.finalize_recording.bind(project)) + project.undo_redo.version_changed.connect(capture_frame) + + func _notification(what: int) -> void: + if what == NOTIFICATION_PREDELETE: + # Needed so that the project won't be forever remained in memory because of bind(). + project.removed.disconnect(recorder_panel.finalize_recording) + + func capture_frame() -> void: + actions_done += 1 + if actions_done % recorder_panel.skip_amount != 0: + return + var image: Image + if recorder_panel.mode == RecorderPanel.Mode.PIXELORAMA: + image = recorder_panel.get_window().get_texture().get_image() + else: + var frame := project.frames[project.current_frame] + image = Image.create(project.size.x, project.size.y, false, Image.FORMAT_RGBA8) + DrawingAlgos.blend_layers(image, frame, Vector2i.ZERO, project) + + if recorder_panel.resize_percent != 100: + var resize := recorder_panel.resize_percent / 100 + var new_width := image.get_width() * resize + var new_height := image.get_height() * resize + image.resize(new_width, new_height, Image.INTERPOLATE_NEAREST) + var save_file := str(project.name, "_", frames_captured, ".png") + image.save_png(save_directory.path_join(save_file)) + frames_captured += 1 + recorder_panel.captured_label.text = str("Saved: ", frames_captured) + + func _ready() -> void: - refresh_projects_list() - project = Global.current_project - frame_saved.connect(_on_frame_saved) + Global.project_switched.connect(_on_project_switched) # Make a recordings folder if there isn't one chosen_dir = Global.home_data_directory.path_join("Recordings") DirAccess.make_dir_recursive_absolute(chosen_dir) path_field.text = chosen_dir - size_label.text = str("(", project.size.x, "×", project.size.y, ")") + + +func _on_project_switched() -> void: + if recorded_projects.has(Global.current_project): + initialize_recording() + start_button.set_pressed_no_signal(true) + Global.change_button_texturerect(start_button.get_child(0), "stop.png") + else: + finalize_recording() + start_button.set_pressed_no_signal(false) + Global.change_button_texturerect(start_button.get_child(0), "start.png") func initialize_recording() -> void: - connect_undo() # connect to detect changes in project - cache.clear() # clear the cache array to store new images - frame_captured = 0 - current_frame_no = skip_amount - 1 - # disable some options that are not required during recording - project_list.visible = false captured_label.visible = true for child in options_container.get_children(): if !child.is_in_group("visible during recording"): child.visible = false - save_dir = chosen_dir - # Remove end back-slashes if present - if save_dir.ends_with("/"): - save_dir[-1] = "" - # Create a new directory based on time - var time_dict := Time.get_time_dict_from_system() - var folder := str(project.name, time_dict.hour, "_", time_dict.minute, "_", time_dict.second) - var dir := DirAccess.open(save_dir) - save_dir = save_dir.path_join(folder) - dir.make_dir_recursive(save_dir) - - capture_frame() # capture first frame - $Timer.start() - - -func capture_frame() -> void: - current_frame_no += 1 - if current_frame_no != skip_amount: - return - current_frame_no = 0 - var image: Image - if mode == Mode.PIXELORAMA: - image = get_tree().root.get_viewport().get_texture().get_image() - else: - var frame := project.frames[project.current_frame] - image = Image.create(project.size.x, project.size.y, false, Image.FORMAT_RGBA8) - DrawingAlgos.blend_layers(image, frame, Vector2i.ZERO, project) - - if mode == Mode.CANVAS: - if resize_percent != 100: - var resize := resize_percent / 100 - image.resize( - image.get_width() * resize, image.get_height() * resize, Image.INTERPOLATE_NEAREST - ) - - cache.append(image) - - -func _on_Timer_timeout() -> void: - # Saves frames little by little during recording - if cache.size() > 0: - save_frame(cache[0]) - cache.remove_at(0) - - -func save_frame(img: Image) -> void: - var save_file := str(project.name, "_", frame_captured, ".png") - img.save_png(save_dir.path_join(save_file)) - frame_saved.emit() - - -func _on_frame_saved() -> void: - frame_captured += 1 - captured_label.text = str("Saved: ", frame_captured) - - -func finalize_recording() -> void: - $Timer.stop() - for img in cache: - save_frame(img) - cache.clear() - disconnect_undo() - project_list.visible = true - captured_label.visible = false - for child in options_container.get_children(): - child.visible = true - if mode == Mode.PIXELORAMA: - size_label.get_parent().visible = false - - -func disconnect_undo() -> void: - project.undo_redo.version_changed.disconnect(capture_frame) - - -func connect_undo() -> void: - project.undo_redo.version_changed.connect(capture_frame) - - -func _on_TargetProjectOption_item_selected(index: int) -> void: - project = Global.projects[index] - - -func _on_TargetProjectOption_pressed() -> void: - refresh_projects_list() - - -func refresh_projects_list() -> void: - project_list.clear() - for proj in Global.projects: - project_list.add_item(proj.name) +func finalize_recording(project := Global.current_project) -> void: + if recorded_projects.has(project): + recorded_projects.erase(project) + if project == Global.current_project: + captured_label.visible = false + for child in options_container.get_children(): + child.visible = true + if mode == Mode.PIXELORAMA: + size_label.get_parent().visible = false func _on_Start_toggled(button_pressed: bool) -> void: if button_pressed: + recorded_projects[Global.current_project] = Recorder.new(Global.current_project, self) initialize_recording() Global.change_button_texturerect(start_button.get_child(0), "stop.png") else: @@ -163,7 +133,8 @@ func _on_Start_toggled(button_pressed: bool) -> void: func _on_Settings_pressed() -> void: - options_dialog.popup_on_parent(Rect2(position, options_dialog.size)) + _on_SpinBox_value_changed(resize_percent) + options_dialog.popup_on_parent(Rect2i(position, options_dialog.size)) func _on_SkipAmount_value_changed(value: float) -> void: @@ -181,7 +152,7 @@ func _on_Mode_toggled(button_pressed: bool) -> void: func _on_SpinBox_value_changed(value: float) -> void: resize_percent = value - var new_size: Vector2 = project.size * (resize_percent / 100.0) + var new_size: Vector2 = Global.current_project.size * (resize_percent / 100.0) size_label.text = str("(", new_size.x, "×", new_size.y, ")") diff --git a/src/UI/Recorder/Recorder.tscn b/src/UI/Recorder/Recorder.tscn index 050d8af9c..5204fcf88 100644 --- a/src/UI/Recorder/Recorder.tscn +++ b/src/UI/Recorder/Recorder.tscn @@ -31,13 +31,6 @@ unique_name_in_owner = true visible = false layout_mode = 2 -[node name="TargetProjectOption" type="OptionButton" parent="ScrollContainer/CenterContainer/GridContainer"] -unique_name_in_owner = true -custom_minimum_size = Vector2(100, 0) -layout_mode = 2 -tooltip_text = "Choose project" -clip_text = true - [node name="Start" type="Button" parent="ScrollContainer/CenterContainer/GridContainer" groups=["UIButtons"]] unique_name_in_owner = true custom_minimum_size = Vector2(32, 32) @@ -143,6 +136,8 @@ text = "Capture frame every" [node name="SkipAmount" type="SpinBox" parent="OptionsDialog/PanelContainer/OptionsContainer/ActionGap"] layout_mode = 2 size_flags_horizontal = 3 +min_value = 1.0 +value = 1.0 suffix = "actions" [node name="ModeHeader" type="HBoxContainer" parent="OptionsDialog/PanelContainer/OptionsContainer" groups=["visible during recording"]] @@ -223,10 +218,6 @@ editable = false layout_mode = 2 text = "Choose" -[node name="Timer" type="Timer" parent="."] - -[connection signal="item_selected" from="ScrollContainer/CenterContainer/GridContainer/TargetProjectOption" to="." method="_on_TargetProjectOption_item_selected"] -[connection signal="pressed" from="ScrollContainer/CenterContainer/GridContainer/TargetProjectOption" to="." method="_on_TargetProjectOption_pressed"] [connection signal="toggled" from="ScrollContainer/CenterContainer/GridContainer/Start" to="." method="_on_Start_toggled"] [connection signal="pressed" from="ScrollContainer/CenterContainer/GridContainer/Settings" to="." method="_on_Settings_pressed"] [connection signal="pressed" from="ScrollContainer/CenterContainer/GridContainer/OpenFolder" to="." method="_on_open_folder_pressed"] @@ -234,4 +225,3 @@ text = "Choose" [connection signal="toggled" from="OptionsDialog/PanelContainer/OptionsContainer/ModeType/Mode" to="." method="_on_Mode_toggled"] [connection signal="value_changed" from="OptionsDialog/PanelContainer/OptionsContainer/OutputScale/Resize" to="." method="_on_SpinBox_value_changed"] [connection signal="pressed" from="OptionsDialog/PanelContainer/OptionsContainer/PathContainer/Choose" to="." method="_on_Choose_pressed"] -[connection signal="timeout" from="Timer" to="." method="_on_Timer_timeout"]