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). /// /// 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. /// public class HsvColorPicker : MonoBehaviour { /// Raised with the new colour when the user moves the square or hue bar. public event Action Changed; private float _h, _s, _v, _a = 1f; 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 + 6f)), 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; if (sv.TryGetComponent(out UiPointerSurface svPointer)) { svPointer.Picked -= OnSvPicked; svPointer.Picked += OnSvPicked; } } var hue = transform.Find("Hue"); if (hue) { if (hue.TryGetComponent(out Image hueImg)) hueImg.sprite = PaintHud.HueSprite(); _hueHandle = hue.Find("Handle") as RectTransform; if (hue.TryGetComponent(out UiPointerSurface huePointer)) { huePointer.Picked -= OnHuePicked; huePointer.Picked += OnHuePicked; } } var swatch = transform.Find("Swatch"); if (swatch) _swatch = swatch.GetComponent(); Refresh(); } 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; } }