Skip to content

Instantly share code, notes, and snippets.

@andrew-raphael-lukasik
Last active June 3, 2025 02:04
Show Gist options
  • Save andrew-raphael-lukasik/e7476208779f70e66fbedf5eb10aa2f8 to your computer and use it in GitHub Desktop.
Save andrew-raphael-lukasik/e7476208779f70e66fbedf5eb10aa2f8 to your computer and use it in GitHub Desktop.
basic color picker for UI Toolkit

Color picker / UI Toolkit

GIF 08 11 2023 01-37-22

// src*: https://gist.github.com/andrew-raphael-lukasik/e7476208779f70e66fbedf5eb10aa2f8
using UnityEngine;
using UnityEngine.UIElements;
[UnityEngine.Scripting.Preserve]
public class ColorPickerElement : VisualElement
{
public new class UxmlFactory : UxmlFactory<ColorPickerElement,UxmlTraits> {}
public new class UxmlTraits : VisualElement.UxmlTraits
{
UxmlFloatAttributeDescription hueAttr = new UxmlFloatAttributeDescription{ name="hue" , defaultValue=0.5f };
UxmlFloatAttributeDescription brightnessAttr = new UxmlFloatAttributeDescription{ name="brightness" , defaultValue=1f };
public override void Init ( VisualElement ve , IUxmlAttributes attributes , CreationContext context )
{
base.Init( ve , attributes , context );
var instance = (ColorPickerElement) ve;
instance.hue = hueAttr.GetValueFromBag(attributes,context);
instance.brightness = brightnessAttr.GetValueFromBag(attributes,context);
}
}
public float hue { get; set; }
public float brightness { get; set; }
public Color color => Color.HSVToRGB(1-hue%1,1,brightness);
public Gradient circleGradient { get; set; } = new Gradient{
mode = GradientMode.Blend ,
colorKeys = new GradientColorKey[]{
new GradientColorKey( Color.HSVToRGB(0,1,1) , 0 ) ,
new GradientColorKey( Color.HSVToRGB(1*1f/6f,1,1) , 1*1f/6f ) ,
new GradientColorKey( Color.HSVToRGB(2*1f/6f,1,1) , 2*1f/6f ) ,
new GradientColorKey( Color.HSVToRGB(3*1f/6f,1,1) , 3*1f/6f ) ,
new GradientColorKey( Color.HSVToRGB(4*1f/6f,1,1) , 4*1f/6f ) ,
new GradientColorKey( Color.HSVToRGB(5*1f/6f,1,1) , 5*1f/6f ) ,
new GradientColorKey( Color.HSVToRGB(1,1,1) , 1 ) ,
}
};
public ColorPickerElement ()
{
generateVisualContent += OnGenerateVisualContent;
this.RegisterCallback<ClickEvent>( OnMouseClicked );
}
void OnMouseClicked ( ClickEvent evt )
{
Vector2 dir = (Vector2)evt.localPosition - contentRect.center;
hue = 0.25f + ( Mathf.Atan2(-dir.y,dir.x) / Mathf.PI ) * -0.5f;
Rect rect = contentRect;
float swh = Mathf.Min( rect.width , rect.height );// smaller dimension
brightness = dir.magnitude / (swh*0.4f);
this.MarkDirtyRepaint();
}
void OnGenerateVisualContent ( MeshGenerationContext mgc )
{
Rect rect = contentRect;
float swh = Mathf.Min( rect.width , rect.height );// smaller dimension
if( swh<0.01f ) return;// skip rendering when collapsed
var paint = mgc.painter2D;
float circleRadius = swh*0.4f;
float gradientWidth = swh*0.05f;
// selected color circle
paint.BeginPath();
{
paint.Arc( rect.center , circleRadius-gradientWidth/2 , 0 , 360 );
paint.fillColor = color;
paint.Fill();
}
paint.ClosePath();
// color ring
paint.BeginPath();
{
paint.Arc( rect.center , circleRadius , 270-0.001f , -90 , ArcDirection.CounterClockwise );
paint.lineWidth = gradientWidth + 0.2f;
paint.strokeColor = Color.black;
paint.Stroke();// border
paint.lineWidth = gradientWidth;
paint.strokeGradient = circleGradient;
paint.Stroke();// hues
}
paint.ClosePath();
// hue position marker
paint.BeginPath();
{
float hueAngle = -Mathf.PI/2 + hue * Mathf.PI*2;
paint.Arc( rect.center + Vector2.Scale(new Vector2(circleRadius,circleRadius),new Vector2(Mathf.Cos(hueAngle),Mathf.Sin(hueAngle))) , swh*0.03f , 0 , 360 );
paint.lineWidth = 0.4f;
paint.strokeColor = Color.white;
paint.Stroke();
}
paint.ClosePath();
}
}
@Agoxandr
Copy link

Agoxandr commented Jun 2, 2025

I made some improvements. Thanks a lot for this. There is still some stuff to improve, but it looks very similar to the built in picker.

using UnityEngine;
using UnityEngine.UIElements;

namespace Runtime.UI
{
    [UxmlElement]
    public partial class ColorPickerElement : VisualElement
    {
        [UxmlAttribute] public float Hue { get; set; }
        [UxmlAttribute] public float Saturation { get; set; }
        [UxmlAttribute] public float Brightness { get; set; }

        public Color Color => Color.HSVToRGB(Hue, Saturation, Brightness);

