using System; using UnityEngine; using UnityEngine.UI; /// /// Compact runtime HSV colour picker: a saturation/value square, a hue bar and a live swatch. The /// square is a pure-hue base with two static gradient overlays on top (white-out -> saturation, /// black-out -> value), so only the base tint changes per hue - no per-frame texture rebuilds. Hue is /// never lost while dragging the square (the square only edits S/V, the bar only edits H). Alpha is /// edited separately by a standalone slider in the HUD, not by this picker; /// still stores the incoming alpha so round-trips it. /// /// creates the child structure (stable names, no sprites or hooks); it can /// be baked into a prefab. then (re)assigns the runtime gradient sprites /// (which a prefab cannot store) and (re)hooks the drag relays - it runs for both the code-built /// picker and one instantiated from a prefab, so the two are behaviourally identical. A leftover /// alpha bar from a pre-split prefab is hidden by , so no re-bake is required. /// public class HsvColorPicker : MonoBehaviour { /// Raised with the new colour (RGB; alpha is preserved, not edited here) when the user moves /// the square or hue bar. public event Action Changed; private float _h, _s, _v, _a = 1f; // Gap between the stacked square / hue rows (matches the layout in Build). private const float RowGap = 6f; private Image _hueBase; // pure-hue background of the SV square private RectTransform _svHandle; private RectTransform _hueHandle; private Image _swatch; /// Current colour (HSV -> RGB, with the preserved alpha). public Color Value { get { Color c = Color.HSVToRGB(_h, _s, _v); c.a = _a; return c; } } /// Shows without raising (external set). public void SetColor(Color c) { Color.RGBToHSV(c, out _h, out _s, out _v); _a = c.a; Refresh(); } /// /// Builds the picker structure (square + hue bar + swatch) under this object, /// top-left aligned. Layout only - no gradient sprites and no event hooks, so the result is /// safe to bake into a prefab. Call afterwards (at runtime) to make it /// functional. /// public void Build(float squareSize, float hueHeight) { // Saturation / value square: base hue + two named gradient overlays, top-left of this rect. var sv = PaintHud.NewRect("SV", transform); TopLeft(sv, new Vector2(0f, 0f), new Vector2(squareSize, squareSize)); PaintHud.AddImage(sv.gameObject, Color.red); // raycast target for the drag relay PaintHud.AddOverlay(sv, "SatOverlay"); PaintHud.AddOverlay(sv, "ValOverlay"); sv.gameObject.AddComponent(); var svHandle = PaintHud.NewRect("Handle", sv); svHandle.sizeDelta = new Vector2(12f, 12f); PaintHud.AddImage(svHandle.gameObject, Color.white).raycastTarget = false; // Hue bar below the square. var hue = PaintHud.NewRect("Hue", transform); TopLeft(hue, new Vector2(0f, -(squareSize + RowGap)), new Vector2(squareSize, hueHeight)); var hueImg = hue.gameObject.AddComponent(); hueImg.type = Image.Type.Simple; hue.gameObject.AddComponent(); var hueHandle = PaintHud.NewRect("Handle", hue); hueHandle.sizeDelta = new Vector2(4f, hueHeight + 4f); PaintHud.AddImage(hueHandle.gameObject, Color.white).raycastTarget = false; // Swatch, to the right of the square. var sw = PaintHud.NewRect("Swatch", transform); TopLeft(sw, new Vector2(squareSize + 10f, 0f), new Vector2(34f, 34f)); PaintHud.AddImage(sw.gameObject, Color.white).raycastTarget = false; } /// /// Re-finds the child widgets, (re)assigns the runtime gradient sprites and (re)hooks the drag /// relays. Idempotent (unsubscribe-then-subscribe), so it is safe whether the structure was just /// built in code or instantiated from a prefab. Call before . /// public void Rebind() { var sv = transform.Find("SV"); if (sv) { _hueBase = sv.GetComponent(); var sat = sv.Find("SatOverlay"); if (sat && sat.TryGetComponent(out Image satImg)) satImg.sprite = PaintHud.SaturationSprite(); var val = sv.Find("ValOverlay"); if (val && val.TryGetComponent(out Image valImg)) valImg.sprite = PaintHud.ValueSprite(); _svHandle = sv.Find("Handle") as RectTransform; Rehook(sv, OnSvPicked); } var hue = transform.Find("Hue") as RectTransform; if (hue) { if (hue.TryGetComponent(out Image hueImg)) hueImg.sprite = PaintHud.HueSprite(); _hueHandle = hue.Find("Handle") as RectTransform; Rehook(hue, OnHuePicked); } // Alpha now lives on a standalone slider (PaintHud), not in the picker. Hide any alpha bar a // pre-split prefab still carries so it does not show as a dead, non-functional strip. var leftoverAlpha = transform.Find("Alpha"); if (leftoverAlpha) leftoverAlpha.gameObject.SetActive(false); var swatch = transform.Find("Swatch"); if (swatch) _swatch = swatch.GetComponent(); Refresh(); } // Idempotently hooks the handler to the surface's drag relay (unsubscribe-then-subscribe). private static void Rehook(Component surface, Action handler) { if (surface.TryGetComponent(out UiPointerSurface pointer)) { pointer.Picked -= handler; pointer.Picked += handler; } } private void OnSvPicked(Vector2 p) { _s = p.x; _v = p.y; Refresh(); Changed?.Invoke(Value); } private void OnHuePicked(Vector2 p) { _h = p.x; Refresh(); Changed?.Invoke(Value); } private void Refresh() { if (_hueBase) _hueBase.color = Color.HSVToRGB(_h, 1f, 1f); if (_svHandle) { _svHandle.anchorMin = _svHandle.anchorMax = new Vector2(_s, _v); _svHandle.anchoredPosition = Vector2.zero; } if (_hueHandle) { _hueHandle.anchorMin = _hueHandle.anchorMax = new Vector2(_h, 0.5f); _hueHandle.anchoredPosition = Vector2.zero; } if (_swatch) _swatch.color = Color.HSVToRGB(_h, _s, _v); } /// Anchors a child to this rect's top-left corner with the given offset and size. private static void TopLeft(RectTransform rt, Vector2 offset, Vector2 size) { rt.anchorMin = rt.anchorMax = new Vector2(0f, 1f); rt.pivot = new Vector2(0f, 1f); rt.anchoredPosition = offset; rt.sizeDelta = size; } }