Color picker / UI Toolkit
-
-
Save andrew-raphael-lukasik/e7476208779f70e66fbedf5eb10aa2f8 to your computer and use it in GitHub Desktop.
| // 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(); | |
| } | |
| } |
Thank you andrew-raphael-lukasik and Agoxandr for your work and revisions on the color picker. I've made some adjustments to suit my own needs, but I’d still love to share them for anyone out there that needs a color picker.
ColorPickerElement.mp4
Major changes to the color picker:
- Has on color change events, write
picker.OnColorChange += <your method>to subscribe to color change events. - UI updates when changing the HSB values (in code or in the UI Builder)
- Has a top bar to display the old color and the currently selected color.
- Upgraded pointer interaction to click and hold to change values.
UxmlColorPicker.cs
using System;
using UnityEngine;
using UnityEngine.UIElements;
using Object = UnityEngine.Object;
[UxmlElement]
public partial class UxmlColorPicker : VisualElement
{
// style sheet specific:
private const string ColorDisplay = "colorDisplay";
private const string ColorDisplayActive = "active";
private const string ColorDisplayPast = "past";
// runtime render constants:
private const float PositionMarkerOutlineThickness = 2f;
private const float ImageMarkerRadiusFactor = 0.05f;
private const float MarkerLineWidth = 3f;
private const float ColorWheelThickness = 0.1f;
private const float ColorWheelCircleRadiusFactor = 0.425f;
private const float ImageRectSizePercent = 50f;
#region Properties
private float _hue;
[UxmlAttribute, Range(0f, 0.999f)]
public float Hue
{
get => _hue;
set
{
_hue = Math.Clamp(value, 0f, 1f);
UpdateGradientTexture();
OnColorUpdate();
_colorWheel.MarkDirtyRepaint();
}
}
private float _saturation = 1;
[UxmlAttribute, Range(0f, 0.999f)]
public float Saturation
{
get => _saturation;
set
{
_saturation = Math.Clamp(value, 0f, 1f);
_innerImage.MarkDirtyRepaint();
OnColorUpdate();
}
}
private float _brightness = 1;
[UxmlAttribute, Range(0f, 0.999f)]
public float Brightness
{
get => _brightness;
set
{
_brightness = Math.Clamp(value, 0f, 1f);
_innerImage.MarkDirtyRepaint();
OnColorUpdate();
}
}
public Color Color => Color.HSVToRGB(Hue, Saturation, Brightness);
private Gradient CircleGradient { get; } = 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)
}
};
/// <summary>
/// Direction as a computed property. It converts the Hue value into a direction vector for the wheel marker
/// and the other way around.
/// </summary>
private Vector2 Direction
{
get
{
var radians = (float)(Hue * 2 * Math.PI);
return new Vector2(Mathf.Cos(radians), -Mathf.Sin(radians));
}
set
{
var degrees = Vector2.SignedAngle(Vector2.right, new Vector2(value.x, -value.y));
Hue = (degrees + 360) % 360 / 360f;
}
}
private Vector2 Position
{
get
{
var width = _innerImage.resolvedStyle.width;
var height = _innerImage.resolvedStyle.height;
var minSide = Mathf.Min(width, height);
var widthReduction = (width - minSide) / 2;
var heightReduction = (height - minSide) / 2;
var posX = Saturation * minSide + widthReduction;
var posY = (1 - Brightness) * minSide + heightReduction;
return new Vector2(posX, posY);
}
set
{
var width = _innerImage.resolvedStyle.width;
var height = _innerImage.resolvedStyle.height;
var minSide = Mathf.Min(width, height);
var widthReduction = (width - minSide) / 2;
var heightReduction = (height - minSide) / 2;
Saturation = (value.x - widthReduction) / (width - 2 * widthReduction);
Brightness = 1 - (value.y - heightReduction) / (height - 2 * heightReduction);
}
}
#endregion
private readonly Texture2D _gradientTexture;
private readonly VisualElement _colorDisplayActive = new() { name = ColorDisplayActive };
private readonly VisualElement _colorDisplayPast = new() { name = ColorDisplayPast };
private readonly VisualElement _colorWheel;
private readonly Image _innerImage = new Image
{
style =
{
width = Length.Percent(ImageRectSizePercent),
height = Length.Percent(ImageRectSizePercent),
marginTop = Length.Auto(),
marginRight = Length.Auto(),
marginBottom = Length.Auto(),
marginLeft = Length.Auto(),
}
};
public event Action<Color> OnColorChange;
public UxmlColorPicker()
{
style.flexGrow = 1;
var colorDisplay = new VisualElement()
{
name = ColorDisplay
};
colorDisplay.Add(_colorDisplayPast);
colorDisplay.Add(_colorDisplayActive);
this.Add(colorDisplay);
// Adding color wheel
_colorWheel = new VisualElement
{
style =
{
flexGrow = 1
}
};
this.Add(_colorWheel);
_colorWheel.generateVisualContent += OnGenerateVisualContent;
SetWheelMouseCallbackEvents();
// Creating gradient texture amd adding inner image
_gradientTexture = new Texture2D(16, 16, TextureFormat.RGB24, false)
{
filterMode = FilterMode.Bilinear,
hideFlags = HideFlags.HideAndDontSave,
wrapMode = TextureWrapMode.Clamp
};
_innerImage.image = _gradientTexture;
UpdateGradientTexture();
_colorWheel.Add(_innerImage);
_innerImage.generateVisualContent += ImageGenerateVisualContent;
SetImageMouseCallbackEvents();
OnColorUpdate();
}
~UxmlColorPicker()
{
Object.Destroy(_gradientTexture);
}
private void OnColorUpdate()
{
_colorDisplayActive.style.backgroundColor = Color;
OnColorChange?.Invoke(Color);
}
public void SetCurrentColor(Color color)
{
_colorDisplayPast.style.backgroundColor = color;
Color.RGBToHSV(color, out var hue, out var saturation, out var brightness);
Hue = hue;
Saturation = saturation;
Brightness = brightness;
}
#region Image
private void SetImageMouseCallbackEvents()
{
_innerImage.RegisterCallback<PointerDownEvent>(evt =>
{
if (!IsPointerOnImage(_innerImage.WorldToLocal(evt.position)))
return;
_innerImage.CaptureMouse();
SetImagePosition(_innerImage.WorldToLocal(evt.position));
});
_innerImage.RegisterCallback<PointerMoveEvent>(evt =>
{
if (_innerImage.HasMouseCapture())
{
SetImagePosition(_innerImage.WorldToLocal(evt.position));
}
});
_innerImage.RegisterCallback<PointerUpEvent>(evt => { _innerImage.ReleaseMouse(); });
}
private bool IsPointerOnImage(Vector2 pointer)
{
var width = _innerImage.resolvedStyle.width;
var height = _innerImage.resolvedStyle.height;
var minSide = Mathf.Min(width, height);
var widthReduction = (width - minSide) / 2;
var heightReduction = (height - minSide) / 2;
return (pointer.x >= widthReduction && pointer.x <= width - widthReduction &&
pointer.y >= heightReduction && pointer.y <= height - heightReduction);
}
private Vector2 ImageClampPosition(Vector2 position)
{
var width = _innerImage.resolvedStyle.width;
var height = _innerImage.resolvedStyle.height;
var minSide = Mathf.Min(width, height);
var widthReduction = (width - minSide) / 2;
var heightReduction = (height - minSide) / 2;
var clampedWidth = Math.Clamp(position.x, widthReduction, width - widthReduction);
var clampedHeight = Math.Clamp(position.y, heightReduction, height - heightReduction);
return new Vector2(clampedWidth, clampedHeight);
}
private void SetImagePosition(Vector2 eventPos)
{
var position = ImageClampPosition(eventPos);
this.Position = position;
_innerImage.MarkDirtyRepaint();
}
private void ImageGenerateVisualContent(MeshGenerationContext context)
{
var rect = _innerImage.contentRect;
var swh = Mathf.Min(rect.width, rect.height);
var paint = context.painter2D;
// Image position marker
paint.BeginPath();
{
paint.Arc(Position, swh * ImageMarkerRadiusFactor - PositionMarkerOutlineThickness, 0, 360);
paint.lineWidth = MarkerLineWidth + PositionMarkerOutlineThickness;
paint.strokeColor = Color.black;
paint.Stroke();
paint.lineWidth = MarkerLineWidth;
paint.strokeColor = Color.white;
paint.Stroke();
}
paint.ClosePath();
}
#endregion
#region Color Wheel
private void SetWheelMouseCallbackEvents()
{
_colorWheel.RegisterCallback<PointerDownEvent>(evt =>
{
if (!IsPointerOnWheel(_colorWheel.WorldToLocal(evt.position)))
return;
_colorWheel.CaptureMouse();
SetWheelHue(_colorWheel.WorldToLocal(evt.position));
});
_colorWheel.RegisterCallback<PointerMoveEvent>(evt =>
{
if (_colorWheel.HasMouseCapture())
{
SetWheelHue(_colorWheel.WorldToLocal(evt.position));
}
});
_colorWheel.RegisterCallback<PointerUpEvent>(evt => { _colorWheel.ReleaseMouse(); });
}
private bool IsPointerOnWheel(Vector2 pointer)
{
var direction = pointer - _colorWheel.contentRect.center;
var rect = _colorWheel.contentRect;
var swh = Mathf.Min(rect.width, rect.height);
var circleRadius = swh * ColorWheelCircleRadiusFactor;
var gradientOffset = swh * ColorWheelThickness / 2;
var length = direction.magnitude;
return length < circleRadius + gradientOffset && length > circleRadius - gradientOffset;
}
private void SetWheelHue(Vector2 eventPosition)
{
var direction = eventPosition - _colorWheel.contentRect.center;
Direction = direction;
}
private void OnGenerateVisualContent(MeshGenerationContext context)
{
var rect = _colorWheel.contentRect;
var swh = Mathf.Min(rect.width, rect.height);
var paint = context.painter2D;
var circleRadius = swh * ColorWheelCircleRadiusFactor;
var gradientWidth = swh * ColorWheelThickness;
// 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,
gradientWidth / 2 - PositionMarkerOutlineThickness, 0, 360
);
paint.lineWidth = MarkerLineWidth + PositionMarkerOutlineThickness;
paint.strokeColor = Color.black;
paint.Stroke();
paint.lineWidth = MarkerLineWidth;
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();
}
#endregion
}ColorPickerMenu.uss
:root {
}
#colorDisplay {
height: 20%;
max-height: 80px;
justify-content: center;
flex-direction: row;
padding-top: 8px;
padding-right: 8px;
padding-bottom: 0;
padding-left: 8px;
}
#colorDisplay > #active {
width: 40%;
background-color: rgb(255, 255, 255);
border-top-right-radius: 15px;
border-bottom-right-radius: 15px;
}
#colorDisplay > #past {
width: 40%;
background-color: rgb(255, 255, 255);
border-top-left-radius: 15px;
border-bottom-left-radius: 15px;
}Thanks LeoWillmann! But unless I'm misunderstanding how it works, you are missing a few namespace imports and a type alias at the top of UxmlColorPicker.cs. Basically edit your comment to put these four using statements on the top of your code.
For anyone else, here is the full script with that correction and with the top bar removed (that bar was the main thing ColorPickerMenu.css was for, so now you don't need that file). Create a file called UxmlColorPicker.cs and paste this into it. Now in UI Builder you can go to Library > Project > Custom Controls to drag this element into your layout. In your own scripts, you can write picker.OnColorChange += <your method> to subscribe to color change events.
using System;
using UnityEngine;
using UnityEngine.UIElements;
using Object = UnityEngine.Object;
[UxmlElement]
public partial class UxmlColorPicker : VisualElement
{
private const string ColorDisplay = "colorDisplay";
// runtime render constants:
private const float PositionMarkerOutlineThickness = 2f;
private const float ImageMarkerRadiusFactor = 0.05f;
private const float MarkerLineWidth = 3f;
private const float ColorWheelThickness = 0.1f;
private const float ColorWheelCircleRadiusFactor = 0.425f;
private const float ImageRectSizePercent = 50f;
#region Properties
private float _hue;
[UxmlAttribute, Range(0f, 0.999f)]
public float Hue
{
get => _hue;
set
{
_hue = Math.Clamp(value, 0f, 1f);
UpdateGradientTexture();
OnColorUpdate();
_colorWheel.MarkDirtyRepaint();
}
}
private float _saturation = 1;
[UxmlAttribute, Range(0f, 0.999f)]
public float Saturation
{
get => _saturation;
set
{
_saturation = Math.Clamp(value, 0f, 1f);
_innerImage.MarkDirtyRepaint();
OnColorUpdate();
}
}
private float _brightness = 1;
[UxmlAttribute, Range(0f, 0.999f)]
public float Brightness
{
get => _brightness;
set
{
_brightness = Math.Clamp(value, 0f, 1f);
_innerImage.MarkDirtyRepaint();
OnColorUpdate();
}
}
public Color Color => Color.HSVToRGB(Hue, Saturation, Brightness);
private Gradient CircleGradient { get; } = 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)
}
};
/// <summary>
/// Direction as a computed property. It converts the Hue value into a direction vector for the wheel marker
/// and the other way around.
/// </summary>
private Vector2 Direction
{
get
{
var radians = (float)(Hue * 2 * Math.PI);
return new Vector2(Mathf.Cos(radians), -Mathf.Sin(radians));
}
set
{
var degrees = Vector2.SignedAngle(Vector2.right, new Vector2(value.x, -value.y));
Hue = (degrees + 360) % 360 / 360f;
}
}
private Vector2 Position
{
get
{
var width = _innerImage.resolvedStyle.width;
var height = _innerImage.resolvedStyle.height;
var minSide = Mathf.Min(width, height);
var widthReduction = (width - minSide) / 2;
var heightReduction = (height - minSide) / 2;
var posX = Saturation * minSide + widthReduction;
var posY = (1 - Brightness) * minSide + heightReduction;
return new Vector2(posX, posY);
}
set
{
var width = _innerImage.resolvedStyle.width;
var height = _innerImage.resolvedStyle.height;
var minSide = Mathf.Min(width, height);
var widthReduction = (width - minSide) / 2;
var heightReduction = (height - minSide) / 2;
Saturation = (value.x - widthReduction) / (width - 2 * widthReduction);
Brightness = 1 - (value.y - heightReduction) / (height - 2 * heightReduction);
}
}
#endregion
private readonly Texture2D _gradientTexture;
private readonly VisualElement _colorWheel;
private readonly Image _innerImage = new Image
{
style =
{
width = Length.Percent(ImageRectSizePercent),
height = Length.Percent(ImageRectSizePercent),
marginTop = Length.Auto(),
marginRight = Length.Auto(),
marginBottom = Length.Auto(),
marginLeft = Length.Auto(),
}
};
public event Action<Color> OnColorChange;
public UxmlColorPicker()
{
style.flexGrow = 1;
var colorDisplay = new VisualElement()
{
name = ColorDisplay
};
this.Add(colorDisplay);
// Adding color wheel
_colorWheel = new VisualElement
{
style =
{
flexGrow = 1
}
};
this.Add(_colorWheel);
_colorWheel.generateVisualContent += OnGenerateVisualContent;
SetWheelMouseCallbackEvents();
// Creating gradient texture amd adding inner image
_gradientTexture = new Texture2D(16, 16, TextureFormat.RGB24, false)
{
filterMode = FilterMode.Bilinear,
hideFlags = HideFlags.HideAndDontSave,
wrapMode = TextureWrapMode.Clamp
};
_innerImage.image = _gradientTexture;
UpdateGradientTexture();
_colorWheel.Add(_innerImage);
_innerImage.generateVisualContent += ImageGenerateVisualContent;
SetImageMouseCallbackEvents();
OnColorUpdate();
}
~UxmlColorPicker()
{
Object.Destroy(_gradientTexture);
}
private void OnColorUpdate()
{
OnColorChange?.Invoke(Color);
}
public void SetCurrentColor(Color color)
{
Color.RGBToHSV(color, out var hue, out var saturation, out var brightness);
Hue = hue;
Saturation = saturation;
Brightness = brightness;
}
#region Image
private void SetImageMouseCallbackEvents()
{
_innerImage.RegisterCallback<PointerDownEvent>(evt =>
{
if (!IsPointerOnImage(_innerImage.WorldToLocal(evt.position)))
return;
_innerImage.CaptureMouse();
SetImagePosition(_innerImage.WorldToLocal(evt.position));
});
_innerImage.RegisterCallback<PointerMoveEvent>(evt =>
{
if (_innerImage.HasMouseCapture())
{
SetImagePosition(_innerImage.WorldToLocal(evt.position));
}
});
_innerImage.RegisterCallback<PointerUpEvent>(evt => { _innerImage.ReleaseMouse(); });
}
private bool IsPointerOnImage(Vector2 pointer)
{
var width = _innerImage.resolvedStyle.width;
var height = _innerImage.resolvedStyle.height;
var minSide = Mathf.Min(width, height);
var widthReduction = (width - minSide) / 2;
var heightReduction = (height - minSide) / 2;
return (pointer.x >= widthReduction && pointer.x <= width - widthReduction &&
pointer.y >= heightReduction && pointer.y <= height - heightReduction);
}
private Vector2 ImageClampPosition(Vector2 position)
{
var width = _innerImage.resolvedStyle.width;
var height = _innerImage.resolvedStyle.height;
var minSide = Mathf.Min(width, height);
var widthReduction = (width - minSide) / 2;
var heightReduction = (height - minSide) / 2;
var clampedWidth = Math.Clamp(position.x, widthReduction, width - widthReduction);
var clampedHeight = Math.Clamp(position.y, heightReduction, height - heightReduction);
return new Vector2(clampedWidth, clampedHeight);
}
private void SetImagePosition(Vector2 eventPos)
{
var position = ImageClampPosition(eventPos);
this.Position = position;
_innerImage.MarkDirtyRepaint();
}
private void ImageGenerateVisualContent(MeshGenerationContext context)
{
var rect = _innerImage.contentRect;
var swh = Mathf.Min(rect.width, rect.height);
var paint = context.painter2D;
// Image position marker
paint.BeginPath();
{
paint.Arc(Position, swh * ImageMarkerRadiusFactor - PositionMarkerOutlineThickness, 0, 360);
paint.lineWidth = MarkerLineWidth + PositionMarkerOutlineThickness;
paint.strokeColor = Color.black;
paint.Stroke();
paint.lineWidth = MarkerLineWidth;
paint.strokeColor = Color.white;
paint.Stroke();
}
paint.ClosePath();
}
#endregion
#region Color Wheel
private void SetWheelMouseCallbackEvents()
{
_colorWheel.RegisterCallback<PointerDownEvent>(evt =>
{
if (!IsPointerOnWheel(_colorWheel.WorldToLocal(evt.position)))
return;
_colorWheel.CaptureMouse();
SetWheelHue(_colorWheel.WorldToLocal(evt.position));
});
_colorWheel.RegisterCallback<PointerMoveEvent>(evt =>
{
if (_colorWheel.HasMouseCapture())
{
SetWheelHue(_colorWheel.WorldToLocal(evt.position));
}
});
_colorWheel.RegisterCallback<PointerUpEvent>(evt => { _colorWheel.ReleaseMouse(); });
}
private bool IsPointerOnWheel(Vector2 pointer)
{
var direction = pointer - _colorWheel.contentRect.center;
var rect = _colorWheel.contentRect;
var swh = Mathf.Min(rect.width, rect.height);
var circleRadius = swh * ColorWheelCircleRadiusFactor;
var gradientOffset = swh * ColorWheelThickness / 2;
var length = direction.magnitude;
return length < circleRadius + gradientOffset && length > circleRadius - gradientOffset;
}
private void SetWheelHue(Vector2 eventPosition)
{
var direction = eventPosition - _colorWheel.contentRect.center;
Direction = direction;
}
private void OnGenerateVisualContent(MeshGenerationContext context)
{
var rect = _colorWheel.contentRect;
var swh = Mathf.Min(rect.width, rect.height);
var paint = context.painter2D;
var circleRadius = swh * ColorWheelCircleRadiusFactor;
var gradientWidth = swh * ColorWheelThickness;
// 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,
gradientWidth / 2 - PositionMarkerOutlineThickness, 0, 360
);
paint.lineWidth = MarkerLineWidth + PositionMarkerOutlineThickness;
paint.strokeColor = Color.black;
paint.Stroke();
paint.lineWidth = MarkerLineWidth;
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();
}
#endregion
}
Yes, I forgot to copy them in. My previous post has been updated.

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.