diff --git a/addons/anima/components/AnimaAccordion.gd b/addons/anima/components/AnimaAccordion.gd new file mode 100644 index 00000000..a5da5d41 --- /dev/null +++ b/addons/anima/components/AnimaAccordion.gd @@ -0,0 +1,254 @@ +tool +extends AnimaAnimatable +class_name AnimaAccordion + +signal animation_completed + +export var label := "Accordion" setget set_label +export (Font) var font setget set_font +export var expanded := true setget set_expanded +export var set_size_flags := true + +var _title: AnimaButton +var _wrapper: VBoxContainer +var _icon: Sprite +var _content_control: Control +var _is_animating := false + +const BASE_COLOR = Color("663169") + +const CUSTOM_PROPERTIES := { + # Animation + ANIMATION_NAME = { + name = "Animation/Name", + type = TYPE_STRING, + default = "fadeIn" + }, + # Panel + PANEL_FILL_COLOR = { + name = "Panel/FillColor", + type = TYPE_COLOR, + default = Color("1b212e") + } +} + +var _all_properties: Dictionary = AnimaButton.BUTTON_BASE_PROPERTIES.duplicate() +var _is_ready := false + +var _button_colors = { + NORMAL_FILL_COLOR = BASE_COLOR, + FOCUSED_FILL_COLOR = BASE_COLOR.lightened(0.1), + HOVERED_FILL_COLOR = BASE_COLOR.lightened(0.2), + PRESSED_FILL_COLOR = BASE_COLOR.lightened(0.1), +} + +func _enter_tree(): + set_expanded(expanded, false) + +func _init(): + ._init() + + _init_layout() + + for color_key in _button_colors: + _all_properties[color_key].default = _button_colors[color_key] + + _add_properties(CUSTOM_PROPERTIES) + _add_properties(_all_properties) + + rect_clip_content = true + +func _ready(): + _content_control = get_child(get_child_count() - 1) + + set_expanded(expanded) + set_label(label) + + _is_ready = true + +func _draw(): + draw_rect(Rect2(Vector2(0, 0), rect_size), get_property(CUSTOM_PROPERTIES.PANEL_FILL_COLOR.name), true) + +func _get_configuration_warning(): + if get_child_count() > 3: + if _content_control: + _on_content_control_removed() + + return "AnimaAccordion can only have 1 child component" + + _content_control = get_child(get_child_count() - 1) + _on_content_control_added() + + return "" + +func _init_layout() -> void: + _wrapper = VBoxContainer.new() + _icon = Sprite.new() + + _title = AnimaButton.new() + + _icon.texture = load("res://addons/anima/icons/collapse.svg") + _icon.position = Vector2(16, 16) + + _title.anchor_right = 1 + _title.anchor_bottom = 1 + _title.rect_min_size.y = 32 + _title.rect_size.y = 32 + _title.set(_title.BUTTON_BASE_PROPERTIES.BUTTON_ALIGN.name, 1) + _title.connect("pressed", self, "_on_Title_pressed") + _title.connect("mouse_entered", self, "_on_mouse_entered") + _title.connect("mouse_exited", self, "_on_mouse_exited") + + _title.add_child(_icon) + + for color_key in _button_colors: + _title.set(AnimaButton.BUTTON_BASE_PROPERTIES[color_key].name, _button_colors[color_key]) + + _wrapper.anchor_right = 1 + _wrapper.add_child(_title) + + add_child(_wrapper) + +func set_expanded(is_expanded: bool, animate := true) -> void: + expanded = is_expanded + + if not is_inside_tree(): + if not expanded: + _icon.rotate(- PI / 2) + + return + + if _is_ready and animate: + _on_content_control_added() + _animate_height_change() + + return + + var y: float = _get_expanded_height() if is_expanded else _get_collapsed_height() + + rect_min_size.y = y + rect_size.y = y + # In case there was a fading animation applied + if _content_control: + _content_control.modulate.a = 1.0 + _content_control.rect_scale = Vector2.ONE + +func _get_collapsed_height() -> float: + return _title.rect_min_size.y + +func _get_expanded_height() -> float: + if _content_control == null: + for child in get_children(): + if child is VBoxContainer and not child == _wrapper: + _content_control = child + + break + + if not _content_control: + return _get_collapsed_height() + + _content_control.margin_bottom = 0 + return _get_collapsed_height() + _content_control.rect_size.y + +func _set(property: String, value): + ._set(property, value) + + if _title and property == "label": + _title.set(property, value) + _title._on_mouse_exited() + elif property.begins_with("Button"): + var p = property.replace("Button", ""); + + _title.set(p, value) + +func _animate_height_change() -> void: + var anima: AnimaNode = Anima.begin_single_shot(self, "accordion") + var easing: int = get_easing() + var duration = get_duration() + _is_animating = true + + if not should_animate_property_change(): + duration = ANIMA.MINIMUM_DURATION + + anima.set_default_duration(duration) + + var to_height := _get_expanded_height() + var from_height := _get_collapsed_height() + + anima.then( + Anima.Node(self) \ + .anima_property("min_size:y", to_height) \ + .anima_from(from_height) \ + .anima_easing(easing) + ) + anima.with( + Anima.Node(self) \ + .anima_property("size:y", to_height) \ + .anima_from(from_height) \ + .anima_easing(easing) + ) + + anima.with( + Anima.Node(_content_control) \ + .anima_animation(get_property(CUSTOM_PROPERTIES.ANIMATION_NAME.name), null, true) + ) + anima.with( + Anima.Node(_icon) \ + .anima_property("rotate", 0) \ + .anima_from(-90) \ + .anima_pivot(ANIMA.PIVOT.CENTER) \ + .anima_easing(easing) + ) + + if expanded: + anima.play() + else: + anima.play_backwards_with_speed(1.5) + + yield(anima, "animation_completed") + + if not should_animate_property_change() and not expanded: + rect_size.y = from_height + + _is_animating = false + + emit_signal("animation_completed") + +func set_label(new_label: String) -> void: + label = new_label + + _title.set(AnimaButton.BUTTON_BASE_PROPERTIES.BUTTON_LABEL.name, label) + +func set_font(new_font: Font) -> void: + font = new_font + + _title.set(AnimaButton.BUTTON_BASE_PROPERTIES.BUTTON_FONT.name, font) + +func _on_Title_pressed(): + set_expanded(!expanded) + +func _on_content_control_added() -> void: + if _content_control == null: + return + + _content_control.set_position(Vector2(0, _title.rect_min_size.y)) + + if set_size_flags: + _content_control.size_flags_horizontal = SIZE_EXPAND_FILL + _content_control.size_flags_vertical = SIZE_EXPAND_FILL + + _content_control.anchor_right = 1 + _content_control.anchor_bottom = 1 + _content_control.margin_right = 0 + _content_control.margin_bottom = 0 + + _content_control.margin_top = _title.rect_min_size.y + +func _on_content_control_removed() -> void: + _content_control = null + +func _on_mouse_entered() -> void: + emit_signal("mouse_entered") + +func _on_mouse_exited() -> void: + emit_signal("mouse_exited") diff --git a/addons/anima/components/AnimaButton.gd b/addons/anima/components/AnimaButton.gd new file mode 100644 index 00000000..d4f4f881 --- /dev/null +++ b/addons/anima/components/AnimaButton.gd @@ -0,0 +1,392 @@ +tool +extends AnimaRectangle +class_name AnimaButton, "res://addons/anima/icons/button.svg" + +signal button_down +signal button_up +signal toggled(button_pressed) + +enum STATE { + NORMAL, + PRESSED + HOVERED, + DISABLED, + DRAW_HOVER_PRESSED, +} + +const STATES := { + STATE.NORMAL: "Normal", + STATE.DISABLED: "Normal", + STATE.HOVERED: "Hovered", + STATE.DRAW_HOVER_PRESSED: "Focused", + STATE.PRESSED: "Pressed" +} + +export (STATE) var _test_state = STATE.NORMAL setget _set_test_state + +const BASE_COLOR = Color("314569") + +const BUTTON_BASE_PROPERTIES := { + # Button + BUTTON_LABEL = { + name = "Button/Text", + type = TYPE_STRING, + default = "Anima Button", + animatable = false + }, + BUTTON_ICON = { + name = "Button/ICON", + type = TYPE_OBJECT, + hint = PROPERTY_HINT_RESOURCE_TYPE, + hint_string = "Texture", + default = null, + animatable = false + }, + BUTTON_ALIGN = { + name = "Button/Align", + type = TYPE_INT, + hint = PROPERTY_HINT_ENUM, + hint_string = "Left,Center,Right", + default = 1, + animatable = false + }, + BUTTON_FONT = { + name = "Button/Font", + type = TYPE_OBJECT, + hint = PROPERTY_HINT_RESOURCE_TYPE, + hint_string = "Font", + default = null, + animatable = false + }, + BUTTON_DISABLED = { + name = "Button/Disabled", + type = TYPE_BOOL, + default = false, + }, + BUTTON_TOGGLE_MODE = { + name = "Button/ToggleMode", + type = TYPE_BOOL, + default = false, + }, + BUTTON_SHORTCUT_IN_TOOLTIP = { + name = "Button/ShortcutInTooltip", + type = TYPE_BOOL, + default = true, + }, + BUTTON_PRESSED = { + name = "Button/Pressed", + type = TYPE_BOOL, + default = false, + }, + BUTTON_CONTENT_MARGIN = { + name = "Button/ContentMargin", + type = TYPE_INT, + default = 12, + }, + BUTTON_GROUP = { + name = "Button/Group", + type = TYPE_OBJECT, + hint = PROPERTY_HINT_RESOURCE_TYPE, + hint_string = "ButtonGroup", + default = null + }, + + # Normal + NORMAL_FILL_COLOR = { + name = "Normal/FillColor", + type = TYPE_COLOR, + }, + NORMAL_FONT_COLOR = { + name = "Normal/FontColor", + type = TYPE_COLOR, + default = Color("fff") + }, + + # Hovered + HOVERED_USE_STYLE = { + name = "Hovered/UseSameStyleOf", + type = TYPE_STRING, + hint = PROPERTY_HINT_ENUM, + hint_string = ",Normal,Pressed,Focused", + default = "" + }, + HOVERED_FILL_COLOR = { + name = "Hovered/FillColor", + type = TYPE_COLOR, + }, + HOVERED_FONT_COLOR = { + name = "Hovered/FontColor", + type = TYPE_COLOR, + default = Color.transparent + }, + HOVERED_SCALE = { + name = "Hovered/Scale", + type = TYPE_VECTOR2, + default = Vector2.ONE, + }, + + # Pressed + PRESSED_USE_STYLE = { + name = "Pressed/UseSameStyleOf", + type = TYPE_STRING, + hint = PROPERTY_HINT_ENUM, + hint_string = ",Normal,Hovered,Focused", + default = "" + }, + PRESSED_FILL_COLOR = { + name = "Pressed/FillColor", + type = TYPE_COLOR, + }, + PRESSED_FONT_COLOR = { + name = "Pressed/FontColor", + type = TYPE_COLOR, + default = Color.transparent + }, + + # Focused + FOCUSED_USE_STYLE = { + name = "Focused/UseSameStyleOf", + type = TYPE_STRING, + hint = PROPERTY_HINT_ENUM, + hint_string = ",Normal,Hovered,Pressed", + default = "" + }, + FOCUSED_FILL_COLOR = { + name = "Focused/FillColor", + type = TYPE_COLOR, + }, + FOCUSED_FONT_COLOR = { + name = "Focused/FontColor", + type = TYPE_COLOR, + default = Color.transparent + }, +} + +var _all_properties := BUTTON_BASE_PROPERTIES +var _button := Button.new() +var _old_state: int + +func _init(): + ._init() + + var extra_keys = ["Normal", "Hovered", "Focused", "Pressed"] + + for key in PROPERTIES: + for extra_key_index in extra_keys.size(): + var extra_key: String = extra_keys[extra_key_index] + var new_key = key.replace("RECTANGLE", extra_key.to_upper()) + + if BUTTON_BASE_PROPERTIES.has(new_key): + continue + + var new_value = PROPERTIES[key].duplicate() + + if extra_key_index > 0: + if new_value.default is float or new_value.default is int: + new_value.default = -1 + elif new_value.default is Vector2: + new_value.default = Vector2(-1, -1) + elif new_value.default is Rect2: + new_value.default = Rect2(-1, -1, -1, -1) + elif new_value.default is Color: + new_value.default = Color.transparent + new_value.default.a = 0.00 + + new_value.name = new_value.name.replace("Rectangle/", extra_key + "/") + + _all_properties[new_key] = new_value + + + var button_colors = { + NORMAL_FILL_COLOR = BASE_COLOR, + FOCUSED_FILL_COLOR = BASE_COLOR.lightened(0.1), + HOVERED_FILL_COLOR = BASE_COLOR.lightened(0.2), + PRESSED_FILL_COLOR = BASE_COLOR.lightened(0.1), + } + + for color_key in button_colors: + _all_properties[color_key].default = button_colors[color_key] + + # We don't want to expose the Rectangulare properties as they're only used "internally" + _hide_properties(PROPERTIES) + + _add_properties(_all_properties) + + _copy_properties("Normal") + + _test_state = STATE.NORMAL + + _init_button() + +func _ready(): + _set_button_property(BUTTON_BASE_PROPERTIES.BUTTON_LABEL.name) + _set_button_property(BUTTON_BASE_PROPERTIES.BUTTON_ALIGN.name) + _set_button_property(BUTTON_BASE_PROPERTIES.BUTTON_FONT.name) + _set_button_property(BUTTON_BASE_PROPERTIES.BUTTON_TOGGLE_MODE.name) + _set_button_property(BUTTON_BASE_PROPERTIES.BUTTON_PRESSED.name) + + _copy_properties("Normal") + + connect("item_rect_changed", self, "_on_resize_me") + _button.connect("draw", self, "_refresh_button") + +func _set_button_property(key: String) -> void: + _set(key, get_property(key)) + +func _init_button() -> void: + var style := StyleBoxEmpty.new() + + for s in ["normal", "hover", "pressed", "disabled", "focus"]: + _button.add_stylebox_override(s, style) + + for c in ["font_color_disabled", "font_color_focus", "font_color", "font_color_hover", "font_color_pressed"]: + _button.add_color_override(c, Color.white) + + _button.connect("button_down", self, "_on_button_down") + _button.connect("button_up", self, "_on_button_down") + _button.connect("pressed", self, "_on_pressed") + _button.connect("toggled", self, "_on_toggled") + _button.connect("mouse_entered", self, "_on_mouse_entered") + _button.connect("mouse_exited", self, "_on_mouse_exited") + + add_child(_button) + + _on_resize_me() + +func _copy_properties(from: String) -> void: + var copy_from_key = from.to_upper() + + for key in _all_properties: + if key.find(copy_from_key) == 0: + var property_name: String = _all_properties[key].name + var rectangle_property_name: String = property_name.replace(from + "/", "Rectangle/") + var value = get_property(property_name) + + if _property_exists(rectangle_property_name): + _property_list.set(rectangle_property_name, value) + elif property_name.find("/FontColor") > 0: + _button.add_color_override("font_color", value) + +func _animate_state(root_key: String) -> void: + var override_key = get_property(root_key + "/UseSameStyleOf") + + if override_key: + root_key = override_key + + var from: String = root_key.to_upper() + var params_to_animate := [] + + for key in _all_properties: + if key.find(from) == 0: + var property_name: String = _all_properties[key].name + var rectangle_property_name: String = property_name.replace(root_key + "/", "Rectangle/") + var current_value = get_property(rectangle_property_name) + var final_value = get_property(property_name) + + if final_value is String \ + or final_value is bool \ + or str(final_value).find("-1") >= 0 \ + or (final_value is Color and final_value.a == 0): + continue + + if current_value != final_value: + params_to_animate.push_back({ property = rectangle_property_name, to = final_value }) + + if root_key == "Normal" and rect_scale != Vector2.ONE: + params_to_animate.push_back({ property = "scale", to = Vector2.ONE }) + + if params_to_animate.size() > 0: + animate_params(params_to_animate) + +func refresh() -> void: + var state = _button.get_draw_mode() + + if state == _old_state: + return + + if _is_animating_property_change: + _anima.stop() + + _animate_state(STATES[state]) + + _old_state = state + +func _set(property: String, value) -> void: + if Engine.editor_hint and property.find("Rectangle/") < 0: + prevent_animate_property_change() + + ._set(property, value) + + if property.find("Button/") == 0: + if property == BUTTON_BASE_PROPERTIES.BUTTON_CONTENT_MARGIN.name: + var style: StyleBoxEmpty = _button.get_stylebox("normal") + + style.content_margin_bottom = value + style.content_margin_top = value + style.content_margin_left = value + style.content_margin_right = value + + _on_resize_me() + else: + var p: String = property.replace("Button/", "").capitalize().replace(" ", "_").to_lower() + _button.set(p.replace(" ", "_").to_lower(), value) + elif property.find("FontColor") > 0: + _button.add_color_override("font_color", value) + elif property == "Rectangle/Scale": + rect_scale = value + + restore_animate_property_change() + + if Engine.editor_hint and property.find(STATES[_test_state]) >= 0 and is_inside_tree(): + _animate_state(STATES[_test_state]) + +func get(property): + if property.find("FontColor") >= 0: + var color = _button.get_color("font_color") + + return color + elif property.find("/Scale") > 0: + return Vector2.ONE + + return .get(property) + +func set_label(label: String) -> void: + set(BUTTON_BASE_PROPERTIES.BUTTON_LABEL.name, label) + +func get_label() -> String: + return get(BUTTON_BASE_PROPERTIES.BUTTON_LABEL.name) + +func set_icon(icon: Texture) -> void: + set(BUTTON_BASE_PROPERTIES.BUTTON_ICON.name, icon) + +func _on_button_down() -> void: + emit_signal("button_down") + +func _on_button_up() -> void: + emit_signal("button_up") + +func _on_pressed() -> void: + emit_signal("pressed") + +func _set_test_state(new_state) -> void: + if Engine.editor_hint: + _test_state = new_state + _animate_state(STATES[new_state]) + +func _on_resize_me() -> void: + _button.rect_size = rect_size + + if rect_size < _button.rect_size: + rect_size = _button.rect_size + +func _on_toggled(is_toggled) -> void: + emit_signal("toggled", is_toggled) + +func _refresh_button() -> void: + refresh() + +func _on_mouse_entered() -> void: + emit_signal("mouse_entered") + +func _on_mouse_exited() -> void: + emit_signal("mouse_exited") diff --git a/addons/anima/components/AnimaChars.gd b/addons/anima/components/AnimaChars.gd new file mode 100644 index 00000000..ad442dc8 --- /dev/null +++ b/addons/anima/components/AnimaChars.gd @@ -0,0 +1,148 @@ +tool +extends HBoxContainer +class_name AnimaChars + +enum Splitting { + LETTER, + WORD, + CUSTOM +} + +export (String) var label = 'Anima' setget set_label +export (Splitting) var split = Splitting.LETTER setget set_split +export (String) var custom_splitting setget set_custom_splitting +export (Font) var font setget set_font +export (Color) var font_color setget set_font_color +export (float) var letter_spacing = 0 setget set_letter_spacing +export (ShaderMaterial) var letters_shader setget set_letters_shader + +var _anima_label: AnimaLabel +var _old_label: String +var _old_split: int +var _old_splitted_label + +func _ready(): + connect("item_rect_changed", self, '_update_label') + _update_label() + +func _draw(): + _update_label() + +func set_label(new_label: String) -> void: + label = new_label + + update() + +func set_font(new_font: Font) -> void: + font = new_font + + update() + +func set_letter_spacing(spacing: float) -> void: + letter_spacing = spacing + + update() + +func set_font_color(color: Color) -> void: + font_color = color + + update() + +func set_custom_splitting(custom: String) -> void: + custom_splitting = custom + + update() + +func set_split(s: int) -> void: + split = s + + update() + +func set_letters_shader(value: ShaderMaterial) -> void: + letters_shader = value + + for child in get_children(): + child.set_label_shader(value) + +func _update_label() -> void: + var anima_label = _get_anima_label() + var parts = _split_label() + var length = parts.size() if parts is Array else parts.length() + + for index in length: + var letter = parts[index] + var clone = null + + if get_child_count() > index: + clone = get_child(index) + else: + clone = _anima_label.duplicate() + clone.name = 'AnimaLabel' + str(index) + + add_child(clone) + + clone.mouse_filter = mouse_filter + clone.set_label(letter) + clone.set_should_auto_resize(true) + clone.set_label_shader(letters_shader) + clone.show() + + for child_index in range(get_child_count() - 1, length - 1, -1): + var child = get_child(child_index) + + remove_child(child) + + add_constant_override("separation", letter_spacing) + _update_font_and_size() + +func _split_label() -> Array: + if label == _old_label and split != Splitting.CUSTOM and split == _old_split: + return _old_splitted_label + + if split == Splitting.LETTER: + return label + + _old_split = split + + var splitting_symbol = ' ' if split == Splitting.WORD else custom_splitting + var parts = label.split(splitting_symbol) + var result := [] + + for part in parts: + result.push_back(part) + result.push_back(splitting_symbol) + + return result.slice(0, -2) + +func _update_font_and_size() -> void: + if font == null: + return + + for child in get_children(): + child.set_font(font) + child.set_font_color(font_color) + + var width = 0 + + for letter in child.label: + var size = font.get_char_size(letter.to_ascii()[0]) + + width += size.x + letter_spacing + + var child_size = Vector2(width, rect_min_size.y) + + child.set_size(child_size) + +func _get_anima_label() -> AnimaLabel: + if _anima_label: + return _anima_label + + _anima_label = AnimaLabel.new() + _anima_label.set_should_auto_resize(true) + _anima_label.size_flags_horizontal = SIZE_FILL + _anima_label.size_flags_vertical = SIZE_FILL + + return _anima_label + +func _on_AnimaChars_item_rect_changed(): + update() diff --git a/addons/anima/components/AnimaChars.tscn b/addons/anima/components/AnimaChars.tscn new file mode 100644 index 00000000..b7a2c187 --- /dev/null +++ b/addons/anima/components/AnimaChars.tscn @@ -0,0 +1,13 @@ +[gd_scene load_steps=2 format=2] + +[ext_resource path="res://addons/anima/components/AnimaChars.gd" type="Script" id=1] + +[node name="AnimaChars" type="HBoxContainer"] +custom_constants/separation = 0 +alignment = 1 +script = ExtResource( 1 ) +__meta__ = { +"_edit_use_anchors_": false +} + +[connection signal="item_rect_changed" from="." to="." method="_on_AnimaChars_item_rect_changed"] diff --git a/addons/anima/components/AnimaLabel.gd b/addons/anima/components/AnimaLabel.gd new file mode 100644 index 00000000..becaafc2 --- /dev/null +++ b/addons/anima/components/AnimaLabel.gd @@ -0,0 +1,127 @@ +tool +class_name AnimaLabel +extends Control + +const SpriteLabel = preload('./helpers/SpriteLabel.gd') + +export (String) var label = 'AnimaLabel' setget set_label +export (Font) var font setget set_font +export (ANIMA.Align) var align = ANIMA.Align.CENTER setget set_align +export (ANIMA.VAlign) var valign = ANIMA.VAlign.CENTER setget set_valign +export (Color) var font_color = Color.white setget set_font_color +export (Vector2) var text_offset = Vector2.ZERO setget set_text_offset_px +export (Vector2) var text_scale = Vector2(1, 1) setget set_text_scale +export (ShaderMaterial) var label_shader setget set_label_shader +export (bool) var make_label_shader_unique = false setget set_make_label_shader_unique + +var should_auto_resize := false setget set_should_auto_resize + +onready var _label := SpriteLabel.new() + +func _ready(): + connect("resized", self, '_on_resized') + + set_clip_contents(true) + + _safe_set('label', label) + _safe_set('font', font) + _safe_set('align', align) + _safe_set('valign', valign) + _safe_set('font_color', font_color) + _safe_set('text_offset', text_offset) + _safe_set('scale', text_scale) + _update_label_shader() + _on_resized() + + add_child(_label) + +func _safe_set(property: String, value, should_update := false) -> void: + if not _label: + return + + _label.set(property, value) + + if should_update: + _label.update() + + _auto_resize() + +func set_label(new_label: String) -> void: + label = new_label + + _safe_set('label', label) + +func set_font(new_font: Font) -> void: + font = new_font + + _safe_set('font', font) + + if font and not font.is_connected("changed", self, '_update_font'): + font.connect("changed", self, '_update_font') + +func set_align(new_align: int) -> void: + align = new_align + + _safe_set('align', align) + +func set_valign(new_valign: int) -> void: + valign = new_valign + + _safe_set('valign', valign) + +func set_font_color(new_color: Color) -> void: + font_color = new_color + + _safe_set('font_color', font_color) + +func set_text_offset_px(new_offset: Vector2) -> void: + text_offset = new_offset + + _safe_set('text_offset', new_offset) + +func set_text_scale(new_scale: Vector2) -> void: + text_scale = new_scale + + _safe_set('scale', text_scale, true) + +func set_should_auto_resize(should: bool) -> void: + should_auto_resize = should + + _auto_resize() + +func set_label_shader(value: ShaderMaterial) -> void: + label_shader = value + + _update_label_shader() + +func set_make_label_shader_unique(is_unique: bool) -> void: + make_label_shader_unique = is_unique + + _update_label_shader() + +func _update_label_shader() -> void: + var shader = label_shader + + if shader == null: + return + + if make_label_shader_unique: + shader = shader.duplicate() + + _safe_set('label_shader', shader) + +func _update_font() -> void: + _safe_set('font', font) + +func _on_resized(): + _safe_set('size', rect_size) + +func _auto_resize() -> void: + if not _label: + return + + if should_auto_resize: + var size = _label.get_size() + + rect_min_size.x = size.x + rect_size.x = size.x diff --git a/addons/anima/components/Animatable.gd b/addons/anima/components/Animatable.gd new file mode 100644 index 00000000..107c3004 --- /dev/null +++ b/addons/anima/components/Animatable.gd @@ -0,0 +1,161 @@ +tool +extends Control +class_name AnimaAnimatable + +const BASE_PROPERTIES := { + ANIMATE_PROPERTY_CHANGE = { + name = "Animation/AnimatePropertyChange", + type = TYPE_BOOL, + default = true + }, + ANIMATION_SPEED = { + name = "Animation/Speed", + type = TYPE_REAL, + default = 0.3 + }, + ANIMATION_EASING = { + name = "Animation/Easing", + type = TYPE_INT, + hint = PROPERTY_HINT_ENUM, + hint_string = AnimaEasing.EASING, + default = ANIMA.EASING.LINEAR + }, +} + +var _property_list := AnimaPropertyList.new() +var _is_animating_property_change := false +var _old_animate_property_change: bool +var _anima: AnimaNode + +func _init(): + _add_properties(BASE_PROPERTIES) + + _anima = Anima.begin(self) + +func _add_properties(properties: Dictionary) -> void: + for key in properties: + _property_list.add(properties[key]) + +func _add_property(name: String, value: Dictionary) -> void: + value.name = name + + _property_list.add(value) + +func _property_exists(name: String) -> bool: + return _property_list.exists(name) + +func _hide_properties(properties: Dictionary) -> void: + for key in properties: + _property_list.hide(properties[key].name) + +func _get(property: String): + return _property_list.get(property) + +func _set(property: String, value): + if not _property_list.exists(property): + return + + var old_value = _property_list.get(property) + var is_animatable = _property_list.is_animatable(property) + + _property_list.set(property, value) + + if value is bool or value is String or value is Resource or not is_animatable: + return update() + + var has_value_changed = old_value != value + + if not is_inside_tree() or \ + not has_value_changed: + return + + var animate_property_change = get_property(BASE_PROPERTIES.ANIMATE_PROPERTY_CHANGE.name) + + if _is_animating_property_change or not animate_property_change: + update() + else: + var from = old_value if Engine.editor_hint else null + + animate_param(property, value, from) + +func prevent_animate_property_change() -> void: + _old_animate_property_change = get_property(BASE_PROPERTIES.ANIMATE_PROPERTY_CHANGE.name) + + _property_list.set(BASE_PROPERTIES.ANIMATE_PROPERTY_CHANGE.name, false) + +func restore_animate_property_change() -> void: + if not _old_animate_property_change: + return + + _property_list.set(BASE_PROPERTIES.ANIMATE_PROPERTY_CHANGE.name, _old_animate_property_change) + +func set(name: String, value) -> void: + prevent_animate_property_change() + + .set(name, value) + + restore_animate_property_change() + +func _get_property_list() -> Array: + if _property_list: + return _property_list.get_property_list() + + return [] + +func get_property(name: String): + return _get(name) + +func get_property_initial_value(key: String): + return _property_list.get_initial_value(key) + +func animate_param(property: String, value, from = null) -> void: + animate_params([{ property = property, to = value, from = from }]) + +func animate_params(params: Array) -> void: + var animations := [] + var easing: int = get_property(BASE_PROPERTIES.ANIMATION_EASING.name) + var duration: float = get_property(BASE_PROPERTIES.ANIMATION_SPEED.name) + + for param in params: + var animation := Anima.Node(self).anima_property(param.property, param.to, duration) + + if param.has("from"): + animation.anima_from(param.from) + + animation.anima_easing(easing) + animations.push_back(animation) + + _animate(animations) + +func _animate(anima_data: Array) -> AnimaNode: + var anima: AnimaNode = Anima.begin_single_shot(self) + + for data in anima_data: + anima.with(data) + + _is_animating_property_change = true + + anima.play() + yield(anima, "animation_completed") + + _is_animating_property_change = false + + return anima + +func set_position(position: Vector2, default := false) -> void: + animate_param("position", position) + +func set_size(size: Vector2, default := false) -> void: + animate_param("size", size) + +func set_scale(scale: Vector2, default := false) -> void: + animate_param("scale", scale) + +func should_animate_property_change() -> bool: + return get_property(BASE_PROPERTIES.ANIMATE_PROPERTY_CHANGE.name) + +func get_easing() -> int: + return get_property(BASE_PROPERTIES.ANIMATION_EASING.name) + +func get_duration() -> int: + return get_property(BASE_PROPERTIES.ANIMATION_SPEED.name) diff --git a/addons/anima/components/Carousel.gd b/addons/anima/components/Carousel.gd new file mode 100644 index 00000000..f5f22dcc --- /dev/null +++ b/addons/anima/components/Carousel.gd @@ -0,0 +1,144 @@ +tool +extends VBoxContainer +class_name AnimaCarousel + +signal carousel_size_changed(new_size) +signal carousel_height_changed(final_height) +signal index_changed(new_index) + +onready var _container: HBoxContainer = find_node('Container') +onready var _controls: HBoxContainer = find_node('Controls') + +export (int) var index setget set_index +export (float) var duration = 0.3 +export (float) var padding := 0.0 setget set_padding +export (ANIMA.EASING) var scroll_easing = ANIMA.EASING.LINEAR +export (ANIMA.EASING) var height_easing = ANIMA.EASING.LINEAR + +var _heights: Array +var _old_size_x: float + +func _ready(): + for index in _controls.get_child_count(): + var child: Node = _controls.get_child(index) + + if child.has_signal("pressed"): + child.connect("pressed", self, "_on_control_pressed", [index]) + + $Wrapper.anchor_right = 0 + + update_size() + set_index(index) + +func update_size() -> void: + if _container == null: + return + + var size: float = rect_size.x * _container.get_child_count() + + _container.rect_min_size.x = size + _container.rect_size.x = size + + for child in _container.get_children(): + var node: Control = child + + node.size_flags_horizontal = SIZE_EXPAND_FILL + node.size_flags_vertical = 0 + + _heights.push_back(node.rect_size.y + padding) + +func _maybe_get_container() -> void: + _container = find_node('Container') + +func get_active_index() -> int: + return index + +func set_index(new_index: int) -> void: + update_size() + + if not is_inside_tree() or get_child_count() == 0: + return + + if _container == null: + _container = find_node('Container') + + var count: int = max(0, _container.get_child_count() - 1) + index = clamp(new_index, 0, count) + + if _heights.size() == 0: + return + + # + # Need to set the mouse filter to ignore for the children that are + # not "visible", otherwise they stop interaction of the mouse in the + # AnimaEditor + # + for child_index in get_child_count(): + var filter = MOUSE_FILTER_PASS if child_index == index else MOUSE_FILTER_IGNORE + var node: Node = get_child(child_index) + + if node is Control: + node.mouse_filter = filter + + var x = rect_size.x * index + var wrapper_height = get_expected_wrapper_height() + var height = get_expected_height() + + var anima: AnimaNode = Anima.begin_single_shot(self) + anima.set_default_duration(duration) + + anima.then( + Anima.Node(self) \ + .anima_property("min_size:y", height) \ + .anima_easing(height_easing) + ) + anima.with( + Anima.Node(self) \ + .anima_size_y(height) \ + .anima_easing(height_easing) + ) + anima.with( + Anima.Node(_container) \ + .anima_position_x(-x) \ + .anima_easing(scroll_easing) + ) + anima.with( + Anima.Node($Wrapper) \ + .anima_property("min_size:y", wrapper_height) \ + .anima_easing(height_easing) + ) + anima.play() + +func get_expected_wrapper_height() -> float: + return _heights[index] + +func get_expected_height() -> float: + var height = _controls.rect_size.y + get_expected_wrapper_height() + + return height + +func _on_Container_item_rect_changed() -> void: + emit_signal("carousel_size_changed", rect_size) + +func _on_control_pressed(new_index: int) -> void: + set_index(new_index) + +func _on_Carousel_item_rect_changed(): + if rect_size.x == _old_size_x: + return + + update_size() + + # TODO: Why this throws an error? + # set_index(index) + + var x = -rect_size.x * index + if _container.rect_position.x != x: + _container.rect_position.x = x + + _old_size_x = rect_size.x + +func set_padding(new_padding: float) -> void: + padding = new_padding + + add_constant_override("separation", padding) diff --git a/addons/anima/components/Carousel.tscn b/addons/anima/components/Carousel.tscn new file mode 100644 index 00000000..fcf28b7a --- /dev/null +++ b/addons/anima/components/Carousel.tscn @@ -0,0 +1,34 @@ +[gd_scene load_steps=2 format=2] + +[ext_resource path="res://addons/anima/components/Carousel.gd" type="Script" id=1] + +[node name="Carousel" type="VBoxContainer"] +anchor_right = 1.0 +rect_clip_content = true +size_flags_horizontal = 3 +size_flags_vertical = 3 +custom_constants/separation = 0 +script = ExtResource( 1 ) + +[node name="Controls" type="HBoxContainer" parent="."] +margin_right = 1024.0 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="Wrapper" type="Control" parent="."] +margin_right = 1024.0 +rect_clip_content = true + +[node name="Container" type="HBoxContainer" parent="Wrapper"] +anchor_right = 1.0 +margin_right = -1024.0 +size_flags_horizontal = 3 +size_flags_vertical = 3 +__meta__ = { +"_edit_use_anchors_": false, +"_item_size": 0.0 +} + +[connection signal="item_rect_changed" from="." to="." method="_on_Carousel_item_rect_changed"] +[connection signal="item_rect_changed" from="Wrapper/Container" to="." method="_on_Container_item_rect_changed"] diff --git a/addons/anima/components/TextureRect.gd b/addons/anima/components/TextureRect.gd new file mode 100644 index 00000000..74182b07 --- /dev/null +++ b/addons/anima/components/TextureRect.gd @@ -0,0 +1,7 @@ +extends TextureRect + +export (Font) var _font + +func _draw(): + draw_circle(Vector2(100, 100), 100, Color.red) + draw_string(_font, Vector2(100, 100), "AnimaLabel") diff --git a/addons/anima/components/helpers/SpriteLabel.gd b/addons/anima/components/helpers/SpriteLabel.gd new file mode 100644 index 00000000..03084c32 --- /dev/null +++ b/addons/anima/components/helpers/SpriteLabel.gd @@ -0,0 +1,116 @@ +tool +extends Sprite + +var label := 'AnimaLabel' setget set_label +var font: Font setget set_font +var align: int = Anima.Align.CENTER setget set_align +var valign: int = Anima.VAlign.CENTER setget set_valign +var font_color := Color.white setget set_font_color +var text_offset := Vector2.ZERO setget set_text_offset_px +var text_scale := Vector2(1, 1) setget set_text_scale +var container_scale := Vector2(1, 1) setget set_container_scale +var size: Vector2 setget set_size +var label_shader: ShaderMaterial setget set_label_shader + +var _old_label: String +var _old_text_size: Vector2 + +func _ready(): + connect("item_rect_changed", self, '_on_item_rect_changed') + set_centered(false) + +func _draw(): + if not font: + return + + var char_size: Vector2 = font.get_char_size(97) + var text_size = get_size() + var my_size: Vector2 = size / scale + + var position := Vector2(0, char_size.y / 2) + + if align == Anima.Align.CENTER: + position.x = (my_size.x - text_size.x) / 2 + elif align == Anima.Align.RIGHT: + position.x = my_size.x - text_size.x + + if valign == Anima.Align.CENTER: + position.y = (my_size.y / 2) + (text_size.y / 2) + elif valign == Anima.VAlign.BOTTOM: + position.y = my_size.y + + draw_string(font, position + text_offset, label, font_color) + +func get_size() -> Vector2: + if label == _old_label or font == null: + return _old_text_size + + var width := 0.0 + var height := font.get_ascent() - font.get_descent() + + for letter in label: + var char_size: Vector2 = font.get_char_size(letter.to_ascii()[0]) + + width += char_size.x + + _old_text_size = Vector2(width, height) + + var m: float = height / 10 + #if material: + # material.set_shader_param('uv_multiplier', m - 1) + + return _old_text_size + +func set_label(new_label: String) -> void: + label = new_label + + update() + +func set_font(new_font: Font) -> void: + font = new_font + + if font and not font.is_connected("changed", self, '_update_font'): + font.connect("changed", self, '_update_font') + +func set_align(new_align: int) -> void: + align = new_align + + update() + +func set_valign(new_valign: int) -> void: + valign = new_valign + + update() + +func set_font_color(new_color: Color) -> void: + font_color = new_color + + update() + +func set_text_offset_px(new_offset: Vector2) -> void: + text_offset = new_offset + + update() + +func set_text_scale(new_scale: Vector2) -> void: + text_scale = new_scale + + update() + +func set_container_scale(new_scale: Vector2) -> void: + container_scale = new_scale + + update() + +func set_size(new_size: Vector2) -> void: + size = new_size + + update() + +func set_label_shader(shader: ShaderMaterial) -> void: + label_shader = shader + + material = label_shader + +func _on_item_rect_changed() -> void: + update() diff --git a/project.godot b/project.godot index d5b72511..3aa0742b 100644 --- a/project.godot +++ b/project.godot @@ -9,6 +9,16 @@ config_version=4 _global_script_classes=[ { +"base": "AnimaAnimatable", +"class": "AnimaAccordion", +"language": "GDScript", +"path": "res://addons/anima/components/AnimaAccordion.gd" +}, { +"base": "Control", +"class": "AnimaAnimatable", +"language": "GDScript", +"path": "res://addons/anima/components/Animatable.gd" +}, { "base": "Reference", "class": "AnimaAnimationsUtils", "language": "GDScript", @@ -19,6 +29,21 @@ _global_script_classes=[ { "language": "GDScript", "path": "res://addons/anima/core/deferred_add_child.gd" }, { +"base": "AnimaRectangle", +"class": "AnimaButton", +"language": "GDScript", +"path": "res://addons/anima/components/AnimaButton.gd" +}, { +"base": "VBoxContainer", +"class": "AnimaCarousel", +"language": "GDScript", +"path": "res://addons/anima/components/Carousel.gd" +}, { +"base": "HBoxContainer", +"class": "AnimaChars", +"language": "GDScript", +"path": "res://addons/anima/components/AnimaChars.gd" +}, { "base": "Object", "class": "AnimaDeclarationBase", "language": "GDScript", @@ -69,6 +94,11 @@ _global_script_classes=[ { "language": "GDScript", "path": "res://addons/anima/core/keyframes-engine.gd" }, { +"base": "Control", +"class": "AnimaLabel", +"language": "GDScript", +"path": "res://addons/anima/components/AnimaLabel.gd" +}, { "base": "Node", "class": "AnimaNode", "language": "GDScript", @@ -110,8 +140,13 @@ _global_script_classes=[ { "path": "res://addons/anima/core/visual_node.gd" } ] _global_script_class_icons={ +"AnimaAccordion": "", +"AnimaAnimatable": "", "AnimaAnimationsUtils": "", "AnimaAwaitableAddChild": "", +"AnimaButton": "", +"AnimaCarousel": "", +"AnimaChars": "", "AnimaDeclarationBase": "", "AnimaDeclarationForAnimation": "", "AnimaDeclarationForProperty": "", @@ -122,6 +157,7 @@ _global_script_class_icons={ "AnimaDeclarationNodes": "", "AnimaEasing": "", "AnimaKeyframesEngine": "", +"AnimaLabel": "", "AnimaNode": "", "AnimaNodesProperties": "", "AnimaPlayer": "",