scale and rotate

This commit is contained in:
Face
2025-08-10 14:52:35 +03:00
parent 6fdf422f7e
commit 0e069bd438
7 changed files with 422 additions and 2 deletions

View File

@@ -897,6 +897,18 @@ static func parse_utility_class_internal(rule: CSSRule, utility_name: String) ->
"object-contain": rule.properties["object-fit"] = "contain"; return
"object-cover": rule.properties["object-fit"] = "cover"; return
if utility_name.begins_with("scale-") or utility_name.begins_with("scale-x-") or utility_name.begins_with("scale-y-"):
var transform_properties = TransformUtils.parse_scale_utility(utility_name)
for property in transform_properties:
rule.properties[property] = transform_properties[property]
return
if utility_name.begins_with("rotate-") or utility_name.begins_with("rotate-x-") or utility_name.begins_with("rotate-y-"):
var transform_properties = TransformUtils.parse_rotation_utility(utility_name)
for property in transform_properties:
rule.properties[property] = transform_properties[property]
return
# Handle more utility classes as needed
# Add more cases here for other utilities

View File

@@ -2523,6 +2523,139 @@ var HTML_CONTENT = """<head>
</body>
""".to_utf8_buffer()
# Set the active HTML content to use the tween demo
var HTML_CONTENT_TRANSFORM_TEST = """<head>
<title>Transform CSS Demo - Scale & Rotation</title>
<icon src="https://picsum.photos/32/32?random=1">
<meta name="theme-color" content="#8b5cf6">
<meta name="description" content="Testing scale and rotation CSS utilities">
<style>
body { bg-[#f8fafc] p-8 }
h1 { text-[#8b5cf6] text-4xl font-bold text-center mb-8 }
h2 { text-[#7c3aed] text-2xl font-semibold mb-4 }
.demo-grid { flex flex-wrap gap-6 justify-center }
.demo-item { bg-white p-6 rounded-xl shadow-lg text-center }
.demo-box { w-[100px] h-[100px] bg-[#3b82f6] text-white text-xl font-bold rounded-lg flex items-center justify-center mb-4 mx-auto }
.code-block { bg-[#1e293b] text-[#e2e8f0] p-3 rounded font-mono text-sm }
.description { text-[#64748b] text-sm mb-3 }
</style>
</head>
<body>
<h1>🔄 Scale & Rotation CSS Demonstration</h1>
<div style="demo-grid">
<!-- Normal (no transform) -->
<div style="demo-item">
<h2>Normal</h2>
<div style="description">No transform applied</div>
<div style="demo-box">Box</div>
<div style="code-block">No transform</div>
</div>
<!-- Scale 50% -->
<div style="demo-item">
<h2>Scale 50%</h2>
<div style="description">Scaled to 50% size</div>
<div style="demo-box scale-50">Box</div>
<div style="code-block">scale-50</div>
</div>
<!-- Scale 150% -->
<div style="demo-item">
<h2>Scale 150%</h2>
<div style="description">Scaled to 150% size</div>
<div style="demo-box scale-150">Box</div>
<div style="code-block">scale-150</div>
</div>
<!-- Scale X-axis only -->
<div style="demo-item">
<h2>Scale X 75%</h2>
<div style="description">X-axis scaled to 75%</div>
<div style="demo-box scale-x-75">Box</div>
<div style="code-block">scale-x-75</div>
</div>
<!-- Scale Y-axis only -->
<div style="demo-item">
<h2>Scale Y 125%</h2>
<div style="description">Y-axis scaled to 125%</div>
<div style="demo-box scale-y-125">Box</div>
<div style="code-block">scale-y-125</div>
</div>
<!-- Custom scale with bracket notation -->
<div style="demo-item">
<h2>Custom Scale</h2>
<div style="description">Custom scale value 1.7</div>
<div style="demo-box scale-[170]">Box</div>
<div style="code-block">scale-[170]</div>
</div>
<!-- Rotate 45 degrees -->
<div style="demo-item">
<h2>Rotate 45°</h2>
<div style="description">Rotated 45 degrees</div>
<div style="demo-box rotate-45">Box</div>
<div style="code-block">rotate-45</div>
</div>
<!-- Rotate 90 degrees -->
<div style="demo-item">
<h2>Rotate 90°</h2>
<div style="description">Rotated 90 degrees</div>
<div style="demo-box rotate-90">Box</div>
<div style="code-block">rotate-90</div>
</div>
<!-- Custom rotation with bracket notation -->
<div style="demo-item">
<h2>Custom Rotate</h2>
<div style="description">Custom rotation 30deg</div>
<div style="demo-box rotate-[30deg]">Box</div>
<div style="code-block">rotate-[30deg]</div>
</div>
<!-- Combined scale and rotation -->
<div style="demo-item">
<h2>Scale + Rotate</h2>
<div style="description">Scale 125% and rotate 45°</div>
<div style="demo-box scale-125 rotate-45">Box</div>
<div style="code-block">scale-125 rotate-45</div>
</div>
<!-- Rotation in radians -->
<div style="demo-item">
<h2>Radians</h2>
<div style="description">Rotation using radians</div>
<div style="demo-box rotate-[1.57rad]">Box</div>
<div style="code-block">rotate-[1.57rad]</div>
</div>
<!-- Hover effects with transforms -->
<div style="demo-item">
<h2>Hover Effect</h2>
<div style="description">Hover to see scale effect</div>
<div style="demo-box hover:scale-110">Box</div>
<div style="code-block">hover:scale-110</div>
</div>
</div>
<div style="bg-white p-6 rounded-xl shadow-lg mt-8 max-w-4xl mx-auto">
<h2>📝 Transform Utility Reference</h2>
<div style="text-[#475569]">
<p><strong>Scale utilities:</strong> scale-{value}, scale-x-{value}, scale-y-{value}</p>
<p><strong>Named values:</strong> 0, 50, 75, 90, 95, 100, 105, 110, 125, 150, 200</p>
<p><strong>Custom values:</strong> scale-[{number}] where number is percentage (100 = 1.0)</p>
<p><strong>Rotation utilities:</strong> rotate-{degrees}, rotate-x-{degrees}, rotate-y-{degrees}</p>
<p><strong>Named degrees:</strong> 0, 1, 2, 3, 6, 12, 45, 90, 180, 270</p>
<p><strong>Custom rotations:</strong> rotate-[{value}deg], rotate-[{value}rad], rotate-[{value}turn]</p>
</div>
</div>
</body>
""".to_utf8_buffer()
# Set the active HTML content to use the transform demo
func _ready():
HTML_CONTENT = HTML_CONTENT_TWEEN
HTML_CONTENT = HTML_CONTENT_TRANSFORM_TEST

