Skip to content

Commit

Permalink
Reference Images (#771)
Browse files Browse the repository at this point in the history
* Reference Images: Part 1

* Reference Images: Proper import workflow

* Reference Images: gdformat/gdlint checks

* Reference Images: Remove random src/Main.tscn changes

* Reference Images: Switch to ValueSliders, autowraps, fix opacity going out of range
  • Loading branch information
20kdc authored Nov 8, 2022
1 parent 82acf3f commit 20bce81
Show file tree
Hide file tree
Showing 11 changed files with 361 additions and 25 deletions.
12 changes: 12 additions & 0 deletions project.godot
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,16 @@ _global_script_classes=[ {
"language": "GDScript",
"path": "res://src/Classes/Project.gd"
}, {
"base": "Sprite",
"class": "ReferenceImage",
"language": "GDScript",
"path": "res://src/UI/Canvas/ReferenceImage.gd"
}, {
"base": "VBoxContainer",
"class": "ReferencesPanel",
"language": "GDScript",
"path": "res://src/UI/ReferencesPanel.gd"
}, {
"base": "Image",
"class": "SelectionMap",
"language": "GDScript",
Expand Down Expand Up @@ -201,6 +211,8 @@ _global_script_class_icons={
"PixelCel": "",
"PixelLayer": "",
"Project": "",
"ReferenceImage": "",
"ReferencesPanel": "",
"SelectionMap": "",
"SelectionTool": "",
"ShaderImageEffect": "",
Expand Down
2 changes: 2 additions & 0 deletions src/Autoload/Global.gd
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,8 @@ onready var brushes_popup: Popup = control.find_node("BrushesPopup")
onready var patterns_popup: Popup = control.find_node("PatternsPopup")
onready var palette_panel: PalettePanel = control.find_node("Palettes")

onready var references_panel: ReferencesPanel = control.find_node("References")

onready var top_menu_container: Panel = control.find_node("TopMenuContainer")
onready var rotation_level_button: Button = control.find_node("RotationLevel")
onready var rotation_level_spinbox: SpinBox = control.find_node("RotationSpinbox")
Expand Down
9 changes: 9 additions & 0 deletions src/Autoload/OpenSave.gd
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,15 @@ func open_image_as_new_layer(image: Image, file_name: String, frame_index := 0)
project.undo_redo.commit_action()


func import_reference_image_from_path(path: String):
var project: Project = Global.current_project
var ri := ReferenceImage.new()
ri.project = project
ri.deserialize({"image_path": path})
Global.canvas.add_child(ri)
project.change_project()


func set_new_imported_tab(project: Project, path: String) -> void:
var prev_project_empty: bool = Global.current_project.is_empty()
var prev_project_pos: int = Global.current_project_index
Expand Down
15 changes: 15 additions & 0 deletions src/Classes/Project.gd
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ var selected_cels := [[0, 0]] # Array of Arrays of 2 integers (frame & layer)
var animation_tags := [] setget _animation_tags_changed # Array of AnimationTags
var guides := [] # Array of Guides
var brushes := [] # Array of Images
var reference_images := [] # Array of ReferenceImages
var fps := 6.0

var x_symmetry_point
Expand Down Expand Up @@ -88,6 +89,8 @@ func _init(_frames := [], _name := tr("untitled"), _size := Vector2(64, 64)) ->

func remove() -> void:
undo_redo.free()
for ri in reference_images:
ri.queue_free()
for guide in guides:
guide.queue_free()
# Prevents memory leak (due to the layers' project reference stopping ref counting from freeing)
Expand Down Expand Up @@ -179,6 +182,7 @@ func change_project() -> void:
Global.animation_timeline.fps_spinbox.value = fps
Global.horizontal_ruler.update()
Global.vertical_ruler.update()
Global.references_panel.project_changed()
Global.cursor_position_label.text = "[%s×%s]" % [size.x, size.y]

Global.window_title = "%s - Pixelorama %s" % [name, Global.current_version]
Expand Down Expand Up @@ -292,6 +296,10 @@ func serialize() -> Dictionary:
for brush in brushes:
brush_data.append({"size_x": brush.get_size().x, "size_y": brush.get_size().y})

var reference_image_data := []
for reference_image in reference_images:
reference_image_data.append(reference_image.serialize())

var tile_mask_data := {
"size_x": tiles.tile_mask.get_size().x, "size_y": tiles.tile_mask.get_size().y
}
Expand All @@ -316,6 +324,7 @@ func serialize() -> Dictionary:
"symmetry_points": [x_symmetry_point, y_symmetry_point],
"frames": frame_data,
"brushes": brush_data,
"reference_images": reference_image_data,
"export_directory_path": directory_path,
"export_file_name": file_name,
"export_file_format": file_format,
Expand Down Expand Up @@ -397,6 +406,12 @@ func deserialize(dict: Dictionary) -> void:
guide.has_focus = false
guide.project = self
Global.canvas.add_child(guide)
if dict.has("reference_images"):
for g in dict.reference_images:
var ri := ReferenceImage.new()
ri.project = self
ri.deserialize(g)
Global.canvas.add_child(ri)
if dict.has("symmetry_points"):
x_symmetry_point = dict.symmetry_points[0]
y_symmetry_point = dict.symmetry_points[1]
Expand Down
81 changes: 81 additions & 0 deletions src/UI/Canvas/ReferenceImage.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
class_name ReferenceImage
extends Sprite
# A class describing a reference image

signal properties_changed

var project = Global.current_project

var image_path: String = ""


func _ready() -> void:
project.reference_images.append(self)


func change_properties():
emit_signal("properties_changed")


# Resets the position and scale of the reference image.
func position_reset():
position = project.size / 2.0
if texture != null:
scale = (
Vector2.ONE
* min(project.size.x / texture.get_width(), project.size.y / texture.get_height())
)
else:
scale = Vector2.ONE


# Serialize details of the reference image.
func serialize():
return {
"x": position.x,
"y": position.y,
"scale_x": scale.x,
"scale_y": scale.y,
"modulate_r": modulate.r,
"modulate_g": modulate.g,
"modulate_b": modulate.b,
"modulate_a": modulate.a,
"image_path": image_path
}


# Load details of the reference image from a dictionary.
# Be aware that new ReferenceImages are created via deserialization.
# This is because deserialization sets up some nice defaults.
func deserialize(d: Dictionary):
modulate = Color(1, 1, 1, 0.5)
if d.has("image_path"):
# Note that reference images are referred to by path.
# These images may be rather big.
# Also
image_path = d["image_path"]
var img = Image.new()
if img.load(image_path) == OK:
var itex = ImageTexture.new()
# don't do FLAG_REPEAT - it could cause visual issues
itex.create_from_image(img, Texture.FLAG_MIPMAPS | Texture.FLAG_FILTER)
texture = itex
# Now that the image may have been established...
position_reset()
if d.has("x"):
position.x = d["x"]
if d.has("y"):
position.y = d["y"]
if d.has("scale_x"):
scale.x = d["scale_x"]
if d.has("scale_y"):
scale.y = d["scale_y"]
if d.has("modulate_r"):
modulate.r = d["modulate_r"]
if d.has("modulate_g"):
modulate.g = d["modulate_g"]
if d.has("modulate_b"):
modulate.b = d["modulate_b"]
if d.has("modulate_a"):
modulate.a = d["modulate_a"]
change_properties()
5 changes: 5 additions & 0 deletions src/UI/Dialogs/PreviewDialog.gd
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ enum ImageImportOptions {
NEW_FRAME,
REPLACE_CEL,
NEW_LAYER,
NEW_REFERENCE_IMAGE,
PALETTE,
BRUSH,
PATTERN
Expand Down Expand Up @@ -49,6 +50,7 @@ func _on_PreviewDialog_about_to_show() -> void:
import_options.add_item("New frame")
import_options.add_item("Replace cel")
import_options.add_item("New layer")
import_options.add_item("New reference image")
import_options.add_item("New palette")
import_options.add_item("New brush")
import_options.add_item("New pattern")
Expand Down Expand Up @@ -141,6 +143,9 @@ func _on_PreviewDialog_confirmed() -> void:
var frame_index: int = new_layer_options.get_node("AtFrameSpinbox").value - 1
OpenSave.open_image_as_new_layer(image, path.get_basename().get_file(), frame_index)

elif current_import_option == ImageImportOptions.NEW_REFERENCE_IMAGE:
OpenSave.import_reference_image_from_path(path)

elif current_import_option == ImageImportOptions.PALETTE:
Palettes.import_palette_from_path(path)

Expand Down
67 changes: 67 additions & 0 deletions src/UI/ReferenceImageButton.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
extends Container
# UI to handle reference image editing.

var element: ReferenceImage
var _ignore_spinbox_changes = false


func _ready():
$Interior/Path.text = element.image_path
element.connect("properties_changed", self, "_update_properties")
_update_properties()


func _update_properties():
# This is because otherwise a little dance will occur.
# This also breaks non-uniform scales (not supported UI-wise, but...)
_ignore_spinbox_changes = true
$Interior/Options/Scale.value = element.scale.x * 100
$Interior/Options/X.value = element.position.x
$Interior/Options/Y.value = element.position.y
$Interior/Options/X.max_value = element.project.size.x
$Interior/Options/Y.max_value = element.project.size.y
$Interior/Options2/Opacity.value = element.modulate.a * 100
_ignore_spinbox_changes = false


func _on_Reset_pressed():
element.position_reset()
element.change_properties()


func _on_Remove_pressed():
var index = Global.current_project.reference_images.find(element)
if index != -1:
queue_free()
element.queue_free()
Global.current_project.reference_images.remove(index)
Global.current_project.change_project()


func _on_Scale_value_changed(value):
if _ignore_spinbox_changes:
return
element.scale.x = value / 100
element.scale.y = value / 100
element.change_properties()


func _on_X_value_changed(value):
if _ignore_spinbox_changes:
return
element.position.x = value
element.change_properties()


func _on_Y_value_changed(value):
if _ignore_spinbox_changes:
return
element.position.y = value
element.change_properties()


func _on_Opacity_value_changed(value):
if _ignore_spinbox_changes:
return
element.modulate.a = value / 100
element.change_properties()
94 changes: 94 additions & 0 deletions src/UI/ReferenceImageButton.tscn
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
[gd_scene load_steps=3 format=2]

[ext_resource path="res://src/UI/ReferenceImageButton.gd" type="Script" id=1]
[ext_resource path="res://src/UI/Nodes/ValueSlider.tscn" type="PackedScene" id=2]

[node name="ReferenceImageButton" type="PanelContainer"]
anchor_right = 1.0
anchor_bottom = 1.0
margin_right = -969.0
margin_bottom = -581.0
size_flags_horizontal = 3
script = ExtResource( 1 )

[node name="Interior" type="VBoxContainer" parent="."]
margin_left = 7.0
margin_top = 7.0
margin_right = 304.0
margin_bottom = 132.0

[node name="Path" type="Label" parent="Interior"]
margin_right = 297.0
margin_bottom = 14.0
size_flags_horizontal = 3
autowrap = true

[node name="Options" type="HBoxContainer" parent="Interior"]
margin_top = 18.0
margin_right = 297.0
margin_bottom = 42.0

[node name="Label2" type="Label" parent="Interior/Options"]
margin_top = 5.0
margin_right = 56.0
margin_bottom = 19.0
text = "Position:"

[node name="X" parent="Interior/Options" instance=ExtResource( 2 )]
margin_left = 60.0
margin_right = 122.0
allow_greater = true
allow_lesser = true

[node name="Y" parent="Interior/Options" instance=ExtResource( 2 )]
margin_left = 126.0
margin_right = 189.0
allow_greater = true
allow_lesser = true

[node name="Label" type="Label" parent="Interior/Options"]
margin_left = 193.0
margin_top = 5.0
margin_right = 230.0
margin_bottom = 19.0
text = "Scale:"

[node name="Scale" parent="Interior/Options" instance=ExtResource( 2 )]
margin_left = 234.0
margin_right = 297.0
allow_greater = true
allow_lesser = true

[node name="Options2" type="HBoxContainer" parent="Interior"]
margin_top = 46.0
margin_right = 297.0
margin_bottom = 70.0

[node name="Label" type="Label" parent="Interior/Options2"]
margin_top = 5.0
margin_right = 53.0
margin_bottom = 19.0
text = "Opacity:"

[node name="Opacity" parent="Interior/Options2" instance=ExtResource( 2 )]
margin_left = 57.0
margin_right = 177.0

[node name="Reset" type="Button" parent="Interior/Options2"]
margin_left = 181.0
margin_right = 229.0
margin_bottom = 24.0
text = "Reset"

[node name="Remove" type="Button" parent="Interior/Options2"]
margin_left = 233.0
margin_right = 297.0
margin_bottom = 24.0
text = "Remove"

[connection signal="value_changed" from="Interior/Options/X" to="." method="_on_X_value_changed"]
[connection signal="value_changed" from="Interior/Options/Y" to="." method="_on_Y_value_changed"]
[connection signal="value_changed" from="Interior/Options/Scale" to="." method="_on_Scale_value_changed"]
[connection signal="value_changed" from="Interior/Options2/Opacity" to="." method="_on_Opacity_value_changed"]
[connection signal="pressed" from="Interior/Options2/Reset" to="." method="_on_Reset_pressed"]
[connection signal="pressed" from="Interior/Options2/Remove" to="." method="_on_Remove_pressed"]
Loading

0 comments on commit 20bce81

Please sign in to comment.