Files
blockout/BlockOutReplica/Assets/Scripts/Presentation/BlockOutHudTypography.cs
T
2026-10-20 13:21:04 +08:00

211 lines
15 KiB
C#

using System;
using System.Collections.Generic;
using UnityEngine;
namespace BlockOutReplica.Presentation
{
public enum OriginalTextRole { Body, Counter, Title, Button, Timer, Navigation, JourneyTitle, ButtonGreen, ButtonRed, PopupLabel }
public sealed partial class BlockOutHud
{
private static OriginalSdfFontData recoveredTypeface;
private static Font typographyFallback;
private static readonly Dictionary<string, Material> typographyMaterials = new Dictionary<string, Material>();
private static readonly HashSet<string> mirroredHalfSprites = new HashSet<string> {
"Home_Button_Green", "Home_Button_Red", "Pause_Btn_Resume", "Pause_Btn_Quit", "Pause_Btn_BG",
"Prelevel_Btn_Play", "Prelevel_Btn_Play_Bg_Default", "Journey_Reward_Card", "Prelevel_Booster_Area_Default",
"Home_Button_Bg", "Home_Button_Green_0", "Home_Settings_Button", "Home_Settings_Button_Bg",
"Prelevel_Booster_Btn_Bg_Default", "Prelevel_Booster_Btn_Blue",
"Journey_Poster_Frame", "Journey_Use_Btn", "Journey_Use_Btn_BG", "Journey_Poster_Level_BG"
};
public static bool HasOriginalTypeface => Typeface != null;
private static OriginalSdfFontData Typeface => recoveredTypeface != null ? recoveredTypeface :
(recoveredTypeface = Resources.Load<OriginalSdfFontData>("OriginalTypography/MuseoSansRounded-1000"));
public static GUIStyle CreateOriginalTextStyle(int pixelSize, OriginalTextRole role = OriginalTextRole.Body,
TextAnchor alignment = TextAnchor.MiddleCenter)
{
if (typographyFallback == null) typographyFallback = Resources.Load<Font>("OriginalTypography/Roboto-Bold");
var style = new GUIStyle(GUI.skin.label) {name = "Original.Museo." + role, font = typographyFallback,
fontSize = pixelSize, fontStyle = FontStyle.Normal, alignment = alignment, wordWrap = false,
normal = {textColor = role == OriginalTextRole.Body ? new Color(.2f, .14509805f, .63529414f) : Color.white}};
return style;
}
public static bool TryDrawOriginalText(string value, Rect rect, GUIStyle style, float outlineWidth = -1)
{
var font = Typeface;
if (font == null || font.Atlas == null || font.RenderingMaterial == null || !font.Supports(value)) return false;
if (Event.current.type != EventType.Repaint) return true;
string role = style.name.StartsWith("Original.Museo.", StringComparison.Ordinal) ? style.name.Substring("Original.Museo.".Length) : "Counter";
OriginalSdfFontData.Profile profile = font.GetProfile(role);
if (profile == null) return false;
Color face = style.normal.textColor;
Material material = TypographyMaterial(role, font.RenderingMaterial);
material.SetColor("_FaceColor", face);
material.SetColor("_OutlineColor", profile.Outline);
material.SetFloat("_FaceDilate", profile.FaceDilate);
material.SetFloat("_ScaleRatioA", profile.ScaleRatioA);
material.SetFloat("_OutlineWidth", outlineWidth < 0 ? (profile.OutlineEnabled ? profile.OutlineWidth : 0) : outlineWidth);
int size = Mathf.Max(1, style.fontSize);
string[] lines = WrapOriginalText(value, rect.width, size, style.wordWrap, font);
float lineHeight = font.LineHeight * size / font.PointSize;
float blockHeight = font.CapHeight * size / font.PointSize + Mathf.Max(0, lines.Length - 1) * lineHeight;
float y = rect.y + VerticalAlignment(style.alignment, rect.height - blockHeight);
Color tint = GUI.color * GUI.contentColor;
for (int i = 0; i < lines.Length; i++)
{
float width = font.Measure(lines[i], size);
float x = rect.x + HorizontalAlignment(style.alignment, rect.width - width);
float baseline = y + font.CapHeight * size / font.PointSize + i * lineHeight;
// Underlay is behind every title layer, not between the expanded layers and face.
if (role != "Body" && role != "Navigation" && profile.UnderlayEnabled && profile.Underlay.a > 0 &&
(profile.UnderlayDilate > 0 || profile.UnderlayOffsetX != 0 || profile.UnderlayOffsetY != 0))
{
Material shadow = TypographyMaterial(role + ".Underlay", font.RenderingMaterial);
shadow.SetColor("_FaceColor", profile.Underlay); shadow.SetColor("_OutlineColor", Color.clear);
shadow.SetFloat("_FaceDilate", profile.FaceDilate + profile.UnderlayDilate * profile.ScaleRatioC / Mathf.Max(.0001f, profile.ScaleRatioA));
shadow.SetFloat("_ScaleRatioA", profile.ScaleRatioA);
shadow.SetFloat("_OutlineWidth", 0);
DrawOriginalLine(lines[i], x + profile.UnderlayOffsetX * profile.ScaleRatioC * (font.Padding + 1) * size / font.PointSize,
baseline - profile.UnderlayOffsetY * profile.ScaleRatioC * (font.Padding + 1) * size / font.PointSize, size, font, shadow, tint);
}
if (role == "Button" || role == "ButtonGreen" || role == "ButtonRed")
{
// LevelPausePopup uses expanded colored glyphs behind a plain white atlas
// face, not PlayButtonPurpleUnderlay. Retain those actual source materials.
if (role == "ButtonRed")
DrawOriginalProfileLine(lines[i], x, baseline, size, font, "ButtonRedOuter", tint);
DrawOriginalProfileLine(lines[i], x, baseline, size, font,
role == "ButtonRed" ? "ButtonRedOutline" : "ButtonGreenOutline", tint);
}
if (role == "Title" || role == "JourneyTitle")
{
// Keep the source glyphs/dilation/scale ratios. Expanded faces need the source
// purple palette, not the material's default white: original TMP vertex tints
// were stripped from the export. Their palette composition is reconstructed.
var outerProfile = font.GetProfile(role == "JourneyTitle" ? "JourneyTitleOuter" : "TitleOuter");
var middleProfile = font.GetProfile(role == "JourneyTitle" ? "JourneyTitleWhiteStroke" : "TitleExpanded");
Color outerColor = role == "JourneyTitle" ? profile.Outline : outerProfile.Underlay;
// Do not reuse the deep-purple inner contour for the wide expanded face:
// that collapsed the title into white + a single flat dark-purple frame.
// DilateFull is the actual Pause layer binding, not a "white stroke";
// the bright tint is adapted from a distinct original palette material.
Color middleColor = role == "JourneyTitle" ? profile.Underlay : font.GetProfile("TitleBrightPalette").Face;
Material outer = TypographyMaterial(role + ".Outer", font.RenderingMaterial);
outer.SetColor("_FaceColor", outerColor); outer.SetColor("_OutlineColor", outerColor);
outer.SetFloat("_FaceDilate", outerProfile.FaceDilate); outer.SetFloat("_OutlineWidth", outerProfile.OutlineEnabled ? outerProfile.OutlineWidth : 0);
outer.SetFloat("_ScaleRatioA", outerProfile.ScaleRatioA);
DrawOriginalLine(lines[i], x, baseline, size, font, outer, tint);
Material middle = TypographyMaterial(role + ".Middle", font.RenderingMaterial);
middle.SetColor("_FaceColor", middleColor); middle.SetColor("_OutlineColor", Color.clear);
middle.SetFloat("_FaceDilate", middleProfile.FaceDilate); middle.SetFloat("_OutlineWidth", 0);
middle.SetFloat("_ScaleRatioA", middleProfile.ScaleRatioA);
DrawOriginalLine(lines[i], x, baseline, size, font, middle, tint);
}
DrawOriginalLine(lines[i], x, baseline, size, font, material, tint);
}
return true;
}
public static void DrawOriginalText(string value, Rect rect, int pixelSize, OriginalTextRole role = OriginalTextRole.Body,
TextAnchor alignment = TextAnchor.MiddleCenter, Color? color = null)
{
var style = CreateOriginalTextStyle(pixelSize, role, alignment);
if (color.HasValue) style.normal.textColor = color.Value;
if (!TryDrawOriginalText(value, rect, style)) GUI.Label(rect, value, style); // Explicit non-Museo fallback for absent Unicode.
}
public static float MeasureOriginalText(string value, float pixelSize) => Typeface == null ? 0 : Typeface.Measure(value, pixelSize);
public static void DrawOriginalTitle(string value, Rect rect, int pixelSize, bool journey = false) =>
DrawOriginalText(value, rect, pixelSize, journey ? OriginalTextRole.JourneyTitle : OriginalTextRole.Title);
private static Material TypographyMaterial(string key, Material source)
{
if (!typographyMaterials.TryGetValue(key, out Material material) || material == null)
{material = new Material(source) {hideFlags = HideFlags.HideAndDontSave}; typographyMaterials[key] = material;}
return material;
}
private static void DrawOriginalProfileLine(string line, float x, float baseline, int size,
OriginalSdfFontData font, string role, Color tint)
{
var profile = font.GetProfile(role);
if (profile == null) return;
if (profile.UnderlayEnabled)
{
Material shadow = TypographyMaterial(role + ".Underlay", font.RenderingMaterial);
shadow.SetColor("_FaceColor", profile.Underlay); shadow.SetColor("_OutlineColor", Color.clear);
shadow.SetFloat("_FaceDilate", profile.FaceDilate + profile.UnderlayDilate * profile.ScaleRatioC / Mathf.Max(.0001f, profile.ScaleRatioA));
shadow.SetFloat("_ScaleRatioA", profile.ScaleRatioA); shadow.SetFloat("_OutlineWidth", 0);
DrawOriginalLine(line, x + profile.UnderlayOffsetX * profile.ScaleRatioC * (font.Padding + 1) * size / font.PointSize,
baseline - profile.UnderlayOffsetY * profile.ScaleRatioC * (font.Padding + 1) * size / font.PointSize, size, font, shadow, tint);
}
Material material = TypographyMaterial(role, font.RenderingMaterial);
material.SetColor("_FaceColor", profile.Face); material.SetColor("_OutlineColor", profile.Outline);
material.SetFloat("_FaceDilate", profile.FaceDilate); material.SetFloat("_ScaleRatioA", profile.ScaleRatioA);
material.SetFloat("_OutlineWidth", profile.OutlineEnabled ? profile.OutlineWidth : 0);
DrawOriginalLine(line, x, baseline, size, font, material, tint);
}
private static void DrawOriginalLine(string line, float x, float baseline, int size, OriginalSdfFontData font, Material material, Color tint)
{
float scale = (float)size / font.PointSize;
foreach (char character in line)
{
if (!font.TryGetGlyph(character, out OriginalSdfFontData.Glyph glyph)) continue;
float s = scale * glyph.Scale;
if (glyph.Width > 0 && glyph.Height > 0)
{
float padding = font.Padding;
Rect target = new Rect(x + (glyph.BearingX - padding) * s, baseline - (glyph.BearingY + padding) * s,
(glyph.Rect.width + padding * 2) * s, (glyph.Rect.height + padding * 2) * s);
Rect uv = new Rect((glyph.Rect.x - padding) / font.Atlas.width, (glyph.Rect.y - padding) / font.Atlas.height,
(glyph.Rect.width + padding * 2) / font.Atlas.width, (glyph.Rect.height + padding * 2) / font.Atlas.height);
Graphics.DrawTexture(target, font.Atlas, uv, 0, 0, 0, 0, tint, material);
}
x += glyph.Advance * s;
}
}
private static string[] WrapOriginalText(string value, float width, int size, bool wrap, OriginalSdfFontData font)
{
if (!wrap) return value.Replace("\r", "").Split('\n');
var result = new List<string>();
foreach (string paragraph in value.Replace("\r", "").Split('\n'))
{
string line = "";
foreach (string word in paragraph.Split(' '))
{
string candidate = line.Length == 0 ? word : line + " " + word;
if (line.Length > 0 && font.Measure(candidate, size) > width) {result.Add(line); line = word;}
else line = candidate;
}
result.Add(line);
}
return result.ToArray();
}
private static float HorizontalAlignment(TextAnchor alignment, float extra) =>
alignment == TextAnchor.UpperRight || alignment == TextAnchor.MiddleRight || alignment == TextAnchor.LowerRight ? extra :
alignment == TextAnchor.UpperCenter || alignment == TextAnchor.MiddleCenter || alignment == TextAnchor.LowerCenter ? extra * .5f : 0;
private static float VerticalAlignment(TextAnchor alignment, float extra) =>
alignment == TextAnchor.LowerLeft || alignment == TextAnchor.LowerCenter || alignment == TextAnchor.LowerRight ? extra :
alignment == TextAnchor.MiddleLeft || alignment == TextAnchor.MiddleCenter || alignment == TextAnchor.MiddleRight ? extra * .5f : 0;
public static OriginalSpriteLayout GetOriginalSpriteLayout(Sprite sprite) => sprite == null ? OriginalSpriteLayout.Full :
mirroredHalfSprites.Contains(sprite.name) ? OriginalSpriteLayout.MirroredHalf : sprite.border == Vector4.zero ? OriginalSpriteLayout.Full : OriginalSpriteLayout.NineSlice;
/// <summary>Explicit layout mode avoids treating every atlas half-panel as a full button.</summary>
public static void DrawSlicedSprite(Sprite sprite, Rect target, OriginalSpriteLayout layout = OriginalSpriteLayout.Auto, float pixelsPerSourcePixel = 0)
{
if (sprite == null || sprite.texture == null) return;
if (layout == OriginalSpriteLayout.Auto) layout = GetOriginalSpriteLayout(sprite);
foreach (var segment in OriginalSpriteGeometry.Build(sprite.textureRect, sprite.border, target, layout, pixelsPerSourcePixel))
{
Rect source = segment.Source;
GUI.DrawTextureWithTexCoords(segment.Target, sprite.texture, new Rect(source.x / sprite.texture.width,
source.y / sprite.texture.height, source.width / sprite.texture.width, source.height / sprite.texture.height), true);
}
}
}
}