Skip to content

Instantly share code, notes, and snippets.

@softlion
Last active August 6, 2025 06:32
Show Gist options
  • Save softlion/481cb546cf3b2f74cda136cfa1102856 to your computer and use it in GitHub Desktop.
Save softlion/481cb546cf3b2f74cda136cfa1102856 to your computer and use it in GitHub Desktop.
Maui color picker using GraphicsView (no SkiaSharp)
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:vm="clr-namespace:Vapolia.OustGame.ViewModels"
xmlns:controls="clr-namespace:Vapolia.OustGame.Views.Controls"
x:Class="Vapolia.OustGame.Views.ColorPickerPopup"
x:DataType="vm:ColorPickerPopupViewModel"
BackgroundColor="Transparent"
NavigationPage.HasNavigationBar="False">
<Grid BackgroundColor="#80000000">
<Grid.GestureRecognizers>
<TapGestureRecognizer Command="{Binding BackgroundTappedCommand}" />
</Grid.GestureRecognizers>
<!-- Popup content container -->
<Border x:Name="PopupContainer"
BackgroundColor="White"
StrokeShape="RoundRectangle 15" Stroke="Gray" StrokeThickness="1"
Padding="20"
HorizontalOptions="Center" VerticalOptions="Center"
WidthRequest="350" HeightRequest="320"
Scale="0">
<!-- Prevent tap events from bubbling to background -->
<Border.GestureRecognizers>
<TapGestureRecognizer />
</Border.GestureRecognizers>
<VerticalStackLayout Spacing="15">
<!-- Color preview -->
<Border HorizontalOptions="Center"
WidthRequest="100" HeightRequest="40"
StrokeShape="RoundRectangle 8" Stroke="Gray" StrokeThickness="1"
BackgroundColor="{Binding SelectedColor}" />
<controls:MauiColorPicker x:Name="ColorPicker"
WidthRequest="300" HeightRequest="180"
HorizontalOptions="Center" BackgroundColor="Transparent"
SpectrumType="SingleColor"
PickerRatio="1"
SpectrumRadius="15" SpectrumRect="10,10,220,160"
BarVertical="True" BarRect="250, 10, 20, 160" BarRadius="7"
ColorData="{Binding ColorData}" />
</VerticalStackLayout>
</Border>
</Grid>
</ContentPage>
namespace Vapolia.OustGame.Views;
public partial class ColorPickerPopup : ContentPage
{
private readonly ColorPickerPopupViewModel viewModel;
public ColorPickerPopup(ColorPickerPopupViewModel viewModel)
{
BindingContext = this.viewModel = viewModel;
InitializeComponent();
}
protected override async void OnAppearing()
{
base.OnAppearing();
if(PopupContainer.Scale == 0)
await AnimatePopIn();
}
private async Task AnimatePopIn()
{
await PopupContainer.ScaleTo(1.0, 300, Easing.CubicOut);
//await PopupContainer.ScaleTo(1.0, 100, Easing.CubicInOut);
}
private async Task AnimatePopOut()
{
await PopupContainer.ScaleTo(0, 150, Easing.CubicIn);
}
public static async Task<Color?> ShowAsync(INavigation navigation, Color initialColor)
{
var tcs = new TaskCompletionSource<Color?>();
var popup = new ColorPickerPopup(new ColorPickerPopupViewModel(initialColor, tcs));
await navigation.PushModalAsync(popup);
var result = await tcs.Task;
await popup.AnimatePopOut();
await navigation.PopModalAsync();
return result;
}
protected override bool OnBackButtonPressed()
{
viewModel.BackgroundTappedCommand.Execute(null);
return true;
}
}
using Vapolia.OustGame.Views.Controls;
namespace Vapolia.OustGame.ViewModels;
public partial class ColorPickerPopupViewModel : ObservableObject
{
private readonly TaskCompletionSource<Color?> taskCompletionSource;
[ObservableProperty]
public partial Color InitialColor { get; set; }
[ObservableProperty]
public partial Color SelectedColor { get; set; }
public ColorDataLinked ColorData { get; }
public ColorPickerPopupViewModel(Color initialColor, TaskCompletionSource<Color?> tcs)
{
taskCompletionSource = tcs;
InitialColor = SelectedColor = initialColor;
ColorData = new ColorDataLinked(initialColor);
ColorData.PropertyChanged += (sender, args) =>
{
if (args.PropertyName == null)
SelectedColor = ColorData.PickedColor;
};
}
[RelayCommand]
private void BackgroundTapped() => taskCompletionSource.SetResult(SelectedColor);
}
using MauiGestures;
namespace Vapolia.OustGame.Views.Controls;
//Migrated from https://github.com/ss1969/Maui.Color.Picker
using System.ComponentModel;
using System.Globalization;
using System.Runtime.CompilerServices;
// Range:
// H : 0~359
// S : 0~100
// V : 0~100
//
// R : 0~255
// G : 0~255
// B : 0~255
// A : 0~255
//
// TODO:
// speed optimize (use dirty rect ?)
// parameter change cannot correctly draw
// add color name float label
// 2 Color Pickers will interfere
public record ColorDataLinked : INotifyPropertyChanged
{
// Only 1 notify for all related properties update to UI, no separate properties,
// that will cause unhandlable loop notification of set.
// use basic property syntax for value clamp, value sync, fastly update.
public event PropertyChangedEventHandler? PropertyChanged;
private void NotifyPropertyChanged( string? propertyName = null )
=> PropertyChanged?.Invoke( this, new PropertyChangedEventArgs( propertyName ) );
//public event Action<string?>? ParameterChanged;
private Color pickedColor;
private string pickedColorHex;
private byte red;
private byte green;
private byte blue;
private int hue;
private int sat;
private int val;
public bool Updating { get; set; }
public Color PickedColor
{
get => pickedColor;
set
{
if (value != null && !value.Equals(pickedColor))
{
pickedColor = value;
SyncAndNotify();
}
}
}
public string PickedColorHex
{
get => pickedColorHex;
set
{
if ( pickedColorHex != value )
{
pickedColorHex = value;
if (pickedColorHex.Length == 7)
SyncAndNotify();
}
}
}
public byte Red
{
get => red;
set
{
if ( red != value )
{
red = value;
SyncAndNotify();
}
}
}
public byte Green
{
get => green;
set
{
if ( green != value )
{
green = value;
SyncAndNotify();
}
}
}
public byte Blue
{
get => blue;
set
{
if ( blue != value )
{
blue = value;
SyncAndNotify();
}
}
}
public int Hue
{
get => hue;
set
{
value = value > 359 ? 359 : value < 0 ? 0 : value;
if ( hue != value )
{
hue = value;
SyncAndNotify();
}
}
}
public int Sat
{
get => sat;
set
{
value = value > 100 ? 100 : value < 0 ? 0 : value;
if ( sat != value )
{
sat = value;
SyncAndNotify();
}
}
}
public int Val
{
get => val;
set
{
value = value > 100 ? 100 : value < 0 ? 0 : value;
if ( val != value )
{
val = value;
SyncAndNotify();
}
}
}
public ColorDataLinked()
{
hue = 0;
sat = 100;
val = 100;
red = 255;
green = 0;
blue = 0;
pickedColor = PickedColor = Color.FromRgb( red, green, blue );
pickedColorHex = PickedColorHex = "#FF0000";
Updating = false;
}
public ColorDataLinked(Color initialColor) : this()
{
PickedColor = initialColor;
}
public ColorDataLinked(string initialColor) : this()
{
PickedColorHex = initialColor;
}
// Sync every value if one changes
public void SyncAndNotify([CallerMemberName] string? propertyName = null)
{
if (Updating)
return;
switch (propertyName)
{
case nameof(Red):
case nameof(Green):
case nameof(Blue):
pickedColor = Color.FromRgb( red, green, blue );
pickedColorHex = pickedColor.ToHex();
UpdateHsv();
break;
case nameof(Hue):
case nameof(Sat):
case nameof(Val):
pickedColor = Color.FromHsv( hue / 359F, sat / 100F, val / 100F );
pickedColorHex = pickedColor.ToHex();
pickedColor.ToRgb( out red, out green, out blue );
break;
case nameof(PickedColor):
pickedColorHex = pickedColor.ToHex();
pickedColor.ToRgb( out red, out green, out blue );
UpdateHsv();
break;
case nameof(PickedColorHex):
UpdateByHex();
pickedColor.ToRgb( out red, out green, out blue );
UpdateHsv();
break;
}
// Notify View to update value
NotifyPropertyChanged();
//ParameterChanged?.Invoke(propertyName);
}
private void UpdateHsv()
{
pickedColor.ToHsl( out var h, out var s, out var l );
hue = (int)( h * 359 );
sat = (int)( s * 100 );
val = (int)( l * 100 );
}
private void UpdateByHex()
{
try
{
red = byte.Parse( pickedColorHex[1..3], NumberStyles.HexNumber );
green = byte.Parse( pickedColorHex[3..5], NumberStyles.HexNumber );
blue = byte.Parse( pickedColorHex[5..7], NumberStyles.HexNumber );
pickedColor = Color.FromRgb( red, green, blue );
}
catch (ArgumentException)
{
}
}
}
public enum SpectrumType
{
FullColor,
SingleColor
}
public class MauiColorPicker : GraphicsView, IDrawable
{
public static readonly BindableProperty ColorDataProperty = BindableProperty.Create( nameof( ColorData ), typeof( ColorDataLinked ), typeof( MauiColorPicker ), new ColorDataLinked(), propertyChanged: ColorDataChanged);
public static readonly BindableProperty PickerPosProperty = BindableProperty.Create( nameof( PickerPos ), typeof( Point ), typeof( MauiColorPicker ), Point.Zero);
public static readonly BindableProperty PickerRatioProperty = BindableProperty.Create( nameof( PickerRatio ), typeof( float ), typeof( MauiColorPicker ), 1f);
public static readonly BindableProperty SpectrumTypeProperty = BindableProperty.Create( nameof( SpectrumType ), typeof( SpectrumType ), typeof( MauiColorPicker ), SpectrumType.FullColor, BindingMode.OneTime);
public static readonly BindableProperty SpectrumRectProperty = BindableProperty.Create( nameof( SpectrumRect ), typeof( RectF ), typeof( MauiColorPicker ), new RectF( 0, 0, 100, 100 ));
public static readonly BindableProperty SpectrumRadiusProperty = BindableProperty.Create( nameof( SpectrumRadius ), typeof( float ), typeof( MauiColorPicker ), 0f);
public static readonly BindableProperty BarRectProperty = BindableProperty.Create( nameof( BarRect ), typeof( RectF ), typeof( MauiColorPicker ), new RectF( 0, 0, 100, 20 ));
public static readonly BindableProperty BarVerticalProperty = BindableProperty.Create( nameof( BarVertical ), typeof( bool ), typeof( MauiColorPicker ), false);
public static readonly BindableProperty BarRadiusProperty = BindableProperty.Create( nameof( BarRadius ), typeof( float ), typeof( MauiColorPicker ), 0f);
#region Bindable Properties
public ColorDataLinked ColorData { get => (ColorDataLinked)GetValue( ColorDataProperty ); set => SetValue( ColorDataProperty, value ); }
public Point PickerPos { get => (Point)GetValue( PickerPosProperty ); set => SetValue( PickerPosProperty, value ); }
public float PickerRatio { get => (float)GetValue( PickerRatioProperty ); set => SetValue( PickerRatioProperty, value ); }
public SpectrumType SpectrumType { get => (SpectrumType)GetValue( SpectrumTypeProperty ); set => SetValue( SpectrumTypeProperty, value ); }
public RectF SpectrumRect { get => (RectF)GetValue( SpectrumRectProperty ); set => SetValue( SpectrumRectProperty, value ); }
public float SpectrumRadius { get => (float)GetValue( SpectrumRadiusProperty ); set => SetValue( SpectrumRadiusProperty, value ); }
public RectF BarRect { get => (RectF)GetValue( BarRectProperty ); set => SetValue( BarRectProperty, value ); }
public bool BarVertical { get => (bool)GetValue( BarVerticalProperty ); set => SetValue( BarVerticalProperty, value ); }
public float BarRadius { get => (float)GetValue( BarRadiusProperty ); set => SetValue( BarRadiusProperty, value ); }
#endregion
private static void ColorDataChanged(BindableObject bindable, object oldValue, object newValue)
{
if (bindable is MauiColorPicker colorPicker)
colorPicker.ColorDataChanged((ColorDataLinked?)oldValue, (ColorDataLinked?)newValue);
}
private void ColorDataChanged(ColorDataLinked? oldValue, ColorDataLinked? newValue)
{
//oldValue?.ParameterChanged -= ColorData_ParameterChanged;
//newValue?.ParameterChanged += ColorData_ParameterChanged;
oldValue?.PropertyChanged -= ColorData_ParameterChanged;
newValue?.PropertyChanged += ColorData_ParameterChanged;
ColorData_ParameterChanged(this, new PropertyChangedEventArgs(null));
}
// Buffers for Spectrum & bar
private PictureCanvas? barBuffer;
private IPicture? barPicture;
private PointF barPickerPos;
// true to create new bar picture buffer
private bool updateBar = true;
private PictureCanvas? spectrumBuffer;
private IPicture? spectrumPicture;
private PointF spectrumPickerPos;
// true to create new spectrum picture buffer
private bool updateSpectrum = true;
private bool pointerDown;
// 0 : not down in spectrum or bar rect. 1: down in spectrumRect, 2: down in bar rect.
private int pointerDownFrom;
public MauiColorPicker()
{
Drawable = this;
//Pointer
var pointerGestureRecognizer = new PointerGestureRecognizer();
pointerGestureRecognizer.PointerPressed += PointerGestureRecognizer_PointerPressed;
pointerGestureRecognizer.PointerMoved += PointerGestureRecognizer_PointerMoved;
pointerGestureRecognizer.PointerReleased += PointerGestureRecognizer_PointerReleased;
GestureRecognizers.Add(pointerGestureRecognizer);
//Touch
Gesture.SetIsPanImmediate(this, true);
Gesture.SetPanPointCommand(this, new Command<PointEventArgs>(args =>
{
var point = (PointF)args.Point;
if(SpectrumRect.Contains(point))
pointerDownFrom = 1;
else if(BarRect.Contains(point))
pointerDownFrom = 2;
CalculateTapPosition(args.Point);
}));
ColorDataChanged(null, ColorData);
}
//private void ColorData_ParameterChanged(string? propertyName)
private void ColorData_ParameterChanged(object? _, PropertyChangedEventArgs __)
{
UpdatePickerPosByColor();
updateSpectrum = true;
updateBar = true;
//pointerDown = false;
Invalidate();
}
#region Pointer gesture
private void PointerGestureRecognizer_PointerPressed( object? sender, PointerEventArgs e )
{
if ( !pointerDown )
{
var clickPosition = e.GetPosition( this );
if ( clickPosition == null ) return;
if ( SpectrumRect.Contains( clickPosition.Value ) )
pointerDownFrom = 1;
else if ( BarRect.Contains( clickPosition.Value ) )
pointerDownFrom = 2;
else
pointerDownFrom = 0;
CalculateTapPosition( clickPosition.Value );
}
pointerDown = true;
}
private void PointerGestureRecognizer_PointerMoved( object? sender, PointerEventArgs e )
{
if ( !pointerDown ) return;
var clickPosition = e.GetPosition( this );
if ( clickPosition == null ) return;
CalculateTapPosition( clickPosition.Value );
}
private void PointerGestureRecognizer_PointerReleased( object? sender, PointerEventArgs e )
{
if ( pointerDown )
{
var clickPosition = e.GetPosition( this );
if ( clickPosition == null ) return;
CalculateTapPosition( clickPosition.Value );
}
pointerDown = false;
pointerDownFrom = 0;
}
#endregion
/// <summary>
/// Monitor properties and ask for redraw
/// </summary>
protected override void OnPropertyChanged(string? propertyName = null)
{
base.OnPropertyChanged(propertyName);
switch(propertyName)
{
case nameof(WidthRequest):
case nameof(HeightRequest):
case nameof(BackgroundColor):
case nameof(PickerRatio):
case nameof(ColorData):
break;
case nameof(SpectrumType):
updateSpectrum = true;
updateBar = true;
break;
case nameof(SpectrumRadius):
case nameof(SpectrumRect):
spectrumPickerPos = new PointF( SpectrumRect.X, SpectrumRect.Y );
updateSpectrum = true;
break;
case nameof(BarRect):
case nameof(BarVertical):
case nameof(BarRadius):
barPickerPos = new PointF(BarRect.X, BarRect.Y);
updateBar = true;
break;
default:
return;
}
// Init redraw
Invalidate();
}
private void CalculateTapPosition( Point clickPosition )
{
//Outside
if (pointerDownFrom == 0)
return;
//Inside spectrum
if(pointerDownFrom == 1 && !SpectrumRect.Contains( clickPosition ))
return;
//Inside bar
if(pointerDownFrom == 2 && !BarRect.Contains( clickPosition ))
return;
//var d = ( clickPosition - _prev );
//if (Math.Abs(d.Width) + Math.Abs(d.Height) < 5 ) return;
//_prev = clickPosition;
// we update manually later
ColorData.Updating = true;
if ( pointerDownFrom == 1 )
spectrumPickerPos = clickPosition;
else if ( pointerDownFrom == 2 )
{
barPickerPos = clickPosition;
// align picker to bar center
if (BarVertical)
barPickerPos.X = BarRect.X + BarRect.Width / 2;
else
barPickerPos.Y = BarRect.Y + BarRect.Height / 2;
}
switch (SpectrumType)
{
case SpectrumType.FullColor:
{
var val = BarVertical ? ( BarRect.Height - barPickerPos.Y + BarRect.Y ) / BarRect.Height
: ( BarRect.Width - barPickerPos.X + BarRect.X ) / BarRect.Width;
ColorData.Val = (int)( val * 100 );
// Horizontal => Hue 0 to 359
ColorData.Hue = (int)( ( spectrumPickerPos.X - SpectrumRect.X ) * 359 / SpectrumRect.Width );
// Vertical => Sat 100:0
ColorData.Sat = (int)( ( SpectrumRect.Height - ( spectrumPickerPos.Y - SpectrumRect.Y ) ) * 100 / SpectrumRect.Height );
updateBar = true;
updateSpectrum = true;
break;
}
case SpectrumType.SingleColor:
{
var hueValue = BarVertical ? ( barPickerPos.Y - BarRect.Y ) / BarRect.Height
: ( barPickerPos.X - BarRect.X ) / BarRect.Width;
ColorData.Hue = (int)( hueValue * 359 );
// Horizontal => Sat 0:100
ColorData.Sat = (int)( ( spectrumPickerPos.X - SpectrumRect.X ) * 100 / SpectrumRect.Width );
// Vertical => Value 100:0
ColorData.Val = (int)( ( SpectrumRect.Height - ( spectrumPickerPos.Y - SpectrumRect.Y ) ) * 100 / SpectrumRect.Height );
updateSpectrum = true;
break;
}
}
// Done updating ColorData
ColorData.Updating = false;
//Update internal data + notify with a null parameter
ColorData.SyncAndNotify(nameof(ColorData.Hue));
//OnPropertyChanged(nameof(ColorData));
}
private void UpdatePickerPosByColor()
{
if ( SpectrumType == SpectrumType.FullColor )
{
spectrumPickerPos.X = ( ColorData.Hue / 359f * SpectrumRect.Width ) + SpectrumRect.X;
spectrumPickerPos.Y = ( SpectrumRect.Height - ColorData.Sat / 100F * SpectrumRect.Height ) + SpectrumRect.Y;
if ( BarVertical )
{
barPickerPos.X = BarRect.X + BarRect.Width / 2;
barPickerPos.Y = ColorData.Val / 100F * BarRect.Height + BarRect.Y;
}
else
{
barPickerPos.X = ColorData.Val / 100F * BarRect.Width + BarRect.X;
barPickerPos.Y = BarRect.Y + BarRect.Height / 2;
}
}
else if ( SpectrumType == SpectrumType.SingleColor )
{
spectrumPickerPos.X = ( ColorData.Sat / 100F * SpectrumRect.Width ) + SpectrumRect.X;
spectrumPickerPos.Y = ( SpectrumRect.Height - ColorData.Val / 100F * SpectrumRect.Height ) + SpectrumRect.Y;
if ( BarVertical )
{
barPickerPos.X = BarRect.X + BarRect.Width / 2;
barPickerPos.Y = ColorData.Hue / 359F * BarRect.Height + BarRect.Y;
}
else
{
barPickerPos.X = ColorData.Hue / 359F * BarRect.Width + BarRect.X;
barPickerPos.Y = BarRect.Y + BarRect.Height / 2;
}
}
}
#region Draw
public void Draw( ICanvas canvas, RectF dirtyRectF )
{
canvas.ResetState();
DrawBackground(canvas, dirtyRectF);
if (dirtyRectF.IntersectsWith(BarRect))
{
DrawBarRect(canvas);
DrawBarPicker(canvas);
}
var pickerRadius = 6 * PickerRatio;
dirtyRectF.Inflate(pickerRadius, pickerRadius);
if (dirtyRectF.IntersectsWith(SpectrumRect))
{
DrawSpectrumRect(canvas);
DrawSpectrumPicker(canvas);
}
}
private void DrawBackground(ICanvas canvas, RectF dirtyRect)
{
canvas.FillColor = BackgroundColor;
canvas.FillRectangle( dirtyRect );
}
private void DrawSpectrumRect(ICanvas canvas)
{
// if need to update, draw to picture & store
if(updateSpectrum)
{
updateSpectrum = false;
spectrumBuffer?.Dispose();
spectrumBuffer = new PictureCanvas(SpectrumRect.X, SpectrumRect.Y, SpectrumRect.Width, SpectrumRect.Height)
{
Antialias = true,
StrokeSize = 2,
StrokeColor = Black
};
spectrumBuffer.DrawRoundedRectangle( SpectrumRect.X, SpectrumRect.Y, SpectrumRect.Width, SpectrumRect.Height, SpectrumRadius );
if ( SpectrumType == SpectrumType.FullColor )
{
var gradientBrush = new LinearGradientBrush
{
StartPoint = new Point( 0, 0 ),
EndPoint = new Point( 1, 0 ),
GradientStops = [
new GradientStop {Color = Red, Offset=0},
new GradientStop {Color = Color.FromRgb(255, 255, 0), Offset=0.1666F},
new GradientStop {Color = Green, Offset=0.3333F},
new GradientStop {Color = Color.FromRgb(0, 255, 255), Offset=0.5f},
new GradientStop {Color = Blue, Offset=0.6666F},
new GradientStop {Color = Color.FromRgb(255, 0, 255), Offset=0.8333F},
],
};
spectrumBuffer.SetFillPaint(gradientBrush, SpectrumRect);
spectrumBuffer.FillRoundedRectangle(SpectrumRect.X, SpectrumRect.Y, SpectrumRect.Width, SpectrumRect.Height, SpectrumRadius);
gradientBrush = new LinearGradientBrush
{
StartPoint = new Point( 0, 0 ),
EndPoint = new Point( 0, 1 ), // Vertical, White Mask
GradientStops =
[
new GradientStop { Color = White.WithAlpha(0), Offset = 0f }, // Start Color 0% White
new GradientStop { Color = White, Offset = 1f }, // End Color 100% White
]
};
spectrumBuffer.SetFillPaint( gradientBrush, SpectrumRect );
spectrumBuffer.FillRoundedRectangle( SpectrumRect.X, SpectrumRect.Y, SpectrumRect.Width, SpectrumRect.Height, SpectrumRadius );
}
if ( SpectrumType == SpectrumType.SingleColor )
{
spectrumBuffer.FillColor = Color.FromHsva( ColorData.Hue / 359f, 1, 1, 1 );
spectrumBuffer.FillRoundedRectangle( SpectrumRect.X, SpectrumRect.Y, SpectrumRect.Width, SpectrumRect.Height, SpectrumRadius );
var gradientBrush = new LinearGradientBrush
{
StartPoint = new Point( 0, 0 ),
EndPoint = new Point( 1, 0 ), // Horizontal, White Mask
GradientStops =
[
new GradientStop { Color = White, Offset = 0 }, // Start Color 100% White
new GradientStop { Color = White.WithAlpha(0), Offset = 1 }, // End Color 0% White
]
};
spectrumBuffer.SetFillPaint( gradientBrush, SpectrumRect );
spectrumBuffer.FillRoundedRectangle( SpectrumRect.X, SpectrumRect.Y, SpectrumRect.Width, SpectrumRect.Height, SpectrumRadius );
gradientBrush = new LinearGradientBrush
{
StartPoint = new Point( 0, 0 ),
EndPoint = new Point( 0, 1 ), // Vertical, Black Mask
GradientStops =
[
new GradientStop { Color = Black.WithAlpha(0), Offset = 0 }, // Start Color 0% Black
new GradientStop { Color = Black, Offset = 1 }, // End Color 100% Black
],
};
spectrumBuffer.SetFillPaint( gradientBrush, SpectrumRect );
spectrumBuffer.FillRoundedRectangle( SpectrumRect.X, SpectrumRect.Y, SpectrumRect.Width, SpectrumRect.Height, SpectrumRadius );
}
// Store
spectrumPicture = spectrumBuffer.Picture;
}
// Use stored buffer
spectrumPicture?.Draw(canvas);
}
private void DrawBarRect(ICanvas canvas)
{
// if need to update, draw to picture and store
if ( updateBar )
{
updateBar = false;
barBuffer?.Dispose();
barBuffer = new PictureCanvas( BarRect.X, BarRect.Y, BarRect.Width, BarRect.Height )
{
Antialias = true,
StrokeSize = 2,
StrokeColor = Black
};
barBuffer.DrawRoundedRectangle( BarRect.X, BarRect.Y, BarRect.Width, BarRect.Height, BarRadius );
if ( SpectrumType == SpectrumType.FullColor )
{
canvas.FillColor = Color.FromHsv( ColorData.Hue / 359f, 1, 1 );
barBuffer.FillRoundedRectangle( BarRect.X, BarRect.Y, BarRect.Width, BarRect.Height, BarRadius );
var gradientBrush = new LinearGradientBrush
{
StartPoint = new Point( 0, 0 ),
EndPoint = new Point( BarVertical ? 0 : 1, BarVertical ? 1 : 0 ),
GradientStops = [
new GradientStop { Color = Black.WithAlpha(0), Offset = 0.0f }, // Start Color 0% Black
new GradientStop { Color = Black, Offset = 1.0f }, // End Color 100% Black
],
};
barBuffer.SetFillPaint( gradientBrush, BarRect );
barBuffer.FillRoundedRectangle( BarRect.X, BarRect.Y, BarRect.Width, BarRect.Height, BarRadius );
}
if ( SpectrumType == SpectrumType.SingleColor )
{
var gradientBrush = new LinearGradientBrush
{
StartPoint = new Point( 0, 0 ),
EndPoint = new Point( BarVertical ? 0 : 1, BarVertical ? 1 : 0 ),
GradientStops = [
new GradientStop {Color = Red, Offset=0f},
new GradientStop {Color = Color.FromRgb(255, 255, 0), Offset=0.1666f},
new GradientStop {Color = Green, Offset=0.3333f},
new GradientStop {Color = Color.FromRgb(0, 255, 255), Offset=0.5f},
new GradientStop {Color = Blue, Offset=0.6666f},
new GradientStop {Color = Color.FromRgb(255, 0, 255), Offset=0.8333f},
],
};
barBuffer.SetFillPaint( gradientBrush, BarRect );
barBuffer.FillRoundedRectangle( BarRect.X, BarRect.Y, BarRect.Width, BarRect.Height, BarRadius );
}
// store
barPicture = barBuffer.Picture;
}
// use stored buffer
barPicture?.Draw( canvas );
}
private void DrawSpectrumPicker(ICanvas canvas)
{
canvas.StrokeSize = 3 * PickerRatio;
canvas.StrokeColor = ColorData.Red + ColorData.Green + ColorData.Blue < 382 ? White : Black;
canvas.DrawCircle( spectrumPickerPos, 6 * PickerRatio );
}
private void DrawBarPicker(ICanvas canvas)
{
canvas.StrokeSize = 8 * PickerRatio;
canvas.StrokeColor = Color.FromRgb( 255, 255, 255 );
canvas.FillColor = Color.FromRgb( 0, 0, 0 );
RectF indicator;
if (BarVertical)
indicator = new(
BarRect.X,
barPickerPos.Y - 5 * PickerRatio, // make it center
BarRect.Width,
10 * PickerRatio);
else
indicator = new(
barPickerPos.X - 5 * PickerRatio, // make it center
BarRect.Y,
10 * PickerRatio,
BarRect.Height);
canvas.DrawRoundedRectangle( indicator, 20 );
canvas.FillRoundedRectangle( indicator, 20 );
}
#endregion
}
@softlion
Copy link
Author

softlion commented Aug 6, 2025

image

@softlion
Copy link
Author

softlion commented Aug 6, 2025

image

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