mirror of
https://github.com/aluxnimm/outlookcaldavsynchronizer.git
synced 2025-10-05 16:02:49 +02:00
Use Unicolour library for colour comparison and remove ColorMine package.
This commit is contained in:
@@ -77,9 +77,6 @@
|
||||
<Service Include="{82A7F48D-3B50-4B1E-B82E-3ADA8210C358}" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ColorMineStandard">
|
||||
<Version>1.0.0</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="DDay.iCal">
|
||||
<Version>1.0.2.575</Version>
|
||||
</PackageReference>
|
||||
|
@@ -1,106 +1,113 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio 15
|
||||
VisualStudioVersion = 15.0.26730.12
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CalDavSynchronizer", "CalDavSynchronizer\CalDavSynchronizer.csproj", "{64937844-752B-49A4-9B3F-3526601E93E1}"
|
||||
ProjectSection(ProjectDependencies) = postProject
|
||||
{A53D3CEB-F7AF-41AB-AA04-4D3CF684BBAF} = {A53D3CEB-F7AF-41AB-AA04-4D3CF684BBAF}
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CalDavSynchronizer.UnitTests", "CalDavSynchronizer.UnitTest\CalDavSynchronizer.UnitTests.csproj", "{60B1D6FA-B087-42D8-B330-7C147B62FC7A}"
|
||||
EndProject
|
||||
Project("{54435603-DBB4-11D2-8724-00A0C9A8B90C}") = "CalDavSynchronizer.Setup", "CalDavSynchronizer.Setup\CalDavSynchronizer.Setup.vdproj", "{C05F2805-55A8-4ABB-9892-515781049EB6}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GenSync", "GenSync\GenSync.csproj", "{76C932E7-ECA5-4010-B602-2104327EE5EE}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GenSync.UnitTests", "GenSync.UnitTests\GenSync.UnitTests.csproj", "{EDB87679-422D-4297-8F46-6F7B220CDF37}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CalDavDataAccessIntegrationTests", "CalDavDataAccessIntegrationTests\CalDavDataAccessIntegrationTests.csproj", "{BE747238-9388-43CE-AF64-536C64442EB9}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CalDavSynchronizer.OAuth.Google", "CalDavSynchronizer.OAuth.Google\CalDavSynchronizer.OAuth.Google.csproj", "{230914CF-6FF1-4B09-A831-C37C6B41D127}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CalDavSynchronizer.CustomInstaller", "CalDavSynchronizer.CustomInstaller\CalDavSynchronizer.CustomInstaller.csproj", "{0C682D77-1CBB-4336-8F6E-CDDA9B6C0C97}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CalDavSynchronizer.Conversions", "CalDavSynchronizer.Conversions\CalDavSynchronizer.Conversions.csproj", "{71021DE1-8DC8-4414-AF56-FE821DD92F47}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CalDavSynchronizer.Conversions.UnitTests", "CalDavSynchronizer.Conversions.UnitTests\CalDavSynchronizer.Conversions.UnitTests.csproj", "{85C64408-3019-4097-9508-89E46F2E9738}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Thought.vCards", "Thought.vCards\Thought.vCards.csproj", "{A53D3CEB-F7AF-41AB-AA04-4D3CF684BBAF}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Thought.vCards.UnitTests", "Thought.vCards.UnitTests\Thought.vCards.UnitTests.csproj", "{D7A4725B-CB7B-4F10-8DD1-DE2E4AECA498}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CalDavSynchronizer.IntegrationTests", "CalDavSynchronizer.IntegrationTests\CalDavSynchronizer.IntegrationTests.csproj", "{13B5D6BF-EB84-4570-A3E4-1F272C0F2BC0}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CalDavSynchronizer.OAuth.Swisscom", "CalDavSynchronizer.OAuth.Swisscom\CalDavSynchronizer.OAuth.Swisscom.csproj", "{424F803E-B641-4473-B9C3-B8DF40E0B3B3}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{64937844-752B-49A4-9B3F-3526601E93E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{64937844-752B-49A4-9B3F-3526601E93E1}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{64937844-752B-49A4-9B3F-3526601E93E1}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{64937844-752B-49A4-9B3F-3526601E93E1}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{60B1D6FA-B087-42D8-B330-7C147B62FC7A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{60B1D6FA-B087-42D8-B330-7C147B62FC7A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{60B1D6FA-B087-42D8-B330-7C147B62FC7A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{60B1D6FA-B087-42D8-B330-7C147B62FC7A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{C05F2805-55A8-4ABB-9892-515781049EB6}.Debug|Any CPU.ActiveCfg = Debug
|
||||
{C05F2805-55A8-4ABB-9892-515781049EB6}.Release|Any CPU.ActiveCfg = Release
|
||||
{76C932E7-ECA5-4010-B602-2104327EE5EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{76C932E7-ECA5-4010-B602-2104327EE5EE}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{76C932E7-ECA5-4010-B602-2104327EE5EE}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{76C932E7-ECA5-4010-B602-2104327EE5EE}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{EDB87679-422D-4297-8F46-6F7B220CDF37}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{EDB87679-422D-4297-8F46-6F7B220CDF37}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{EDB87679-422D-4297-8F46-6F7B220CDF37}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{EDB87679-422D-4297-8F46-6F7B220CDF37}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{BE747238-9388-43CE-AF64-536C64442EB9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{BE747238-9388-43CE-AF64-536C64442EB9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{BE747238-9388-43CE-AF64-536C64442EB9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{BE747238-9388-43CE-AF64-536C64442EB9}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{230914CF-6FF1-4B09-A831-C37C6B41D127}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{230914CF-6FF1-4B09-A831-C37C6B41D127}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{230914CF-6FF1-4B09-A831-C37C6B41D127}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{230914CF-6FF1-4B09-A831-C37C6B41D127}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{0C682D77-1CBB-4336-8F6E-CDDA9B6C0C97}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{0C682D77-1CBB-4336-8F6E-CDDA9B6C0C97}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{0C682D77-1CBB-4336-8F6E-CDDA9B6C0C97}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{0C682D77-1CBB-4336-8F6E-CDDA9B6C0C97}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{71021DE1-8DC8-4414-AF56-FE821DD92F47}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{71021DE1-8DC8-4414-AF56-FE821DD92F47}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{71021DE1-8DC8-4414-AF56-FE821DD92F47}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{71021DE1-8DC8-4414-AF56-FE821DD92F47}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{85C64408-3019-4097-9508-89E46F2E9738}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{85C64408-3019-4097-9508-89E46F2E9738}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{85C64408-3019-4097-9508-89E46F2E9738}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{85C64408-3019-4097-9508-89E46F2E9738}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{A53D3CEB-F7AF-41AB-AA04-4D3CF684BBAF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A53D3CEB-F7AF-41AB-AA04-4D3CF684BBAF}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A53D3CEB-F7AF-41AB-AA04-4D3CF684BBAF}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A53D3CEB-F7AF-41AB-AA04-4D3CF684BBAF}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{D7A4725B-CB7B-4F10-8DD1-DE2E4AECA498}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{D7A4725B-CB7B-4F10-8DD1-DE2E4AECA498}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{D7A4725B-CB7B-4F10-8DD1-DE2E4AECA498}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{D7A4725B-CB7B-4F10-8DD1-DE2E4AECA498}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{13B5D6BF-EB84-4570-A3E4-1F272C0F2BC0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{13B5D6BF-EB84-4570-A3E4-1F272C0F2BC0}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{13B5D6BF-EB84-4570-A3E4-1F272C0F2BC0}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{13B5D6BF-EB84-4570-A3E4-1F272C0F2BC0}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{424F803E-B641-4473-B9C3-B8DF40E0B3B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{424F803E-B641-4473-B9C3-B8DF40E0B3B3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{424F803E-B641-4473-B9C3-B8DF40E0B3B3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{424F803E-B641-4473-B9C3-B8DF40E0B3B3}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
RESX_AutoCreateNewLanguageFiles = True
|
||||
RESX_SortFileContentOnSave = True
|
||||
SolutionGuid = {C6CA0E38-AF86-45D6-BDCA-263A18B40738}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 16
|
||||
VisualStudioVersion = 16.0.33130.400
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CalDavSynchronizer", "CalDavSynchronizer\CalDavSynchronizer.csproj", "{64937844-752B-49A4-9B3F-3526601E93E1}"
|
||||
ProjectSection(ProjectDependencies) = postProject
|
||||
{A8F93F45-42EA-4D4C-8DD1-B03410CC7C55} = {A8F93F45-42EA-4D4C-8DD1-B03410CC7C55}
|
||||
{A53D3CEB-F7AF-41AB-AA04-4D3CF684BBAF} = {A53D3CEB-F7AF-41AB-AA04-4D3CF684BBAF}
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CalDavSynchronizer.UnitTests", "CalDavSynchronizer.UnitTest\CalDavSynchronizer.UnitTests.csproj", "{60B1D6FA-B087-42D8-B330-7C147B62FC7A}"
|
||||
EndProject
|
||||
Project("{54435603-DBB4-11D2-8724-00A0C9A8B90C}") = "CalDavSynchronizer.Setup", "CalDavSynchronizer.Setup\CalDavSynchronizer.Setup.vdproj", "{C05F2805-55A8-4ABB-9892-515781049EB6}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GenSync", "GenSync\GenSync.csproj", "{76C932E7-ECA5-4010-B602-2104327EE5EE}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GenSync.UnitTests", "GenSync.UnitTests\GenSync.UnitTests.csproj", "{EDB87679-422D-4297-8F46-6F7B220CDF37}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CalDavDataAccessIntegrationTests", "CalDavDataAccessIntegrationTests\CalDavDataAccessIntegrationTests.csproj", "{BE747238-9388-43CE-AF64-536C64442EB9}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CalDavSynchronizer.OAuth.Google", "CalDavSynchronizer.OAuth.Google\CalDavSynchronizer.OAuth.Google.csproj", "{230914CF-6FF1-4B09-A831-C37C6B41D127}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CalDavSynchronizer.CustomInstaller", "CalDavSynchronizer.CustomInstaller\CalDavSynchronizer.CustomInstaller.csproj", "{0C682D77-1CBB-4336-8F6E-CDDA9B6C0C97}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CalDavSynchronizer.Conversions", "CalDavSynchronizer.Conversions\CalDavSynchronizer.Conversions.csproj", "{71021DE1-8DC8-4414-AF56-FE821DD92F47}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CalDavSynchronizer.Conversions.UnitTests", "CalDavSynchronizer.Conversions.UnitTests\CalDavSynchronizer.Conversions.UnitTests.csproj", "{85C64408-3019-4097-9508-89E46F2E9738}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Thought.vCards", "Thought.vCards\Thought.vCards.csproj", "{A53D3CEB-F7AF-41AB-AA04-4D3CF684BBAF}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Thought.vCards.UnitTests", "Thought.vCards.UnitTests\Thought.vCards.UnitTests.csproj", "{D7A4725B-CB7B-4F10-8DD1-DE2E4AECA498}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CalDavSynchronizer.IntegrationTests", "CalDavSynchronizer.IntegrationTests\CalDavSynchronizer.IntegrationTests.csproj", "{13B5D6BF-EB84-4570-A3E4-1F272C0F2BC0}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CalDavSynchronizer.OAuth.Swisscom", "CalDavSynchronizer.OAuth.Swisscom\CalDavSynchronizer.OAuth.Swisscom.csproj", "{424F803E-B641-4473-B9C3-B8DF40E0B3B3}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Wacton.Unicolour", "Unicolour\Wacton.Unicolour.csproj", "{A8F93F45-42EA-4D4C-8DD1-B03410CC7C55}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{64937844-752B-49A4-9B3F-3526601E93E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{64937844-752B-49A4-9B3F-3526601E93E1}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{64937844-752B-49A4-9B3F-3526601E93E1}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{64937844-752B-49A4-9B3F-3526601E93E1}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{60B1D6FA-B087-42D8-B330-7C147B62FC7A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{60B1D6FA-B087-42D8-B330-7C147B62FC7A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{60B1D6FA-B087-42D8-B330-7C147B62FC7A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{60B1D6FA-B087-42D8-B330-7C147B62FC7A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{C05F2805-55A8-4ABB-9892-515781049EB6}.Debug|Any CPU.ActiveCfg = Debug
|
||||
{C05F2805-55A8-4ABB-9892-515781049EB6}.Release|Any CPU.ActiveCfg = Release
|
||||
{76C932E7-ECA5-4010-B602-2104327EE5EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{76C932E7-ECA5-4010-B602-2104327EE5EE}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{76C932E7-ECA5-4010-B602-2104327EE5EE}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{76C932E7-ECA5-4010-B602-2104327EE5EE}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{EDB87679-422D-4297-8F46-6F7B220CDF37}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{EDB87679-422D-4297-8F46-6F7B220CDF37}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{EDB87679-422D-4297-8F46-6F7B220CDF37}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{EDB87679-422D-4297-8F46-6F7B220CDF37}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{BE747238-9388-43CE-AF64-536C64442EB9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{BE747238-9388-43CE-AF64-536C64442EB9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{BE747238-9388-43CE-AF64-536C64442EB9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{BE747238-9388-43CE-AF64-536C64442EB9}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{230914CF-6FF1-4B09-A831-C37C6B41D127}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{230914CF-6FF1-4B09-A831-C37C6B41D127}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{230914CF-6FF1-4B09-A831-C37C6B41D127}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{230914CF-6FF1-4B09-A831-C37C6B41D127}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{0C682D77-1CBB-4336-8F6E-CDDA9B6C0C97}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{0C682D77-1CBB-4336-8F6E-CDDA9B6C0C97}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{0C682D77-1CBB-4336-8F6E-CDDA9B6C0C97}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{0C682D77-1CBB-4336-8F6E-CDDA9B6C0C97}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{71021DE1-8DC8-4414-AF56-FE821DD92F47}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{71021DE1-8DC8-4414-AF56-FE821DD92F47}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{71021DE1-8DC8-4414-AF56-FE821DD92F47}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{71021DE1-8DC8-4414-AF56-FE821DD92F47}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{85C64408-3019-4097-9508-89E46F2E9738}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{85C64408-3019-4097-9508-89E46F2E9738}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{85C64408-3019-4097-9508-89E46F2E9738}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{85C64408-3019-4097-9508-89E46F2E9738}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{A53D3CEB-F7AF-41AB-AA04-4D3CF684BBAF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A53D3CEB-F7AF-41AB-AA04-4D3CF684BBAF}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A53D3CEB-F7AF-41AB-AA04-4D3CF684BBAF}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A53D3CEB-F7AF-41AB-AA04-4D3CF684BBAF}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{D7A4725B-CB7B-4F10-8DD1-DE2E4AECA498}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{D7A4725B-CB7B-4F10-8DD1-DE2E4AECA498}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{D7A4725B-CB7B-4F10-8DD1-DE2E4AECA498}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{D7A4725B-CB7B-4F10-8DD1-DE2E4AECA498}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{13B5D6BF-EB84-4570-A3E4-1F272C0F2BC0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{13B5D6BF-EB84-4570-A3E4-1F272C0F2BC0}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{13B5D6BF-EB84-4570-A3E4-1F272C0F2BC0}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{13B5D6BF-EB84-4570-A3E4-1F272C0F2BC0}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{424F803E-B641-4473-B9C3-B8DF40E0B3B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{424F803E-B641-4473-B9C3-B8DF40E0B3B3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{424F803E-B641-4473-B9C3-B8DF40E0B3B3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{424F803E-B641-4473-B9C3-B8DF40E0B3B3}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{A8F93F45-42EA-4D4C-8DD1-B03410CC7C55}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A8F93F45-42EA-4D4C-8DD1-B03410CC7C55}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A8F93F45-42EA-4D4C-8DD1-B03410CC7C55}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A8F93F45-42EA-4D4C-8DD1-B03410CC7C55}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {C6CA0E38-AF86-45D6-BDCA-263A18B40738}
|
||||
RESX_SortFileContentOnSave = True
|
||||
RESX_AutoCreateNewLanguageFiles = True
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
@@ -910,14 +910,15 @@
|
||||
<Project>{a53d3ceb-f7af-41ab-aa04-4d3cf684bbaf}</Project>
|
||||
<Name>Thought.vCards</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\Unicolour\Wacton.Unicolour.csproj">
|
||||
<Project>{a8f93f45-42ea-4d4c-8dd1-b03410cc7c55}</Project>
|
||||
<Name>Wacton.Unicolour</Name>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BouncyCastle">
|
||||
<Version>1.8.9</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="ColorMineStandard">
|
||||
<Version>1.0.0</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="DDay.iCal">
|
||||
<Version>1.0.2.575</Version>
|
||||
</PackageReference>
|
||||
|
@@ -26,8 +26,7 @@ using System.Data;
|
||||
using System.Reflection;
|
||||
using CalDavSynchronizer.DataAccess;
|
||||
using log4net;
|
||||
using ColorMine;
|
||||
using ColorMine.ColorSpaces;
|
||||
using Wacton.Unicolour;
|
||||
using Microsoft.Office.Interop.Outlook;
|
||||
|
||||
namespace CalDavSynchronizer.Utilities
|
||||
@@ -67,8 +66,7 @@ namespace CalDavSynchronizer.Utilities
|
||||
{OlCategoryColor.olCategoryColorDarkMaroon, ArgbColor.FromRgb(0x93446b)}
|
||||
};
|
||||
|
||||
|
||||
public static OlCategoryColor FindMatchingCategoryColor(Color color)
|
||||
public static OlCategoryColor FindMatchingCategoryColor (Color color)
|
||||
{
|
||||
var minDistance = double.MaxValue;
|
||||
var matchingCategoryColor = OlCategoryColor.olCategoryColorNone;
|
||||
@@ -77,10 +75,10 @@ namespace CalDavSynchronizer.Utilities
|
||||
{
|
||||
var catColor = Color.FromArgb(cat.Value.ArgbValue);
|
||||
|
||||
var a = new Rgb {R = color.R, G = color.G, B = color.B};
|
||||
var b = new Rgb {R = catColor.R, G = catColor.G, B = catColor.B};
|
||||
|
||||
var curDistance = a.Compare(b, new ColorMine.ColorSpaces.Comparisons.CieDe2000Comparison());
|
||||
var unicolour_a = new Unicolour(ColourSpace.Rgb255, color.R, color.G, color.B);
|
||||
var unicolour_b = new Unicolour(ColourSpace.Rgb255, catColor.R, catColor.G, catColor.B);
|
||||
|
||||
var curDistance = unicolour_a.Difference(DeltaE.Ciede2000, unicolour_b);
|
||||
|
||||
if (curDistance < minDistance)
|
||||
{
|
||||
@@ -91,7 +89,7 @@ namespace CalDavSynchronizer.Utilities
|
||||
|
||||
return matchingCategoryColor;
|
||||
}
|
||||
|
||||
|
||||
public static OlCategoryColor FindMatchingCategoryColor(ArgbColor argbColor)
|
||||
{
|
||||
var color = Color.FromArgb(argbColor.ArgbValue);
|
||||
|
@@ -105,7 +105,7 @@ see [https://www.davx5.com](https://www.davx5.com), so we can really recommend i
|
||||
- [Apache log4net](https://logging.apache.org/log4net/)
|
||||
- [Thought.vCard](http://nugetmusthaves.com/Package/Thought.vCards)
|
||||
- [NodaTime](http://nodatime.org/)
|
||||
- [ColorMine](https://www.nuget.org/packages/ColorMine/)
|
||||
- [Unicolour](https://github.com/waacton/Unicolour)
|
||||
|
||||
### Install instructions ###
|
||||
|
||||
|
45
Unicolour/Adaptation.cs
Normal file
45
Unicolour/Adaptation.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
namespace Wacton.Unicolour
|
||||
{
|
||||
|
||||
internal static class Adaptation
|
||||
{
|
||||
private static readonly Matrix Bradford = new(new[,]
|
||||
{
|
||||
{ +0.8951, +0.2664, -0.1614 },
|
||||
{ -0.7502, +1.7135, +0.0367 },
|
||||
{ +0.0389, -0.0685, +1.0296 }
|
||||
});
|
||||
|
||||
internal static Matrix WhitePoint(Matrix matrix, WhitePoint sourceWhitePoint, WhitePoint destinationWhitePoint)
|
||||
{
|
||||
if (sourceWhitePoint == destinationWhitePoint)
|
||||
{
|
||||
return matrix;
|
||||
}
|
||||
|
||||
var adaptedBradford = AdaptedBradfordMatrix(sourceWhitePoint, destinationWhitePoint);
|
||||
return adaptedBradford.Multiply(matrix);
|
||||
}
|
||||
|
||||
// http://www.brucelindbloom.com/index.html?Eqn_ChromAdapt.html
|
||||
private static Matrix AdaptedBradfordMatrix(WhitePoint sourceWhitePoint, WhitePoint destinationWhitePoint)
|
||||
{
|
||||
var sourceWhite = sourceWhitePoint.AsXyzMatrix();
|
||||
var destinationWhite = destinationWhitePoint.AsXyzMatrix();
|
||||
|
||||
var sourceLms = Bradford.Multiply(sourceWhite).ToTriplet();
|
||||
var destinationLms = Bradford.Multiply(destinationWhite).ToTriplet();
|
||||
|
||||
var lmsRatios = new Matrix(new[,]
|
||||
{
|
||||
{ destinationLms.First / sourceLms.First, 0, 0 },
|
||||
{ 0, destinationLms.Second / sourceLms.Second, 0 },
|
||||
{ 0, 0, destinationLms.Third / sourceLms.Third }
|
||||
});
|
||||
|
||||
var inverseBradford = Bradford.Inverse();
|
||||
var adaptedBradfordMatrix = inverseBradford.Multiply(lmsRatios).Multiply(Bradford);
|
||||
return adaptedBradfordMatrix;
|
||||
}
|
||||
}
|
||||
}
|
15
Unicolour/Alpha.cs
Normal file
15
Unicolour/Alpha.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using System;
|
||||
|
||||
namespace Wacton.Unicolour
|
||||
{
|
||||
|
||||
public record Alpha(double A)
|
||||
{
|
||||
public double A { get; } = A;
|
||||
public double ConstrainedA => double.IsNaN(A) ? 0 : A.Clamp(0.0, 1.0);
|
||||
public int A255 => (int)Math.Round(ConstrainedA * 255);
|
||||
public string Hex => $"{A255:X2}";
|
||||
|
||||
public override string ToString() => $"{A:F2}";
|
||||
}
|
||||
}
|
68
Unicolour/BoundingLines.cs
Normal file
68
Unicolour/BoundingLines.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Wacton.Unicolour
|
||||
{
|
||||
|
||||
internal static class BoundingLines
|
||||
{
|
||||
internal static double CalculateMaxChroma(double lightness, double hue)
|
||||
{
|
||||
var hueRad = hue / 360 * Math.PI * 2;
|
||||
return GetBoundingLines(lightness).Select(x => DistanceFromOriginAngle(hueRad, x)).Min();
|
||||
}
|
||||
|
||||
internal static double CalculateMaxChroma(double lightness)
|
||||
{
|
||||
return GetBoundingLines(lightness).Select(DistanceFromOrigin).Min();
|
||||
}
|
||||
|
||||
// https://github.com/hsluv/hsluv-haxe/blob/master/src/hsluv/Hsluv.hx#L249
|
||||
private static IEnumerable<Line> GetBoundingLines(double l)
|
||||
{
|
||||
const double kappa = 903.2962962;
|
||||
const double epsilon = 0.0088564516;
|
||||
var matrixR = Matrix.FromTriplet(3.240969941904521, -1.537383177570093, -0.498610760293);
|
||||
var matrixG = Matrix.FromTriplet(-0.96924363628087, 1.87596750150772, 0.041555057407175);
|
||||
var matrixB = Matrix.FromTriplet(0.055630079696993, -0.20397695888897, 1.056971514242878);
|
||||
|
||||
var sub1 = Math.Pow(l + 16, 3) / 1560896;
|
||||
var sub2 = sub1 > epsilon ? sub1 : l / kappa;
|
||||
|
||||
IEnumerable<Line> CalculateLines(Matrix matrix)
|
||||
{
|
||||
var s1 = sub2 * (284517 * matrix[0, 0] - 94839 * matrix[2, 0]);
|
||||
var s2 = sub2 * (838422 * matrix[2, 0] + 769860 * matrix[1, 0] + 731718 * matrix[0, 0]);
|
||||
var s3 = sub2 * (632260 * matrix[2, 0] - 126452 * matrix[1, 0]);
|
||||
|
||||
var slope0 = s1 / s3;
|
||||
var intercept0 = s2 * l / s3;
|
||||
var slope1 = s1 / (s3 + 126452);
|
||||
var intercept1 = (s2 - 769860) * l / (s3 + 126452);
|
||||
return new[] { new Line(slope0, intercept0), new Line(slope1, intercept1) };
|
||||
}
|
||||
|
||||
var lines = new List<Line>();
|
||||
lines.AddRange(CalculateLines(matrixR));
|
||||
lines.AddRange(CalculateLines(matrixG));
|
||||
lines.AddRange(CalculateLines(matrixB));
|
||||
return lines;
|
||||
}
|
||||
|
||||
private static double DistanceFromOriginAngle(double theta, Line line)
|
||||
{
|
||||
var distance = line.Intercept / (Math.Sin(theta) - line.Slope * Math.Cos(theta));
|
||||
return distance < 0 ? double.PositiveInfinity : distance;
|
||||
}
|
||||
|
||||
private static double DistanceFromOrigin(Line line) =>
|
||||
Math.Abs(line.Intercept) / Math.Sqrt(Math.Pow(line.Slope, 2) + 1);
|
||||
|
||||
private record Line(double Slope, double Intercept)
|
||||
{
|
||||
public double Slope { get; } = Slope;
|
||||
public double Intercept { get; } = Intercept;
|
||||
}
|
||||
}
|
||||
}
|
127
Unicolour/Cam.cs
Normal file
127
Unicolour/Cam.cs
Normal file
@@ -0,0 +1,127 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Wacton.Unicolour
|
||||
{
|
||||
|
||||
using static Utils;
|
||||
|
||||
public static class Cam
|
||||
{
|
||||
public record Model(double J, double C, double H, double M, double S, double Q)
|
||||
{
|
||||
public double J { get; } = J;
|
||||
public double C { get; } = C;
|
||||
public double H { get; } = H;
|
||||
public string Hc { get; } = HueData.GetHueComposition(H); // technically not in CAM02 but 🤷
|
||||
public double M { get; } = M;
|
||||
public double S { get; } = S;
|
||||
public double Q { get; } = Q;
|
||||
|
||||
public double Lightness => J;
|
||||
public double Chroma => C;
|
||||
public double HueAngle => H;
|
||||
public string HueComposition => Hc;
|
||||
public double Colourfulness => M;
|
||||
public double Saturation => S;
|
||||
public double Brightness => Q;
|
||||
|
||||
internal Ucs ToUcs()
|
||||
{
|
||||
var j = 1.7 * J / (1 + 0.007 * J);
|
||||
var m = Math.Log(1 + 0.0228 * M) / 0.0228;
|
||||
(j, var a, var b) = FromLchTriplet(new(j, m, H));
|
||||
return new Ucs(j, a, b);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* NOTE: if ever want to support CAM02-LCD & CAM02-SCD (Large / Small Colour Difference)
|
||||
* see Table 2.2 in "CIECAM02 and Its Recent Developments" (https://doi.org/10.1007/978-1-4419-6190-7_2)
|
||||
* essentially a small tweak to how j / m / ΔE' are calculated
|
||||
* but would need to consider how it affects the structure of Unicolour objects
|
||||
*/
|
||||
public record Ucs(double J, double A, double B)
|
||||
{
|
||||
public double J { get; } = J;
|
||||
public double A { get; } = A;
|
||||
public double B { get; } = B;
|
||||
|
||||
internal Model ToModel(ViewingConditions view)
|
||||
{
|
||||
var j = J / (1.7 - 0.007 * J);
|
||||
(j, var m, var h) = ToLchTriplet(j, A, B);
|
||||
m = (Math.Exp(0.0228 * m) - 1) / 0.0228;
|
||||
|
||||
var q = 4 / view.C * Math.Pow(j / 100.0, 0.5) * (view.Aw + 4) * Math.Pow(view.Fl, 0.25);
|
||||
var c = m / Math.Pow(view.Fl, 0.25);
|
||||
var s = 100 * Math.Pow(m / q, 0.5);
|
||||
return new Model(j, c, h, m, s, q);
|
||||
}
|
||||
}
|
||||
|
||||
internal record ViewingConditions(
|
||||
double C, double Nc, double Dr, double Dg, double Db,
|
||||
double Fl, double N, double Z, double Nbb, double Ncb, double Aw)
|
||||
{
|
||||
public double C { get; } = C;
|
||||
public double Nc { get; } = Nc;
|
||||
public double Dr { get; } = Dr;
|
||||
public double Dg { get; } = Dg;
|
||||
public double Db { get; } = Db;
|
||||
public double Fl { get; } = Fl;
|
||||
public double N { get; } = N;
|
||||
public double Z { get; } = Z;
|
||||
public double Nbb { get; } = Nbb;
|
||||
public double Ncb { get; } = Ncb;
|
||||
public double Aw { get; } = Aw;
|
||||
}
|
||||
|
||||
internal static class HueData
|
||||
{
|
||||
private const double Angle1 = 20.14;
|
||||
private const double Angle2 = 90.00;
|
||||
private const double Angle3 = 164.25;
|
||||
private const double Angle4 = 237.53;
|
||||
private const double Angle5 = 380.14;
|
||||
|
||||
private static readonly string[] Names = { "R", "Y", "G", "B", "R" };
|
||||
private static readonly double[] Angles = { Angle1, Angle2, Angle3, Angle4, Angle5 };
|
||||
private static readonly double[] Es = { 0.8, 0.7, 1.0, 1.2, 0.8 };
|
||||
private static readonly double[] Quads = { 0.0, 100.0, 200.0, 300.0, 400.0 };
|
||||
|
||||
private static string Name(int i) => Get(Names, i);
|
||||
private static double Angle(int i) => Get(Angles, i);
|
||||
private static double E(int i) => Get(Es, i);
|
||||
private static double Quad(int i) => Get(Quads, i);
|
||||
private static T Get<T>(IReadOnlyList<T> array, int i) => array[i - 1];
|
||||
|
||||
private static double GetHPrime(double h) => h < Angle1 ? h + 360 : h;
|
||||
internal static double GetEccentricity(double h) => 0.25 * (Math.Cos(ToRadians(GetHPrime(h)) + 2) + 3.8);
|
||||
|
||||
internal static string GetHueComposition(double h)
|
||||
{
|
||||
if (double.IsNaN(h)) return "-";
|
||||
var hPrime = GetHPrime(h);
|
||||
int? index = hPrime switch
|
||||
{
|
||||
>= Angle1 and < Angle2 => 1,
|
||||
>= Angle2 and < Angle3 => 2,
|
||||
>= Angle3 and < Angle4 => 3,
|
||||
>= Angle4 and < Angle5 => 4,
|
||||
_ => null
|
||||
};
|
||||
|
||||
if (index == null) return "-";
|
||||
var i = index.Value;
|
||||
var hQuad = Quad(i) +
|
||||
100 * E(i + 1) * (hPrime - Angle(i)) /
|
||||
(E(i + 1) * (hPrime - Angle(i)) + E(i) * (Angle(i + 1) - hPrime));
|
||||
var pl = Quad(i + 1) - hQuad;
|
||||
var pr = hQuad - Quad(i);
|
||||
var hc = $"{pl:f0}{Name(i)}{pr:f0}{Name(i + 1)}";
|
||||
return hc;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
225
Unicolour/Cam02.cs
Normal file
225
Unicolour/Cam02.cs
Normal file
@@ -0,0 +1,225 @@
|
||||
using System;
|
||||
|
||||
namespace Wacton.Unicolour
|
||||
{
|
||||
|
||||
using static Cam;
|
||||
using static Utils;
|
||||
|
||||
public record Cam02 : ColourRepresentation
|
||||
{
|
||||
protected override int? HueIndex => null;
|
||||
public double J => First;
|
||||
public double A => Second;
|
||||
public double B => Third;
|
||||
public Ucs Ucs { get; }
|
||||
public Model Model { get; }
|
||||
|
||||
// J lightness bounds not clear (and is different between Model and UCS)
|
||||
// presumably also greyscale when A.Equals(0.0) && B.Equals(0.0)
|
||||
internal override bool IsGreyscale => Model.Chroma <= 0;
|
||||
|
||||
public Cam02(double j, double a, double b, CamConfiguration camConfig) : this(new Ucs(j, a, b), camConfig,
|
||||
ColourHeritage.None)
|
||||
{
|
||||
}
|
||||
|
||||
internal Cam02(Model model, CamConfiguration camConfig, ColourHeritage heritage) : this(model.ToUcs(),
|
||||
camConfig, heritage)
|
||||
{
|
||||
Model = model;
|
||||
}
|
||||
|
||||
internal Cam02(Ucs ucs, CamConfiguration camConfig, ColourHeritage heritage) : base(ucs.J, ucs.A, ucs.B,
|
||||
heritage)
|
||||
{
|
||||
// Model will only be non-null if the constructor that takes Model is called (currently not possible from external code)
|
||||
Ucs = ucs;
|
||||
Model ??= ucs.ToModel(ViewingConditions(camConfig));
|
||||
}
|
||||
|
||||
protected override string FirstString => $"{J:F2}";
|
||||
protected override string SecondString => $"{A:+0.00;-0.00;0.00}";
|
||||
protected override string ThirdString => $"{B:+0.00;-0.00;0.00}";
|
||||
public override string ToString() => base.ToString();
|
||||
|
||||
/*
|
||||
* CAM02 is a transform of XYZ
|
||||
* Forward: https://doi.org/10.1007/978-1-4419-6190-7_2 · https://doi.org/10.1002/col.20227 · https://doi.org/10.48550/arXiv.1802.06067
|
||||
* Reverse: https://doi.org/10.1007/978-1-4419-6190-7_2 · https://doi.org/10.1002/col.20227 · https://doi.org/10.48550/arXiv.1802.06067
|
||||
*/
|
||||
|
||||
private static readonly Matrix MCAT02 = new(new[,]
|
||||
{
|
||||
{ +0.7328, +0.4296, -0.1624 },
|
||||
{ -0.7036, +1.6975, +0.0061 },
|
||||
{ +0.0030, +0.0136, +0.9834 }
|
||||
});
|
||||
|
||||
private static readonly Matrix MHPE = new(new[,]
|
||||
{
|
||||
{ +0.38971, +0.68898, -0.07868 },
|
||||
{ -0.22981, +1.18340, +0.04641 },
|
||||
{ 0.00000, 0.00000, +1.00000 }
|
||||
});
|
||||
|
||||
private static readonly Matrix ForwardStep4 = new(new[,]
|
||||
{
|
||||
{ 2, 1, 1 / 20.0 },
|
||||
{ 1, -12 / 11.0, 1 / 11.0 },
|
||||
{ 1 / 9.0, 1 / 9.0, -2 / 9.0 },
|
||||
{ 1, 1, 21 / 20.0 }
|
||||
});
|
||||
|
||||
private static readonly Matrix ReverseStep4 = new Matrix(new double[,]
|
||||
{
|
||||
{ 460, 451, 288 },
|
||||
{ 460, -891, -261 },
|
||||
{ 460, -220, -6300 }
|
||||
}).Scale(1 / 1403.0);
|
||||
|
||||
private static ViewingConditions ViewingConditions(CamConfiguration camConfig)
|
||||
{
|
||||
var (xw, yw, zw) = (camConfig.WhitePoint.X, camConfig.WhitePoint.Y, camConfig.WhitePoint.Z);
|
||||
var la = camConfig.AdaptingLuminance;
|
||||
var yb = camConfig.BackgroundLuminance;
|
||||
var c = camConfig.C;
|
||||
var f = camConfig.F;
|
||||
var nc = camConfig.Nc;
|
||||
|
||||
// step 0
|
||||
var xyzWhitePointMatrix = Matrix.FromTriplet(xw, yw, zw);
|
||||
var (rw, gw, bw) = MCAT02.Multiply(xyzWhitePointMatrix).ToTriplet();
|
||||
|
||||
var d = (f * (1 - 1 / 3.6 * Math.Exp((-la - 42) / 92.0))).Clamp(0, 1);
|
||||
var (dr, dg, db) = (D(rw), D(gw), D(bw));
|
||||
double D(double input) => d * (yw / input) + 1 - d;
|
||||
|
||||
var k = 1 / (5 * la + 1);
|
||||
var fl = 0.2 * Math.Pow(k, 4) * (5 * la) + 0.1 * Math.Pow(1 - Math.Pow(k, 4), 2) * CubeRoot(5 * la);
|
||||
var n = yb / yw;
|
||||
var z = 1.48 + Math.Sqrt(n);
|
||||
var nbb = 0.725 * Math.Pow(1 / n, 0.2);
|
||||
var ncb = nbb;
|
||||
|
||||
// slightly different to CAM16
|
||||
var wcMatrix = Matrix.FromTriplet(dr * rw, dg * gw, db * bw);
|
||||
var (rwPrime, gwPrime, bwPrime) = MHPE.Multiply(MCAT02.Inverse()).Multiply(wcMatrix).ToTriplet();
|
||||
|
||||
var (raw, gaw, baw) = (Aw(rwPrime), Aw(gwPrime), Aw(bwPrime));
|
||||
|
||||
double Aw(double input)
|
||||
{
|
||||
var power = Math.Pow(fl * input / 100.0, 0.42);
|
||||
return 400 * (power / (power + 27.13)) + 0.1;
|
||||
}
|
||||
|
||||
var aw = (2 * raw + gaw + baw / 20.0 - 0.305) * nbb;
|
||||
return new ViewingConditions(c, nc, dr, dg, db, fl, n, z, nbb, ncb, aw);
|
||||
}
|
||||
|
||||
public static Cam02 FromXyz(Xyz xyz, CamConfiguration camConfig, XyzConfiguration xyzConfig)
|
||||
{
|
||||
var view = ViewingConditions(camConfig);
|
||||
|
||||
// step 1
|
||||
var xyzMatrix = Matrix.FromTriplet(xyz.X, xyz.Y, xyz.Z);
|
||||
xyzMatrix = Adaptation.WhitePoint(xyzMatrix, xyzConfig.WhitePoint, camConfig.WhitePoint)
|
||||
.Select(x => x * 100);
|
||||
var rgb = MCAT02.Multiply(xyzMatrix).ToTriplet();
|
||||
|
||||
// step 2
|
||||
var cMatrix = Matrix.FromTriplet(view.Dr * rgb.First, view.Dg * rgb.Second, view.Db * rgb.Third);
|
||||
|
||||
// step 3, slightly different to CAM16
|
||||
var (rPrime, gPrime, bPrime) = MHPE.Multiply(MCAT02.Inverse()).Multiply(cMatrix).ToTriplet();
|
||||
|
||||
// step 4
|
||||
var (ra, ga, ba) = (A(rPrime), A(gPrime), A(bPrime));
|
||||
|
||||
double A(double input)
|
||||
{
|
||||
if (double.IsNaN(input)) return double.NaN;
|
||||
var power = Math.Pow(view.Fl * Math.Abs(input) / 100.0, 0.42);
|
||||
return 400 * Math.Sign(input) * (power / (power + 27.13));
|
||||
}
|
||||
|
||||
// step 5
|
||||
var aMatrix = Matrix.FromTriplet(ra, ga, ba);
|
||||
var components = ForwardStep4.Multiply(aMatrix);
|
||||
var (p2, a, b, u) = (components[0, 0], components[1, 0], components[2, 0], components[3, 0]);
|
||||
var h = ToDegrees(Math.Atan2(b, a)).Modulo(360);
|
||||
|
||||
// step 6
|
||||
var et = HueData.GetEccentricity(h);
|
||||
|
||||
// step 7
|
||||
var achromatic = p2 * view.Nbb;
|
||||
|
||||
// step 8
|
||||
var j = 100 * Math.Pow(achromatic / view.Aw, view.C * view.Z);
|
||||
|
||||
// step 9
|
||||
var q = 4 / view.C * Math.Pow(j / 100.0, 0.5) * (view.Aw + 4) * Math.Pow(view.Fl, 0.25);
|
||||
|
||||
// step 10
|
||||
var t = 50000 / 13.0 * view.Nc * view.Ncb * et * Math.Sqrt(Math.Pow(a, 2) + Math.Pow(b, 2)) / (u + 0.305);
|
||||
var alpha = Math.Pow(t, 0.9) * Math.Pow(1.64 - Math.Pow(0.29, view.N), 0.73);
|
||||
var c = alpha * Math.Sqrt(j / 100.0);
|
||||
var m = c * Math.Pow(view.Fl, 0.25);
|
||||
var s = 50 * Math.Sqrt(alpha * view.C / (view.Aw + 4));
|
||||
return new Cam02(new Model(j, c, h, m, s, q), camConfig, ColourHeritage.From(xyz));
|
||||
}
|
||||
|
||||
public static Xyz ToXyz(Cam02 cam, CamConfiguration camConfig, XyzConfiguration xyzConfig)
|
||||
{
|
||||
var view = ViewingConditions(camConfig);
|
||||
|
||||
var j = cam.Model.J;
|
||||
var c = cam.Model.C;
|
||||
var h = cam.Model.H;
|
||||
|
||||
// step 1
|
||||
var alpha = j == 0 ? 0 : c / Math.Sqrt(j / 100.0);
|
||||
var t = Math.Pow(alpha / Math.Pow(1.64 - Math.Pow(0.29, view.N), 0.73), 1 / 0.9);
|
||||
|
||||
// step 2
|
||||
var et = 0.25 * (Math.Cos(ToRadians(h) + 2) + 3.8);
|
||||
var achromatic = view.Aw * Math.Pow(j / 100.0, 1 / (view.C * view.Z));
|
||||
var p1 = et * (50000 / 13.0) * view.Nc * view.Ncb;
|
||||
var p2 = achromatic / view.Nbb;
|
||||
|
||||
// step 3
|
||||
var gamma = 23 * (p2 + 0.305) * t /
|
||||
(23 * p1 + 11 * t * Math.Cos(ToRadians(h)) + 108 * t * Math.Sin(ToRadians(h)));
|
||||
var a = gamma * Math.Cos(ToRadians(h));
|
||||
var b = gamma * Math.Sin(ToRadians(h));
|
||||
|
||||
// step 4
|
||||
var components = Matrix.FromTriplet(p2, a, b);
|
||||
var (ra, ga, ba) = ReverseStep4.Multiply(components).ToTriplet();
|
||||
|
||||
// step 5, slightly different to CAM16
|
||||
var primeMatrix = Matrix.FromTriplet(C(ra), C(ga), C(ba));
|
||||
|
||||
double C(double input)
|
||||
{
|
||||
if (double.IsNaN(input)) return double.NaN;
|
||||
return Math.Sign(input) * (100 / view.Fl) *
|
||||
Math.Pow(27.13 * Math.Abs(input) / (400 - Math.Abs(input)), 1 / 0.42);
|
||||
}
|
||||
|
||||
var (rc, gc, bc) = MCAT02.Multiply(MHPE.Inverse()).Multiply(primeMatrix).ToTriplet();
|
||||
|
||||
// step 6
|
||||
var rgbMatrix = Matrix.FromTriplet(rc / view.Dr, gc / view.Dg, bc / view.Db);
|
||||
|
||||
// step 7
|
||||
var xyzMatrix = MCAT02.Inverse().Multiply(rgbMatrix);
|
||||
xyzMatrix = Adaptation.WhitePoint(xyzMatrix, camConfig.WhitePoint, xyzConfig.WhitePoint)
|
||||
.Select(x => x / 100.0);
|
||||
return new Xyz(xyzMatrix.ToTriplet(), ColourHeritage.From(cam));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
209
Unicolour/Cam16.cs
Normal file
209
Unicolour/Cam16.cs
Normal file
@@ -0,0 +1,209 @@
|
||||
using System;
|
||||
|
||||
namespace Wacton.Unicolour
|
||||
{
|
||||
|
||||
using static Cam;
|
||||
using static Utils;
|
||||
|
||||
public record Cam16 : ColourRepresentation
|
||||
{
|
||||
protected override int? HueIndex => null;
|
||||
public double J => First;
|
||||
public double A => Second;
|
||||
public double B => Third;
|
||||
public Ucs Ucs { get; }
|
||||
public Model Model { get; }
|
||||
|
||||
// J lightness bounds not clear (and is different between Model and UCS)
|
||||
// presumably also greyscale when A.Equals(0.0) && B.Equals(0.0)
|
||||
internal override bool IsGreyscale => Model.Chroma <= 0;
|
||||
|
||||
public Cam16(double j, double a, double b, CamConfiguration camConfig) : this(new Ucs(j, a, b), camConfig,
|
||||
ColourHeritage.None)
|
||||
{
|
||||
}
|
||||
|
||||
internal Cam16(Model model, CamConfiguration camConfig, ColourHeritage heritage) : this(model.ToUcs(),
|
||||
camConfig, heritage)
|
||||
{
|
||||
Model = model;
|
||||
}
|
||||
|
||||
internal Cam16(Ucs ucs, CamConfiguration camConfig, ColourHeritage heritage) : base(ucs.J, ucs.A, ucs.B,
|
||||
heritage)
|
||||
{
|
||||
// Model will only be non-null if the constructor that takes Cam16Model is called (currently not possible from external code)
|
||||
Ucs = ucs;
|
||||
Model ??= ucs.ToModel(ViewingConditions(camConfig));
|
||||
}
|
||||
|
||||
protected override string FirstString => $"{J:F2}";
|
||||
protected override string SecondString => $"{A:+0.00;-0.00;0.00}";
|
||||
protected override string ThirdString => $"{B:+0.00;-0.00;0.00}";
|
||||
public override string ToString() => base.ToString();
|
||||
|
||||
/*
|
||||
* CAM16 is a transform of XYZ
|
||||
* Forward: https://doi.org/10.1002/col.22131 · https://doi.org/10.48550/arXiv.1802.06067
|
||||
* Reverse: https://doi.org/10.1002/col.22131 · https://doi.org/10.48550/arXiv.1802.06067
|
||||
*/
|
||||
|
||||
private static readonly Matrix M16 = new(new[,]
|
||||
{
|
||||
{ +0.401288, +0.650173, -0.051461 },
|
||||
{ -0.250268, +1.204414, +0.045854 },
|
||||
{ -0.002079, +0.048952, +0.953127 }
|
||||
});
|
||||
|
||||
private static readonly Matrix ForwardStep4 = new(new[,]
|
||||
{
|
||||
{ 2, 1, 1 / 20.0 },
|
||||
{ 1, -12 / 11.0, 1 / 11.0 },
|
||||
{ 1 / 9.0, 1 / 9.0, -2 / 9.0 },
|
||||
{ 1, 1, 21 / 20.0 }
|
||||
});
|
||||
|
||||
private static readonly Matrix ReverseStep4 = new Matrix(new double[,]
|
||||
{
|
||||
{ 460, 451, 288 },
|
||||
{ 460, -891, -261 },
|
||||
{ 460, -220, -6300 }
|
||||
}).Scale(1 / 1403.0);
|
||||
|
||||
private static ViewingConditions ViewingConditions(CamConfiguration camConfig)
|
||||
{
|
||||
var (xw, yw, zw) = (camConfig.WhitePoint.X, camConfig.WhitePoint.Y, camConfig.WhitePoint.Z);
|
||||
var la = camConfig.AdaptingLuminance;
|
||||
var yb = camConfig.BackgroundLuminance;
|
||||
var c = camConfig.C;
|
||||
var f = camConfig.F;
|
||||
var nc = camConfig.Nc;
|
||||
|
||||
// step 0
|
||||
var xyzWhitePointMatrix = Matrix.FromTriplet(xw, yw, zw);
|
||||
var (rw, gw, bw) = M16.Multiply(xyzWhitePointMatrix).ToTriplet();
|
||||
|
||||
var d = (f * (1 - 1 / 3.6 * Math.Exp((-la - 42) / 92.0))).Clamp(0, 1);
|
||||
var (dr, dg, db) = (D(rw), D(gw), D(bw));
|
||||
double D(double input) => d * (yw / input) + 1 - d;
|
||||
|
||||
var k = 1 / (5 * la + 1);
|
||||
var fl = 0.2 * Math.Pow(k, 4) * (5 * la) + 0.1 * Math.Pow(1 - Math.Pow(k, 4), 2) * CubeRoot(5 * la);
|
||||
var n = yb / yw;
|
||||
var z = 1.48 + Math.Sqrt(n);
|
||||
var nbb = 0.725 * Math.Pow(1 / n, 0.2);
|
||||
var ncb = nbb;
|
||||
|
||||
var (rwc, gwc, bwc) = (dr * rw, dg * gw, db * bw);
|
||||
var (raw, gaw, baw) = (Aw(rwc), Aw(gwc), Aw(bwc));
|
||||
|
||||
double Aw(double input)
|
||||
{
|
||||
var power = Math.Pow(fl * input / 100.0, 0.42);
|
||||
return 400 * (power / (power + 27.13)) + 0.1;
|
||||
}
|
||||
|
||||
var aw = (2 * raw + gaw + baw / 20.0 - 0.305) * nbb;
|
||||
return new ViewingConditions(c, nc, dr, dg, db, fl, n, z, nbb, ncb, aw);
|
||||
}
|
||||
|
||||
public static Cam16 FromXyz(Xyz xyz, CamConfiguration camConfig, XyzConfiguration xyzConfig)
|
||||
{
|
||||
var view = ViewingConditions(camConfig);
|
||||
|
||||
// step 1
|
||||
var xyzMatrix = Matrix.FromTriplet(xyz.X, xyz.Y, xyz.Z);
|
||||
xyzMatrix = Adaptation.WhitePoint(xyzMatrix, xyzConfig.WhitePoint, camConfig.WhitePoint)
|
||||
.Select(x => x * 100);
|
||||
var rgb = M16.Multiply(xyzMatrix).ToTriplet();
|
||||
|
||||
// step 2
|
||||
var (rc, gc, bc) = (view.Dr * rgb.First, view.Dg * rgb.Second, view.Db * rgb.Third);
|
||||
|
||||
// step 3
|
||||
var (ra, ga, ba) = (A(rc), A(gc), A(bc));
|
||||
|
||||
double A(double input)
|
||||
{
|
||||
if (double.IsNaN(input)) return double.NaN;
|
||||
var power = Math.Pow(view.Fl * Math.Abs(input) / 100.0, 0.42);
|
||||
return 400 * Math.Sign(input) * (power / (power + 27.13));
|
||||
}
|
||||
|
||||
// step 4
|
||||
var aMatrix = Matrix.FromTriplet(ra, ga, ba);
|
||||
var components = ForwardStep4.Multiply(aMatrix);
|
||||
var (p2, a, b, u) = (components[0, 0], components[1, 0], components[2, 0], components[3, 0]);
|
||||
var h = ToDegrees(Math.Atan2(b, a)).Modulo(360);
|
||||
|
||||
// step 5
|
||||
var et = HueData.GetEccentricity(h);
|
||||
|
||||
// step 6
|
||||
var achromatic = p2 * view.Nbb;
|
||||
|
||||
// step 7
|
||||
var j = 100 * Math.Pow(achromatic / view.Aw, view.C * view.Z);
|
||||
|
||||
// step 8
|
||||
var q = 4 / view.C * Math.Pow(j / 100.0, 0.5) * (view.Aw + 4) * Math.Pow(view.Fl, 0.25);
|
||||
|
||||
// step 9
|
||||
var t = 50000 / 13.0 * view.Nc * view.Ncb * et * Math.Sqrt(Math.Pow(a, 2) + Math.Pow(b, 2)) / (u + 0.305);
|
||||
var alpha = Math.Pow(t, 0.9) * Math.Pow(1.64 - Math.Pow(0.29, view.N), 0.73);
|
||||
var c = alpha * Math.Sqrt(j / 100.0);
|
||||
var m = c * Math.Pow(view.Fl, 0.25);
|
||||
var s = 50 * Math.Sqrt(alpha * view.C / (view.Aw + 4));
|
||||
return new Cam16(new Model(j, c, h, m, s, q), camConfig, ColourHeritage.From(xyz));
|
||||
}
|
||||
|
||||
public static Xyz ToXyz(Cam16 cam, CamConfiguration camConfig, XyzConfiguration xyzConfig)
|
||||
{
|
||||
var view = ViewingConditions(camConfig);
|
||||
|
||||
var j = cam.Model.J;
|
||||
var c = cam.Model.C;
|
||||
var h = cam.Model.H;
|
||||
|
||||
// step 1 (NOTE: currently not supporting partial models - all model values will be present)
|
||||
var alpha = j == 0 ? 0 : c / Math.Sqrt(j / 100.0);
|
||||
var t = Math.Pow(alpha / Math.Pow(1.64 - Math.Pow(0.29, view.N), 0.73), 1 / 0.9);
|
||||
|
||||
// step 2
|
||||
var et = 0.25 * (Math.Cos(ToRadians(h) + 2) + 3.8);
|
||||
var achromatic = view.Aw * Math.Pow(j / 100.0, 1 / (view.C * view.Z));
|
||||
var p1 = et * (50000 / 13.0) * view.Nc * view.Ncb;
|
||||
var p2 = achromatic / view.Nbb;
|
||||
|
||||
// step 3
|
||||
var gamma = 23 * (p2 + 0.305) * t /
|
||||
(23 * p1 + 11 * t * Math.Cos(ToRadians(h)) + 108 * t * Math.Sin(ToRadians(h)));
|
||||
var a = gamma * Math.Cos(ToRadians(h));
|
||||
var b = gamma * Math.Sin(ToRadians(h));
|
||||
|
||||
// step 4
|
||||
var components = Matrix.FromTriplet(p2, a, b);
|
||||
var (ra, ga, ba) = ReverseStep4.Multiply(components).ToTriplet();
|
||||
|
||||
// step 5
|
||||
var (rc, gc, bc) = (C(ra), C(ga), C(ba));
|
||||
|
||||
double C(double input)
|
||||
{
|
||||
if (double.IsNaN(input)) return double.NaN;
|
||||
return Math.Sign(input) * (100 / view.Fl) *
|
||||
Math.Pow(27.13 * Math.Abs(input) / (400 - Math.Abs(input)), 1 / 0.42);
|
||||
}
|
||||
|
||||
// step 6
|
||||
var rgbMatrix = Matrix.FromTriplet(rc / view.Dr, gc / view.Dg, bc / view.Db);
|
||||
|
||||
// step 7
|
||||
var xyzMatrix = M16.Inverse().Multiply(rgbMatrix);
|
||||
xyzMatrix = Adaptation.WhitePoint(xyzMatrix, camConfig.WhitePoint, xyzConfig.WhitePoint)
|
||||
.Select(x => x / 100.0);
|
||||
return new Xyz(xyzMatrix.ToTriplet(), ColourHeritage.From(cam));
|
||||
}
|
||||
}
|
||||
}
|
91
Unicolour/CamConfiguration.cs
Normal file
91
Unicolour/CamConfiguration.cs
Normal file
@@ -0,0 +1,91 @@
|
||||
using System;
|
||||
|
||||
namespace Wacton.Unicolour
|
||||
{
|
||||
|
||||
// NOTE: "discounting" parameter which I've noticed in other implementations
|
||||
// is not a parameter in the CAM papers (there's probably some other paper I've missed 🤷)
|
||||
// therefore D is always calculated
|
||||
public class CamConfiguration
|
||||
{
|
||||
public WhitePoint WhitePoint { get; }
|
||||
|
||||
public double
|
||||
AdaptingLuminance
|
||||
{
|
||||
get;
|
||||
} // [L_A] Luminance of adapting field (brightness of the room where the colour is being viewed)
|
||||
|
||||
public double
|
||||
BackgroundLuminance
|
||||
{
|
||||
get;
|
||||
} // [Y_b] Luminance of background (brightness of the area surrounding the colour)
|
||||
|
||||
internal readonly Surround
|
||||
Surround; // 0 = dark (movie theatre), 1 = dim (bright TV in dim room), 2 = average (surface colours)
|
||||
|
||||
internal double F => Surround switch
|
||||
{
|
||||
Surround.Dark => 0.8,
|
||||
Surround.Dim => 0.9,
|
||||
Surround.Average => 1.0,
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
};
|
||||
|
||||
internal double C => Surround switch
|
||||
{
|
||||
Surround.Dark => 0.525,
|
||||
Surround.Dim => 0.59,
|
||||
Surround.Average => 0.69,
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
};
|
||||
|
||||
internal double Nc => Surround switch
|
||||
{
|
||||
Surround.Dark => 0.8,
|
||||
Surround.Dim => 0.9,
|
||||
Surround.Average => 1.0,
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
};
|
||||
|
||||
/*
|
||||
* hard to find guidance for default CAM settings; this is based on data in "Usage guidelines for CIECAM97s" (Moroney, 2000)
|
||||
* - sRGB standard ambient illumination level of 64 lux ~= 4
|
||||
* - La = E * R / PI / 5 where E = lux & R = 1 --> 64 / PI / 5
|
||||
* ----------
|
||||
* I don't know why Google's HCT luminance calculations don't match the above
|
||||
* they suggest 200 lux -> ~11.72 luminance, but the formula above gives ~12.73 luminance
|
||||
* and they appear to ignore the division by 5 and incorporate XYZ luminance (Y)
|
||||
*/
|
||||
public static readonly CamConfiguration StandardRgb = new(WhitePoint.From(Illuminant.D65), LuxToLuminance(64),
|
||||
20, Surround.Average);
|
||||
|
||||
public static readonly CamConfiguration Hct = new(WhitePoint.From(Illuminant.D65),
|
||||
LuxToLuminance(200) * 5 * DefaultHctY(), DefaultHctY() * 100, Surround.Average);
|
||||
|
||||
internal static double LuxToLuminance(double lux) => lux / Math.PI / 5.0;
|
||||
|
||||
// just for HCT, use specific XYZ configuration
|
||||
private const double DefaultHctLightness = 50;
|
||||
private static double DefaultHctY() => Lab.ToXyz(new Lab(DefaultHctLightness, 0, 0), XyzConfiguration.D65).Y;
|
||||
|
||||
public CamConfiguration(WhitePoint whitePoint, double adaptingLuminance, double backgroundLuminance,
|
||||
Surround surround)
|
||||
{
|
||||
WhitePoint = whitePoint;
|
||||
AdaptingLuminance = adaptingLuminance;
|
||||
BackgroundLuminance = backgroundLuminance;
|
||||
Surround = surround;
|
||||
}
|
||||
|
||||
public override string ToString() => $"CAM {AdaptingLuminance:f0} {BackgroundLuminance:f0} {Surround}";
|
||||
}
|
||||
|
||||
public enum Surround
|
||||
{
|
||||
Dark = 0,
|
||||
Dim = 1,
|
||||
Average = 2
|
||||
}
|
||||
}
|
10
Unicolour/Chromaticity.cs
Normal file
10
Unicolour/Chromaticity.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace Wacton.Unicolour
|
||||
{
|
||||
|
||||
public record Chromaticity(double X, double Y)
|
||||
{
|
||||
public double X { get; } = X;
|
||||
public double Y { get; } = Y;
|
||||
public override string ToString() => $"({X:F4}, {Y:F4})";
|
||||
}
|
||||
}
|
98
Unicolour/ColourDescription.cs
Normal file
98
Unicolour/ColourDescription.cs
Normal file
@@ -0,0 +1,98 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Wacton.Unicolour
|
||||
{
|
||||
|
||||
internal record ColourDescription(string description)
|
||||
{
|
||||
private readonly string description = description;
|
||||
|
||||
internal static readonly ColourDescription NotApplicable = new("-");
|
||||
internal static readonly ColourDescription Black = new(nameof(Black));
|
||||
internal static readonly ColourDescription Shadow = new(nameof(Shadow));
|
||||
internal static readonly ColourDescription Dark = new(nameof(Dark));
|
||||
internal static readonly ColourDescription Pure = new(nameof(Pure));
|
||||
internal static readonly ColourDescription Light = new(nameof(Light));
|
||||
internal static readonly ColourDescription Pale = new(nameof(Pale));
|
||||
internal static readonly ColourDescription White = new(nameof(White));
|
||||
internal static readonly ColourDescription Grey = new(nameof(Grey));
|
||||
internal static readonly ColourDescription Faint = new(nameof(Faint));
|
||||
internal static readonly ColourDescription Weak = new(nameof(Weak));
|
||||
internal static readonly ColourDescription Mild = new(nameof(Mild));
|
||||
internal static readonly ColourDescription Strong = new(nameof(Strong));
|
||||
internal static readonly ColourDescription Vibrant = new(nameof(Vibrant));
|
||||
internal static readonly ColourDescription Red = new(nameof(Red));
|
||||
internal static readonly ColourDescription Orange = new(nameof(Orange));
|
||||
internal static readonly ColourDescription Yellow = new(nameof(Yellow));
|
||||
internal static readonly ColourDescription Chartreuse = new(nameof(Chartreuse));
|
||||
internal static readonly ColourDescription Green = new(nameof(Green));
|
||||
internal static readonly ColourDescription Mint = new(nameof(Mint));
|
||||
internal static readonly ColourDescription Cyan = new(nameof(Cyan));
|
||||
internal static readonly ColourDescription Azure = new(nameof(Azure));
|
||||
internal static readonly ColourDescription Blue = new(nameof(Blue));
|
||||
internal static readonly ColourDescription Violet = new(nameof(Violet));
|
||||
internal static readonly ColourDescription Magenta = new(nameof(Magenta));
|
||||
internal static readonly ColourDescription Rose = new(nameof(Rose));
|
||||
|
||||
internal static readonly List<ColourDescription> Lightnesses = new() { Shadow, Dark, Pure, Light, Pale };
|
||||
internal static readonly List<ColourDescription> Saturations = new() { Faint, Weak, Mild, Strong, Vibrant };
|
||||
|
||||
internal static readonly List<ColourDescription> Hues = new()
|
||||
{ Red, Orange, Yellow, Chartreuse, Green, Mint, Cyan, Azure, Blue, Violet, Magenta, Rose };
|
||||
|
||||
internal static readonly List<ColourDescription> Greyscales = new() { Black, Grey, White };
|
||||
|
||||
internal static IEnumerable<ColourDescription> Get(Hsl hsl)
|
||||
{
|
||||
if (hsl.UseAsNaN) return new List<ColourDescription> { NotApplicable };
|
||||
|
||||
var (h, s, l) = hsl.ConstrainedTriplet;
|
||||
switch (l)
|
||||
{
|
||||
case <= 0: return new List<ColourDescription> { Black };
|
||||
case >= 1: return new List<ColourDescription> { White };
|
||||
}
|
||||
|
||||
var lightness = l switch
|
||||
{
|
||||
< 0.20 => Shadow,
|
||||
< 0.40 => Dark,
|
||||
< 0.60 => Pure,
|
||||
< 0.80 => Light,
|
||||
_ => Pale
|
||||
};
|
||||
|
||||
if (hsl.UseAsGreyscale) return new List<ColourDescription> { lightness, Grey };
|
||||
|
||||
var strength = s switch
|
||||
{
|
||||
< 0.20 => Faint,
|
||||
< 0.40 => Weak,
|
||||
< 0.60 => Mild,
|
||||
< 0.80 => Strong,
|
||||
_ => Vibrant
|
||||
};
|
||||
|
||||
var hue = h switch
|
||||
{
|
||||
< 15 => Red,
|
||||
< 45 => Orange,
|
||||
< 75 => Yellow,
|
||||
< 105 => Chartreuse,
|
||||
< 135 => Green,
|
||||
< 165 => Mint,
|
||||
< 195 => Cyan,
|
||||
< 225 => Azure,
|
||||
< 255 => Blue,
|
||||
< 285 => Violet,
|
||||
< 315 => Magenta,
|
||||
< 345 => Rose,
|
||||
_ => Red
|
||||
};
|
||||
|
||||
return new List<ColourDescription> { lightness, strength, hue };
|
||||
}
|
||||
|
||||
public override string ToString() => description.ToLower();
|
||||
}
|
||||
}
|
47
Unicolour/ColourHeritage.cs
Normal file
47
Unicolour/ColourHeritage.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
namespace Wacton.Unicolour
|
||||
{
|
||||
|
||||
internal record ColourHeritage(string description)
|
||||
{
|
||||
private readonly string description = description;
|
||||
|
||||
internal static readonly ColourHeritage None = new(nameof(None));
|
||||
internal static readonly ColourHeritage NaN = new(nameof(NaN));
|
||||
internal static readonly ColourHeritage Greyscale = new(nameof(Greyscale));
|
||||
internal static readonly ColourHeritage Hued = new(nameof(Hued));
|
||||
internal static readonly ColourHeritage GreyscaleAndHued = new(nameof(GreyscaleAndHued));
|
||||
|
||||
internal static ColourHeritage From(ColourRepresentation parent)
|
||||
{
|
||||
if (parent.UseAsNaN) return NaN;
|
||||
return parent switch
|
||||
{
|
||||
{ UseAsGreyscale: true, UseAsHued: false } => Greyscale,
|
||||
{ UseAsGreyscale: false, UseAsHued: true } => Hued,
|
||||
{ UseAsGreyscale: true, UseAsHued: true } => GreyscaleAndHued,
|
||||
_ => None
|
||||
};
|
||||
}
|
||||
|
||||
internal static ColourHeritage From(ColourRepresentation firstParent, ColourRepresentation secondParent)
|
||||
{
|
||||
var first = From(firstParent);
|
||||
var second = From(secondParent);
|
||||
|
||||
var eitherNaN = first == NaN || second == NaN;
|
||||
var bothGreyscale = (first == Greyscale || first == GreyscaleAndHued) &&
|
||||
(second == Greyscale || second == GreyscaleAndHued);
|
||||
var eitherHued = first == Hued || first == GreyscaleAndHued || second == Hued || second == GreyscaleAndHued;
|
||||
|
||||
if (eitherNaN) return NaN;
|
||||
if (bothGreyscale)
|
||||
{
|
||||
return eitherHued ? GreyscaleAndHued : Greyscale;
|
||||
}
|
||||
|
||||
return eitherHued ? Hued : None;
|
||||
}
|
||||
|
||||
public override string ToString() => description;
|
||||
}
|
||||
}
|
67
Unicolour/ColourRepresentation.cs
Normal file
67
Unicolour/ColourRepresentation.cs
Normal file
@@ -0,0 +1,67 @@
|
||||
namespace Wacton.Unicolour
|
||||
{
|
||||
|
||||
public abstract record ColourRepresentation
|
||||
{
|
||||
protected readonly double First;
|
||||
protected readonly double Second;
|
||||
protected readonly double Third;
|
||||
protected abstract int? HueIndex { get; }
|
||||
public ColourTriplet Triplet => new(First, Second, Third, HueIndex);
|
||||
internal ColourHeritage Heritage { get; }
|
||||
|
||||
protected virtual double ConstrainedFirst => First;
|
||||
protected virtual double ConstrainedSecond => Second;
|
||||
protected virtual double ConstrainedThird => Third;
|
||||
public ColourTriplet ConstrainedTriplet => new(ConstrainedFirst, ConstrainedSecond, ConstrainedThird, HueIndex);
|
||||
|
||||
internal bool IsNaN => double.IsNaN(First) || double.IsNaN(Second) || double.IsNaN(Third);
|
||||
internal bool UseAsNaN => Heritage == ColourHeritage.NaN || IsNaN;
|
||||
|
||||
/*
|
||||
* a representation may be non-greyscale according to its values
|
||||
* but should be used as greyscale if it was generated from a representation that was greyscale
|
||||
* e.g. RGB(1,1,1) is greyscale and converts to LAB(99.99999999999999, 0, -2.220446049250313E-14)
|
||||
* LAB doesn't report as greyscale since B != 0 (and I don't want to make assumptions via precision-based tolerance comparison)
|
||||
* but it should be considered greyscale since the source RGB representation definitely is
|
||||
*/
|
||||
internal abstract bool IsGreyscale { get; }
|
||||
|
||||
internal bool UseAsGreyscale => Heritage == ColourHeritage.Greyscale ||
|
||||
Heritage == ColourHeritage.GreyscaleAndHued || (!UseAsNaN && IsGreyscale);
|
||||
|
||||
/*
|
||||
* a representation is considered "hued" when it has a hue component (e.g. HSL / LCH) and is not greyscale
|
||||
* enabling differentiation between representations where:
|
||||
* a) a hue value is meaningful ------------------------------ e.g. HSB(0,0,0) = red with no saturation or brightness
|
||||
* b) a hue value is used as a fallback when there is no hue - e.g. RGB(0,0,0) -> HSB(0,0,0) = black with no red
|
||||
* which is essential for proper mixing;
|
||||
* [RGB(0,0,0) black -> RGB(0,0,255) blue] via HSB is [HSB(0,0,0) red with no colour -> HSB(240,1,1) blue with full colour]
|
||||
* but the mixing should only start at the red hue if the value 0 was provided by the user (FromHsb instead of FromRgb)
|
||||
*/
|
||||
internal bool HasHueComponent => HueIndex != null;
|
||||
|
||||
internal bool UseAsHued =>
|
||||
(Heritage == ColourHeritage.None || Heritage == ColourHeritage.Hued ||
|
||||
Heritage == ColourHeritage.GreyscaleAndHued) && !UseAsNaN && HasHueComponent;
|
||||
|
||||
internal ColourRepresentation(double first, double second, double third, ColourHeritage heritage)
|
||||
{
|
||||
First = first;
|
||||
Second = second;
|
||||
Third = third;
|
||||
Heritage = heritage;
|
||||
}
|
||||
|
||||
protected abstract string FirstString { get; }
|
||||
protected abstract string SecondString { get; }
|
||||
protected abstract string ThirdString { get; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var values = $"{FirstString} {SecondString} {ThirdString}";
|
||||
return UseAsNaN ? $"NaN [{values}]" : values;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
29
Unicolour/ColourSpace.cs
Normal file
29
Unicolour/ColourSpace.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
namespace Wacton.Unicolour
|
||||
{
|
||||
|
||||
public enum ColourSpace
|
||||
{
|
||||
Rgb,
|
||||
Rgb255,
|
||||
RgbLinear,
|
||||
Hsb,
|
||||
Hsl,
|
||||
Hwb,
|
||||
Xyz,
|
||||
Xyy,
|
||||
Lab,
|
||||
Lchab,
|
||||
Luv,
|
||||
Lchuv,
|
||||
Hsluv,
|
||||
Hpluv,
|
||||
Ictcp,
|
||||
Jzazbz,
|
||||
Jzczhz,
|
||||
Oklab,
|
||||
Oklch,
|
||||
Cam02,
|
||||
Cam16,
|
||||
Hct
|
||||
}
|
||||
}
|
84
Unicolour/ColourTriplet.cs
Normal file
84
Unicolour/ColourTriplet.cs
Normal file
@@ -0,0 +1,84 @@
|
||||
using System;
|
||||
|
||||
namespace Wacton.Unicolour
|
||||
{
|
||||
|
||||
public record ColourTriplet(double First, double Second, double Third, int? HueIndex = null)
|
||||
{
|
||||
public double First { get; } = First;
|
||||
public double Second { get; } = Second;
|
||||
public double Third { get; } = Third;
|
||||
public (double, double, double) Tuple => (First, Second, Third);
|
||||
public int? HueIndex { get; } = HueIndex;
|
||||
|
||||
public double[] AsArray() => new[] { First, Second, Third };
|
||||
|
||||
internal double HueValue()
|
||||
{
|
||||
return HueIndex switch
|
||||
{
|
||||
0 => First,
|
||||
2 => Third,
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
};
|
||||
}
|
||||
|
||||
internal ColourTriplet WithHueOverride(double hue)
|
||||
{
|
||||
return HueIndex switch
|
||||
{
|
||||
0 => new(hue, Second, Third, HueIndex),
|
||||
2 => new(First, Second, hue, HueIndex),
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
};
|
||||
}
|
||||
|
||||
internal ColourTriplet WithHueModulo()
|
||||
{
|
||||
return HueIndex switch
|
||||
{
|
||||
0 => new(First.Modulo(360), Second, Third, HueIndex),
|
||||
2 => new(First, Second, Third.Modulo(360), HueIndex),
|
||||
null => new(First, Second, Third, HueIndex),
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
};
|
||||
}
|
||||
|
||||
internal ColourTriplet WithPremultipliedAlpha(double alpha)
|
||||
{
|
||||
return HueIndex switch
|
||||
{
|
||||
0 => new(First, Second * alpha, Third * alpha, HueIndex),
|
||||
2 => new(First * alpha, Second * alpha, Third, HueIndex),
|
||||
null => new(First * alpha, Second * alpha, Third * alpha, HueIndex),
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
};
|
||||
}
|
||||
|
||||
internal ColourTriplet WithUnpremultipliedAlpha(double alpha)
|
||||
{
|
||||
if (alpha == 0)
|
||||
{
|
||||
alpha = 1.0;
|
||||
}
|
||||
|
||||
return HueIndex switch
|
||||
{
|
||||
0 => new(First, Second / alpha, Third / alpha, HueIndex),
|
||||
2 => new(First / alpha, Second / alpha, Third, HueIndex),
|
||||
null => new(First / alpha, Second / alpha, Third / alpha, HueIndex),
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
};
|
||||
}
|
||||
|
||||
// need a custom deconstruct to ignore the nullable hue index
|
||||
public void Deconstruct(out double first, out double second, out double third)
|
||||
{
|
||||
first = First;
|
||||
second = Second;
|
||||
third = Third;
|
||||
}
|
||||
|
||||
public override string ToString() => Tuple.ToString();
|
||||
}
|
||||
}
|
17
Unicolour/Companding.cs
Normal file
17
Unicolour/Companding.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using System;
|
||||
|
||||
namespace Wacton.Unicolour
|
||||
{
|
||||
|
||||
public static class Companding
|
||||
{
|
||||
public static double Gamma(double value, double gamma) => Math.Pow(value, 1 / gamma);
|
||||
public static double InverseGamma(double value, double gamma) => Math.Pow(value, gamma);
|
||||
|
||||
public static double ReflectWhenNegative(double value, Func<double, double> function)
|
||||
{
|
||||
if (double.IsNaN(value)) return double.NaN;
|
||||
return Math.Sign(value) * function(Math.Abs(value));
|
||||
}
|
||||
}
|
||||
}
|
246
Unicolour/Comparison.cs
Normal file
246
Unicolour/Comparison.cs
Normal file
@@ -0,0 +1,246 @@
|
||||
using System;
|
||||
|
||||
namespace Wacton.Unicolour
|
||||
{
|
||||
using static Utils;
|
||||
|
||||
internal static class Comparison
|
||||
{
|
||||
// https://www.w3.org/WAI/WCAG21/Techniques/general/G18.html#tests
|
||||
// minimal recommended contrast ratio is 4.5, or 3 for larger font-sizes
|
||||
internal static double Contrast(Unicolour colour1, Unicolour colour2)
|
||||
{
|
||||
var luminance1 = colour1.RelativeLuminance;
|
||||
var luminance2 = colour2.RelativeLuminance;
|
||||
var l1 = Math.Max(luminance1, luminance2); // lighter of the colours
|
||||
var l2 = Math.Min(luminance1, luminance2); // darker of the colours
|
||||
return (l1 + 0.05) / (l2 + 0.05);
|
||||
}
|
||||
|
||||
internal static double Difference(DeltaE deltaE, Unicolour reference, Unicolour sample)
|
||||
{
|
||||
return deltaE switch
|
||||
{
|
||||
DeltaE.Cie76 => DeltaE76(reference, sample),
|
||||
DeltaE.Cie94 => DeltaE94(reference, sample),
|
||||
DeltaE.Cie94Textiles => DeltaE94(reference, sample, isForTextiles: true),
|
||||
DeltaE.Ciede2000 => DeltaE00(reference, sample),
|
||||
DeltaE.CmcAcceptability => DeltaECmc(reference, sample, l: 1, c: 1),
|
||||
DeltaE.CmcPerceptibility => DeltaECmc(reference, sample, l: 2, c: 1),
|
||||
DeltaE.Itp => DeltaEItp(reference, sample),
|
||||
DeltaE.Z => DeltaEz(reference, sample),
|
||||
DeltaE.Hyab => DeltaEHyab(reference, sample),
|
||||
DeltaE.Ok => DeltaEOk(reference, sample),
|
||||
DeltaE.Cam02 => DeltaECam02(reference, sample),
|
||||
DeltaE.Cam16 => DeltaECam16(reference, sample),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(deltaE), deltaE, null)
|
||||
};
|
||||
}
|
||||
|
||||
// https://en.wikipedia.org/wiki/Color_difference#CIE76
|
||||
private static double DeltaE76(Unicolour reference, Unicolour sample)
|
||||
{
|
||||
var (l1, a1, b1) = reference.Lab.Triplet;
|
||||
var (l2, a2, b2) = sample.Lab.Triplet;
|
||||
return Math.Sqrt(SquaredDiff(l1, l2) + SquaredDiff(a1, a2) + SquaredDiff(b1, b2));
|
||||
}
|
||||
|
||||
// https://en.wikipedia.org/wiki/Color_difference#CIE94
|
||||
private static double DeltaE94(Unicolour reference, Unicolour sample, bool isForTextiles = false)
|
||||
{
|
||||
var (l1, a1, b1) = reference.Lab.Triplet;
|
||||
var (l2, a2, b2) = sample.Lab.Triplet;
|
||||
var (_, c1, _) = reference.Lchab.Triplet;
|
||||
var (_, c2, _) = sample.Lchab.Triplet;
|
||||
|
||||
var lDelta = l1 - l2;
|
||||
var aDelta = a1 - a2;
|
||||
var bDelta = b1 - b2;
|
||||
var cDelta = c1 - c2;
|
||||
var hDelta = Math.Sqrt(
|
||||
Math.Pow(aDelta, 2) +
|
||||
Math.Pow(bDelta, 2) -
|
||||
Math.Pow(cDelta, 2)
|
||||
);
|
||||
|
||||
var k1 = isForTextiles ? 0.048 : 0.045;
|
||||
var k2 = isForTextiles ? 0.014 : 0.015;
|
||||
var kl = isForTextiles ? 2 : 1;
|
||||
const int kc = 1;
|
||||
const int kh = 1;
|
||||
|
||||
const int sl = 1;
|
||||
var sc = 1 + k1 * c1;
|
||||
var sh = 1 + k2 * c1;
|
||||
|
||||
return Math.Sqrt(
|
||||
Math.Pow(lDelta / (kl * sl), 2) +
|
||||
Math.Pow(cDelta / (kc * sc), 2) +
|
||||
Math.Pow(hDelta / (kh * sh), 2)
|
||||
);
|
||||
}
|
||||
|
||||
// https://en.wikipedia.org/wiki/Color_difference#CIEDE2000
|
||||
private static double DeltaE00(Unicolour reference, Unicolour sample)
|
||||
{
|
||||
var (l1, a1, b1) = reference.Lab.Triplet;
|
||||
var (l2, a2, b2) = sample.Lab.Triplet;
|
||||
var (_, c1, _) = reference.Lchab.Triplet;
|
||||
var (_, c2, _) = sample.Lchab.Triplet;
|
||||
|
||||
double Power2(double value) => Math.Pow(value, 2);
|
||||
double Power7(double value) => Math.Pow(value, 7);
|
||||
double SqrtCPower7(double c) => Math.Sqrt(Power7(c) / (Power7(c) + Power7(25)));
|
||||
|
||||
var avgL = (l1 + l2) / 2.0;
|
||||
var avgC = (c1 + c2) / 2.0;
|
||||
var g = 0.5 * (1 - SqrtCPower7(avgC));
|
||||
var a1Prime = a1 * (1 + g);
|
||||
var a2Prime = a2 * (1 + g);
|
||||
var c1Prime = Math.Sqrt(Power2(a1Prime) + Power2(b1));
|
||||
var c2Prime = Math.Sqrt(Power2(a2Prime) + Power2(b2));
|
||||
var avgCPrime = (c1Prime + c2Prime) / 2.0;
|
||||
var h1Prime = ToDegrees(Math.Atan2(b1, a1Prime)).Modulo(360);
|
||||
var h2Prime = ToDegrees(Math.Atan2(b2, a2Prime)).Modulo(360);
|
||||
|
||||
var hPrimeDelta = Math.Abs(h1Prime - h2Prime) switch
|
||||
{
|
||||
<= 180 => h2Prime - h1Prime,
|
||||
> 180 when h2Prime <= h1Prime => h2Prime - h1Prime + 360,
|
||||
> 180 when h2Prime > h1Prime => h2Prime - h1Prime - 360,
|
||||
_ => double.NaN
|
||||
};
|
||||
|
||||
var avgHPrime = Math.Abs(h1Prime - h2Prime) switch
|
||||
{
|
||||
<= 180 => (h1Prime + h2Prime) / 2.0,
|
||||
> 180 when h1Prime + h2Prime < 360 => (h1Prime + h2Prime + 360) / 2.0,
|
||||
> 180 when h1Prime + h2Prime >= 360 => (h1Prime + h2Prime - 360) / 2.0,
|
||||
_ => double.NaN
|
||||
};
|
||||
|
||||
var t = 1 -
|
||||
0.17 * Math.Cos(ToRadians(avgHPrime - 30)) +
|
||||
0.24 * Math.Cos(ToRadians(2 * avgHPrime)) +
|
||||
0.32 * Math.Cos(ToRadians(3 * avgHPrime + 6)) -
|
||||
0.20 * Math.Cos(ToRadians(4 * avgHPrime - 63));
|
||||
|
||||
var deltaLPrime = l2 - l1;
|
||||
var deltaCPrime = c2 - c1;
|
||||
var deltaHPrime = 2 * Math.Sqrt(c1Prime * c2Prime) * Math.Sin(ToRadians(hPrimeDelta / 2.0));
|
||||
|
||||
const int kl = 1;
|
||||
const int kc = 1;
|
||||
const int kh = 1;
|
||||
|
||||
var sl = 1 + (0.015 * Power2(avgL - 50)) / Math.Sqrt(20 + Power2(avgL - 50));
|
||||
var sc = 1 + 0.045 * avgCPrime;
|
||||
var sh = 1 + 0.015 * avgCPrime * t;
|
||||
|
||||
var deltaTheta = 30 * Math.Exp(-Power2((avgHPrime - 275) / 25.0));
|
||||
var rc = 2 * SqrtCPower7(avgCPrime);
|
||||
var rt = -rc * Math.Sin(ToRadians(2 * deltaTheta));
|
||||
|
||||
double Ratio(double delta, double k, double s) => delta / (k * s);
|
||||
var ratioL = Ratio(deltaLPrime, kl, sl);
|
||||
var ratioC = Ratio(deltaCPrime, kc, sc);
|
||||
var ratioH = Ratio(deltaHPrime, kh, sh);
|
||||
return Math.Sqrt(
|
||||
Power2(ratioL) +
|
||||
Power2(ratioC) +
|
||||
Power2(ratioH) +
|
||||
rt * ratioC * ratioH);
|
||||
}
|
||||
|
||||
// https://en.wikipedia.org/wiki/Color_difference#CMC_l:c_(1984)
|
||||
private static double DeltaECmc(Unicolour reference, Unicolour sample, double l, double c)
|
||||
{
|
||||
var (l1, a1, b1) = reference.Lab.Triplet;
|
||||
var (l2, a2, b2) = sample.Lab.Triplet;
|
||||
var (_, c1, h1) = reference.Lchab.ConstrainedTriplet;
|
||||
var (_, c2, _) = sample.Lchab.Triplet;
|
||||
|
||||
var lDelta = l1 - l2;
|
||||
var aDelta = a1 - a2;
|
||||
var bDelta = b1 - b2;
|
||||
var cDelta = c1 - c2;
|
||||
var hDelta = Math.Sqrt(
|
||||
Math.Pow(aDelta, 2) +
|
||||
Math.Pow(bDelta, 2) -
|
||||
Math.Pow(cDelta, 2)
|
||||
);
|
||||
|
||||
var f = Math.Sqrt(Math.Pow(c1, 4) / (Math.Pow(c1, 4) + 1900));
|
||||
var t = h1 is > 165 and <= 345
|
||||
? 0.56 + Math.Abs(0.2 * Math.Cos(ToRadians(h1 + 168)))
|
||||
: 0.36 + Math.Abs(0.4 * Math.Cos(ToRadians(h1 + 35)));
|
||||
|
||||
var sl = l1 < 16 ? 0.511 : 0.040975 * l1 / (1 + 0.01765 * l1);
|
||||
var sc = 0.0638 * c1 / (1 + 0.0131 * c1) + 0.638;
|
||||
var sh = sc * (f * t + 1 - f);
|
||||
|
||||
return Math.Sqrt(
|
||||
Math.Pow(lDelta / (l * sl), 2) +
|
||||
Math.Pow(cDelta / (c * sc), 2) +
|
||||
Math.Pow(hDelta / sh, 2)
|
||||
);
|
||||
}
|
||||
|
||||
// https://www.itu.int/dms_pubrec/itu-r/rec/bt/R-REC-BT.2124-0-201901-I!!PDF-E.pdf
|
||||
private static double DeltaEItp(Unicolour reference, Unicolour sample)
|
||||
{
|
||||
ColourTriplet ToItp(Ictcp ictcp) => new(ictcp.I, 0.5 * ictcp.Ct, ictcp.Cp);
|
||||
var (i1, t1, c1) = ToItp(reference.Ictcp);
|
||||
var (i2, t2, c2) = ToItp(sample.Ictcp);
|
||||
return 720 * Math.Sqrt(SquaredDiff(i1, i2) + SquaredDiff(t1, t2) + SquaredDiff(c1, c2));
|
||||
}
|
||||
|
||||
// https://doi.org/10.1364/OE.25.015131
|
||||
private static double DeltaEz(Unicolour reference, Unicolour sample)
|
||||
{
|
||||
var (jz1, cz1, hz1) = reference.Jzczhz.Triplet;
|
||||
var (jz2, cz2, hz2) = sample.Jzczhz.Triplet;
|
||||
|
||||
var deltaHz = hz1 - hz2;
|
||||
deltaHz = 2 * Math.Sqrt(reference.Jzczhz.C * sample.Jzczhz.C) * Math.Sin(ToRadians(deltaHz / 2.0));
|
||||
deltaHz = Math.Pow(deltaHz, 2);
|
||||
return Math.Sqrt(SquaredDiff(jz1, jz2) + SquaredDiff(cz1, cz2) + deltaHz);
|
||||
}
|
||||
|
||||
// https://en.wikipedia.org/wiki/Color_difference#Other_geometric_constructions
|
||||
private static double DeltaEHyab(Unicolour reference, Unicolour sample)
|
||||
{
|
||||
var (l1, a1, b1) = reference.Lab.Triplet;
|
||||
var (l2, a2, b2) = sample.Lab.Triplet;
|
||||
return Math.Sqrt(SquaredDiff(a1, a2) + SquaredDiff(b1, b2)) + Math.Abs(l2 - l1);
|
||||
}
|
||||
|
||||
// https://www.w3.org/TR/css-color-4/#color-difference-OK
|
||||
private static double DeltaEOk(Unicolour reference, Unicolour sample)
|
||||
{
|
||||
var (l1, a1, b1) = reference.Oklab.Triplet;
|
||||
var (l2, a2, b2) = sample.Oklab.Triplet;
|
||||
return Math.Sqrt(SquaredDiff(l1, l2) + SquaredDiff(a1, a2) + SquaredDiff(b1, b2));
|
||||
}
|
||||
|
||||
// https://doi.org/10.1007/978-1-4419-6190-7_2
|
||||
// currently only support UCS, not LCD or SCD - no need to handle ΔJ / kl since kl = 1
|
||||
private static double DeltaECam02(Unicolour reference, Unicolour sample)
|
||||
{
|
||||
var (j1, a1, b1) = reference.Cam02.Triplet;
|
||||
var (j2, a2, b2) = sample.Cam02.Triplet;
|
||||
return Math.Sqrt(SquaredDiff(j1, j2) + SquaredDiff(a1, a2) + SquaredDiff(b1, b2));
|
||||
}
|
||||
|
||||
// https://doi.org/10.1002/col.22131
|
||||
private static double DeltaECam16(Unicolour reference, Unicolour sample)
|
||||
{
|
||||
var (j1, a1, b1) = reference.Cam16.Triplet;
|
||||
var (j2, a2, b2) = sample.Cam16.Triplet;
|
||||
var deltaE = Math.Sqrt(SquaredDiff(j1, j2) + SquaredDiff(a1, a2) + SquaredDiff(b1, b2));
|
||||
return 1.41 * Math.Pow(deltaE, 0.63);
|
||||
}
|
||||
|
||||
private static double SquaredDiff(double first, double second) => Math.Pow(second - first, 2);
|
||||
}
|
||||
}
|
34
Unicolour/Configuration.cs
Normal file
34
Unicolour/Configuration.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using System;
|
||||
|
||||
namespace Wacton.Unicolour
|
||||
{
|
||||
|
||||
public class Configuration
|
||||
{
|
||||
internal readonly Guid Id = Guid.NewGuid();
|
||||
|
||||
public RgbConfiguration Rgb { get; }
|
||||
public XyzConfiguration Xyz { get; }
|
||||
public CamConfiguration Cam { get; }
|
||||
public double IctcpScalar { get; }
|
||||
public double JzazbzScalar { get; }
|
||||
|
||||
public static readonly Configuration Default = new();
|
||||
|
||||
public Configuration(
|
||||
RgbConfiguration? rgbConfiguration = null,
|
||||
XyzConfiguration? xyzConfiguration = null,
|
||||
CamConfiguration? camConfiguration = null,
|
||||
double ictcpScalar = 100,
|
||||
double jzazbzScalar = 100)
|
||||
{
|
||||
Rgb = rgbConfiguration ?? RgbConfiguration.StandardRgb;
|
||||
Xyz = xyzConfiguration ?? XyzConfiguration.D65;
|
||||
Cam = camConfiguration ?? CamConfiguration.StandardRgb;
|
||||
IctcpScalar = ictcpScalar;
|
||||
JzazbzScalar = jzazbzScalar;
|
||||
}
|
||||
|
||||
public override string ToString() => $"{Id}";
|
||||
}
|
||||
}
|
19
Unicolour/DeltaE.cs
Normal file
19
Unicolour/DeltaE.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
namespace Wacton.Unicolour
|
||||
{
|
||||
|
||||
public enum DeltaE
|
||||
{
|
||||
Cie76,
|
||||
Cie94,
|
||||
Cie94Textiles,
|
||||
Ciede2000,
|
||||
CmcAcceptability,
|
||||
CmcPerceptibility,
|
||||
Itp,
|
||||
Z,
|
||||
Hyab,
|
||||
Ok,
|
||||
Cam02,
|
||||
Cam16
|
||||
}
|
||||
}
|
91
Unicolour/GamutMapping.cs
Normal file
91
Unicolour/GamutMapping.cs
Normal file
@@ -0,0 +1,91 @@
|
||||
namespace Wacton.Unicolour
|
||||
{
|
||||
|
||||
internal static class GamutMapping
|
||||
{
|
||||
/*
|
||||
* adapted from https://www.w3.org/TR/css-color-4/#css-gamut-mapping & https://www.w3.org/TR/css-color-4/#binsearch
|
||||
* the pseudocode doesn't appear to handle the edge case scenario where:
|
||||
* a) origin colour OKLCH chroma < epsilon
|
||||
* b) origin colour destination (RGB here) is out-of-gamut
|
||||
* e.g. OKLCH (0.99999, 0, 0) --> RGB (1.00010, 0.99998, 0.99974)
|
||||
* - the search never executes since chroma = 0 (min 0 - max 0 < epsilon 0.0001)
|
||||
* - even if the search did execute, would not return clipped variant since ΔE is *too small*, and min never changes from 0
|
||||
* so need to clip if the mapped colour is somehow out-of-gamut (i.e. not processed)
|
||||
*/
|
||||
internal static Unicolour ToRgbGamut(Unicolour unicolour)
|
||||
{
|
||||
var config = unicolour.Config;
|
||||
var rgb = unicolour.Rgb;
|
||||
var alpha = unicolour.Alpha.A;
|
||||
if (unicolour.IsInDisplayGamut) return new Unicolour(ColourSpace.Rgb, config, rgb.Triplet.Tuple, alpha);
|
||||
|
||||
var oklch = unicolour.Oklch;
|
||||
if (oklch.L >= 1.0) return new Unicolour(ColourSpace.Rgb, config, 1, 1, 1, alpha);
|
||||
if (oklch.L <= 0.0) return new Unicolour(ColourSpace.Rgb, config, 0, 0, 0, alpha);
|
||||
|
||||
const double jnd = 0.02;
|
||||
const double epsilon = 0.0001;
|
||||
var minChroma = 0.0;
|
||||
var maxChroma = oklch.C;
|
||||
var minChromaInGamut = true;
|
||||
|
||||
// iteration count ensures the while loop doesn't get stuck in an endless cycle if bad input is provided
|
||||
// e.g. double.Epsilon
|
||||
var iterations = 0;
|
||||
Unicolour? current = null;
|
||||
bool HasChromaConverged() => maxChroma - minChroma <= epsilon;
|
||||
while (!HasChromaConverged() && iterations < 1000)
|
||||
{
|
||||
iterations++;
|
||||
|
||||
var chroma = (minChroma + maxChroma) / 2.0;
|
||||
current = FromOklchWithChroma(chroma);
|
||||
|
||||
if (minChromaInGamut && current.Rgb.IsInGamut)
|
||||
{
|
||||
minChroma = chroma;
|
||||
continue;
|
||||
}
|
||||
|
||||
var clipped = FromRgbWithClipping(current.Rgb);
|
||||
var deltaE = clipped.Difference(DeltaE.Ok, current);
|
||||
|
||||
var isNoticeableDifference = deltaE >= jnd;
|
||||
if (isNoticeableDifference)
|
||||
{
|
||||
maxChroma = chroma;
|
||||
}
|
||||
else
|
||||
{
|
||||
// not clear to me why a clipped colour must have ΔE from "current" colour between 0.0199 - 0.02
|
||||
// effectively: only returning clipped when ΔE == JND, but continue if the non-noticeable ΔE is *too small*
|
||||
// but I assume it's something to do with this comment about intersecting shallow and concave gamut boundaries
|
||||
// https://github.com/w3c/csswg-drafts/issues/7653#issuecomment-1489096489
|
||||
var isUnnoticeableDifferenceLargeEnough = jnd - deltaE < epsilon;
|
||||
if (isUnnoticeableDifferenceLargeEnough)
|
||||
{
|
||||
return clipped;
|
||||
}
|
||||
|
||||
minChromaInGamut = false;
|
||||
minChroma = chroma;
|
||||
}
|
||||
}
|
||||
|
||||
// in case while loop never executes (e.g. Oklch.C == 0)
|
||||
current ??= FromOklchWithChroma(oklch.C);
|
||||
|
||||
// it's possible for the "current" colour to still be out of RGB gamut, either because:
|
||||
// a) the original OKLCH was not processed (chroma too low) and was already out of RGB gamut
|
||||
// b) the algorithm converged on an OKLCH that is out of RGB gamut (happens ~5% of the time for me with using random OKLCH inputs)
|
||||
return current.IsInDisplayGamut ? current : FromRgbWithClipping(current.Rgb);
|
||||
|
||||
Unicolour FromOklchWithChroma(double chroma) =>
|
||||
new(ColourSpace.Oklch, config, oklch.L, chroma, oklch.H, alpha);
|
||||
|
||||
Unicolour FromRgbWithClipping(Rgb unclippedRgb) =>
|
||||
new(ColourSpace.Rgb, config, unclippedRgb.ConstrainedTriplet.Tuple, alpha);
|
||||
}
|
||||
}
|
||||
}
|
156
Unicolour/Hct.cs
Normal file
156
Unicolour/Hct.cs
Normal file
@@ -0,0 +1,156 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Wacton.Unicolour
|
||||
{
|
||||
|
||||
public record Hct : ColourRepresentation
|
||||
{
|
||||
protected override int? HueIndex => 0;
|
||||
public double H => First;
|
||||
public double C => Second;
|
||||
public double T => Third;
|
||||
public double ConstrainedH => ConstrainedFirst;
|
||||
protected override double ConstrainedFirst => H.Modulo(360.0);
|
||||
internal override bool IsGreyscale => C <= 0.0 || T is <= 0.0 or >= 100.0;
|
||||
|
||||
public Hct(double h, double c, double t) : this(h, c, t, ColourHeritage.None)
|
||||
{
|
||||
}
|
||||
|
||||
internal Hct(double h, double c, double t, ColourHeritage heritage) : base(h, c, t, heritage)
|
||||
{
|
||||
}
|
||||
|
||||
protected override string FirstString => UseAsHued ? $"{H:F1}°" : "—°";
|
||||
protected override string SecondString => $"{C:F2}";
|
||||
protected override string ThirdString => $"{T:F2}";
|
||||
public override string ToString() => base.ToString();
|
||||
|
||||
/*
|
||||
* HCT is a transform of XYZ
|
||||
* (just a combination of LAB & CAM16, but with specific XYZ & CAM configuration, so can't reuse existing colour space calculations)
|
||||
* Forward: https://material.io/blog/science-of-color-design
|
||||
* Reverse: n/a - no published reverse transform and I don't want to port Google code, so using my own naive search
|
||||
*/
|
||||
|
||||
internal static Cam16 Cam16Component(Xyz xyz) => Cam16.FromXyz(xyz, CamConfiguration.Hct, XyzConfiguration.D65);
|
||||
internal static Lab LabComponent(Xyz xyz) => Lab.FromXyz(xyz, XyzConfiguration.D65);
|
||||
|
||||
internal static Hct FromXyz(Xyz xyz, XyzConfiguration xyzConfig)
|
||||
{
|
||||
var xyzMatrix = Matrix.FromTriplet(xyz.Triplet);
|
||||
var d65Matrix = Adaptation.WhitePoint(xyzMatrix, xyzConfig.WhitePoint, WhitePoint.From(Illuminant.D65));
|
||||
var d65Xyz = new Xyz(d65Matrix.ToTriplet(), ColourHeritage.From(xyz));
|
||||
|
||||
var cam16 = Cam16Component(d65Xyz);
|
||||
var lab = LabComponent(d65Xyz);
|
||||
|
||||
var h = cam16.Model.H;
|
||||
var c = cam16.Model.C;
|
||||
var t = lab.L;
|
||||
return new Hct(h, c, t, ColourHeritage.From(xyz));
|
||||
}
|
||||
|
||||
internal static Xyz ToXyz(Hct hct, XyzConfiguration xyzConfig)
|
||||
{
|
||||
var targetY = Lab.ToXyz(new Lab(hct.T, 0, 0), XyzConfiguration.D65).Y;
|
||||
var result = FindBestJ(targetY, hct);
|
||||
var d65Xyz = result.Converged ? result.Data.Xyz : new Xyz(double.NaN, double.NaN, double.NaN);
|
||||
var d65Matrix = Matrix.FromTriplet(d65Xyz.Triplet);
|
||||
var xyzMatrix = Adaptation.WhitePoint(d65Matrix, WhitePoint.From(Illuminant.D65), xyzConfig.WhitePoint);
|
||||
var xyz = new Xyz(xyzMatrix.ToTriplet(), ColourHeritage.From(hct))
|
||||
{
|
||||
HctToXyzSearchResult = result
|
||||
};
|
||||
|
||||
return xyz;
|
||||
}
|
||||
|
||||
// i'm sure some smart people have some fancy-pants algorithms to do this efficiently
|
||||
// but until there's some kind of published reverse transformation algorithm, this gets the job done
|
||||
// (albeit rather slowly...)
|
||||
private static HctToXyzSearchResult FindBestJ(double targetY, Hct hct)
|
||||
{
|
||||
HctToXyzSearchData latest = GetStartingData(targetY, hct);
|
||||
HctToXyzSearchData best = latest;
|
||||
|
||||
var step = latest.J;
|
||||
var iterations = 0;
|
||||
while (!double.IsNaN(latest.DeltaY) && Math.Abs(latest.DeltaY) > 0.000000001 && iterations < 100)
|
||||
{
|
||||
var j = latest.J + (latest.DeltaY > 0 ? -step : step);
|
||||
var data = ProcessJ(targetY, j, hct);
|
||||
var deltaY = data.DeltaY;
|
||||
if (Math.Abs(deltaY) < Math.Abs(best.DeltaY))
|
||||
{
|
||||
best = data;
|
||||
}
|
||||
|
||||
// change in sign of delta means target is now in the other direction
|
||||
var overshot = double.IsNaN(deltaY) || Math.Sign(latest.DeltaY) != Math.Sign(deltaY);
|
||||
if (overshot)
|
||||
{
|
||||
step /= 2.0;
|
||||
}
|
||||
|
||||
latest = data;
|
||||
iterations++;
|
||||
}
|
||||
|
||||
var converged = !double.IsNaN(latest.DeltaY) && iterations < 100;
|
||||
return new HctToXyzSearchResult(best, iterations, converged);
|
||||
}
|
||||
|
||||
private static HctToXyzSearchData GetStartingData(double targetY, Hct hct)
|
||||
{
|
||||
var xzPairs = new List<(double, double)> { (0, 0), (0, 1), (1, 0), (1, 1) };
|
||||
HctToXyzSearchData best = InitialData;
|
||||
foreach (var (x, z) in xzPairs)
|
||||
{
|
||||
var j = Cam16.FromXyz(new Xyz(x, targetY, z), CamConfiguration.Hct, XyzConfiguration.D65).Model.J;
|
||||
var data = ProcessJ(targetY, j, hct);
|
||||
if (Math.Abs(data.DeltaY) < best.DeltaY)
|
||||
{
|
||||
best = data;
|
||||
}
|
||||
}
|
||||
|
||||
return best;
|
||||
}
|
||||
|
||||
private static HctToXyzSearchData ProcessJ(double targetY, double j, Hct hct)
|
||||
{
|
||||
var camModel = new Cam.Model(j, hct.C, hct.H, 0, 0, 0);
|
||||
var cam16 = new Cam16(camModel, CamConfiguration.Hct, ColourHeritage.None);
|
||||
var xyz = Cam16.ToXyz(cam16, CamConfiguration.Hct, XyzConfiguration.D65);
|
||||
var deltaY = xyz.Y - targetY;
|
||||
return new HctToXyzSearchData(hct, j, cam16, xyz, targetY, deltaY);
|
||||
}
|
||||
|
||||
private static readonly HctToXyzSearchData InitialData = new(
|
||||
J: double.PositiveInfinity, DeltaY: double.PositiveInfinity,
|
||||
Hct: null!, Cam16: null!, Xyz: null!, TargetY: double.NaN);
|
||||
}
|
||||
|
||||
// only for potential debugging or diagnostics
|
||||
// until there is an "official" HCT -> XYZ reverse transform
|
||||
internal record HctToXyzSearchResult(HctToXyzSearchData Data, int Iterations, bool Converged)
|
||||
{
|
||||
internal HctToXyzSearchData Data { get; } = Data;
|
||||
internal int Iterations { get; } = Iterations;
|
||||
internal bool Converged { get; } = Converged;
|
||||
public override string ToString() => $"{Data} · Iterations:{Iterations} · Converged:{Converged}";
|
||||
}
|
||||
|
||||
internal record HctToXyzSearchData(Hct Hct, double J, Cam16 Cam16, Xyz Xyz, double TargetY, double DeltaY)
|
||||
{
|
||||
internal Hct Hct { get; } = Hct;
|
||||
internal double J { get; } = J;
|
||||
internal Cam16 Cam16 { get; } = Cam16;
|
||||
internal Xyz Xyz { get; } = Xyz;
|
||||
internal double TargetY { get; } = TargetY;
|
||||
internal double DeltaY { get; } = DeltaY;
|
||||
public override string ToString() => $"J:{J:F4} · ΔY:{DeltaY:F4}";
|
||||
}
|
||||
}
|
95
Unicolour/Hpluv.cs
Normal file
95
Unicolour/Hpluv.cs
Normal file
@@ -0,0 +1,95 @@
|
||||
namespace Wacton.Unicolour
|
||||
{
|
||||
|
||||
public record Hpluv : ColourRepresentation
|
||||
{
|
||||
protected override int? HueIndex => 0;
|
||||
public double H => First;
|
||||
public double S => Second;
|
||||
public double L => Third;
|
||||
public double ConstrainedH => ConstrainedFirst;
|
||||
public double ConstrainedS => ConstrainedSecond;
|
||||
public double ConstrainedL => ConstrainedThird;
|
||||
protected override double ConstrainedFirst => H.Modulo(360.0);
|
||||
protected override double ConstrainedSecond => S.Clamp(0.0, 100.0);
|
||||
protected override double ConstrainedThird => L.Clamp(0.0, 100.0);
|
||||
internal override bool IsGreyscale => S <= 0.0 || L is <= 0.0 or >= 100.0;
|
||||
|
||||
public Hpluv(double h, double s, double l) : this(h, s, l, ColourHeritage.None)
|
||||
{
|
||||
}
|
||||
|
||||
internal Hpluv(double h, double s, double l, ColourHeritage heritage) : base(h, s, l, heritage)
|
||||
{
|
||||
}
|
||||
|
||||
protected override string FirstString => UseAsHued ? $"{H:F1}°" : "—°";
|
||||
protected override string SecondString => $"{S:F1}%";
|
||||
protected override string ThirdString => $"{L:F1}%";
|
||||
public override string ToString() => base.ToString();
|
||||
|
||||
/*
|
||||
* HPLUV is a transform of LCHUV
|
||||
* Forward: https://github.com/hsluv/hsluv-haxe/blob/master/src/hsluv/Hsluv.hx#L397
|
||||
* Reverse: https://github.com/hsluv/hsluv-haxe/blob/master/src/hsluv/Hsluv.hx#L380
|
||||
*/
|
||||
|
||||
internal static Hpluv FromLchuv(Lchuv lchuv)
|
||||
{
|
||||
var (lchLightness, chroma, hue) = lchuv.ConstrainedTriplet;
|
||||
double saturation;
|
||||
double lightness;
|
||||
|
||||
switch (lchLightness)
|
||||
{
|
||||
case > 99.9999999:
|
||||
saturation = 0.0;
|
||||
lightness = 100.0;
|
||||
break;
|
||||
case < 0.00000001:
|
||||
saturation = 0.0;
|
||||
lightness = 0.0;
|
||||
break;
|
||||
default:
|
||||
{
|
||||
var maxChroma = BoundingLines.CalculateMaxChroma(lchLightness);
|
||||
saturation = chroma / maxChroma * 100;
|
||||
lightness = lchLightness;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return new Hpluv(hue, saturation, lightness, ColourHeritage.From(lchuv));
|
||||
}
|
||||
|
||||
internal static Lchuv ToLchuv(Hpluv hpluv)
|
||||
{
|
||||
var hue = hpluv.ConstrainedH;
|
||||
var saturation = hpluv.S;
|
||||
var hslLightness = hpluv.L;
|
||||
double lightness;
|
||||
double chroma;
|
||||
|
||||
switch (hslLightness)
|
||||
{
|
||||
case > 99.9999999:
|
||||
lightness = 100.0;
|
||||
chroma = 0.0;
|
||||
break;
|
||||
case < 0.00000001:
|
||||
lightness = 0.0;
|
||||
chroma = 0.0;
|
||||
break;
|
||||
default:
|
||||
{
|
||||
var maxChroma = BoundingLines.CalculateMaxChroma(hslLightness);
|
||||
chroma = maxChroma / 100 * saturation;
|
||||
lightness = hslLightness;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return new Lchuv(lightness, chroma, hue, ColourHeritage.From(hpluv));
|
||||
}
|
||||
}
|
||||
}
|
82
Unicolour/Hsb.cs
Normal file
82
Unicolour/Hsb.cs
Normal file
@@ -0,0 +1,82 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace Wacton.Unicolour
|
||||
{
|
||||
|
||||
public record Hsb : ColourRepresentation
|
||||
{
|
||||
protected override int? HueIndex => 0;
|
||||
public double H => First;
|
||||
public double S => Second;
|
||||
public double B => Third;
|
||||
public double ConstrainedH => ConstrainedFirst;
|
||||
public double ConstrainedS => ConstrainedSecond;
|
||||
public double ConstrainedB => ConstrainedThird;
|
||||
protected override double ConstrainedFirst => H.Modulo(360.0);
|
||||
protected override double ConstrainedSecond => S.Clamp(0.0, 1.0);
|
||||
protected override double ConstrainedThird => B.Clamp(0.0, 1.0);
|
||||
internal override bool IsGreyscale => S <= 0.0 || B <= 0.0;
|
||||
|
||||
public Hsb(double h, double s, double b) : this(h, s, b, ColourHeritage.None)
|
||||
{
|
||||
}
|
||||
|
||||
internal Hsb(double h, double s, double b, ColourHeritage heritage) : base(h, s, b, heritage)
|
||||
{
|
||||
}
|
||||
|
||||
protected override string FirstString => UseAsHued ? $"{H:F1}°" : "—°";
|
||||
protected override string SecondString => $"{S * 100:F1}%";
|
||||
protected override string ThirdString => $"{B * 100:F1}%";
|
||||
public override string ToString() => base.ToString();
|
||||
|
||||
/*
|
||||
* HSB is a transform of RGB
|
||||
* Forward: https://en.wikipedia.org/wiki/HSL_and_HSV#From_RGB
|
||||
* Reverse: https://en.wikipedia.org/wiki/HSL_and_HSV#HSV_to_RGB
|
||||
*/
|
||||
|
||||
internal static Hsb FromRgb(Rgb rgb)
|
||||
{
|
||||
var (r, g, b) = rgb.ConstrainedTriplet;
|
||||
var components = new[] { r, g, b };
|
||||
var xMax = components.Max();
|
||||
var xMin = components.Min();
|
||||
var chroma = xMax - xMin;
|
||||
|
||||
double hue;
|
||||
if (chroma == 0.0) hue = 0;
|
||||
else if (xMax == r) hue = 60 * (0 + (g - b) / chroma);
|
||||
else if (xMax == g) hue = 60 * (2 + (b - r) / chroma);
|
||||
else if (xMax == b) hue = 60 * (4 + (r - g) / chroma);
|
||||
else hue = double.NaN;
|
||||
var brightness = xMax;
|
||||
var saturation = brightness == 0 ? 0 : chroma / brightness;
|
||||
return new Hsb(hue.Modulo(360.0), saturation, brightness, ColourHeritage.From(rgb));
|
||||
}
|
||||
|
||||
internal static Rgb ToRgb(Hsb hsb)
|
||||
{
|
||||
var (hue, saturation, brightness) = hsb.ConstrainedTriplet;
|
||||
var chroma = brightness * saturation;
|
||||
var h = hue / 60;
|
||||
var x = chroma * (1 - Math.Abs(h % 2 - 1));
|
||||
|
||||
var (r, g, b) = h switch
|
||||
{
|
||||
< 1 => (chroma, x, 0.0),
|
||||
< 2 => (x, chroma, 0.0),
|
||||
< 3 => (0.0, chroma, x),
|
||||
< 4 => (0.0, x, chroma),
|
||||
< 5 => (x, 0.0, chroma),
|
||||
< 6 => (chroma, 0.0, x),
|
||||
_ => (0.0, 0.0, 0.0)
|
||||
};
|
||||
|
||||
var m = brightness - chroma;
|
||||
var (red, green, blue) = (r + m, g + m, b + m);
|
||||
return new Rgb(red, green, blue, ColourHeritage.From(hsb));
|
||||
}
|
||||
}
|
||||
}
|
61
Unicolour/Hsl.cs
Normal file
61
Unicolour/Hsl.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
using System;
|
||||
|
||||
namespace Wacton.Unicolour
|
||||
{
|
||||
|
||||
public record Hsl : ColourRepresentation
|
||||
{
|
||||
protected override int? HueIndex => 0;
|
||||
public double H => First;
|
||||
public double S => Second;
|
||||
public double L => Third;
|
||||
public double ConstrainedH => ConstrainedFirst;
|
||||
public double ConstrainedS => ConstrainedSecond;
|
||||
public double ConstrainedL => ConstrainedThird;
|
||||
protected override double ConstrainedFirst => H.Modulo(360.0);
|
||||
protected override double ConstrainedSecond => S.Clamp(0.0, 1.0);
|
||||
protected override double ConstrainedThird => L.Clamp(0.0, 1.0);
|
||||
internal override bool IsGreyscale => S <= 0.0 || L is <= 0.0 or >= 1.0;
|
||||
|
||||
public Hsl(double h, double s, double l) : this(h, s, l, ColourHeritage.None)
|
||||
{
|
||||
}
|
||||
|
||||
internal Hsl(double h, double s, double l, ColourHeritage heritage) : base(h, s, l, heritage)
|
||||
{
|
||||
}
|
||||
|
||||
protected override string FirstString => UseAsHued ? $"{H:F1}°" : "—°";
|
||||
protected override string SecondString => $"{S * 100:F1}%";
|
||||
protected override string ThirdString => $"{L * 100:F1}%";
|
||||
public override string ToString() => base.ToString();
|
||||
|
||||
/*
|
||||
* HSL is a transform of HSB (in terms of Unicolour implementation)
|
||||
* Forward: https://en.wikipedia.org/wiki/HSL_and_HSV#HSV_to_HSL
|
||||
* Reverse: https://en.wikipedia.org/wiki/HSL_and_HSV#HSL_to_HSV
|
||||
*/
|
||||
|
||||
internal static Hsl FromHsb(Hsb hsb)
|
||||
{
|
||||
var (hue, hsbSaturation, brightness) = hsb.ConstrainedTriplet;
|
||||
var lightness = brightness * (1 - hsbSaturation / 2);
|
||||
var saturation = lightness is > 0.0 and < 1.0
|
||||
? (brightness - lightness) / Math.Min(lightness, 1 - lightness)
|
||||
: 0;
|
||||
|
||||
return new Hsl(hue, saturation, lightness, ColourHeritage.From(hsb));
|
||||
}
|
||||
|
||||
internal static Hsb ToHsb(Hsl hsl)
|
||||
{
|
||||
var (hue, hslSaturation, lightness) = hsl.ConstrainedTriplet;
|
||||
var brightness = lightness + hslSaturation * Math.Min(lightness, 1 - lightness);
|
||||
var saturation = brightness > 0.0
|
||||
? 2 * (1 - lightness / brightness)
|
||||
: 0;
|
||||
|
||||
return new Hsb(hue, saturation, brightness, ColourHeritage.From(hsl));
|
||||
}
|
||||
}
|
||||
}
|
95
Unicolour/Hsluv.cs
Normal file
95
Unicolour/Hsluv.cs
Normal file
@@ -0,0 +1,95 @@
|
||||
namespace Wacton.Unicolour
|
||||
{
|
||||
|
||||
public record Hsluv : ColourRepresentation
|
||||
{
|
||||
protected override int? HueIndex => 0;
|
||||
public double H => First;
|
||||
public double S => Second;
|
||||
public double L => Third;
|
||||
public double ConstrainedH => ConstrainedFirst;
|
||||
public double ConstrainedS => ConstrainedSecond;
|
||||
public double ConstrainedL => ConstrainedThird;
|
||||
protected override double ConstrainedFirst => H.Modulo(360.0);
|
||||
protected override double ConstrainedSecond => S.Clamp(0.0, 100.0);
|
||||
protected override double ConstrainedThird => L.Clamp(0.0, 100.0);
|
||||
internal override bool IsGreyscale => S <= 0.0 || L is <= 0.0 or >= 100.0;
|
||||
|
||||
public Hsluv(double h, double s, double l) : this(h, s, l, ColourHeritage.None)
|
||||
{
|
||||
}
|
||||
|
||||
internal Hsluv(double h, double s, double l, ColourHeritage heritage) : base(h, s, l, heritage)
|
||||
{
|
||||
}
|
||||
|
||||
protected override string FirstString => UseAsHued ? $"{H:F1}°" : "—°";
|
||||
protected override string SecondString => $"{S:F1}%";
|
||||
protected override string ThirdString => $"{L:F1}%";
|
||||
public override string ToString() => base.ToString();
|
||||
|
||||
/*
|
||||
* HSLUV is a transform of LCHUV
|
||||
* Forward: https://github.com/hsluv/hsluv-haxe/blob/master/src/hsluv/Hsluv.hx#L363
|
||||
* Reverse: https://github.com/hsluv/hsluv-haxe/blob/master/src/hsluv/Hsluv.hx#L346
|
||||
*/
|
||||
|
||||
internal static Hsluv FromLchuv(Lchuv lchuv)
|
||||
{
|
||||
var (lchLightness, chroma, hue) = lchuv.ConstrainedTriplet;
|
||||
double saturation;
|
||||
double lightness;
|
||||
|
||||
switch (lchLightness)
|
||||
{
|
||||
case > 99.9999999:
|
||||
saturation = 0.0;
|
||||
lightness = 100.0;
|
||||
break;
|
||||
case < 0.00000001:
|
||||
saturation = 0.0;
|
||||
lightness = 0.0;
|
||||
break;
|
||||
default:
|
||||
{
|
||||
var maxChroma = BoundingLines.CalculateMaxChroma(lchLightness, hue);
|
||||
saturation = chroma / maxChroma * 100;
|
||||
lightness = lchLightness;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return new Hsluv(hue, saturation, lightness, ColourHeritage.From(lchuv));
|
||||
}
|
||||
|
||||
internal static Lchuv ToLchuv(Hsluv hsluv)
|
||||
{
|
||||
var hue = hsluv.ConstrainedH;
|
||||
var saturation = hsluv.S;
|
||||
var hslLightness = hsluv.L;
|
||||
double lightness;
|
||||
double chroma;
|
||||
|
||||
switch (hslLightness)
|
||||
{
|
||||
case > 99.9999999:
|
||||
lightness = 100.0;
|
||||
chroma = 0.0;
|
||||
break;
|
||||
case < 0.00000001:
|
||||
lightness = 0.0;
|
||||
chroma = 0.0;
|
||||
break;
|
||||
default:
|
||||
{
|
||||
var maxChroma = BoundingLines.CalculateMaxChroma(hslLightness, hue);
|
||||
chroma = maxChroma / 100 * saturation;
|
||||
lightness = hslLightness;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return new Lchuv(lightness, chroma, hue, ColourHeritage.From(hsluv));
|
||||
}
|
||||
}
|
||||
}
|
65
Unicolour/Hwb.cs
Normal file
65
Unicolour/Hwb.cs
Normal file
@@ -0,0 +1,65 @@
|
||||
namespace Wacton.Unicolour
|
||||
{
|
||||
|
||||
public record Hwb : ColourRepresentation
|
||||
{
|
||||
protected override int? HueIndex => 0;
|
||||
public double H => First;
|
||||
public double W => Second;
|
||||
public double B => Third;
|
||||
public double ConstrainedH => ConstrainedFirst;
|
||||
public double ConstrainedW => ConstrainedSecond;
|
||||
public double ConstrainedB => ConstrainedThird;
|
||||
protected override double ConstrainedFirst => H.Modulo(360.0);
|
||||
protected override double ConstrainedSecond => W.Clamp(0.0, 1.0);
|
||||
protected override double ConstrainedThird => B.Clamp(0.0, 1.0);
|
||||
internal override bool IsGreyscale => ConstrainedW + ConstrainedB >= 1.0;
|
||||
|
||||
public Hwb(double h, double w, double b) : this(h, w, b, ColourHeritage.None)
|
||||
{
|
||||
}
|
||||
|
||||
internal Hwb(double h, double w, double b, ColourHeritage heritage) : base(h, w, b, heritage)
|
||||
{
|
||||
}
|
||||
|
||||
protected override string FirstString => UseAsHued ? $"{H:F1}°" : "—°";
|
||||
protected override string SecondString => $"{W * 100:F1}%";
|
||||
protected override string ThirdString => $"{B * 100:F1}%";
|
||||
public override string ToString() => base.ToString();
|
||||
|
||||
/*
|
||||
* HWB is a transform of HSB (in terms of Unicolour implementation)
|
||||
* Forward: https://en.wikipedia.org/wiki/HWB_color_model#Conversion
|
||||
* Reverse: https://en.wikipedia.org/wiki/HWB_color_model#Conversion
|
||||
*/
|
||||
|
||||
internal static Hwb FromHsb(Hsb hsb)
|
||||
{
|
||||
var (hue, s, b) = hsb.ConstrainedTriplet;
|
||||
var whiteness = (1 - s) * b;
|
||||
var blackness = 1 - b;
|
||||
return new Hwb(hue, whiteness, blackness, ColourHeritage.From(hsb));
|
||||
}
|
||||
|
||||
internal static Hsb ToHsb(Hwb hwb)
|
||||
{
|
||||
var (hue, w, b) = hwb.ConstrainedTriplet;
|
||||
|
||||
double brightness;
|
||||
double saturation;
|
||||
if (hwb.IsGreyscale)
|
||||
{
|
||||
brightness = w / (w + b);
|
||||
saturation = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
brightness = 1 - b;
|
||||
saturation = brightness == 0.0 ? 0 : 1 - w / brightness;
|
||||
}
|
||||
|
||||
return new Hsb(hue, saturation, brightness, ColourHeritage.From(hwb));
|
||||
}
|
||||
}
|
||||
}
|
76
Unicolour/Ictcp.cs
Normal file
76
Unicolour/Ictcp.cs
Normal file
@@ -0,0 +1,76 @@
|
||||
namespace Wacton.Unicolour
|
||||
{
|
||||
|
||||
public record Ictcp : ColourRepresentation
|
||||
{
|
||||
protected override int? HueIndex => null;
|
||||
public double I => First;
|
||||
public double Ct => Second;
|
||||
public double Cp => Third;
|
||||
|
||||
// no clear lightness upper-bound
|
||||
internal override bool IsGreyscale => I <= 0.0 || (Ct.Equals(0.0) && Cp.Equals(0.0));
|
||||
|
||||
public Ictcp(double i, double ct, double cp) : this(i, ct, cp, ColourHeritage.None)
|
||||
{
|
||||
}
|
||||
|
||||
internal Ictcp(ColourTriplet triplet, ColourHeritage heritage) : this(triplet.First, triplet.Second,
|
||||
triplet.Third, heritage)
|
||||
{
|
||||
}
|
||||
|
||||
internal Ictcp(double i, double ct, double cp, ColourHeritage heritage) : base(i, ct, cp, heritage)
|
||||
{
|
||||
}
|
||||
|
||||
protected override string FirstString => $"{I:F2}";
|
||||
protected override string SecondString => $"{Ct:+0.00;-0.00;0.00}";
|
||||
protected override string ThirdString => $"{Cp:+0.00;-0.00;0.00}";
|
||||
public override string ToString() => base.ToString();
|
||||
|
||||
/*
|
||||
* ICTCP is a transform of XYZ
|
||||
* Forward: https://professional.dolby.com/siteassets/pdfs/dolby-vision-measuring-perceptual-color-volume-v7.1.pdf
|
||||
* Reverse: not specified in the above paper; implementation unit tested to confirm roundtrip conversion
|
||||
* -------
|
||||
* currently only support PQ transfer function, not HLG (https://en.wikipedia.org/wiki/Hybrid_log%E2%80%93gamma)
|
||||
*/
|
||||
|
||||
private static readonly Matrix M1 = new(new[,]
|
||||
{
|
||||
{ 0.3593, 0.6976, -0.0359 },
|
||||
{ -0.1921, 1.1005, 0.0754 },
|
||||
{ 0.0071, 0.0748, 0.8433 }
|
||||
});
|
||||
|
||||
private static readonly Matrix M2 = new Matrix(new double[,]
|
||||
{
|
||||
{ 2048, 2048, 0 },
|
||||
{ 6610, -13613, 7003 },
|
||||
{ 17933, -17390, -543 }
|
||||
}).Scale(1 / 4096.0);
|
||||
|
||||
internal static Ictcp FromXyz(Xyz xyz, double ictcpScalar, XyzConfiguration xyzConfig)
|
||||
{
|
||||
var xyzMatrix = Matrix.FromTriplet(xyz.Triplet);
|
||||
var d65Matrix = Adaptation.WhitePoint(xyzMatrix, xyzConfig.WhitePoint, WhitePoint.From(Illuminant.D65));
|
||||
var d65ScaledMatrix = d65Matrix.Scale(ictcpScalar);
|
||||
var lmsMatrix = M1.Multiply(d65ScaledMatrix);
|
||||
var lmsPrimeMatrix = lmsMatrix.Select(Pq.Smpte.InverseEotf);
|
||||
var ictcpMatrix = M2.Multiply(lmsPrimeMatrix);
|
||||
return new Ictcp(ictcpMatrix.ToTriplet(), ColourHeritage.From(xyz));
|
||||
}
|
||||
|
||||
internal static Xyz ToXyz(Ictcp ictcp, double ictcpScalar, XyzConfiguration xyzConfig)
|
||||
{
|
||||
var ictcpMatrix = Matrix.FromTriplet(ictcp.Triplet);
|
||||
var lmsPrimeMatrix = M2.Inverse().Multiply(ictcpMatrix);
|
||||
var lmsMatrix = lmsPrimeMatrix.Select(Pq.Smpte.Eotf);
|
||||
var d65ScaledMatrix = lmsMatrix.Scale(1 / ictcpScalar);
|
||||
var d65Matrix = M1.Inverse().Multiply(d65ScaledMatrix);
|
||||
var xyzMatrix = Adaptation.WhitePoint(d65Matrix, WhitePoint.From(Illuminant.D65), xyzConfig.WhitePoint);
|
||||
return new Xyz(xyzMatrix.ToTriplet(), ColourHeritage.From(ictcp));
|
||||
}
|
||||
}
|
||||
}
|
123
Unicolour/Interpolation.cs
Normal file
123
Unicolour/Interpolation.cs
Normal file
@@ -0,0 +1,123 @@
|
||||
using System;
|
||||
|
||||
namespace Wacton.Unicolour
|
||||
{
|
||||
|
||||
internal static class Interpolation
|
||||
{
|
||||
internal static Unicolour Mix(ColourSpace colourSpace, Unicolour startColour, Unicolour endColour,
|
||||
double distance, bool premultiplyAlpha)
|
||||
{
|
||||
GuardConfiguration(startColour, endColour);
|
||||
|
||||
var startRepresentation = startColour.GetRepresentation(colourSpace);
|
||||
var startAlpha = startColour.Alpha;
|
||||
var endRepresentation = endColour.GetRepresentation(colourSpace);
|
||||
var endAlpha = endColour.Alpha;
|
||||
|
||||
(ColourTriplet start, ColourTriplet end) = GetTripletsToInterpolate(
|
||||
(startRepresentation, startAlpha),
|
||||
(endRepresentation, endAlpha),
|
||||
premultiplyAlpha);
|
||||
|
||||
var triplet = InterpolateTriplet(start, end, distance).WithHueModulo();
|
||||
var alpha = Interpolate(startColour.Alpha.ConstrainedA, endColour.Alpha.ConstrainedA, distance);
|
||||
|
||||
if (premultiplyAlpha)
|
||||
{
|
||||
triplet = triplet.WithUnpremultipliedAlpha(alpha);
|
||||
}
|
||||
|
||||
var heritage = ColourHeritage.From(startRepresentation, endRepresentation);
|
||||
var (first, second, third) = triplet;
|
||||
return new Unicolour(colourSpace, startColour.Config, heritage, first, second, third, alpha);
|
||||
}
|
||||
|
||||
private static (ColourTriplet start, ColourTriplet end) GetTripletsToInterpolate(
|
||||
(ColourRepresentation representation, Alpha alpha) start,
|
||||
(ColourRepresentation representation, Alpha alpha) end,
|
||||
bool premultiplyAlpha)
|
||||
{
|
||||
ColourTriplet startTriplet;
|
||||
ColourTriplet endTriplet;
|
||||
|
||||
// these can't give different answers since they use the same colour space
|
||||
// (except by reflection, in which case an error would be thrown when later trying to read the hue component)
|
||||
var hasHueComponent = start.representation.HasHueComponent || end.representation.HasHueComponent;
|
||||
if (hasHueComponent)
|
||||
{
|
||||
(startTriplet, endTriplet) = GetTripletsWithHue(start.representation, end.representation);
|
||||
}
|
||||
else
|
||||
{
|
||||
startTriplet = start.representation.Triplet;
|
||||
endTriplet = end.representation.Triplet;
|
||||
}
|
||||
|
||||
if (premultiplyAlpha)
|
||||
{
|
||||
startTriplet = startTriplet.WithPremultipliedAlpha(start.alpha.ConstrainedA);
|
||||
endTriplet = endTriplet.WithPremultipliedAlpha(end.alpha.ConstrainedA);
|
||||
}
|
||||
|
||||
return (startTriplet, endTriplet);
|
||||
}
|
||||
|
||||
private static (ColourTriplet start, ColourTriplet end) GetTripletsWithHue(
|
||||
ColourRepresentation startRepresentation, ColourRepresentation endRepresentation)
|
||||
{
|
||||
var startTriplet = startRepresentation.Triplet;
|
||||
var endTriplet = endRepresentation.Triplet;
|
||||
|
||||
(ColourTriplet, ColourTriplet) HueResult(double startHue, double endHue) => (
|
||||
startTriplet.WithHueOverride(startHue),
|
||||
endTriplet.WithHueOverride(endHue));
|
||||
|
||||
var startHasHue = startRepresentation.UseAsHued;
|
||||
var endHasHue = endRepresentation.UseAsHued;
|
||||
var ignoreHue = !startHasHue && !endHasHue;
|
||||
|
||||
// don't change hue if one colour is greyscale (e.g. black n/a° to green 120° should always stay at hue 120°)
|
||||
var startHue = ignoreHue || startHasHue ? startTriplet.HueValue() : endTriplet.HueValue();
|
||||
var endHue = ignoreHue || endHasHue ? endTriplet.HueValue() : startTriplet.HueValue();
|
||||
|
||||
if (startHue > endHue)
|
||||
{
|
||||
var endViaZero = endHue + 360;
|
||||
var interpolateViaZero = Math.Abs(startHue - endViaZero) < Math.Abs(startHue - endHue);
|
||||
return HueResult(startHue, interpolateViaZero ? endViaZero : endHue);
|
||||
}
|
||||
|
||||
if (endHue > startHue)
|
||||
{
|
||||
var startViaZero = startHue + 360;
|
||||
var interpolateViaZero = Math.Abs(endHue - startViaZero) < Math.Abs(endHue - startHue);
|
||||
return HueResult(interpolateViaZero ? startViaZero : startHue, endHue);
|
||||
}
|
||||
|
||||
return HueResult(startHue, endHue);
|
||||
}
|
||||
|
||||
private static ColourTriplet InterpolateTriplet(ColourTriplet start, ColourTriplet end, double distance)
|
||||
{
|
||||
var first = Interpolate(start.First, end.First, distance);
|
||||
var second = Interpolate(start.Second, end.Second, distance);
|
||||
var third = Interpolate(start.Third, end.Third, distance);
|
||||
return new(first, second, third, start.HueIndex);
|
||||
}
|
||||
|
||||
internal static double Interpolate(double startValue, double endValue, double distance)
|
||||
{
|
||||
var difference = endValue - startValue;
|
||||
return startValue + (difference * distance);
|
||||
}
|
||||
|
||||
private static void GuardConfiguration(Unicolour unicolour1, Unicolour unicolour2)
|
||||
{
|
||||
if (unicolour1.Config != unicolour2.Config)
|
||||
{
|
||||
throw new InvalidOperationException("Can only mix unicolours with the same configuration reference");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
98
Unicolour/Jzazbz.cs
Normal file
98
Unicolour/Jzazbz.cs
Normal file
@@ -0,0 +1,98 @@
|
||||
using System;
|
||||
|
||||
namespace Wacton.Unicolour
|
||||
{
|
||||
|
||||
public record Jzazbz : ColourRepresentation
|
||||
{
|
||||
protected override int? HueIndex => null;
|
||||
public double J => First;
|
||||
public double A => Second;
|
||||
public double B => Third;
|
||||
|
||||
// based on the figures from the paper, greyscale behaviour is the same as LAB
|
||||
// i.e. non-lightness axes are zero
|
||||
// but no clear lightness upper-bound
|
||||
internal override bool IsGreyscale => J <= 0.0 || (A.Equals(0.0) && B.Equals(0.0));
|
||||
|
||||
public Jzazbz(double j, double a, double b) : this(j, a, b, ColourHeritage.None)
|
||||
{
|
||||
}
|
||||
|
||||
internal Jzazbz(double j, double a, double b, ColourHeritage heritage) : base(j, a, b, heritage)
|
||||
{
|
||||
}
|
||||
|
||||
protected override string FirstString => $"{J:F3}";
|
||||
protected override string SecondString => $"{A:+0.000;-0.000;0.000}";
|
||||
protected override string ThirdString => $"{B:+0.000;-0.000;0.000}";
|
||||
public override string ToString() => base.ToString();
|
||||
|
||||
/*
|
||||
* JZAZBZ is a transform of XYZ
|
||||
* Forward: https://doi.org/10.1364/OE.25.015131
|
||||
* Reverse: https://doi.org/10.1364/OE.25.015131
|
||||
* -------
|
||||
* also useful: https://opticapublishing.figshare.com/articles/software/JzAzBz_m/5016299
|
||||
*/
|
||||
|
||||
// ReSharper disable InconsistentNaming
|
||||
private const double b = 1.15;
|
||||
private const double g = 0.66;
|
||||
private const double d = -0.56;
|
||||
private const double d0 = 1.6295499532821566e-11;
|
||||
// ReSharper restore InconsistentNaming
|
||||
|
||||
private static readonly Matrix M1 = new(new[,]
|
||||
{
|
||||
{ +0.41478972, +0.579999, +0.0146480 },
|
||||
{ -0.2015100, +1.120649, +0.0531008 },
|
||||
{ -0.0166008, +0.264800, +0.6684799 }
|
||||
});
|
||||
|
||||
private static readonly Matrix M2 = new(new[,]
|
||||
{
|
||||
{ +0.5, +0.5, 0 },
|
||||
{ +3.524000, -4.066708, +0.542708 },
|
||||
{ +0.199076, +1.096799, -1.295875 }
|
||||
});
|
||||
|
||||
internal static Jzazbz FromXyz(Xyz xyz, double jzazbzScalar, XyzConfiguration xyzConfig)
|
||||
{
|
||||
var xyzMatrix = Matrix.FromTriplet(xyz.Triplet);
|
||||
var d65Matrix = Adaptation.WhitePoint(xyzMatrix, xyzConfig.WhitePoint, WhitePoint.From(Illuminant.D65));
|
||||
var d65ScaledMatrix = d65Matrix.Scale(jzazbzScalar).Select(x => Math.Max(x, 0));
|
||||
var (x65, y65, z65) = d65ScaledMatrix.ToTriplet();
|
||||
|
||||
var x65Prime = b * x65 - (b - 1) * z65;
|
||||
var y65Prime = g * y65 - (g - 1) * x65;
|
||||
var xyz65PrimeMatrix = Matrix.FromTriplet(x65Prime, y65Prime, z65);
|
||||
var lmsMatrix = M1.Multiply(xyz65PrimeMatrix);
|
||||
var lmsPrimeMatrix = lmsMatrix.Select(Pq.Jzazbz.InverseEotf);
|
||||
var izazbzMatrix = M2.Multiply(lmsPrimeMatrix);
|
||||
|
||||
var (iz, az, bz) = izazbzMatrix.ToTriplet();
|
||||
var jz = (1 + d) * iz / (1 + d * iz) - d0;
|
||||
return new Jzazbz(jz, az, bz, ColourHeritage.From(xyz));
|
||||
}
|
||||
|
||||
internal static Xyz ToXyz(Jzazbz jzazbz, double jzazbzScalar, XyzConfiguration xyzConfig)
|
||||
{
|
||||
var (jz, az, bz) = jzazbz.Triplet;
|
||||
var iz = (jz + d0) / (1 + d - d * (jz + d0));
|
||||
var izazbzMatrix = Matrix.FromTriplet(iz, az, bz);
|
||||
var lmsPrimeMatrix = M2.Inverse().Multiply(izazbzMatrix);
|
||||
var lmsMatrix = lmsPrimeMatrix.Select(Pq.Jzazbz.Eotf);
|
||||
var xyz65PrimeMatrix = M1.Inverse().Multiply(lmsMatrix);
|
||||
var (x65Prime, y65Prime, z65Prime) = xyz65PrimeMatrix.ToTriplet();
|
||||
|
||||
var x65 = (x65Prime + (b - 1) * z65Prime) / b;
|
||||
var y65 = (y65Prime + (g - 1) * x65) / g;
|
||||
var z65 = z65Prime;
|
||||
var d65ScaledMatrix = Matrix.FromTriplet(x65, y65, z65);
|
||||
var d65Matrix = d65ScaledMatrix.Scale(1 / jzazbzScalar);
|
||||
var xyzMatrix = Adaptation.WhitePoint(d65Matrix, WhitePoint.From(Illuminant.D65), xyzConfig.WhitePoint);
|
||||
return new Xyz(xyzMatrix.ToTriplet(), ColourHeritage.From(jzazbz));
|
||||
}
|
||||
}
|
||||
}
|
50
Unicolour/Jzczhz.cs
Normal file
50
Unicolour/Jzczhz.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
namespace Wacton.Unicolour
|
||||
{
|
||||
|
||||
using static Utils;
|
||||
|
||||
public record Jzczhz : ColourRepresentation
|
||||
{
|
||||
protected override int? HueIndex => 2;
|
||||
public double J => First;
|
||||
public double C => Second;
|
||||
public double H => Third;
|
||||
public double ConstrainedH => ConstrainedThird;
|
||||
protected override double ConstrainedThird => H.Modulo(360.0);
|
||||
|
||||
// no clear lightness upper-bound
|
||||
// (paper says lightness J is 0 - 1 but seems like it's a scaling of their plot of Rec.2020 gamut - in my tests maxes out after ~0.17)
|
||||
internal override bool IsGreyscale => J <= 0.0 || C <= 0.0;
|
||||
|
||||
public Jzczhz(double j, double c, double h) : this(j, c, h, ColourHeritage.None)
|
||||
{
|
||||
}
|
||||
|
||||
internal Jzczhz(double j, double c, double h, ColourHeritage heritage) : base(j, c, h, heritage)
|
||||
{
|
||||
}
|
||||
|
||||
protected override string FirstString => $"{J:F3}";
|
||||
protected override string SecondString => $"{C:F3}";
|
||||
protected override string ThirdString => UseAsHued ? $"{H:F1}°" : "—°";
|
||||
public override string ToString() => base.ToString();
|
||||
|
||||
/*
|
||||
* JZCZHZ is a transform of JZAZBZ
|
||||
* Forward: https://en.wikipedia.org/wiki/CIELAB_color_space#CIEHLC_cylindrical_model
|
||||
* Reverse: https://en.wikipedia.org/wiki/CIELAB_color_space#CIEHLC_cylindrical_model
|
||||
*/
|
||||
|
||||
internal static Jzczhz FromJzazbz(Jzazbz jzazbz)
|
||||
{
|
||||
var (jz, cz, hz) = ToLchTriplet(jzazbz.J, jzazbz.A, jzazbz.B);
|
||||
return new Jzczhz(jz, cz, hz, ColourHeritage.From(jzazbz));
|
||||
}
|
||||
|
||||
internal static Jzazbz ToJzazbz(Jzczhz jzczhz)
|
||||
{
|
||||
var (jz, az, bz) = FromLchTriplet(jzczhz.ConstrainedTriplet);
|
||||
return new Jzazbz(jz, az, bz, ColourHeritage.From(jzczhz));
|
||||
}
|
||||
}
|
||||
}
|
21
Unicolour/LICENSE
Normal file
21
Unicolour/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022-2023 William Acton
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
67
Unicolour/Lab.cs
Normal file
67
Unicolour/Lab.cs
Normal file
@@ -0,0 +1,67 @@
|
||||
using System;
|
||||
|
||||
namespace Wacton.Unicolour
|
||||
{
|
||||
|
||||
using static Utils;
|
||||
|
||||
public record Lab : ColourRepresentation
|
||||
{
|
||||
protected override int? HueIndex => null;
|
||||
public double L => First;
|
||||
public double A => Second;
|
||||
public double B => Third;
|
||||
internal override bool IsGreyscale => L is <= 0.0 or >= 100.0 || (A.Equals(0.0) && B.Equals(0.0));
|
||||
|
||||
public Lab(double l, double a, double b) : this(l, a, b, ColourHeritage.None)
|
||||
{
|
||||
}
|
||||
|
||||
internal Lab(double l, double a, double b, ColourHeritage heritage) : base(l, a, b, heritage)
|
||||
{
|
||||
}
|
||||
|
||||
protected override string FirstString => $"{L:F2}";
|
||||
protected override string SecondString => $"{A:+0.00;-0.00;0.00}";
|
||||
protected override string ThirdString => $"{B:+0.00;-0.00;0.00}";
|
||||
public override string ToString() => base.ToString();
|
||||
|
||||
/*
|
||||
* LAB is a transform of XYZ
|
||||
* Forward: https://en.wikipedia.org/wiki/CIELAB_color_space#From_CIEXYZ_to_CIELAB
|
||||
* Reverse: https://en.wikipedia.org/wiki/CIELAB_color_space#From_CIELAB_to_CIEXYZ
|
||||
*/
|
||||
|
||||
// ReSharper disable InconsistentNaming
|
||||
private const double delta = 6.0 / 29.0;
|
||||
// ReSharper restore InconsistentNaming
|
||||
|
||||
internal static Lab FromXyz(Xyz xyz, XyzConfiguration xyzConfig)
|
||||
{
|
||||
var (x, y, z) = xyz.Triplet;
|
||||
var referenceWhite = xyzConfig.WhitePoint;
|
||||
var xRatio = x * 100 / referenceWhite.X;
|
||||
var yRatio = y * 100 / referenceWhite.Y;
|
||||
var zRatio = z * 100 / referenceWhite.Z;
|
||||
|
||||
double F(double t) =>
|
||||
t > Math.Pow(delta, 3) ? CubeRoot(t) : t * (1 / 3.0) * Math.Pow(delta, -2) + 4.0 / 29.0;
|
||||
|
||||
var l = 116 * F(yRatio) - 16;
|
||||
var a = 500 * (F(xRatio) - F(yRatio));
|
||||
var b = 200 * (F(yRatio) - F(zRatio));
|
||||
return new Lab(l, a, b, ColourHeritage.From(xyz));
|
||||
}
|
||||
|
||||
internal static Xyz ToXyz(Lab lab, XyzConfiguration xyzConfig)
|
||||
{
|
||||
var (l, a, b) = lab.Triplet;
|
||||
var referenceWhite = xyzConfig.WhitePoint;
|
||||
double F(double t) => t > delta ? Math.Pow(t, 3.0) : 3 * Math.Pow(delta, 2) * (t - 4.0 / 29.0);
|
||||
var x = referenceWhite.X / 100.0 * F((l + 16) / 116.0 + a / 500.0);
|
||||
var y = referenceWhite.Y / 100.0 * F((l + 16) / 116.0);
|
||||
var z = referenceWhite.Z / 100.0 * F((l + 16) / 116.0 - b / 200.0);
|
||||
return new Xyz(x, y, z, ColourHeritage.From(lab));
|
||||
}
|
||||
}
|
||||
}
|
47
Unicolour/Lchab.cs
Normal file
47
Unicolour/Lchab.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
namespace Wacton.Unicolour
|
||||
{
|
||||
|
||||
using static Utils;
|
||||
|
||||
public record Lchab : ColourRepresentation
|
||||
{
|
||||
protected override int? HueIndex => 2;
|
||||
public double L => First;
|
||||
public double C => Second;
|
||||
public double H => Third;
|
||||
public double ConstrainedH => ConstrainedThird;
|
||||
protected override double ConstrainedThird => H.Modulo(360.0);
|
||||
internal override bool IsGreyscale => L is <= 0.0 or >= 100.0 || C <= 0.0;
|
||||
|
||||
public Lchab(double l, double c, double h) : this(l, c, h, ColourHeritage.None)
|
||||
{
|
||||
}
|
||||
|
||||
internal Lchab(double l, double c, double h, ColourHeritage heritage) : base(l, c, h, heritage)
|
||||
{
|
||||
}
|
||||
|
||||
protected override string FirstString => $"{L:F2}";
|
||||
protected override string SecondString => $"{C:F2}";
|
||||
protected override string ThirdString => UseAsHued ? $"{H:F1}°" : "—°";
|
||||
public override string ToString() => base.ToString();
|
||||
|
||||
/*
|
||||
* LCHAB is a transform of LAB
|
||||
* Forward: https://en.wikipedia.org/wiki/CIELAB_color_space#CIEHLC_cylindrical_model
|
||||
* Reverse: https://en.wikipedia.org/wiki/CIELAB_color_space#CIEHLC_cylindrical_model
|
||||
*/
|
||||
|
||||
internal static Lchab FromLab(Lab lab)
|
||||
{
|
||||
var (l, c, h) = ToLchTriplet(lab.L, lab.A, lab.B);
|
||||
return new Lchab(l, c, h, ColourHeritage.From(lab));
|
||||
}
|
||||
|
||||
internal static Lab ToLab(Lchab lchab)
|
||||
{
|
||||
var (l, a, b) = FromLchTriplet(lchab.ConstrainedTriplet);
|
||||
return new Lab(l, a, b, ColourHeritage.From(lchab));
|
||||
}
|
||||
}
|
||||
}
|
47
Unicolour/Lchuv.cs
Normal file
47
Unicolour/Lchuv.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
namespace Wacton.Unicolour
|
||||
{
|
||||
|
||||
using static Utils;
|
||||
|
||||
public record Lchuv : ColourRepresentation
|
||||
{
|
||||
protected override int? HueIndex => 2;
|
||||
public double L => First;
|
||||
public double C => Second;
|
||||
public double H => Third;
|
||||
public double ConstrainedH => ConstrainedThird;
|
||||
protected override double ConstrainedThird => H.Modulo(360.0);
|
||||
internal override bool IsGreyscale => L is <= 0.0 or >= 100.0 || C <= 0.0;
|
||||
|
||||
public Lchuv(double l, double c, double h) : this(l, c, h, ColourHeritage.None)
|
||||
{
|
||||
}
|
||||
|
||||
internal Lchuv(double l, double c, double h, ColourHeritage heritage) : base(l, c, h, heritage)
|
||||
{
|
||||
}
|
||||
|
||||
protected override string FirstString => $"{L:F2}";
|
||||
protected override string SecondString => $"{C:F2}";
|
||||
protected override string ThirdString => UseAsHued ? $"{H:F1}°" : "—°";
|
||||
public override string ToString() => base.ToString();
|
||||
|
||||
/*
|
||||
* LCHUV is a transform of LUV
|
||||
* Forward: https://en.wikipedia.org/wiki/CIELAB_color_space#CIEHLC_cylindrical_model
|
||||
* Reverse: https://en.wikipedia.org/wiki/CIELAB_color_space#CIEHLC_cylindrical_model
|
||||
*/
|
||||
|
||||
internal static Lchuv FromLuv(Luv luv)
|
||||
{
|
||||
var (l, c, h) = ToLchTriplet(luv.L, luv.U, luv.V);
|
||||
return new Lchuv(l, c, h, ColourHeritage.From(luv));
|
||||
}
|
||||
|
||||
internal static Luv ToLuv(Lchuv lchuv)
|
||||
{
|
||||
var (l, u, v) = FromLchTriplet(lchuv.ConstrainedTriplet);
|
||||
return new Luv(l, u, v, ColourHeritage.From(lchuv));
|
||||
}
|
||||
}
|
||||
}
|
76
Unicolour/Luv.cs
Normal file
76
Unicolour/Luv.cs
Normal file
@@ -0,0 +1,76 @@
|
||||
using System;
|
||||
|
||||
namespace Wacton.Unicolour
|
||||
{
|
||||
|
||||
using static Utils;
|
||||
|
||||
public record Luv : ColourRepresentation
|
||||
{
|
||||
protected override int? HueIndex => null;
|
||||
public double L => First;
|
||||
public double U => Second;
|
||||
public double V => Third;
|
||||
internal override bool IsGreyscale => L is <= 0.0 or >= 100.0 || (U.Equals(0.0) && V.Equals(0.0));
|
||||
|
||||
public Luv(double l, double u, double v) : this(l, u, v, ColourHeritage.None)
|
||||
{
|
||||
}
|
||||
|
||||
internal Luv(double l, double u, double v, ColourHeritage heritage) : base(l, u, v, heritage)
|
||||
{
|
||||
}
|
||||
|
||||
protected override string FirstString => $"{L:F2}";
|
||||
protected override string SecondString => $"{U:+0.00;-0.00;0.00}";
|
||||
protected override string ThirdString => $"{V:+0.00;-0.00;0.00}";
|
||||
public override string ToString() => base.ToString();
|
||||
|
||||
/*
|
||||
* LUV is a transform of XYZ
|
||||
* Forward: https://en.wikipedia.org/wiki/CIELUV#The_forward_transformation
|
||||
* Reverse: https://en.wikipedia.org/wiki/CIELUV#The_reverse_transformation
|
||||
*/
|
||||
|
||||
internal static Luv FromXyz(Xyz xyz, XyzConfiguration xyzConfig)
|
||||
{
|
||||
var (x, y, z) = xyz.Triplet;
|
||||
var (xRef, yRef, zRef) = xyzConfig.WhitePoint;
|
||||
|
||||
double U(double xu, double yu, double zu) => 4 * xu / (xu + 15 * yu + 3 * zu);
|
||||
double V(double xv, double yv, double zv) => 9 * yv / (xv + 15 * yv + 3 * zv);
|
||||
var uPrime = U(x * 100, y * 100, z * 100);
|
||||
var uPrimeRef = U(xRef, yRef, zRef);
|
||||
var vPrime = V(x * 100, y * 100, z * 100);
|
||||
var vPrimeRef = V(xRef, yRef, zRef);
|
||||
|
||||
var yRatio = y * 100 / yRef;
|
||||
var l = yRatio > Math.Pow(6.0 / 29.0, 3) ? 116 * CubeRoot(yRatio) - 16 : Math.Pow(29 / 3.0, 3) * yRatio;
|
||||
var u = 13 * l * (uPrime - uPrimeRef);
|
||||
var v = 13 * l * (vPrime - vPrimeRef);
|
||||
|
||||
double ZeroNaN(double value) => double.IsNaN(value) ? 0.0 : value;
|
||||
return new Luv(ZeroNaN(l), ZeroNaN(u), ZeroNaN(v), ColourHeritage.From(xyz));
|
||||
}
|
||||
|
||||
internal static Xyz ToXyz(Luv luv, XyzConfiguration xyzConfig)
|
||||
{
|
||||
var (l, u, v) = luv.Triplet;
|
||||
double U(double x, double y, double z) => 4 * x / (x + 15 * y + 3 * z);
|
||||
double V(double x, double y, double z) => 9 * y / (x + 15 * y + 3 * z);
|
||||
|
||||
var (xRef, yRef, zRef) = xyzConfig.WhitePoint;
|
||||
var uPrimeRef = U(xRef, yRef, zRef);
|
||||
var uPrime = u / (13 * l) + uPrimeRef;
|
||||
var vPrimeRef = V(xRef, yRef, zRef);
|
||||
var vPrime = v / (13 * l) + vPrimeRef;
|
||||
|
||||
var y = (l > 8 ? yRef * Math.Pow((l + 16) / 116.0, 3) : yRef * l * Math.Pow(3 / 29.0, 3)) / 100.0;
|
||||
var x = y * ((9 * uPrime) / (4 * vPrime));
|
||||
var z = y * ((12 - 3 * uPrime - 20 * vPrime) / (4 * vPrime));
|
||||
|
||||
double ZeroNaN(double value) => double.IsNaN(value) || double.IsInfinity(value) ? 0.0 : value;
|
||||
return new Xyz(ZeroNaN(x), ZeroNaN(y), ZeroNaN(z), ColourHeritage.From(luv));
|
||||
}
|
||||
}
|
||||
}
|
126
Unicolour/Matrix.cs
Normal file
126
Unicolour/Matrix.cs
Normal file
@@ -0,0 +1,126 @@
|
||||
using System;
|
||||
|
||||
namespace Wacton.Unicolour
|
||||
{
|
||||
|
||||
internal class Matrix
|
||||
{
|
||||
internal double[,] Data { get; }
|
||||
internal double this[int row, int col] => Data[row, col];
|
||||
private int Rows => Data.GetLength(0);
|
||||
private int Cols => Data.GetLength(1);
|
||||
|
||||
internal Matrix(double[,] data)
|
||||
{
|
||||
Data = data;
|
||||
}
|
||||
|
||||
internal Matrix Multiply(Matrix other)
|
||||
{
|
||||
if (other.Rows != Cols)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"Cannot multiply {this} matrix by {other} matrix, incompatible dimensions");
|
||||
}
|
||||
|
||||
var dimension = Cols;
|
||||
var rows = Rows;
|
||||
var cols = other.Cols;
|
||||
|
||||
var result = new double[rows, cols];
|
||||
for (var row = 0; row < rows; row++)
|
||||
{
|
||||
for (var col = 0; col < cols; col++)
|
||||
{
|
||||
result[row, col] = 0;
|
||||
for (var i = 0; i < dimension; i++)
|
||||
{
|
||||
result[row, col] += this[row, i] * other[i, col];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new Matrix(result);
|
||||
}
|
||||
|
||||
internal Matrix Inverse()
|
||||
{
|
||||
if (Rows != 3 || Cols != 3)
|
||||
{
|
||||
throw new InvalidOperationException("Only inverse of 3x3 matrix is supported");
|
||||
}
|
||||
|
||||
var a = this[0, 0];
|
||||
var b = this[0, 1];
|
||||
var c = this[0, 2];
|
||||
var d = this[1, 0];
|
||||
var e = this[1, 1];
|
||||
var f = this[1, 2];
|
||||
var g = this[2, 0];
|
||||
var h = this[2, 1];
|
||||
var i = this[2, 2];
|
||||
|
||||
var determinant = a * e * i + b * f * g + c * d * h - c * e * g - a * f * h - b * d * i;
|
||||
var adjugate = new[,]
|
||||
{
|
||||
{ e * i - f * h, h * c - i * b, b * f - c * e },
|
||||
{ g * f - d * i, a * i - g * c, d * c - a * f },
|
||||
{ d * h - g * e, g * b - a * h, a * e - d * b }
|
||||
};
|
||||
|
||||
var inverse = new double[3, 3];
|
||||
for (var row = 0; row < 3; row++)
|
||||
{
|
||||
for (var col = 0; col < 3; col++)
|
||||
{
|
||||
inverse[row, col] = determinant == 0
|
||||
? double.NaN
|
||||
: adjugate[row, col] / determinant;
|
||||
}
|
||||
}
|
||||
|
||||
return new Matrix(inverse);
|
||||
}
|
||||
|
||||
internal Matrix Scale(double scalar) => Select(x => x * scalar);
|
||||
|
||||
internal Matrix Select(Func<double, double> operation)
|
||||
{
|
||||
var result = new double[Rows, Cols];
|
||||
for (var row = 0; row < Rows; row++)
|
||||
{
|
||||
for (var col = 0; col < Cols; col++)
|
||||
{
|
||||
result[row, col] = operation(this[row, col]);
|
||||
}
|
||||
}
|
||||
|
||||
return new Matrix(result);
|
||||
}
|
||||
|
||||
internal ColourTriplet ToTriplet()
|
||||
{
|
||||
if (Rows != 3 || Cols != 1)
|
||||
{
|
||||
throw new InvalidOperationException("Can only create triplet from 3x1 matrix");
|
||||
}
|
||||
|
||||
return new ColourTriplet(Data[0, 0], Data[1, 0], Data[2, 0]);
|
||||
}
|
||||
|
||||
internal static Matrix FromTriplet(ColourTriplet triplet) =>
|
||||
FromTriplet(triplet.First, triplet.Second, triplet.Third);
|
||||
|
||||
internal static Matrix FromTriplet(double first, double second, double third)
|
||||
{
|
||||
return new Matrix(new[,]
|
||||
{
|
||||
{ first },
|
||||
{ second },
|
||||
{ third }
|
||||
});
|
||||
}
|
||||
|
||||
public override string ToString() => $"{Rows}x{Cols}";
|
||||
}
|
||||
}
|
74
Unicolour/Oklab.cs
Normal file
74
Unicolour/Oklab.cs
Normal file
@@ -0,0 +1,74 @@
|
||||
using System;
|
||||
|
||||
namespace Wacton.Unicolour
|
||||
{
|
||||
|
||||
using static Utils;
|
||||
|
||||
public record Oklab : ColourRepresentation
|
||||
{
|
||||
protected override int? HueIndex => null;
|
||||
public double L => First;
|
||||
public double A => Second;
|
||||
public double B => Third;
|
||||
internal override bool IsGreyscale => L is <= 0.0 or >= 1.0 || (A.Equals(0.0) && B.Equals(0.0));
|
||||
|
||||
public Oklab(double l, double a, double b) : this(l, a, b, ColourHeritage.None)
|
||||
{
|
||||
}
|
||||
|
||||
internal Oklab(ColourTriplet triplet, ColourHeritage heritage) : this(triplet.First, triplet.Second,
|
||||
triplet.Third, heritage)
|
||||
{
|
||||
}
|
||||
|
||||
internal Oklab(double l, double a, double b, ColourHeritage heritage) : base(l, a, b, heritage)
|
||||
{
|
||||
}
|
||||
|
||||
protected override string FirstString => $"{L:F2}";
|
||||
protected override string SecondString => $"{A:+0.00;-0.00;0.00}";
|
||||
protected override string ThirdString => $"{B:+0.00;-0.00;0.00}";
|
||||
public override string ToString() => base.ToString();
|
||||
|
||||
/*
|
||||
* OKLAB is a transform of XYZ
|
||||
* Forward: https://bottosson.github.io/posts/oklab/#converting-from-xyz-to-oklab
|
||||
* Reverse: https://bottosson.github.io/posts/oklab/#converting-from-xyz-to-oklab
|
||||
*/
|
||||
|
||||
private static readonly Matrix M1 = new(new[,]
|
||||
{
|
||||
{ +0.8189330101, +0.3618667424, -0.1288597137 },
|
||||
{ +0.0329845436, +0.9293118715, +0.0361456387 },
|
||||
{ +0.0482003018, +0.2643662691, +0.6338517070 }
|
||||
});
|
||||
|
||||
private static readonly Matrix M2 = new(new[,]
|
||||
{
|
||||
{ +0.2104542553, +0.7936177850, -0.0040720468 },
|
||||
{ +1.9779984951, -2.4285922050, +0.4505937099 },
|
||||
{ +0.0259040371, +0.7827717662, -0.8086757660 }
|
||||
});
|
||||
|
||||
internal static Oklab FromXyz(Xyz xyz, XyzConfiguration xyzConfig)
|
||||
{
|
||||
var xyzMatrix = Matrix.FromTriplet(xyz.Triplet);
|
||||
var d65Matrix = Adaptation.WhitePoint(xyzMatrix, xyzConfig.WhitePoint, WhitePoint.From(Illuminant.D65));
|
||||
var lmsMatrix = M1.Multiply(d65Matrix);
|
||||
var lmsNonLinearMatrix = lmsMatrix.Select(CubeRoot);
|
||||
var labMatrix = M2.Multiply(lmsNonLinearMatrix);
|
||||
return new Oklab(labMatrix.ToTriplet(), ColourHeritage.From(xyz));
|
||||
}
|
||||
|
||||
internal static Xyz ToXyz(Oklab oklab, XyzConfiguration xyzConfig)
|
||||
{
|
||||
var labMatrix = Matrix.FromTriplet(oklab.Triplet);
|
||||
var lmsNonLinearMatrix = M2.Inverse().Multiply(labMatrix);
|
||||
var lmsMatrix = lmsNonLinearMatrix.Select(x => Math.Pow(x, 3));
|
||||
var d65Matrix = M1.Inverse().Multiply(lmsMatrix);
|
||||
var xyzMatrix = Adaptation.WhitePoint(d65Matrix, WhitePoint.From(Illuminant.D65), xyzConfig.WhitePoint);
|
||||
return new Xyz(xyzMatrix.ToTriplet(), ColourHeritage.From(oklab));
|
||||
}
|
||||
}
|
||||
}
|
47
Unicolour/Oklch.cs
Normal file
47
Unicolour/Oklch.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
namespace Wacton.Unicolour
|
||||
{
|
||||
|
||||
using static Utils;
|
||||
|
||||
public record Oklch : ColourRepresentation
|
||||
{
|
||||
protected override int? HueIndex => 2;
|
||||
public double L => First;
|
||||
public double C => Second;
|
||||
public double H => Third;
|
||||
public double ConstrainedH => ConstrainedThird;
|
||||
protected override double ConstrainedThird => H.Modulo(360.0);
|
||||
internal override bool IsGreyscale => L is <= 0.0 or >= 1.0 || C <= 0.0;
|
||||
|
||||
public Oklch(double l, double c, double h) : this(l, c, h, ColourHeritage.None)
|
||||
{
|
||||
}
|
||||
|
||||
internal Oklch(double l, double c, double h, ColourHeritage heritage) : base(l, c, h, heritage)
|
||||
{
|
||||
}
|
||||
|
||||
protected override string FirstString => $"{L:F2}";
|
||||
protected override string SecondString => $"{C:F2}";
|
||||
protected override string ThirdString => UseAsHued ? $"{H:F1}°" : "—°";
|
||||
public override string ToString() => base.ToString();
|
||||
|
||||
/*
|
||||
* OKLCH is a transform of OKLAB
|
||||
* Forward: https://en.wikipedia.org/wiki/CIELAB_color_space#CIEHLC_cylindrical_model
|
||||
* Reverse: https://en.wikipedia.org/wiki/CIELAB_color_space#CIEHLC_cylindrical_model
|
||||
*/
|
||||
|
||||
internal static Oklch FromOklab(Oklab oklab)
|
||||
{
|
||||
var (l, c, h) = ToLchTriplet(oklab.L, oklab.A, oklab.B);
|
||||
return new Oklch(l, c, h, ColourHeritage.From(oklab));
|
||||
}
|
||||
|
||||
internal static Oklab ToOklab(Oklch oklch)
|
||||
{
|
||||
var (l, a, b) = FromLchTriplet(oklch.ConstrainedTriplet);
|
||||
return new Oklab(l, a, b, ColourHeritage.From(oklch));
|
||||
}
|
||||
}
|
||||
}
|
59
Unicolour/Pq.cs
Normal file
59
Unicolour/Pq.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
using System;
|
||||
|
||||
namespace Wacton.Unicolour
|
||||
{
|
||||
|
||||
internal static class Pq
|
||||
{
|
||||
internal static class Smpte
|
||||
{
|
||||
private const double M1 = 2610 / 16384.0;
|
||||
private const double M2 = 2523 / 4096.0 * 128;
|
||||
private const double C1 = 3424 / 4096.0;
|
||||
private const double C2 = 2413 / 4096.0 * 32;
|
||||
private const double C3 = 2392 / 4096.0 * 32;
|
||||
|
||||
internal static double Eotf(double ePrime)
|
||||
{
|
||||
var rootEPrime = Math.Pow(ePrime, 1 / M2);
|
||||
var top = Math.Max(rootEPrime - C1, 0);
|
||||
var bottom = C2 - C3 * rootEPrime;
|
||||
return 10000 * Math.Pow(top / bottom, 1 / M1);
|
||||
}
|
||||
|
||||
internal static double InverseEotf(double f)
|
||||
{
|
||||
var y = f / 10000.0;
|
||||
var powerY = Math.Pow(y, M1);
|
||||
var top = C1 + C2 * powerY;
|
||||
var bottom = 1 + C3 * powerY;
|
||||
return Math.Pow(top / bottom, M2);
|
||||
}
|
||||
}
|
||||
|
||||
internal static class Jzazbz
|
||||
{
|
||||
private static readonly double C1 = 3424 / Math.Pow(2, 12);
|
||||
private static readonly double C2 = 2413 / Math.Pow(2, 7);
|
||||
private static readonly double C3 = 2392 / Math.Pow(2, 7);
|
||||
private static readonly double N = 2610 / Math.Pow(2, 14);
|
||||
private static readonly double P = 1.7 * 2523 / Math.Pow(2, 5);
|
||||
|
||||
internal static double Eotf(double value)
|
||||
{
|
||||
var rootP = Math.Pow(value, 1 / P);
|
||||
var top = C1 - rootP;
|
||||
var bottom = C3 * rootP - C2;
|
||||
return 10000 * Math.Pow(top / bottom, 1 / N);
|
||||
}
|
||||
|
||||
internal static double InverseEotf(double value)
|
||||
{
|
||||
var powerN = Math.Pow(value / 10000.0, N);
|
||||
var top = C1 + C2 * powerN;
|
||||
var bottom = 1 + C3 * powerN;
|
||||
return Math.Pow(top / bottom, P);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
456
Unicolour/README.md
Normal file
456
Unicolour/README.md
Normal file
@@ -0,0 +1,456 @@
|
||||
# <img src="https://gitlab.com/Wacton/Unicolour/-/raw/main/Unicolour/Resources/Unicolour.png" width="32" height="32"> Unicolour
|
||||
[](https://gitlab.com/Wacton/Unicolour/-/commits/main)
|
||||
[](https://gitlab.com/Wacton/Unicolour/-/pipelines)
|
||||
[](https://gitlab.com/Wacton/Unicolour/-/pipelines)
|
||||
[](https://www.nuget.org/packages/Wacton.Unicolour/)
|
||||
|
||||
Unicolour is a .NET library written in C# for working with colour:
|
||||
- Colour space conversion
|
||||
- Colour mixing / colour interpolation
|
||||
- Colour comparison
|
||||
- Colour information
|
||||
- Colour gamut mapping
|
||||
|
||||
Targets [.NET Standard 2.0](https://docs.microsoft.com/en-us/dotnet/standard/net-standard?tabs=net-standard-2-0) for use in .NET 5.0+, .NET Core 2.0+ and .NET Framework 4.6.1+ applications.
|
||||
|
||||
**Contents**
|
||||
1. 🧭 [Overview](#-overview)
|
||||
2. ⚡ [Quickstart](#-quickstart)
|
||||
3. 🔦 [Features](#-features)
|
||||
4. 🌈 [How to use](#-how-to-use)
|
||||
5. ✨ [Examples](#-examples)
|
||||
6. 💡 [Configuration](#-configuration)
|
||||
7. 🔮 [Datasets](#-datasets)
|
||||
8. 🦺 [Work in progress](#-work-in-progress)
|
||||
|
||||
## 🧭 Overview
|
||||
A `Unicolour` encapsulates a single colour and its representation across different colour spaces.
|
||||
It supports:
|
||||
- RGB
|
||||
- Linear RGB
|
||||
- HSB/HSV
|
||||
- HSL
|
||||
- HWB
|
||||
- CIEXYZ
|
||||
- CIExyY
|
||||
- CIELAB
|
||||
- CIELCh<sub>ab</sub>
|
||||
- CIELUV
|
||||
- CIELCh<sub>uv</sub>
|
||||
- HSLuv
|
||||
- HPLuv
|
||||
- IC<sub>T</sub>C<sub>P</sub>
|
||||
- J<sub>z</sub>a<sub>z</sub>b<sub>z</sub>
|
||||
- J<sub>z</sub>C<sub>z</sub>h<sub>z</sub>
|
||||
- Oklab
|
||||
- Oklch
|
||||
- CIECAM02
|
||||
- CAM16
|
||||
- HCT
|
||||
|
||||
<details>
|
||||
<summary>Diagram of colour space relationships</summary>
|
||||
|
||||
```mermaid
|
||||
%%{
|
||||
init: {
|
||||
"theme": "base",
|
||||
"themeVariables": {
|
||||
"primaryColor": "#4C566A",
|
||||
"primaryTextColor": "#ECEFF4",
|
||||
"primaryBorderColor": "#2E3440",
|
||||
"lineColor": "#8FBCBB",
|
||||
"secondaryColor": "#404046",
|
||||
"tertiaryColor": "#404046"
|
||||
}
|
||||
}
|
||||
}%%
|
||||
|
||||
flowchart TD
|
||||
XYY(xyY)
|
||||
RGBLIN(Linear RGB)
|
||||
RGB(RGB)
|
||||
HSB(HSB)
|
||||
HSL(HSL)
|
||||
HWB(HWB)
|
||||
XYZ(XYZ)
|
||||
LAB(LAB)
|
||||
LCHAB(LCHab)
|
||||
LUV(LUV)
|
||||
LCHUV(LCHuv)
|
||||
HSLUV(HSLuv)
|
||||
HPLUV(HPLuv)
|
||||
ICTCP(ICtCp)
|
||||
JZAZBZ(JzAzBz)
|
||||
JZCZHZ(JzCzHz)
|
||||
OKLAB(Oklab)
|
||||
OKLCH(Oklch)
|
||||
CAM02(CAM02)
|
||||
CAM02UCS(CAM02-UCS)
|
||||
CAM16(CAM16)
|
||||
CAM16UCS(CAM16-UCS)
|
||||
HCT(HCT)
|
||||
|
||||
XYZ --> XYY
|
||||
XYZ --> RGBLIN
|
||||
RGBLIN --> RGB
|
||||
RGB --> HSB
|
||||
HSB --> HSL
|
||||
HSB --> HWB
|
||||
XYZ --> LAB
|
||||
LAB --> LCHAB
|
||||
XYZ --> LUV
|
||||
LUV --> LCHUV
|
||||
LCHUV --> HSLUV
|
||||
LCHUV --> HPLUV
|
||||
XYZ --> ICTCP
|
||||
XYZ --> JZAZBZ
|
||||
JZAZBZ --> JZCZHZ
|
||||
XYZ --> OKLAB
|
||||
OKLAB --> OKLCH
|
||||
XYZ --> CAM02
|
||||
CAM02 -.-> CAM02UCS
|
||||
XYZ --> CAM16
|
||||
CAM16 -.-> CAM16UCS
|
||||
XYZ --> HCT
|
||||
```
|
||||
|
||||
This diagram summarises how colour space conversions are implemented in Unicolour.
|
||||
Arrows indicate forward transformations from one space to another.
|
||||
For each forward transformation there is a corresponding reverse transformation.
|
||||
XYZ is considered the root colour space.
|
||||
</details>
|
||||
|
||||
This library was initially written for personal projects since existing libraries had complex APIs or missing features.
|
||||
The goal of this library is to be accurate, intuitive, and easy to use.
|
||||
Although performance is not a priority, conversions are only calculated once; when first evaluated (either on access or as part of an intermediate conversion step) the result is stored for future use.
|
||||
It is also [extensively tested](Unicolour.Tests), including verification of roundtrip conversions, validation using known colour values, and 100% line coverage and branch coverage.
|
||||
|
||||
## ⚡ Quickstart
|
||||
| Colour space | Create | Get |
|
||||
|-----------------------------------------|---------------------------------|----------------|
|
||||
| RGB (Hex) | `new(hex)` | `.Hex` |
|
||||
| RGB (0–255) | `new(ColourSpace.Rgb255, ⋯)` | `.Rgb.Byte255` |
|
||||
| RGB | `new(ColourSpace.Rgb, ⋯)` | `.Rgb` |
|
||||
| Linear RGB | `new(ColourSpace.RgbLinear, ⋯)` | `.RgbLinear` |
|
||||
| HSB/HSV | `new(ColourSpace.Hsb, ⋯)` | `.Hsb` |
|
||||
| HSL | `new(ColourSpace.Hsl, ⋯)` | `.Hsl` |
|
||||
| HWB | `new(ColourSpace.Hwb, ⋯)` | `.Hwb` |
|
||||
| CIEXYZ | `new(ColourSpace.Xyz, ⋯)` | `.Xyz` |
|
||||
| CIExyY | `new(ColourSpace.Xyy, ⋯)` | `.Xyy` |
|
||||
| CIELAB | `new(ColourSpace.Lab, ⋯)` | `.Lab` |
|
||||
| CIELCh<sub>ab</sub> | `new(ColourSpace.Lchab, ⋯)` | `.Lchab` |
|
||||
| CIELUV | `new(ColourSpace.Luv, ⋯)` | `.Luv` |
|
||||
| CIELCh<sub>uv</sub> | `new(ColourSpace.Lchuv, ⋯)` | `.Lchuv` |
|
||||
| HSLuv | `new(ColourSpace.Hsluv, ⋯)` | `.Hsluv` |
|
||||
| HPLuv | `new(ColourSpace.Hpluv, ⋯)` | `.Hpluv` |
|
||||
| IC<sub>T</sub>C<sub>P</sub> | `new(ColourSpace.Ictcp, ⋯)` | `.Ictcp` |
|
||||
| J<sub>z</sub>a<sub>z</sub>b<sub>z</sub> | `new(ColourSpace.Jzazbz, ⋯)` | `.Jzazbz` |
|
||||
| J<sub>z</sub>C<sub>z</sub>h<sub>z</sub> | `new(ColourSpace.Jzczhz, ⋯)` | `.Jzczhz` |
|
||||
| Oklab | `new(ColourSpace.Oklab, ⋯)` | `.Oklab` |
|
||||
| Oklch | `new(ColourSpace.Oklch, ⋯)` | `.Oklch` |
|
||||
| CIECAM02 | `new(ColourSpace.Cam02, ⋯)` | `.Cam02` |
|
||||
| CAM16 | `new(ColourSpace.Cam16, ⋯)` | `.Cam16` |
|
||||
| HCT | `new(ColourSpace.Hct, ⋯)` | `.Hct` |
|
||||
|
||||
## 🔦 Features
|
||||
A `Unicolour` can be instantiated using any of the supported colour spaces.
|
||||
Conversion to other colour spaces is handled by Unicolour, and the results can be accessed through properties.
|
||||
|
||||
Two colours can be mixed / interpolated through any colour space, with or without premultiplied alpha.
|
||||
|
||||
Colour difference / colour distance can be calculated using various delta E metrics:
|
||||
- ΔE<sub>76</sub> (CIE76)
|
||||
- ΔE<sub>94</sub> (CIE94)
|
||||
- ΔE<sub>00</sub> (CIEDE2000)
|
||||
- ΔE<sub>CMC</sub> (CMC l:c)
|
||||
- ΔE<sub>ITP</sub>
|
||||
- ΔE<sub>z</sub>
|
||||
- ΔE<sub>HyAB</sub>
|
||||
- ΔE<sub>OK</sub>
|
||||
- ΔE<sub>CAM02</sub>
|
||||
- ΔE<sub>CAM16</sub>
|
||||
|
||||
The following colour information is available:
|
||||
- Hex representation
|
||||
- Relative luminance
|
||||
- Temperature (CCT and Duv)
|
||||
|
||||
Simulation of colour vision deficiency (CVD) / colour blindness is supported for:
|
||||
- Protanopia (no red perception)
|
||||
- Deuteranopia (no green perception)
|
||||
- Tritanopia (no blue perception)
|
||||
- Achromatopsia (no colour perception)
|
||||
|
||||
If a colour is outwith the display gamut, the closest in-gamut colour can be obtained using gamut mapping.
|
||||
The algorithm implemented in Unicolour conforms to CSS specifications.
|
||||
|
||||
Unicolour uses sRGB as the default RGB model and standard illuminant D65 (2° observer) as the default white point of the XYZ colour space.
|
||||
These [can be overridden](#-configuration) using the `Configuration` parameter.
|
||||
|
||||
## 🌈 How to use
|
||||
1. Install the package from [NuGet](https://www.nuget.org/packages/Wacton.Unicolour/)
|
||||
```
|
||||
dotnet add package Wacton.Unicolour
|
||||
```
|
||||
|
||||
2. Import the package
|
||||
```c#
|
||||
using Wacton.Unicolour;
|
||||
```
|
||||
|
||||
3. Create a `Unicolour`
|
||||
```c#
|
||||
var unicolour = new Unicolour("#FF1493");
|
||||
var unicolour = new Unicolour(ColourSpace.Rgb255, 255, 20, 147);
|
||||
var unicolour = new Unicolour(ColourSpace.Rgb, 1.00, 0.08, 0.58);
|
||||
var unicolour = new Unicolour(ColourSpace.RgbLinear, 1.00, 0.01, 0.29);
|
||||
var unicolour = new Unicolour(ColourSpace.Hsb, 327.6, 0.922, 1.000);
|
||||
var unicolour = new Unicolour(ColourSpace.Hsl, 327.6, 1.000, 0.539);
|
||||
var unicolour = new Unicolour(ColourSpace.Hwb, 327.6, 0.078, 0.000);
|
||||
var unicolour = new Unicolour(ColourSpace.Xyz, 0.4676, 0.2387, 0.2974);
|
||||
var unicolour = new Unicolour(ColourSpace.Xyy, 0.4658, 0.2378, 0.2387);
|
||||
var unicolour = new Unicolour(ColourSpace.Lab, 55.96, 84.54, -5.7);
|
||||
var unicolour = new Unicolour(ColourSpace.Lchab, 55.96, 84.73, 356.1);
|
||||
var unicolour = new Unicolour(ColourSpace.Luv, 55.96, 131.47, -24.35);
|
||||
var unicolour = new Unicolour(ColourSpace.Lchuv, 55.96, 133.71, 349.5);
|
||||
var unicolour = new Unicolour(ColourSpace.Hsluv, 349.5, 100.0, 56.0);
|
||||
var unicolour = new Unicolour(ColourSpace.Hpluv, 349.5, 303.2, 56.0);
|
||||
var unicolour = new Unicolour(ColourSpace.Ictcp, 0.38, 0.12, 0.19);
|
||||
var unicolour = new Unicolour(ColourSpace.Jzazbz, 0.106, 0.107, 0.005);
|
||||
var unicolour = new Unicolour(ColourSpace.Jzczhz, 0.106, 0.107, 2.6);
|
||||
var unicolour = new Unicolour(ColourSpace.Oklab, 0.65, 0.26, -0.01);
|
||||
var unicolour = new Unicolour(ColourSpace.Oklch, 0.65, 0.26, 356.9);
|
||||
var unicolour = new Unicolour(ColourSpace.Cam02, 62.86, 40.81, -1.18);
|
||||
var unicolour = new Unicolour(ColourSpace.Cam16, 62.47, 42.60, -1.36);
|
||||
var unicolour = new Unicolour(ColourSpace.Hct, 358.2, 100.38, 55.96);
|
||||
```
|
||||
|
||||
4. Get colour space representations
|
||||
```c#
|
||||
var rgb = unicolour.Rgb;
|
||||
var rgbLinear = unicolour.RgbLinear;
|
||||
var hsb = unicolour.Hsb;
|
||||
var hsl = unicolour.Hsl;
|
||||
var hwb = unicolour.Hwb;
|
||||
var xyz = unicolour.Xyz;
|
||||
var xyy = unicolour.Xyy;
|
||||
var lab = unicolour.Lab;
|
||||
var lchab = unicolour.Lchab;
|
||||
var luv = unicolour.Luv;
|
||||
var lchuv = unicolour.Lchuv;
|
||||
var hsluv = unicolour.Hsluv;
|
||||
var hpluv = unicolour.Hpluv;
|
||||
var ictcp = unicolour.Ictcp;
|
||||
var jzazbz = unicolour.Jzazbz;
|
||||
var jzczhz = unicolour.Jzczhz;
|
||||
var oklab = unicolour.Oklab;
|
||||
var oklch = unicolour.Oklch;
|
||||
var cam02 = unicolour.Cam02;
|
||||
var cam16 = unicolour.Cam16;
|
||||
var hct = unicolour.Hct;
|
||||
```
|
||||
|
||||
5. Get colour properties
|
||||
```c#
|
||||
var hex = unicolour.Hex;
|
||||
var relativeLuminance = unicolour.RelativeLuminance;
|
||||
var temperature = unicolour.Temperature;
|
||||
var inGamut = unicolour.IsInDisplayGamut;
|
||||
```
|
||||
|
||||
6. Mix colours (interpolate between them)
|
||||
```c#
|
||||
var mixed = unicolour1.Mix(ColourSpace.Rgb, unicolour2);
|
||||
var mixed = unicolour1.Mix(ColourSpace.RgbLinear, unicolour2);
|
||||
var mixed = unicolour1.Mix(ColourSpace.Hsb, unicolour2);
|
||||
var mixed = unicolour1.Mix(ColourSpace.Hsl, unicolour2);
|
||||
var mixed = unicolour1.Mix(ColourSpace.Hwb, unicolour2);
|
||||
var mixed = unicolour1.Mix(ColourSpace.Xyz, unicolour2);
|
||||
var mixed = unicolour1.Mix(ColourSpace.Xyy, unicolour2);
|
||||
var mixed = unicolour1.Mix(ColourSpace.Lab, unicolour2);
|
||||
var mixed = unicolour1.Mix(ColourSpace.Lchab, unicolour2);
|
||||
var mixed = unicolour1.Mix(ColourSpace.Luv, unicolour2);
|
||||
var mixed = unicolour1.Mix(ColourSpace.Lchuv, unicolour2);
|
||||
var mixed = unicolour1.Mix(ColourSpace.Hsluv, unicolour2);
|
||||
var mixed = unicolour1.Mix(ColourSpace.Hpluv, unicolour2);
|
||||
var mixed = unicolour1.Mix(ColourSpace.Ictcp, unicolour2);
|
||||
var mixed = unicolour1.Mix(ColourSpace.Jzazbz, unicolour2);
|
||||
var mixed = unicolour1.Mix(ColourSpace.Jzczhz, unicolour2);
|
||||
var mixed = unicolour1.Mix(ColourSpace.Oklab, unicolour2);
|
||||
var mixed = unicolour1.Mix(ColourSpace.Oklch, unicolour2);
|
||||
var mixed = unicolour1.Mix(ColourSpace.Cam02, unicolour2);
|
||||
var mixed = unicolour1.Mix(ColourSpace.Cam16, unicolour2);
|
||||
var mixed = unicolour1.Mix(ColourSpace.Hct, unicolour2);
|
||||
```
|
||||
|
||||
7. Compare colours
|
||||
```c#
|
||||
var contrast = unicolour1.Contrast(unicolour2);
|
||||
var difference = unicolour1.Difference(DeltaE.Cie76, unicolour2);
|
||||
var difference = unicolour1.Difference(DeltaE.Cie94, unicolour2);
|
||||
var difference = unicolour1.Difference(DeltaE.Cie94Textiles, unicolour2);
|
||||
var difference = unicolour1.Difference(DeltaE.Ciede2000, unicolour2);
|
||||
var difference = unicolour1.Difference(DeltaE.CmcAcceptability, unicolour2);
|
||||
var difference = unicolour1.Difference(DeltaE.CmcPerceptibility, unicolour2);
|
||||
var difference = unicolour1.Difference(DeltaE.Itp, unicolour2);
|
||||
var difference = unicolour1.Difference(DeltaE.Z, unicolour2);
|
||||
var difference = unicolour1.Difference(DeltaE.Hyab, unicolour2);
|
||||
var difference = unicolour1.Difference(DeltaE.Ok, unicolour2);
|
||||
var difference = unicolour1.Difference(DeltaE.Cam02, unicolour2);
|
||||
var difference = unicolour1.Difference(DeltaE.Cam16, unicolour2);
|
||||
```
|
||||
|
||||
8. Map colour to display gamut
|
||||
```c#
|
||||
var mapped = unicolour.MapToGamut();
|
||||
```
|
||||
|
||||
9. Simulate colour vision deficiency
|
||||
```c#
|
||||
var protanopia = unicolour.SimulateProtanopia();
|
||||
var deuteranopia = unicolour.SimulateDeuteranopia();
|
||||
var tritanopia = unicolour.SimulateTritanopia();
|
||||
var achromatopsia = unicolour.SimulateAchromatopsia();
|
||||
```
|
||||
|
||||
## ✨ Examples
|
||||
This repo contains an [example project](Unicolour.Example/Program.cs) that uses `Unicolour` to:
|
||||
1. Generate gradients through different colour spaces
|
||||
2. Render the colour spectrum with different colour vision deficiencies
|
||||
3. Demonstrate interpolation with and without premultiplied alpha
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
There is also a [console application](Unicolour.Console/Program.cs) that uses `Unicolour` to show colour information for a given hex value:
|
||||
|
||||

|
||||
|
||||
## 💡 Configuration
|
||||
A `Configuration` parameter can be used to customise the RGB model (e.g. Display P3, Rec. 2020)
|
||||
and the white point of the XYZ colour space (e.g. D50 reference white used by ICC profiles).
|
||||
|
||||
- RGB configuration requires red, green, and blue chromaticity coordinates, the reference white point, and the companding functions.
|
||||
Default configuration for sRGB, Display P3, and Rec. 2020 is provided.
|
||||
|
||||
- XYZ configuration only requires the reference white point.
|
||||
Default configuration for D65 and D50 (2° observer) is provided.
|
||||
|
||||
```c#
|
||||
// built-in configuration for Rec. 2020 RGB + D65 XYZ
|
||||
var config = new Configuration(RgbConfiguration.Rec2020, XyzConfiguration.D65);
|
||||
var unicolour = new Unicolour(ColourSpace.Rgb255, config, 204, 64, 132);
|
||||
```
|
||||
|
||||
```c#
|
||||
// manual configuration for wide-gamut RGB
|
||||
var rgbConfig = new RgbConfiguration(
|
||||
chromaticityR: new(0.7347, 0.2653),
|
||||
chromaticityG: new(0.1152, 0.8264),
|
||||
chromaticityB: new(0.1566, 0.0177),
|
||||
whitePoint: WhitePoint.From(Illuminant.D50),
|
||||
fromLinear: value => Companding.Gamma(value, 2.19921875),
|
||||
toLinear: value => Companding.InverseGamma(value, 2.19921875)
|
||||
);
|
||||
|
||||
// manual configuration for Illuminant C (10° observer) XYZ
|
||||
var xyzConfig = new XyzConfiguration(
|
||||
whitePoint: WhitePoint.From(Illuminant.C, Observer.Supplementary10)
|
||||
);
|
||||
|
||||
var config = new Configuration(rgbConfig, xyzConfig);
|
||||
var unicolour = new Unicolour(ColourSpace.Rgb255, config, 255, 20, 147);
|
||||
```
|
||||
|
||||
Configuration is also available for CAM02 & CAM16 viewing conditions,
|
||||
IC<sub>T</sub>C<sub>P</sub> scalar,
|
||||
and J<sub>z</sub>a<sub>z</sub>b<sub>z</sub> scalar.
|
||||
|
||||
The default white point used by all colour spaces is D65.
|
||||
This table lists which `Configuration` property determines the white point of each colour space.
|
||||
|
||||
| Colour space | White point configuration |
|
||||
|-----------------------------------------|-------------------------------------|
|
||||
| RGB | `RgbConfiguration` |
|
||||
| Linear RGB | `RgbConfiguration` |
|
||||
| HSB/HSV | `RgbConfiguration` |
|
||||
| HSL | `RgbConfiguration` |
|
||||
| HWB | `RgbConfiguration` |
|
||||
| CIEXYZ | `XyzConfiguration` |
|
||||
| CIExyY | `XyzConfiguration` |
|
||||
| CIELAB | `XyzConfiguration` |
|
||||
| CIELUV | `XyzConfiguration` |
|
||||
| CIELCh<sub>uv</sub> | `XyzConfiguration` |
|
||||
| HSLuv | `XyzConfiguration` |
|
||||
| HPLuv | `XyzConfiguration` |
|
||||
| IC<sub>T</sub>C<sub>P</sub> | None (always D65) |
|
||||
| J<sub>z</sub>a<sub>z</sub>b<sub>z</sub> | None (always D65) |
|
||||
| J<sub>z</sub>C<sub>z</sub>h<sub>z</sub> | None (always D65) |
|
||||
| Oklab | None (always D65) |
|
||||
| Oklch | None (always D65) |
|
||||
| CIECAM02 | `CamConfiguration` |
|
||||
| CAM16 | `CamConfiguration` |
|
||||
| HCT | None (always D65) |
|
||||
|
||||
A `Unicolour` can be converted to a different configuration, which enables conversions between different RGB and XYZ models.
|
||||
|
||||
```c#
|
||||
// pure sRGB green
|
||||
var srgbConfig = new Configuration(RgbConfiguration.StandardRgb);
|
||||
var unicolourSrgb = new Unicolour(ColourSpace.Rgb, srgbConfig, 0, 1, 0);
|
||||
Console.WriteLine(unicolourSrgb.Rgb); // 0.00 1.00 0.00
|
||||
|
||||
// ⟶ Display P3
|
||||
var displayP3Config = new Configuration(RgbConfiguration.DisplayP3);
|
||||
var unicolourDisplayP3 = unicolourSrgb.ConvertToConfiguration(displayP3Config);
|
||||
Console.WriteLine(unicolourDisplayP3.Rgb); // 0.46 0.99 0.30
|
||||
|
||||
// ⟶ Rec. 2020
|
||||
var rec2020Config = new Configuration(RgbConfiguration.Rec2020);
|
||||
var unicolourRec2020 = unicolourDisplayP3.ConvertToConfiguration(rec2020Config);
|
||||
Console.WriteLine(unicolourRec2020.Rgb); // 0.57 0.96 0.27
|
||||
```
|
||||
|
||||
## 🔮 Datasets
|
||||
Some colour datasets have been compiled for convenience and are available as a [NuGet package](https://www.nuget.org/packages/Wacton.Unicolour.Datasets/).
|
||||
|
||||
Commonly used sets of colours:
|
||||
- [CSS specification](https://www.w3.org/TR/css-color-4/#named-colors) named colours
|
||||
- [Macbeth ColorChecker](https://en.wikipedia.org/wiki/ColorChecker) colour rendition chart
|
||||
|
||||
Colour data used in academic literature:
|
||||
- [Hung-Berns](https://doi.org/10.1002/col.5080200506) constant hue loci data
|
||||
- [Ebner-Fairchild](https://doi.org/10.1117/12.298269) constant perceived-hue data
|
||||
|
||||
Example usage:
|
||||
|
||||
1. Install the package from [NuGet](https://www.nuget.org/packages/Wacton.Unicolour.Datasets/)
|
||||
```
|
||||
dotnet add package Wacton.Unicolour.Datasets
|
||||
```
|
||||
|
||||
2. Import the package
|
||||
```c#
|
||||
using Wacton.Unicolour.Datasets;
|
||||
```
|
||||
|
||||
3. Reference the predefined `Unicolour`
|
||||
```c#
|
||||
var unicolour = Css.DeepPink;
|
||||
```
|
||||
|
||||
## 🦺 Work in progress
|
||||
Version 4 of Unicolour is in development and aims to provide more new features:
|
||||
- 🌡️ Create a `Unicolour` from temperature (CCT and Duv)
|
||||
- 🎯 More accurate calculation of temperature (CCT and Duv)
|
||||
- 📈 Create a `Unicolour` from a spectral power distribution
|
||||
- 🚥 More modes of hue interpolation
|
||||
- 🎨 More default RGB models (e.g. A98, ProPhoto)
|
||||
|
||||
---
|
||||
|
||||
[Wacton.Unicolour](https://github.com/waacton/Unicolour) is licensed under the [MIT License](https://choosealicense.com/licenses/mit/), copyright © 2022-2023 William Acton.
|
BIN
Unicolour/Resources/Unicolour.ico
Normal file
BIN
Unicolour/Resources/Unicolour.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 165 KiB |
BIN
Unicolour/Resources/Unicolour.png
Normal file
BIN
Unicolour/Resources/Unicolour.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.3 KiB |
66
Unicolour/Rgb.cs
Normal file
66
Unicolour/Rgb.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
using System;
|
||||
|
||||
namespace Wacton.Unicolour
|
||||
{
|
||||
|
||||
public record Rgb : ColourRepresentation
|
||||
{
|
||||
protected override int? HueIndex => null;
|
||||
public double R => First;
|
||||
public double G => Second;
|
||||
public double B => Third;
|
||||
public double ConstrainedR => ConstrainedFirst;
|
||||
public double ConstrainedG => ConstrainedSecond;
|
||||
public double ConstrainedB => ConstrainedThird;
|
||||
protected override double ConstrainedFirst => R.Clamp(0.0, 1.0);
|
||||
protected override double ConstrainedSecond => G.Clamp(0.0, 1.0);
|
||||
protected override double ConstrainedThird => B.Clamp(0.0, 1.0);
|
||||
internal override bool IsGreyscale => ConstrainedR.Equals(ConstrainedG) && ConstrainedG.Equals(ConstrainedB);
|
||||
|
||||
// for almost all cases, doing this check in linear RGB will return the same result
|
||||
// but handling it here feels most natural as it is the intended "display" space
|
||||
// and isn't concerned about questionable custom inverse-companding-to-linear functions (e.g. where where RGB <= 1.0 but RGB-Linear > 1.0)
|
||||
internal bool IsInGamut => !UseAsNaN && R is >= 0 and <= 1.0 && G is >= 0 and <= 1.0 && B is >= 0 and <= 1.0;
|
||||
public Rgb255 Byte255 => new(To255(R), To255(G), To255(B), ColourHeritage.From(this));
|
||||
|
||||
public Rgb(double r, double g, double b) : this(r, g, b, ColourHeritage.None)
|
||||
{
|
||||
}
|
||||
|
||||
internal Rgb(ColourTriplet triplet, ColourHeritage heritage) : this(triplet.First, triplet.Second,
|
||||
triplet.Third, heritage)
|
||||
{
|
||||
}
|
||||
|
||||
internal Rgb(double r, double g, double b, ColourHeritage heritage) : base(r, g, b, heritage)
|
||||
{
|
||||
}
|
||||
|
||||
private static double To255(double value) => Math.Round(value * 255);
|
||||
|
||||
protected override string FirstString => $"{R:F2}";
|
||||
protected override string SecondString => $"{G:F2}";
|
||||
protected override string ThirdString => $"{B:F2}";
|
||||
public override string ToString() => base.ToString();
|
||||
|
||||
/*
|
||||
* RGB is a transform of RGB Linear
|
||||
* Forward: https://en.wikipedia.org/wiki/SRGB#From_CIE_XYZ_to_sRGB
|
||||
* Reverse: https://en.wikipedia.org/wiki/SRGB#From_sRGB_to_CIE_XYZ
|
||||
*/
|
||||
|
||||
internal static Rgb FromRgbLinear(RgbLinear rgbLinear, RgbConfiguration rgbConfig)
|
||||
{
|
||||
var rgbLinearMatrix = Matrix.FromTriplet(rgbLinear.Triplet);
|
||||
var rgbMatrix = rgbLinearMatrix.Select(rgbConfig.CompandFromLinear);
|
||||
return new Rgb(rgbMatrix.ToTriplet(), ColourHeritage.From(rgbLinear));
|
||||
}
|
||||
|
||||
internal static RgbLinear ToRgbLinear(Rgb rgb, RgbConfiguration rgbConfig)
|
||||
{
|
||||
var rgbMatrix = Matrix.FromTriplet(rgb.Triplet);
|
||||
var rgbLinearMatrix = rgbMatrix.Select(rgbConfig.InverseCompandToLinear);
|
||||
return new RgbLinear(rgbLinearMatrix.ToTriplet(), ColourHeritage.From(rgb));
|
||||
}
|
||||
}
|
||||
}
|
33
Unicolour/Rgb255.cs
Normal file
33
Unicolour/Rgb255.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
namespace Wacton.Unicolour
|
||||
{
|
||||
|
||||
public record Rgb255 : ColourRepresentation
|
||||
{
|
||||
protected override int? HueIndex => null;
|
||||
public int R => (int)First;
|
||||
public int G => (int)Second;
|
||||
public int B => (int)Third;
|
||||
public int ConstrainedR => (int)ConstrainedFirst;
|
||||
public int ConstrainedG => (int)ConstrainedSecond;
|
||||
public int ConstrainedB => (int)ConstrainedThird;
|
||||
protected override double ConstrainedFirst => R.Clamp(0, 255);
|
||||
protected override double ConstrainedSecond => G.Clamp(0, 255);
|
||||
protected override double ConstrainedThird => B.Clamp(0, 255);
|
||||
internal override bool IsGreyscale => ConstrainedR.Equals(ConstrainedG) && ConstrainedG.Equals(ConstrainedB);
|
||||
|
||||
public string ConstrainedHex => UseAsNaN ? "-" : $"#{ConstrainedR:X2}{ConstrainedG:X2}{ConstrainedB:X2}";
|
||||
|
||||
public Rgb255(double r, double g, double b) : this(r, g, b, ColourHeritage.None)
|
||||
{
|
||||
}
|
||||
|
||||
internal Rgb255(double r, double g, double b, ColourHeritage heritage) : base(r, g, b, heritage)
|
||||
{
|
||||
}
|
||||
|
||||
protected override string FirstString => $"{R}";
|
||||
protected override string SecondString => $"{G}";
|
||||
protected override string ThirdString => $"{B}";
|
||||
public override string ToString() => base.ToString();
|
||||
}
|
||||
}
|
39
Unicolour/RgbConfiguration.cs
Normal file
39
Unicolour/RgbConfiguration.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using System;
|
||||
|
||||
namespace Wacton.Unicolour
|
||||
{
|
||||
|
||||
public class RgbConfiguration
|
||||
{
|
||||
public static readonly RgbConfiguration StandardRgb = RgbModels.StandardRgb.RgbConfiguration;
|
||||
public static readonly RgbConfiguration DisplayP3 = RgbModels.DisplayP3.RgbConfiguration;
|
||||
public static readonly RgbConfiguration Rec2020 = RgbModels.Rec2020.RgbConfiguration;
|
||||
public static readonly RgbConfiguration A98 = RgbModels.A98.RgbConfiguration;
|
||||
public static readonly RgbConfiguration ProPhoto = RgbModels.ProPhoto.RgbConfiguration;
|
||||
|
||||
public Chromaticity ChromaticityR { get; }
|
||||
public Chromaticity ChromaticityG { get; }
|
||||
public Chromaticity ChromaticityB { get; }
|
||||
public WhitePoint WhitePoint { get; }
|
||||
public Func<double, double> CompandFromLinear { get; }
|
||||
public Func<double, double> InverseCompandToLinear { get; }
|
||||
|
||||
public RgbConfiguration(
|
||||
Chromaticity chromaticityR,
|
||||
Chromaticity chromaticityG,
|
||||
Chromaticity chromaticityB,
|
||||
WhitePoint whitePoint,
|
||||
Func<double, double> fromLinear,
|
||||
Func<double, double> toLinear)
|
||||
{
|
||||
ChromaticityR = chromaticityR;
|
||||
ChromaticityG = chromaticityG;
|
||||
ChromaticityB = chromaticityB;
|
||||
WhitePoint = whitePoint;
|
||||
CompandFromLinear = fromLinear;
|
||||
InverseCompandToLinear = toLinear;
|
||||
}
|
||||
|
||||
public override string ToString() => $"RGB {WhitePoint} {ChromaticityR} {ChromaticityG} {ChromaticityB}";
|
||||
}
|
||||
}
|
97
Unicolour/RgbLinear.cs
Normal file
97
Unicolour/RgbLinear.cs
Normal file
@@ -0,0 +1,97 @@
|
||||
namespace Wacton.Unicolour
|
||||
{
|
||||
|
||||
public record RgbLinear : ColourRepresentation
|
||||
{
|
||||
protected override int? HueIndex => null;
|
||||
public double R => First;
|
||||
public double G => Second;
|
||||
public double B => Third;
|
||||
public double ConstrainedR => ConstrainedFirst;
|
||||
public double ConstrainedG => ConstrainedSecond;
|
||||
public double ConstrainedB => ConstrainedThird;
|
||||
protected override double ConstrainedFirst => R.Clamp(0.0, 1.0);
|
||||
protected override double ConstrainedSecond => G.Clamp(0.0, 1.0);
|
||||
protected override double ConstrainedThird => B.Clamp(0.0, 1.0);
|
||||
internal override bool IsGreyscale => ConstrainedR.Equals(ConstrainedG) && ConstrainedG.Equals(ConstrainedB);
|
||||
|
||||
// https://www.w3.org/TR/WCAG21/#dfn-relative-luminance - effectively an approximation of Y from XYZ, but will stick to the specification
|
||||
internal double RelativeLuminance => UseAsNaN ? double.NaN : 0.2126 * R + 0.7152 * G + 0.0722 * B;
|
||||
|
||||
public RgbLinear(double r, double g, double b) : this(r, g, b, ColourHeritage.None)
|
||||
{
|
||||
}
|
||||
|
||||
internal RgbLinear(ColourTriplet triplet, ColourHeritage heritage) : this(triplet.First, triplet.Second,
|
||||
triplet.Third, heritage)
|
||||
{
|
||||
}
|
||||
|
||||
internal RgbLinear(double r, double g, double b, ColourHeritage heritage) : base(r, g, b, heritage)
|
||||
{
|
||||
}
|
||||
|
||||
protected override string FirstString => $"{R:F2}";
|
||||
protected override string SecondString => $"{G:F2}";
|
||||
protected override string ThirdString => $"{B:F2}";
|
||||
public override string ToString() => base.ToString();
|
||||
|
||||
/*
|
||||
* RGB Linear is a transform of XYZ
|
||||
* Forward: https://en.wikipedia.org/wiki/SRGB#From_CIE_XYZ_to_sRGB
|
||||
* Reverse: https://en.wikipedia.org/wiki/SRGB#From_sRGB_to_CIE_XYZ
|
||||
*/
|
||||
|
||||
internal static RgbLinear FromXyz(Xyz xyz, RgbConfiguration rgbConfig, XyzConfiguration xyzConfig)
|
||||
{
|
||||
var xyzMatrix = Matrix.FromTriplet(xyz.Triplet);
|
||||
var transformationMatrix = RgbLinearToXyzMatrix(rgbConfig, xyzConfig).Inverse();
|
||||
var rgbLinearMatrix = transformationMatrix.Multiply(xyzMatrix);
|
||||
return new RgbLinear(rgbLinearMatrix.ToTriplet(), ColourHeritage.From(xyz));
|
||||
}
|
||||
|
||||
internal static Xyz ToXyz(RgbLinear rgbLinear, RgbConfiguration rgbConfig, XyzConfiguration xyzConfig)
|
||||
{
|
||||
var rgbLinearMatrix = Matrix.FromTriplet(rgbLinear.Triplet);
|
||||
var transformationMatrix = RgbLinearToXyzMatrix(rgbConfig, xyzConfig);
|
||||
var xyzMatrix = transformationMatrix.Multiply(rgbLinearMatrix);
|
||||
return new Xyz(xyzMatrix.ToTriplet(), ColourHeritage.From(rgbLinear));
|
||||
}
|
||||
|
||||
// http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
|
||||
internal static Matrix RgbLinearToXyzMatrix(RgbConfiguration rgbConfig, XyzConfiguration xyzConfig)
|
||||
{
|
||||
var cr = rgbConfig.ChromaticityR;
|
||||
var cg = rgbConfig.ChromaticityG;
|
||||
var cb = rgbConfig.ChromaticityB;
|
||||
|
||||
double X(Chromaticity c) => c.X / c.Y;
|
||||
double Y(Chromaticity c) => 1;
|
||||
double Z(Chromaticity c) => (1 - c.X - c.Y) / c.Y;
|
||||
|
||||
var (xr, yr, zr) = (X(cr), Y(cr), Z(cr));
|
||||
var (xg, yg, zg) = (X(cg), Y(cg), Z(cg));
|
||||
var (xb, yb, zb) = (X(cb), Y(cb), Z(cb));
|
||||
|
||||
var fromPrimaries = new Matrix(new[,]
|
||||
{
|
||||
{ xr, xg, xb },
|
||||
{ yr, yg, yb },
|
||||
{ zr, zg, zb }
|
||||
});
|
||||
|
||||
var sourceWhite = rgbConfig.WhitePoint.AsXyzMatrix();
|
||||
var (sr, sg, sb) = fromPrimaries.Inverse().Multiply(sourceWhite).ToTriplet();
|
||||
|
||||
var matrix = new Matrix(new[,]
|
||||
{
|
||||
{ sr * xr, sg * xg, sb * xb },
|
||||
{ sr * yr, sg * yg, sb * yb },
|
||||
{ sr * zr, sg * zg, sb * zb }
|
||||
});
|
||||
|
||||
var adaptedMatrix = Adaptation.WhitePoint(matrix, rgbConfig.WhitePoint, xyzConfig.WhitePoint);
|
||||
return adaptedMatrix;
|
||||
}
|
||||
}
|
||||
}
|
126
Unicolour/RgbModels.cs
Normal file
126
Unicolour/RgbModels.cs
Normal file
@@ -0,0 +1,126 @@
|
||||
using System;
|
||||
|
||||
namespace Wacton.Unicolour
|
||||
{
|
||||
|
||||
public static class RgbModels
|
||||
{
|
||||
public static class StandardRgb
|
||||
{
|
||||
public static readonly Chromaticity R = new(0.6400, 0.3300);
|
||||
public static readonly Chromaticity G = new(0.3000, 0.6000);
|
||||
public static readonly Chromaticity B = new(0.1500, 0.0600);
|
||||
public static WhitePoint WhitePoint => WhitePoint.From(Illuminant.D65);
|
||||
|
||||
public static double FromLinear(double linear)
|
||||
{
|
||||
return Companding.ReflectWhenNegative(linear, value =>
|
||||
value <= 0.0031308
|
||||
? 12.92 * value
|
||||
: 1.055 * Companding.Gamma(value, 2.4) - 0.055);
|
||||
}
|
||||
|
||||
public static double ToLinear(double nonlinear)
|
||||
{
|
||||
return Companding.ReflectWhenNegative(nonlinear, value =>
|
||||
value <= 0.04045
|
||||
? value / 12.92
|
||||
: Companding.InverseGamma((value + 0.055) / 1.055, 2.4));
|
||||
}
|
||||
|
||||
public static RgbConfiguration RgbConfiguration => new(R, G, B, WhitePoint, FromLinear, ToLinear);
|
||||
}
|
||||
|
||||
public static class DisplayP3
|
||||
{
|
||||
public static readonly Chromaticity R = new(0.680, 0.320);
|
||||
public static readonly Chromaticity G = new(0.265, 0.690);
|
||||
public static readonly Chromaticity B = new(0.150, 0.060);
|
||||
public static WhitePoint WhitePoint => WhitePoint.From(Illuminant.D65);
|
||||
|
||||
public static double FromLinear(double value) => StandardRgb.FromLinear(value);
|
||||
public static double ToLinear(double value) => StandardRgb.ToLinear(value);
|
||||
|
||||
public static RgbConfiguration RgbConfiguration => new(R, G, B, WhitePoint, FromLinear, ToLinear);
|
||||
}
|
||||
|
||||
public static class Rec2020
|
||||
{
|
||||
public static readonly Chromaticity R = new(0.708, 0.292);
|
||||
public static readonly Chromaticity G = new(0.170, 0.797);
|
||||
public static readonly Chromaticity B = new(0.131, 0.046);
|
||||
public static WhitePoint WhitePoint => WhitePoint.From(Illuminant.D65);
|
||||
|
||||
private const double Alpha = 1.09929682680944;
|
||||
private const double Beta = 0.018053968510807;
|
||||
|
||||
public static double FromLinear(double linear)
|
||||
{
|
||||
return Companding.ReflectWhenNegative(linear, e =>
|
||||
{
|
||||
if (e < Beta) return 4.5 * e;
|
||||
return Alpha * Math.Pow(e, 0.45) - (Alpha - 1);
|
||||
});
|
||||
}
|
||||
|
||||
public static double ToLinear(double nonlinear)
|
||||
{
|
||||
return Companding.ReflectWhenNegative(nonlinear, ePrime =>
|
||||
{
|
||||
if (ePrime < Beta * 4.5) return ePrime / 4.5;
|
||||
return Math.Pow((ePrime + (Alpha - 1)) / Alpha, 1 / 0.45);
|
||||
});
|
||||
}
|
||||
|
||||
public static RgbConfiguration RgbConfiguration => new(R, G, B, WhitePoint, FromLinear, ToLinear);
|
||||
}
|
||||
|
||||
public static class A98
|
||||
{
|
||||
public static readonly Chromaticity R = new(0.6400, 0.3300);
|
||||
public static readonly Chromaticity G = new(0.2100, 0.7100);
|
||||
public static readonly Chromaticity B = new(0.1500, 0.0600);
|
||||
public static WhitePoint WhitePoint => WhitePoint.From(Illuminant.D65);
|
||||
|
||||
public static double FromLinear(double linear)
|
||||
{
|
||||
return Companding.ReflectWhenNegative(linear, value => Companding.Gamma(value, 563 / 256.0));
|
||||
}
|
||||
|
||||
public static double ToLinear(double nonlinear)
|
||||
{
|
||||
return Companding.ReflectWhenNegative(nonlinear, value => Companding.InverseGamma(value, 563 / 256.0));
|
||||
}
|
||||
|
||||
public static RgbConfiguration RgbConfiguration => new(R, G, B, WhitePoint, FromLinear, ToLinear);
|
||||
}
|
||||
|
||||
public static class ProPhoto
|
||||
{
|
||||
public static readonly Chromaticity R = new(0.734699, 0.265301);
|
||||
public static readonly Chromaticity G = new(0.159597, 0.840403);
|
||||
public static readonly Chromaticity B = new(0.036598, 0.000105);
|
||||
public static WhitePoint WhitePoint => WhitePoint.From(Illuminant.D50);
|
||||
|
||||
private const double Et = 1 / 512.0;
|
||||
|
||||
public static double FromLinear(double linear)
|
||||
{
|
||||
return Companding.ReflectWhenNegative(linear, value =>
|
||||
value < Et
|
||||
? 16 * value
|
||||
: Companding.Gamma(value, 1.8));
|
||||
}
|
||||
|
||||
public static double ToLinear(double nonlinear)
|
||||
{
|
||||
return Companding.ReflectWhenNegative(nonlinear, value =>
|
||||
value < Et * 16
|
||||
? value / 16.0
|
||||
: Companding.InverseGamma(value, 1.8));
|
||||
}
|
||||
|
||||
public static RgbConfiguration RgbConfiguration => new(R, G, B, WhitePoint, FromLinear, ToLinear);
|
||||
}
|
||||
}
|
||||
}
|
124
Unicolour/Temperature.cs
Normal file
124
Unicolour/Temperature.cs
Normal file
@@ -0,0 +1,124 @@
|
||||
using System;
|
||||
|
||||
namespace Wacton.Unicolour
|
||||
{
|
||||
|
||||
public record Temperature(double Cct, double Duv)
|
||||
{
|
||||
public double Cct { get; } = Cct;
|
||||
public double Duv { get; } = Duv;
|
||||
|
||||
private static Temperature NaN() => new(double.NaN, double.NaN);
|
||||
|
||||
// https://en.wikipedia.org/wiki/CIE_1960_color_space
|
||||
internal static Temperature Get(Xyz xyz)
|
||||
{
|
||||
var (x, y, z) = xyz.Triplet;
|
||||
var u = 4.0 * x / (x + 15.0 * y + 3.0 * z);
|
||||
var v = 6.0 * y / (x + 15.0 * y + 3.0 * z);
|
||||
|
||||
return double.IsNaN(u) || double.IsNaN(v) ? NaN() : Get(u, v);
|
||||
}
|
||||
|
||||
// https://en.wikipedia.org/wiki/Correlated_color_temperature#Robertson's_method
|
||||
internal static Temperature Get(double u, double v)
|
||||
{
|
||||
for (var i = 0; i < Isotherms.Length - 1; i++)
|
||||
{
|
||||
var lowerIsotherm = Isotherms[i];
|
||||
var upperIsotherm = Isotherms[i + 1];
|
||||
|
||||
// not actually the distance yet, but can be used to inform if between isotherms
|
||||
var lowerDistance = v - lowerIsotherm.V - lowerIsotherm.M * (u - lowerIsotherm.U);
|
||||
var upperDistance = v - upperIsotherm.V - upperIsotherm.M * (u - upperIsotherm.U);
|
||||
|
||||
// when distance to the 2 isotherms are in different directions, test point (u, v) is between them
|
||||
var isBetweenIsotherms = Math.Sign(lowerDistance) != Math.Sign(upperDistance);
|
||||
if (!isBetweenIsotherms)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// once between isotherms, can calculate the distance and interpolate
|
||||
lowerDistance /= Math.Sqrt(1.0 + Math.Pow(lowerIsotherm.M, 2));
|
||||
upperDistance /= Math.Sqrt(1.0 + Math.Pow(upperIsotherm.M, 2));
|
||||
|
||||
// signs are different, will add the distances together
|
||||
var totalDistance = lowerDistance - upperDistance;
|
||||
var interpolationDistance = lowerDistance / totalDistance;
|
||||
var reciprocalMegakelvin = Interpolation.Interpolate(lowerIsotherm.ReciprocalMegakelvin,
|
||||
upperIsotherm.ReciprocalMegakelvin, interpolationDistance);
|
||||
var kelvins = 1000000 / reciprocalMegakelvin;
|
||||
var duv = GetDuv(u, v);
|
||||
return new Temperature(kelvins, duv);
|
||||
}
|
||||
|
||||
return NaN();
|
||||
}
|
||||
|
||||
// https://doi.org/10.1080/15502724.2014.839020
|
||||
private static double GetDuv(double u, double v)
|
||||
{
|
||||
const double k6 = -0.00616793;
|
||||
const double k5 = 0.0893944;
|
||||
const double k4 = -0.5179722;
|
||||
const double k3 = 1.5317403;
|
||||
const double k2 = -2.4243787;
|
||||
const double k1 = 1.925865;
|
||||
const double k0 = -0.471106;
|
||||
|
||||
var lfp = Math.Sqrt(Math.Pow(u - 0.292, 2) + Math.Pow(v - 0.240, 2));
|
||||
var a = Math.Acos((u - 0.292) / lfp);
|
||||
var lbb = k6 * Math.Pow(a, 6) + k5 * Math.Pow(a, 5) + k4 * Math.Pow(a, 4) +
|
||||
k3 * Math.Pow(a, 3) + k2 * Math.Pow(a, 2) + k1 * a + k0;
|
||||
return lfp - lbb;
|
||||
}
|
||||
|
||||
private record Isotherm(double U, double V, double M, double ReciprocalMegakelvin)
|
||||
{
|
||||
public double U { get; } = U;
|
||||
public double V { get; } = V;
|
||||
public double M { get; } = M;
|
||||
public double ReciprocalMegakelvin { get; } = ReciprocalMegakelvin; // also known as mired
|
||||
}
|
||||
|
||||
// http://www.brucelindbloom.com/Eqn_XYZ_to_T.html
|
||||
private static readonly Isotherm[] Isotherms =
|
||||
{
|
||||
new(0.18006, 0.26352, -0.24341, double.Epsilon),
|
||||
new(0.18066, 0.26589, -0.25479, 10),
|
||||
new(0.18133, 0.26846, -0.26876, 20),
|
||||
new(0.18208, 0.27119, -0.28539, 30),
|
||||
new(0.18293, 0.27407, -0.30470, 40),
|
||||
new(0.18388, 0.27709, -0.32675, 50),
|
||||
new(0.18494, 0.28021, -0.35156, 60),
|
||||
new(0.18611, 0.28342, -0.37915, 70),
|
||||
new(0.18740, 0.28668, -0.40955, 80),
|
||||
new(0.18880, 0.28997, -0.44278, 90),
|
||||
new(0.19032, 0.29326, -0.47888, 100),
|
||||
new(0.19462, 0.30141, -0.58204, 125),
|
||||
new(0.19962, 0.30921, -0.70471, 150),
|
||||
new(0.20525, 0.31647, -0.84901, 175),
|
||||
new(0.21142, 0.32312, -1.0182, 200),
|
||||
new(0.21807, 0.32909, -1.2168, 225),
|
||||
new(0.22511, 0.33439, -1.4512, 250),
|
||||
new(0.23247, 0.33904, -1.7298, 275),
|
||||
new(0.24010, 0.34308, -2.0637, 300),
|
||||
new(0.24792, 0.34655, -2.4681, 325),
|
||||
new(0.25591, 0.34951, -2.9641, 350),
|
||||
new(0.26400, 0.35200, -3.5814, 375),
|
||||
new(0.27218, 0.35407, -4.3633, 400),
|
||||
new(0.28039, 0.35577, -5.3762, 425),
|
||||
new(0.28863, 0.35714, -6.7262, 450),
|
||||
new(0.29685, 0.35823, -8.5955, 475),
|
||||
new(0.30505, 0.35907, -11.324, 500),
|
||||
new(0.31320, 0.35968, -15.628, 525),
|
||||
new(0.32129, 0.36011, -23.325, 550),
|
||||
new(0.32931, 0.36038, -40.770, 575),
|
||||
new(0.33724, 0.36051, -116.45, 600)
|
||||
};
|
||||
|
||||
public override string ToString() =>
|
||||
double.IsNaN(Cct) || double.IsNaN(Duv) ? "-" : $"{Cct:F1} K (Δuv {Duv:F5})";
|
||||
}
|
||||
}
|
145
Unicolour/Unicolour.cs
Normal file
145
Unicolour/Unicolour.cs
Normal file
@@ -0,0 +1,145 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Wacton.Unicolour
|
||||
{
|
||||
|
||||
public partial class Unicolour : IEquatable<Unicolour>
|
||||
{
|
||||
private Rgb? rgb;
|
||||
private RgbLinear? rgbLinear;
|
||||
private Hsb? hsb;
|
||||
private Hsl? hsl;
|
||||
private Hwb? hwb;
|
||||
private Xyz? xyz;
|
||||
private Xyy? xyy;
|
||||
private Lab? lab;
|
||||
private Lchab? lchab;
|
||||
private Luv? luv;
|
||||
private Lchuv? lchuv;
|
||||
private Hsluv? hsluv;
|
||||
private Hpluv? hpluv;
|
||||
private Ictcp? ictcp;
|
||||
private Jzazbz? jzazbz;
|
||||
private Jzczhz? jzczhz;
|
||||
private Oklab? oklab;
|
||||
private Oklch? oklch;
|
||||
private Cam02? cam02;
|
||||
private Cam16? cam16;
|
||||
private Hct? hct;
|
||||
|
||||
internal readonly ColourRepresentation InitialRepresentation;
|
||||
internal readonly ColourSpace InitialColourSpace;
|
||||
public Rgb Rgb => Get<Rgb>(ColourSpace.Rgb);
|
||||
public RgbLinear RgbLinear => Get<RgbLinear>(ColourSpace.RgbLinear);
|
||||
public Hsb Hsb => Get<Hsb>(ColourSpace.Hsb);
|
||||
public Hsl Hsl => Get<Hsl>(ColourSpace.Hsl);
|
||||
public Hwb Hwb => Get<Hwb>(ColourSpace.Hwb);
|
||||
public Xyz Xyz => Get<Xyz>(ColourSpace.Xyz);
|
||||
public Xyy Xyy => Get<Xyy>(ColourSpace.Xyy);
|
||||
public Lab Lab => Get<Lab>(ColourSpace.Lab);
|
||||
public Lchab Lchab => Get<Lchab>(ColourSpace.Lchab);
|
||||
public Luv Luv => Get<Luv>(ColourSpace.Luv);
|
||||
public Lchuv Lchuv => Get<Lchuv>(ColourSpace.Lchuv);
|
||||
public Hsluv Hsluv => Get<Hsluv>(ColourSpace.Hsluv);
|
||||
public Hpluv Hpluv => Get<Hpluv>(ColourSpace.Hpluv);
|
||||
public Ictcp Ictcp => Get<Ictcp>(ColourSpace.Ictcp);
|
||||
public Jzazbz Jzazbz => Get<Jzazbz>(ColourSpace.Jzazbz);
|
||||
public Jzczhz Jzczhz => Get<Jzczhz>(ColourSpace.Jzczhz);
|
||||
public Oklab Oklab => Get<Oklab>(ColourSpace.Oklab);
|
||||
public Oklch Oklch => Get<Oklch>(ColourSpace.Oklch);
|
||||
public Cam02 Cam02 => Get<Cam02>(ColourSpace.Cam02);
|
||||
public Cam16 Cam16 => Get<Cam16>(ColourSpace.Cam16);
|
||||
public Hct Hct => Get<Hct>(ColourSpace.Hct);
|
||||
public Alpha Alpha { get; }
|
||||
public Configuration Config { get; }
|
||||
|
||||
public string Hex => !IsInDisplayGamut ? "-" : Rgb.Byte255.ConstrainedHex;
|
||||
public bool IsInDisplayGamut => Rgb.IsInGamut;
|
||||
public double RelativeLuminance => RgbLinear.RelativeLuminance;
|
||||
public string Description => string.Join(" ", ColourDescription.Get(Hsl));
|
||||
public Temperature Temperature => Temperature.Get(Xyz);
|
||||
|
||||
internal Unicolour(ColourSpace colourSpace, Configuration config, ColourHeritage heritage, double first,
|
||||
double second, double third, double alpha = 1.0)
|
||||
{
|
||||
if (colourSpace == ColourSpace.Rgb255)
|
||||
{
|
||||
colourSpace = ColourSpace.Rgb;
|
||||
first /= 255.0;
|
||||
second /= 255.0;
|
||||
third /= 255.0;
|
||||
}
|
||||
|
||||
Config = config;
|
||||
Alpha = new Alpha(alpha);
|
||||
InitialRepresentation = CreateRepresentation(colourSpace, first, second, third, config, heritage);
|
||||
InitialColourSpace = colourSpace;
|
||||
SetBackingField(InitialColourSpace);
|
||||
}
|
||||
|
||||
public double Contrast(Unicolour other) => Comparison.Contrast(this, other);
|
||||
public double Difference(DeltaE deltaE, Unicolour reference) => Comparison.Difference(deltaE, this, reference);
|
||||
|
||||
public Unicolour Mix(ColourSpace colourSpace, Unicolour other, double amount = 0.5,
|
||||
bool premultiplyAlpha = true)
|
||||
{
|
||||
return Interpolation.Mix(colourSpace, this, other, amount, premultiplyAlpha);
|
||||
}
|
||||
|
||||
public Unicolour SimulateProtanopia() => VisionDeficiency.SimulateProtanopia(this);
|
||||
public Unicolour SimulateDeuteranopia() => VisionDeficiency.SimulateDeuteranopia(this);
|
||||
public Unicolour SimulateTritanopia() => VisionDeficiency.SimulateTritanopia(this);
|
||||
public Unicolour SimulateAchromatopsia() => VisionDeficiency.SimulateAchromatopsia(this);
|
||||
|
||||
public Unicolour MapToGamut() => GamutMapping.ToRgbGamut(this);
|
||||
|
||||
public Unicolour ConvertToConfiguration(Configuration newConfig)
|
||||
{
|
||||
var xyzMatrix = Matrix.FromTriplet(Xyz.Triplet);
|
||||
var adaptedMatrix = Adaptation.WhitePoint(xyzMatrix, Config.Xyz.WhitePoint, newConfig.Xyz.WhitePoint);
|
||||
return new Unicolour(ColourSpace.Xyz, newConfig, adaptedMatrix.ToTriplet().Tuple, Alpha.A);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var parts = new List<string> { $"from {InitialColourSpace} {InitialRepresentation} alpha {Alpha}" };
|
||||
if (Description != ColourDescription.NotApplicable.ToString())
|
||||
{
|
||||
parts.Add(Description);
|
||||
}
|
||||
|
||||
return string.Join(" · ", parts);
|
||||
}
|
||||
|
||||
// ----- the following is based on auto-generated code -----
|
||||
|
||||
public bool Equals(Unicolour? other)
|
||||
{
|
||||
if (ReferenceEquals(null, other)) return false;
|
||||
if (ReferenceEquals(this, other)) return true;
|
||||
return ColourSpaceEquals(other) && Alpha.Equals(other.Alpha);
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
if (ReferenceEquals(null, obj)) return false;
|
||||
if (ReferenceEquals(this, obj)) return true;
|
||||
if (obj.GetType() != this.GetType()) return false;
|
||||
return Equals((Unicolour)obj);
|
||||
}
|
||||
|
||||
private bool ColourSpaceEquals(Unicolour other)
|
||||
{
|
||||
return InitialRepresentation.Equals(other.InitialRepresentation);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
return (InitialRepresentation.GetHashCode() * 397) ^ Alpha.GetHashCode();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
4
Unicolour/Unicolour.csproj
Normal file
4
Unicolour/Unicolour.csproj
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup />
|
||||
</Project>
|
45
Unicolour/UnicolourConstructors.cs
Normal file
45
Unicolour/UnicolourConstructors.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
namespace Wacton.Unicolour
|
||||
{
|
||||
|
||||
public partial class Unicolour
|
||||
{
|
||||
/* standard constructors */
|
||||
public Unicolour(ColourSpace colourSpace, (double first, double second, double third) tuple,
|
||||
double alpha = 1.0) :
|
||||
this(colourSpace, tuple.first, tuple.second, tuple.third, alpha)
|
||||
{
|
||||
}
|
||||
|
||||
public Unicolour(ColourSpace colourSpace, double first, double second, double third, double alpha = 1.0) :
|
||||
this(colourSpace, Configuration.Default, first, second, third, alpha)
|
||||
{
|
||||
}
|
||||
|
||||
public Unicolour(ColourSpace colourSpace, Configuration config,
|
||||
(double first, double second, double third) tuple, double alpha = 1.0) :
|
||||
this(colourSpace, config, tuple.first, tuple.second, tuple.third, alpha)
|
||||
{
|
||||
}
|
||||
|
||||
public Unicolour(ColourSpace colourSpace, Configuration config,
|
||||
(double first, double second, double third, double alpha) tuple) :
|
||||
this(colourSpace, config, tuple.first, tuple.second, tuple.third, tuple.alpha)
|
||||
{
|
||||
}
|
||||
|
||||
public Unicolour(ColourSpace colourSpace, Configuration config, double first, double second, double third,
|
||||
double alpha = 1.0) :
|
||||
this(colourSpace, config, ColourHeritage.None, first, second, third, alpha)
|
||||
{
|
||||
}
|
||||
|
||||
/* special variations for hex */
|
||||
public Unicolour(string hex) : this(Configuration.Default, hex)
|
||||
{
|
||||
}
|
||||
|
||||
public Unicolour(Configuration config, string hex) : this(ColourSpace.Rgb, config, Utils.ParseColourHex(hex))
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
397
Unicolour/UnicolourLookups.cs
Normal file
397
Unicolour/UnicolourLookups.cs
Normal file
@@ -0,0 +1,397 @@
|
||||
using System;
|
||||
|
||||
namespace Wacton.Unicolour
|
||||
{
|
||||
/*
|
||||
* ColourSpace information needs to be held outwith ColourRepresentation object
|
||||
* otherwise the ColourRepresentation needs to be evaluated just to obtain the ColourSpace it represents
|
||||
*/
|
||||
public partial class Unicolour
|
||||
{
|
||||
private static ColourRepresentation CreateRepresentation(
|
||||
ColourSpace colourSpace, double first, double second, double third,
|
||||
Configuration config, ColourHeritage heritage)
|
||||
{
|
||||
return colourSpace switch
|
||||
{
|
||||
ColourSpace.Rgb => new Rgb(first, second, third, heritage),
|
||||
ColourSpace.RgbLinear => new RgbLinear(first, second, third, heritage),
|
||||
ColourSpace.Hsb => new Hsb(first, second, third, heritage),
|
||||
ColourSpace.Hsl => new Hsl(first, second, third, heritage),
|
||||
ColourSpace.Hwb => new Hwb(first, second, third, heritage),
|
||||
ColourSpace.Xyz => new Xyz(first, second, third, heritage),
|
||||
ColourSpace.Xyy => new Xyy(first, second, third, heritage),
|
||||
ColourSpace.Lab => new Lab(first, second, third, heritage),
|
||||
ColourSpace.Lchab => new Lchab(first, second, third, heritage),
|
||||
ColourSpace.Luv => new Luv(first, second, third, heritage),
|
||||
ColourSpace.Lchuv => new Lchuv(first, second, third, heritage),
|
||||
ColourSpace.Hsluv => new Hsluv(first, second, third, heritage),
|
||||
ColourSpace.Hpluv => new Hpluv(first, second, third, heritage),
|
||||
ColourSpace.Ictcp => new Ictcp(first, second, third, heritage),
|
||||
ColourSpace.Jzazbz => new Jzazbz(first, second, third, heritage),
|
||||
ColourSpace.Jzczhz => new Jzczhz(first, second, third, heritage),
|
||||
ColourSpace.Oklab => new Oklab(first, second, third, heritage),
|
||||
ColourSpace.Oklch => new Oklch(first, second, third, heritage),
|
||||
ColourSpace.Cam02 => new Cam02(new Cam.Ucs(first, second, third), config.Cam, heritage),
|
||||
ColourSpace.Cam16 => new Cam16(new Cam.Ucs(first, second, third), config.Cam, heritage),
|
||||
ColourSpace.Hct => new Hct(first, second, third, heritage),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(colourSpace), colourSpace, null)
|
||||
};
|
||||
}
|
||||
|
||||
public ColourRepresentation GetRepresentation(ColourSpace colourSpace)
|
||||
{
|
||||
return colourSpace switch
|
||||
{
|
||||
ColourSpace.Rgb => Rgb,
|
||||
ColourSpace.Rgb255 => Rgb.Byte255,
|
||||
ColourSpace.RgbLinear => RgbLinear,
|
||||
ColourSpace.Hsb => Hsb,
|
||||
ColourSpace.Hsl => Hsl,
|
||||
ColourSpace.Hwb => Hwb,
|
||||
ColourSpace.Xyz => Xyz,
|
||||
ColourSpace.Xyy => Xyy,
|
||||
ColourSpace.Lab => Lab,
|
||||
ColourSpace.Lchab => Lchab,
|
||||
ColourSpace.Luv => Luv,
|
||||
ColourSpace.Lchuv => Lchuv,
|
||||
ColourSpace.Hsluv => Hsluv,
|
||||
ColourSpace.Hpluv => Hpluv,
|
||||
ColourSpace.Ictcp => Ictcp,
|
||||
ColourSpace.Jzazbz => Jzazbz,
|
||||
ColourSpace.Jzczhz => Jzczhz,
|
||||
ColourSpace.Oklab => Oklab,
|
||||
ColourSpace.Oklch => Oklch,
|
||||
ColourSpace.Cam02 => Cam02,
|
||||
ColourSpace.Cam16 => Cam16,
|
||||
ColourSpace.Hct => Hct,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(colourSpace), colourSpace, null)
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
* getting a value will trigger a chain of gets and conversions if the intermediary values have not been calculated yet
|
||||
* e.g. if Unicolour is created from RGB, and the first request is for LAB:
|
||||
* - Get(ColourSpace.Lab); lab is null, execute: lab = Lab.FromXyz(Xyz, Config)
|
||||
* - Get(ColourSpace.Xyz); xyz is null, execute: xyz = Rgb.ToXyz(Rgb, Config)
|
||||
* - Get(ColourSpace.Rgb); rgb is not null, return value
|
||||
* - xyz is evaluated from rgb and stored
|
||||
* - lab is evaluated from xyz and stored
|
||||
*/
|
||||
private T Get<T>(ColourSpace targetSpace) where T : ColourRepresentation
|
||||
{
|
||||
var backingRepresentation = GetBackingField(targetSpace);
|
||||
if (backingRepresentation == null)
|
||||
{
|
||||
SetBackingField(targetSpace);
|
||||
backingRepresentation = GetBackingField(targetSpace);
|
||||
}
|
||||
|
||||
return (backingRepresentation as T)!;
|
||||
}
|
||||
|
||||
private ColourRepresentation? GetBackingField(ColourSpace colourSpace)
|
||||
{
|
||||
return colourSpace switch
|
||||
{
|
||||
ColourSpace.Rgb => rgb,
|
||||
ColourSpace.RgbLinear => rgbLinear,
|
||||
ColourSpace.Hsb => hsb,
|
||||
ColourSpace.Hsl => hsl,
|
||||
ColourSpace.Hwb => hwb,
|
||||
ColourSpace.Xyz => xyz,
|
||||
ColourSpace.Xyy => xyy,
|
||||
ColourSpace.Lab => lab,
|
||||
ColourSpace.Lchab => lchab,
|
||||
ColourSpace.Luv => luv,
|
||||
ColourSpace.Lchuv => lchuv,
|
||||
ColourSpace.Hsluv => hsluv,
|
||||
ColourSpace.Hpluv => hpluv,
|
||||
ColourSpace.Ictcp => ictcp,
|
||||
ColourSpace.Jzazbz => jzazbz,
|
||||
ColourSpace.Jzczhz => jzczhz,
|
||||
ColourSpace.Oklab => oklab,
|
||||
ColourSpace.Oklch => oklch,
|
||||
ColourSpace.Cam02 => cam02,
|
||||
ColourSpace.Cam16 => cam16,
|
||||
ColourSpace.Hct => hct,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(colourSpace), colourSpace, null)
|
||||
};
|
||||
}
|
||||
|
||||
private void SetBackingField(ColourSpace targetSpace)
|
||||
{
|
||||
Action setField = targetSpace switch
|
||||
{
|
||||
ColourSpace.Rgb => () => rgb = EvaluateRgb(),
|
||||
ColourSpace.RgbLinear => () => rgbLinear = EvaluateRgbLinear(),
|
||||
ColourSpace.Hsb => () => hsb = EvaluateHsb(),
|
||||
ColourSpace.Hsl => () => hsl = EvaluateHsl(),
|
||||
ColourSpace.Hwb => () => hwb = EvaluateHwb(),
|
||||
ColourSpace.Xyz => () => xyz = EvaluateXyz(),
|
||||
ColourSpace.Xyy => () => xyy = EvaluateXyy(),
|
||||
ColourSpace.Lab => () => lab = EvaluateLab(),
|
||||
ColourSpace.Lchab => () => lchab = EvaluateLchab(),
|
||||
ColourSpace.Luv => () => luv = EvaluateLuv(),
|
||||
ColourSpace.Lchuv => () => lchuv = EvaluateLchuv(),
|
||||
ColourSpace.Hsluv => () => hsluv = EvaluateHsluv(),
|
||||
ColourSpace.Hpluv => () => hpluv = EvaluateHpluv(),
|
||||
ColourSpace.Ictcp => () => ictcp = EvaluateIctcp(),
|
||||
ColourSpace.Jzazbz => () => jzazbz = EvaluateJzazbz(),
|
||||
ColourSpace.Jzczhz => () => jzczhz = EvaluateJzczhz(),
|
||||
ColourSpace.Oklab => () => oklab = EvaluateOklab(),
|
||||
ColourSpace.Oklch => () => oklch = EvaluateOklch(),
|
||||
ColourSpace.Cam02 => () => cam02 = EvaluateCam02(),
|
||||
ColourSpace.Cam16 => () => cam16 = EvaluateCam16(),
|
||||
ColourSpace.Hct => () => hct = EvaluateHct(),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(targetSpace), targetSpace, null)
|
||||
};
|
||||
|
||||
setField();
|
||||
}
|
||||
|
||||
/*
|
||||
* evaluation method switch expressions are arranged as follows:
|
||||
* - first item = target space is the initial space, simply return initial representation
|
||||
* - middle items = reverse transforms to the target space; only the immediate transforms
|
||||
* - default item = forward transform from a base space
|
||||
* -----------------
|
||||
* only need to consider the transforms relative to the target-space, as subsequent transforms are handled recursively
|
||||
* e.g. for target-space RGB...
|
||||
* - starting at HSL:
|
||||
* - transforms: HSL ==reverse==> HSB ==reverse==> RGB
|
||||
* - only need to specify: HSB ==reverse==> RGB
|
||||
* - function: Hsb.ToRgb()
|
||||
* - starting at LAB:
|
||||
* - LAB ==reverse==> XYZ ==forward==> RGB Linear ==forward==> RGB
|
||||
* - only need to specify: RGB Linear ==forward==> RGB
|
||||
* - function: Rgb.FromRgbLinear()
|
||||
*/
|
||||
|
||||
private Rgb EvaluateRgb()
|
||||
{
|
||||
return InitialColourSpace switch
|
||||
{
|
||||
ColourSpace.Rgb => (Rgb)InitialRepresentation,
|
||||
ColourSpace.Hsb => Hsb.ToRgb(Hsb),
|
||||
ColourSpace.Hsl => Hsb.ToRgb(Hsb),
|
||||
ColourSpace.Hwb => Hsb.ToRgb(Hsb),
|
||||
_ => Rgb.FromRgbLinear(RgbLinear, Config.Rgb)
|
||||
};
|
||||
}
|
||||
|
||||
private RgbLinear EvaluateRgbLinear()
|
||||
{
|
||||
return InitialColourSpace switch
|
||||
{
|
||||
ColourSpace.RgbLinear => (RgbLinear)InitialRepresentation,
|
||||
ColourSpace.Rgb => Rgb.ToRgbLinear(Rgb, Config.Rgb),
|
||||
ColourSpace.Hsb => Rgb.ToRgbLinear(Rgb, Config.Rgb),
|
||||
ColourSpace.Hsl => Rgb.ToRgbLinear(Rgb, Config.Rgb),
|
||||
ColourSpace.Hwb => Rgb.ToRgbLinear(Rgb, Config.Rgb),
|
||||
_ => RgbLinear.FromXyz(Xyz, Config.Rgb, Config.Xyz)
|
||||
};
|
||||
}
|
||||
|
||||
private Hsb EvaluateHsb()
|
||||
{
|
||||
return InitialColourSpace switch
|
||||
{
|
||||
ColourSpace.Hsb => (Hsb)InitialRepresentation,
|
||||
ColourSpace.Hsl => Hsl.ToHsb(Hsl),
|
||||
ColourSpace.Hwb => Hwb.ToHsb(Hwb),
|
||||
_ => Hsb.FromRgb(Rgb)
|
||||
};
|
||||
}
|
||||
|
||||
private Hsl EvaluateHsl()
|
||||
{
|
||||
return InitialColourSpace switch
|
||||
{
|
||||
ColourSpace.Hsl => (Hsl)InitialRepresentation,
|
||||
_ => Hsl.FromHsb(Hsb)
|
||||
};
|
||||
}
|
||||
|
||||
private Hwb EvaluateHwb()
|
||||
{
|
||||
return InitialColourSpace switch
|
||||
{
|
||||
ColourSpace.Hwb => (Hwb)InitialRepresentation,
|
||||
_ => Hwb.FromHsb(Hsb)
|
||||
};
|
||||
}
|
||||
|
||||
private Xyz EvaluateXyz()
|
||||
{
|
||||
return InitialColourSpace switch
|
||||
{
|
||||
ColourSpace.Xyz => (Xyz)InitialRepresentation,
|
||||
ColourSpace.Rgb => RgbLinear.ToXyz(RgbLinear, Config.Rgb, Config.Xyz),
|
||||
ColourSpace.RgbLinear => RgbLinear.ToXyz(RgbLinear, Config.Rgb, Config.Xyz),
|
||||
ColourSpace.Hsb => RgbLinear.ToXyz(RgbLinear, Config.Rgb, Config.Xyz),
|
||||
ColourSpace.Hsl => RgbLinear.ToXyz(RgbLinear, Config.Rgb, Config.Xyz),
|
||||
ColourSpace.Hwb => RgbLinear.ToXyz(RgbLinear, Config.Rgb, Config.Xyz),
|
||||
ColourSpace.Xyy => Xyy.ToXyz(Xyy),
|
||||
ColourSpace.Lab => Lab.ToXyz(Lab, Config.Xyz),
|
||||
ColourSpace.Lchab => Lab.ToXyz(Lab, Config.Xyz),
|
||||
ColourSpace.Luv => Luv.ToXyz(Luv, Config.Xyz),
|
||||
ColourSpace.Lchuv => Luv.ToXyz(Luv, Config.Xyz),
|
||||
ColourSpace.Hsluv => Luv.ToXyz(Luv, Config.Xyz),
|
||||
ColourSpace.Hpluv => Luv.ToXyz(Luv, Config.Xyz),
|
||||
ColourSpace.Ictcp => Ictcp.ToXyz(Ictcp, Config.IctcpScalar, Config.Xyz),
|
||||
ColourSpace.Jzazbz => Jzazbz.ToXyz(Jzazbz, Config.JzazbzScalar, Config.Xyz),
|
||||
ColourSpace.Jzczhz => Jzazbz.ToXyz(Jzazbz, Config.JzazbzScalar, Config.Xyz),
|
||||
ColourSpace.Oklab => Oklab.ToXyz(Oklab, Config.Xyz),
|
||||
ColourSpace.Oklch => Oklab.ToXyz(Oklab, Config.Xyz),
|
||||
ColourSpace.Cam02 => Cam02.ToXyz(Cam02, Config.Cam, Config.Xyz),
|
||||
ColourSpace.Cam16 => Cam16.ToXyz(Cam16, Config.Cam, Config.Xyz),
|
||||
ColourSpace.Hct => Hct.ToXyz(Hct, Config.Xyz),
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
};
|
||||
}
|
||||
|
||||
private Xyy EvaluateXyy()
|
||||
{
|
||||
return InitialColourSpace switch
|
||||
{
|
||||
ColourSpace.Xyy => (Xyy)InitialRepresentation,
|
||||
_ => Xyy.FromXyz(Xyz, Config.Xyz)
|
||||
};
|
||||
}
|
||||
|
||||
private Lab EvaluateLab()
|
||||
{
|
||||
return InitialColourSpace switch
|
||||
{
|
||||
ColourSpace.Lab => (Lab)InitialRepresentation,
|
||||
ColourSpace.Lchab => Lchab.ToLab(Lchab),
|
||||
_ => Lab.FromXyz(Xyz, Config.Xyz)
|
||||
};
|
||||
}
|
||||
|
||||
private Lchab EvaluateLchab()
|
||||
{
|
||||
return InitialColourSpace switch
|
||||
{
|
||||
ColourSpace.Lchab => (Lchab)InitialRepresentation,
|
||||
_ => Lchab.FromLab(Lab)
|
||||
};
|
||||
}
|
||||
|
||||
private Luv EvaluateLuv()
|
||||
{
|
||||
return InitialColourSpace switch
|
||||
{
|
||||
ColourSpace.Luv => (Luv)InitialRepresentation,
|
||||
ColourSpace.Lchuv => Lchuv.ToLuv(Lchuv),
|
||||
ColourSpace.Hsluv => Lchuv.ToLuv(Lchuv),
|
||||
ColourSpace.Hpluv => Lchuv.ToLuv(Lchuv),
|
||||
_ => Luv.FromXyz(Xyz, Config.Xyz)
|
||||
};
|
||||
}
|
||||
|
||||
private Lchuv EvaluateLchuv()
|
||||
{
|
||||
return InitialColourSpace switch
|
||||
{
|
||||
ColourSpace.Lchuv => (Lchuv)InitialRepresentation,
|
||||
ColourSpace.Hsluv => Hsluv.ToLchuv(Hsluv),
|
||||
ColourSpace.Hpluv => Hpluv.ToLchuv(Hpluv),
|
||||
_ => Lchuv.FromLuv(Luv)
|
||||
};
|
||||
}
|
||||
|
||||
private Hsluv EvaluateHsluv()
|
||||
{
|
||||
return InitialColourSpace switch
|
||||
{
|
||||
ColourSpace.Hsluv => (Hsluv)InitialRepresentation,
|
||||
_ => Hsluv.FromLchuv(Lchuv)
|
||||
};
|
||||
}
|
||||
|
||||
private Hpluv EvaluateHpluv()
|
||||
{
|
||||
return InitialColourSpace switch
|
||||
{
|
||||
ColourSpace.Hpluv => (Hpluv)InitialRepresentation,
|
||||
_ => Hpluv.FromLchuv(Lchuv)
|
||||
};
|
||||
}
|
||||
|
||||
private Ictcp EvaluateIctcp()
|
||||
{
|
||||
return InitialColourSpace switch
|
||||
{
|
||||
ColourSpace.Ictcp => (Ictcp)InitialRepresentation,
|
||||
_ => Ictcp.FromXyz(Xyz, Config.IctcpScalar, Config.Xyz)
|
||||
};
|
||||
}
|
||||
|
||||
private Jzazbz EvaluateJzazbz()
|
||||
{
|
||||
return InitialColourSpace switch
|
||||
{
|
||||
ColourSpace.Jzazbz => (Jzazbz)InitialRepresentation,
|
||||
ColourSpace.Jzczhz => Jzczhz.ToJzazbz(Jzczhz),
|
||||
_ => Jzazbz.FromXyz(Xyz, Config.JzazbzScalar, Config.Xyz)
|
||||
};
|
||||
}
|
||||
|
||||
private Jzczhz EvaluateJzczhz()
|
||||
{
|
||||
return InitialColourSpace switch
|
||||
{
|
||||
ColourSpace.Jzczhz => (Jzczhz)InitialRepresentation,
|
||||
_ => Jzczhz.FromJzazbz(Jzazbz)
|
||||
};
|
||||
}
|
||||
|
||||
private Oklab EvaluateOklab()
|
||||
{
|
||||
return InitialColourSpace switch
|
||||
{
|
||||
ColourSpace.Oklab => (Oklab)InitialRepresentation,
|
||||
ColourSpace.Oklch => Oklch.ToOklab(Oklch),
|
||||
_ => Oklab.FromXyz(Xyz, Config.Xyz)
|
||||
};
|
||||
}
|
||||
|
||||
private Oklch EvaluateOklch()
|
||||
{
|
||||
return InitialColourSpace switch
|
||||
{
|
||||
ColourSpace.Oklch => (Oklch)InitialRepresentation,
|
||||
_ => Oklch.FromOklab(Oklab)
|
||||
};
|
||||
}
|
||||
|
||||
private Cam02 EvaluateCam02()
|
||||
{
|
||||
return InitialColourSpace switch
|
||||
{
|
||||
ColourSpace.Cam02 => (Cam02)InitialRepresentation,
|
||||
_ => Cam02.FromXyz(Xyz, Config.Cam, Config.Xyz)
|
||||
};
|
||||
}
|
||||
|
||||
private Cam16 EvaluateCam16()
|
||||
{
|
||||
return InitialColourSpace switch
|
||||
{
|
||||
ColourSpace.Cam16 => (Cam16)InitialRepresentation,
|
||||
_ => Cam16.FromXyz(Xyz, Config.Cam, Config.Xyz)
|
||||
};
|
||||
}
|
||||
|
||||
private Hct EvaluateHct()
|
||||
{
|
||||
return InitialColourSpace switch
|
||||
{
|
||||
ColourSpace.Hct => (Hct)InitialRepresentation,
|
||||
_ => Hct.FromXyz(Xyz, Config.Xyz)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
77
Unicolour/Utils.cs
Normal file
77
Unicolour/Utils.cs
Normal file
@@ -0,0 +1,77 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace Wacton.Unicolour
|
||||
{
|
||||
|
||||
using System.Globalization;
|
||||
|
||||
internal static class Utils
|
||||
{
|
||||
internal static double Clamp(this double x, double min, double max) => x < min ? min : x > max ? max : x;
|
||||
internal static double Clamp(this int x, int min, int max) => x < min ? min : x > max ? max : x;
|
||||
internal static double CubeRoot(double x) => x < 0 ? -Math.Pow(-x, 1 / 3.0) : Math.Pow(x, 1 / 3.0);
|
||||
internal static double ToDegrees(double radians) => radians * (180.0 / Math.PI);
|
||||
internal static double ToRadians(double degrees) => degrees * (Math.PI / 180.0);
|
||||
|
||||
internal static double Modulo(this double value, double modulus)
|
||||
{
|
||||
if (double.IsNaN(value))
|
||||
{
|
||||
return double.NaN;
|
||||
}
|
||||
|
||||
var remainder = value % modulus;
|
||||
if (remainder == 0.0)
|
||||
{
|
||||
return remainder;
|
||||
}
|
||||
|
||||
// handles negatives, e.g. -10 % 360 returns 350 instead of -10
|
||||
// don't "add a negative" if both values are negative
|
||||
var useSubtraction = remainder < 0 ^ modulus < 0;
|
||||
return useSubtraction ? modulus + remainder : remainder;
|
||||
}
|
||||
|
||||
internal static (double r, double g, double b, double a) ParseColourHex(string colourHex)
|
||||
{
|
||||
var hex = colourHex.TrimStart('#');
|
||||
if (hex.Length is not (6 or 8))
|
||||
{
|
||||
throw new ArgumentException($"{colourHex} contains invalid number of characters");
|
||||
}
|
||||
|
||||
var r = Parse(hex, 0) / 255.0;
|
||||
var g = Parse(hex, 2) / 255.0;
|
||||
var b = Parse(hex, 4) / 255.0;
|
||||
var a = hex.Length == 8 ? Parse(hex, 6) / 255.0 : 1.0;
|
||||
return (r, g, b, a);
|
||||
}
|
||||
|
||||
private static int Parse(string hex, int startIndex)
|
||||
{
|
||||
var chars = hex.Substring(startIndex, 2).ToUpper();
|
||||
if (chars.Any(x => !Uri.IsHexDigit(x)))
|
||||
{
|
||||
throw new ArgumentException($"{chars} cannot be parsed as hex");
|
||||
}
|
||||
|
||||
return int.Parse(chars, NumberStyles.HexNumber);
|
||||
}
|
||||
|
||||
internal static ColourTriplet ToLchTriplet(double lightness, double axis1, double axis2)
|
||||
{
|
||||
var chroma = Math.Sqrt(Math.Pow(axis1, 2) + Math.Pow(axis2, 2));
|
||||
var hue = ToDegrees(Math.Atan2(axis2, axis1));
|
||||
return new ColourTriplet(lightness, chroma, hue.Modulo(360.0));
|
||||
}
|
||||
|
||||
internal static (double lightness, double axis1, double axis2) FromLchTriplet(ColourTriplet lchTriplet)
|
||||
{
|
||||
var (l, c, h) = lchTriplet;
|
||||
var axis1 = c * Math.Cos(ToRadians(h));
|
||||
var axis2 = c * Math.Sin(ToRadians(h));
|
||||
return (l, axis1, axis2);
|
||||
}
|
||||
}
|
||||
}
|
52
Unicolour/VisionDeficiency.cs
Normal file
52
Unicolour/VisionDeficiency.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
namespace Wacton.Unicolour
|
||||
{
|
||||
|
||||
// https://www.inf.ufrgs.br/~oliveira/pubs_files/CVD_Simulation/CVD_Simulation.html
|
||||
// only using full severity matrices to simulate extreme cases
|
||||
internal static class VisionDeficiency
|
||||
{
|
||||
private static readonly Matrix Protanomaly = new(new[,]
|
||||
{
|
||||
{ +0.152286, +1.052583, -0.204868 },
|
||||
{ +0.114503, +0.786281, +0.099216 },
|
||||
{ -0.003882, -0.048116, +1.051998 }
|
||||
});
|
||||
|
||||
private static readonly Matrix Deuteranomaly = new(new[,]
|
||||
{
|
||||
{ +0.367322, +0.860646, -0.227968 },
|
||||
{ +0.280085, +0.672501, +0.047413 },
|
||||
{ -0.011820, +0.042940, +0.968881 }
|
||||
});
|
||||
|
||||
private static readonly Matrix Tritanomaly = new(new[,]
|
||||
{
|
||||
{ +1.255528, -0.076749, -0.178779 },
|
||||
{ -0.078411, +0.930809, +0.147602 },
|
||||
{ +0.004733, +0.691367, +0.303900 }
|
||||
});
|
||||
|
||||
private static Unicolour SimulateCvd(Unicolour unicolour, Matrix cvdMatrix)
|
||||
{
|
||||
var config = unicolour.Config;
|
||||
|
||||
// since simulated RGB-Linear often results in values outwith 0 - 1, seems unnecessary to use constrained inputs
|
||||
var rgbLinearMatrix = Matrix.FromTriplet(unicolour.RgbLinear.Triplet);
|
||||
var simulatedRgbLinearMatrix = cvdMatrix.Multiply(rgbLinearMatrix);
|
||||
return new Unicolour(ColourSpace.RgbLinear, config, simulatedRgbLinearMatrix.ToTriplet().Tuple);
|
||||
}
|
||||
|
||||
internal static Unicolour SimulateProtanopia(Unicolour unicolour) => SimulateCvd(unicolour, Protanomaly);
|
||||
internal static Unicolour SimulateDeuteranopia(Unicolour unicolour) => SimulateCvd(unicolour, Deuteranomaly);
|
||||
internal static Unicolour SimulateTritanopia(Unicolour unicolour) => SimulateCvd(unicolour, Tritanomaly);
|
||||
|
||||
internal static Unicolour SimulateAchromatopsia(Unicolour unicolour)
|
||||
{
|
||||
var config = unicolour.Config;
|
||||
|
||||
// luminance is based on Linear RGB, so needs to be companded back into chosen RGB space
|
||||
var rgbLuminance = config.Rgb.CompandFromLinear(unicolour.RelativeLuminance);
|
||||
return new Unicolour(ColourSpace.Rgb, config, rgbLuminance, rgbLuminance, rgbLuminance);
|
||||
}
|
||||
}
|
||||
}
|
38
Unicolour/Wacton.Unicolour.csproj
Normal file
38
Unicolour/Wacton.Unicolour.csproj
Normal file
@@ -0,0 +1,38 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AssemblyName>Wacton.Unicolour</AssemblyName>
|
||||
<RootNamespace>Wacton.Unicolour</RootNamespace>
|
||||
<PackageId>$(AssemblyName)</PackageId>
|
||||
<Authors>William Acton</Authors>
|
||||
<Description>🌈 Colour / Color conversion, interpolation, and comparison for .NET</Description>
|
||||
<Copyright>William Acton</Copyright>
|
||||
<PackageProjectUrl>https://github.com/waacton/Unicolour</PackageProjectUrl>
|
||||
<RepositoryUrl>https://github.com/waacton/Unicolour</RepositoryUrl>
|
||||
<LangVersion>Preview</LangVersion>
|
||||
<TargetFramework>NetStandard2.0</TargetFramework>
|
||||
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
|
||||
<PackageIcon>Resources\Unicolour.png</PackageIcon>
|
||||
<PackageVersion>3.0.0</PackageVersion>
|
||||
<PackageTags>colour color RGB HSB HSV HSL HWB XYZ xyY LAB LUV LCH LCHab LCHuv HSLuv HPLuv ICtCp JzAzBz JzCzHz Oklab Oklch CAM02 CAM16 HCT converter colour-converter colour-conversion color-converter color-conversion colour-space colour-spaces color-space color-spaces interpolation colour-interpolation color-interpolation colour-mixing color-mixing comparison colour-comparison color-comparison contrast luminance deltaE chromaticity display-p3 rec-2020 gamut-mapping temperature cct duv cvd colour-vision-deficiency color-vision-deficiency colour-blindness color-blindness protanopia deuteranopia tritanopia achromatopsia</PackageTags>
|
||||
<PackageReleaseNotes>Add gamut mapping, support premultiplied alpha interpolation, and improve API</PackageReleaseNotes>
|
||||
<ApplicationIcon>Resources\Unicolour.ico</ApplicationIcon>
|
||||
<PackageLicenseFile>LICENSE</PackageLicenseFile>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="Resources\Unicolour.png">
|
||||
<Pack>True</Pack>
|
||||
<PackagePath>\</PackagePath>
|
||||
</None>
|
||||
|
||||
<None Include="LICENSE">
|
||||
<Pack>True</Pack>
|
||||
<PackagePath>\</PackagePath>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
80
Unicolour/WhitePoint.cs
Normal file
80
Unicolour/WhitePoint.cs
Normal file
@@ -0,0 +1,80 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Wacton.Unicolour
|
||||
{
|
||||
|
||||
public record WhitePoint(double X, double Y, double Z)
|
||||
{
|
||||
public double X { get; } = X;
|
||||
public double Y { get; } = Y;
|
||||
public double Z { get; } = Z;
|
||||
internal Matrix AsXyzMatrix() => Matrix.FromTriplet(X, Y, Z).Select(x => x / 100.0);
|
||||
public override string ToString() => $"({X}, {Y}, {Z})";
|
||||
|
||||
public static WhitePoint From(Illuminant illuminant, Observer observer = Observer.Standard2)
|
||||
{
|
||||
return ByIlluminant[observer][illuminant];
|
||||
}
|
||||
|
||||
public static WhitePoint From(Chromaticity chromaticity)
|
||||
{
|
||||
var xyz = Xyy.ToXyz(new(chromaticity.X, chromaticity.Y, 1));
|
||||
return new WhitePoint(xyz.X * 100, xyz.Y * 100, xyz.Z * 100);
|
||||
}
|
||||
|
||||
// as far as I'm aware, these are the latest ASTM standards
|
||||
private static readonly Dictionary<Observer, Dictionary<Illuminant, WhitePoint>> ByIlluminant = new()
|
||||
{
|
||||
{
|
||||
Observer.Standard2, new()
|
||||
{
|
||||
{ Illuminant.A, new(109.850, 100.000, 35.585) },
|
||||
{ Illuminant.C, new(98.074, 100.000, 118.232) },
|
||||
{ Illuminant.D50, new(96.422, 100.000, 82.521) },
|
||||
{ Illuminant.D55, new(95.682, 100.000, 92.149) },
|
||||
{ Illuminant.D65, new(95.047, 100.000, 108.883) },
|
||||
{ Illuminant.D75, new(94.972, 100.000, 122.638) },
|
||||
{ Illuminant.E, new(100.000, 100.000, 100.000) },
|
||||
{ Illuminant.F2, new(99.186, 100.000, 67.393) },
|
||||
{ Illuminant.F7, new(95.041, 100.000, 108.747) },
|
||||
{ Illuminant.F11, new(100.962, 100.000, 64.350) }
|
||||
}
|
||||
},
|
||||
{
|
||||
Observer.Supplementary10, new()
|
||||
{
|
||||
{ Illuminant.A, new(111.144, 100.000, 35.200) },
|
||||
{ Illuminant.C, new(97.285, 100.000, 116.145) },
|
||||
{ Illuminant.D50, new(96.720, 100.000, 81.427) },
|
||||
{ Illuminant.D55, new(95.799, 100.000, 90.926) },
|
||||
{ Illuminant.D65, new(94.811, 100.000, 107.304) },
|
||||
{ Illuminant.D75, new(94.416, 100.000, 120.641) },
|
||||
{ Illuminant.E, new(100.000, 100.000, 100.000) },
|
||||
{ Illuminant.F2, new(103.279, 100.000, 69.027) },
|
||||
{ Illuminant.F7, new(95.792, 100.000, 107.686) },
|
||||
{ Illuminant.F11, new(103.863, 100.000, 65.607) }
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public enum Illuminant
|
||||
{
|
||||
A,
|
||||
C,
|
||||
D50,
|
||||
D55,
|
||||
D65,
|
||||
D75,
|
||||
E,
|
||||
F2,
|
||||
F7,
|
||||
F11
|
||||
}
|
||||
|
||||
public enum Observer
|
||||
{
|
||||
Standard2,
|
||||
Supplementary10
|
||||
}
|
||||
}
|
65
Unicolour/Xyy.cs
Normal file
65
Unicolour/Xyy.cs
Normal file
@@ -0,0 +1,65 @@
|
||||
using System;
|
||||
|
||||
namespace Wacton.Unicolour
|
||||
{
|
||||
|
||||
public record Xyy : ColourRepresentation
|
||||
{
|
||||
protected override int? HueIndex => null;
|
||||
public Chromaticity Chromaticity => new(First, Second);
|
||||
public double Luminance => Third;
|
||||
public Chromaticity ConstrainedChromaticity => new(ConstrainedFirst, ConstrainedSecond);
|
||||
public double ConstrainedLuminance => ConstrainedThird;
|
||||
protected override double ConstrainedFirst => Math.Max(Chromaticity.X, 0);
|
||||
protected override double ConstrainedSecond => Math.Max(Chromaticity.Y, 0);
|
||||
protected override double ConstrainedThird => Math.Max(Luminance, 0);
|
||||
|
||||
// could compare chromaticity against config.ChromaticityWhite
|
||||
// but requires making assumptions about floating-point comparison, which I don't want to do
|
||||
internal override bool IsGreyscale => Luminance <= 0.0;
|
||||
|
||||
public Xyy(double x, double y, double upperY) : this(x, y, upperY, ColourHeritage.None)
|
||||
{
|
||||
}
|
||||
|
||||
internal Xyy(double x, double y, double upperY, ColourHeritage heritage) : base(x, y, upperY, heritage)
|
||||
{
|
||||
}
|
||||
|
||||
protected override string FirstString => $"{Chromaticity.X:F4}";
|
||||
protected override string SecondString => $"{Chromaticity.Y:F4}";
|
||||
protected override string ThirdString => $"{Luminance:F4}";
|
||||
public override string ToString() => base.ToString();
|
||||
|
||||
/*
|
||||
* XYY is a transform of XYZ (in terms of Unicolour implementation)
|
||||
* Forward: https://en.wikipedia.org/wiki/CIE_1931_color_space#CIE_xy_chromaticity_diagram_and_the_CIE_xyY_color_space
|
||||
* Reverse: https://en.wikipedia.org/wiki/CIE_1931_color_space#CIE_xy_chromaticity_diagram_and_the_CIE_xyY_color_space
|
||||
*/
|
||||
|
||||
internal static Xyy FromXyz(Xyz xyz, XyzConfiguration xyzConfig)
|
||||
{
|
||||
var (x, y, z) = xyz.Triplet;
|
||||
var normalisation = x + y + z;
|
||||
var isBlack = normalisation == 0.0;
|
||||
|
||||
var chromaticityX = isBlack ? xyzConfig.ChromaticityWhite.X : x / normalisation;
|
||||
var chromaticityY = isBlack ? xyzConfig.ChromaticityWhite.Y : y / normalisation;
|
||||
var luminance = isBlack ? 0 : y;
|
||||
return new Xyy(chromaticityX, chromaticityY, luminance, ColourHeritage.From(xyz));
|
||||
}
|
||||
|
||||
internal static Xyz ToXyz(Xyy xyy)
|
||||
{
|
||||
var chromaticity = xyy.ConstrainedChromaticity;
|
||||
var luminance = xyy.ConstrainedLuminance;
|
||||
|
||||
var useZero = chromaticity.Y <= 0;
|
||||
var factor = luminance / chromaticity.Y;
|
||||
var x = useZero ? 0 : factor * chromaticity.X;
|
||||
var y = useZero ? 0 : luminance;
|
||||
var z = useZero ? 0 : factor * (1 - chromaticity.X - chromaticity.Y);
|
||||
return new Xyz(x, y, z, ColourHeritage.From(xyy));
|
||||
}
|
||||
}
|
||||
}
|
42
Unicolour/Xyz.cs
Normal file
42
Unicolour/Xyz.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
namespace Wacton.Unicolour
|
||||
{
|
||||
|
||||
public record Xyz : ColourRepresentation
|
||||
{
|
||||
protected override int? HueIndex => null;
|
||||
public double X => First;
|
||||
public double Y => Second;
|
||||
public double Z => Third;
|
||||
|
||||
// no clear luminance upper-bound; usually Y >= 1 is max luminance
|
||||
// but since custom white points can be provided, don't want to make the assumption
|
||||
internal override bool IsGreyscale => Y <= 0;
|
||||
|
||||
public Xyz(double x, double y, double z) : this(x, y, z, ColourHeritage.None)
|
||||
{
|
||||
}
|
||||
|
||||
internal Xyz(ColourTriplet triplet, ColourHeritage heritage) : this(triplet.First, triplet.Second,
|
||||
triplet.Third, heritage)
|
||||
{
|
||||
}
|
||||
|
||||
internal Xyz(double x, double y, double z, ColourHeritage heritage) : base(x, y, z, heritage)
|
||||
{
|
||||
}
|
||||
|
||||
protected override string FirstString => $"{X:F4}";
|
||||
protected override string SecondString => $"{Y:F4}";
|
||||
protected override string ThirdString => $"{Z:F4}";
|
||||
public override string ToString() => base.ToString();
|
||||
|
||||
/*
|
||||
* XYZ is considered the root colour representation (in terms of Unicolour implementation)
|
||||
* so does not contain any forward (from another space) or reverse (back to original space) functions
|
||||
*/
|
||||
|
||||
// only for potential debugging or diagnostics
|
||||
// until there is an "official" HCT -> XYZ reverse transform
|
||||
internal HctToXyzSearchResult? HctToXyzSearchResult;
|
||||
}
|
||||
}
|
25
Unicolour/XyzConfiguration.cs
Normal file
25
Unicolour/XyzConfiguration.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
namespace Wacton.Unicolour
|
||||
{
|
||||
|
||||
public class XyzConfiguration
|
||||
{
|
||||
public WhitePoint WhitePoint { get; }
|
||||
public Chromaticity ChromaticityWhite { get; }
|
||||
|
||||
public static readonly XyzConfiguration D65 = new(WhitePoint.From(Illuminant.D65));
|
||||
public static readonly XyzConfiguration D50 = new(WhitePoint.From(Illuminant.D50));
|
||||
|
||||
public XyzConfiguration(WhitePoint whitePoint)
|
||||
{
|
||||
WhitePoint = whitePoint;
|
||||
|
||||
var x = WhitePoint.X / 100.0;
|
||||
var y = WhitePoint.Y / 100.0;
|
||||
var z = WhitePoint.Z / 100.0;
|
||||
var normalisation = x + y + z;
|
||||
ChromaticityWhite = new(x / normalisation, y / normalisation);
|
||||
}
|
||||
|
||||
public override string ToString() => $"XYZ {WhitePoint}";
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user