From 6b687b48376b025a3c3f7d099d0efe3b4e527df1 Mon Sep 17 00:00:00 2001 From: Face <69168154+face-hh@users.noreply.github.com> Date: Sun, 10 Aug 2025 15:24:49 +0300 Subject: [PATCH] CSS animations --- README.md | 2 + flumi/Scripts/B9/CSSParser.gd | 4 + flumi/Scripts/Constants.gd | 7 +- flumi/Scripts/StyleManager.gd | 97 +++++++++----------- flumi/Scripts/Tags/button.gd | 52 +++++++++++ flumi/Scripts/Utils/TransformUtils.gd | 18 +--- flumi/Scripts/Utils/UtilityClassValidator.gd | 5 +- 7 files changed, 111 insertions(+), 74 deletions(-) diff --git a/README.md b/README.md index 6314c63..a09d98a 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,8 @@ TODO: Issues: 1. **< br />** counts as 1 element in **WebsiteContainer**, therefore despite being (0,0) in size, it counts as double in spacing 2. **Tween** API doesn't modify CSS, it operates independently at Godot level. +3. Certain properties like `scale` and `rotate` don't apply to the `active` pseudo-class because they rely on mouse_enter and mouse_exit events +4. `
Box
` something like this has the "Box" text (presumably the PanelContainer) as the target of the hover, not the div itself (which has the w/h size) Notes: - **< input />** is sort-of inline in normal web. We render it as a block element (new-line). diff --git a/flumi/Scripts/B9/CSSParser.gd b/flumi/Scripts/B9/CSSParser.gd index c147413..dfb529f 100644 --- a/flumi/Scripts/B9/CSSParser.gd +++ b/flumi/Scripts/B9/CSSParser.gd @@ -908,6 +908,10 @@ static func parse_utility_class_internal(rule: CSSRule, utility_name: String) -> for property in transform_properties: rule.properties[property] = transform_properties[property] return + + if utility_name in ["transition", "transition-colors", "transition-opacity", "transition-transform"]: + rule.properties[utility_name] = "200ms" + return # Handle more utility classes as needed # Add more cases here for other utilities diff --git a/flumi/Scripts/Constants.gd b/flumi/Scripts/Constants.gd index 3fbf404..b8df525 100644 --- a/flumi/Scripts/Constants.gd +++ b/flumi/Scripts/Constants.gd @@ -2636,12 +2636,13 @@ var HTML_CONTENT_TRANSFORM_TEST = """

Hover Effect

Hover to see scale effect
-
Box
+
Box
hover:scale-110
- - + + +

📝 Transform Utility Reference