View File

@@ -181,6 +181,14 @@ static func apply_element_styles(node: Control, element: HTMLParser.HTMLElement,
if label:
apply_styles_to_label(label, styles, element, parser)
var transform_target = node
if node is MarginContainer and node.name.begins_with("MarginWrapper_"):
if node.get_child_count() > 0:
transform_target = node.get_child(0)
apply_transform_properties(transform_target, styles)
return node
static func apply_stylebox_to_panel_container(panel_container: PanelContainer, styles: Dictionary) -> void:
@@ -651,3 +659,88 @@ static func apply_image_styles(image_node: Control, styles: Dictionary) -> void:
texture_rect.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT
"cover":
texture_rect.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_COVERED
static func apply_transform_properties(node: Control, styles: Dictionary) -> void:
if node is FlexContainer:
var has_panel_children = false
for child in node.get_children():
if child is PanelContainer:
has_panel_children = true
break
if has_panel_children:
apply_transform_properties_direct(node, styles)
else:
for child in node.get_children():
if child is RichTextLabel or child is Control:
apply_transform_properties_direct(child, styles)
return
apply_transform_properties_direct(node, styles)
else:
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
# 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
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
# 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)
else:
# Reset transforms to default when no transforms are specified
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
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")

View File

@@ -182,6 +182,8 @@ static func setup_panel_hover_support(panel: PanelContainer, normal_styles: Dict
# Store references for the hover handlers
panel.set_meta("normal_stylebox", normal_stylebox)
panel.set_meta("hover_stylebox", hover_stylebox)
panel.set_meta("normal_styles", normal_styles.duplicate(true))
panel.set_meta("hover_styles", merged_hover_styles.duplicate(true))
# Connect mouse events
panel.mouse_entered.connect(_on_panel_mouse_entered.bind(panel))
@@ -191,11 +193,28 @@ static func _on_panel_mouse_entered(panel: PanelContainer):
if panel.has_meta("hover_stylebox"):
var hover_stylebox = panel.get_meta("hover_stylebox")
panel.add_theme_stylebox_override("panel", hover_stylebox)
if panel.has_meta("hover_styles"):
var hover_styles = panel.get_meta("hover_styles")
var transform_target = find_transform_target_for_panel(panel)
StyleManager.apply_transform_properties_direct(transform_target, hover_styles)
static func _on_panel_mouse_exited(panel: PanelContainer):
if panel.has_meta("normal_stylebox"):
var normal_stylebox = panel.get_meta("normal_stylebox")
panel.add_theme_stylebox_override("panel", normal_stylebox)
if panel.has_meta("normal_styles"):
var normal_styles = panel.get_meta("normal_styles")
var transform_target = find_transform_target_for_panel(panel)
StyleManager.apply_transform_properties_direct(transform_target, normal_styles)
static func find_transform_target_for_panel(panel: PanelContainer) -> Control:
var parent = panel.get_parent()
if parent and parent is FlexContainer:
return parent
return panel
static func needs_background_wrapper(styles: Dictionary) -> bool:
return styles.has("background-color") or styles.has("border-radius") or styles.has("padding") or styles.has("padding-top") or styles.has("padding-right") or styles.has("padding-bottom") or styles.has("padding-left") or styles.has("border-width") or styles.has("border-top-width") or styles.has("border-right-width") or styles.has("border-bottom-width") or styles.has("border-left-width") or styles.has("border-color") or styles.has("border-style") or styles.has("border-top-color") or styles.has("border-right-color") or styles.has("border-bottom-color") or styles.has("border-left-color")

View File

@@ -0,0 +1,158 @@
class_name TransformUtils
extends RefCounted
# Utility functions for parsing CSS transform values (scale and rotation)
# Parse scale values - returns a Vector2 with x and y scale factors
static func parse_scale(val: String) -> Vector2:
if val == null or val.is_empty():
return Vector2.ONE
# Named scale values
var named = {
"0": Vector2.ZERO,
"50": Vector2(0.5, 0.5),
"75": Vector2(0.75, 0.75),
"90": Vector2(0.9, 0.9),
"95": Vector2(0.95, 0.95),
"100": Vector2.ONE,
"105": Vector2(1.05, 1.05),
"110": Vector2(1.1, 1.1),
"125": Vector2(1.25, 1.25),
"150": Vector2(1.5, 1.5),
"200": Vector2(2.0, 2.0)
}
if named.has(val):
return named[val]
# Direct numeric value (treat as percentage: 100 = 1.0, 50 = 0.5, etc.)
if val.is_valid_int():
var scale_factor = float(val) / 100.0
return Vector2(scale_factor, scale_factor)
# Direct decimal value
if val.is_valid_float():
var scale_factor = float(val)
return Vector2(scale_factor, scale_factor)
return Vector2.ONE
# Parse rotation values - returns rotation in radians
static func parse_rotation(val: String) -> float:
if val == null or val.is_empty():
return 0.0
# Named rotation values (in degrees, converted to radians)
var named = {
"0": 0.0,
"1": deg_to_rad(1.0),
"2": deg_to_rad(2.0),
"3": deg_to_rad(3.0),
"6": deg_to_rad(6.0),
"12": deg_to_rad(12.0),
"45": deg_to_rad(45.0),
"90": deg_to_rad(90.0),
"180": deg_to_rad(180.0),
"270": deg_to_rad(270.0)
}
if named.has(val):
return named[val]
# Handle explicit unit specifications
if val.ends_with("deg"):
var degrees = float(val.replace("deg", ""))
return deg_to_rad(degrees)
elif val.ends_with("rad"):
return float(val.replace("rad", ""))
elif val.ends_with("turn"):
var turns = float(val.replace("turn", ""))
return turns * 2.0 * PI
# Direct numeric value - assume degrees
if val.is_valid_float():
return deg_to_rad(float(val))
return 0.0
# Parse arbitrary value from brackets like [1.7] or [45deg] or [3.5rad]
static func parse_bracket_value(bracket_content: String, value_type: String):
if bracket_content.is_empty():
return null
match value_type:
"scale":
return parse_scale(bracket_content)
"rotation":
return parse_rotation(bracket_content)
_:
return null
# Extract bracket content for transform utilities
static func extract_bracket_content(string: String, start_idx: int) -> String:
var open_idx = string.find("[", start_idx)
if open_idx == -1:
return ""
var close_idx = string.find("]", open_idx)
if close_idx == -1:
return ""
return string.substr(open_idx + 1, close_idx - open_idx - 1)
# Parse scale utility and return the appropriate property name and value
static func parse_scale_utility(utility_name: String) -> Dictionary:
var result = {}
if utility_name.begins_with("scale-x-"):
var val = utility_name.substr(8) # after "scale-x-"
if val.begins_with("[") and val.ends_with("]"):
val = val.substr(1, val.length() - 2)
var scale_vec = parse_scale(val)
result["scale-x"] = scale_vec.x
return result
elif utility_name.begins_with("scale-y-"):
var val = utility_name.substr(8) # after "scale-y-"
if val.begins_with("[") and val.ends_with("]"):
val = val.substr(1, val.length() - 2)
var scale_vec = parse_scale(val)
result["scale-y"] = scale_vec.y
return result
elif utility_name.begins_with("scale-"):
var val = utility_name.substr(6) # after "scale-"
if val.begins_with("[") and val.ends_with("]"):
val = val.substr(1, val.length() - 2)
var scale_vec = parse_scale(val)
result["scale-x"] = scale_vec.x
result["scale-y"] = scale_vec.y
return result
return result
# Parse rotation utility and return the appropriate property name and value
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-"):
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
return result
return result

View File

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

View File

@@ -41,6 +41,10 @@ static func init_patterns():
"^opacity-\\[.*\\]$", # custom opacity values
"^z-\\[.*\\]$", # custom z-index values
"^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]
"^(hover|active):", # pseudo classes
]
for pattern in utility_patterns: