├── .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 |
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 |
--------------------------------------------------------------------------------