mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-07 00:39:00 +08:00
feat(text): 添加富文本效果系统和颜色标记功能
- 实现 RichTextEffectBase 基类提供统一的标签命名和参数读取逻辑 - 添加 RichTextBlueEffect、RichTextGoldEffect、RichTextGreenEffect 和 RichTextRedEffect 颜色标记效果 - 添加 RichTextFadeInEffect、RichTextFlyInEffect、RichTextJitterEffect 和 RichTextSineEffect 动画效果 - 实现 DefaultRichTextEffectRegistry 默认效果注册表 - 创建 GfRichTextLabel 富文本标签宿主组件 - 添加 IRichTextEffectRegistry 效果注册表接口 - 实现 RichTextEffectEntry、RichTextEffectsController 和 RichTextProfile 配置管理类 - 添加 RichTextMarkup 语义化标签构建辅助方法 - 创建 RichTextMarkupTests 和 RichTextProfileTests 单元测试
This commit is contained in:
parent
2f4fccabf2
commit
e5ad29314e
38
GFramework.Godot.Tests/Text/RichTextMarkupTests.cs
Normal file
38
GFramework.Godot.Tests/Text/RichTextMarkupTests.cs
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
using GFramework.Godot.Text;
|
||||||
|
|
||||||
|
namespace GFramework.Godot.Tests.Text;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <see cref="RichTextMarkup" /> 的测试。
|
||||||
|
/// </summary>
|
||||||
|
[TestFixture]
|
||||||
|
public sealed class RichTextMarkupTests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 验证颜色快捷方法会输出预期标签。
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void Green_Should_Wrap_Text_With_Green_Tag()
|
||||||
|
{
|
||||||
|
var result = RichTextMarkup.Green("Ready");
|
||||||
|
|
||||||
|
Assert.That(result, Is.EqualTo("[green]Ready[/green]"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证效果方法会按稳定顺序拼接环境参数。
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void Effect_Should_Append_Environment_Parameters()
|
||||||
|
{
|
||||||
|
var env = new Dictionary<string, object?>
|
||||||
|
{
|
||||||
|
["speed"] = 4,
|
||||||
|
["tick"] = 0.1f
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = RichTextMarkup.Effect("Hello", "fade_in", env);
|
||||||
|
|
||||||
|
Assert.That(result, Is.EqualTo("[fade_in speed=4 tick=0.1]Hello[/fade_in]"));
|
||||||
|
}
|
||||||
|
}
|
||||||
31
GFramework.Godot.Tests/Text/RichTextProfileTests.cs
Normal file
31
GFramework.Godot.Tests/Text/RichTextProfileTests.cs
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
using GFramework.Godot.Text;
|
||||||
|
|
||||||
|
namespace GFramework.Godot.Tests.Text;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <see cref="RichTextProfile" /> 的测试。
|
||||||
|
/// </summary>
|
||||||
|
[TestFixture]
|
||||||
|
public sealed class RichTextProfileTests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 验证默认内置配置会暴露完整的第一阶段效果键集合。
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void CreateBuiltInDefault_Should_Contain_The_First_Phase_Effect_Keys()
|
||||||
|
{
|
||||||
|
var profile = RichTextProfile.CreateBuiltInDefault();
|
||||||
|
|
||||||
|
Assert.That(profile.Effects.Select(static entry => entry.Key), Is.EqualTo(new[]
|
||||||
|
{
|
||||||
|
"green",
|
||||||
|
"red",
|
||||||
|
"gold",
|
||||||
|
"blue",
|
||||||
|
"fade_in",
|
||||||
|
"sine",
|
||||||
|
"jitter",
|
||||||
|
"fly_in"
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
65
GFramework.Godot/Text/DefaultRichTextEffectRegistry.cs
Normal file
65
GFramework.Godot/Text/DefaultRichTextEffectRegistry.cs
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
using GFramework.Godot.Text.Effects;
|
||||||
|
|
||||||
|
namespace GFramework.Godot.Text;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 默认的富文本效果注册表。
|
||||||
|
/// 该实现仅负责内置效果键的解析,不处理业务层文本构建或配置持久化。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class DefaultRichTextEffectRegistry : IRichTextEffectRegistry
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 创建当前配置对应的全部效果实例。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="profile">效果组合配置。</param>
|
||||||
|
/// <param name="animatedEffectsEnabled">当前是否允许字符级动态效果生效。</param>
|
||||||
|
/// <returns>内置效果实例集合。</returns>
|
||||||
|
public IReadOnlyList<RichTextEffect> CreateEffects(RichTextProfile profile, bool animatedEffectsEnabled)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(profile);
|
||||||
|
|
||||||
|
var effects = new List<RichTextEffect>(profile.Effects.Length);
|
||||||
|
foreach (var entry in profile.Effects)
|
||||||
|
{
|
||||||
|
if (entry is null || !entry.Enabled || string.IsNullOrWhiteSpace(entry.Key))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var effect = CreateEffect(entry.Key, animatedEffectsEnabled);
|
||||||
|
if (effect is not null)
|
||||||
|
{
|
||||||
|
effects.Add(effect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return effects;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 根据效果键创建单个效果实例。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">效果键。</param>
|
||||||
|
/// <param name="animatedEffectsEnabled">当前是否允许字符级动态效果生效。</param>
|
||||||
|
/// <returns>解析成功时返回效果实例;否则返回 <see langword="null" />。</returns>
|
||||||
|
public RichTextEffect? CreateEffect(string key, bool animatedEffectsEnabled)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(key))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return key.Trim().ToLowerInvariant() switch
|
||||||
|
{
|
||||||
|
"green" => new RichTextGreenEffect(),
|
||||||
|
"red" => new RichTextRedEffect(),
|
||||||
|
"gold" => new RichTextGoldEffect(),
|
||||||
|
"blue" => new RichTextBlueEffect(),
|
||||||
|
"fade_in" => new RichTextFadeInEffect(animatedEffectsEnabled),
|
||||||
|
"sine" => new RichTextSineEffect(animatedEffectsEnabled),
|
||||||
|
"jitter" => new RichTextJitterEffect(animatedEffectsEnabled),
|
||||||
|
"fly_in" => new RichTextFlyInEffect(animatedEffectsEnabled),
|
||||||
|
_ => null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
27
GFramework.Godot/Text/Effects/RichTextBlueEffect.cs
Normal file
27
GFramework.Godot/Text/Effects/RichTextBlueEffect.cs
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
namespace GFramework.Godot.Text.Effects;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 为文本应用蓝色语义高亮。
|
||||||
|
/// </summary>
|
||||||
|
[GlobalClass]
|
||||||
|
[Tool]
|
||||||
|
public partial class RichTextBlueEffect : RichTextEffectBase
|
||||||
|
{
|
||||||
|
private static readonly Color BlueColor = new(0.44f, 0.72f, 0.98f, 1.0f);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取标签名。
|
||||||
|
/// </summary>
|
||||||
|
protected override string TagName => "blue";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 应用蓝色颜色效果。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="charFx">当前字符上下文。</param>
|
||||||
|
/// <returns>始终返回 <see langword="true" />。</returns>
|
||||||
|
public override bool _ProcessCustomFX(CharFXTransform charFx)
|
||||||
|
{
|
||||||
|
charFx.Color = BlueColor;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
81
GFramework.Godot/Text/Effects/RichTextEffectBase.cs
Normal file
81
GFramework.Godot/Text/Effects/RichTextEffectBase.cs
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
namespace GFramework.Godot.Text.Effects;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 富文本效果基类,提供统一的标签命名和环境参数读取辅助逻辑。
|
||||||
|
/// 该基类只负责 Godot 适配细节,不承载业务语义分层。
|
||||||
|
/// </summary>
|
||||||
|
[Tool]
|
||||||
|
public abstract partial class RichTextEffectBase : RichTextEffect
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取当前效果对应的 BBCode 标签名。
|
||||||
|
/// </summary>
|
||||||
|
protected abstract string TagName { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取 Godot 识别当前效果所需的 `bbcode` 属性。
|
||||||
|
/// </summary>
|
||||||
|
public string bbcode => TagName;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 尝试从字符环境参数中读取布尔值。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="transform">当前字符变换上下文。</param>
|
||||||
|
/// <param name="key">参数键。</param>
|
||||||
|
/// <param name="defaultValue">读取失败时使用的默认值。</param>
|
||||||
|
/// <returns>最终布尔值。</returns>
|
||||||
|
protected bool GetBool(CharFXTransform transform, string key, bool defaultValue = false)
|
||||||
|
{
|
||||||
|
if (transform.Env.TryGetValue(Variant.From(key), out var value))
|
||||||
|
{
|
||||||
|
return value.AsBool();
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 尝试从字符环境参数中读取浮点值。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="transform">当前字符变换上下文。</param>
|
||||||
|
/// <param name="key">参数键。</param>
|
||||||
|
/// <param name="defaultValue">读取失败时使用的默认值。</param>
|
||||||
|
/// <returns>最终浮点值。</returns>
|
||||||
|
protected float GetFloat(CharFXTransform transform, string key, float defaultValue)
|
||||||
|
{
|
||||||
|
if (transform.Env.TryGetValue(Variant.From(key), out var value))
|
||||||
|
{
|
||||||
|
return (float)value.AsDouble();
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 尝试从字符环境参数中读取颜色值。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="transform">当前字符变换上下文。</param>
|
||||||
|
/// <param name="key">参数键。</param>
|
||||||
|
/// <param name="defaultValue">读取失败时使用的默认值。</param>
|
||||||
|
/// <returns>最终颜色值。</returns>
|
||||||
|
protected Color GetColor(CharFXTransform transform, string key, Color defaultValue)
|
||||||
|
{
|
||||||
|
if (transform.Env.TryGetValue(Variant.From(key), out var value) &&
|
||||||
|
value.VariantType == Variant.Type.Color)
|
||||||
|
{
|
||||||
|
return (Color)value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从字符环境参数中应用可见性开关。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="transform">当前字符变换上下文。</param>
|
||||||
|
/// <param name="defaultValue">默认可见性。</param>
|
||||||
|
protected void ApplyVisibility(CharFXTransform transform, bool defaultValue = true)
|
||||||
|
{
|
||||||
|
transform.Visible = GetBool(transform, "visible", defaultValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
47
GFramework.Godot/Text/Effects/RichTextFadeInEffect.cs
Normal file
47
GFramework.Godot/Text/Effects/RichTextFadeInEffect.cs
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
namespace GFramework.Godot.Text.Effects;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 为文本提供逐字符淡入效果。
|
||||||
|
/// </summary>
|
||||||
|
[GlobalClass]
|
||||||
|
[Tool]
|
||||||
|
public partial class RichTextFadeInEffect : RichTextEffectBase
|
||||||
|
{
|
||||||
|
private readonly bool _animatedEffectsEnabled;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 初始化淡入效果。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="animatedEffectsEnabled">是否允许动态效果实际生效。</param>
|
||||||
|
public RichTextFadeInEffect(bool animatedEffectsEnabled = true)
|
||||||
|
{
|
||||||
|
_animatedEffectsEnabled = animatedEffectsEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取标签名。
|
||||||
|
/// </summary>
|
||||||
|
protected override string TagName => "fade_in";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 应用淡入动画。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="charFx">当前字符上下文。</param>
|
||||||
|
/// <returns>始终返回 <see langword="true" />。</returns>
|
||||||
|
public override bool _ProcessCustomFX(CharFXTransform charFx)
|
||||||
|
{
|
||||||
|
if (!_animatedEffectsEnabled)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var speed = GetFloat(charFx, "speed", 4.0f);
|
||||||
|
var tick = GetFloat(charFx, "tick", 0.01f);
|
||||||
|
var progress = (float)(charFx.ElapsedTime * speed - charFx.RelativeIndex * tick);
|
||||||
|
var color = charFx.Color;
|
||||||
|
color.A = Mathf.Clamp(progress, 0f, 1f);
|
||||||
|
charFx.Color = color;
|
||||||
|
ApplyVisibility(charFx);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
64
GFramework.Godot/Text/Effects/RichTextFlyInEffect.cs
Normal file
64
GFramework.Godot/Text/Effects/RichTextFlyInEffect.cs
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
namespace GFramework.Godot.Text.Effects;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 为文本提供逐字符飞入效果。
|
||||||
|
/// </summary>
|
||||||
|
[GlobalClass]
|
||||||
|
[Tool]
|
||||||
|
public partial class RichTextFlyInEffect : RichTextEffectBase
|
||||||
|
{
|
||||||
|
private readonly bool _animatedEffectsEnabled;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 初始化飞入效果。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="animatedEffectsEnabled">是否允许动态效果实际生效。</param>
|
||||||
|
public RichTextFlyInEffect(bool animatedEffectsEnabled = true)
|
||||||
|
{
|
||||||
|
_animatedEffectsEnabled = animatedEffectsEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取标签名。
|
||||||
|
/// </summary>
|
||||||
|
protected override string TagName => "fly_in";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 应用飞入动画。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="charFx">当前字符上下文。</param>
|
||||||
|
/// <returns>始终返回 <see langword="true" />。</returns>
|
||||||
|
public override bool _ProcessCustomFX(CharFXTransform charFx)
|
||||||
|
{
|
||||||
|
if (!_animatedEffectsEnabled)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var startOffset = new Vector2(
|
||||||
|
GetFloat(charFx, "offset_x", 12f),
|
||||||
|
GetFloat(charFx, "offset_y", 0f));
|
||||||
|
var speed = GetFloat(charFx, "speed", 3.0f);
|
||||||
|
var tick = GetFloat(charFx, "tick", 0.015f);
|
||||||
|
var progress = Mathf.Clamp((float)(charFx.ElapsedTime * speed - charFx.RelativeIndex * tick), 0f, 1f);
|
||||||
|
var eased = EaseOutQuad(progress);
|
||||||
|
|
||||||
|
charFx.Offset += startOffset * (1f - eased);
|
||||||
|
|
||||||
|
var color = charFx.Color;
|
||||||
|
color.A = eased;
|
||||||
|
charFx.Color = color;
|
||||||
|
ApplyVisibility(charFx);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 计算二次缓出值。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">归一化进度。</param>
|
||||||
|
/// <returns>缓出后的进度。</returns>
|
||||||
|
private static float EaseOutQuad(float value)
|
||||||
|
{
|
||||||
|
return 1f - (1f - value) * (1f - value);
|
||||||
|
}
|
||||||
|
}
|
||||||
27
GFramework.Godot/Text/Effects/RichTextGoldEffect.cs
Normal file
27
GFramework.Godot/Text/Effects/RichTextGoldEffect.cs
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
namespace GFramework.Godot.Text.Effects;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 为文本应用金色语义高亮。
|
||||||
|
/// </summary>
|
||||||
|
[GlobalClass]
|
||||||
|
[Tool]
|
||||||
|
public partial class RichTextGoldEffect : RichTextEffectBase
|
||||||
|
{
|
||||||
|
private static readonly Color GoldColor = new(0.96f, 0.79f, 0.34f, 1.0f);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取标签名。
|
||||||
|
/// </summary>
|
||||||
|
protected override string TagName => "gold";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 应用金色颜色效果。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="charFx">当前字符上下文。</param>
|
||||||
|
/// <returns>始终返回 <see langword="true" />。</returns>
|
||||||
|
public override bool _ProcessCustomFX(CharFXTransform charFx)
|
||||||
|
{
|
||||||
|
charFx.Color = GoldColor;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
27
GFramework.Godot/Text/Effects/RichTextGreenEffect.cs
Normal file
27
GFramework.Godot/Text/Effects/RichTextGreenEffect.cs
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
namespace GFramework.Godot.Text.Effects;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 为文本应用绿色语义高亮。
|
||||||
|
/// </summary>
|
||||||
|
[GlobalClass]
|
||||||
|
[Tool]
|
||||||
|
public partial class RichTextGreenEffect : RichTextEffectBase
|
||||||
|
{
|
||||||
|
private static readonly Color GreenColor = new(0.46f, 0.91f, 0.49f, 1.0f);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取标签名。
|
||||||
|
/// </summary>
|
||||||
|
protected override string TagName => "green";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 应用绿色颜色效果。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="charFx">当前字符上下文。</param>
|
||||||
|
/// <returns>始终返回 <see langword="true" />。</returns>
|
||||||
|
public override bool _ProcessCustomFX(CharFXTransform charFx)
|
||||||
|
{
|
||||||
|
charFx.Color = GreenColor;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
57
GFramework.Godot/Text/Effects/RichTextJitterEffect.cs
Normal file
57
GFramework.Godot/Text/Effects/RichTextJitterEffect.cs
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
namespace GFramework.Godot.Text.Effects;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 为文本提供抖动效果。
|
||||||
|
/// </summary>
|
||||||
|
[GlobalClass]
|
||||||
|
[Tool]
|
||||||
|
public partial class RichTextJitterEffect : RichTextEffectBase
|
||||||
|
{
|
||||||
|
private readonly bool _animatedEffectsEnabled;
|
||||||
|
private readonly FastNoiseLite _noise;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 初始化抖动效果。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="animatedEffectsEnabled">是否允许动态效果实际生效。</param>
|
||||||
|
public RichTextJitterEffect(bool animatedEffectsEnabled = true)
|
||||||
|
{
|
||||||
|
_animatedEffectsEnabled = animatedEffectsEnabled;
|
||||||
|
_noise = new FastNoiseLite
|
||||||
|
{
|
||||||
|
NoiseType = FastNoiseLite.NoiseTypeEnum.Perlin,
|
||||||
|
FractalOctaves = 8,
|
||||||
|
FractalGain = 0.8f
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取标签名。
|
||||||
|
/// </summary>
|
||||||
|
protected override string TagName => "jitter";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 应用抖动位移。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="charFx">当前字符上下文。</param>
|
||||||
|
/// <returns>始终返回 <see langword="true" />。</returns>
|
||||||
|
public override bool _ProcessCustomFX(CharFXTransform charFx)
|
||||||
|
{
|
||||||
|
if (!_animatedEffectsEnabled)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var amplitude = GetFloat(charFx, "amplitude", 3.0f);
|
||||||
|
var speed = GetFloat(charFx, "speed", 600.0f);
|
||||||
|
|
||||||
|
_noise.Seed = (charFx.RelativeIndex + 1) * 131;
|
||||||
|
var x = _noise.GetNoise1D((float)charFx.ElapsedTime * speed);
|
||||||
|
_noise.Seed = (charFx.RelativeIndex + 1) * 737;
|
||||||
|
var y = _noise.GetNoise1D((float)charFx.ElapsedTime * speed);
|
||||||
|
|
||||||
|
charFx.Offset += new Vector2(x, y) * amplitude;
|
||||||
|
ApplyVisibility(charFx);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
27
GFramework.Godot/Text/Effects/RichTextRedEffect.cs
Normal file
27
GFramework.Godot/Text/Effects/RichTextRedEffect.cs
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
namespace GFramework.Godot.Text.Effects;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 为文本应用红色语义高亮。
|
||||||
|
/// </summary>
|
||||||
|
[GlobalClass]
|
||||||
|
[Tool]
|
||||||
|
public partial class RichTextRedEffect : RichTextEffectBase
|
||||||
|
{
|
||||||
|
private static readonly Color RedColor = new(0.96f, 0.35f, 0.35f, 1.0f);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取标签名。
|
||||||
|
/// </summary>
|
||||||
|
protected override string TagName => "red";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 应用红色颜色效果。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="charFx">当前字符上下文。</param>
|
||||||
|
/// <returns>始终返回 <see langword="true" />。</returns>
|
||||||
|
public override bool _ProcessCustomFX(CharFXTransform charFx)
|
||||||
|
{
|
||||||
|
charFx.Color = RedColor;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
47
GFramework.Godot/Text/Effects/RichTextSineEffect.cs
Normal file
47
GFramework.Godot/Text/Effects/RichTextSineEffect.cs
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
namespace GFramework.Godot.Text.Effects;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 为文本提供正弦波形的上下漂浮效果。
|
||||||
|
/// </summary>
|
||||||
|
[GlobalClass]
|
||||||
|
[Tool]
|
||||||
|
public partial class RichTextSineEffect : RichTextEffectBase
|
||||||
|
{
|
||||||
|
private readonly bool _animatedEffectsEnabled;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 初始化正弦效果。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="animatedEffectsEnabled">是否允许动态效果实际生效。</param>
|
||||||
|
public RichTextSineEffect(bool animatedEffectsEnabled = true)
|
||||||
|
{
|
||||||
|
_animatedEffectsEnabled = animatedEffectsEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取标签名。
|
||||||
|
/// </summary>
|
||||||
|
protected override string TagName => "sine";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 应用正弦位移。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="charFx">当前字符上下文。</param>
|
||||||
|
/// <returns>始终返回 <see langword="true" />。</returns>
|
||||||
|
public override bool _ProcessCustomFX(CharFXTransform charFx)
|
||||||
|
{
|
||||||
|
if (!_animatedEffectsEnabled)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var amplitude = GetFloat(charFx, "amplitude", 0.8f);
|
||||||
|
var frequency = GetFloat(charFx, "frequency", 0.5f);
|
||||||
|
var speed = GetFloat(charFx, "speed", 1.5f);
|
||||||
|
var phase = (float)(charFx.ElapsedTime * speed + charFx.RelativeIndex * 0.1f);
|
||||||
|
var offsetY = amplitude * Mathf.Sin(phase * Mathf.Pi * 2f * frequency);
|
||||||
|
charFx.Offset += new Vector2(0f, offsetY);
|
||||||
|
ApplyVisibility(charFx);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
83
GFramework.Godot/Text/GfRichTextLabel.cs
Normal file
83
GFramework.Godot/Text/GfRichTextLabel.cs
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
namespace GFramework.Godot.Text;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// GFramework 提供的组合式富文本标签宿主。
|
||||||
|
/// 该类型只负责桥接 Godot 的 <see cref="RichTextLabel" /> 与框架的效果装配逻辑,不承载具体效果实现。
|
||||||
|
/// </summary>
|
||||||
|
[GlobalClass]
|
||||||
|
[Tool]
|
||||||
|
public partial class GfRichTextLabel : RichTextLabel
|
||||||
|
{
|
||||||
|
private IRichTextEffectRegistry? _effectRegistry;
|
||||||
|
private RichTextEffectsController? _effectsController;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置当前标签使用的效果配置。
|
||||||
|
/// 为空时将回退到内置默认配置。
|
||||||
|
/// </summary>
|
||||||
|
[Export]
|
||||||
|
public RichTextProfile? Profile { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置是否启用框架管理的富文本效果装配。
|
||||||
|
/// </summary>
|
||||||
|
[Export]
|
||||||
|
public bool EnableFrameworkEffects { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置是否允许字符级动态效果实际生效。
|
||||||
|
/// 关闭后仍然会安装对应标签,使富文本内容保持可解析。
|
||||||
|
/// </summary>
|
||||||
|
[Export]
|
||||||
|
public bool AnimatedEffectsEnabled { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取当前使用的效果注册表。
|
||||||
|
/// </summary>
|
||||||
|
internal IRichTextEffectRegistry EffectRegistry
|
||||||
|
{
|
||||||
|
get => _effectRegistry ??= new DefaultRichTextEffectRegistry();
|
||||||
|
set => _effectRegistry = value ?? throw new ArgumentNullException(nameof(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 节点就绪时初始化控制器并安装效果集合。
|
||||||
|
/// </summary>
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
if (EnableFrameworkEffects && !BbcodeEnabled)
|
||||||
|
{
|
||||||
|
BbcodeEnabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
EnsureController().Initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 手动刷新框架效果集合。
|
||||||
|
/// 当调用方在运行时替换配置或切换动画开关时,可通过该方法同步宿主状态。
|
||||||
|
/// </summary>
|
||||||
|
public void RefreshFrameworkEffects()
|
||||||
|
{
|
||||||
|
if (EnableFrameworkEffects && !BbcodeEnabled)
|
||||||
|
{
|
||||||
|
BbcodeEnabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
EnsureController().RefreshEffects();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或创建控制器实例。
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>组合式装配控制器。</returns>
|
||||||
|
private RichTextEffectsController EnsureController()
|
||||||
|
{
|
||||||
|
return _effectsController ??= new RichTextEffectsController(
|
||||||
|
this,
|
||||||
|
EffectRegistry,
|
||||||
|
() => Profile,
|
||||||
|
() => EnableFrameworkEffects,
|
||||||
|
() => AnimatedEffectsEnabled);
|
||||||
|
}
|
||||||
|
}
|
||||||
23
GFramework.Godot/Text/IRichTextEffectRegistry.cs
Normal file
23
GFramework.Godot/Text/IRichTextEffectRegistry.cs
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
namespace GFramework.Godot.Text;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 富文本效果注册表,负责把配置中的效果键解析为可安装的 <see cref="RichTextEffect" /> 实例。
|
||||||
|
/// </summary>
|
||||||
|
public interface IRichTextEffectRegistry
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 根据指定配置创建完整的效果实例集合。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="profile">效果组合配置。</param>
|
||||||
|
/// <param name="animatedEffectsEnabled">当前是否允许字符级动态效果生效。</param>
|
||||||
|
/// <returns>可直接写入 <see cref="RichTextLabel.CustomEffects" /> 的效果实例集合。</returns>
|
||||||
|
IReadOnlyList<RichTextEffect> CreateEffects(RichTextProfile profile, bool animatedEffectsEnabled);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 根据单个效果键创建对应效果实例。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">效果键。</param>
|
||||||
|
/// <param name="animatedEffectsEnabled">当前是否允许字符级动态效果生效。</param>
|
||||||
|
/// <returns>解析成功时返回效果实例;否则返回 <see langword="null" />。</returns>
|
||||||
|
RichTextEffect? CreateEffect(string key, bool animatedEffectsEnabled);
|
||||||
|
}
|
||||||
22
GFramework.Godot/Text/RichTextEffectEntry.cs
Normal file
22
GFramework.Godot/Text/RichTextEffectEntry.cs
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
namespace GFramework.Godot.Text;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 描述一条富文本效果配置项。
|
||||||
|
/// 该资源只负责声明需要启用的效果键与开关状态,不承担实例创建逻辑。
|
||||||
|
/// </summary>
|
||||||
|
[GlobalClass]
|
||||||
|
public partial class RichTextEffectEntry : Resource
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置效果键。
|
||||||
|
/// 键值由 <see cref="IRichTextEffectRegistry" /> 解析为具体的 <see cref="RichTextEffect" /> 实例。
|
||||||
|
/// </summary>
|
||||||
|
[Export]
|
||||||
|
public string Key { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置该配置项是否启用。
|
||||||
|
/// </summary>
|
||||||
|
[Export]
|
||||||
|
public bool Enabled { get; set; } = true;
|
||||||
|
}
|
||||||
70
GFramework.Godot/Text/RichTextEffectsController.cs
Normal file
70
GFramework.Godot/Text/RichTextEffectsController.cs
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
using Array = Godot.Collections.Array;
|
||||||
|
|
||||||
|
namespace GFramework.Godot.Text;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 负责把配置、开关和注册表装配为宿主标签的实际效果集合。
|
||||||
|
/// 该控制器是组合式扩展的装配中心,使 <see cref="GfRichTextLabel" /> 保持轻量。
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class RichTextEffectsController
|
||||||
|
{
|
||||||
|
private readonly Func<bool> _animatedEffectsEnabledAccessor;
|
||||||
|
private readonly Func<bool> _frameworkEffectsEnabledAccessor;
|
||||||
|
private readonly RichTextLabel _host;
|
||||||
|
private readonly Func<RichTextProfile?> _profileAccessor;
|
||||||
|
private readonly IRichTextEffectRegistry _registry;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 初始化控制器实例。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="host">目标富文本标签。</param>
|
||||||
|
/// <param name="registry">效果注册表。</param>
|
||||||
|
/// <param name="profileAccessor">当前配置访问器。</param>
|
||||||
|
/// <param name="frameworkEffectsEnabledAccessor">框架效果总开关访问器。</param>
|
||||||
|
/// <param name="animatedEffectsEnabledAccessor">字符动画开关访问器。</param>
|
||||||
|
public RichTextEffectsController(
|
||||||
|
RichTextLabel host,
|
||||||
|
IRichTextEffectRegistry registry,
|
||||||
|
Func<RichTextProfile?> profileAccessor,
|
||||||
|
Func<bool> frameworkEffectsEnabledAccessor,
|
||||||
|
Func<bool> animatedEffectsEnabledAccessor)
|
||||||
|
{
|
||||||
|
_host = host ?? throw new ArgumentNullException(nameof(host));
|
||||||
|
_registry = registry ?? throw new ArgumentNullException(nameof(registry));
|
||||||
|
_profileAccessor = profileAccessor ?? throw new ArgumentNullException(nameof(profileAccessor));
|
||||||
|
_frameworkEffectsEnabledAccessor = frameworkEffectsEnabledAccessor
|
||||||
|
?? throw new ArgumentNullException(nameof(frameworkEffectsEnabledAccessor));
|
||||||
|
_animatedEffectsEnabledAccessor = animatedEffectsEnabledAccessor
|
||||||
|
?? throw new ArgumentNullException(nameof(animatedEffectsEnabledAccessor));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 初始化并立即刷新宿主标签的效果集合。
|
||||||
|
/// </summary>
|
||||||
|
public void Initialize()
|
||||||
|
{
|
||||||
|
RefreshEffects();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 根据当前配置和开关重建宿主标签上的 <see cref="RichTextLabel.CustomEffects" />。
|
||||||
|
/// </summary>
|
||||||
|
public void RefreshEffects()
|
||||||
|
{
|
||||||
|
if (!_frameworkEffectsEnabledAccessor())
|
||||||
|
{
|
||||||
|
_host.CustomEffects = new Array();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var profile = _profileAccessor() ?? RichTextProfile.CreateBuiltInDefault();
|
||||||
|
var effects = _registry.CreateEffects(profile, _animatedEffectsEnabledAccessor());
|
||||||
|
var customEffects = new Array();
|
||||||
|
foreach (var effect in effects)
|
||||||
|
{
|
||||||
|
customEffects.Add(effect);
|
||||||
|
}
|
||||||
|
|
||||||
|
_host.CustomEffects = customEffects;
|
||||||
|
}
|
||||||
|
}
|
||||||
128
GFramework.Godot/Text/RichTextMarkup.cs
Normal file
128
GFramework.Godot/Text/RichTextMarkup.cs
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace GFramework.Godot.Text;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 提供语义化的富文本标签构建辅助方法。
|
||||||
|
/// 该工具层用于减少业务代码直接手写原始 BBCode 字符串的重复工作。
|
||||||
|
/// </summary>
|
||||||
|
public static class RichTextMarkup
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 使用指定标签包裹文本。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="text">原始文本。</param>
|
||||||
|
/// <param name="tag">标签名。</param>
|
||||||
|
/// <returns>包裹后的 BBCode 文本。</returns>
|
||||||
|
public static string Color(string text, string tag)
|
||||||
|
{
|
||||||
|
return Wrap(text, tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 使用 `green` 标签包裹文本。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="text">原始文本。</param>
|
||||||
|
/// <returns>包裹后的 BBCode 文本。</returns>
|
||||||
|
public static string Green(string text)
|
||||||
|
{
|
||||||
|
return Wrap(text, "green");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 使用 `red` 标签包裹文本。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="text">原始文本。</param>
|
||||||
|
/// <returns>包裹后的 BBCode 文本。</returns>
|
||||||
|
public static string Red(string text)
|
||||||
|
{
|
||||||
|
return Wrap(text, "red");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 使用 `gold` 标签包裹文本。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="text">原始文本。</param>
|
||||||
|
/// <returns>包裹后的 BBCode 文本。</returns>
|
||||||
|
public static string Gold(string text)
|
||||||
|
{
|
||||||
|
return Wrap(text, "gold");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 使用 `blue` 标签包裹文本。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="text">原始文本。</param>
|
||||||
|
/// <returns>包裹后的 BBCode 文本。</returns>
|
||||||
|
public static string Blue(string text)
|
||||||
|
{
|
||||||
|
return Wrap(text, "blue");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 使用指定效果标签包裹文本,并可附带参数环境。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="text">原始文本。</param>
|
||||||
|
/// <param name="tag">标签名。</param>
|
||||||
|
/// <param name="env">可选的标签参数集合。</param>
|
||||||
|
/// <returns>包裹后的 BBCode 文本。</returns>
|
||||||
|
public static string Effect(string text, string tag, IReadOnlyDictionary<string, object?>? env = null)
|
||||||
|
{
|
||||||
|
ArgumentException.ThrowIfNullOrWhiteSpace(tag);
|
||||||
|
|
||||||
|
var builder = new StringBuilder();
|
||||||
|
builder.Append('[');
|
||||||
|
builder.Append(tag);
|
||||||
|
|
||||||
|
if (env is not null)
|
||||||
|
{
|
||||||
|
foreach (var pair in env)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(pair.Key) || pair.Value is null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.Append(' ');
|
||||||
|
builder.Append(pair.Key);
|
||||||
|
builder.Append('=');
|
||||||
|
builder.Append(FormatValue(pair.Value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.Append(']');
|
||||||
|
builder.Append(text ?? string.Empty);
|
||||||
|
builder.Append("[/");
|
||||||
|
builder.Append(tag);
|
||||||
|
builder.Append(']');
|
||||||
|
return builder.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 使用指定标签包裹文本。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="text">原始文本。</param>
|
||||||
|
/// <param name="tag">标签名。</param>
|
||||||
|
/// <returns>包裹后的 BBCode 文本。</returns>
|
||||||
|
private static string Wrap(string text, string tag)
|
||||||
|
{
|
||||||
|
ArgumentException.ThrowIfNullOrWhiteSpace(tag);
|
||||||
|
return $"[{tag}]{text ?? string.Empty}[/{tag}]";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 将标签参数值格式化为稳定的 BBCode 字符串表示。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">待格式化的值。</param>
|
||||||
|
/// <returns>适用于 BBCode 参数的字符串。</returns>
|
||||||
|
private static string FormatValue(object value)
|
||||||
|
{
|
||||||
|
return value switch
|
||||||
|
{
|
||||||
|
string text => text,
|
||||||
|
IFormattable formattable => formattable.ToString(null, CultureInfo.InvariantCulture),
|
||||||
|
_ => value.ToString() ?? string.Empty
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
37
GFramework.Godot/Text/RichTextProfile.cs
Normal file
37
GFramework.Godot/Text/RichTextProfile.cs
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
namespace GFramework.Godot.Text;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 描述一个富文本效果组合配置。
|
||||||
|
/// 该资源是组合式扩展的核心载体,用于声明宿主标签需要安装的效果集合。
|
||||||
|
/// </summary>
|
||||||
|
[GlobalClass]
|
||||||
|
public partial class RichTextProfile : Resource
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置当前配置启用的效果条目集合。
|
||||||
|
/// </summary>
|
||||||
|
[Export]
|
||||||
|
public RichTextEffectEntry[] Effects { get; set; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建包含全部内置效果的默认配置。
|
||||||
|
/// 该方法为第一阶段提供零配置可用的回退组合。
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>包含全部内置效果键的默认配置。</returns>
|
||||||
|
public static RichTextProfile CreateBuiltInDefault()
|
||||||
|
{
|
||||||
|
var profile = new RichTextProfile();
|
||||||
|
profile.Effects =
|
||||||
|
[
|
||||||
|
new RichTextEffectEntry { Key = "green" },
|
||||||
|
new RichTextEffectEntry { Key = "red" },
|
||||||
|
new RichTextEffectEntry { Key = "gold" },
|
||||||
|
new RichTextEffectEntry { Key = "blue" },
|
||||||
|
new RichTextEffectEntry { Key = "fade_in" },
|
||||||
|
new RichTextEffectEntry { Key = "sine" },
|
||||||
|
new RichTextEffectEntry { Key = "jitter" },
|
||||||
|
new RichTextEffectEntry { Key = "fly_in" }
|
||||||
|
];
|
||||||
|
return profile;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user