├── .gitignore ├── README.md ├── assets └── hsv_wheel.png └── src ├── ColorPicker.sln ├── ColorPicker ├── ColorPicker.csproj ├── ColorWheel.axaml ├── ColorWheel.axaml.cs ├── Converters │ ├── BooleanToNumericConverter.cs │ ├── EnumToBooleanConverter.cs │ ├── LogarithmicConverter.cs │ ├── RGBColorToBrushConverter.cs │ ├── RGBColorToHexConverter.cs │ └── StringFormatConverter.cs ├── Structures │ ├── CIE1931.cs │ ├── CIEXYZ.cs │ ├── ColorTemperature.cs │ ├── HSV.cs │ └── RGB.cs ├── Utilities │ └── CircularMath.cs └── Wheels │ ├── ColorWheelBase.cs │ └── HSVWheel.cs └── Sample ├── App.axaml ├── App.axaml.cs ├── Assets └── avalonia-logo.ico ├── Program.cs ├── Sample.csproj ├── ViewLocator.cs ├── ViewModels ├── MainWindowViewModel.cs └── ViewModelBase.cs ├── Views ├── MainWindow.axaml └── MainWindow.axaml.cs └── nuget.config /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # NDepend 14 | # NDepend 15 | *.ndproj 16 | /NDependOut 17 | QueryResult.htm 18 | 19 | # User-specific files (MonoDevelop/Xamarin Studio) 20 | *.userprefs 21 | 22 | # Mono auto generated files 23 | mono_crash.* 24 | 25 | # Build results 26 | [Dd]ebug/ 27 | [Dd]ebugPublic/ 28 | [Rr]elease/ 29 | [Rr]eleases/ 30 | x64/ 31 | x86/ 32 | [Aa][Rr][Mm]/ 33 | [Aa][Rr][Mm]64/ 34 | bld/ 35 | [Bb]in/ 36 | [Oo]bj/ 37 | [Ll]og/ 38 | [Ll]ogs/ 39 | 40 | # Visual Studio 2015/2017 cache/options directory 41 | .vs/ 42 | # Uncomment if you have tasks that create the project's static files in wwwroot 43 | #wwwroot/ 44 | 45 | # Visual Studio 2017 auto generated files 46 | Generated\ Files/ 47 | 48 | # MSTest test Results 49 | [Tt]est[Rr]esult*/ 50 | [Bb]uild[Ll]og.* 51 | 52 | # NUnit 53 | *.VisualState.xml 54 | TestResult.xml 55 | nunit-*.xml 56 | 57 | # Build Results of an ATL Project 58 | [Dd]ebugPS/ 59 | [Rr]eleasePS/ 60 | dlldata.c 61 | 62 | # Benchmark Results 63 | BenchmarkDotNet.Artifacts/ 64 | 65 | # .NET Core 66 | project.lock.json 67 | project.fragment.lock.json 68 | artifacts/ 69 | 70 | # StyleCop 71 | StyleCopReport.xml 72 | 73 | # Files built by Visual Studio 74 | *_i.c 75 | *_p.c 76 | *_h.h 77 | *.ilk 78 | *.meta 79 | *.obj 80 | *.iobj 81 | *.pch 82 | *.pdb 83 | *.ipdb 84 | *.pgc 85 | *.pgd 86 | *.rsp 87 | *.sbr 88 | *.tlb 89 | *.tli 90 | *.tlh 91 | *.tmp 92 | *.tmp_proj 93 | *_wpftmp.csproj 94 | *.log 95 | *.vspscc 96 | *.vssscc 97 | .builds 98 | *.pidb 99 | *.svclog 100 | *.scc 101 | 102 | # Chutzpah Test files 103 | _Chutzpah* 104 | 105 | # Visual C++ cache files 106 | ipch/ 107 | *.aps 108 | *.ncb 109 | *.opendb 110 | *.opensdf 111 | *.sdf 112 | *.cachefile 113 | *.VC.db 114 | *.VC.VC.opendb 115 | 116 | # Visual Studio profiler 117 | *.psess 118 | *.vsp 119 | *.vspx 120 | *.sap 121 | 122 | # Visual Studio Trace Files 123 | *.e2e 124 | 125 | # TFS 2012 Local Workspace 126 | $tf/ 127 | 128 | # Guidance Automation Toolkit 129 | *.gpState 130 | 131 | # ReSharper is a .NET coding add-in 132 | _ReSharper*/ 133 | *.[Rr]e[Ss]harper 134 | *.DotSettings.user 135 | 136 | # JustCode is a .NET coding add-in 137 | .JustCode 138 | 139 | # TeamCity is a build add-in 140 | _TeamCity* 141 | 142 | # DotCover is a Code Coverage Tool 143 | *.dotCover 144 | 145 | # AxoCover is a Code Coverage Tool 146 | .axoCover/* 147 | !.axoCover/settings.json 148 | 149 | # Visual Studio code coverage results 150 | *.coverage 151 | *.coveragexml 152 | 153 | # NCrunch 154 | _NCrunch_* 155 | .*crunch*.local.xml 156 | nCrunchTemp_* 157 | 158 | # MightyMoose 159 | *.mm.* 160 | AutoTest.Net/ 161 | 162 | # Web workbench (sass) 163 | .sass-cache/ 164 | 165 | # Installshield output folder 166 | [Ee]xpress/ 167 | 168 | # DocProject is a documentation generator add-in 169 | DocProject/buildhelp/ 170 | DocProject/Help/*.HxT 171 | DocProject/Help/*.HxC 172 | DocProject/Help/*.hhc 173 | DocProject/Help/*.hhk 174 | DocProject/Help/*.hhp 175 | DocProject/Help/Html2 176 | DocProject/Help/html 177 | 178 | # Click-Once directory 179 | publish/ 180 | 181 | # Publish Web Output 182 | *.[Pp]ublish.xml 183 | *.azurePubxml 184 | # Note: Comment the next line if you want to checkin your web deploy settings, 185 | # but database connection strings (with potential passwords) will be unencrypted 186 | *.pubxml 187 | *.publishproj 188 | 189 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 190 | # checkin your Azure Web App publish settings, but sensitive information contained 191 | # in these scripts will be unencrypted 192 | PublishScripts/ 193 | 194 | # NuGet Packages 195 | *.nupkg 196 | # NuGet Symbol Packages 197 | *.snupkg 198 | # The packages folder can be ignored because of Package Restore 199 | **/[Pp]ackages/* 200 | # except build/, which is used as an MSBuild target. 201 | !**/[Pp]ackages/build/ 202 | # Uncomment if necessary however generally it will be regenerated when needed 203 | #!**/[Pp]ackages/repositories.config 204 | # NuGet v3's project.json files produces more ignorable files 205 | *.nuget.props 206 | *.nuget.targets 207 | 208 | # Microsoft Azure Build Output 209 | csx/ 210 | *.build.csdef 211 | 212 | # Microsoft Azure Emulator 213 | ecf/ 214 | rcf/ 215 | 216 | # Windows Store app package directories and files 217 | AppPackages/ 218 | BundleArtifacts/ 219 | Package.StoreAssociation.xml 220 | _pkginfo.txt 221 | *.appx 222 | *.appxbundle 223 | *.appxupload 224 | 225 | # Visual Studio cache files 226 | # files ending in .cache can be ignored 227 | *.[Cc]ache 228 | # but keep track of directories ending in .cache 229 | !?*.[Cc]ache/ 230 | 231 | # Others 232 | ClientBin/ 233 | ~$* 234 | *~ 235 | *.dbmdl 236 | *.dbproj.schemaview 237 | *.jfm 238 | *.pfx 239 | *.publishsettings 240 | orleans.codegen.cs 241 | 242 | # Including strong name files can present a security risk 243 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 244 | #*.snk 245 | 246 | # Since there are multiple workflows, uncomment next line to ignore bower_components 247 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 248 | #bower_components/ 249 | 250 | # RIA/Silverlight projects 251 | Generated_Code/ 252 | 253 | # Backup & report files from converting an old project file 254 | # to a newer Visual Studio version. Backup files are not needed, 255 | # because we have git ;-) 256 | _UpgradeReport_Files/ 257 | Backup*/ 258 | UpgradeLog*.XML 259 | UpgradeLog*.htm 260 | ServiceFabricBackup/ 261 | *.rptproj.bak 262 | 263 | # SQL Server files 264 | *.mdf 265 | *.ldf 266 | *.ndf 267 | 268 | # Business Intelligence projects 269 | *.rdl.data 270 | *.bim.layout 271 | *.bim_*.settings 272 | *.rptproj.rsuser 273 | *- [Bb]ackup.rdl 274 | *- [Bb]ackup ([0-9]).rdl 275 | *- [Bb]ackup ([0-9][0-9]).rdl 276 | 277 | # Microsoft Fakes 278 | FakesAssemblies/ 279 | 280 | # GhostDoc plugin setting file 281 | *.GhostDoc.xml 282 | 283 | # Node.js Tools for Visual Studio 284 | .ntvs_analysis.dat 285 | node_modules/ 286 | 287 | # Visual Studio 6 build log 288 | *.plg 289 | 290 | # Visual Studio 6 workspace options file 291 | *.opt 292 | 293 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 294 | *.vbw 295 | 296 | # Visual Studio LightSwitch build output 297 | **/*.HTMLClient/GeneratedArtifacts 298 | **/*.DesktopClient/GeneratedArtifacts 299 | **/*.DesktopClient/ModelManifest.xml 300 | **/*.Server/GeneratedArtifacts 301 | **/*.Server/ModelManifest.xml 302 | _Pvt_Extensions 303 | 304 | # Paket dependency manager 305 | .paket/paket.exe 306 | paket-files/ 307 | 308 | # FAKE - F# Make 309 | .fake/ 310 | 311 | # CodeRush personal settings 312 | .cr/personal 313 | 314 | # Python Tools for Visual Studio (PTVS) 315 | __pycache__/ 316 | *.pyc 317 | 318 | # Cake - Uncomment if you are using it 319 | # tools/** 320 | # !tools/packages.config 321 | 322 | # Tabs Studio 323 | *.tss 324 | 325 | # Telerik's JustMock configuration file 326 | *.jmconfig 327 | 328 | # BizTalk build output 329 | *.btp.cs 330 | *.btm.cs 331 | *.odx.cs 332 | *.xsd.cs 333 | 334 | # OpenCover UI analysis results 335 | OpenCover/ 336 | 337 | # Azure Stream Analytics local run output 338 | ASALocalRun/ 339 | 340 | # MSBuild Binary and Structured Log 341 | *.binlog 342 | 343 | # NVidia Nsight GPU debugger configuration file 344 | *.nvuser 345 | 346 | # MFractors (Xamarin productivity tool) working folder 347 | .mfractor/ 348 | 349 | # Local History for Visual Studio 350 | .localhistory/ 351 | 352 | # BeatPulse healthcheck temp database 353 | healthchecksdb 354 | 355 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 356 | MigrationBackup/ 357 | 358 | # Ionide (cross platform F# VS Code tools) working folder 359 | .ionide/ 360 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Avalonia Color Picker 2 | An Avalonia Color Picker. Includes a HSV color wheel, CIE views and Sketch like pickers. 3 | 4 | ## Update 5 | This project has been picked up by [Aura.UI](https://github.com/PieroCastillo/Aura.UI). I'd recommend using that library instead :) 6 | 7 | 8 |

