Split the single HUD panel into four independent per-channel sub-panels plus a global brush-size panel, each a standalone RectTransform with its own header (drag-handle ready). PaintHud now binds to a baked layout prefab via recursive name lookup instead of building everything in code, with a code-gen fallback. HsvColorPicker gains Rebind() to re-apply the procedural gradient sprites and drag relays that a prefab cannot serialize. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
172 lines
6.2 KiB
C#
172 lines
6.2 KiB
C#
using System;
|
|
using UnityEngine;
|
|
using UnityEngine.UI;
|
|
|
|
/// <summary>
|
|
/// 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).
|
|
///
|
|
/// <see cref="Build"/> creates the child structure (stable names, no sprites or hooks); it can
|
|
/// be baked into a prefab. <see cref="Rebind"/> 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.
|
|
/// </summary>
|
|
public class HsvColorPicker : MonoBehaviour
|
|
{
|
|
/// <summary>Raised with the new colour when the user moves the square or hue bar.</summary>
|
|
public event Action<Color> 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;
|
|
|
|
/// <summary>Current colour (HSV -> RGB, with the preserved alpha).</summary>
|
|
public Color Value
|
|
{
|
|
get
|
|
{
|
|
Color c = Color.HSVToRGB(_h, _s, _v);
|
|
c.a = _a;
|
|
return c;
|
|
}
|
|
}
|
|
|
|
/// <summary>Shows <paramref name="c"/> without raising <see cref="Changed"/> (external set).</summary>
|
|
public void SetColor(Color c)
|
|
{
|
|
Color.RGBToHSV(c, out _h, out _s, out _v);
|
|
_a = c.a;
|
|
Refresh();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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 <see cref="Rebind"/> afterwards (at runtime) to make it functional.
|
|
/// </summary>
|
|
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<UiPointerSurface>();
|
|
|
|
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<Image>();
|
|
hueImg.type = Image.Type.Simple;
|
|
|
|
hue.gameObject.AddComponent<UiPointerSurface>();
|
|
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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 <see cref="SetColor"/>.
|
|
/// </summary>
|
|
public void Rebind()
|
|
{
|
|
var sv = transform.Find("SV");
|
|
if (sv)
|
|
{
|
|
_hueBase = sv.GetComponent<Image>();
|
|
|
|
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<Image>();
|
|
|
|
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);
|
|
}
|
|
|
|
/// <summary>Anchors a child to this rect's top-left corner with the given offset and size.</summary>
|
|
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;
|
|
}
|
|
}
|