extends RichTextEffect ## Shows spriteframes within richtext label.[br] ## @usage use prepare_message before to load the image and prepare the message accordingly class_name SpriteFrameEffect const TRANSPARENT = preload("res://addons/twitcher/assets/transparent.tres") static var regex: RegEx = RegEx.create_from_string("\\[sprite id=(?.*?)\\](?.*?)\\[/sprite\\]") ## Custom BB Code to use var bbcode = "sprite" ## To track the emojis ## Key: Id as String Value: Emoji var cache: Dictionary = {} ## To check if the message got already prepared var ready: bool ## Save theme for calculating line height var parent_label: RichTextLabel ## If you don't want to have spaces between images (made for Foolbox <3) var no_space: bool func prepare_message(message: String, parent: RichTextLabel) -> String: parent_label = parent var found_matches = regex.search_all(message) as Array[RegExMatch] # We are changing the message content and want to preserve the match beginnings. # So we need to handle them in reverse found_matches.reverse() for m: RegExMatch in found_matches: var path = m.get_string("path") var id = m.get_string("id") var resource = ResourceLoader.load(path, "SpriteFrames") as SpriteFrames if resource == null: continue var tex = resource.get_frame_texture("default", 0) var size = tex.get_size() var start = m.get_start(0) # Add an empty image to make the correct amount of space message = message.replace(path, "[img width=%s height=%s]%s[/img]" % [size.x, size.y, TRANSPARENT.resource_path]) var emoji = _create_emoji(resource) emoji.name = id emoji.set_meta("size", size) cache[id] = emoji parent.add_child(emoji) if no_space: message = message.replace("[/sprite] [sprite", "[/sprite][sprite") ready = true return message func _create_emoji(resource: SpriteFrames): var node = AnimatedSprite2D.new() node.sprite_frames = resource node.centered = true node.play() return node func _process_custom_fx(char_fx: CharFXTransform) -> bool: if not ready: return true var id = char_fx.env['id'] # unknown image just ignore if !cache.has(id): return true # Hide the original characters of the [sprite] tag content # (which should just be the placeholder [img] tag now) char_fx.visible = false # Only position the sprite once, using the first character's info if char_fx.relative_index != 0: return true var node: AnimatedSprite2D = cache[id] var image_size: Vector2 = node.get_meta("size") # Already stored in prepare_message # --- Get Font Metrics from RichTextLabel Theme --- var font: Font = char_fx.font if char_fx.font else parent_label.get_theme_font(&"normal_font") var font_size: int = parent_label.get_theme_font_size(&"normal_font_size") var font_ascent: float = 0.0 var font_descent: float = 0.0 # Distance below baseline (positive value) if font: if font_size <= 0: font_size = parent_label.get_theme_default_font_size() if font_size <= 0: font_size = 16 # Absolute fallback # Get ascent and descent, scaled by the RichTextLabel's scale font_ascent = font.get_ascent(font_size) * parent_label.scale.y font_descent = font.get_descent(font_size) * parent_label.scale.y else: # Fallback estimation if font_size <= 0: font_size = 16 var scaled_font_size = float(font_size) * parent_label.scale.y font_ascent = scaled_font_size * 0.8 # Estimate font_descent = scaled_font_size * 0.2 # Estimate printerr("SpriteFrameEffect: Could not determine font for ascent/descent calculation, using estimate.") # --- Calculate Position --- var baseline_origin: Vector2 = char_fx.transform.get_origin() # Horizontal center (remains the same) var center_x: float = baseline_origin.x + image_size.x / 2.0 # Vertical position: Align sprite center with the vertical center of the font glyph box. # The top of the font box is at baseline_origin.y - font_ascent. # The bottom of the font box is at baseline_origin.y + font_descent. # The total height of the font box is font_ascent + font_descent. # The center relative to the baseline is baseline - ascent + (total_height / 2) # = baseline - ascent + (ascent + descent) / 2 # = baseline - (ascent / 2) + (descent / 2) # = baseline - (ascent - descent) / 2.0 var center_y: float = baseline_origin.y - (font_ascent - font_descent) / 2.0 node.position = Vector2(center_x, center_y) return true