9 | 10 | HSV Color Wheel 11 | 12 |

13 | 14 | 15 | ## Getting Started 16 | 17 | ### Import the XAML namespaces 18 | ``` 19 | 20 | xmlns:cp="clr-namespace:ColorPicker;assembly=ColorPicker" 21 | 22 | ``` 23 | 24 | ### HSV Color Wheel 25 | ``` 26 | 28 | ``` 29 | 30 | ## Provided Converters 31 | * RGBColor To Hex 32 | * RGBColor To SolidBrush 33 | -------------------------------------------------------------------------------- /assets/hsv_wheel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MikeCodesDotNET/ColorPicker/044aa2e4394b8eb9950494fe8adbf3a76f5ecdc0/assets/hsv_wheel.png -------------------------------------------------------------------------------- /src/ColorPicker.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30128.74 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample", "Sample\Sample.csproj", "{C68751DC-994E-4F7A-BCE0-C72DBF8B39F3}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ColorPicker", "ColorPicker\ColorPicker.csproj", "{5705E063-0919-4A16-8DFB-CCB4E7445B75}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {C68751DC-994E-4F7A-BCE0-C72DBF8B39F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {C68751DC-994E-4F7A-BCE0-C72DBF8B39F3}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {C68751DC-994E-4F7A-BCE0-C72DBF8B39F3}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {C68751DC-994E-4F7A-BCE0-C72DBF8B39F3}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {5705E063-0919-4A16-8DFB-CCB4E7445B75}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {5705E063-0919-4A16-8DFB-CCB4E7445B75}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {5705E063-0919-4A16-8DFB-CCB4E7445B75}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {5705E063-0919-4A16-8DFB-CCB4E7445B75}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {747D3D83-B5C2-4107-8614-B39B07C686D0} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /src/ColorPicker/ColorPicker.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | Library 6 | 8.0 7 | disable 8 | $(NoWarn);NU1701 9 | 10 | 11 | 12 | 0.10.0 13 | preview2 14 | Mike James 15 | Mike James 16 | Avalonia color-picker controls (HSV colour picker, arc-slider, XYZ/xyY color picker) 17 | 18 | Includes: 19 | - CIE1931 luminance correction. 20 | - CIE XYZ/xyY models for device-independent colour control. 21 | Copyright © Mike James 2020 22 | ColorPicker 23 | Color Picker 24 | 25 | 26 | 27 | true 28 | 29 | 30 | 31 | true 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | %(Filename) 40 | 41 | 42 | Designer 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/ColorPicker/ColorWheel.axaml: -------------------------------------------------------------------------------- 1 |  7 | 8 | 9 | 16 | 17 | 20 | 21 | 22 | 23 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/ColorPicker/ColorWheel.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls; 3 | using Avalonia.Controls.Shapes; 4 | using Avalonia.Input; 5 | using Avalonia.Markup.Xaml; 6 | using Avalonia.Media; 7 | using ColorPicker.Structures; 8 | using ColorPicker.Utilities; 9 | using ColorPicker.Wheels; 10 | using System; 11 | 12 | namespace ColorPicker 13 | { 14 | public class ColorWheel : UserControl 15 | { 16 | 17 | public static readonly StyledProperty ThumbSizeProperty = AvaloniaProperty.Register(nameof(ThumbSize)); 18 | public static readonly StyledProperty ThetaProperty = AvaloniaProperty.Register(nameof(Theta)); 19 | public static readonly StyledProperty RadProperty = AvaloniaProperty.Register(nameof(Rad)); 20 | public static readonly StyledProperty SelectedColorProperty = AvaloniaProperty.Register(nameof(SelectedColor), new RGBColor(255, 255, 255), false, Avalonia.Data.BindingMode.TwoWay); 21 | 22 | 23 | //UI Controls (defined in XAML) 24 | private Ellipse _selector; 25 | private Grid _grid; 26 | 27 | 28 | private HSVColor hsv; 29 | 30 | 31 | private Type wheelClass = typeof(HSVWheel); 32 | private ColorWheelBase wheel; 33 | private bool isDragging = false; 34 | 35 | public Type WheelClass 36 | { 37 | get { return typeof(HSVWheel); } 38 | set 39 | { 40 | wheelClass = value; 41 | InstantiateWheel(); 42 | } 43 | } 44 | 45 | public ColorWheel() 46 | { 47 | this.InitializeComponent(); 48 | } 49 | 50 | private void InitializeComponent() 51 | { 52 | AvaloniaXamlLoader.Load(this); 53 | 54 | _selector = this.Get("selector"); 55 | _grid = this.Get("grid"); 56 | 57 | _selector.PointerMoved += _selector_PointerMoved; 58 | _selector.PointerPressed += _selector_PointerPressed; 59 | _selector.PointerReleased += _selector_PointerReleased; 60 | 61 | WheelClass = typeof(HSVWheel); 62 | } 63 | 64 | 65 | //Public properties 66 | public double ThumbSize 67 | { 68 | get { return (double)GetValue(ThumbSizeProperty); } 69 | set { SetValue(ThumbSizeProperty, value); UpdateThumbSize(); } 70 | } 71 | 72 | public double Theta 73 | { 74 | get { return (double)GetValue(ThetaProperty); } 75 | set { SetValue(ThetaProperty, CircularMath.Mod(value)); } 76 | } 77 | 78 | public double Rad 79 | { 80 | get { return (double)GetValue(RadProperty); } 81 | set { SetValue(RadProperty, value); } 82 | } 83 | 84 | public RGBColor SelectedColor 85 | { 86 | get { return GetValue(SelectedColorProperty); } 87 | set { SetValue(SelectedColorProperty, value); } 88 | } 89 | 90 | 91 | //Wheel Creation & Configuration 92 | 93 | public override void Render(DrawingContext context) 94 | { 95 | UpdateSelector(); 96 | base.Render(context); 97 | } 98 | 99 | void InstantiateWheel() 100 | { 101 | 102 | //Leaving this here in case I create more color wheel types... 103 | 104 | if (wheel != null) 105 | this._grid.Children.Remove(wheel); 106 | 107 | if (wheelClass != null) 108 | { 109 | wheel = (ColorWheelBase)Activator.CreateInstance(WheelClass); 110 | wheel.Name = "wheel"; 111 | wheel.PointerPressed += Wheel_PointerPressed; ; 112 | wheel.ZIndex = -2; 113 | _grid.Children.Add(wheel); 114 | 115 | wheel.PointerPressed += Wheel_PointerPressed; 116 | } 117 | } 118 | 119 | 120 | private void UpdateThumbSize() 121 | { 122 | _selector.Width = ThumbSize; 123 | _selector.Height = ThumbSize; 124 | } 125 | 126 | 127 | 128 | //Calculations 129 | private double CalculateTheta(Point point) 130 | { 131 | double cx = Bounds.Width / 2; 132 | double cy = Bounds.Height / 2; 133 | 134 | double dx = point.X - cx; 135 | double dy = point.Y - cy; 136 | 137 | double angle = Math.Atan2(dx, dy) / Math.PI * 180.0; 138 | 139 | // Theta is offset by 180 degrees, so red appears at the top 140 | return CircularMath.Mod(angle - 180.0); 141 | } 142 | 143 | private double CalculateR(Point point) 144 | { 145 | double cx = Bounds.Width / 2; 146 | double cy = Bounds.Height / 2; 147 | 148 | double dx = point.X - cx; 149 | double dy = point.Y - cy; 150 | 151 | double dist = Math.Sqrt(dx * dx + dy * dy); 152 | 153 | return Math.Min(dist, wheel.ActualOuterRadius) / wheel.ActualOuterRadius; 154 | //return (float)((Math.Min(dist, wheel.ActualOuterRadius) - wheel.ActualInnerRadius) / (wheel.ActualOuterRadius - wheel.ActualInnerRadius)); 155 | } 156 | 157 | 158 | 159 | 160 | //Pointer Events 161 | private void Wheel_PointerPressed(object sender, PointerPressedEventArgs e) 162 | { 163 | // e.Pointer.Capture(this); 164 | UpdateSelectorFromPoint(e.GetPosition(this)); 165 | } 166 | 167 | private void _selector_PointerReleased(object sender, PointerReleasedEventArgs e) 168 | { 169 | e.Pointer.Capture(null); 170 | isDragging = false; 171 | } 172 | 173 | private void _selector_PointerMoved(object sender, PointerEventArgs e) 174 | { 175 | if (isDragging) 176 | { 177 | // Calculate Theta and Rad from the mouse position 178 | UpdateSelectorFromPoint(e.GetPosition(this)); 179 | } 180 | } 181 | 182 | public void _selector_PointerPressed(object sender, PointerPressedEventArgs e) 183 | { 184 | isDragging = true; 185 | UpdateSelectorFromPoint(e.GetPosition(this)); 186 | } 187 | 188 | 189 | 190 | //Thumb Selector 191 | private void UpdateSelector() 192 | { 193 | if (!double.IsNaN(Theta) && !double.IsNaN(this.Rad)) 194 | { 195 | double cx = Bounds.Width / 2.0; 196 | double cy = Bounds.Height / 2.0; 197 | 198 | double radius = (wheel.ActualOuterRadius - wheel.ActualInnerRadius) * this.Rad + wheel.ActualInnerRadius; 199 | 200 | // Snap to middle of wheel when inside InnerRadius 201 | if (radius < wheel.ActualInnerRadius + float.Epsilon) 202 | radius = 0.0; 203 | 204 | double angle = Theta + 180.0f; 205 | 206 | double x = radius * Math.Sin(angle * Math.PI / 180.0); 207 | double y = radius * Math.Cos(angle * Math.PI / 180.0); 208 | 209 | double mx = cx + x - _selector.Bounds.Width / 2; 210 | double my = cy + y - _selector.Bounds.Height / 2; 211 | 212 | hsv.hue = (float)Theta; 213 | hsv.sat = (float)Rad; 214 | hsv.value = 1.0f; 215 | 216 | _selector.Margin = new Thickness(mx, my, 0, 0); 217 | _selector.Fill = new SolidColorBrush(SelectedColor); 218 | } 219 | } 220 | 221 | 222 | private void UpdateSelectorFromPoint(Point point) 223 | { 224 | Theta = CalculateTheta(point); 225 | Rad = CalculateR(point); 226 | SelectedColor = wheel.ColorMapping(Rad, Theta, 1.0); 227 | 228 | UpdateSelector(); 229 | } 230 | 231 | 232 | 233 | //Animation 234 | private void AnimateTo(Point point) 235 | { 236 | Point from = new Point(this.Theta, this.Rad); 237 | Point to = new Point(CalculateTheta(point), CalculateR(point)); 238 | 239 | double _x = 0; 240 | 241 | // The shortest path actually crosses the 360-0 discontinuity 242 | if (from.X - to.X > 180.0) 243 | _x += 360.0; 244 | if (from.X - to.X < -180.0) 245 | _x -= 360.0; 246 | 247 | Bounds = new Rect(new Point(_x, to.Y), this.Bounds.Size); 248 | } 249 | 250 | 251 | 252 | 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /src/ColorPicker/Converters/BooleanToNumericConverter.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Data.Converters; 3 | using System; 4 | using System.Globalization; 5 | 6 | namespace ColorPicker.Converters 7 | { 8 | public class BooleanToNumericConverter : IValueConverter 9 | { 10 | public double TrueValue { get; set; } 11 | public double FalseValue { get; set; } 12 | 13 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture) 14 | { 15 | bool cond = (bool)value; 16 | return cond ? TrueValue : FalseValue; 17 | } 18 | 19 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) 20 | { 21 | // TODO - Probably not a good idea to compare doubles 22 | double val = (double)value; 23 | 24 | if (val == TrueValue) 25 | return true; 26 | if (val == FalseValue) 27 | return false; 28 | 29 | return AvaloniaProperty.UnsetValue; 30 | 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/ColorPicker/Converters/EnumToBooleanConverter.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Data.Converters; 3 | using System; 4 | using System.Globalization; 5 | 6 | namespace ColorPicker.Converters 7 | { 8 | /// 9 | /// See http://stackoverflow.com/questions/397556/how-to-bind-radiobuttons-to-an-enum 10 | /// 11 | public class EnumToBooleanConverter : IValueConverter 12 | { 13 | public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) 14 | { 15 | return value.Equals(parameter); 16 | } 17 | 18 | public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) 19 | { 20 | return value.Equals(true) ? parameter : AvaloniaProperty.UnsetValue; 21 | ; 22 | } 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/ColorPicker/Converters/LogarithmicConverter.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Data.Converters; 2 | using System; 3 | using System.Globalization; 4 | 5 | namespace ColorPicker.Converters 6 | { 7 | /// 8 | /// Turns a linear-scaled slider (0.0 to 1.0) into a logarithmic value, 9 | /// defined by Minimum and Maximum. 10 | /// The logarithm is in base 10, so for best results Minimum & Maximum should be a power of 10. 11 | /// 12 | public class LogarithmicConverter : IValueConverter 13 | { 14 | public double Maximum 15 | { 16 | get { return linMax; } 17 | set { linMax = value; logMax = Math.Log10(value); } 18 | } 19 | public double Minimum 20 | { 21 | get { return linMin; } 22 | set { linMin = value; logMin = Math.Log10(value); } 23 | } 24 | 25 | // Cached values 26 | private double linMin; 27 | private double linMax; 28 | private double logMin; 29 | private double logMax; 30 | 31 | public object Convert(object _value, Type targetType, object parameter, CultureInfo culture) 32 | { 33 | double scale = (logMax - logMin); 34 | double value = (double)_value; 35 | 36 | return (Math.Log10(value) - logMin) / scale; 37 | } 38 | 39 | public object ConvertBack(object _value, Type targetType, object parameter, CultureInfo culture) 40 | { 41 | double scale = (logMax - logMin); 42 | double value = (double)_value; 43 | 44 | return Math.Pow(10.0, logMin + scale * value); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/ColorPicker/Converters/RGBColorToBrushConverter.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Data.Converters; 3 | using Avalonia.Media; 4 | using ColorPicker.Structures; 5 | using System; 6 | using System.Globalization; 7 | 8 | namespace ColorPicker.Converters 9 | { 10 | public class RGBColorToBrushConverter : IValueConverter 11 | { 12 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture) 13 | { 14 | if(value is RGBColor color) 15 | { 16 | return new SolidColorBrush(color); 17 | } 18 | else 19 | { 20 | return AvaloniaProperty.UnsetValue; 21 | } 22 | } 23 | 24 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) 25 | { 26 | if (value is SolidColorBrush brush) 27 | { 28 | return new RGBColor(brush.Color.R, brush.Color.G, brush.Color.B); 29 | } 30 | else 31 | { 32 | return AvaloniaProperty.UnsetValue; 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/ColorPicker/Converters/RGBColorToHexConverter.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Data.Converters; 3 | using ColorPicker.Structures; 4 | using System; 5 | using System.Globalization; 6 | 7 | namespace ColorPicker.Converters 8 | { 9 | public class RGBColorToHexConverter : IValueConverter 10 | { 11 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture) 12 | { 13 | if(value is RGBColor color) 14 | { 15 | return color.ToHexRGB(); 16 | } 17 | else 18 | { 19 | return AvaloniaProperty.UnsetValue; 20 | } 21 | } 22 | 23 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) 24 | { 25 | return AvaloniaProperty.UnsetValue; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/ColorPicker/Converters/StringFormatConverter.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Data; 3 | using Avalonia.Data.Converters; 4 | using System; 5 | using System.Globalization; 6 | 7 | namespace ColorPicker.Converters 8 | { 9 | public class StringFormatConverter : IValueConverter 10 | { 11 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture) 12 | { 13 | return String.Format((string)parameter, value); 14 | } 15 | 16 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) 17 | { 18 | // Do nothing 19 | return AvaloniaProperty.UnsetValue; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/ColorPicker/Structures/CIE1931.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ColorPicker.Structures 4 | { 5 | public class CIE1931 6 | { 7 | /// 8 | /// Apply CIE intensity perception to the given lumininace value 9 | /// 10 | /// The luminance, between 0.0 and 1.0 11 | /// Percieved intensity, between 0.0 and 1.0 12 | public static float LumToBrightness(float L) 13 | { 14 | // See: http://www.poynton.com/notes/colour_and_gamma/ColorFAQ.html#RTFToC2 15 | // (Yn is assumed to be 1.0) 16 | return (L <= 0.08f) ? (L / 9.033f) : (float)Math.Pow((L + 0.16f) / 1.16f, 3); 17 | } 18 | 19 | /// 20 | /// Apply CIE correction to an RGB colour. 21 | /// This is useful for driving LEDs, as their output is linear, but our perception to light is not. 22 | /// 23 | /// The color to correct 24 | /// CIE corrected color 25 | public static RGBColor CorrectRGB(RGBColor input) 26 | { 27 | return new RGBColor( 28 | LumToBrightness(input.r), 29 | LumToBrightness(input.g), 30 | LumToBrightness(input.b) 31 | ); 32 | } 33 | 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/ColorPicker/Structures/CIEXYZ.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using MathNet.Numerics.LinearAlgebra; 3 | using MathNet.Numerics.LinearAlgebra.Double; 4 | 5 | 6 | /* Provides a way to specify device-independant colors, in a linear colorspace. 7 | * 8 | * The default conversion from XYZ to RGB uses the primary colors as specified in the BT.709/sRGB standard: 9 | * http://www.itu.int/dms_pubrec/itu-r/rec/bt/R-REC-BT.709-5-200204-I!!PDF-E.pdf 10 | * Note that the RGB returned is NOT sRGB. It is linear! 11 | * 12 | * Useful calculator here: 13 | * http://www.brucelindbloom.com/index.html?ColorCalcHelp.html 14 | */ 15 | 16 | namespace ColorPicker.Structures 17 | { 18 | /// 19 | /// Defines the RGB primaries to use when converting XYZ to RGB colorspace. 20 | /// This is because XYZ is device-independant. 21 | /// 22 | public class CIERGBDefinition 23 | { 24 | // Primaries as defined in BT709 standard: 25 | public static readonly CIERGBDefinition sRGB = new CIERGBDefinition( 26 | new CIEXYYColor(0.64, 0.33), // red 27 | new CIEXYYColor(0.30, 0.60), // green 28 | new CIEXYYColor(0.15, 0.06), // blue 29 | new CIEXYYColor(0.3127, 0.3290) // reference white (D65) 30 | ); 31 | 32 | // Primaries as defined by the CIE1931 standard: 33 | public static readonly CIERGBDefinition CIERGB = new CIERGBDefinition( 34 | new CIEXYYColor(0.735, 0.265), 35 | new CIEXYYColor(0.274, 0.717), 36 | new CIEXYYColor(0.167, 0.009), 37 | new CIEXYYColor(1 / 3.0, 1 / 3.0) 38 | ); 39 | 40 | public CIEXYZColor Red { get; private set; } 41 | public CIEXYZColor Green { get; private set; } 42 | public CIEXYZColor Blue { get; private set; } 43 | public CIEXYZColor White { get; private set; } 44 | 45 | public Matrix rgb2xyz { get; private set; } 46 | public Matrix xyz2rgb { get; private set; } 47 | 48 | 49 | public CIERGBDefinition(CIEXYZColor red, CIEXYZColor green, CIEXYZColor blue, CIEXYZColor white) 50 | { 51 | this.Red = red; 52 | this.Green = green; 53 | this.Blue = blue; 54 | this.White = white; 55 | 56 | // Calculate the RGB transform model 57 | var m = DenseMatrix.OfArray(new double[,] { 58 | {Red.X, Green.X, Blue.X}, 59 | {Red.Y, Green.Y, Blue.Y}, //NB: Y should be 1.0 60 | {Red.Z, Green.Z, Blue.Z} 61 | }); 62 | var mi = m.Inverse(); 63 | 64 | var refwhite = (Vector)White; 65 | var srgb = mi * refwhite; 66 | 67 | this.rgb2xyz = DenseMatrix.OfArray(new double[,] { 68 | {srgb[0]*m[0,0], srgb[1]*m[0,1], srgb[2]*m[0,2]}, 69 | {srgb[0]*m[1,0], srgb[1]*m[1,1], srgb[2]*m[1,2]}, 70 | {srgb[0]*m[2,0], srgb[1]*m[2,1], srgb[2]*m[2,2]}, 71 | }).Transpose(); 72 | this.xyz2rgb = rgb2xyz.Inverse(); 73 | } 74 | } 75 | 76 | /// 77 | /// Defines a color using XYZ tristimulus colorspace. Y is equivalent to luminance, all values must be positive. 78 | /// All values are linear, but do not represent perceptual linearity. 79 | /// XYZ and xyY can be implicitly converted between each other. 80 | /// 81 | public struct CIEXYZColor 82 | { 83 | public double X, Y, Z; 84 | 85 | public CIEXYZColor(double X, double Y, double Z) 86 | { 87 | this.X = X; 88 | this.Y = Y; 89 | this.Z = Z; 90 | } 91 | 92 | public RGBColor ToRGB(CIERGBDefinition primaries, bool limitGamut = true) 93 | { 94 | // NOTE: Assumes linear RGB, not sRGB. 95 | var mat = primaries.xyz2rgb; 96 | var rgb = mat * this; 97 | 98 | if (limitGamut && (rgb.Maximum() > 1.0 || rgb.Minimum() < 0.0)) 99 | { 100 | // Outside the gamut 101 | return new RGBColor(float.NaN, float.NaN, float.NaN); 102 | } 103 | else 104 | { 105 | return new RGBColor((float)rgb[0], (float)rgb[1], (float)rgb[2]); 106 | } 107 | } 108 | 109 | public static CIEXYZColor FromRGB(RGBColor rgb, CIERGBDefinition primaries) 110 | { 111 | var mat = primaries.rgb2xyz; 112 | var rgbvec = DenseVector.OfArray(new double[] { rgb.r, rgb.g, rgb.b }); 113 | var xyz = mat * rgbvec; 114 | return new CIEXYZColor(xyz[0], xyz[1], xyz[2]); 115 | } 116 | 117 | public static implicit operator RGBColor(CIEXYZColor xyz) 118 | { 119 | return xyz.ToRGB(CIERGBDefinition.sRGB); 120 | } 121 | 122 | public static implicit operator CIEXYZColor(RGBColor rgb) 123 | { 124 | return CIEXYZColor.FromRGB(rgb, CIERGBDefinition.sRGB); 125 | } 126 | 127 | public static implicit operator Vector(CIEXYZColor xyz) 128 | { 129 | return DenseVector.OfArray(new double[] { xyz.X, xyz.Y, xyz.Z }); 130 | } 131 | 132 | 133 | public override string ToString() 134 | { 135 | return String.Format("xyz({0:0.00},{1:0.00},{2:0.00})", X, Y, Z); 136 | } 137 | } 138 | 139 | /// 140 | /// Defines a color using xyY colorspace. Y is luminance, xy is chrominance. 141 | /// All values are linear, but do not represent perceptual linearity. 142 | /// XYZ and xyY can be implicitly converted between each other. 143 | /// 144 | public struct CIEXYYColor 145 | { 146 | public double x, y, Y; 147 | 148 | public CIEXYYColor(double x, double y, double Y = 1.0) 149 | { 150 | this.x = x; 151 | this.y = y; 152 | this.Y = Y; 153 | } 154 | 155 | public static implicit operator CIEXYZColor(CIEXYYColor xyy) 156 | { 157 | if (xyy.y == 0.0f) 158 | { 159 | return new CIEXYZColor(0, 0, 0); 160 | } 161 | else 162 | { 163 | double X, Z; 164 | X = (xyy.Y / xyy.y) * xyy.x; 165 | Z = (xyy.Y / xyy.y) * (1 - xyy.x - xyy.y); 166 | return new CIEXYZColor(X, xyy.Y, Z); 167 | } 168 | } 169 | 170 | public static implicit operator CIEXYYColor(CIEXYZColor xyz) 171 | { 172 | double x, y, s; 173 | s = (xyz.X + xyz.Y + xyz.Z); 174 | x = xyz.X / s; 175 | y = xyz.Y / s; 176 | return new CIEXYYColor(x, y, xyz.Y); 177 | } 178 | 179 | public static implicit operator RGBColor(CIEXYYColor xyy) 180 | { 181 | return (RGBColor)(CIEXYZColor)xyy; 182 | } 183 | 184 | 185 | public override string ToString() 186 | { 187 | return String.Format("xyz({0:0.00},{1:0.00},{2:0.00})", x, y, Y); 188 | } 189 | 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/ColorPicker/Structures/ColorTemperature.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace ColorPicker.Structures 6 | { 7 | public struct ColorTemperature 8 | { 9 | public float k; // Temperature in kelvins 10 | 11 | #region Constants 12 | 13 | public static ColorTemperature Hot { get { return new ColorTemperature(1000); } } 14 | public static ColorTemperature Warm { get { return new ColorTemperature(2200); } } 15 | public static ColorTemperature Neutral { get { return new ColorTemperature(4500); } } 16 | public static ColorTemperature Cool { get { return new ColorTemperature(9000); } } 17 | public static ColorTemperature Cold { get { return new ColorTemperature(11000); } } 18 | 19 | #endregion 20 | 21 | public ColorTemperature(float temperature) 22 | { 23 | this.k = temperature; 24 | } 25 | 26 | public RGBColor ToRGB() 27 | { 28 | //DEPRECATED: RGB is not device-independant so is not good for specifying color temperature. 29 | // Use the XYZ color space instead. 30 | 31 | float r, g, b; 32 | 33 | // Red 34 | if (k < 6600) 35 | { 36 | r = 255; 37 | } 38 | else 39 | { 40 | r = k - 6000.0f; 41 | r = 329.698727446f * (float)Math.Pow(r / 100.0, -0.1332047592); 42 | } 43 | 44 | // Green 45 | if (k < 6600) 46 | { 47 | g = k; 48 | g = 99.4708025861f * (float)Math.Log(g / 100.0) - 161.1195681661f; 49 | } 50 | else 51 | { 52 | g = k - 6000; 53 | g = 288.1221695283f * (float)Math.Pow(g / 100.0, -0.0755148492); 54 | } 55 | 56 | // Blue 57 | if (k > 6600) 58 | { 59 | b = 255; 60 | } 61 | else 62 | { 63 | //if (K < 1900) 64 | b = k - 1000; 65 | b = 138.5177312231f * (float)Math.Log(b / 100.0) - 305.0447927307f; 66 | } 67 | 68 | r /= 255.0f; 69 | g /= 255.0f; 70 | b /= 255.0f; 71 | 72 | if (r > 1.0f) r = 1.0f; 73 | if (g > 1.0f) g = 1.0f; 74 | if (b > 1.0f) b = 1.0f; 75 | if (r < 0.0f) r = 0.0f; 76 | if (g < 0.0f) g = 0.0f; 77 | if (b < 0.0f) b = 0.0f; 78 | 79 | return new RGBColor(r, g, b); 80 | } 81 | 82 | 83 | public CIEXYZColor ToXYZ() 84 | { 85 | // See http://en.wikipedia.org/wiki/Planckian_locus#Approximation 86 | 87 | double xc = 0, yc = 0; 88 | double T = this.k; 89 | double M = 10e+3 / T; 90 | 91 | if (T < 1667.0 || T > 25000.0) 92 | { 93 | // Undefined 94 | return new CIEXYZColor(double.NaN, double.NaN, double.NaN); 95 | } 96 | 97 | //TODO: This doesn't work properly, and produces out-of-gamut colors, 98 | // even though the blue temperatures should be completely within the gamut. 99 | // Need to create an xyY plot for checking the values... 100 | 101 | double arg1 = 1e9 / (T * T * T), arg2 = 1e6 / (T * T), arg3 = 1e3 / T; 102 | if (T >= 1667 && T <= 4000) 103 | { 104 | xc = -0.2661239 * arg1 - 0.2343580 * arg2 + 0.8776956 * arg3 + 0.179910; 105 | } 106 | else if (T > 4000 && T <= 25000) 107 | { 108 | xc = -3.0258469 * arg1 + 2.1070379 * arg2 + 0.2226347 * arg3 + 0.240390; 109 | } 110 | double xc3 = xc * xc * xc, xc2 = xc * xc; 111 | if (T >= 1667 && T <= 2222) 112 | { 113 | yc = -1.1063814 * xc3 - 1.34811020 * xc2 + 2.18555832 * xc - 0.20219683; 114 | } 115 | else if (T > 2222 && T <= 4000) 116 | { 117 | yc = -0.9549476 * xc3 - 1.37418593 * xc2 + 2.09137015 * xc - 0.16748867; 118 | } 119 | else if (T > 4000 && T <= 25000) 120 | { 121 | yc = +3.0817580 * xc3 - 5.87338670 * xc2 + 3.75112997 * xc - 0.37001483; 122 | } 123 | 124 | return new CIEXYYColor(xc, yc, 0.2); 125 | } 126 | 127 | /*public static implicit operator RGBColor(ColorTemperature ct) 128 | { 129 | return ct.ToRGB(); 130 | }*/ 131 | 132 | public static implicit operator CIEXYZColor(ColorTemperature ct) 133 | { 134 | return ct.ToXYZ(); 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/ColorPicker/Structures/HSV.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ColorPicker.Structures 4 | { 5 | public struct HSVColor 6 | { 7 | /// 8 | /// Hue (0.0 to 360.0) 9 | /// 10 | public float hue; 11 | 12 | /// 13 | /// Saturation (0.0 to 1.0) 14 | /// 15 | public float sat; 16 | 17 | /// 18 | /// Value (0.0 to 1.0) 19 | /// 20 | public float value; 21 | 22 | public HSVColor(float hue, float saturation, float value) 23 | { 24 | this.hue = hue; 25 | this.sat = saturation; 26 | this.value = value; 27 | } 28 | 29 | public void Clamp() 30 | { 31 | if (hue > 360.0f) hue = 360.0f; 32 | if (sat > 1.0f) sat = 1.0f; 33 | if (value > 1.0f) value = 1.0f; 34 | if (hue < 0.0f) hue = 0.0f; 35 | if (sat < 0.0f) sat = 0.0f; 36 | if (value < 0.0f) value = 0.0f; 37 | } 38 | 39 | /// 40 | /// Convert this HSV color to RGB colorspace 41 | /// 42 | public RGBColor ToRGB() 43 | { 44 | float hue = this.hue; 45 | float sat = this.sat; 46 | float value = this.value; 47 | float r = 0; 48 | float g = 0; 49 | float b = 0; 50 | 51 | int hi = (int)Math.Floor(hue / 60) % 6; 52 | float f = hue / 60 - (float)Math.Floor(hue / 60); 53 | 54 | value = value * 255; 55 | int v = (int)value; 56 | int p = (int)(value * (1.0 - sat)); 57 | int q = (int)(value * (1.0 - f * sat)); 58 | int t = (int)(value * (1.0 - (1.0 - f) * sat)); 59 | 60 | switch (hi) 61 | { 62 | case 0: r = v; g = t; b = p; break; 63 | case 1: r = q; g = v; b = p; break; 64 | case 2: r = p; g = v; b = t; break; 65 | case 3: r = p; g = q; b = v; break; 66 | case 4: r = t; g = p; b = v; break; 67 | case 5: r = v; g = p; b = q; break; 68 | } 69 | r /= 255.0f; 70 | g /= 255.0f; 71 | b /= 255.0f; 72 | 73 | return new RGBColor(r, g, b); 74 | } 75 | 76 | public static HSVColor FromRGB(RGBColor rgb) 77 | { 78 | HSVColor hsv = new HSVColor(); 79 | 80 | float max = (float)Math.Max(rgb.r, Math.Max(rgb.g, rgb.b)); 81 | float min = (float)Math.Min(rgb.r, Math.Min(rgb.g, rgb.b)); 82 | 83 | hsv.value = max; 84 | 85 | float delta = max - min; 86 | 87 | if (max > float.Epsilon) 88 | { 89 | hsv.sat = delta / max; 90 | } 91 | else 92 | { 93 | // r = g = b = 0 94 | hsv.sat = 0; 95 | hsv.hue = float.NaN; // Undefined 96 | return hsv; 97 | } 98 | 99 | if (rgb.r == max) 100 | hsv.hue = (rgb.g - rgb.b) / delta; // Between yellow and magenta 101 | else if (rgb.g == max) 102 | hsv.hue = 2 + (rgb.b - rgb.r) / delta; // Between cyan and yellow 103 | else 104 | hsv.hue = 4 + (rgb.r - rgb.g) / delta; // Between magenta and cyan 105 | 106 | hsv.hue *= 60.0f; // degrees 107 | if (hsv.hue < 0) 108 | hsv.hue += 360; 109 | 110 | return hsv; 111 | } 112 | 113 | public static implicit operator RGBColor(HSVColor hsv) 114 | { 115 | return hsv.ToRGB(); 116 | } 117 | 118 | public static implicit operator HSVColor(RGBColor rgb) 119 | { 120 | return HSVColor.FromRGB(rgb); 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/ColorPicker/Structures/RGB.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ColorPicker.Structures 4 | { 5 | public struct RGBStruct 6 | { 7 | public byte r, g, b, a; 8 | 9 | public RGBStruct(byte r, byte g, byte b, byte a = 255) 10 | { 11 | this.r = r; this.g = g; this.b = b; this.a = a; 12 | } 13 | 14 | public int ToARGB32() 15 | { 16 | return (a << 24) | (r << 16) | (g << 8) | (b << 0); 17 | } 18 | 19 | public int ToRGB32() 20 | { 21 | return (r << 16) | (g << 8) | (b << 0); 22 | } 23 | } 24 | 25 | public struct RGBColor 26 | { 27 | public float r, g, b; 28 | 29 | public byte Rb { get { return FloatToByte(r); } set { r = ByteToFloat(value); } } 30 | public byte Gb { get { return FloatToByte(g); } set { g = ByteToFloat(value); } } 31 | public byte Bb { get { return FloatToByte(b); } set { b = ByteToFloat(value); } } 32 | //public float R { get { return r; } set { r = value; } } 33 | 34 | public RGBColor(float red, float green, float blue) 35 | { 36 | r = red; 37 | g = green; 38 | b = blue; 39 | } 40 | 41 | #region Private Utilities 42 | private byte FloatToByte(double value) 43 | { 44 | if (value < 0.0) return 0; 45 | if (value > 1.0) return 255; 46 | return (byte)(value * 255.0); 47 | } 48 | 49 | private float ByteToFloat(byte value) 50 | { 51 | return (float)value / 255.0f; 52 | } 53 | 54 | #endregion 55 | 56 | #region Operations 57 | 58 | /// 59 | /// Clamps the RGB values between 0.0 and 1.0 60 | /// 61 | public void Clamp() 62 | { 63 | if (r > 1.0f) r = 1.0f; 64 | if (g > 1.0f) g = 1.0f; 65 | if (b > 1.0f) b = 1.0f; 66 | if (r < 0.0f) r = 0.0f; 67 | if (g < 0.0f) g = 0.0f; 68 | if (b < 0.0f) b = 0.0f; 69 | } 70 | 71 | public bool OutOfGamut 72 | { 73 | get 74 | { 75 | return (r < 0.0f || g < 0.0f || b < 0.0f || r > 1.0f || g > 1.0f || b > 1.0f); 76 | } 77 | } 78 | 79 | #endregion 80 | 81 | #region Implicit Conversion 82 | 83 | public float GetSaturation() 84 | { 85 | float max = (float)Math.Max(r, Math.Max(g, b)); 86 | float min = (float)Math.Min(r, Math.Min(g, b)); 87 | return (max == 0.0f) ? 0.0f : 1.0f - (1.0f * min / max); 88 | } 89 | 90 | public float GetValue() 91 | { 92 | return Math.Max(r, Math.Max(g, b)); 93 | } 94 | 95 | /// 96 | /// Returns the grayscale intensity of the RGB colour 97 | /// (Colours with the same saturation appear as different intensities) 98 | /// 99 | public float GetIntensity() 100 | { 101 | return 0.2126f * r + 0.7152f * g + 0.0722f * b; 102 | } 103 | 104 | public static implicit operator RGBColor(Avalonia.Media.Color c) 105 | { 106 | return new RGBColor(c.R / 255.0f, c.G / 255.0f, c.B / 255.0f); 107 | } 108 | 109 | public static implicit operator RGBColor(System.Drawing.Color c) 110 | { 111 | return new RGBColor(c.R / 255.0f, c.G / 255.0f, c.B / 255.0f); 112 | } 113 | 114 | public static implicit operator Avalonia.Media.Color(RGBColor c) 115 | { 116 | return new Avalonia.Media.Color(255, (byte)(c.r * 255), (byte)(c.g * 255), (byte)(c.b * 255)); 117 | } 118 | 119 | public static implicit operator System.Drawing.Color(RGBColor c) 120 | { 121 | return System.Drawing.Color.FromArgb(255, (byte)(c.r * 255), (byte)(c.g * 255), (byte)(c.b * 255)); 122 | } 123 | 124 | #endregion 125 | 126 | #region operators 127 | 128 | // Colour1 + Colour2 129 | public static RGBColor operator +(RGBColor c1, RGBColor c2) 130 | { 131 | return new RGBColor( 132 | c1.r + c2.r, 133 | c1.g + c2.g, 134 | c1.b + c2.b 135 | ); 136 | } 137 | 138 | // Colour1 * Color 2 139 | public static RGBColor operator *(RGBColor c1, RGBColor c2) 140 | { 141 | return new RGBColor( 142 | c1.r * c2.r, 143 | c1.g * c2.g, 144 | c1.b * c2.b 145 | ); 146 | } 147 | 148 | // Colour1 + float 149 | public static RGBColor operator +(RGBColor c, float f) 150 | { 151 | return new RGBColor( 152 | c.r + f, 153 | c.g + f, 154 | c.b + f 155 | ); 156 | } 157 | 158 | // Colour1 * float 159 | public static RGBColor operator *(RGBColor c, float f) 160 | { 161 | return new RGBColor( 162 | c.r * f, 163 | c.g * f, 164 | c.b * f 165 | ); 166 | } 167 | 168 | public static RGBColor Interpolate(RGBColor c1, RGBColor c2, float value) 169 | { 170 | return (c1 * (1.0f - value)) + (c2 * value); 171 | } 172 | 173 | #endregion 174 | 175 | #region ToString Methods 176 | 177 | public string ToHexRGB() 178 | { 179 | return String.Format("#{0:x2}{1:x2}{2:x2}", Rb, Gb, Bb); 180 | } 181 | 182 | public override string ToString() 183 | { 184 | return String.Format("rgb({0:0.00},{1:0.00},{2:0.00})", r, g, b); 185 | } 186 | 187 | #endregion 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/ColorPicker/Utilities/CircularMath.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using System; 3 | 4 | namespace ColorPicker.Utilities 5 | { 6 | /// 7 | /// Circular math utility functions. 8 | /// Inputs represent angles around a circle, such that: 9 | /// 450 == 90 == -270 mod 360 10 | /// All angles are assumed to be degrees (not radians) 11 | /// 12 | internal static class CircularMath 13 | { 14 | public const double N = 360.0; 15 | 16 | /// 17 | /// Calculates the angle in degrees of the line between pt and the origin, 18 | /// with respect to the vertical axis. (0 degrees = up) 19 | /// 20 | public static double AngleFromPoint(Point pt, Point origin) 21 | { 22 | double dx = pt.X - origin.X; 23 | double dy = pt.Y - origin.Y; 24 | 25 | return Mod(180.0 - (Math.Atan2(dx, dy) / Math.PI * 180.0)); 26 | } 27 | 28 | /// 29 | /// Calculates the point given by the specified radius and angle from the origin. 30 | /// 31 | public static Point PointFromAngle(double angle, double radius, Point origin) 32 | { 33 | angle = 180.0 - angle; 34 | 35 | double x = radius * Math.Sin(angle * Math.PI / 180.0); 36 | double y = radius * Math.Cos(angle * Math.PI / 180.0); 37 | 38 | return new Point(origin.X + x, origin.Y + y); 39 | } 40 | 41 | /// 42 | /// Mathematical modulus 43 | /// a mod n (n defaults to 360.0) 44 | /// 45 | public static double Mod(double a, double n = N) 46 | { 47 | return ((a % n) + n) % n; 48 | } 49 | 50 | /// 51 | /// Returns true if a >= x >= b, mod 360. 52 | /// 53 | public static bool Between(double a, double b, double x) 54 | { 55 | a = Mod(a); 56 | b = Mod(b); 57 | x = Mod(x); 58 | 59 | if (a > b) // a >= 0.0 >= b, mod 360 60 | return ((x >= a && x < N) || (x >= 0.0 && x <= b)); 61 | else 62 | return (x >= a && x <= b); 63 | } 64 | 65 | /// 66 | /// Map the angle x from start and stop to 0.0 and 1.0. 67 | /// Values outside start and stop are defined as NaN, since the 68 | /// number space is circular. 69 | /// 70 | public static double NormMap(double start, double stop, double x, bool clockwise = true) 71 | { 72 | start = Mod(start); 73 | stop = Mod(stop); 74 | x = Mod(x); 75 | 76 | double value = Mod(x - start) / Mod(stop - start); 77 | 78 | if ((x != start) && (x != stop) && Between(stop, start, x)) 79 | value = double.NaN; 80 | return value; 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/ColorPicker/Wheels/ColorWheelBase.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls; 3 | using Avalonia.Controls.Shapes; 4 | using Avalonia.Media; 5 | using Avalonia.Media.Imaging; 6 | using Avalonia.Platform; 7 | 8 | using ColorPicker.Structures; 9 | 10 | using System; 11 | using System.Diagnostics; 12 | using Point = Avalonia.Point; 13 | using HA = Avalonia.Layout.HorizontalAlignment; 14 | using VA = Avalonia.Layout.VerticalAlignment; 15 | 16 | 17 | namespace ColorPicker.Wheels 18 | { 19 | public abstract class ColorWheelBase : Panel 20 | { 21 | public static StyledProperty InnerRadiusProperty = AvaloniaProperty.Register(nameof(InnerRadius)); 22 | 23 | public static void OnPropertyChanged(AvaloniaObject obj, AvaloniaPropertyChangedEventArgs args) 24 | { 25 | HSVWheel ctl = (obj as HSVWheel); 26 | ctl.InvalidateVisual(); 27 | } 28 | 29 | 30 | public double InnerRadius 31 | { 32 | get { return (double)base.GetValue(InnerRadiusProperty); } 33 | set { base.SetValue(InnerRadiusProperty, value); } 34 | } 35 | 36 | 37 | public double ActualOuterRadius { get; private set; } 38 | public double ActualInnerRadius { get { return ActualOuterRadius * InnerRadius; } } 39 | 40 | 41 | public override void Render(DrawingContext dc) 42 | { 43 | base.Render(dc); 44 | DrawHsvDial(dc); 45 | } 46 | 47 | /// 48 | /// The function used to draw the pixels in the color wheel. 49 | /// 50 | protected RGBStruct ColorFunction(double r, double theta) 51 | { 52 | RGBColor rgb = ColorMapping(r, theta, 1.0); 53 | return new RGBStruct(rgb.Rb, rgb.Gb, rgb.Bb, 255); 54 | } 55 | 56 | /// 57 | /// The color mapping between Rad/Theta and RGB 58 | /// 59 | /// Radius/Saturation, between 0 and 1 60 | /// Angle/Hue, between 0 and 360 61 | /// The RGB colour 62 | public virtual RGBColor ColorMapping(double radius, double theta, double value) 63 | { 64 | return new RGBColor(1.0f, 1.0f, 1.0f); 65 | } 66 | 67 | public virtual Point InverseColorMapping(RGBColor rgb) 68 | { 69 | return new Point(0, 0); 70 | } 71 | 72 | Ellipse border; 73 | 74 | protected void DrawHsvDial(DrawingContext drawingContext) 75 | { 76 | float cx = (float)(Bounds.Width) / 2.0f; 77 | float cy = (float)(Bounds.Height) / 2.0f; 78 | 79 | float outer_radius = (float)Math.Min(cx, cy); 80 | ActualOuterRadius = outer_radius; 81 | 82 | int bmp_width = (int)Bounds.Width; 83 | int bmp_height = (int)Bounds.Height; 84 | 85 | if (bmp_width <= 0 || bmp_height <= 0) 86 | return; 87 | 88 | 89 | var stopwatch = new Stopwatch(); 90 | stopwatch.Start(); 91 | 92 | //This probably wants to move somewhere else.... 93 | if (border == null) 94 | { 95 | border = new Ellipse(); 96 | border.Fill = new SolidColorBrush(Colors.Transparent); 97 | border.Stroke = new SolidColorBrush(Colors.Black); 98 | border.StrokeThickness = 3; 99 | border.IsHitTestVisible = false; 100 | border.Opacity = 50; 101 | this.Children.Add(border); 102 | border.HorizontalAlignment = HA.Center; 103 | border.VerticalAlignment = VA.Center; 104 | } 105 | 106 | border.Width = Math.Min(bmp_width, bmp_height) + (border.StrokeThickness /2); 107 | border.Height = Math.Min(bmp_width, bmp_height) + (border.StrokeThickness /2); 108 | 109 | var writeableBitmap = new WriteableBitmap(new PixelSize(bmp_width, bmp_height), new Vector(96, 96), PixelFormat.Bgra8888); 110 | 111 | using (var lockedFrameBuffer = writeableBitmap.Lock()) 112 | { 113 | unsafe 114 | { 115 | IntPtr bufferPtr = new IntPtr(lockedFrameBuffer.Address.ToInt64()); 116 | 117 | for (int y = 0; y < bmp_height; y++) 118 | { 119 | for (int x = 0; x < bmp_width; x++) 120 | { 121 | int color_data = 0; 122 | 123 | // Convert xy to normalized polar co-ordinates 124 | double dx = x - cx; 125 | double dy = y - cy; 126 | double pr = Math.Sqrt(dx * dx + dy * dy); 127 | 128 | // Only draw stuff within the circle 129 | if (pr <= outer_radius) 130 | { 131 | // Compute the color for the given pixel using polar co-ordinates 132 | double pa = Math.Atan2(dx, dy); 133 | RGBStruct c = ColorFunction(pr / outer_radius, ((pa + Math.PI) * 180.0 / Math.PI)); 134 | 135 | // Anti-aliasing 136 | // This works by adjusting the alpha to the alias error between the outer radius (which is integer) 137 | // and the computed radius, pr (which is float). 138 | double aadelta = pr - (outer_radius - 1.0); 139 | if (aadelta >= 0.0) 140 | c.a = (byte)(255 - aadelta * 255); 141 | 142 | color_data = c.ToARGB32(); 143 | } 144 | 145 | *((int*)bufferPtr) = color_data; 146 | bufferPtr += 4; 147 | } 148 | } 149 | } 150 | } 151 | 152 | drawingContext.DrawImage(writeableBitmap, Bounds); 153 | 154 | stopwatch.Stop(); 155 | Debug.WriteLine($"YO! This puppy took {stopwatch.ElapsedMilliseconds} MS to complete"); 156 | } 157 | 158 | } 159 | } 160 | 161 | -------------------------------------------------------------------------------- /src/ColorPicker/Wheels/HSVWheel.cs: -------------------------------------------------------------------------------- 1 | using ColorPicker.Structures; 2 | using System; 3 | using System.Windows; 4 | using Point = Avalonia.Point; 5 | 6 | namespace ColorPicker.Wheels 7 | { 8 | public class HSVWheel : ColorWheelBase 9 | { 10 | private const double whiteFactor = 2.2; // Provide more accuracy around the white-point 11 | 12 | public override RGBColor ColorMapping(double radius, double theta, double value) 13 | { 14 | HSVColor hsv = new HSVColor((float)theta, (float)Math.Pow(radius, whiteFactor), (float)value); 15 | RGBColor rgb = hsv.ToRGB(); 16 | return rgb; 17 | } 18 | 19 | public override Point InverseColorMapping(RGBColor rgb) 20 | { 21 | double theta, rad; 22 | HSVColor hsv = (HSVColor)rgb; 23 | theta = hsv.hue; 24 | rad = Math.Pow(hsv.sat, 1.0 / whiteFactor); 25 | 26 | return new Point(theta, rad); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Sample/App.axaml: -------------------------------------------------------------------------------- 1 |  5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/Sample/App.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls.ApplicationLifetimes; 3 | using Avalonia.Markup.Xaml; 4 | using Sample.ViewModels; 5 | using Sample.Views; 6 | 7 | 8 | namespace Sample 9 | { 10 | public class App : Application 11 | { 12 | public override void Initialize() 13 | { 14 | AvaloniaXamlLoader.Load(this); 15 | } 16 | 17 | public override void OnFrameworkInitializationCompleted() 18 | { 19 | if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) 20 | { 21 | desktop.MainWindow = new MainWindow 22 | { 23 | DataContext = new MainWindowViewModel(), 24 | }; 25 | } 26 | 27 | 28 | base.OnFrameworkInitializationCompleted(); 29 | } 30 | 31 | 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Sample/Assets/avalonia-logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MikeCodesDotNET/ColorPicker/044aa2e4394b8eb9950494fe8adbf3a76f5ecdc0/src/Sample/Assets/avalonia-logo.ico -------------------------------------------------------------------------------- /src/Sample/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Avalonia; 3 | using Avalonia.Controls.ApplicationLifetimes; 4 | using Avalonia.ReactiveUI; 5 | 6 | namespace Sample 7 | { 8 | class Program 9 | { 10 | // Initialization code. Don't use any Avalonia, third-party APIs or any 11 | // SynchronizationContext-reliant code before AppMain is called: things aren't initialized 12 | // yet and stuff might break. 13 | public static void Main(string[] args) => BuildAvaloniaApp() 14 | .StartWithClassicDesktopLifetime(args); 15 | 16 | // Avalonia configuration, don't remove; also used by visual designer. 17 | public static AppBuilder BuildAvaloniaApp() 18 | => AppBuilder.Configure() 19 | .UsePlatformDetect() 20 | .LogToDebug() 21 | .UseReactiveUI(); 22 | 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Sample/Sample.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | WinExe 4 | netcoreapp3.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/Sample/ViewLocator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Avalonia.Controls; 3 | using Avalonia.Controls.Templates; 4 | using Sample.ViewModels; 5 | 6 | namespace Sample 7 | { 8 | public class ViewLocator : IDataTemplate 9 | { 10 | public bool SupportsRecycling => false; 11 | 12 | public IControl Build(object data) 13 | { 14 | var name = data.GetType().FullName.Replace("ViewModel", "View"); 15 | var type = Type.GetType(name); 16 | 17 | if (type != null) 18 | { 19 | return (Control)Activator.CreateInstance(type); 20 | } 21 | else 22 | { 23 | return new TextBlock { Text = "Not Found: " + name }; 24 | } 25 | } 26 | 27 | public bool Match(object data) 28 | { 29 | return data is ViewModelBase; 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /src/Sample/ViewModels/MainWindowViewModel.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using ColorPicker.Structures; 3 | using ReactiveUI; 4 | 5 | namespace Sample.ViewModels 6 | { 7 | public class MainWindowViewModel : ViewModelBase 8 | { 9 | RGBColor _selectedColor; 10 | 11 | public RGBColor SelectedColor 12 | { 13 | get => _selectedColor; 14 | set { 15 | this.RaiseAndSetIfChanged(ref _selectedColor, value); 16 | } 17 | } 18 | 19 | 20 | public MainWindowViewModel() 21 | { 22 | SelectedColor = new RGBColor(); 23 | 24 | 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Sample/ViewModels/ViewModelBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using ReactiveUI; 5 | 6 | namespace Sample.ViewModels 7 | { 8 | public class ViewModelBase : ReactiveObject 9 | { 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Sample/Views/MainWindow.axaml: -------------------------------------------------------------------------------- 1 |  13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /src/Sample/Views/MainWindow.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls; 3 | using Avalonia.Markup.Xaml; 4 | using System; 5 | using System.Collections.Generic; 6 | 7 | namespace Sample.Views 8 | { 9 | public class MainWindow : Window 10 | { 11 | public MainWindow() 12 | { 13 | InitializeComponent(); 14 | 15 | #if DEBUG 16 | this.AttachDevTools(); 17 | #endif 18 | 19 | } 20 | 21 | 22 | 23 | private void InitializeComponent() 24 | { 25 | AvaloniaXamlLoader.Load(this); 26 | 27 | } 28 | } 29 | 30 | 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/Sample/nuget.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | --------------------------------------------------------------------------------