using TMPro; using UnityEngine; using UnityEngine.UI; namespace S3.Modules.CarCards; static class CardUi { public const float Round = 8f; static Sprite? _white; static Sprite? _round; static Sprite? _pin; static Sprite? _dots; static Sprite? _x; static Sprite? _couple; static Sprite? _brake; static Sprite? _locate; static Sprite? _cut; static Sprite? _drop; static Sprite? _pickup; static TMP_FontAsset? _tmp; static Font? _uiFont; public static Sprite White() { if (_white != null) return _white; var tex = Texture2D.whiteTexture; _white = Sprite.Create(tex, new Rect(0f, 0f, tex.width, tex.height), new Vector2(0.5f, 0.5f), 4f); _white.name = "S3CardWhite"; return _white; } public static Sprite RoundSprite() { if (_round != null) return _round; // 8 UI-unit corners. uGUI slice size is border * canvasRefPPU / spritePPU // (canvas reference PPU is 100), so sprite PPU must be aa * 100 to keep // the radius at 8 while the texture is supersampled. const int radiusUi = 8; const int aa = 8; const int r = radiusUi * aa; const int s = r * 2 + 32; const float ppu = aa * 100f; var tex = new Texture2D(s, s, TextureFormat.RGBA32, false) { wrapMode = TextureWrapMode.Clamp, filterMode = FilterMode.Bilinear, hideFlags = HideFlags.HideAndDontSave, }; var px = new Color32[s * s]; for (int y = 0; y < s; y++) { for (int x = 0; x < s; x++) { float a = CoverRound(x + 0.5f, y + 0.5f, s, r); byte b = (byte)Mathf.Clamp(Mathf.RoundToInt(a * 255f), 0, 255); px[y * s + x] = new Color32(255, 255, 255, b); } } tex.SetPixels32(px); tex.Apply(false, true); _round = Sprite.Create( tex, new Rect(0f, 0f, s, s), new Vector2(0.5f, 0.5f), ppu, 0, SpriteMeshType.FullRect, new Vector4(r, r, r, r)); _round.name = "S3Round8"; return _round; } static float CoverRound(float x, float y, int s, int r) { float dx = x < r ? r - x : (x > s - r ? x - (s - r) : 0f); float dy = y < r ? r - y : (y > s - r ? y - (s - r) : 0f); if (dx == 0f || dy == 0f) return 1f; float d = Mathf.Sqrt(dx * dx + dy * dy); return Mathf.Clamp01(r + 0.5f - d); } public static Sprite PinSprite() { if (_pin != null) return _pin; _pin = MakeGlyph(32, (tex, s) => { FillCircle(tex, s, 16, 12, 7, Color.white); FillCircle(tex, s, 16, 12, 3, new Color(0, 0, 0, 0)); FillTri(tex, s, 16, 16, 10, 28, 22, 16, Color.white); }); _pin.name = "S3Pin"; return _pin; } public static Sprite DotsSprite() { if (_dots != null) return _dots; _dots = MakeGlyph(32, (tex, s) => { FillCircle(tex, s, 16, 8, 3, Color.white); FillCircle(tex, s, 16, 16, 3, Color.white); FillCircle(tex, s, 16, 24, 3, Color.white); }); _dots.name = "S3Dots"; return _dots; } public static Sprite XSprite() { if (_x != null) return _x; _x = MakeGlyph(32, (tex, s) => { StrokeLine(tex, s, 8, 8, 24, 24, 2.2f, Color.white); StrokeLine(tex, s, 24, 8, 8, 24, 2.2f, Color.white); }); _x.name = "S3X"; return _x; } public static Sprite CoupleSprite() { if (_couple != null) return _couple; _couple = MakeGlyph(32, (tex, s) => { StrokeLine(tex, s, 6, 10, 14, 10, 2.2f, Color.white); StrokeLine(tex, s, 14, 10, 14, 22, 2.2f, Color.white); StrokeLine(tex, s, 14, 22, 6, 22, 2.2f, Color.white); StrokeLine(tex, s, 26, 10, 18, 10, 2.2f, Color.white); StrokeLine(tex, s, 18, 10, 18, 22, 2.2f, Color.white); StrokeLine(tex, s, 18, 22, 26, 22, 2.2f, Color.white); }); _couple.name = "S3Couple"; return _couple; } public static Sprite BrakeSprite() { if (_brake != null) return _brake; _brake = MakeGlyph(32, (tex, s) => { FillCircle(tex, s, 16, 14, 9, Color.white); FillCircle(tex, s, 16, 14, 5, new Color(0, 0, 0, 0)); StrokeLine(tex, s, 16, 14, 16, 28, 2.2f, Color.white); }); _brake.name = "S3Brake"; return _brake; } public static Sprite LocateSprite() { if (_locate != null) return _locate; _locate = MakeGlyph(32, (tex, s) => { FillCircle(tex, s, 16, 16, 8, Color.white); FillCircle(tex, s, 16, 16, 4, new Color(0, 0, 0, 0)); StrokeLine(tex, s, 16, 4, 16, 10, 2f, Color.white); StrokeLine(tex, s, 16, 22, 16, 28, 2f, Color.white); StrokeLine(tex, s, 4, 16, 10, 16, 2f, Color.white); StrokeLine(tex, s, 22, 16, 28, 16, 2f, Color.white); }); _locate.name = "S3Locate"; return _locate; } public static Sprite CutSprite() { if (_cut != null) return _cut; _cut = MakeGlyph(64, (px, s) => { StrokeCar(px, s, 4f, 18f, 22f, 28f, 2.6f); StrokeCar(px, s, 38f, 18f, 22f, 28f, 2.6f); }); _cut.name = "S3Cut"; return _cut; } public static Sprite DropSprite() { if (_drop != null) return _drop; _drop = MakeGlyph(64, (px, s) => { StrokeCar(px, s, 16f, 8f, 32f, 24f, 2.6f); StrokeLine(px, s, 32f, 36f, 32f, 50f, 2.8f, Color.white); FillTri(px, s, 32f, 58f, 21f, 46f, 43f, 46f, Color.white); }); _drop.name = "S3Drop"; return _drop; } public static Sprite PickupSprite() { if (_pickup != null) return _pickup; _pickup = MakeGlyph(64, (px, s) => { StrokeCar(px, s, 16f, 32f, 32f, 24f, 2.6f); StrokeLine(px, s, 32f, 28f, 32f, 14f, 2.8f, Color.white); FillTri(px, s, 32f, 6f, 21f, 18f, 43f, 18f, Color.white); }); _pickup.name = "S3Pickup"; return _pickup; } static void StrokeCar(Color[] px, int s, float x, float y, float w, float h, float thick) { StrokeLine(px, s, x, y, x + w, y, thick, Color.white); StrokeLine(px, s, x + w, y, x + w, y + h, thick, Color.white); StrokeLine(px, s, x + w, y + h, x, y + h, thick, Color.white); StrokeLine(px, s, x, y + h, x, y, thick, Color.white); float wy = y + 4f; FillCircle(px, s, x + w * 0.28f, wy, 3.2f, Color.white); FillCircle(px, s, x + w * 0.72f, wy, 3.2f, Color.white); } public static void TintButton(Button btn, bool on, Color onColor) { if (btn == null) return; var img = btn.targetGraphic as Image; if (img == null) return; img.color = on ? onColor : new Color(0.22f, 0.22f, 0.24f, 1f); } static Sprite MakeGlyph(int s, System.Action draw) { var tex = new Texture2D(s, s, TextureFormat.RGBA32, false) { filterMode = FilterMode.Bilinear, hideFlags = HideFlags.HideAndDontSave, }; var px = new Color[s * s]; draw(px, s); tex.SetPixels(px); tex.Apply(false, true); return Sprite.Create(tex, new Rect(0f, 0f, s, s), new Vector2(0.5f, 0.5f), 100f); } static void FillCircle(Color[] px, int s, float cx, float cy, float r, Color c) { for (int y = 0; y < s; y++) for (int x = 0; x < s; x++) { float d = Vector2.Distance(new Vector2(x + 0.5f, y + 0.5f), new Vector2(cx, cy)); float a = Mathf.Clamp01(r + 0.5f - d); if (a <= 0f) continue; int i = y * s + x; if (c.a <= 0.01f) px[i] = Color.clear; else { Color dcol = c; dcol.a *= a; px[i] = Blend(px[i], dcol); } } } static void FillTri(Color[] px, int s, float x1, float y1, float x2, float y2, float x3, float y3, Color c) { for (int y = 0; y < s; y++) for (int x = 0; x < s; x++) { float pxp = x + 0.5f, pyp = y + 0.5f; if (!InTri(pxp, pyp, x1, y1, x2, y2, x3, y3)) continue; px[y * s + x] = Blend(px[y * s + x], c); } } static bool InTri(float px, float py, float x1, float y1, float x2, float y2, float x3, float y3) { float d1 = Sign(px, py, x1, y1, x2, y2); float d2 = Sign(px, py, x2, y2, x3, y3); float d3 = Sign(px, py, x3, y3, x1, y1); bool hasNeg = d1 < 0 || d2 < 0 || d3 < 0; bool hasPos = d1 > 0 || d2 > 0 || d3 > 0; return !(hasNeg && hasPos); } static float Sign(float px, float py, float x1, float y1, float x2, float y2) => (px - x2) * (y1 - y2) - (x1 - x2) * (py - y2); static void StrokeLine(Color[] px, int s, float x0, float y0, float x1, float y1, float thick, Color c) { for (int y = 0; y < s; y++) for (int x = 0; x < s; x++) { float d = DistToSeg(x + 0.5f, y + 0.5f, x0, y0, x1, y1); float a = Mathf.Clamp01(thick + 0.5f - d); if (a <= 0f) continue; Color dcol = c; dcol.a *= a; px[y * s + x] = Blend(px[y * s + x], dcol); } } static float DistToSeg(float px, float py, float x0, float y0, float x1, float y1) { float dx = x1 - x0, dy = y1 - y0; float l2 = dx * dx + dy * dy; if (l2 < 0.0001f) return Vector2.Distance(new Vector2(px, py), new Vector2(x0, y0)); float t = Mathf.Clamp01(((px - x0) * dx + (py - y0) * dy) / l2); return Vector2.Distance(new Vector2(px, py), new Vector2(x0 + t * dx, y0 + t * dy)); } static Color Blend(Color under, Color over) { float a = over.a + under.a * (1f - over.a); if (a < 0.0001f) return Color.clear; return new Color( (over.r * over.a + under.r * under.a * (1f - over.a)) / a, (over.g * over.a + under.g * under.a * (1f - over.a)) / a, (over.b * over.a + under.b * under.a * (1f - over.a)) / a, a); } public static TMP_FontAsset? TmpFont() { if (_tmp != null) return _tmp; try { _tmp = TMP_Settings.defaultFontAsset; } catch { } if (_tmp == null) { var all = Resources.FindObjectsOfTypeAll(); if (all != null && all.Length > 0) _tmp = all[0]; } return _tmp; } public static Font UiFont() { if (_uiFont != null) return _uiFont; _uiFont = Resources.GetBuiltinResource("Arial.ttf"); return _uiFont; } public static RectTransform Rt(GameObject go) => (RectTransform)go.transform; public static Image MakeImage(RectTransform parent, string name, Color color, bool raycast, bool round = true) { var go = new GameObject(name, typeof(RectTransform), typeof(Image)); var rt = Rt(go); rt.SetParent(parent, false); rt.anchorMin = rt.anchorMax = new Vector2(0.5f, 0.5f); rt.pivot = new Vector2(0.5f, 0.5f); var img = go.GetComponent(); if (round) { img.sprite = RoundSprite(); img.type = Image.Type.Sliced; img.pixelsPerUnitMultiplier = 1f; } else { img.sprite = White(); img.type = Image.Type.Simple; } img.color = color; img.raycastTarget = raycast; return img; } public static Image MakeIcon(RectTransform parent, string name, Sprite sprite, Color color, bool raycast) { var go = new GameObject(name, typeof(RectTransform), typeof(Image)); var rt = Rt(go); rt.SetParent(parent, false); var img = go.GetComponent(); img.sprite = sprite; img.type = Image.Type.Simple; img.preserveAspect = true; img.color = color; img.raycastTarget = raycast; return img; } public static TextMeshProUGUI Tmp( RectTransform parent, string name, float size, Color color, FontStyles style = FontStyles.Normal, TextAlignmentOptions align = TextAlignmentOptions.TopLeft) { var go = new GameObject(name, typeof(RectTransform), typeof(TextMeshProUGUI)); var rt = Rt(go); rt.SetParent(parent, false); var tmp = go.GetComponent(); tmp.fontSize = size; tmp.color = color; tmp.fontStyle = style; tmp.alignment = align; tmp.raycastTarget = false; tmp.textWrappingMode = TextWrappingModes.Normal; tmp.overflowMode = TextOverflowModes.Truncate; var font = TmpFont(); if (font != null) tmp.font = font; return tmp; } public static Button Button(RectTransform parent, string name, string label, Vector2 size) { var img = MakeImage(parent, name, new Color(0.22f, 0.22f, 0.24f, 1f), raycast: true); img.rectTransform.sizeDelta = size; var btn = img.gameObject.AddComponent