diff --git a/flumi/Scripts/StyleManager.gd b/flumi/Scripts/StyleManager.gd index d222a55..bd21331 100644 --- a/flumi/Scripts/StyleManager.gd +++ b/flumi/Scripts/StyleManager.gd @@ -680,67 +680,58 @@ static func apply_transform_properties(node: Control, styles: Dictionary) -> voi apply_transform_properties_direct(node, styles) static func apply_transform_properties_direct(node: Control, styles: Dictionary) -> void: - var has_transform = false - var scale_x = 1.0 - var scale_y = 1.0 - var rotation_z = 0.0 + var scale_x = styles.get("scale-x", 1.0) + var scale_y = styles.get("scale-y", 1.0) + var rotation = styles.get("rotate", 0.0) - # Check for scale properties - if styles.has("scale-x"): - scale_x = styles["scale-x"] - has_transform = true - if styles.has("scale-y"): - scale_y = styles["scale-y"] - has_transform = true - - # Check for rotation properties (prioritize Z-axis for 2D) - if styles.has("rotate-z"): - rotation_z = styles["rotate-z"] - has_transform = true - elif styles.has("rotate-x"): - rotation_z = styles["rotate-x"] - has_transform = true - elif styles.has("rotate-y"): - rotation_z = styles["rotate-y"] - has_transform = true + var has_transform = scale_x != 1.0 or scale_y != 1.0 or rotation != 0.0 + var duration = get_transition_duration(styles) if has_transform: - # Set metadata flag to tell FlexContainer to preserve transforms node.set_meta("css_transform_applied", true) - node.set_meta("pending_scale", Vector2(scale_x, scale_y)) - node.set_meta("pending_rotation", rotation_z) - # Apply immediately - node.scale = Vector2(scale_x, scale_y) - node.rotation = rotation_z + # Set pivot point to center + node.pivot_offset = node.size / 2 - # Reapply after FlexContainer layout using timer - var timer = Timer.new() - timer.wait_time = 0.1 - timer.one_shot = true - timer.autostart = true - var main_scene = Engine.get_main_loop().current_scene - if main_scene: - timer.timeout.connect(func(): _reapply_transforms(node)) - main_scene.add_child(timer) + if duration > 0: + animate_transform(node, Vector2(scale_x, scale_y), rotation, duration) + else: + node.scale = Vector2(scale_x, scale_y) + node.rotation = rotation + await_and_restore_transform(node, Vector2(scale_x, scale_y), rotation) else: - # Reset transforms to default when no transforms are specified - node.scale = Vector2.ONE - node.rotation = 0.0 + if duration > 0: + animate_transform(node, Vector2.ONE, 0.0, duration) + else: + node.scale = Vector2.ONE + node.rotation = 0.0 if node.has_meta("css_transform_applied"): node.remove_meta("css_transform_applied") -static func _reapply_transforms(node: Control) -> void: - if not is_instance_valid(node): - return +static func get_transition_duration(styles: Dictionary) -> float: + if styles.has("transition-transform"): + return parse_transition_duration(styles["transition-transform"]) + elif styles.has("transition"): + return parse_transition_duration(styles["transition"]) + return 0.0 + +static func parse_transition_duration(value: String) -> float: + if value.ends_with("ms"): + return float(value.replace("ms", "")) / 1000.0 + elif value.ends_with("s"): + return float(value.replace("s", "")) + return float(value) if value.is_valid_float() else 0.0 + +static func animate_transform(node: Control, target_scale: Vector2, target_rotation: float, duration: float) -> void: + var tween = node.create_tween() + tween.set_parallel(true) + tween.tween_property(node, "scale", target_scale, duration).set_trans(Tween.TRANS_CUBIC).set_ease(Tween.EASE_IN_OUT) + tween.tween_property(node, "rotation", target_rotation, duration).set_trans(Tween.TRANS_CUBIC).set_ease(Tween.EASE_IN_OUT) + +static func await_and_restore_transform(node: Control, target_scale: Vector2, target_rotation: float) -> void: + var tree = Engine.get_main_loop() - if node.has_meta("pending_scale"): - node.scale = node.get_meta("pending_scale") - - if node.has_meta("pending_rotation"): - node.rotation = node.get_meta("pending_rotation") - - if node.has_meta("pending_scale"): - node.remove_meta("pending_scale") - if node.has_meta("pending_rotation"): - node.remove_meta("pending_rotation") + await tree.process_frame + node.scale = target_scale + node.rotation = target_rotation + node.pivot_offset = node.size / 2 diff --git a/flumi/Scripts/Tags/button.gd b/flumi/Scripts/Tags/button.gd index 4bd6fb2..8f768fb 100644 --- a/flumi/Scripts/Tags/button.gd +++ b/flumi/Scripts/Tags/button.gd @@ -51,6 +51,9 @@ func apply_button_styles(element: HTMLParser.HTMLElement, parser: HTMLParser) -> # Apply text color with state-dependent colors apply_button_text_color(button_node, styles, hover_styles, active_styles) + # Apply transform properties with hover support + apply_button_transforms(button_node, styles, hover_styles) + # Apply background color (hover: + active:) if styles.has("background-color"): var normal_color: Color = styles["background-color"] @@ -194,3 +197,52 @@ func apply_size_and_flags(ctrl: Control, width: Variant, height: Variant) -> voi ctrl.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN if height != null: ctrl.size_flags_vertical = Control.SIZE_SHRINK_BEGIN + +func apply_button_transforms(button: Button, normal_styles: Dictionary, hover_styles: Dictionary) -> void: + # Apply normal transforms to the parent HBoxContainer (self) + StyleManager.apply_transform_properties_direct(self, normal_styles) + + # Set pivot point to center of the container + self.pivot_offset = self.size / 2 + + # Set up hover transforms if present + var has_hover_transforms = hover_styles.has("scale-x") or hover_styles.has("scale-y") or hover_styles.has("rotate") + if has_hover_transforms: + # Store original and hover values + var original_scale = Vector2( + normal_styles.get("scale-x", 1.0), + normal_styles.get("scale-y", 1.0) + ) + var original_rotation = normal_styles.get("rotate", 0.0) + + var hover_scale = Vector2( + hover_styles.get("scale-x", original_scale.x), + hover_styles.get("scale-y", original_scale.y) + ) + var hover_rotation = hover_styles.get("rotate", original_rotation) + + # Get transition duration + var duration = StyleManager.get_transition_duration(normal_styles) + if duration == 0: + duration = StyleManager.get_transition_duration(hover_styles) + + # Connect hover events to the button but apply transforms to self + button.mouse_entered.connect(func(): + # Update pivot point in case size changed + self.pivot_offset = self.size / 2 + if duration > 0: + StyleManager.animate_transform(self, hover_scale, hover_rotation, duration) + else: + self.scale = hover_scale + self.rotation = hover_rotation + ) + + button.mouse_exited.connect(func(): + # Update pivot point in case size changed + self.pivot_offset = self.size / 2 + if duration > 0: + StyleManager.animate_transform(self, original_scale, original_rotation, duration) + else: + self.scale = original_scale + self.rotation = original_rotation + ) diff --git a/flumi/Scripts/Utils/TransformUtils.gd b/flumi/Scripts/Utils/TransformUtils.gd index 3c4436f..6620507 100644 --- a/flumi/Scripts/Utils/TransformUtils.gd +++ b/flumi/Scripts/Utils/TransformUtils.gd @@ -132,26 +132,12 @@ static func parse_scale_utility(utility_name: String) -> Dictionary: static func parse_rotation_utility(utility_name: String) -> Dictionary: var result = {} - if utility_name.begins_with("rotate-x-"): - var val = utility_name.substr(9) # after "rotate-x-" - if val.begins_with("[") and val.ends_with("]"): - val = val.substr(1, val.length() - 2) - var rotation = parse_rotation(val) - result["rotate-x"] = rotation - return result - elif utility_name.begins_with("rotate-y-"): - var val = utility_name.substr(9) # after "rotate-y-" - if val.begins_with("[") and val.ends_with("]"): - val = val.substr(1, val.length() - 2) - var rotation = parse_rotation(val) - result["rotate-y"] = rotation - return result - elif utility_name.begins_with("rotate-"): + if utility_name.begins_with("rotate-"): var val = utility_name.substr(7) # after "rotate-" if val.begins_with("[") and val.ends_with("]"): val = val.substr(1, val.length() - 2) var rotation = parse_rotation(val) - result["rotate-z"] = rotation # Default rotation is around Z-axis + result["rotate"] = rotation return result return result diff --git a/flumi/Scripts/Utils/UtilityClassValidator.gd b/flumi/Scripts/Utils/UtilityClassValidator.gd index 7817cad..03c05aa 100644 --- a/flumi/Scripts/Utils/UtilityClassValidator.gd +++ b/flumi/Scripts/Utils/UtilityClassValidator.gd @@ -43,8 +43,9 @@ static func init_patterns(): "^cursor-[a-zA-Z-]+$", # cursor types "^scale-(x-|y-)?\\d+$", # scale utilities like scale-100, scale-x-75, scale-y-150 "^scale-(x-|y-)?\\[.*\\]$", # custom scale values like scale-[1.5], scale-x-[2.0] - "^rotate-(x-|y-)?\\d+$", # rotation utilities like rotate-45, rotate-x-90, rotate-y-180 - "^rotate-(x-|y-)?\\[.*\\]$", # custom rotation values like rotate-[45deg], rotate-x-[90deg], rotate-y-[3.5rad] + "^rotate-\\d+$", # rotation utilities like rotate-45, rotate-90 + "^rotate-\\[.*\\]$", # custom rotation values like rotate-[45deg], rotate-[3.5rad] + "^transition(-colors|-opacity|-transform)?$", # transition utilities "^(hover|active):", # pseudo classes ] for pattern in utility_patterns: