using System; using UnityEngine; using UnityEngine.UI; /// /// Compact runtime HSV colour picker: a saturation/value square, a hue bar, an alpha 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, the alpha bar only edits A). /// /// 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. For a prefab /// baked before the alpha bar existed it also builds that bar on the fly (and grows the picker's /// reserved layout height), so no re-bake is required. /// public class HsvColorPicker : MonoBehaviour { /// Raised with the new colour when the user moves the square, hue bar or alpha bar. public event Action Changed; private float _h, _s, _v, _a = 1f; // Gap between the stacked square / hue / alpha rows (matches the layout in Build). private const float RowGap = 6f; private static readonly Color AlphaBackdrop = new Color(0.5f, 0.5f, 0.5f, 1f); private Image _hueBase; // pure-hue background of the SV square private RectTransform _svHandle; private RectTransform _hueHandle; private Image _alphaGrad; // colour-tinted transparency ramp of the alpha bar private RectTransform _alphaHandle; 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 + alpha 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; // Alpha bar below the hue bar. BuildAlphaBar(sv, hue); // 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; } /// /// Builds the alpha bar (grey backdrop + colour-tinted transparency ramp + drag relay + handle) /// directly below the hue bar, sized/positioned from the existing SV square and hue bar so it /// works for both a fresh build and an older baked prefab. Sprites/hooks are applied by /// . /// private RectTransform BuildAlphaBar(RectTransform sv, RectTransform hue) { float width = sv.sizeDelta.x; float barHeight = hue.sizeDelta.y; float y = hue.anchoredPosition.y - (barHeight + RowGap); var alpha = PaintHud.NewRect("Alpha", transform); TopLeft(alpha, new Vector2(0f, y), new Vector2(width, barHeight)); PaintHud.AddImage(alpha.gameObject, AlphaBackdrop); // raycast target for the drag relay PaintHud.AddOverlay(alpha, "AlphaGrad"); // tinted alpha ramp, sprite assigned in Rebind alpha.gameObject.AddComponent(); var handle = PaintHud.NewRect("Handle", alpha); handle.sizeDelta = new Vector2(4f, barHeight + 4f); PaintHud.AddImage(handle.gameObject, Color.white).raycastTarget = false; return alpha; } /// /// 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") as RectTransform; 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; } } // Alpha bar: build it if the (older) prefab lacks it, then (re)assign sprite + relay. var alpha = transform.Find("Alpha") as RectTransform; bool builtAlpha = false; if (!alpha && sv is RectTransform svRect && hue) { alpha = BuildAlphaBar(svRect, hue); builtAlpha = true; } if (alpha) { var grad = alpha.Find("AlphaGrad"); if (grad && grad.TryGetComponent(out Image gradImg)) { gradImg.sprite = PaintHud.AlphaSprite(); _alphaGrad = gradImg; } _alphaHandle = alpha.Find("Handle") as RectTransform; if (alpha.TryGetComponent(out UiPointerSurface alphaPointer)) { alphaPointer.Picked -= OnAlphaPicked; alphaPointer.Picked += OnAlphaPicked; } if (builtAlpha) GrowReservedHeight(alpha.sizeDelta.y + RowGap); } var swatch = transform.Find("Swatch"); if (swatch) _swatch = swatch.GetComponent(); Refresh(); } /// Grows this picker's LayoutElement so a runtime-added alpha bar does not overlap the /// widget below it; the panel's ContentSizeFitter then expands to match. No-op without a LayoutElement. private void GrowReservedHeight(float extra) { if (!TryGetComponent(out LayoutElement le)) return; if (le.minHeight > 0f) le.minHeight += extra; if (le.preferredHeight > 0f) le.preferredHeight += extra; } 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 OnAlphaPicked(Vector2 p) { _a = 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 (_alphaGrad) _alphaGrad.color = Color.HSVToRGB(_h, _s, _v); // tint the transparency ramp to the current colour if (_alphaHandle) { _alphaHandle.anchorMin = _alphaHandle.anchorMax = new Vector2(_a, 0.5f); _alphaHandle.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; } }