        public Gradient CircleGradient { get; set; } = new()
        {
            mode = GradientMode.Blend,
            colorKeys = new GradientColorKey[]
            {
                new(Color.HSVToRGB(0, 1, 1), 0),
                new(Color.HSVToRGB(1 * 1f / 6f, 1, 1), 1 * 1f / 6f),
                new(Color.HSVToRGB(2 * 1f / 6f, 1, 1), 2 * 1f / 6f),
                new(Color.HSVToRGB(3 * 1f / 6f, 1, 1), 3 * 1f / 6f),
                new(Color.HSVToRGB(4 * 1f / 6f, 1, 1), 4 * 1f / 6f),
                new(Color.HSVToRGB(5 * 1f / 6f, 1, 1), 5 * 1f / 6f),
                new(Color.HSVToRGB(1, 1, 1), 1)
            }
        };

        private readonly Texture2D _gradientTexture;
        private Vector2 _direction = Vector2.right;
        private Vector2 _position;

        public ColorPickerElement()
        {
            generateVisualContent += OnGenerateVisualContent;
            RegisterCallback<ClickEvent>(OnMouseClicked);

            _gradientTexture = new Texture2D(16, 16, TextureFormat.RGB24, false)
            {
                filterMode = FilterMode.Bilinear,
                hideFlags = HideFlags.HideAndDontSave,
                wrapMode = TextureWrapMode.Clamp
            };
            UpdateGradientTexture();
            var image = new Image
            {
                image = _gradientTexture,
                style =
                {
                    width = Length.Percent(50f),
                    height = Length.Percent(50f),
                    marginTop = Length.Auto(),
                    marginRight = Length.Auto(),
                    marginBottom = Length.Auto(),
                    marginLeft = Length.Auto()
                }
            };
            image.generateVisualContent += context =>
            {
                var rect = contentRect;
                var swh = Mathf.Min(rect.width, rect.height);
                var paint = context.painter2D;

                // Image position marker
                paint.BeginPath();
                {
                    paint.Arc(_position, swh * 0.025f - 2f, 0, 360);

                    paint.lineWidth = 1f + 2f;
                    paint.strokeColor = Color.black;
                    paint.Stroke();

                    paint.lineWidth = 1f;
                    paint.strokeColor = Color.white;
                    paint.Stroke();
                }
                paint.ClosePath();
            };
            image.RegisterCallback<ClickEvent>(evt =>
            {
                var rect = contentRect;
                var width = rect.width * .5f;
                var height = rect.height * .5f;
                var swh = Mathf.Min(width, height);
                var widthReduction = (width - swh) * .5f;
                var heightReduction = (height - swh) * .5f;
                var position = (Vector2)evt.localPosition;
                var realWidth = width - widthReduction;
                var realHeight = height - heightReduction;
                if (position.x >= widthReduction && position.x <= realWidth &&
                    position.y >= heightReduction && position.y <= realHeight)
                {
                    _position = position;
                    Saturation = (position.x - widthReduction) / (realWidth - widthReduction);
                    Brightness = (position.y - heightReduction) / (realHeight - heightReduction);
                    image.MarkDirtyRepaint();
                }
            });
            Add(image);
        }

        ~ColorPickerElement()
        {
            Object.Destroy(_gradientTexture);
        }

        private float GetAngleFromVector(float x, float y)
        {
            var radians = Mathf.Atan2(y, x);
            var degrees = radians * (180f / Mathf.PI);
            return ((degrees + 360) % 360 + 360) % 360 / 360f;
        }

        private void OnMouseClicked(ClickEvent evt)
        {
            var direction = (Vector2)evt.localPosition - contentRect.center;

            var rect = contentRect;
            var swh = Mathf.Min(rect.width, rect.height);
            var circleRadius = swh * 0.425f;
            var gradientOffset = swh * 0.1f * .5f;
            var length = direction.magnitude;
            if (length < circleRadius + gradientOffset && length > circleRadius - gradientOffset)
            {
                _direction = direction;
                var angle = GetAngleFromVector(direction.x, -direction.y);
                Hue = angle;
                UpdateGradientTexture();
                MarkDirtyRepaint();
            }
        }

        private void OnGenerateVisualContent(MeshGenerationContext context)
        {
            var rect = contentRect;
            var swh = Mathf.Min(rect.width, rect.height);
            var paint = context.painter2D;
            var circleRadius = swh * 0.425f;
            var gradientWidth = swh * 0.1f;

            // Color ring
            paint.BeginPath();
            {
                paint.Arc(rect.center, circleRadius, 270f, -90f, ArcDirection.CounterClockwise);

                paint.lineWidth = gradientWidth;
                paint.strokeGradient = CircleGradient;
                paint.Stroke();
            }
            paint.ClosePath();

            // Hue position marker
            paint.BeginPath();
            {
                paint.Arc(
                    rect.center + _direction.normalized * circleRadius,
                    swh * 0.05f - 2f, 0, 360
                );

                paint.lineWidth = 1f + 2f;
                paint.strokeColor = Color.black;
                paint.Stroke();

                paint.lineWidth = 1f;
                paint.strokeColor = Color.white;
                paint.Stroke();
            }
            paint.ClosePath();
        }

        private void UpdateGradientTexture()
        {
            if (!_gradientTexture) return;
            var pixels = new Color[_gradientTexture.width * _gradientTexture.height];

            for (var x = 0; x < _gradientTexture.width; x++)
            {
                for (var y = 0; y < _gradientTexture.height; y++)
                {
                    pixels[x * _gradientTexture.width + y] = Color.HSVToRGB(
                        Hue,
                        (float)y / _gradientTexture.height,
                        (float)x / _gradientTexture.width
                    );
                }
            }

            _gradientTexture.SetPixels(pixels);
            _gradientTexture.Apply();
        }
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment