Rebrand project and refresh title presentation

This commit is contained in:
Alexander Sellite 2026-08-25 17:14:12 -04:00
parent 17fef2c5d4
commit 7ae20789a9
138 changed files with 1511 additions and 1498 deletions

View file

@ -2,6 +2,13 @@ class_name BubbleButton
extends Button
const ICON_FILL_RATIO: float = 0.76
const LABELED_ICON_FILL_RATIO: float = 0.58
const BubbleGradientBackgroundType = preload(
"res://ui/components/bubble_menu/bubble_gradient_background.gd"
)
const BubbleHoverDrawerType = preload(
"res://ui/components/bubble_menu/bubble_hover_drawer.gd"
)
@export var profile: BubbleMenuProfile
@ -16,6 +23,9 @@ const ICON_FILL_RATIO: float = 0.76
@export_range(1, 256, 1) var minimum_font_size: int = 14
@export_range(1, 256, 1) var maximum_font_size: int = 32
@export_group("Icon")
@export_range(0.0, 1.0, 0.01) var icon_fill_ratio_override: float = 0.0
@export_group("Motion")
@export_range(0.0, 32.0, 0.1) var horizontal_amplitude: float = 1.8
@export_range(0.0, 32.0, 0.1) var vertical_amplitude: float = 4.0
@ -23,20 +33,37 @@ const ICON_FILL_RATIO: float = 0.76
@export_range(0.0, TAU, 0.01) var motion_phase: float = 0.0
@export_range(0.0, 0.2, 0.001) var deformation_amplitude: float = 0.016
@export_range(0.1, 30.0, 0.1) var deformation_period: float = 6.0
@export var hover_wiggle_amplitude: Vector2 = Vector2.ZERO
@export_range(0.1, 30.0, 0.1) var hover_wiggle_period: float = 2.0
@export_group("Hover Drawer")
@export var hover_drawer_text: String = ""
@export_enum("Right", "Left Vertical") var hover_drawer_mode: int = 0
@export_range(0.1, 12.0, 0.1) var hover_drawer_speed: float = 4.0
@export_group("Hit Area")
@export var elliptical_hit_area: bool = true
var neutral_position: Vector2 = Vector2.ZERO
var presented_size: Vector2 = Vector2.ZERO
var emphasis: float = 0.0
var _hovered: bool = false
var _drawer_focused: bool = false
var _drawer_reveal: float = 0.0
var _gradient_background: BubbleGradientBackground
var _hover_drawer: BubbleHoverDrawer
func _ready() -> void:
mouse_entered.connect(_set_hovered.bind(true))
mouse_exited.connect(_set_hovered.bind(false))
focus_entered.connect(_set_drawer_focused.bind(true))
focus_exited.connect(_set_drawer_focused.bind(false))
resized.connect(_update_pivot)
_update_pivot()
apply_profile()
_apply_icon_presentation(neutral_size)
_update_hover_drawer()
func _gui_input(event: InputEvent) -> void:
@ -97,6 +124,8 @@ func apply_layout(
presented_size = bubble_size
_update_pivot()
_apply_icon_presentation(bubble_size)
if _hover_drawer != null:
_hover_drawer.refresh_layout()
var label_control: Control = get_label_control()
var font_size := clampi(
roundi(minf(bubble_size.x, bubble_size.y) * font_size_ratio),
@ -134,17 +163,26 @@ func _uses_icon_only_presentation() -> bool:
func _apply_icon_presentation(bubble_size: Vector2) -> void:
if icon == null:
return
vertical_icon_alignment = VERTICAL_ALIGNMENT_CENTER
expand_icon = true
texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
var fill_ratio: float = (
ICON_FILL_RATIO
if _uses_icon_only_presentation()
else LABELED_ICON_FILL_RATIO
)
if _uses_icon_only_presentation() and icon_fill_ratio_override > 0.0:
fill_ratio = icon_fill_ratio_override
add_theme_constant_override(
"icon_max_width",
maxi(1, roundi(minf(bubble_size.x, bubble_size.y) * fill_ratio)),
)
if not _uses_icon_only_presentation():
return
alignment = HORIZONTAL_ALIGNMENT_CENTER
icon_alignment = HORIZONTAL_ALIGNMENT_CENTER
vertical_icon_alignment = VERTICAL_ALIGNMENT_CENTER
expand_icon = true
texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
add_theme_constant_override(
"icon_max_width",
maxi(1, roundi(minf(bubble_size.x, bubble_size.y) * ICON_FILL_RATIO)),
)
func advance_emphasis(delta: float) -> void:
@ -154,6 +192,27 @@ func advance_emphasis(delta: float) -> void:
target,
profile.emphasis_speed * delta
)
if _gradient_background != null:
_gradient_background.queue_redraw()
var drawer_target: float = 1.0 if (_hovered or _drawer_focused) else 0.0
_drawer_reveal = move_toward(
_drawer_reveal,
drawer_target,
hover_drawer_speed * delta,
)
if _hover_drawer != null:
_hover_drawer.set_reveal(_drawer_reveal)
func reset_hover_presentation() -> void:
_hovered = false
_drawer_focused = false
emphasis = 0.0
_drawer_reveal = 0.0
if _gradient_background != null:
_gradient_background.queue_redraw()
if _hover_drawer != null:
_hover_drawer.set_reveal(0.0)
func calculate_target(
@ -169,12 +228,17 @@ func calculate_target(
sin(phase * 0.73 + motion_phase) * horizontal_amplitude,
sin(phase) * vertical_amplitude
) * layout_scale * motion_scale
var hover_phase: float = elapsed / hover_wiggle_period * TAU
var hover_offset := Vector2(
sin(hover_phase),
sin(hover_phase * 2.0 + motion_phase) * 0.5,
) * hover_wiggle_amplitude * emphasis * layout_scale * motion_scale
idle_offset.y -= (
profile.hover_focus_lift
* emphasis
* layout_scale
)
return neutral_position + idle_offset
return neutral_position + idle_offset + hover_offset
func calculate_visual_scale(
@ -219,6 +283,8 @@ func apply_presentation(
func _has_point(point: Vector2) -> bool:
if not elliptical_hit_area:
return Rect2(Vector2.ZERO, size).has_point(point)
var radius: Vector2 = size * 0.5
if radius.x <= 0.0 or radius.y <= 0.0:
return false
@ -228,6 +294,12 @@ func _has_point(point: Vector2) -> bool:
func _set_hovered(value: bool) -> void:
_hovered = value
if _gradient_background != null:
_gradient_background.queue_redraw()
func _set_drawer_focused(value: bool) -> void:
_drawer_focused = value
func _update_pivot() -> void:
@ -237,6 +309,7 @@ func _update_pivot() -> void:
func apply_profile() -> void:
if profile == null:
return
_update_gradient_background()
add_theme_stylebox_override("normal", profile.make_normal_style())
var hover_style: StyleBoxFlat = profile.make_hover_style()
add_theme_stylebox_override("hover", hover_style)
@ -251,3 +324,35 @@ func apply_profile() -> void:
var label_control: Control = get_label_control()
if label_control != self:
label_control.add_theme_color_override("font_color", profile.text_color)
func _update_gradient_background() -> void:
if not profile.gradient_enabled:
if _gradient_background != null:
_gradient_background.queue_free()
_gradient_background = null
return
if _gradient_background == null:
_gradient_background = BubbleGradientBackgroundType.new()
_gradient_background.name = "GradientBackground"
add_child(_gradient_background)
move_child(_gradient_background, 0)
_gradient_background.configure(self, profile)
func _update_hover_drawer() -> void:
if hover_drawer_text.is_empty():
if _hover_drawer != null:
_hover_drawer.queue_free()
_hover_drawer = null
return
if _hover_drawer == null:
_hover_drawer = BubbleHoverDrawerType.new()
_hover_drawer.name = "HoverDrawer"
add_child(_hover_drawer)
move_child(_hover_drawer, 0)
_hover_drawer.configure(
self,
hover_drawer_text,
hover_drawer_mode as BubbleHoverDrawer.DrawerMode,
)

View file

@ -9,6 +9,7 @@ const ControllerFocusNavigationType = preload(
@export var desktop_reference_size: Vector2 = Vector2(396.0, 318.0)
@export var compact_reference_size: Vector2 = Vector2(294.0, 200.0)
@export_range(0.0, 1.0, 0.01) var motion_scale: float = 1.0
@export var collision_separation_enabled: bool = true
var _bubbles: Array[BubbleButton] = []
var _elapsed: float = 0.0
@ -70,39 +71,40 @@ func advance_motion(delta: float) -> void:
targets.append(target)
visual_scales.append(visual_scale)
visual_radii.append(bubble.get_visual_radius(visual_scale))
for first_index: int in _bubbles.size():
for second_index: int in range(first_index + 1, _bubbles.size()):
var first_center: Vector2 = (
targets[first_index]
+ _bubbles[first_index].presented_size * 0.5
)
var second_center: Vector2 = (
targets[second_index]
+ _bubbles[second_index].presented_size * 0.5
)
var center_delta: Vector2 = second_center - first_center
var distance: float = center_delta.length()
var desired_distance: float = (
visual_radii[first_index]
+ visual_radii[second_index]
+ profile.contact_gap * _layout_scale
)
if distance >= desired_distance:
continue
var direction := (
center_delta / distance
if distance > 0.001
else Vector2.RIGHT.rotated(float(first_index + 1))
)
var correction: Vector2 = (
direction
* minf(
(desired_distance - distance) * 0.5,
profile.maximum_separation * _layout_scale
if collision_separation_enabled:
for first_index: int in _bubbles.size():
for second_index: int in range(first_index + 1, _bubbles.size()):
var first_center: Vector2 = (
targets[first_index]
+ _bubbles[first_index].presented_size * 0.5
)
)
targets[first_index] -= correction
targets[second_index] += correction
var second_center: Vector2 = (
targets[second_index]
+ _bubbles[second_index].presented_size * 0.5
)
var center_delta: Vector2 = second_center - first_center
var distance: float = center_delta.length()
var desired_distance: float = (
visual_radii[first_index]
+ visual_radii[second_index]
+ profile.contact_gap * _layout_scale
)
if distance >= desired_distance:
continue
var direction := (
center_delta / distance
if distance > 0.001
else Vector2.RIGHT.rotated(float(first_index + 1))
)
var correction: Vector2 = (
direction
* minf(
(desired_distance - distance) * 0.5,
profile.maximum_separation * _layout_scale
)
)
targets[first_index] -= correction
targets[second_index] += correction
var position_weight: float = 1.0 - exp(
-profile.position_response * delta
)

View file

@ -0,0 +1,79 @@
class_name BubbleGradientBackground
extends Control
const CORNER_SEGMENTS: int = 8
var host_button: BaseButton
var profile: BubbleMenuProfile
func configure(
button: BaseButton,
menu_profile: BubbleMenuProfile,
) -> void:
host_button = button
profile = menu_profile
mouse_filter = Control.MOUSE_FILTER_IGNORE
show_behind_parent = true
set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
queue_redraw()
func _draw() -> void:
if host_button == null or profile == null or size.x <= 0.0 or size.y <= 0.0:
return
var fill_color: Color = _get_fill_color()
var top_color: Color = fill_color.lightened(profile.gradient_top_lift)
var points := PackedVector2Array()
_append_arc(points, Vector2(profile.corner_radius, profile.corner_radius), PI, PI * 1.5)
_append_arc(
points,
Vector2(size.x - profile.corner_radius, profile.corner_radius),
PI * 1.5,
TAU,
)
_append_arc(
points,
Vector2(size.x - profile.corner_radius, size.y - profile.corner_radius),
0.0,
PI * 0.5,
)
_append_arc(
points,
Vector2(profile.corner_radius, size.y - profile.corner_radius),
PI * 0.5,
PI,
)
var colors := PackedColorArray()
for point: Vector2 in points:
colors.append(top_color.lerp(fill_color, clampf(point.y / size.y, 0.0, 1.0)))
draw_polygon(points, colors)
func _append_arc(
points: PackedVector2Array,
center: Vector2,
start_angle: float,
end_angle: float,
) -> void:
var radius: float = minf(
float(profile.corner_radius),
minf(size.x, size.y) * 0.5,
)
for index: int in range(CORNER_SEGMENTS + 1):
var weight: float = float(index) / float(CORNER_SEGMENTS)
var angle: float = lerpf(start_angle, end_angle, weight)
points.append(center + Vector2(cos(angle), sin(angle)) * radius)
func _get_fill_color() -> Color:
if host_button.disabled:
return profile.disabled_fill
match host_button.get_draw_mode():
BaseButton.DRAW_PRESSED, BaseButton.DRAW_HOVER_PRESSED:
return profile.pressed_fill
BaseButton.DRAW_HOVER:
return profile.hover_fill
if host_button.has_focus():
return profile.hover_fill
return profile.normal_fill

View file

@ -0,0 +1 @@
uid://cckk3lwdqadjn

View file

@ -0,0 +1,97 @@
class_name BubbleHoverDrawer
extends Control
enum DrawerMode {
RIGHT,
LEFT_VERTICAL,
}
const DRAWER_COLOR: Color = Color(0.031, 0.122, 0.169, 1.0)
const TEXT_COLOR: Color = Color(0.764706, 0.87451, 0.901961, 1.0)
const RIGHT_SIZE: Vector2 = Vector2(132.0, 48.0)
const LEFT_VERTICAL_SIZE: Vector2 = Vector2(50.0, 176.0)
const OVERLAP: float = 12.0
var host_button: BaseButton
var drawer_mode: DrawerMode = DrawerMode.RIGHT
var _panel: Panel
var _label: Label
func configure(
button: BaseButton,
drawer_text: String,
mode: DrawerMode,
) -> void:
host_button = button
drawer_mode = mode
mouse_filter = Control.MOUSE_FILTER_IGNORE
show_behind_parent = true
_build_controls()
_label.text = drawer_text
_refresh_label_layout()
refresh_layout()
set_reveal(0.0)
func refresh_layout() -> void:
if host_button == null:
return
if drawer_mode == DrawerMode.RIGHT:
size = RIGHT_SIZE
position = Vector2(
host_button.size.x - OVERLAP,
(host_button.size.y - size.y) * 0.5,
)
pivot_offset = Vector2(0.0, size.y * 0.5)
else:
size = LEFT_VERTICAL_SIZE
position = Vector2(
-size.x + OVERLAP,
(host_button.size.y - size.y) * 0.5,
)
pivot_offset = Vector2(size.x, size.y * 0.5)
_refresh_label_layout()
func set_reveal(amount: float) -> void:
var reveal: float = clampf(amount, 0.0, 1.0)
visible = reveal > 0.001
scale = Vector2(maxf(reveal, 0.001), 1.0)
func _build_controls() -> void:
if _panel != null:
return
_panel = Panel.new()
_panel.mouse_filter = Control.MOUSE_FILTER_IGNORE
_panel.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
var panel_style := StyleBoxFlat.new()
panel_style.bg_color = DRAWER_COLOR
panel_style.corner_radius_top_left = 12
panel_style.corner_radius_top_right = 12
panel_style.corner_radius_bottom_right = 12
panel_style.corner_radius_bottom_left = 12
_panel.add_theme_stylebox_override("panel", panel_style)
add_child(_panel)
_label = Label.new()
_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
_label.add_theme_color_override("font_color", TEXT_COLOR)
_label.add_theme_font_size_override("font_size", 20)
_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
add_child(_label)
func _refresh_label_layout() -> void:
if _label == null:
return
if drawer_mode == DrawerMode.RIGHT:
_label.rotation = 0.0
_label.position = Vector2.ZERO
_label.size = size
else:
_label.rotation = -PI * 0.5
_label.position = Vector2(-OVERLAP * 0.5, size.y)
_label.size = Vector2(size.y, size.x)

View file

@ -0,0 +1 @@
uid://jvw68gltfda0

View file

@ -21,6 +21,10 @@ extends Resource
@export_range(0, 12, 1) var normal_border_width: int = 2
@export_range(0, 12, 1) var emphasized_border_width: int = 3
@export_group("Gradient")
@export var gradient_enabled: bool = false
@export_range(0.0, 0.5, 0.01) var gradient_top_lift: float = 0.12
@export_group("Colors")
@export var normal_fill: Color = Color(0.82, 0.93, 0.96, 0.96)
@export var normal_border: Color = Color(0.91, 0.98, 1.0, 0.85)
@ -78,7 +82,11 @@ func _make_style(
style.content_margin_top = content_margin
style.content_margin_right = content_margin
style.content_margin_bottom = content_margin
style.bg_color = fill_color
style.bg_color = (
Color(fill_color.r, fill_color.g, fill_color.b, 0.0)
if gradient_enabled
else fill_color
)
style.border_width_left = outline_width
style.border_width_top = outline_width
style.border_width_right = outline_width

View file

@ -0,0 +1,22 @@
[gd_resource type="Resource" script_class="BubbleMenuProfile" load_steps=2 format=3]
[ext_resource type="Script" path="res://ui/components/bubble_menu/bubble_menu_profile.gd" id="1_profile"]
[resource]
script = ExtResource("1_profile")
font_size_ratio = 0.14
hover_focus_scale = 1.02
hover_focus_lift = 0.0
emphasis_speed = 7.0
emphasized_deformation_scale = 0.0
contact_gap = 0.0
maximum_separation = 0.0
content_margin = 6.0
corner_radius = 36
normal_border_width = 0
emphasized_border_width = 0
gradient_enabled = false
normal_fill = Color(1, 1, 1, 1)
hover_fill = Color(1, 1, 1, 1)
pressed_fill = Color(1, 1, 1, 1)
disabled_fill = Color(1, 1, 1, 0.78)

View file

@ -0,0 +1,22 @@
[gd_resource type="Resource" script_class="BubbleMenuProfile" load_steps=2 format=3]
[ext_resource type="Script" path="res://ui/components/bubble_menu/bubble_menu_profile.gd" id="1_profile"]
[resource]
script = ExtResource("1_profile")
font_size_ratio = 0.14
hover_focus_scale = 1.02
hover_focus_lift = 0.0
emphasis_speed = 7.0
emphasized_deformation_scale = 0.0
contact_gap = 0.0
maximum_separation = 0.0
content_margin = 6.0
corner_radius = 16
normal_border_width = 0
emphasized_border_width = 0
gradient_enabled = false
normal_fill = Color(1, 1, 1, 1)
hover_fill = Color(1, 1, 1, 1)
pressed_fill = Color(1, 1, 1, 1)
disabled_fill = Color(1, 1, 1, 0.78)