From dc66c238e1cbce6d81bca20e7eb8cb53fcb3b399 Mon Sep 17 00:00:00 2001 From: Chandan Date: Sat, 25 Oct 2025 15:22:41 +0530 Subject: [PATCH 1/2] Add comprehensive iOS demo app showcasing ColorsKit features & update README details. --- .../ConsumerSample/ConsumerSample.swift | 2 +- .../ColorsKitDemo.xcodeproj/project.pbxproj | 378 +++++++ Example/iOSAppDemo/ColorsKitDemo/App.swift | 10 + .../ColorsKitDemo/BlendingModesView.swift | 184 ++++ .../ColorsKitDemo/ColorHarmonyView.swift | 134 +++ .../ColorsKitDemo/ColorPsychologyView.swift | 228 +++++ .../ColorsKitDemo/ContentView.swift | 68 ++ .../ColorsKitDemo/DataVisualizationView.swift | 232 +++++ .../ColorsKitDemo/PerceptualColorsView.swift | 220 +++++ .../TemperatureGradientsView.swift | 358 +++++++ Example/iOSAppDemo/Package.swift | 28 + Example/iOSAppDemo/README.md | 133 ++- Example/iOSAppDemo/Sources/App.swift | 10 + .../Sources/BlendingModesView.swift | 184 ++++ .../iOSAppDemo/Sources/ColorHarmonyView.swift | 134 +++ .../Sources/ColorPsychologyView.swift | 228 +++++ Example/iOSAppDemo/Sources/ContentView.swift | 68 ++ .../Sources/DataVisualizationView.swift | 232 +++++ .../Sources/PerceptualColorsView.swift | 220 +++++ .../Sources/TemperatureGradientsView.swift | 358 +++++++ Sources/ColorCore/ColorCore.swift | 931 +++++++++++++++++- Sources/ColorPalettes/ColorPalettes.swift | 276 +++++- Sources/ColorUtilities/ColorUtilities.swift | 858 +++++++++++++++- 23 files changed, 5411 insertions(+), 63 deletions(-) create mode 100644 Example/iOSAppDemo/ColorsKitDemo.xcodeproj/project.pbxproj create mode 100644 Example/iOSAppDemo/ColorsKitDemo/App.swift create mode 100644 Example/iOSAppDemo/ColorsKitDemo/BlendingModesView.swift create mode 100644 Example/iOSAppDemo/ColorsKitDemo/ColorHarmonyView.swift create mode 100644 Example/iOSAppDemo/ColorsKitDemo/ColorPsychologyView.swift create mode 100644 Example/iOSAppDemo/ColorsKitDemo/ContentView.swift create mode 100644 Example/iOSAppDemo/ColorsKitDemo/DataVisualizationView.swift create mode 100644 Example/iOSAppDemo/ColorsKitDemo/PerceptualColorsView.swift create mode 100644 Example/iOSAppDemo/ColorsKitDemo/TemperatureGradientsView.swift create mode 100644 Example/iOSAppDemo/Package.swift create mode 100644 Example/iOSAppDemo/Sources/App.swift create mode 100644 Example/iOSAppDemo/Sources/BlendingModesView.swift create mode 100644 Example/iOSAppDemo/Sources/ColorHarmonyView.swift create mode 100644 Example/iOSAppDemo/Sources/ColorPsychologyView.swift create mode 100644 Example/iOSAppDemo/Sources/ContentView.swift create mode 100644 Example/iOSAppDemo/Sources/DataVisualizationView.swift create mode 100644 Example/iOSAppDemo/Sources/PerceptualColorsView.swift create mode 100644 Example/iOSAppDemo/Sources/TemperatureGradientsView.swift diff --git a/Example/ConsumerSample/Sources/ConsumerSample/ConsumerSample.swift b/Example/ConsumerSample/Sources/ConsumerSample/ConsumerSample.swift index 243c839..992f912 100644 --- a/Example/ConsumerSample/Sources/ConsumerSample/ConsumerSample.swift +++ b/Example/ConsumerSample/Sources/ConsumerSample/ConsumerSample.swift @@ -22,7 +22,7 @@ struct ConsumerSample { print("WCAG AA normal text: \(Accessibility.meets(.AA, foreground: white, background: black))") // 3) Palettes - let theme = Palettes.defaultLight + let theme = defaultLight if let primaryHex = theme.colors["primary"], let primaryRGBA = try? HexColorFormatter.parse(primaryHex) { print("DefaultLight primary hex: \(primaryHex) -> rgba r=\(String(format: "%.3f", primaryRGBA.r))") } diff --git a/Example/iOSAppDemo/ColorsKitDemo.xcodeproj/project.pbxproj b/Example/iOSAppDemo/ColorsKitDemo.xcodeproj/project.pbxproj new file mode 100644 index 0000000..eb0cc75 --- /dev/null +++ b/Example/iOSAppDemo/ColorsKitDemo.xcodeproj/project.pbxproj @@ -0,0 +1,378 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + A1000001000000000000001 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1000002000000000000001 /* App.swift */; }; + A1000003000000000000001 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1000004000000000000001 /* ContentView.swift */; }; + A1000005000000000000001 /* ColorHarmonyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1000006000000000000001 /* ColorHarmonyView.swift */; }; + A1000007000000000000001 /* PerceptualColorsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1000008000000000000001 /* PerceptualColorsView.swift */; }; + A1000009000000000000001 /* BlendingModesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1000010000000000000001 /* BlendingModesView.swift */; }; + A1000011000000000000001 /* ColorPsychologyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1000012000000000000001 /* ColorPsychologyView.swift */; }; + A1000013000000000000001 /* DataVisualizationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1000014000000000000001 /* DataVisualizationView.swift */; }; + A1000015000000000000001 /* TemperatureGradientsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1000016000000000000001 /* TemperatureGradientsView.swift */; }; + A1000017000000000000001 /* ColorsKit in Frameworks */ = {isa = PBXBuildFile; productRef = A1000018000000000000001 /* ColorsKit */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + A1000019000000000000001 /* ColorsKitDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ColorsKitDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; + A1000002000000000000001 /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = ""; }; + A1000004000000000000001 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + A1000006000000000000001 /* ColorHarmonyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorHarmonyView.swift; sourceTree = ""; }; + A1000008000000000000001 /* PerceptualColorsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PerceptualColorsView.swift; sourceTree = ""; }; + A1000010000000000000001 /* BlendingModesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlendingModesView.swift; sourceTree = ""; }; + A1000012000000000000001 /* ColorPsychologyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorPsychologyView.swift; sourceTree = ""; }; + A1000014000000000000001 /* DataVisualizationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataVisualizationView.swift; sourceTree = ""; }; + A1000016000000000000001 /* TemperatureGradientsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemperatureGradientsView.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + A1000020000000000000001 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + A1000017000000000000001 /* ColorsKit in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + A1000021000000000000001 = { + isa = PBXGroup; + children = ( + A1000022000000000000001 /* ColorsKitDemo */, + A1000023000000000000001 /* Products */, + ); + sourceTree = ""; + }; + A1000022000000000000001 /* ColorsKitDemo */ = { + isa = PBXGroup; + children = ( + A1000002000000000000001 /* App.swift */, + A1000004000000000000001 /* ContentView.swift */, + A1000006000000000000001 /* ColorHarmonyView.swift */, + A1000008000000000000001 /* PerceptualColorsView.swift */, + A1000010000000000000001 /* BlendingModesView.swift */, + A1000012000000000000001 /* ColorPsychologyView.swift */, + A1000014000000000000001 /* DataVisualizationView.swift */, + A1000016000000000000001 /* TemperatureGradientsView.swift */, + ); + path = ColorsKitDemo; + sourceTree = ""; + }; + A1000023000000000000001 /* Products */ = { + isa = PBXGroup; + children = ( + A1000019000000000000001 /* ColorsKitDemo.app */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + A1000024000000000000001 /* ColorsKitDemo */ = { + isa = PBXNativeTarget; + buildConfigurationList = A1000025000000000000001 /* Build configuration list for PBXNativeTarget "ColorsKitDemo" */; + buildPhases = ( + A1000026000000000000001 /* Sources */, + A1000020000000000000001 /* Frameworks */, + A1000027000000000000001 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = ColorsKitDemo; + packageProductDependencies = ( + A1000018000000000000001 /* ColorsKit */, + ); + productName = ColorsKitDemo; + productReference = A1000019000000000000001 /* ColorsKitDemo.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + A1000028000000000000001 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1500; + LastUpgradeCheck = 1500; + TargetAttributes = { + A1000024000000000000001 = { + CreatedOnToolsVersion = 15.0; + }; + }; + }; + buildConfigurationList = A1000029000000000000001 /* Build configuration list for PBXProject "ColorsKitDemo" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = A1000021000000000000001; + packageReferences = ( + A1000030000000000000001 /* XCLocalSwiftPackageReference "../../" */, + ); + productRefGroup = A1000023000000000000001 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + A1000024000000000000001 /* ColorsKitDemo */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + A1000027000000000000001 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + A1000026000000000000001 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A1000001000000000000001 /* App.swift in Sources */, + A1000003000000000000001 /* ContentView.swift in Sources */, + A1000005000000000000001 /* ColorHarmonyView.swift in Sources */, + A1000007000000000000001 /* PerceptualColorsView.swift in Sources */, + A1000009000000000000001 /* BlendingModesView.swift in Sources */, + A1000011000000000000001 /* ColorPsychologyView.swift in Sources */, + A1000013000000000000001 /* DataVisualizationView.swift in Sources */, + A1000015000000000000001 /* TemperatureGradientsView.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + A1000031000000000000001 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + A1000032000000000000001 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + A1000033000000000000001 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.ColorsKitDemo; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + A1000034000000000000001 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.ColorsKitDemo; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + A1000029000000000000001 /* Build configuration list for PBXProject "ColorsKitDemo" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A1000031000000000000001 /* Debug */, + A1000032000000000000001 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + A1000025000000000000001 /* Build configuration list for PBXNativeTarget "ColorsKitDemo" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A1000033000000000000001 /* Debug */, + A1000034000000000000001 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + A1000030000000000000001 /* XCLocalSwiftPackageReference "../../" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = "../../"; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + A1000018000000000000001 /* ColorsKit */ = { + isa = XCSwiftPackageProductDependency; + package = A1000030000000000000001 /* XCLocalSwiftPackageReference "../../" */; + productName = ColorsKit; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = A1000028000000000000001 /* Project object */; +} \ No newline at end of file diff --git a/Example/iOSAppDemo/ColorsKitDemo/App.swift b/Example/iOSAppDemo/ColorsKitDemo/App.swift new file mode 100644 index 0000000..99f5d00 --- /dev/null +++ b/Example/iOSAppDemo/ColorsKitDemo/App.swift @@ -0,0 +1,10 @@ +import SwiftUI + +@main +struct ColorsKitDemoApp: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } +} \ No newline at end of file diff --git a/Example/iOSAppDemo/ColorsKitDemo/BlendingModesView.swift b/Example/iOSAppDemo/ColorsKitDemo/BlendingModesView.swift new file mode 100644 index 0000000..5d5f43a --- /dev/null +++ b/Example/iOSAppDemo/ColorsKitDemo/BlendingModesView.swift @@ -0,0 +1,184 @@ +import SwiftUI +import ColorsKit + +struct BlendingModesView: View { + @State private var baseColor = "#FF6B6B" + @State private var overlayColor = "#4ECDC4" + @State private var selectedBlendMode: ColorsKit.BlendMode = .normal + @State private var blendedResult: String = "#FF6B6B" + + var body: some View { + ScrollView { + VStack(spacing: 24) { + Text("Advanced Color Blending") + .font(.title2) + .fontWeight(.bold) + .padding(.top) + + // Color Input Section + VStack(spacing: 16) { + HStack { + VStack(alignment: .leading) { + Text("Base Color") + .font(.headline) + HStack { + TextField("Hex color", text: $baseColor) + .textFieldStyle(RoundedBorderTextFieldStyle()) + Rectangle() + .fill(Color(hex: baseColor) ?? Color.gray) + .frame(width: 40, height: 40) + .cornerRadius(8) + } + } + + VStack(alignment: .leading) { + Text("Overlay Color") + .font(.headline) + HStack { + TextField("Hex color", text: $overlayColor) + .textFieldStyle(RoundedBorderTextFieldStyle()) + Rectangle() + .fill(Color(hex: overlayColor) ?? Color.gray) + .frame(width: 40, height: 40) + .cornerRadius(8) + } + } + } + .padding(.horizontal) + + VStack(alignment: .leading) { + Text("Blend Mode") + .font(.headline) + .padding(.horizontal) + + Picker("Blend Mode", selection: $selectedBlendMode) { + ForEach(ColorsKit.BlendMode.allCases, id: \.self) { mode in + Text(mode.rawValue.capitalized) + .tag(mode) + } + } + .pickerStyle(MenuPickerStyle()) + .padding(.horizontal) + } + + Button("Blend Colors") { + blendColors() + } + .buttonStyle(.borderedProminent) + } + + // Blended Result + VStack(spacing: 16) { + Text("Blended Result") + .font(.headline) + + HStack(spacing: 16) { + VStack { + Rectangle() + .fill(Color(hex: baseColor) ?? Color.gray) + .frame(width: 80, height: 80) + .cornerRadius(12) + Text("Base") + .font(.caption) + } + + Text("+") + .font(.title2) + .fontWeight(.bold) + + VStack { + Rectangle() + .fill(Color(hex: overlayColor) ?? Color.gray) + .frame(width: 80, height: 80) + .cornerRadius(12) + Text("Overlay") + .font(.caption) + } + + Text("=") + .font(.title2) + .fontWeight(.bold) + + VStack { + Rectangle() + .fill(Color(hex: blendedResult) ?? Color.gray) + .frame(width: 80, height: 80) + .cornerRadius(12) + Text(selectedBlendMode.rawValue.capitalized) + .font(.caption) + } + } + + Text("Result: \(blendedResult)") + .font(.monospaced(.body)()) + .padding() + .background(Color.gray.opacity(0.1)) + .cornerRadius(8) + .padding(.horizontal) + } + + // Blend Mode Gallery + VStack(alignment: .leading, spacing: 16) { + Text("Blend Mode Gallery") + .font(.headline) + .padding(.horizontal) + + LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 3), spacing: 12) { + ForEach(ColorsKit.BlendMode.allCases, id: \.self) { mode in + VStack(spacing: 8) { + Rectangle() + .fill(Color(hex: getBlendedColor(mode: mode)) ?? Color.gray) + .frame(height: 60) + .cornerRadius(8) + + Text(mode.rawValue.capitalized) + .font(.caption) + .fontWeight(.medium) + .multilineTextAlignment(.center) + } + .onTapGesture { + selectedBlendMode = mode + blendColors() + } + } + } + .padding(.horizontal) + } + + Spacer() + } + } + .onAppear { + blendColors() + } + .onChange(of: baseColor) { _ in blendColors() } + .onChange(of: overlayColor) { _ in blendColors() } + .onChange(of: selectedBlendMode) { _ in blendColors() } + } + + private func blendColors() { + guard let base = try? HexColorFormatter.parse(baseColor), + let overlay = try? HexColorFormatter.parse(overlayColor) else { + return + } + + let blended = AdvancedBlending.blend(overlay, base, mode: selectedBlendMode) + blendedResult = HexColorFormatter.format(blended) ?? "#000000" + } + + private func getBlendedColor(mode: ColorsKit.BlendMode) -> String { + guard let base = try? HexColorFormatter.parse(baseColor), + let overlay = try? HexColorFormatter.parse(overlayColor) else { + return "#000000" + } + + let blended = AdvancedBlending.blend(overlay, base, mode: mode) + return HexColorFormatter.format(blended) ?? "#000000" + } +} + + + +#Preview { + BlendingModesView() +} \ No newline at end of file diff --git a/Example/iOSAppDemo/ColorsKitDemo/ColorHarmonyView.swift b/Example/iOSAppDemo/ColorsKitDemo/ColorHarmonyView.swift new file mode 100644 index 0000000..4eb5650 --- /dev/null +++ b/Example/iOSAppDemo/ColorsKitDemo/ColorHarmonyView.swift @@ -0,0 +1,134 @@ +import SwiftUI +import ColorsKit + +struct ColorHarmonyView: View { + @Binding var baseColor: String + @Binding var selectedHarmony: ColorHarmonyType + @State private var generatedColors: [String] = [] + + var body: some View { + ScrollView { + VStack(spacing: 20) { + Text("Color Harmony Generator") + .font(.title2) + .fontWeight(.bold) + .padding(.top) + + // Base Color Input + VStack(alignment: .leading, spacing: 8) { + Text("Base Color") + .font(.headline) + + HStack { + TextField("Enter hex color", text: $baseColor) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .onChange(of: baseColor) { _ in + generateHarmony() + } + + Rectangle() + .fill(Color(hex: baseColor) ?? Color.gray) + .frame(width: 40, height: 40) + .cornerRadius(8) + } + } + .padding(.horizontal) + + // Harmony Type Picker + VStack(alignment: .leading, spacing: 8) { + Text("Harmony Type") + .font(.headline) + + Picker("Harmony Type", selection: $selectedHarmony) { + Text("Complementary").tag(ColorHarmonyType.complementary) + Text("Analogous").tag(ColorHarmonyType.analogous) + Text("Triadic").tag(ColorHarmonyType.triadic) + Text("Tetradic").tag(ColorHarmonyType.tetradic) + Text("Split Complementary").tag(ColorHarmonyType.splitComplementary) + Text("Monochromatic").tag(ColorHarmonyType.monochromatic) + } + .pickerStyle(SegmentedPickerStyle()) + .onChange(of: selectedHarmony) { _ in + generateHarmony() + } + } + .padding(.horizontal) + + // Generated Colors Display + if !generatedColors.isEmpty { + VStack(alignment: .leading, spacing: 12) { + Text("Generated Harmony") + .font(.headline) + .padding(.horizontal) + + LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 3), spacing: 12) { + ForEach(generatedColors, id: \.self) { colorHex in + VStack(spacing: 4) { + Rectangle() + .fill(Color(hex: colorHex) ?? Color.gray) + .frame(height: 80) + .cornerRadius(8) + + Text(colorHex) + .font(.caption) + .fontWeight(.medium) + } + } + } + .padding(.horizontal) + + // Accessibility Information + VStack(alignment: .leading, spacing: 8) { + Text("Accessibility Analysis") + .font(.headline) + .padding(.horizontal) + + ForEach(Array(generatedColors.enumerated()), id: \.offset) { index, colorHex in + HStack { + Rectangle() + .fill(Color(hex: colorHex) ?? Color.gray) + .frame(width: 20, height: 20) + .cornerRadius(4) + + Text(colorHex) + .font(.caption) + .fontWeight(.medium) + + Spacer() + + if let contrastRatio = getContrastRatio(colorHex, "#FFFFFF") { + Text("AA: \(contrastRatio >= 4.5 ? "✓" : "✗")") + .font(.caption) + .foregroundColor(contrastRatio >= 4.5 ? .green : .red) + } + } + .padding(.horizontal) + } + } + } + } + + Spacer() + } + } + .onAppear { + generateHarmony() + } + } + + private func generateHarmony() { + generatedColors = ColorHarmonyGenerator.generateHarmony(from: baseColor, type: selectedHarmony) + } + + private func getContrastRatio(_ foreground: String, _ background: String) -> Double? { + guard let fg = try? HexColorFormatter.parse(foreground), + let bg = try? HexColorFormatter.parse(background) else { + return nil + } + return ColorMath.contrastRatio(fg, bg) + } +} + +#Preview { + ColorHarmonyView(baseColor: .constant("#0A84FF"), selectedHarmony: .constant(.complementary)) +} \ No newline at end of file diff --git a/Example/iOSAppDemo/ColorsKitDemo/ColorPsychologyView.swift b/Example/iOSAppDemo/ColorsKitDemo/ColorPsychologyView.swift new file mode 100644 index 0000000..9651663 --- /dev/null +++ b/Example/iOSAppDemo/ColorsKitDemo/ColorPsychologyView.swift @@ -0,0 +1,228 @@ +import SwiftUI +import ColorsKit + +struct ColorPsychologyView: View { + @State private var selectedEmotion: EmotionalCategory = .calm + @State private var emotionColors: [String] = [] + @State private var inputColor = "#FF6B6B" + @State private var emotionalProfile: [EmotionalCategory: Double] = [:] + @State private var primaryEmotion: EmotionalCategory = .calm + @State private var selectedEmotions: Set = [.calm, .energetic] + @State private var multiEmotionPalette: [String] = [] + + var body: some View { + ScrollView { + VStack(spacing: 24) { + Text("Color Psychology Engine") + .font(.title2) + .fontWeight(.bold) + .padding(.top) + + // Emotion-Based Color Generation + VStack(alignment: .leading, spacing: 16) { + Text("Generate Colors by Emotion") + .font(.headline) + .padding(.horizontal) + + VStack(spacing: 12) { + Picker("Select Emotion", selection: $selectedEmotion) { + ForEach(EmotionalCategory.allCases, id: \.self) { emotion in + Text(emotion.rawValue.capitalized) + .tag(emotion) + } + } + .pickerStyle(MenuPickerStyle()) + .padding(.horizontal) + + Button("Generate Colors") { + generateEmotionColors() + } + .buttonStyle(.borderedProminent) + + if !emotionColors.isEmpty { + VStack(alignment: .leading, spacing: 8) { + Text("\(selectedEmotion.rawValue.capitalized) Colors") + .font(.subheadline) + .fontWeight(.medium) + .padding(.horizontal) + + LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 4), spacing: 8) { + ForEach(emotionColors, id: \.self) { colorHex in + VStack(spacing: 4) { + Rectangle() + .fill(Color(hex: colorHex) ?? Color.gray) + .frame(height: 60) + .cornerRadius(8) + + Text(colorHex) + .font(.caption2) + .fontWeight(.medium) + } + } + } + .padding(.horizontal) + } + } + } + } + + Divider() + + // Color Emotional Analysis + VStack(alignment: .leading, spacing: 16) { + Text("Analyze Color Emotions") + .font(.headline) + .padding(.horizontal) + + VStack(spacing: 12) { + HStack { + TextField("Enter hex color", text: $inputColor) + .textFieldStyle(RoundedBorderTextFieldStyle()) + + Rectangle() + .fill(Color(hex: inputColor) ?? Color.gray) + .frame(width: 50, height: 40) + .cornerRadius(8) + } + .padding(.horizontal) + + Button("Analyze Emotions") { + analyzeColorEmotions() + } + .buttonStyle(.borderedProminent) + + if !emotionalProfile.isEmpty { + VStack(alignment: .leading, spacing: 12) { + Text("Primary Emotion: \(primaryEmotion.rawValue.capitalized)") + .font(.subheadline) + .fontWeight(.bold) + .padding(.horizontal) + + Text("Emotional Profile") + .font(.subheadline) + .fontWeight(.medium) + .padding(.horizontal) + + ForEach(emotionalProfile.sorted(by: { $0.value > $1.value }), id: \.key) { emotion, confidence in + HStack { + Text(emotion.rawValue.capitalized) + .font(.caption) + .frame(width: 80, alignment: .leading) + + ProgressView(value: confidence, total: 1.0) + .progressViewStyle(LinearProgressViewStyle()) + + Text("\(Int(confidence * 100))%") + .font(.caption) + .fontWeight(.medium) + .frame(width: 40, alignment: .trailing) + } + .padding(.horizontal) + } + } + } + } + } + + Divider() + + // Multi-Emotion Palette Generation + VStack(alignment: .leading, spacing: 16) { + Text("Multi-Emotion Palette") + .font(.headline) + .padding(.horizontal) + + VStack(spacing: 12) { + Text("Select Emotions to Combine") + .font(.subheadline) + .padding(.horizontal) + + LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 3), spacing: 8) { + ForEach(EmotionalCategory.allCases, id: \.self) { emotion in + Button(action: { + if selectedEmotions.contains(emotion) { + selectedEmotions.remove(emotion) + } else { + selectedEmotions.insert(emotion) + } + }) { + Text(emotion.rawValue.capitalized) + .font(.caption) + .padding(.vertical, 8) + .padding(.horizontal, 12) + .background(selectedEmotions.contains(emotion) ? Color.blue : Color.gray.opacity(0.2)) + .foregroundColor(selectedEmotions.contains(emotion) ? .white : .primary) + .cornerRadius(8) + } + } + } + .padding(.horizontal) + + Button("Generate Multi-Emotion Palette") { + generateMultiEmotionPalette() + } + .buttonStyle(.borderedProminent) + .disabled(selectedEmotions.isEmpty) + + if !multiEmotionPalette.isEmpty { + VStack(alignment: .leading, spacing: 8) { + Text("Combined Palette") + .font(.subheadline) + .fontWeight(.medium) + .padding(.horizontal) + + LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 5), spacing: 8) { + ForEach(multiEmotionPalette, id: \.self) { colorHex in + VStack(spacing: 4) { + Rectangle() + .fill(Color(hex: colorHex) ?? Color.gray) + .frame(height: 50) + .cornerRadius(8) + + Text(colorHex) + .font(.caption2) + .fontWeight(.medium) + } + } + } + .padding(.horizontal) + } + } + } + } + + Spacer() + } + } + .onAppear { + generateEmotionColors() + analyzeColorEmotions() + } + } + + private func generateEmotionColors() { + emotionColors = ColorPsychology.colorsFor(emotion: selectedEmotion) + } + + private func analyzeColorEmotions() { + guard let rgba = try? HexColorFormatter.parse(inputColor) else { + return + } + + primaryEmotion = ColorPsychology.primaryEmotion(for: rgba) + emotionalProfile = ColorPsychology.emotionalProfile(for: rgba) + } + + private func generateMultiEmotionPalette() { + multiEmotionPalette = ColorPsychology.generatePalette( + for: Array(selectedEmotions), + count: min(selectedEmotions.count * 2, 10) + ) + } +} + + + +#Preview { + ColorPsychologyView() +} \ No newline at end of file diff --git a/Example/iOSAppDemo/ColorsKitDemo/ContentView.swift b/Example/iOSAppDemo/ColorsKitDemo/ContentView.swift new file mode 100644 index 0000000..8ae2d6f --- /dev/null +++ b/Example/iOSAppDemo/ColorsKitDemo/ContentView.swift @@ -0,0 +1,68 @@ +import SwiftUI +import ColorsKit + +struct ContentView: View { + @State private var selectedTab = 0 + @State private var baseColor = "#0A84FF" + @State private var selectedEmotion: EmotionalCategory = .energetic + @State private var selectedHarmony: ColorHarmonyType = .complementary + @State private var selectedBlendMode: ColorsKit.BlendMode = .multiply + @State private var temperature: Double = 6500 + @State private var tint: Double = 0 + + var body: some View { + TabView(selection: $selectedTab) { + // Color Harmony Tab + ColorHarmonyView(baseColor: $baseColor, selectedHarmony: $selectedHarmony) + .tabItem { + Image(systemName: "paintpalette") + Text("Harmony") + } + .tag(0) + + // Perceptual Colors Tab + PerceptualColorsView() + .tabItem { + Image(systemName: "eye") + Text("Perceptual") + } + .tag(1) + + // Blending Modes Tab + BlendingModesView() + .tabItem { + Image(systemName: "square.on.square") + Text("Blending") + } + .tag(2) + + // Color Psychology Tab + ColorPsychologyView() + .tabItem { + Image(systemName: "brain.head.profile") + Text("Psychology") + } + .tag(3) + + // Data Visualization Tab + DataVisualizationView() + .tabItem { + Image(systemName: "chart.bar") + Text("Data Viz") + } + .tag(4) + + // Temperature & Gradients Tab + TemperatureGradientsView() + .tabItem { + Image(systemName: "thermometer") + Text("Temperature") + } + .tag(5) + } + } +} + +#Preview { + ContentView() +} \ No newline at end of file diff --git a/Example/iOSAppDemo/ColorsKitDemo/DataVisualizationView.swift b/Example/iOSAppDemo/ColorsKitDemo/DataVisualizationView.swift new file mode 100644 index 0000000..982edfe --- /dev/null +++ b/Example/iOSAppDemo/ColorsKitDemo/DataVisualizationView.swift @@ -0,0 +1,232 @@ +import SwiftUI +import ColorsKit + +struct DataVisualizationView: View { + @State private var selectedGradientType: DataVisualizationType = .sequential + @State private var gradientSteps = 7 + @State private var generatedGradient: [String] = [] + @State private var sampleData: [Double] = [0.1, 0.3, 0.5, 0.7, 0.9, 0.4, 0.8, 0.2, 0.6] + + var body: some View { + ScrollView { + VStack(spacing: 24) { + Text("Data Visualization Gradients") + .font(.title2) + .fontWeight(.bold) + .padding(.top) + + // Gradient Type Selection + VStack(alignment: .leading, spacing: 16) { + Text("Gradient Type") + .font(.headline) + .padding(.horizontal) + + Picker("Gradient Type", selection: $selectedGradientType) { + ForEach(DataVisualizationType.allCases, id: \.self) { type in + Text(type.displayName) + .tag(type) + } + } + .pickerStyle(MenuPickerStyle()) + .padding(.horizontal) + + VStack(alignment: .leading) { + Text("Steps: \(gradientSteps)") + .font(.subheadline) + .padding(.horizontal) + + Slider(value: Binding( + get: { Double(gradientSteps) }, + set: { gradientSteps = Int($0) } + ), in: 3...12, step: 1) + .padding(.horizontal) + } + + Button("Generate Gradient") { + generateGradient() + } + .buttonStyle(.borderedProminent) + } + + // Generated Gradient Display + if !generatedGradient.isEmpty { + VStack(alignment: .leading, spacing: 16) { + Text("Generated Gradient") + .font(.headline) + .padding(.horizontal) + + // Gradient Bar + HStack(spacing: 2) { + ForEach(generatedGradient, id: \.self) { colorHex in + Rectangle() + .fill(Color(hex: colorHex) ?? Color.gray) + .frame(height: 60) + } + } + .cornerRadius(8) + .padding(.horizontal) + + // Color Values + LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: min(gradientSteps, 4)), spacing: 8) { + ForEach(Array(generatedGradient.enumerated()), id: \.offset) { index, colorHex in + VStack(spacing: 4) { + Rectangle() + .fill(Color(hex: colorHex) ?? Color.gray) + .frame(height: 40) + .cornerRadius(6) + + Text(colorHex) + .font(.caption2) + .fontWeight(.medium) + } + } + } + .padding(.horizontal) + } + } + + // Sample Data Visualization + if !generatedGradient.isEmpty { + VStack(alignment: .leading, spacing: 16) { + Text("Sample Data Visualization") + .font(.headline) + .padding(.horizontal) + + // Bar Chart + HStack(alignment: .bottom, spacing: 4) { + ForEach(Array(sampleData.enumerated()), id: \.offset) { index, value in + let colorIndex = min(Int(value * Double(generatedGradient.count - 1)), generatedGradient.count - 1) + let colorHex = generatedGradient[colorIndex] + + VStack(spacing: 4) { + Rectangle() + .fill(Color(hex: colorHex) ?? Color.gray) + .frame(width: 30, height: CGFloat(value * 100)) + .cornerRadius(4) + + Text("\(Int(value * 100))") + .font(.caption2) + } + } + } + .frame(height: 120) + .padding(.horizontal) + + // Heatmap Grid + Text("Heatmap Sample") + .font(.subheadline) + .fontWeight(.medium) + .padding(.horizontal) + + LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 6), spacing: 2) { + ForEach(0..<24, id: \.self) { index in + let value = Double.random(in: 0...1) + let colorIndex = min(Int(value * Double(generatedGradient.count - 1)), generatedGradient.count - 1) + let colorHex = generatedGradient[colorIndex] + + Rectangle() + .fill(Color(hex: colorHex) ?? Color.gray) + .frame(height: 30) + .cornerRadius(4) + } + } + .padding(.horizontal) + } + } + + // Gradient Information + VStack(alignment: .leading, spacing: 12) { + Text("Gradient Information") + .font(.headline) + .padding(.horizontal) + + VStack(alignment: .leading, spacing: 8) { + Text(selectedGradientType.description) + .font(.body) + .padding(.horizontal) + + Text("Best Use Cases:") + .font(.subheadline) + .fontWeight(.medium) + .padding(.horizontal) + + ForEach(selectedGradientType.useCases, id: \.self) { useCase in + Text("• \(useCase)") + .font(.caption) + .padding(.horizontal) + } + } + } + + Spacer() + } + } + .onAppear { + generateGradient() + } + .onChange(of: selectedGradientType) { _ in generateGradient() } + .onChange(of: gradientSteps) { _ in generateGradient() } + } + + private func generateGradient() { + generatedGradient = GradientGenerator.generateDataVisualizationGradient( + type: selectedGradientType, + steps: gradientSteps + ) + } +} + +extension DataVisualizationType: @retroactive CaseIterable { + public static var allCases: [DataVisualizationType] { + return [.sequential, .diverging, .heatmap, .viridis, .plasma, .temperature] + } + + var displayName: String { + switch self { + case .sequential: return "Sequential" + case .diverging: return "Diverging" + case .heatmap: return "Heatmap" + case .viridis: return "Viridis" + case .plasma: return "Plasma" + case .temperature: return "Temperature" + } + } + + var description: String { + switch self { + case .sequential: + return "Sequential gradients show progression from low to high values using a single hue with varying lightness and saturation." + case .diverging: + return "Diverging gradients emphasize deviations from a central value, using two contrasting hues that meet at a neutral midpoint." + case .heatmap: + return "Heatmap gradients use the classic blue-to-red spectrum, ideal for showing intensity or density data." + case .viridis: + return "Viridis is a perceptually uniform colormap that works well for scientific data visualization and is colorblind-friendly." + case .plasma: + return "Plasma provides high contrast and perceptual uniformity, excellent for highlighting data patterns and outliers." + case .temperature: + return "Temperature gradients simulate thermal imaging, progressing from cool blues through warm reds to hot whites." + } + } + + var useCases: [String] { + switch self { + case .sequential: + return ["Population density maps", "Sales performance charts", "Progress indicators", "Elevation maps"] + case .diverging: + return ["Temperature anomalies", "Survey responses", "Financial gains/losses", "Correlation matrices"] + case .heatmap: + return ["Website analytics", "Correlation matrices", "Density plots", "Activity tracking"] + case .viridis: + return ["Scientific data", "Medical imaging", "Accessibility-focused charts", "Academic publications"] + case .plasma: + return ["Astronomical data", "High-contrast visualizations", "Pattern detection", "Outlier identification"] + case .temperature: + return ["Thermal imaging", "Weather maps", "Heat distribution", "Energy consumption"] + } + } +} + +#Preview { + DataVisualizationView() +} \ No newline at end of file diff --git a/Example/iOSAppDemo/ColorsKitDemo/PerceptualColorsView.swift b/Example/iOSAppDemo/ColorsKitDemo/PerceptualColorsView.swift new file mode 100644 index 0000000..52213de --- /dev/null +++ b/Example/iOSAppDemo/ColorsKitDemo/PerceptualColorsView.swift @@ -0,0 +1,220 @@ +import SwiftUI +import ColorsKit + +struct PerceptualColorsView: View { + @State private var inputColor = "#FF6B6B" + @State private var targetColor = "#4ECDC4" + @State private var blendSteps = 5 + @State private var perceptualGradient: [String] = [] + @State private var regularGradient: [String] = [] + + var body: some View { + ScrollView { + VStack(spacing: 24) { + Text("Perceptual Color Spaces") + .font(.title2) + .fontWeight(.bold) + .padding(.top) + + // Color Input Section + VStack(spacing: 16) { + HStack { + VStack(alignment: .leading) { + Text("Start Color") + .font(.headline) + HStack { + TextField("Hex color", text: $inputColor) + .textFieldStyle(RoundedBorderTextFieldStyle()) + Rectangle() + .fill(Color(hex: inputColor) ?? Color.gray) + .frame(width: 40, height: 40) + .cornerRadius(8) + } + } + + VStack(alignment: .leading) { + Text("End Color") + .font(.headline) + HStack { + TextField("Hex color", text: $targetColor) + .textFieldStyle(RoundedBorderTextFieldStyle()) + Rectangle() + .fill(Color(hex: targetColor) ?? Color.gray) + .frame(width: 40, height: 40) + .cornerRadius(8) + } + } + } + .padding(.horizontal) + + VStack(alignment: .leading) { + Text("Blend Steps: \(blendSteps)") + .font(.headline) + Slider(value: Binding( + get: { Double(blendSteps) }, + set: { blendSteps = Int($0) } + ), in: 3...10, step: 1) + } + .padding(.horizontal) + + Button("Generate Gradients") { + generateGradients() + } + .buttonStyle(.borderedProminent) + } + + // Color Space Information + if let rgba = try? HexColorFormatter.parse(inputColor) { + VStack(alignment: .leading, spacing: 12) { + Text("Color Space Conversions") + .font(.headline) + .padding(.horizontal) + + VStack(alignment: .leading, spacing: 8) { + let xyz = ColorSpaceConverter.rgbaToXYZ(rgba) + let lab = ColorSpaceConverter.xyzToLAB(xyz) + let luv = ColorSpaceConverter.xyzToLUV(xyz) + + HStack { + Rectangle() + .fill(Color(hex: inputColor) ?? Color.gray) + .frame(width: 30, height: 30) + .cornerRadius(6) + + VStack(alignment: .leading, spacing: 2) { + Text("RGBA: (\(String(format: "%.2f", rgba.r)), \(String(format: "%.2f", rgba.g)), \(String(format: "%.2f", rgba.b)), \(String(format: "%.2f", rgba.a)))") + .font(.caption) + Text("XYZ: (\(String(format: "%.2f", xyz.x)), \(String(format: "%.2f", xyz.y)), \(String(format: "%.2f", xyz.z)))") + .font(.caption) + Text("LAB: (\(String(format: "%.1f", lab.l)), \(String(format: "%.1f", lab.a)), \(String(format: "%.1f", lab.b)))") + .font(.caption) + Text("LUV: (\(String(format: "%.1f", luv.l)), \(String(format: "%.1f", luv.u)), \(String(format: "%.1f", luv.v)))") + .font(.caption) + } + Spacer() + } + .padding(.horizontal) + } + } + } + + // Gradient Comparison + if !perceptualGradient.isEmpty && !regularGradient.isEmpty { + VStack(alignment: .leading, spacing: 16) { + Text("Gradient Comparison") + .font(.headline) + .padding(.horizontal) + + VStack(alignment: .leading, spacing: 12) { + Text("Perceptual (LAB) Gradient") + .font(.subheadline) + .fontWeight(.medium) + .padding(.horizontal) + + HStack(spacing: 2) { + ForEach(perceptualGradient, id: \.self) { colorHex in + Rectangle() + .fill(Color(hex: colorHex) ?? Color.gray) + .frame(height: 60) + } + } + .cornerRadius(8) + .padding(.horizontal) + + Text("Regular (RGB) Gradient") + .font(.subheadline) + .fontWeight(.medium) + .padding(.horizontal) + + HStack(spacing: 2) { + ForEach(regularGradient, id: \.self) { colorHex in + Rectangle() + .fill(Color(hex: colorHex) ?? Color.gray) + .frame(height: 60) + } + } + .cornerRadius(8) + .padding(.horizontal) + } + + // Delta E Analysis + if perceptualGradient.count > 1 { + VStack(alignment: .leading, spacing: 8) { + Text("Perceptual Uniformity (Delta E)") + .font(.subheadline) + .fontWeight(.medium) + .padding(.horizontal) + + ForEach(0.. Color { + guard !generatedGradient.isEmpty else { return Color.gray } + + let index = min(Int(animationProgress * Double(generatedGradient.count - 1)), generatedGradient.count - 1) + return Color(hex: generatedGradient[index]) ?? Color.gray + } + + private func toggleAnimation() { + if isAnimating { + isAnimating = false + } else { + isAnimating = true + animateProgress() + } + } + + private func animateProgress() { + guard isAnimating else { return } + + withAnimation(.linear(duration: 0.05)) { + animationProgress += 0.02 + if animationProgress >= 1.0 { + animationProgress = 0.0 + } + } + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { + animateProgress() + } + } + + private func resetAnimation() { + isAnimating = false + animationProgress = 0.0 + } + + private func applyTemperaturePreset(_ preset: TemperaturePreset) { + startColor = preset.colors.first ?? "#000000" + endColor = preset.colors.last ?? "#FFFFFF" + generateGradient() + } +} + +extension GradientInterpolation { + var displayName: String { + switch self { + case .linear: return "Linear RGB" + case .perceptual: return "Perceptual (LAB)" + case .hsl: return "HSL" + case .bezier: return "Bézier" + case .ease: return "Ease" + } + } + + var description: String { + switch self { + case .linear: + return "Linear interpolation in RGB color space. Simple and fast, but may produce muddy colors in the middle." + case .perceptual: + return "Perceptually uniform interpolation using LAB color space. Produces more natural-looking gradients." + case .hsl: + return "Interpolation in HSL color space, maintaining hue relationships for more vibrant transitions." + case .bezier: + return "Smooth Bézier curve interpolation for elegant, non-linear color transitions." + case .ease: + return "Eased interpolation with smooth acceleration and deceleration for natural motion." + } + } +} + +enum TemperaturePreset: String, CaseIterable { + case coolToWarm = "coolToWarm" + case thermal = "thermal" + case arctic = "arctic" + case sunset = "sunset" + case ocean = "ocean" + case fire = "fire" + + var displayName: String { + switch self { + case .coolToWarm: return "Cool to Warm" + case .thermal: return "Thermal" + case .arctic: return "Arctic" + case .sunset: return "Sunset" + case .ocean: return "Ocean Depths" + case .fire: return "Fire" + } + } + + var colors: [String] { + switch self { + case .coolToWarm: + return ["#0066CC", "#FFFFFF", "#FF3366"] + case .thermal: + return ["#000080", "#0000FF", "#00FFFF", "#00FF00", "#FFFF00", "#FF0000", "#FFFFFF"] + case .arctic: + return ["#001122", "#003366", "#0066CC", "#66CCFF", "#FFFFFF"] + case .sunset: + return ["#FF6B35", "#F7931E", "#FFD23F", "#FF6B6B", "#C44569"] + case .ocean: + return ["#000080", "#0033AA", "#0066CC", "#0099FF", "#66CCFF"] + case .fire: + return ["#8B0000", "#FF0000", "#FF4500", "#FFA500", "#FFFF00", "#FFFFFF"] + } + } +} + +#Preview { + TemperatureGradientsView() +} \ No newline at end of file diff --git a/Example/iOSAppDemo/Package.swift b/Example/iOSAppDemo/Package.swift new file mode 100644 index 0000000..61e3b7a --- /dev/null +++ b/Example/iOSAppDemo/Package.swift @@ -0,0 +1,28 @@ +// swift-tools-version: 5.9 +import PackageDescription + +let package = Package( + name: "iOSAppDemo", + platforms: [ + .iOS(.v15), + .macOS(.v12) + ], + products: [ + .library( + name: "iOSAppDemo", + targets: ["iOSAppDemo"] + ) + ], + dependencies: [ + .package(path: "../../") + ], + targets: [ + .target( + name: "iOSAppDemo", + dependencies: [ + .product(name: "ColorsKit", package: "ColorsKit") + ], + path: "Sources" + ) + ] +) \ No newline at end of file diff --git a/Example/iOSAppDemo/README.md b/Example/iOSAppDemo/README.md index 6688110..de71698 100644 --- a/Example/iOSAppDemo/README.md +++ b/Example/iOSAppDemo/README.md @@ -1,36 +1,101 @@ -# iOSAppDemo - -A minimal SwiftUI demo app showcasing ColorsKit usage. - -## Create the demo - -1. Open Xcode > File > New > Project > iOS App (SwiftUI). -2. Name: ColorsKitDemo, Interface: SwiftUI, Language: Swift. -3. Add this package via File > Add Packages... and search your repo URL. -4. In `ContentView.swift`: - -```swift -import SwiftUI -import ColorsKit - -struct ContentView: View { - var body: some View { - VStack(spacing: 24) { - Text("ColorsKit Demo") - .font(.title) - .foregroundStyle(Color.dynamic(lightHex: "#1C1C1E", darkHex: "#FFFFFF")) - - Rectangle() - .fill(SwiftUIGradientBuilder.linear(hexColors: ["#0A84FF", "#5E5CE6"])) - .frame(height: 120) - .clipShape(RoundedRectangle(cornerRadius: 12)) - - Text("Contrast AA: \(AccessibilityUtils.meetsAA(foreground: "#000000", background: "#FFFFFF"))") - } - .padding() - .background(Color.dynamic(lightHex: Palettes.defaultLight.colors["surface"]!, darkHex: Palettes.defaultDark.colors["surface"]!)) - } -} +# ColorsKit iOS Demo App + +This directory contains a comprehensive SwiftUI demo app showcasing all advanced ColorsKit features. + +## Features Demonstrated + +### 🎨 Color Harmony +- Generate complementary, analogous, triadic, tetradic, split complementary, and monochromatic color schemes +- Interactive color picker and harmony type selection +- Accessibility analysis for generated colors + +### 🔬 Perceptual Color Spaces +- Color space conversions (RGBA, XYZ, LAB, LUV) +- Perceptual vs. regular gradient comparison +- Delta E analysis for perceptual uniformity +- Customizable blend steps + +### 🎭 Advanced Color Blending +- 12 different blend modes (multiply, screen, overlay, soft light, etc.) +- Interactive base and overlay color selection +- Real-time blend mode gallery preview +- Visual blend result comparison + +### 🧠 Color Psychology Engine +- Emotion-based color generation (calm, energetic, warm, cool, etc.) +- Color emotional analysis with confidence scores +- Multi-emotion palette generation +- Primary emotion detection + +### 📊 Data Visualization Gradients +- Sequential, diverging, heatmap gradients +- Scientific colormaps (Viridis, Plasma) +- Temperature gradients for thermal imaging +- Sample data visualization with bar charts and heatmaps + +### 🌡️ Temperature & Advanced Gradients +- Multiple interpolation methods (Linear, Perceptual, HSL, Bézier, Ease) +- Animated gradient transitions +- Temperature preset gradients +- Real-time gradient generation + +## Setup Options + +### Option 1: Xcode Project (Recommended) +1. Open `ColorsKitDemo.xcodeproj` in Xcode +2. The project is already configured with ColorsKit as a local package dependency +3. Build and run the app + +### Option 2: Swift Package Manager +1. Navigate to this directory in Terminal +2. Run `swift build` to build the package +3. Use the Package.swift for integration into other projects + +## Project Structure + +``` +iOSAppDemo/ +├── ColorsKitDemo.xcodeproj/ # Xcode project file +├── ColorsKitDemo/ # Source files +│ ├── App.swift # Main app entry point +│ ├── ContentView.swift # Main tab view +│ ├── ColorHarmonyView.swift # Color harmony demo +│ ├── PerceptualColorsView.swift # Perceptual color spaces +│ ├── BlendingModesView.swift # Color blending modes +│ ├── ColorPsychologyView.swift # Color psychology engine +│ ├── DataVisualizationView.swift # Data viz gradients +│ └── TemperatureGradientsView.swift # Advanced gradients +├── Sources/ # Swift Package sources (mirror) +├── Package.swift # Swift Package configuration +└── README.md # This file ``` -Run the app and toggle dark mode to see dynamic adaptation. \ No newline at end of file +## Requirements + +- iOS 15.0+ +- Xcode 15.0+ +- Swift 5.9+ + +## Advanced Features Showcased + +### Color Psychology Engine +- **Emotional Categories**: Calm, Energetic, Warm, Cool, Happy, Melancholy, Sophisticated, Playful, Natural, Neutral +- **Color Analysis**: HSL-based emotional profiling with confidence scores +- **Palette Generation**: Multi-emotion color palette creation + +### Perceptual Color Mathematics +- **Color Spaces**: RGBA, XYZ, LAB, LUV conversions +- **Delta E Calculations**: CIE Delta E 2000 for perceptual color differences +- **Uniform Gradients**: Perceptually uniform color transitions + +### Data Visualization +- **Scientific Colormaps**: Viridis, Plasma for research applications +- **Specialized Gradients**: Sequential, diverging, heatmap types +- **Real-world Examples**: Bar charts, heatmaps, thermal imaging + +### Advanced Interpolation +- **Multiple Methods**: Linear RGB, Perceptual LAB, HSL, Bézier curves +- **Animation Support**: Smooth gradient transitions for UI animations +- **Temperature Mapping**: Thermal imaging color schemes + +This demo app serves as both a showcase and a reference implementation for integrating ColorsKit's advanced features into iOS applications. \ No newline at end of file diff --git a/Example/iOSAppDemo/Sources/App.swift b/Example/iOSAppDemo/Sources/App.swift new file mode 100644 index 0000000..99f5d00 --- /dev/null +++ b/Example/iOSAppDemo/Sources/App.swift @@ -0,0 +1,10 @@ +import SwiftUI + +@main +struct ColorsKitDemoApp: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } +} \ No newline at end of file diff --git a/Example/iOSAppDemo/Sources/BlendingModesView.swift b/Example/iOSAppDemo/Sources/BlendingModesView.swift new file mode 100644 index 0000000..5d5f43a --- /dev/null +++ b/Example/iOSAppDemo/Sources/BlendingModesView.swift @@ -0,0 +1,184 @@ +import SwiftUI +import ColorsKit + +struct BlendingModesView: View { + @State private var baseColor = "#FF6B6B" + @State private var overlayColor = "#4ECDC4" + @State private var selectedBlendMode: ColorsKit.BlendMode = .normal + @State private var blendedResult: String = "#FF6B6B" + + var body: some View { + ScrollView { + VStack(spacing: 24) { + Text("Advanced Color Blending") + .font(.title2) + .fontWeight(.bold) + .padding(.top) + + // Color Input Section + VStack(spacing: 16) { + HStack { + VStack(alignment: .leading) { + Text("Base Color") + .font(.headline) + HStack { + TextField("Hex color", text: $baseColor) + .textFieldStyle(RoundedBorderTextFieldStyle()) + Rectangle() + .fill(Color(hex: baseColor) ?? Color.gray) + .frame(width: 40, height: 40) + .cornerRadius(8) + } + } + + VStack(alignment: .leading) { + Text("Overlay Color") + .font(.headline) + HStack { + TextField("Hex color", text: $overlayColor) + .textFieldStyle(RoundedBorderTextFieldStyle()) + Rectangle() + .fill(Color(hex: overlayColor) ?? Color.gray) + .frame(width: 40, height: 40) + .cornerRadius(8) + } + } + } + .padding(.horizontal) + + VStack(alignment: .leading) { + Text("Blend Mode") + .font(.headline) + .padding(.horizontal) + + Picker("Blend Mode", selection: $selectedBlendMode) { + ForEach(ColorsKit.BlendMode.allCases, id: \.self) { mode in + Text(mode.rawValue.capitalized) + .tag(mode) + } + } + .pickerStyle(MenuPickerStyle()) + .padding(.horizontal) + } + + Button("Blend Colors") { + blendColors() + } + .buttonStyle(.borderedProminent) + } + + // Blended Result + VStack(spacing: 16) { + Text("Blended Result") + .font(.headline) + + HStack(spacing: 16) { + VStack { + Rectangle() + .fill(Color(hex: baseColor) ?? Color.gray) + .frame(width: 80, height: 80) + .cornerRadius(12) + Text("Base") + .font(.caption) + } + + Text("+") + .font(.title2) + .fontWeight(.bold) + + VStack { + Rectangle() + .fill(Color(hex: overlayColor) ?? Color.gray) + .frame(width: 80, height: 80) + .cornerRadius(12) + Text("Overlay") + .font(.caption) + } + + Text("=") + .font(.title2) + .fontWeight(.bold) + + VStack { + Rectangle() + .fill(Color(hex: blendedResult) ?? Color.gray) + .frame(width: 80, height: 80) + .cornerRadius(12) + Text(selectedBlendMode.rawValue.capitalized) + .font(.caption) + } + } + + Text("Result: \(blendedResult)") + .font(.monospaced(.body)()) + .padding() + .background(Color.gray.opacity(0.1)) + .cornerRadius(8) + .padding(.horizontal) + } + + // Blend Mode Gallery + VStack(alignment: .leading, spacing: 16) { + Text("Blend Mode Gallery") + .font(.headline) + .padding(.horizontal) + + LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 3), spacing: 12) { + ForEach(ColorsKit.BlendMode.allCases, id: \.self) { mode in + VStack(spacing: 8) { + Rectangle() + .fill(Color(hex: getBlendedColor(mode: mode)) ?? Color.gray) + .frame(height: 60) + .cornerRadius(8) + + Text(mode.rawValue.capitalized) + .font(.caption) + .fontWeight(.medium) + .multilineTextAlignment(.center) + } + .onTapGesture { + selectedBlendMode = mode + blendColors() + } + } + } + .padding(.horizontal) + } + + Spacer() + } + } + .onAppear { + blendColors() + } + .onChange(of: baseColor) { _ in blendColors() } + .onChange(of: overlayColor) { _ in blendColors() } + .onChange(of: selectedBlendMode) { _ in blendColors() } + } + + private func blendColors() { + guard let base = try? HexColorFormatter.parse(baseColor), + let overlay = try? HexColorFormatter.parse(overlayColor) else { + return + } + + let blended = AdvancedBlending.blend(overlay, base, mode: selectedBlendMode) + blendedResult = HexColorFormatter.format(blended) ?? "#000000" + } + + private func getBlendedColor(mode: ColorsKit.BlendMode) -> String { + guard let base = try? HexColorFormatter.parse(baseColor), + let overlay = try? HexColorFormatter.parse(overlayColor) else { + return "#000000" + } + + let blended = AdvancedBlending.blend(overlay, base, mode: mode) + return HexColorFormatter.format(blended) ?? "#000000" + } +} + + + +#Preview { + BlendingModesView() +} \ No newline at end of file diff --git a/Example/iOSAppDemo/Sources/ColorHarmonyView.swift b/Example/iOSAppDemo/Sources/ColorHarmonyView.swift new file mode 100644 index 0000000..4eb5650 --- /dev/null +++ b/Example/iOSAppDemo/Sources/ColorHarmonyView.swift @@ -0,0 +1,134 @@ +import SwiftUI +import ColorsKit + +struct ColorHarmonyView: View { + @Binding var baseColor: String + @Binding var selectedHarmony: ColorHarmonyType + @State private var generatedColors: [String] = [] + + var body: some View { + ScrollView { + VStack(spacing: 20) { + Text("Color Harmony Generator") + .font(.title2) + .fontWeight(.bold) + .padding(.top) + + // Base Color Input + VStack(alignment: .leading, spacing: 8) { + Text("Base Color") + .font(.headline) + + HStack { + TextField("Enter hex color", text: $baseColor) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .onChange(of: baseColor) { _ in + generateHarmony() + } + + Rectangle() + .fill(Color(hex: baseColor) ?? Color.gray) + .frame(width: 40, height: 40) + .cornerRadius(8) + } + } + .padding(.horizontal) + + // Harmony Type Picker + VStack(alignment: .leading, spacing: 8) { + Text("Harmony Type") + .font(.headline) + + Picker("Harmony Type", selection: $selectedHarmony) { + Text("Complementary").tag(ColorHarmonyType.complementary) + Text("Analogous").tag(ColorHarmonyType.analogous) + Text("Triadic").tag(ColorHarmonyType.triadic) + Text("Tetradic").tag(ColorHarmonyType.tetradic) + Text("Split Complementary").tag(ColorHarmonyType.splitComplementary) + Text("Monochromatic").tag(ColorHarmonyType.monochromatic) + } + .pickerStyle(SegmentedPickerStyle()) + .onChange(of: selectedHarmony) { _ in + generateHarmony() + } + } + .padding(.horizontal) + + // Generated Colors Display + if !generatedColors.isEmpty { + VStack(alignment: .leading, spacing: 12) { + Text("Generated Harmony") + .font(.headline) + .padding(.horizontal) + + LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 3), spacing: 12) { + ForEach(generatedColors, id: \.self) { colorHex in + VStack(spacing: 4) { + Rectangle() + .fill(Color(hex: colorHex) ?? Color.gray) + .frame(height: 80) + .cornerRadius(8) + + Text(colorHex) + .font(.caption) + .fontWeight(.medium) + } + } + } + .padding(.horizontal) + + // Accessibility Information + VStack(alignment: .leading, spacing: 8) { + Text("Accessibility Analysis") + .font(.headline) + .padding(.horizontal) + + ForEach(Array(generatedColors.enumerated()), id: \.offset) { index, colorHex in + HStack { + Rectangle() + .fill(Color(hex: colorHex) ?? Color.gray) + .frame(width: 20, height: 20) + .cornerRadius(4) + + Text(colorHex) + .font(.caption) + .fontWeight(.medium) + + Spacer() + + if let contrastRatio = getContrastRatio(colorHex, "#FFFFFF") { + Text("AA: \(contrastRatio >= 4.5 ? "✓" : "✗")") + .font(.caption) + .foregroundColor(contrastRatio >= 4.5 ? .green : .red) + } + } + .padding(.horizontal) + } + } + } + } + + Spacer() + } + } + .onAppear { + generateHarmony() + } + } + + private func generateHarmony() { + generatedColors = ColorHarmonyGenerator.generateHarmony(from: baseColor, type: selectedHarmony) + } + + private func getContrastRatio(_ foreground: String, _ background: String) -> Double? { + guard let fg = try? HexColorFormatter.parse(foreground), + let bg = try? HexColorFormatter.parse(background) else { + return nil + } + return ColorMath.contrastRatio(fg, bg) + } +} + +#Preview { + ColorHarmonyView(baseColor: .constant("#0A84FF"), selectedHarmony: .constant(.complementary)) +} \ No newline at end of file diff --git a/Example/iOSAppDemo/Sources/ColorPsychologyView.swift b/Example/iOSAppDemo/Sources/ColorPsychologyView.swift new file mode 100644 index 0000000..4c833b9 --- /dev/null +++ b/Example/iOSAppDemo/Sources/ColorPsychologyView.swift @@ -0,0 +1,228 @@ +import SwiftUI +import ColorsKit + +struct ColorPsychologyView: View { + @State private var selectedEmotion: EmotionalCategory = .calm + @State private var emotionColors: [String] = [] + @State private var inputColor = "#FF6B6B" + @State private var emotionalProfile: [EmotionalCategory: Double] = [:] + @State private var primaryEmotion: EmotionalCategory = .calm + @State private var selectedEmotions: Set = [.calm, .energetic] + @State private var multiEmotionPalette: [String] = [] + + var body: some View { + ScrollView { + VStack(spacing: 24) { + Text("Color Psychology Engine") + .font(.title2) + .fontWeight(.bold) + .padding(.top) + + // Emotion-Based Color Generation + VStack(alignment: .leading, spacing: 16) { + Text("Generate Colors by Emotion") + .font(.headline) + .padding(.horizontal) + + VStack(spacing: 12) { + Picker("Select Emotion", selection: $selectedEmotion) { + ForEach(EmotionalCategory.allCases, id: \.self) { emotion in + Text(emotion.rawValue.capitalized) + .tag(emotion) + } + } + .pickerStyle(MenuPickerStyle()) + .padding(.horizontal) + + Button("Generate Colors") { + generateEmotionColors() + } + .buttonStyle(.borderedProminent) + + if !emotionColors.isEmpty { + VStack(alignment: .leading, spacing: 8) { + Text("\(selectedEmotion.rawValue.capitalized) Colors") + .font(.subheadline) + .fontWeight(.medium) + .padding(.horizontal) + + LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 4), spacing: 8) { + ForEach(emotionColors, id: \.self) { colorHex in + VStack(spacing: 4) { + Rectangle() + .fill(Color(hex: colorHex) ?? Color.gray) + .frame(height: 60) + .cornerRadius(8) + + Text(colorHex) + .font(.caption2) + .fontWeight(.medium) + } + } + } + .padding(.horizontal) + } + } + } + } + + Divider() + + // Color Emotional Analysis + VStack(alignment: .leading, spacing: 16) { + Text("Analyze Color Emotions") + .font(.headline) + .padding(.horizontal) + + VStack(spacing: 12) { + HStack { + TextField("Enter hex color", text: $inputColor) + .textFieldStyle(RoundedBorderTextFieldStyle()) + + Rectangle() + .fill(Color(hex: inputColor) ?? Color.gray) + .frame(width: 50, height: 40) + .cornerRadius(8) + } + .padding(.horizontal) + + Button("Analyze Emotions") { + analyzeColorEmotions() + } + .buttonStyle(.borderedProminent) + + if !emotionalProfile.isEmpty { + VStack(alignment: .leading, spacing: 12) { + Text("Primary Emotion: \(primaryEmotion.rawValue.capitalized)") + .font(.subheadline) + .fontWeight(.bold) + .padding(.horizontal) + + Text("Emotional Profile") + .font(.subheadline) + .fontWeight(.medium) + .padding(.horizontal) + + ForEach(emotionalProfile.sorted(by: { $0.value > $1.value }), id: \.key) { emotion, confidence in + HStack { + Text(emotion.rawValue.capitalized) + .font(.caption) + .frame(width: 80, alignment: .leading) + + ProgressView(value: confidence, total: 1.0) + .progressViewStyle(LinearProgressViewStyle()) + + Text("\(Int(confidence * 100))%") + .font(.caption) + .fontWeight(.medium) + .frame(width: 40, alignment: .trailing) + } + .padding(.horizontal) + } + } + } + } + } + + Divider() + + // Multi-Emotion Palette Generation + VStack(alignment: .leading, spacing: 16) { + Text("Multi-Emotion Palette") + .font(.headline) + .padding(.horizontal) + + VStack(spacing: 12) { + Text("Select Emotions to Combine") + .font(.subheadline) + .padding(.horizontal) + + LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 3), spacing: 8) { + ForEach(EmotionalCategory.allCases, id: \.self) { emotion in + Button(action: { + if selectedEmotions.contains(emotion) { + selectedEmotions.remove(emotion) + } else { + selectedEmotions.insert(emotion) + } + }) { + Text(emotion.rawValue.capitalized) + .font(.caption) + .padding(.vertical, 8) + .padding(.horizontal, 12) + .background(selectedEmotions.contains(emotion) ? Color.blue : Color.gray.opacity(0.2)) + .foregroundColor(selectedEmotions.contains(emotion) ? .white : .primary) + .cornerRadius(8) + } + } + } + .padding(.horizontal) + + Button("Generate Multi-Emotion Palette") { + generateMultiEmotionPalette() + } + .buttonStyle(.borderedProminent) + .disabled(selectedEmotions.isEmpty) + + if !multiEmotionPalette.isEmpty { + VStack(alignment: .leading, spacing: 8) { + Text("Combined Palette") + .font(.subheadline) + .fontWeight(.medium) + .padding(.horizontal) + + LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 5), spacing: 8) { + ForEach(multiEmotionPalette, id: \.self) { colorHex in + VStack(spacing: 4) { + Rectangle() + .fill(Color(hex: colorHex) ?? Color.gray) + .frame(height: 50) + .cornerRadius(8) + + Text(colorHex) + .font(.caption2) + .fontWeight(.medium) + } + } + } + .padding(.horizontal) + } + } + } + } + + Spacer() + } + } + .onAppear { + generateEmotionColors() + analyzeColorEmotions() + } + } + + private func generateEmotionColors() { + emotionColors = ColorPsychology.colorsFor(emotion: selectedEmotion) + } + + private func analyzeColorEmotions() { + guard let rgba = try? HexColorFormatter.parse(inputColor) else { + return + } + + primaryEmotion = ColorPsychology.primaryEmotion(for: rgba) ?? .calm + emotionalProfile = ColorPsychology.emotionalProfile(for: rgba) + } + + private func generateMultiEmotionPalette() { + multiEmotionPalette = ColorPsychology.generatePalette( + for: Array(selectedEmotions), + count: min(selectedEmotions.count * 2, 10) + ) + } +} + + + +#Preview { + ColorPsychologyView() +} \ No newline at end of file diff --git a/Example/iOSAppDemo/Sources/ContentView.swift b/Example/iOSAppDemo/Sources/ContentView.swift new file mode 100644 index 0000000..8ae2d6f --- /dev/null +++ b/Example/iOSAppDemo/Sources/ContentView.swift @@ -0,0 +1,68 @@ +import SwiftUI +import ColorsKit + +struct ContentView: View { + @State private var selectedTab = 0 + @State private var baseColor = "#0A84FF" + @State private var selectedEmotion: EmotionalCategory = .energetic + @State private var selectedHarmony: ColorHarmonyType = .complementary + @State private var selectedBlendMode: ColorsKit.BlendMode = .multiply + @State private var temperature: Double = 6500 + @State private var tint: Double = 0 + + var body: some View { + TabView(selection: $selectedTab) { + // Color Harmony Tab + ColorHarmonyView(baseColor: $baseColor, selectedHarmony: $selectedHarmony) + .tabItem { + Image(systemName: "paintpalette") + Text("Harmony") + } + .tag(0) + + // Perceptual Colors Tab + PerceptualColorsView() + .tabItem { + Image(systemName: "eye") + Text("Perceptual") + } + .tag(1) + + // Blending Modes Tab + BlendingModesView() + .tabItem { + Image(systemName: "square.on.square") + Text("Blending") + } + .tag(2) + + // Color Psychology Tab + ColorPsychologyView() + .tabItem { + Image(systemName: "brain.head.profile") + Text("Psychology") + } + .tag(3) + + // Data Visualization Tab + DataVisualizationView() + .tabItem { + Image(systemName: "chart.bar") + Text("Data Viz") + } + .tag(4) + + // Temperature & Gradients Tab + TemperatureGradientsView() + .tabItem { + Image(systemName: "thermometer") + Text("Temperature") + } + .tag(5) + } + } +} + +#Preview { + ContentView() +} \ No newline at end of file diff --git a/Example/iOSAppDemo/Sources/DataVisualizationView.swift b/Example/iOSAppDemo/Sources/DataVisualizationView.swift new file mode 100644 index 0000000..982edfe --- /dev/null +++ b/Example/iOSAppDemo/Sources/DataVisualizationView.swift @@ -0,0 +1,232 @@ +import SwiftUI +import ColorsKit + +struct DataVisualizationView: View { + @State private var selectedGradientType: DataVisualizationType = .sequential + @State private var gradientSteps = 7 + @State private var generatedGradient: [String] = [] + @State private var sampleData: [Double] = [0.1, 0.3, 0.5, 0.7, 0.9, 0.4, 0.8, 0.2, 0.6] + + var body: some View { + ScrollView { + VStack(spacing: 24) { + Text("Data Visualization Gradients") + .font(.title2) + .fontWeight(.bold) + .padding(.top) + + // Gradient Type Selection + VStack(alignment: .leading, spacing: 16) { + Text("Gradient Type") + .font(.headline) + .padding(.horizontal) + + Picker("Gradient Type", selection: $selectedGradientType) { + ForEach(DataVisualizationType.allCases, id: \.self) { type in + Text(type.displayName) + .tag(type) + } + } + .pickerStyle(MenuPickerStyle()) + .padding(.horizontal) + + VStack(alignment: .leading) { + Text("Steps: \(gradientSteps)") + .font(.subheadline) + .padding(.horizontal) + + Slider(value: Binding( + get: { Double(gradientSteps) }, + set: { gradientSteps = Int($0) } + ), in: 3...12, step: 1) + .padding(.horizontal) + } + + Button("Generate Gradient") { + generateGradient() + } + .buttonStyle(.borderedProminent) + } + + // Generated Gradient Display + if !generatedGradient.isEmpty { + VStack(alignment: .leading, spacing: 16) { + Text("Generated Gradient") + .font(.headline) + .padding(.horizontal) + + // Gradient Bar + HStack(spacing: 2) { + ForEach(generatedGradient, id: \.self) { colorHex in + Rectangle() + .fill(Color(hex: colorHex) ?? Color.gray) + .frame(height: 60) + } + } + .cornerRadius(8) + .padding(.horizontal) + + // Color Values + LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: min(gradientSteps, 4)), spacing: 8) { + ForEach(Array(generatedGradient.enumerated()), id: \.offset) { index, colorHex in + VStack(spacing: 4) { + Rectangle() + .fill(Color(hex: colorHex) ?? Color.gray) + .frame(height: 40) + .cornerRadius(6) + + Text(colorHex) + .font(.caption2) + .fontWeight(.medium) + } + } + } + .padding(.horizontal) + } + } + + // Sample Data Visualization + if !generatedGradient.isEmpty { + VStack(alignment: .leading, spacing: 16) { + Text("Sample Data Visualization") + .font(.headline) + .padding(.horizontal) + + // Bar Chart + HStack(alignment: .bottom, spacing: 4) { + ForEach(Array(sampleData.enumerated()), id: \.offset) { index, value in + let colorIndex = min(Int(value * Double(generatedGradient.count - 1)), generatedGradient.count - 1) + let colorHex = generatedGradient[colorIndex] + + VStack(spacing: 4) { + Rectangle() + .fill(Color(hex: colorHex) ?? Color.gray) + .frame(width: 30, height: CGFloat(value * 100)) + .cornerRadius(4) + + Text("\(Int(value * 100))") + .font(.caption2) + } + } + } + .frame(height: 120) + .padding(.horizontal) + + // Heatmap Grid + Text("Heatmap Sample") + .font(.subheadline) + .fontWeight(.medium) + .padding(.horizontal) + + LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 6), spacing: 2) { + ForEach(0..<24, id: \.self) { index in + let value = Double.random(in: 0...1) + let colorIndex = min(Int(value * Double(generatedGradient.count - 1)), generatedGradient.count - 1) + let colorHex = generatedGradient[colorIndex] + + Rectangle() + .fill(Color(hex: colorHex) ?? Color.gray) + .frame(height: 30) + .cornerRadius(4) + } + } + .padding(.horizontal) + } + } + + // Gradient Information + VStack(alignment: .leading, spacing: 12) { + Text("Gradient Information") + .font(.headline) + .padding(.horizontal) + + VStack(alignment: .leading, spacing: 8) { + Text(selectedGradientType.description) + .font(.body) + .padding(.horizontal) + + Text("Best Use Cases:") + .font(.subheadline) + .fontWeight(.medium) + .padding(.horizontal) + + ForEach(selectedGradientType.useCases, id: \.self) { useCase in + Text("• \(useCase)") + .font(.caption) + .padding(.horizontal) + } + } + } + + Spacer() + } + } + .onAppear { + generateGradient() + } + .onChange(of: selectedGradientType) { _ in generateGradient() } + .onChange(of: gradientSteps) { _ in generateGradient() } + } + + private func generateGradient() { + generatedGradient = GradientGenerator.generateDataVisualizationGradient( + type: selectedGradientType, + steps: gradientSteps + ) + } +} + +extension DataVisualizationType: @retroactive CaseIterable { + public static var allCases: [DataVisualizationType] { + return [.sequential, .diverging, .heatmap, .viridis, .plasma, .temperature] + } + + var displayName: String { + switch self { + case .sequential: return "Sequential" + case .diverging: return "Diverging" + case .heatmap: return "Heatmap" + case .viridis: return "Viridis" + case .plasma: return "Plasma" + case .temperature: return "Temperature" + } + } + + var description: String { + switch self { + case .sequential: + return "Sequential gradients show progression from low to high values using a single hue with varying lightness and saturation." + case .diverging: + return "Diverging gradients emphasize deviations from a central value, using two contrasting hues that meet at a neutral midpoint." + case .heatmap: + return "Heatmap gradients use the classic blue-to-red spectrum, ideal for showing intensity or density data." + case .viridis: + return "Viridis is a perceptually uniform colormap that works well for scientific data visualization and is colorblind-friendly." + case .plasma: + return "Plasma provides high contrast and perceptual uniformity, excellent for highlighting data patterns and outliers." + case .temperature: + return "Temperature gradients simulate thermal imaging, progressing from cool blues through warm reds to hot whites." + } + } + + var useCases: [String] { + switch self { + case .sequential: + return ["Population density maps", "Sales performance charts", "Progress indicators", "Elevation maps"] + case .diverging: + return ["Temperature anomalies", "Survey responses", "Financial gains/losses", "Correlation matrices"] + case .heatmap: + return ["Website analytics", "Correlation matrices", "Density plots", "Activity tracking"] + case .viridis: + return ["Scientific data", "Medical imaging", "Accessibility-focused charts", "Academic publications"] + case .plasma: + return ["Astronomical data", "High-contrast visualizations", "Pattern detection", "Outlier identification"] + case .temperature: + return ["Thermal imaging", "Weather maps", "Heat distribution", "Energy consumption"] + } + } +} + +#Preview { + DataVisualizationView() +} \ No newline at end of file diff --git a/Example/iOSAppDemo/Sources/PerceptualColorsView.swift b/Example/iOSAppDemo/Sources/PerceptualColorsView.swift new file mode 100644 index 0000000..52213de --- /dev/null +++ b/Example/iOSAppDemo/Sources/PerceptualColorsView.swift @@ -0,0 +1,220 @@ +import SwiftUI +import ColorsKit + +struct PerceptualColorsView: View { + @State private var inputColor = "#FF6B6B" + @State private var targetColor = "#4ECDC4" + @State private var blendSteps = 5 + @State private var perceptualGradient: [String] = [] + @State private var regularGradient: [String] = [] + + var body: some View { + ScrollView { + VStack(spacing: 24) { + Text("Perceptual Color Spaces") + .font(.title2) + .fontWeight(.bold) + .padding(.top) + + // Color Input Section + VStack(spacing: 16) { + HStack { + VStack(alignment: .leading) { + Text("Start Color") + .font(.headline) + HStack { + TextField("Hex color", text: $inputColor) + .textFieldStyle(RoundedBorderTextFieldStyle()) + Rectangle() + .fill(Color(hex: inputColor) ?? Color.gray) + .frame(width: 40, height: 40) + .cornerRadius(8) + } + } + + VStack(alignment: .leading) { + Text("End Color") + .font(.headline) + HStack { + TextField("Hex color", text: $targetColor) + .textFieldStyle(RoundedBorderTextFieldStyle()) + Rectangle() + .fill(Color(hex: targetColor) ?? Color.gray) + .frame(width: 40, height: 40) + .cornerRadius(8) + } + } + } + .padding(.horizontal) + + VStack(alignment: .leading) { + Text("Blend Steps: \(blendSteps)") + .font(.headline) + Slider(value: Binding( + get: { Double(blendSteps) }, + set: { blendSteps = Int($0) } + ), in: 3...10, step: 1) + } + .padding(.horizontal) + + Button("Generate Gradients") { + generateGradients() + } + .buttonStyle(.borderedProminent) + } + + // Color Space Information + if let rgba = try? HexColorFormatter.parse(inputColor) { + VStack(alignment: .leading, spacing: 12) { + Text("Color Space Conversions") + .font(.headline) + .padding(.horizontal) + + VStack(alignment: .leading, spacing: 8) { + let xyz = ColorSpaceConverter.rgbaToXYZ(rgba) + let lab = ColorSpaceConverter.xyzToLAB(xyz) + let luv = ColorSpaceConverter.xyzToLUV(xyz) + + HStack { + Rectangle() + .fill(Color(hex: inputColor) ?? Color.gray) + .frame(width: 30, height: 30) + .cornerRadius(6) + + VStack(alignment: .leading, spacing: 2) { + Text("RGBA: (\(String(format: "%.2f", rgba.r)), \(String(format: "%.2f", rgba.g)), \(String(format: "%.2f", rgba.b)), \(String(format: "%.2f", rgba.a)))") + .font(.caption) + Text("XYZ: (\(String(format: "%.2f", xyz.x)), \(String(format: "%.2f", xyz.y)), \(String(format: "%.2f", xyz.z)))") + .font(.caption) + Text("LAB: (\(String(format: "%.1f", lab.l)), \(String(format: "%.1f", lab.a)), \(String(format: "%.1f", lab.b)))") + .font(.caption) + Text("LUV: (\(String(format: "%.1f", luv.l)), \(String(format: "%.1f", luv.u)), \(String(format: "%.1f", luv.v)))") + .font(.caption) + } + Spacer() + } + .padding(.horizontal) + } + } + } + + // Gradient Comparison + if !perceptualGradient.isEmpty && !regularGradient.isEmpty { + VStack(alignment: .leading, spacing: 16) { + Text("Gradient Comparison") + .font(.headline) + .padding(.horizontal) + + VStack(alignment: .leading, spacing: 12) { + Text("Perceptual (LAB) Gradient") + .font(.subheadline) + .fontWeight(.medium) + .padding(.horizontal) + + HStack(spacing: 2) { + ForEach(perceptualGradient, id: \.self) { colorHex in + Rectangle() + .fill(Color(hex: colorHex) ?? Color.gray) + .frame(height: 60) + } + } + .cornerRadius(8) + .padding(.horizontal) + + Text("Regular (RGB) Gradient") + .font(.subheadline) + .fontWeight(.medium) + .padding(.horizontal) + + HStack(spacing: 2) { + ForEach(regularGradient, id: \.self) { colorHex in + Rectangle() + .fill(Color(hex: colorHex) ?? Color.gray) + .frame(height: 60) + } + } + .cornerRadius(8) + .padding(.horizontal) + } + + // Delta E Analysis + if perceptualGradient.count > 1 { + VStack(alignment: .leading, spacing: 8) { + Text("Perceptual Uniformity (Delta E)") + .font(.subheadline) + .fontWeight(.medium) + .padding(.horizontal) + + ForEach(0.. Color { + guard !generatedGradient.isEmpty else { return Color.gray } + + let index = min(Int(animationProgress * Double(generatedGradient.count - 1)), generatedGradient.count - 1) + return Color(hex: generatedGradient[index]) ?? Color.gray + } + + private func toggleAnimation() { + if isAnimating { + isAnimating = false + } else { + isAnimating = true + animateProgress() + } + } + + private func animateProgress() { + guard isAnimating else { return } + + withAnimation(.linear(duration: 0.05)) { + animationProgress += 0.02 + if animationProgress >= 1.0 { + animationProgress = 0.0 + } + } + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { + animateProgress() + } + } + + private func resetAnimation() { + isAnimating = false + animationProgress = 0.0 + } + + private func applyTemperaturePreset(_ preset: TemperaturePreset) { + startColor = preset.colors.first ?? "#000000" + endColor = preset.colors.last ?? "#FFFFFF" + generateGradient() + } +} + +extension GradientInterpolation { + var displayName: String { + switch self { + case .linear: return "Linear RGB" + case .perceptual: return "Perceptual (LAB)" + case .hsl: return "HSL" + case .bezier: return "Bézier" + case .ease: return "Ease" + } + } + + var description: String { + switch self { + case .linear: + return "Linear interpolation in RGB color space. Simple and fast, but may produce muddy colors in the middle." + case .perceptual: + return "Perceptually uniform interpolation using LAB color space. Produces more natural-looking gradients." + case .hsl: + return "Interpolation in HSL color space, maintaining hue relationships for more vibrant transitions." + case .bezier: + return "Smooth Bézier curve interpolation for elegant, non-linear color transitions." + case .ease: + return "Eased interpolation with smooth acceleration and deceleration for natural motion." + } + } +} + +enum TemperaturePreset: String, CaseIterable { + case coolToWarm = "coolToWarm" + case thermal = "thermal" + case arctic = "arctic" + case sunset = "sunset" + case ocean = "ocean" + case fire = "fire" + + var displayName: String { + switch self { + case .coolToWarm: return "Cool to Warm" + case .thermal: return "Thermal" + case .arctic: return "Arctic" + case .sunset: return "Sunset" + case .ocean: return "Ocean Depths" + case .fire: return "Fire" + } + } + + var colors: [String] { + switch self { + case .coolToWarm: + return ["#0066CC", "#FFFFFF", "#FF3366"] + case .thermal: + return ["#000080", "#0000FF", "#00FFFF", "#00FF00", "#FFFF00", "#FF0000", "#FFFFFF"] + case .arctic: + return ["#001122", "#003366", "#0066CC", "#66CCFF", "#FFFFFF"] + case .sunset: + return ["#FF6B35", "#F7931E", "#FFD23F", "#FF6B6B", "#C44569"] + case .ocean: + return ["#000080", "#0033AA", "#0066CC", "#0099FF", "#66CCFF"] + case .fire: + return ["#8B0000", "#FF0000", "#FF4500", "#FFA500", "#FFFF00", "#FFFFFF"] + } + } +} + +#Preview { + TemperatureGradientsView() +} \ No newline at end of file diff --git a/Sources/ColorCore/ColorCore.swift b/Sources/ColorCore/ColorCore.swift index 4fa9868..bd0a9b1 100644 --- a/Sources/ColorCore/ColorCore.swift +++ b/Sources/ColorCore/ColorCore.swift @@ -1,7 +1,7 @@ import Foundation // Core RGBA model using sRGB color space -public struct RGBA: Equatable { +public struct RGBA: Equatable, Sendable { public var r: Double // 0...1 public var g: Double // 0...1 public var b: Double // 0...1 @@ -17,6 +17,265 @@ public struct RGBA: Equatable { public static func clamp(_ v: Double) -> Double { max(0.0, min(1.0, v)) } } +// MARK: - Perceptually Uniform Color Spaces + +/// CIE XYZ color space (1931 2° Standard Observer, D65 illuminant) +public struct CIEXYZ: Equatable, Sendable { + public var x: Double // 0...95.047 (D65) + public var y: Double // 0...100.0 + public var z: Double // 0...108.883 (D65) + + public init(x: Double, y: Double, z: Double) { + self.x = x + self.y = y + self.z = z + } +} + +/// CIE L*a*b* color space (perceptually uniform) +public struct CIELAB: Equatable, Sendable { + public var l: Double // 0...100 (lightness) + public var a: Double // -128...127 (green-red) + public var b: Double // -128...127 (blue-yellow) + + public init(l: Double, a: Double, b: Double) { + self.l = l + self.a = a + self.b = b + } +} + +/// CIE L*u*v* color space (perceptually uniform) +public struct CIELUV: Equatable, Sendable { + public var l: Double // 0...100 (lightness) + public var u: Double // -134...220 + public var v: Double // -140...122 + + public init(l: Double, u: Double, v: Double) { + self.l = l + self.u = u + self.v = v + } +} + +// MARK: - Color Space Conversions +public struct ColorSpaceConverter { + + // MARK: - Constants for D65 illuminant + private static let xn: Double = 95.047 // D65 illuminant + private static let yn: Double = 100.0 + private static let zn: Double = 108.883 + + // MARK: - sRGB to CIE XYZ + public static func rgbaToXYZ(_ rgba: RGBA) -> CIEXYZ { + // Convert sRGB to linear RGB + func sRGBToLinear(_ c: Double) -> Double { + return c <= 0.04045 ? c / 12.92 : pow((c + 0.055) / 1.055, 2.4) + } + + let r = sRGBToLinear(rgba.r) + let g = sRGBToLinear(rgba.g) + let b = sRGBToLinear(rgba.b) + + // sRGB to XYZ matrix (D65) + let x = r * 0.4124564 + g * 0.3575761 + b * 0.1804375 + let y = r * 0.2126729 + g * 0.7151522 + b * 0.0721750 + let z = r * 0.0193339 + g * 0.1191920 + b * 0.9503041 + + return CIEXYZ(x: x * 100, y: y * 100, z: z * 100) + } + + // MARK: - CIE XYZ to sRGB + public static func xyzToRGBA(_ xyz: CIEXYZ) -> RGBA { + let x = xyz.x / 100 + let y = xyz.y / 100 + let z = xyz.z / 100 + + // XYZ to sRGB matrix (D65) + let r = x * 3.2404542 + y * -1.5371385 + z * -0.4985314 + let g = x * -0.9692660 + y * 1.8760108 + z * 0.0415560 + let b = x * 0.0556434 + y * -0.2040259 + z * 1.0572252 + + // Linear RGB to sRGB + func linearToSRGB(_ c: Double) -> Double { + let clamped = max(0, min(1, c)) + return clamped <= 0.0031308 ? clamped * 12.92 : 1.055 * pow(clamped, 1.0/2.4) - 0.055 + } + + return RGBA( + r: RGBA.clamp(linearToSRGB(r)), + g: RGBA.clamp(linearToSRGB(g)), + b: RGBA.clamp(linearToSRGB(b)), + a: 1.0 + ) + } + + // MARK: - CIE XYZ to CIE L*a*b* + public static func xyzToLAB(_ xyz: CIEXYZ) -> CIELAB { + func f(_ t: Double) -> Double { + let delta = 6.0 / 29.0 + return t > pow(delta, 3) ? pow(t, 1.0/3.0) : t / (3 * delta * delta) + 4.0/29.0 + } + + let fx = f(xyz.x / xn) + let fy = f(xyz.y / yn) + let fz = f(xyz.z / zn) + + let l = 116 * fy - 16 + let a = 500 * (fx - fy) + let b = 200 * (fy - fz) + + return CIELAB(l: l, a: a, b: b) + } + + // MARK: - CIE L*a*b* to CIE XYZ + public static func labToXYZ(_ lab: CIELAB) -> CIEXYZ { + let fy = (lab.l + 16) / 116 + let fx = lab.a / 500 + fy + let fz = fy - lab.b / 200 + + func finv(_ t: Double) -> Double { + let delta = 6.0 / 29.0 + return t > delta ? pow(t, 3) : 3 * delta * delta * (t - 4.0/29.0) + } + + let x = xn * finv(fx) + let y = yn * finv(fy) + let z = zn * finv(fz) + + return CIEXYZ(x: x, y: y, z: z) + } + + // MARK: - CIE XYZ to CIE L*u*v* + public static func xyzToLUV(_ xyz: CIEXYZ) -> CIELUV { + let yr = xyz.y / yn + let l = yr > pow(6.0/29.0, 3) ? 116 * pow(yr, 1.0/3.0) - 16 : pow(29.0/3.0, 3) * yr + + let denom = xyz.x + 15 * xyz.y + 3 * xyz.z + let denomN = xn + 15 * yn + 3 * zn + + guard denom != 0 && denomN != 0 else { + return CIELUV(l: l, u: 0, v: 0) + } + + let up = 4 * xyz.x / denom + let vp = 9 * xyz.y / denom + let upN = 4 * xn / denomN + let vpN = 9 * yn / denomN + + let u = 13 * l * (up - upN) + let v = 13 * l * (vp - vpN) + + return CIELUV(l: l, u: u, v: v) + } + + // MARK: - CIE L*u*v* to CIE XYZ + public static func luvToXYZ(_ luv: CIELUV) -> CIEXYZ { + let yr = luv.l > 8 ? pow((luv.l + 16) / 116, 3) : luv.l / pow(29.0/3.0, 3) + let y = yr * yn + + let denomN = xn + 15 * yn + 3 * zn + let upN = 4 * xn / denomN + let vpN = 9 * yn / denomN + + guard luv.l != 0 else { + return CIEXYZ(x: 0, y: 0, z: 0) + } + + let up = luv.u / (13 * luv.l) + upN + let vp = luv.v / (13 * luv.l) + vpN + + guard vp != 0 else { + return CIEXYZ(x: 0, y: y, z: 0) + } + + let x = y * 9 * up / (4 * vp) + let z = y * (12 - 3 * up - 20 * vp) / (4 * vp) + + return CIEXYZ(x: x, y: y, z: z) + } + + // MARK: - Convenience conversions + public static func rgbaToLAB(_ rgba: RGBA) -> CIELAB { + return xyzToLAB(rgbaToXYZ(rgba)) + } + + public static func labToRGBA(_ lab: CIELAB) -> RGBA { + return xyzToRGBA(labToXYZ(lab)) + } + + public static func rgbaToLUV(_ rgba: RGBA) -> CIELUV { + return xyzToLUV(rgbaToXYZ(rgba)) + } + + public static func luvToRGBA(_ luv: CIELUV) -> RGBA { + return xyzToRGBA(luvToXYZ(luv)) + } +} + +// MARK: - Perceptual Color Operations +public struct PerceptualColorMath { + + /// Calculate perceptual distance between two colors using CIE Delta E 2000 + /// This is more accurate than RGB distance for human perception + public static func deltaE2000(_ color1: RGBA, _ color2: RGBA) -> Double { + let lab1 = ColorSpaceConverter.rgbaToLAB(color1) + let lab2 = ColorSpaceConverter.rgbaToLAB(color2) + + // Simplified Delta E 2000 calculation + // For full implementation, consider using a specialized color science library + let deltaL = lab2.l - lab1.l + let deltaA = lab2.a - lab1.a + let deltaB = lab2.b - lab1.b + + let c1 = sqrt(lab1.a * lab1.a + lab1.b * lab1.b) + let c2 = sqrt(lab2.a * lab2.a + lab2.b * lab2.b) + let deltaC = c2 - c1 + + let deltaH = sqrt(deltaA * deltaA + deltaB * deltaB - deltaC * deltaC) + + let sl = 1.0 + let sc = 1 + 0.045 * c1 + let sh = 1 + 0.015 * c1 + + let deltaE = sqrt( + pow(deltaL / sl, 2) + + pow(deltaC / sc, 2) + + pow(deltaH / sh, 2) + ) + + return deltaE + } + + /// Blend two colors in perceptually uniform LAB space + /// This produces more natural-looking color transitions + public static func perceptualBlend(_ color1: RGBA, _ color2: RGBA, ratio: Double) -> RGBA { + let lab1 = ColorSpaceConverter.rgbaToLAB(color1) + let lab2 = ColorSpaceConverter.rgbaToLAB(color2) + + let t = max(0, min(1, ratio)) + let blendedLab = CIELAB( + l: lab1.l + (lab2.l - lab1.l) * t, + a: lab1.a + (lab2.a - lab1.a) * t, + b: lab1.b + (lab2.b - lab1.b) * t + ) + + return ColorSpaceConverter.labToRGBA(blendedLab) + } + + /// Generate a perceptually uniform gradient between colors + public static func perceptualGradient(from start: RGBA, to end: RGBA, steps: Int) -> [RGBA] { + guard steps > 1 else { return [start] } + + var colors: [RGBA] = [] + for i in 0.. RGBA { + let adjustedTop = RGBA(r: top.r, g: top.g, b: top.b, a: top.a * RGBA.clamp(opacity)) + + switch mode { + case .normal: + return ColorMath.blend(adjustedTop, bottom) + case .multiply: + return multiply(adjustedTop, bottom) + case .screen: + return screen(adjustedTop, bottom) + case .overlay: + return overlay(adjustedTop, bottom) + case .softLight: + return softLight(adjustedTop, bottom) + case .hardLight: + return hardLight(adjustedTop, bottom) + case .colorDodge: + return colorDodge(adjustedTop, bottom) + case .colorBurn: + return colorBurn(adjustedTop, bottom) + case .darken: + return darken(adjustedTop, bottom) + case .lighten: + return lighten(adjustedTop, bottom) + case .difference: + return difference(adjustedTop, bottom) + case .exclusion: + return exclusion(adjustedTop, bottom) + case .hue: + return hue(adjustedTop, bottom) + case .saturation: + return saturation(adjustedTop, bottom) + case .color: + return color(adjustedTop, bottom) + case .luminosity: + return luminosity(adjustedTop, bottom) + } + } + + // MARK: - Basic Blending Modes + + private static func multiply(_ top: RGBA, _ bottom: RGBA) -> RGBA { + let blended = RGBA( + r: top.r * bottom.r, + g: top.g * bottom.g, + b: top.b * bottom.b, + a: 1.0 + ) + return ColorMath.blend(RGBA(r: blended.r, g: blended.g, b: blended.b, a: top.a), bottom) + } + + private static func screen(_ top: RGBA, _ bottom: RGBA) -> RGBA { + let blended = RGBA( + r: 1.0 - (1.0 - top.r) * (1.0 - bottom.r), + g: 1.0 - (1.0 - top.g) * (1.0 - bottom.g), + b: 1.0 - (1.0 - top.b) * (1.0 - bottom.b), + a: 1.0 + ) + return ColorMath.blend(RGBA(r: blended.r, g: blended.g, b: blended.b, a: top.a), bottom) + } + + private static func overlay(_ top: RGBA, _ bottom: RGBA) -> RGBA { + func overlayChannel(_ a: Double, _ b: Double) -> Double { + return b < 0.5 ? 2.0 * a * b : 1.0 - 2.0 * (1.0 - a) * (1.0 - b) + } + + let blended = RGBA( + r: overlayChannel(top.r, bottom.r), + g: overlayChannel(top.g, bottom.g), + b: overlayChannel(top.b, bottom.b), + a: 1.0 + ) + return ColorMath.blend(RGBA(r: blended.r, g: blended.g, b: blended.b, a: top.a), bottom) + } + + private static func softLight(_ top: RGBA, _ bottom: RGBA) -> RGBA { + func softLightChannel(_ a: Double, _ b: Double) -> Double { + if a <= 0.5 { + return b - (1.0 - 2.0 * a) * b * (1.0 - b) + } else { + let d = b <= 0.25 ? ((16.0 * b - 12.0) * b + 4.0) * b : sqrt(b) + return b + (2.0 * a - 1.0) * (d - b) + } + } + + let blended = RGBA( + r: softLightChannel(top.r, bottom.r), + g: softLightChannel(top.g, bottom.g), + b: softLightChannel(top.b, bottom.b), + a: 1.0 + ) + return ColorMath.blend(RGBA(r: blended.r, g: blended.g, b: blended.b, a: top.a), bottom) + } + + private static func hardLight(_ top: RGBA, _ bottom: RGBA) -> RGBA { + func hardLightChannel(_ a: Double, _ b: Double) -> Double { + return a < 0.5 ? 2.0 * a * b : 1.0 - 2.0 * (1.0 - a) * (1.0 - b) + } + + let blended = RGBA( + r: hardLightChannel(top.r, bottom.r), + g: hardLightChannel(top.g, bottom.g), + b: hardLightChannel(top.b, bottom.b), + a: 1.0 + ) + return ColorMath.blend(RGBA(r: blended.r, g: blended.g, b: blended.b, a: top.a), bottom) + } + + private static func colorDodge(_ top: RGBA, _ bottom: RGBA) -> RGBA { + func colorDodgeChannel(_ a: Double, _ b: Double) -> Double { + if a >= 1.0 { return 1.0 } + return min(1.0, b / (1.0 - a)) + } + + let blended = RGBA( + r: colorDodgeChannel(top.r, bottom.r), + g: colorDodgeChannel(top.g, bottom.g), + b: colorDodgeChannel(top.b, bottom.b), + a: 1.0 + ) + return ColorMath.blend(RGBA(r: blended.r, g: blended.g, b: blended.b, a: top.a), bottom) + } + + private static func colorBurn(_ top: RGBA, _ bottom: RGBA) -> RGBA { + func colorBurnChannel(_ a: Double, _ b: Double) -> Double { + if a <= 0.0 { return 0.0 } + return max(0.0, 1.0 - (1.0 - b) / a) + } + + let blended = RGBA( + r: colorBurnChannel(top.r, bottom.r), + g: colorBurnChannel(top.g, bottom.g), + b: colorBurnChannel(top.b, bottom.b), + a: 1.0 + ) + return ColorMath.blend(RGBA(r: blended.r, g: blended.g, b: blended.b, a: top.a), bottom) + } + + private static func darken(_ top: RGBA, _ bottom: RGBA) -> RGBA { + let blended = RGBA( + r: min(top.r, bottom.r), + g: min(top.g, bottom.g), + b: min(top.b, bottom.b), + a: 1.0 + ) + return ColorMath.blend(RGBA(r: blended.r, g: blended.g, b: blended.b, a: top.a), bottom) + } + + private static func lighten(_ top: RGBA, _ bottom: RGBA) -> RGBA { + let blended = RGBA( + r: max(top.r, bottom.r), + g: max(top.g, bottom.g), + b: max(top.b, bottom.b), + a: 1.0 + ) + return ColorMath.blend(RGBA(r: blended.r, g: blended.g, b: blended.b, a: top.a), bottom) + } + + private static func difference(_ top: RGBA, _ bottom: RGBA) -> RGBA { + let blended = RGBA( + r: abs(top.r - bottom.r), + g: abs(top.g - bottom.g), + b: abs(top.b - bottom.b), + a: 1.0 + ) + return ColorMath.blend(RGBA(r: blended.r, g: blended.g, b: blended.b, a: top.a), bottom) + } + + private static func exclusion(_ top: RGBA, _ bottom: RGBA) -> RGBA { + let blended = RGBA( + r: top.r + bottom.r - 2.0 * top.r * bottom.r, + g: top.g + bottom.g - 2.0 * top.g * bottom.g, + b: top.b + bottom.b - 2.0 * top.b * bottom.b, + a: 1.0 + ) + return ColorMath.blend(RGBA(r: blended.r, g: blended.g, b: blended.b, a: top.a), bottom) + } + + // MARK: - HSL-based Blending Modes + + private static func hue(_ top: RGBA, _ bottom: RGBA) -> RGBA { + let topHSL = rgbaToHSL(top) + let bottomHSL = rgbaToHSL(bottom) + let blended = hslToRGBA(HSL(h: topHSL.h, s: bottomHSL.s, l: bottomHSL.l)) + return ColorMath.blend(RGBA(r: blended.r, g: blended.g, b: blended.b, a: top.a), bottom) + } + + private static func saturation(_ top: RGBA, _ bottom: RGBA) -> RGBA { + let topHSL = rgbaToHSL(top) + let bottomHSL = rgbaToHSL(bottom) + let blended = hslToRGBA(HSL(h: bottomHSL.h, s: topHSL.s, l: bottomHSL.l)) + return ColorMath.blend(RGBA(r: blended.r, g: blended.g, b: blended.b, a: top.a), bottom) + } + + private static func color(_ top: RGBA, _ bottom: RGBA) -> RGBA { + let topHSL = rgbaToHSL(top) + let bottomHSL = rgbaToHSL(bottom) + let blended = hslToRGBA(HSL(h: topHSL.h, s: topHSL.s, l: bottomHSL.l)) + return ColorMath.blend(RGBA(r: blended.r, g: blended.g, b: blended.b, a: top.a), bottom) + } + + private static func luminosity(_ top: RGBA, _ bottom: RGBA) -> RGBA { + let topHSL = rgbaToHSL(top) + let bottomHSL = rgbaToHSL(bottom) + let blended = hslToRGBA(HSL(h: bottomHSL.h, s: bottomHSL.s, l: topHSL.l)) + return ColorMath.blend(RGBA(r: blended.r, g: blended.g, b: blended.b, a: top.a), bottom) + } + + // MARK: - HSL Conversion Helpers + + private struct HSL { + let h: Double // 0...360 + let s: Double // 0...1 + let l: Double // 0...1 + } + + private static func rgbaToHSL(_ rgba: RGBA) -> HSL { + let max = Swift.max(rgba.r, rgba.g, rgba.b) + let min = Swift.min(rgba.r, rgba.g, rgba.b) + let delta = max - min + + let l = (max + min) / 2.0 + + guard delta > 0 else { + return HSL(h: 0, s: 0, l: l) + } + + let s = l > 0.5 ? delta / (2.0 - max - min) : delta / (max + min) + + var h: Double + switch max { + case rgba.r: + h = ((rgba.g - rgba.b) / delta) + (rgba.g < rgba.b ? 6 : 0) + case rgba.g: + h = (rgba.b - rgba.r) / delta + 2 + default: // rgba.b + h = (rgba.r - rgba.g) / delta + 4 + } + h *= 60 + + return HSL(h: h, s: s, l: l) + } + + private static func hslToRGBA(_ hsl: HSL) -> RGBA { + guard hsl.s > 0 else { + return RGBA(r: hsl.l, g: hsl.l, b: hsl.l, a: 1.0) + } + + func hueToRGB(_ p: Double, _ q: Double, _ t: Double) -> Double { + var t = t + if t < 0 { t += 1 } + if t > 1 { t -= 1 } + if t < 1/6 { return p + (q - p) * 6 * t } + if t < 1/2 { return q } + if t < 2/3 { return p + (q - p) * (2/3 - t) * 6 } + return p + } + + let q = hsl.l < 0.5 ? hsl.l * (1 + hsl.s) : hsl.l + hsl.s - hsl.l * hsl.s + let p = 2 * hsl.l - q + let h = hsl.h / 360 + + return RGBA( + r: hueToRGB(p, q, h + 1/3), + g: hueToRGB(p, q, h), + b: hueToRGB(p, q, h - 1/3), + a: 1.0 + ) + } +} + // MARK: - Accessibility helpers -public enum WCAGLevel { case AA, AAA } +public enum WCAGLevel: Sendable { case AA, AAA } public struct Accessibility { /// Check contrast ratio against WCAG thresholds (normal text) @@ -118,6 +680,371 @@ public struct Accessibility { } } +// MARK: - Advanced Gradient Generation + +/// Gradient interpolation methods for different use cases +public enum GradientInterpolation: Sendable, CaseIterable { + case linear // Simple linear interpolation in RGB + case perceptual // Perceptually uniform interpolation in LAB space + case hsl // HSL interpolation for smooth hue transitions + case bezier // Bezier curve interpolation for smooth transitions + case ease // Eased interpolation with smooth acceleration/deceleration +} + +/// Gradient generation optimized for data visualization and smooth transitions +public struct GradientGenerator { + + /// Generate a gradient between two colors + /// - Parameters: + /// - startColor: Starting color (hex string) + /// - endColor: Ending color (hex string) + /// - steps: Number of steps in the gradient + /// - interpolation: Interpolation method to use + /// - Returns: Array of hex color strings representing the gradient + public static func generateGradient( + from startColor: String, + to endColor: String, + steps: Int, + interpolation: GradientInterpolation = .perceptual + ) -> [String] { + guard let startRGBA = try? HexColorFormatter.parse(startColor), + let endRGBA = try? HexColorFormatter.parse(endColor), + steps > 1 else { return [startColor, endColor] } + + return generateGradient(from: startRGBA, to: endRGBA, steps: steps, interpolation: interpolation) + } + + /// Generate a gradient between two RGBA colors + /// - Parameters: + /// - startColor: Starting RGBA color + /// - endColor: Ending RGBA color + /// - steps: Number of steps in the gradient + /// - interpolation: Interpolation method to use + /// - Returns: Array of hex color strings representing the gradient + public static func generateGradient( + from startColor: RGBA, + to endColor: RGBA, + steps: Int, + interpolation: GradientInterpolation = .perceptual + ) -> [String] { + guard steps > 1 else { return [HexColorFormatter.format(startColor), HexColorFormatter.format(endColor)] } + + var colors: [String] = [] + + for i in 0.. [String] { + guard colors.count >= 2, steps > 1 else { return colors } + + let rgbaColors = colors.compactMap { try? HexColorFormatter.parse($0) } + guard rgbaColors.count == colors.count else { return colors } + + return generateMultiStopGradient(colors: rgbaColors, steps: steps, interpolation: interpolation) + } + + /// Generate a multi-stop gradient with RGBA colors + /// - Parameters: + /// - colors: Array of RGBA color stops + /// - steps: Total number of steps in the gradient + /// - interpolation: Interpolation method to use + /// - Returns: Array of hex color strings representing the gradient + public static func generateMultiStopGradient( + colors: [RGBA], + steps: Int, + interpolation: GradientInterpolation = .perceptual + ) -> [String] { + guard colors.count >= 2, steps > 1 else { + return colors.map { HexColorFormatter.format($0) } + } + + var gradientColors: [String] = [] + let segmentCount = colors.count - 1 + let stepsPerSegment = max(1, steps / segmentCount) + + for i in 0.. [String] { + switch type { + case .sequential: + return generateGradient( + from: "#F7FBFF", + to: "#08306B", + steps: steps, + interpolation: .perceptual + ) + case .diverging: + return generateMultiStopGradient( + colors: ["#D73027", "#FFFFBF", "#1A9850"], + steps: steps, + interpolation: .perceptual + ) + case .heatmap: + return generateMultiStopGradient( + colors: ["#000428", "#004e92", "#009ffd", "#00d2ff"], + steps: steps, + interpolation: .perceptual + ) + case .viridis: + return generateMultiStopGradient( + colors: ["#440154", "#31688e", "#35b779", "#fde725"], + steps: steps, + interpolation: .perceptual + ) + case .plasma: + return generateMultiStopGradient( + colors: ["#0d0887", "#7e03a8", "#cc4778", "#f89441", "#f0f921"], + steps: steps, + interpolation: .perceptual + ) + case .temperature: + return generateMultiStopGradient( + colors: ["#313695", "#74add1", "#ffffbf", "#f46d43", "#a50026"], + steps: steps, + interpolation: .perceptual + ) + } + } + + /// Generate a smooth color transition optimized for UI animations + /// - Parameters: + /// - startColor: Starting color (hex string) + /// - endColor: Ending color (hex string) + /// - duration: Animation duration in seconds + /// - fps: Frames per second for the animation + /// - Returns: Array of hex color strings for smooth animation + public static func generateAnimationGradient( + from startColor: String, + to endColor: String, + duration: Double, + fps: Int = 60 + ) -> [String] { + let totalFrames = Int(duration * Double(fps)) + return generateGradient( + from: startColor, + to: endColor, + steps: totalFrames, + interpolation: .ease + ) + } + + // MARK: - Private Helper Methods + + private static func linearInterpolate(_ start: RGBA, _ end: RGBA, t: Double) -> RGBA { + return RGBA( + r: start.r + (end.r - start.r) * t, + g: start.g + (end.g - start.g) * t, + b: start.b + (end.b - start.b) * t, + a: start.a + (end.a - start.a) * t + ) + } + + private static func perceptualInterpolate(_ start: RGBA, _ end: RGBA, t: Double) -> RGBA { + let startLAB = ColorSpaceConverter.rgbaToLAB(start) + let endLAB = ColorSpaceConverter.rgbaToLAB(end) + + let interpolatedLAB = CIELAB( + l: startLAB.l + (endLAB.l - startLAB.l) * t, + a: startLAB.a + (endLAB.a - startLAB.a) * t, + b: startLAB.b + (endLAB.b - startLAB.b) * t + ) + + var result = ColorSpaceConverter.labToRGBA(interpolatedLAB) + result.a = start.a + (end.a - start.a) * t + return result + } + + private static func hslInterpolate(_ start: RGBA, _ end: RGBA, t: Double) -> RGBA { + let startHSL = rgbaToHSL(start) + let endHSL = rgbaToHSL(end) + + // Handle hue interpolation (shortest path around the color wheel) + var hueDiff = endHSL.h - startHSL.h + if hueDiff > 180 { + hueDiff -= 360 + } else if hueDiff < -180 { + hueDiff += 360 + } + + let interpolatedHSL = HSL( + h: (startHSL.h + hueDiff * t).truncatingRemainder(dividingBy: 360), + s: startHSL.s + (endHSL.s - startHSL.s) * t, + l: startHSL.l + (endHSL.l - startHSL.l) * t + ) + + var result = hslToRGBA(interpolatedHSL) + result.a = start.a + (end.a - start.a) * t + return result + } + + private static func bezierInterpolate(_ start: RGBA, _ end: RGBA, t: Double) -> RGBA { + // Use cubic bezier curve for smooth interpolation + let controlPoint1 = RGBA( + r: start.r + (end.r - start.r) * 0.33, + g: start.g + (end.g - start.g) * 0.33, + b: start.b + (end.b - start.b) * 0.33, + a: start.a + (end.a - start.a) * 0.33 + ) + + let controlPoint2 = RGBA( + r: start.r + (end.r - start.r) * 0.67, + g: start.g + (end.g - start.g) * 0.67, + b: start.b + (end.b - start.b) * 0.67, + a: start.a + (end.a - start.a) * 0.67 + ) + + // Cubic bezier interpolation + let oneMinusT = 1.0 - t + let oneMinusTSquared = oneMinusT * oneMinusT + let oneMinusTCubed = oneMinusTSquared * oneMinusT + let tSquared = t * t + let tCubed = tSquared * t + + return RGBA( + r: oneMinusTCubed * start.r + 3 * oneMinusTSquared * t * controlPoint1.r + 3 * oneMinusT * tSquared * controlPoint2.r + tCubed * end.r, + g: oneMinusTCubed * start.g + 3 * oneMinusTSquared * t * controlPoint1.g + 3 * oneMinusT * tSquared * controlPoint2.g + tCubed * end.g, + b: oneMinusTCubed * start.b + 3 * oneMinusTSquared * t * controlPoint1.b + 3 * oneMinusT * tSquared * controlPoint2.b + tCubed * end.b, + a: oneMinusTCubed * start.a + 3 * oneMinusTSquared * t * controlPoint1.a + 3 * oneMinusT * tSquared * controlPoint2.a + tCubed * end.a + ) + } + + private static func applyEasing(_ t: Double, for interpolation: GradientInterpolation) -> Double { + guard interpolation == .ease else { return t } + + // Ease-in-out cubic function + return t < 0.5 ? 4 * t * t * t : 1 - pow(-2 * t + 2, 3) / 2 + } + + private struct HSL { + let h: Double // 0...360 + let s: Double // 0...1 + let l: Double // 0...1 + } + + private static func rgbaToHSL(_ rgba: RGBA) -> HSL { + let max = Swift.max(rgba.r, rgba.g, rgba.b) + let min = Swift.min(rgba.r, rgba.g, rgba.b) + let delta = max - min + + let l = (max + min) / 2.0 + + guard delta > 0 else { + return HSL(h: 0, s: 0, l: l) + } + + let s = l > 0.5 ? delta / (2.0 - max - min) : delta / (max + min) + + var h: Double + switch max { + case rgba.r: + h = ((rgba.g - rgba.b) / delta) + (rgba.g < rgba.b ? 6 : 0) + case rgba.g: + h = (rgba.b - rgba.r) / delta + 2 + default: // rgba.b + h = (rgba.r - rgba.g) / delta + 4 + } + h *= 60 + + return HSL(h: h, s: s, l: l) + } + + private static func hslToRGBA(_ hsl: HSL) -> RGBA { + let c = (1.0 - abs(2.0 * hsl.l - 1.0)) * hsl.s + let x = c * (1.0 - abs((hsl.h / 60.0).truncatingRemainder(dividingBy: 2.0) - 1.0)) + let m = hsl.l - c / 2.0 + + var r, g, b: Double + + switch hsl.h { + case 0..<60: + (r, g, b) = (c, x, 0) + case 60..<120: + (r, g, b) = (x, c, 0) + case 120..<180: + (r, g, b) = (0, c, x) + case 180..<240: + (r, g, b) = (0, x, c) + case 240..<300: + (r, g, b) = (x, 0, c) + default: + (r, g, b) = (c, 0, x) + } + + return RGBA(r: r + m, g: g + m, b: b + m, a: 1.0) + } +} + +/// Data visualization gradient types +public enum DataVisualizationType: Sendable { + case sequential // Single hue progression + case diverging // Two-hue progression with neutral center + case heatmap // Heat map visualization + case viridis // Perceptually uniform colormap + case plasma // High contrast colormap + case temperature // Temperature visualization +} + // MARK: - Theme public struct Theme: Sendable { public let name: String diff --git a/Sources/ColorPalettes/ColorPalettes.swift b/Sources/ColorPalettes/ColorPalettes.swift index 73f72ad..3098dea 100644 --- a/Sources/ColorPalettes/ColorPalettes.swift +++ b/Sources/ColorPalettes/ColorPalettes.swift @@ -3,33 +3,259 @@ import Foundation import ColorCore #endif -public enum Palettes { - public static let defaultLight = Theme(name: "DefaultLight", colors: [ - "primary": "#0A84FF", - "secondary": "#5E5CE6", - "background": "#FFFFFF", - "surface": "#F2F2F7", - "text": "#1C1C1E", - "danger": "#FF3B30", - "success": "#34C759" - ]) +// MARK: - Default Themes +public let defaultLight = Theme(name: "Light", colors: [ + "primary": "#007AFF", + "secondary": "#5856D6", + "success": "#34C759", + "warning": "#FF9500", + "danger": "#FF3B30", + "background": "#FFFFFF", + "surface": "#F2F2F7", + "text": "#000000", + "textSecondary": "#8E8E93" +]) + +public let defaultDark = Theme(name: "Dark", colors: [ + "primary": "#0A84FF", + "secondary": "#5E5CE6", + "success": "#30D158", + "warning": "#FF9F0A", + "danger": "#FF453A", + "background": "#000000", + "surface": "#1C1C1E", + "text": "#FFFFFF", + "textSecondary": "#8E8E93" +]) - public static let defaultDark = Theme(name: "DefaultDark", colors: [ - "primary": "#0A84FF", - "secondary": "#5E5CE6", - "background": "#000000", - "surface": "#1C1C1E", - "text": "#FFFFFF", - "danger": "#FF453A", - "success": "#30D158" +// MARK: - Material Blue Theme +public func materialBlue() -> Theme { + return Theme(name: "Material Blue", colors: [ + "primary": "#2196F3", + "primaryLight": "#BBDEFB", + "primaryDark": "#1976D2", + "accent": "#FF4081", + "background": "#FAFAFA", + "surface": "#FFFFFF", + "text": "#212121", + "textSecondary": "#757575" ]) +} + +// MARK: - Data Visualization Palettes - public static func materialBlue(name: String = "MaterialBlue") -> Theme { - return Theme(name: name, colors: [ - "blue50": "#E3F2FD", "blue100": "#BBDEFB", "blue200": "#90CAF9", - "blue300": "#64B5F6", "blue400": "#42A5F5", "blue500": "#2196F3", - "blue600": "#1E88E5", "blue700": "#1976D2", "blue800": "#1565C0", - "blue900": "#0D47A1" - ]) +/// Specialized color palettes for data visualization +public struct DataVisualizationPalettes { + + // MARK: - Sequential Palettes (for continuous data) + + /// Blues sequential palette - ideal for showing data progression + public static let blues: [String] = [ + "#F7FBFF", "#DEEBF7", "#C6DBEF", "#9ECAE1", + "#6BAED6", "#4292C6", "#2171B5", "#08519C", "#08306B" + ] + + /// Greens sequential palette - great for positive metrics + public static let greens: [String] = [ + "#F7FCF5", "#E5F5E0", "#C7E9C0", "#A1D99B", + "#74C476", "#41AB5D", "#238B45", "#006D2C", "#00441B" + ] + + /// Reds sequential palette - effective for highlighting issues + public static let reds: [String] = [ + "#FFF5F0", "#FEE0D2", "#FCBBA1", "#FC9272", + "#FB6A4A", "#EF3B2C", "#CB181D", "#A50F15", "#67000D" + ] + + /// Purples sequential palette - elegant for general data + public static let purples: [String] = [ + "#FCFBFD", "#EFEDF5", "#DADAEB", "#BCBDDC", + "#9E9AC8", "#807DBA", "#6A51A3", "#54278F", "#3F007D" + ] + + /// Oranges sequential palette - warm and attention-grabbing + public static let oranges: [String] = [ + "#FFF5EB", "#FEE6CE", "#FDD0A2", "#FDAE6B", + "#FD8D3C", "#F16913", "#D94801", "#A63603", "#7F2704" + ] + + // MARK: - Diverging Palettes (for data with meaningful center) + + /// Red-Blue diverging palette - classic for showing opposing values + public static let redBlue: [String] = [ + "#67001F", "#B2182B", "#D6604D", "#F4A582", "#FDDBC7", + "#F7F7F7", "#D1E5F0", "#92C5DE", "#4393C3", "#2166AC", "#053061" + ] + + /// Red-Yellow-Blue diverging palette - great for temperature-like data + public static let redYellowBlue: [String] = [ + "#A50026", "#D73027", "#F46D43", "#FDAE61", "#FEE090", + "#FFFFBF", "#E0F3F8", "#ABD9E9", "#74ADD1", "#4575B4", "#313695" + ] + + /// Purple-Green diverging palette - sophisticated alternative + public static let purpleGreen: [String] = [ + "#40004B", "#762A83", "#9970AB", "#C2A5CF", "#E7D4E8", + "#F7F7F7", "#D9F0D3", "#A6DBA0", "#5AAE61", "#1B7837", "#00441B" + ] + + /// Brown-Teal diverging palette - earthy and modern + public static let brownTeal: [String] = [ + "#8C510A", "#BF812D", "#DFC27D", "#F6E8C3", "#F5F5F5", + "#C7EAE5", "#80CDC1", "#35978F", "#01665E", "#003C30" + ] + + // MARK: - Qualitative Palettes (for categorical data) + + /// Set1 qualitative palette - high contrast for distinct categories + public static let set1: [String] = [ + "#E41A1C", "#377EB8", "#4DAF4A", "#984EA3", + "#FF7F00", "#FFFF33", "#A65628", "#F781BF", "#999999" + ] + + /// Set2 qualitative palette - softer colors for better readability + public static let set2: [String] = [ + "#66C2A5", "#FC8D62", "#8DA0CB", "#E78AC3", + "#A6D854", "#FFD92F", "#E5C494", "#B3B3B3" + ] + + /// Set3 qualitative palette - pastel colors for subtle distinctions + public static let set3: [String] = [ + "#8DD3C7", "#FFFFB3", "#BEBADA", "#FB8072", + "#80B1D3", "#FDB462", "#B3DE69", "#FCCDE5", + "#D9D9D9", "#BC80BD", "#CCEBC5", "#FFED6F" + ] + + /// Dark2 qualitative palette - darker colors for professional look + public static let dark2: [String] = [ + "#1B9E77", "#D95F02", "#7570B3", "#E7298A", + "#66A61E", "#E6AB02", "#A6761D", "#666666" + ] + + /// Tableau10 qualitative palette - optimized for data visualization + public static let tableau10: [String] = [ + "#4E79A7", "#F28E2C", "#E15759", "#76B7B2", + "#59A14F", "#EDC949", "#AF7AA1", "#FF9DA7", + "#9C755F", "#BAB0AB" + ] + + // MARK: - Accessibility-Friendly Palettes + + /// Colorblind-safe qualitative palette + public static let colorblindSafe: [String] = [ + "#1F77B4", "#FF7F0E", "#2CA02C", "#D62728", + "#9467BD", "#8C564B", "#E377C2", "#7F7F7F", + "#BCBD22", "#17BECF" + ] + + /// High contrast palette for accessibility + public static let highContrast: [String] = [ + "#000000", "#FFFFFF", "#FF0000", "#00FF00", + "#0000FF", "#FFFF00", "#FF00FF", "#00FFFF" + ] + + // MARK: - Specialized Palettes + + /// Traffic light palette for status indicators + public static let trafficLight: [String] = [ + "#FF4444", "#FFAA00", "#00AA00" + ] + + /// Heatmap palette for intensity visualization + public static let heatmap: [String] = [ + "#000428", "#004CFF", "#009FFF", "#00FFFF", + "#5AFF00", "#FFFF00", "#FF9500", "#FF0000" + ] + + /// Viridis palette - perceptually uniform and colorblind-friendly + public static let viridis: [String] = [ + "#440154", "#482777", "#3F4A8A", "#31678E", + "#26838F", "#1F9D8A", "#6CCE5A", "#B6DE2B", "#FEE825" + ] + + /// Plasma palette - vibrant and perceptually uniform + public static let plasma: [String] = [ + "#0C0786", "#40039A", "#6A00A7", "#8F0DA4", + "#B12A90", "#CC4678", "#E16462", "#F1834C", "#FCA636", "#FCCE25" + ] + + // MARK: - Palette Utilities + + /// Get a subset of colors from a palette + /// - Parameters: + /// - palette: Source color palette + /// - count: Number of colors needed + /// - Returns: Evenly distributed colors from the palette + public static func subset(from palette: [String], count: Int) -> [String] { + guard count > 0 && !palette.isEmpty else { return [] } + guard count < palette.count else { return palette } + + var result: [String] = [] + let step = Double(palette.count - 1) / Double(count - 1) + + for i in 0.. [String] { + guard let baseRGBA = try? HexColorFormatter.parse(baseColor) else { return [baseColor] } + + let lightRGBA = RGBA(r: 0.98, g: 0.98, b: 0.98, a: 1.0) + var colors: [String] = [] + + for i in 0.. [String] { + guard let startRGBA = try? HexColorFormatter.parse(startColor), + let endRGBA = try? HexColorFormatter.parse(endColor) else { + return [startColor, endColor] + } + + let centerRGBA = RGBA(r: 0.97, g: 0.97, b: 0.97, a: 1.0) // Light gray center + let halfSteps = steps / 2 + var colors: [String] = [] + + // First half: start to center + for i in 0.. [String] { + guard let baseRGBA = try? HexColorFormatter.parse(baseHex) else { return [baseHex] } + + // Convert to HSL for easier hue manipulation + let hsl = rgbaToHSL(baseRGBA) + + switch type { + case .complementary: + return generateComplementary(hsl: hsl) + case .analogous: + return generateAnalogous(hsl: hsl) + case .triadic: + return generateTriadic(hsl: hsl) + case .tetradic: + return generateTetradic(hsl: hsl) + case .splitComplementary: + return generateSplitComplementary(hsl: hsl) + case .monochromatic: + return generateMonochromatic(hsl: hsl) + } + } + + // MARK: - Private Harmony Generators + + private static func generateComplementary(hsl: HSL) -> [String] { + let complementHue = (hsl.h + 180).truncatingRemainder(dividingBy: 360) + let complement = HSL(h: complementHue, s: hsl.s, l: hsl.l) + + return [ + HexColorFormatter.format(hslToRGBA(hsl)), + HexColorFormatter.format(hslToRGBA(complement)) + ] + } + + private static func generateAnalogous(hsl: HSL) -> [String] { + let hue1 = (hsl.h - 30 + 360).truncatingRemainder(dividingBy: 360) + let hue2 = (hsl.h + 30).truncatingRemainder(dividingBy: 360) + + return [ + HexColorFormatter.format(hslToRGBA(HSL(h: hue1, s: hsl.s, l: hsl.l))), + HexColorFormatter.format(hslToRGBA(hsl)), + HexColorFormatter.format(hslToRGBA(HSL(h: hue2, s: hsl.s, l: hsl.l))) + ] + } + + private static func generateTriadic(hsl: HSL) -> [String] { + let hue1 = (hsl.h + 120).truncatingRemainder(dividingBy: 360) + let hue2 = (hsl.h + 240).truncatingRemainder(dividingBy: 360) + + return [ + HexColorFormatter.format(hslToRGBA(hsl)), + HexColorFormatter.format(hslToRGBA(HSL(h: hue1, s: hsl.s, l: hsl.l))), + HexColorFormatter.format(hslToRGBA(HSL(h: hue2, s: hsl.s, l: hsl.l))) + ] + } + + private static func generateTetradic(hsl: HSL) -> [String] { + let hue1 = (hsl.h + 90).truncatingRemainder(dividingBy: 360) + let hue2 = (hsl.h + 180).truncatingRemainder(dividingBy: 360) + let hue3 = (hsl.h + 270).truncatingRemainder(dividingBy: 360) + + return [ + HexColorFormatter.format(hslToRGBA(hsl)), + HexColorFormatter.format(hslToRGBA(HSL(h: hue1, s: hsl.s, l: hsl.l))), + HexColorFormatter.format(hslToRGBA(HSL(h: hue2, s: hsl.s, l: hsl.l))), + HexColorFormatter.format(hslToRGBA(HSL(h: hue3, s: hsl.s, l: hsl.l))) + ] + } + + private static func generateSplitComplementary(hsl: HSL) -> [String] { + let complementBase = (hsl.h + 180).truncatingRemainder(dividingBy: 360) + let hue1 = (complementBase - 30 + 360).truncatingRemainder(dividingBy: 360) + let hue2 = (complementBase + 30).truncatingRemainder(dividingBy: 360) + + return [ + HexColorFormatter.format(hslToRGBA(hsl)), + HexColorFormatter.format(hslToRGBA(HSL(h: hue1, s: hsl.s, l: hsl.l))), + HexColorFormatter.format(hslToRGBA(HSL(h: hue2, s: hsl.s, l: hsl.l))) + ] + } + + private static func generateMonochromatic(hsl: HSL) -> [String] { + let lightnesses: [Double] = [0.2, 0.4, hsl.l, 0.7, 0.9] + return lightnesses.map { lightness in + HexColorFormatter.format(hslToRGBA(HSL(h: hsl.h, s: hsl.s, l: lightness))) + } + } + + // MARK: - HSL Conversion Helpers + + private struct HSL { + let h: Double // 0-360 + let s: Double // 0-1 + let l: Double // 0-1 + } + + private static func rgbaToHSL(_ rgba: RGBA) -> HSL { + let r = rgba.r + let g = rgba.g + let b = rgba.b + + let max = Swift.max(r, g, b) + let min = Swift.min(r, g, b) + let delta = max - min + + // Lightness + let l = (max + min) / 2.0 + + // Saturation + let s: Double + if delta == 0 { + s = 0 + } else { + s = l > 0.5 ? delta / (2.0 - max - min) : delta / (max + min) + } + + // Hue + let h: Double + if delta == 0 { + h = 0 + } else if max == r { + h = ((g - b) / delta + (g < b ? 6 : 0)) * 60 + } else if max == g { + h = ((b - r) / delta + 2) * 60 + } else { + h = ((r - g) / delta + 4) * 60 + } + + return HSL(h: h, s: s, l: l) + } + + private static func hslToRGBA(_ hsl: HSL) -> RGBA { + let h = hsl.h / 360.0 + let s = hsl.s + let l = hsl.l + + if s == 0 { + // Achromatic (gray) + return RGBA(r: l, g: l, b: l, a: 1.0) + } + + func hueToRGB(_ p: Double, _ q: Double, _ t: Double) -> Double { + var t = t + if t < 0 { t += 1 } + if t > 1 { t -= 1 } + if t < 1/6 { return p + (q - p) * 6 * t } + if t < 1/2 { return q } + if t < 2/3 { return p + (q - p) * (2/3 - t) * 6 } + return p + } + + let q = l < 0.5 ? l * (1 + s) : l + s - l * s + let p = 2 * l - q + + let r = hueToRGB(p, q, h + 1/3) + let g = hueToRGB(p, q, h) + let b = hueToRGB(p, q, h - 1/3) + + return RGBA(r: r, g: g, b: b, a: 1.0) + } +} + +// MARK: - Color Temperature and Tint Adjustment + +/// Tools for adjusting color temperature and tint, similar to photo editing software +public struct ColorTemperatureAdjustment { + + /// Adjust the color temperature of a color + /// - Parameters: + /// - color: The input color in hex format + /// - temperature: Temperature adjustment (-100 to 100, where negative is cooler/bluer, positive is warmer/yellower) + /// - Returns: Color with adjusted temperature + public static func adjustTemperature(_ color: String, temperature: Double) -> String { + guard let rgba = try? HexColorFormatter.parse(color) else { return color } + let adjustedRGBA = adjustTemperature(rgba, temperature: temperature) + return HexColorFormatter.format(adjustedRGBA) + } + + /// Adjust the color temperature of an RGBA color + /// - Parameters: + /// - rgba: The input RGBA color + /// - temperature: Temperature adjustment (-100 to 100) + /// - Returns: RGBA color with adjusted temperature + public static func adjustTemperature(_ rgba: RGBA, temperature: Double) -> RGBA { + let clampedTemp = max(-100, min(100, temperature)) + let tempFactor = clampedTemp / 100.0 + + // Convert to linear RGB for more accurate color manipulation + let linearR = sRGBToLinear(rgba.r) + let linearG = sRGBToLinear(rgba.g) + let linearB = sRGBToLinear(rgba.b) + + var adjustedR = linearR + var adjustedG = linearG + var adjustedB = linearB + + if tempFactor > 0 { + // Warmer (more yellow/red) + adjustedR = linearR + tempFactor * 0.3 + adjustedG = linearG + tempFactor * 0.1 + adjustedB = linearB - tempFactor * 0.2 + } else { + // Cooler (more blue) + adjustedR = linearR + tempFactor * 0.2 + adjustedG = linearG + tempFactor * 0.1 + adjustedB = linearB - tempFactor * 0.3 + } + + return RGBA( + r: RGBA.clamp(linearToSRGB(adjustedR)), + g: RGBA.clamp(linearToSRGB(adjustedG)), + b: RGBA.clamp(linearToSRGB(adjustedB)), + a: rgba.a + ) + } + + /// Adjust the tint of a color + /// - Parameters: + /// - color: The input color in hex format + /// - tint: Tint adjustment (-100 to 100, where negative is more green, positive is more magenta) + /// - Returns: Color with adjusted tint + public static func adjustTint(_ color: String, tint: Double) -> String { + guard let rgba = try? HexColorFormatter.parse(color) else { return color } + let adjustedRGBA = adjustTint(rgba, tint: tint) + return HexColorFormatter.format(adjustedRGBA) + } + + /// Adjust the tint of an RGBA color + /// - Parameters: + /// - rgba: The input RGBA color + /// - tint: Tint adjustment (-100 to 100) + /// - Returns: RGBA color with adjusted tint + public static func adjustTint(_ rgba: RGBA, tint: Double) -> RGBA { + let clampedTint = max(-100, min(100, tint)) + let tintFactor = clampedTint / 100.0 + + // Convert to linear RGB + let linearR = sRGBToLinear(rgba.r) + let linearG = sRGBToLinear(rgba.g) + let linearB = sRGBToLinear(rgba.b) + + var adjustedR = linearR + var adjustedG = linearG + var adjustedB = linearB + + if tintFactor > 0 { + // More magenta + adjustedR = linearR + tintFactor * 0.2 + adjustedG = linearG - tintFactor * 0.1 + adjustedB = linearB + tintFactor * 0.1 + } else { + // More green + adjustedR = linearR + tintFactor * 0.1 + adjustedG = linearG - tintFactor * 0.2 + adjustedB = linearB + tintFactor * 0.05 + } + + return RGBA( + r: RGBA.clamp(linearToSRGB(adjustedR)), + g: RGBA.clamp(linearToSRGB(adjustedG)), + b: RGBA.clamp(linearToSRGB(adjustedB)), + a: rgba.a + ) + } + + /// Adjust both temperature and tint simultaneously + /// - Parameters: + /// - color: The input color in hex format + /// - temperature: Temperature adjustment (-100 to 100) + /// - tint: Tint adjustment (-100 to 100) + /// - Returns: Color with adjusted temperature and tint + public static func adjustTemperatureAndTint(_ color: String, temperature: Double, tint: Double) -> String { + guard let rgba = try? HexColorFormatter.parse(color) else { return color } + let adjustedRGBA = adjustTemperatureAndTint(rgba, temperature: temperature, tint: tint) + return HexColorFormatter.format(adjustedRGBA) + } + + /// Adjust both temperature and tint of an RGBA color simultaneously + /// - Parameters: + /// - rgba: The input RGBA color + /// - temperature: Temperature adjustment (-100 to 100) + /// - tint: Tint adjustment (-100 to 100) + /// - Returns: RGBA color with adjusted temperature and tint + public static func adjustTemperatureAndTint(_ rgba: RGBA, temperature: Double, tint: Double) -> RGBA { + let tempAdjusted = adjustTemperature(rgba, temperature: temperature) + return adjustTint(tempAdjusted, tint: tint) + } + + /// Get the color temperature of a color in Kelvin (approximate) + /// - Parameter color: The input color in hex format + /// - Returns: Approximate color temperature in Kelvin (2000-12000K range) + public static func getColorTemperature(_ color: String) -> Double { + guard let rgba = try? HexColorFormatter.parse(color) else { return 6500 } // Default daylight + return getColorTemperature(rgba) + } + + /// Get the color temperature of an RGBA color in Kelvin (approximate) + /// - Parameter rgba: The input RGBA color + /// - Returns: Approximate color temperature in Kelvin + public static func getColorTemperature(_ rgba: RGBA) -> Double { + // Simple approximation based on red/blue ratio + let redBlueRatio = rgba.r / max(rgba.b, 0.001) + + // Map ratio to temperature (very approximate) + if redBlueRatio > 1.5 { + return 2000 + (redBlueRatio - 1.5) * 1000 // Warm colors + } else if redBlueRatio < 0.8 { + return 6500 + (0.8 - redBlueRatio) * 7000 // Cool colors + } else { + return 5500 + (redBlueRatio - 1.0) * 2000 // Neutral colors + } + } + + /// Convert color temperature in Kelvin to RGB color + /// - Parameter kelvin: Temperature in Kelvin (1000-40000K) + /// - Returns: RGB color representing the blackbody radiation at that temperature + public static func kelvinToRGB(_ kelvin: Double) -> RGBA { + let temp = max(1000, min(40000, kelvin)) / 100.0 + + var red: Double + var green: Double + var blue: Double + + // Calculate red + if temp <= 66 { + red = 255 + } else { + red = temp - 60 + red = 329.698727446 * pow(red, -0.1332047592) + red = max(0, min(255, red)) + } + + // Calculate green + if temp <= 66 { + green = temp + green = 99.4708025861 * log(green) - 161.1195681661 + } else { + green = temp - 60 + green = 288.1221695283 * pow(green, -0.0755148492) + } + green = max(0, min(255, green)) + + // Calculate blue + if temp >= 66 { + blue = 255 + } else if temp <= 19 { + blue = 0 + } else { + blue = temp - 10 + blue = 138.5177312231 * log(blue) - 305.0447927307 + blue = max(0, min(255, blue)) + } + + return RGBA( + r: red / 255.0, + g: green / 255.0, + b: blue / 255.0, + a: 1.0 + ) + } + + // MARK: - Helper Functions + + /// Convert sRGB to linear RGB + private static func sRGBToLinear(_ value: Double) -> Double { + return value <= 0.04045 ? value / 12.92 : pow((value + 0.055) / 1.055, 2.4) + } + + /// Convert linear RGB to sRGB + private static func linearToSRGB(_ value: Double) -> Double { + return value <= 0.0031308 ? value * 12.92 : 1.055 * pow(value, 1.0/2.4) - 0.055 + } +} + +// MARK: - White Balance Presets + +/// Common white balance presets for color temperature adjustment +public struct WhiteBalancePresets { + + /// Candlelight (1900K) + public static let candlelight = ColorTemperatureAdjustment.kelvinToRGB(1900) + + /// Tungsten (2700K) + public static let tungsten = ColorTemperatureAdjustment.kelvinToRGB(2700) + + /// Warm fluorescent (3000K) + public static let warmFluorescent = ColorTemperatureAdjustment.kelvinToRGB(3000) + + /// Cool white fluorescent (4100K) + public static let coolWhiteFluorescent = ColorTemperatureAdjustment.kelvinToRGB(4100) + + /// Daylight (5500K) + public static let daylight = ColorTemperatureAdjustment.kelvinToRGB(5500) + + /// Flash (5500K) + public static let flash = ColorTemperatureAdjustment.kelvinToRGB(5500) + + /// Cloudy (6500K) + public static let cloudy = ColorTemperatureAdjustment.kelvinToRGB(6500) + + /// Shade (7500K) + public static let shade = ColorTemperatureAdjustment.kelvinToRGB(7500) + + /// Get all presets as a dictionary + public static let allPresets: [String: RGBA] = [ + "Candlelight": candlelight, + "Tungsten": tungsten, + "Warm Fluorescent": warmFluorescent, + "Cool White Fluorescent": coolWhiteFluorescent, + "Daylight": daylight, + "Flash": flash, + "Cloudy": cloudy, + "Shade": shade + ] + + /// Apply a white balance preset to a color + /// - Parameters: + /// - color: Input color in hex format + /// - preset: White balance preset name + /// - strength: Strength of the effect (0.0 to 1.0) + /// - Returns: Color adjusted with the white balance preset + public static func applyPreset(_ color: String, preset: String, strength: Double = 1.0) -> String { + guard let rgba = try? HexColorFormatter.parse(color), + let presetColor = allPresets[preset] else { return color } + + let clampedStrength = max(0, min(1, strength)) + let blended = PerceptualColorMath.perceptualBlend(rgba, presetColor, ratio: clampedStrength * 0.3) + return HexColorFormatter.format(blended) + } +} + +// MARK: - Color Psychology Engine + +/// Emotional categories for color psychology +public enum EmotionalCategory: String, CaseIterable { + case calm = "calm" + case energetic = "energetic" + case warm = "warm" + case cool = "cool" + case professional = "professional" + case creative = "creative" + case trustworthy = "trustworthy" + case luxurious = "luxurious" + case playful = "playful" + case natural = "natural" + case romantic = "romantic" + case mysterious = "mysterious" + case confident = "confident" + case peaceful = "peaceful" + case exciting = "exciting" + case sophisticated = "sophisticated" + case friendly = "friendly" + case powerful = "powerful" + case fresh = "fresh" + case elegant = "elegant" +} + +/// Color psychology associations and emotional responses +public struct ColorPsychology { + + /// Get colors associated with a specific emotion + /// - Parameter emotion: The desired emotional category + /// - Returns: Array of hex colors that evoke the specified emotion + public static func colorsFor(emotion: EmotionalCategory) -> [String] { + switch emotion { + case .calm: + return ["#E8F4FD", "#B3D9F2", "#7FB8D3", "#4F94CD", "#2E8B57", "#87CEEB", "#F0F8FF", "#E6E6FA"] + case .energetic: + return ["#FF6B35", "#F7931E", "#FFD23F", "#EE4B2B", "#FF4500", "#FF1493", "#32CD32", "#ADFF2F"] + case .warm: + return ["#FF6347", "#FF7F50", "#FFA500", "#FFD700", "#F4A460", "#DEB887", "#CD853F", "#D2691E"] + case .cool: + return ["#4169E1", "#00CED1", "#20B2AA", "#48D1CC", "#87CEEB", "#B0E0E6", "#E0FFFF", "#F0F8FF"] + case .professional: + return ["#2C3E50", "#34495E", "#7F8C8D", "#95A5A6", "#BDC3C7", "#1ABC9C", "#3498DB", "#9B59B6"] + case .creative: + return ["#E74C3C", "#F39C12", "#F1C40F", "#2ECC71", "#3498DB", "#9B59B6", "#E67E22", "#1ABC9C"] + case .trustworthy: + return ["#3498DB", "#2980B9", "#1ABC9C", "#16A085", "#27AE60", "#2ECC71", "#34495E", "#2C3E50"] + case .luxurious: + return ["#8E44AD", "#9B59B6", "#2C3E50", "#34495E", "#F39C12", "#E67E22", "#C0392B", "#A93226"] + case .playful: + return ["#FF69B4", "#FF1493", "#00FF7F", "#FFD700", "#FF6347", "#32CD32", "#FF4500", "#DA70D6"] + case .natural: + return ["#228B22", "#32CD32", "#9ACD32", "#6B8E23", "#556B2F", "#8FBC8F", "#98FB98", "#F0FFF0"] + case .romantic: + return ["#FFB6C1", "#FFC0CB", "#FF69B4", "#FF1493", "#DC143C", "#B22222", "#CD5C5C", "#F08080"] + case .mysterious: + return ["#2F1B69", "#4B0082", "#483D8B", "#2E2E2E", "#36454F", "#1C1C1C", "#191970", "#000080"] + case .confident: + return ["#DC143C", "#B22222", "#8B0000", "#FF4500", "#FF6347", "#2F4F4F", "#000000", "#800000"] + case .peaceful: + return ["#E6E6FA", "#F0F8FF", "#F5F5DC", "#FFF8DC", "#FFFACD", "#F0FFF0", "#F5FFFA", "#FFFFF0"] + case .exciting: + return ["#FF0000", "#FF4500", "#FF6347", "#FF1493", "#FF69B4", "#ADFF2F", "#00FF00", "#FFD700"] + case .sophisticated: + return ["#2C2C2C", "#36454F", "#708090", "#2F4F4F", "#696969", "#A9A9A9", "#C0C0C0", "#D3D3D3"] + case .friendly: + return ["#FFA500", "#FFD700", "#FFFF00", "#ADFF2F", "#32CD32", "#00CED1", "#87CEEB", "#DDA0DD"] + case .powerful: + return ["#000000", "#8B0000", "#B22222", "#2F4F4F", "#36454F", "#191970", "#4B0082", "#800080"] + case .fresh: + return ["#00FF7F", "#32CD32", "#98FB98", "#90EE90", "#ADFF2F", "#7CFC00", "#00FA9A", "#00FF00"] + case .elegant: + return ["#2C2C2C", "#36454F", "#C0C0C0", "#D3D3D3", "#E6E6FA", "#F5F5DC", "#FFF8DC", "#FFFFF0"] + } + } + + /// Get the primary emotion associated with a color + /// - Parameter color: Hex color string + /// - Returns: The primary emotional category for this color + public static func primaryEmotion(for color: String) -> EmotionalCategory? { + guard let rgba = try? HexColorFormatter.parse(color) else { return nil } + return primaryEmotion(for: rgba) + } + + /// Get the primary emotion associated with an RGBA color + /// - Parameter rgba: RGBA color + /// - Returns: The primary emotional category for this color + public static func primaryEmotion(for rgba: RGBA) -> EmotionalCategory? { + let hsl = rgbaToHSL(rgba) + let hue = hsl.h + let saturation = hsl.s + let lightness = hsl.l + + // Analyze color properties to determine emotion + if lightness > 0.8 && saturation < 0.3 { + return .peaceful + } else if saturation > 0.8 && lightness > 0.5 { + if hue >= 0 && hue < 60 { + return .energetic // Red-Orange + } else if hue >= 60 && hue < 120 { + return .fresh // Yellow-Green + } else if hue >= 120 && hue < 180 { + return .natural // Green-Cyan + } else if hue >= 180 && hue < 240 { + return .cool // Cyan-Blue + } else if hue >= 240 && hue < 300 { + return .mysterious // Blue-Purple + } else { + return .romantic // Purple-Red + } + } else if lightness < 0.3 { + return saturation > 0.5 ? .powerful : .sophisticated + } else if saturation < 0.2 { + return .professional + } else { + // Medium saturation and lightness + if hue >= 0 && hue < 60 { + return .warm + } else if hue >= 180 && hue < 240 { + return .trustworthy + } else { + return .friendly + } + } + } + + /// Get all emotions associated with a color (with confidence scores) + /// - Parameter color: Hex color string + /// - Returns: Dictionary of emotions with confidence scores (0.0 to 1.0) + public static func emotionalProfile(for color: String) -> [EmotionalCategory: Double] { + guard let rgba = try? HexColorFormatter.parse(color) else { return [:] } + return emotionalProfile(for: rgba) + } + + /// Get all emotions associated with an RGBA color (with confidence scores) + /// - Parameter rgba: RGBA color + /// - Returns: Dictionary of emotions with confidence scores (0.0 to 1.0) + public static func emotionalProfile(for rgba: RGBA) -> [EmotionalCategory: Double] { + let hsl = rgbaToHSL(rgba) + var profile: [EmotionalCategory: Double] = [:] + + let hue = hsl.h + let saturation = hsl.s + let lightness = hsl.l + + // Calculate confidence scores for each emotion based on color properties + + // Calm: High lightness, low saturation, cool hues + let calmScore = (lightness * 0.4) + ((1.0 - saturation) * 0.3) + (coolHueScore(hue) * 0.3) + profile[.calm] = min(1.0, calmScore) + + // Energetic: High saturation, warm hues + let energeticScore = (saturation * 0.5) + (warmHueScore(hue) * 0.5) + profile[.energetic] = min(1.0, energeticScore) + + // Professional: Medium lightness, low-medium saturation, neutral colors + let professionalScore = (1.0 - abs(lightness - 0.5)) * 0.4 + ((1.0 - saturation) * 0.6) + profile[.professional] = min(1.0, professionalScore) + + // Luxurious: Deep colors, high saturation, purple/gold hues + let luxuriousScore = ((1.0 - lightness) * 0.4) + (saturation * 0.3) + (luxuryHueScore(hue) * 0.3) + profile[.luxurious] = min(1.0, luxuriousScore) + + // Natural: Green hues, medium saturation + let naturalScore = greenHueScore(hue) * 0.7 + (saturation * 0.3) + profile[.natural] = min(1.0, naturalScore) + + // Add more emotion calculations... + profile[.trustworthy] = min(1.0, blueHueScore(hue) * 0.8 + (saturation * 0.2)) + profile[.romantic] = min(1.0, pinkRedHueScore(hue) * 0.6 + (lightness * 0.4)) + profile[.mysterious] = min(1.0, ((1.0 - lightness) * 0.6) + (purpleHueScore(hue) * 0.4)) + profile[.peaceful] = min(1.0, (lightness * 0.5) + ((1.0 - saturation) * 0.5)) + profile[.powerful] = min(1.0, ((1.0 - lightness) * 0.7) + (saturation * 0.3)) + + return profile.filter { $0.value > 0.1 } // Only return emotions with meaningful scores + } + + /// Generate a color palette based on desired emotions + /// - Parameters: + /// - emotions: Array of desired emotions + /// - count: Number of colors to generate + /// - Returns: Array of hex colors that evoke the specified emotions + public static func generatePalette(for emotions: [EmotionalCategory], count: Int = 5) -> [String] { + var allColors: [String] = [] + + // Collect colors from all specified emotions + for emotion in emotions { + allColors.append(contentsOf: colorsFor(emotion: emotion)) + } + + // Remove duplicates and select diverse colors + let uniqueColors = Array(Set(allColors)) + + guard uniqueColors.count >= count else { + return Array(uniqueColors.prefix(count)) + } + + // Select colors that are visually diverse + var selectedColors: [String] = [] + var remainingColors = uniqueColors + + // Start with a random color + if let firstColor = remainingColors.randomElement() { + selectedColors.append(firstColor) + remainingColors.removeAll { $0 == firstColor } + } + + // Select remaining colors based on diversity + while selectedColors.count < count && !remainingColors.isEmpty { + var bestColor = remainingColors[0] + var maxMinDistance = 0.0 + + for candidate in remainingColors { + var minDistance = Double.infinity + + for selected in selectedColors { + let distance = colorDistance(candidate, selected) + minDistance = min(minDistance, distance) + } + + if minDistance > maxMinDistance { + maxMinDistance = minDistance + bestColor = candidate + } + } + + selectedColors.append(bestColor) + remainingColors.removeAll { $0 == bestColor } + } + + return selectedColors + } + + /// Suggest complementary emotions for a given emotion + /// - Parameter emotion: The base emotion + /// - Returns: Array of emotions that work well together + public static func complementaryEmotions(for emotion: EmotionalCategory) -> [EmotionalCategory] { + switch emotion { + case .calm: + return [.peaceful, .trustworthy, .professional] + case .energetic: + return [.exciting, .confident, .playful] + case .warm: + return [.friendly, .romantic, .natural] + case .cool: + return [.calm, .trustworthy, .professional] + case .professional: + return [.trustworthy, .sophisticated, .confident] + case .creative: + return [.playful, .energetic, .exciting] + case .trustworthy: + return [.professional, .calm, .confident] + case .luxurious: + return [.sophisticated, .elegant, .mysterious] + case .playful: + return [.creative, .friendly, .energetic] + case .natural: + return [.fresh, .calm, .peaceful] + case .romantic: + return [.warm, .elegant, .luxurious] + case .mysterious: + return [.sophisticated, .powerful, .luxurious] + case .confident: + return [.powerful, .professional, .trustworthy] + case .peaceful: + return [.calm, .natural, .fresh] + case .exciting: + return [.energetic, .playful, .creative] + case .sophisticated: + return [.elegant, .luxurious, .professional] + case .friendly: + return [.warm, .playful, .trustworthy] + case .powerful: + return [.confident, .mysterious, .sophisticated] + case .fresh: + return [.natural, .energetic, .peaceful] + case .elegant: + return [.sophisticated, .luxurious, .romantic] + } + } + + // MARK: - Helper Functions + + private struct HSL { + let h: Double // 0...360 + let s: Double // 0...1 + let l: Double // 0...1 + } + + private static func rgbaToHSL(_ rgba: RGBA) -> HSL { + let max = Swift.max(rgba.r, rgba.g, rgba.b) + let min = Swift.min(rgba.r, rgba.g, rgba.b) + let delta = max - min + + let l = (max + min) / 2.0 + + guard delta > 0 else { + return HSL(h: 0, s: 0, l: l) + } + + let s = l > 0.5 ? delta / (2.0 - max - min) : delta / (max + min) + + var h: Double + switch max { + case rgba.r: + h = ((rgba.g - rgba.b) / delta) + (rgba.g < rgba.b ? 6 : 0) + case rgba.g: + h = (rgba.b - rgba.r) / delta + 2 + default: // rgba.b + h = (rgba.r - rgba.g) / delta + 4 + } + h *= 60 + + return HSL(h: h, s: s, l: l) + } + + private static func warmHueScore(_ hue: Double) -> Double { + // Red to yellow range (0-60 and 300-360) + if hue <= 60 || hue >= 300 { + return 1.0 + } else if hue <= 120 { + return 1.0 - (hue - 60) / 60.0 + } else if hue >= 240 { + return (hue - 240) / 60.0 + } + return 0.0 + } + + private static func coolHueScore(_ hue: Double) -> Double { + // Blue to cyan range (180-240) + if hue >= 180 && hue <= 240 { + return 1.0 + } else if hue >= 120 && hue < 180 { + return (hue - 120) / 60.0 + } else if hue > 240 && hue <= 300 { + return 1.0 - (hue - 240) / 60.0 + } + return 0.0 + } + + private static func greenHueScore(_ hue: Double) -> Double { + // Green range (60-180) + if hue >= 90 && hue <= 150 { + return 1.0 + } else if hue >= 60 && hue < 90 { + return (hue - 60) / 30.0 + } else if hue > 150 && hue <= 180 { + return 1.0 - (hue - 150) / 30.0 + } + return 0.0 + } + + private static func blueHueScore(_ hue: Double) -> Double { + // Blue range (200-260) + if hue >= 200 && hue <= 260 { + return 1.0 + } else if hue >= 180 && hue < 200 { + return (hue - 180) / 20.0 + } else if hue > 260 && hue <= 280 { + return 1.0 - (hue - 260) / 20.0 + } + return 0.0 + } + + private static func purpleHueScore(_ hue: Double) -> Double { + // Purple range (260-320) + if hue >= 260 && hue <= 320 { + return 1.0 + } else if hue >= 240 && hue < 260 { + return (hue - 240) / 20.0 + } else if hue > 320 && hue <= 340 { + return 1.0 - (hue - 320) / 20.0 + } + return 0.0 + } + + private static func pinkRedHueScore(_ hue: Double) -> Double { + // Pink-red range (320-360 and 0-20) + if (hue >= 320 && hue <= 360) || (hue >= 0 && hue <= 20) { + return 1.0 + } else if hue >= 300 && hue < 320 { + return (hue - 300) / 20.0 + } else if hue > 20 && hue <= 40 { + return 1.0 - (hue - 20) / 20.0 + } + return 0.0 + } + + private static func luxuryHueScore(_ hue: Double) -> Double { + // Purple and gold hues + let purpleScore = purpleHueScore(hue) + let goldScore = hue >= 40 && hue <= 60 ? 1.0 : 0.0 + return max(purpleScore, goldScore) + } + + private static func colorDistance(_ color1: String, _ color2: String) -> Double { + guard let rgba1 = try? HexColorFormatter.parse(color1), + let rgba2 = try? HexColorFormatter.parse(color2) else { return 0.0 } + + // Simple Euclidean distance in RGB space + let dr = rgba1.r - rgba2.r + let dg = rgba1.g - rgba2.g + let db = rgba1.b - rgba2.b + + return sqrt(dr * dr + dg * dg + db * db) + } +} + // MARK: - Palette Generator public struct PaletteGenerator { /// Generate tints and shades around a base color @@ -89,4 +945,4 @@ public struct ColorBlindnessSimulator { guard let rgba = try? HexColorFormatter.parse(hex) else { return nil } return HexColorFormatter.format(simulate(type, rgba: rgba)) } -} \ No newline at end of file +} From 5555f1ae7b0c065ceff43e0e37c6e7f6774271e2 Mon Sep 17 00:00:00 2001 From: Chandan Date: Sat, 25 Oct 2025 15:23:11 +0530 Subject: [PATCH 2/2] Update readme with advanced features and demo app details. --- README.md | 152 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 148 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 32d6f0f..73646c7 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,130 @@ let palette = PaletteGenerator.generate(from: "#1A73E8", steps: 6, range: 0.25) // Simulate color‑vision deficiency let protanopia = ColorBlindnessSimulator.simulate(.protanopia, rgba: fg) + +// 🚀 Advanced Features +// Data visualization palettes +let scientificColors = DataVisualizationPalettes.viridis +let heatmapColors = DataVisualizationPalettes.heatmap + +// Color harmony generation +let complementary = ColorHarmony.complementary(from: "#1A73E8") +let analogous = ColorHarmony.analogous(from: "#1A73E8", count: 5) + +// Color psychology +let calmColors = ColorPsychology.colorsFor(emotion: .calm) +let primaryEmotion = ColorPsychology.primaryEmotion(for: fg) + +// Perceptual color operations +let perceptualGradient = PerceptualColorMath.perceptualGradient(from: fg, to: bg, steps: 10) +let deltaE = PerceptualColorMath.deltaE2000(fg, bg) +``` + +## 🚀 Advanced Features + +ColorsKit goes beyond basic color utilities with powerful advanced features for professional color work: + +### 📊 Data Visualization Palettes +Professional color schemes optimized for charts, graphs, and scientific visualization: + +```swift +// Sequential palettes for continuous data +let blues = DataVisualizationPalettes.blues +let greens = DataVisualizationPalettes.greens + +// Diverging palettes for data with meaningful center +let redBlue = DataVisualizationPalettes.redBlue +let purpleGreen = DataVisualizationPalettes.purpleGreen + +// Scientific colormaps (perceptually uniform) +let viridis = DataVisualizationPalettes.viridis +let plasma = DataVisualizationPalettes.plasma + +// Accessibility-friendly palettes +let colorblindSafe = DataVisualizationPalettes.colorblindSafe +let highContrast = DataVisualizationPalettes.highContrast +``` + +### 🔬 Perceptual Color Mathematics +Advanced color space conversions and perceptually uniform operations: + +```swift +// Convert between color spaces +let xyz = ColorSpaceConverter.rgbaToXYZ(rgba) +let lab = ColorSpaceConverter.xyzToLAB(xyz) +let luv = ColorSpaceConverter.xyzToLUV(xyz) + +// Perceptually uniform gradients +let gradient = PerceptualColorMath.perceptualGradient(from: color1, to: color2, steps: 10) + +// Perceptual color blending +let blended = PerceptualColorMath.perceptualBlend(color1, color2, ratio: 0.5) + +// Delta E color difference (CIE Delta E 2000) +let difference = PerceptualColorMath.deltaE2000(color1, color2) +``` + +### 🎨 Color Harmony Generation +Generate harmonious color schemes based on color theory: + +```swift +// Generate complementary colors +let complementary = ColorHarmony.complementary(from: "#FF6B6B") + +// Create analogous color schemes +let analogous = ColorHarmony.analogous(from: "#4ECDC4", count: 5) + +// Generate triadic and tetradic schemes +let triadic = ColorHarmony.triadic(from: "#45B7D1") +let tetradic = ColorHarmony.tetradic(from: "#96CEB4") + +// Split complementary for balanced contrast +let splitComplementary = ColorHarmony.splitComplementary(from: "#FFEAA7") +``` + +### 🎭 Advanced Color Blending +Professional blend modes for sophisticated color mixing: + +```swift +// Multiple blend modes available +let multiplied = BlendMode.multiply.blend(base: baseColor, overlay: overlayColor) +let screened = BlendMode.screen.blend(base: baseColor, overlay: overlayColor) +let overlayed = BlendMode.overlay.blend(base: baseColor, overlay: overlayColor) + +// Soft light, hard light, color dodge, color burn, and more +let softLight = BlendMode.softLight.blend(base: baseColor, overlay: overlayColor) +let colorDodge = BlendMode.colorDodge.blend(base: baseColor, overlay: overlayColor) +``` + +### 🧠 Color Psychology Engine +Emotion-based color generation and analysis: + +```swift +// Generate colors for specific emotions +let calmColors = ColorPsychology.colorsFor(emotion: .calm) +let energeticColors = ColorPsychology.colorsFor(emotion: .energetic) +let professionalColors = ColorPsychology.colorsFor(emotion: .professional) + +// Analyze emotional properties of colors +let primaryEmotion = ColorPsychology.primaryEmotion(for: rgba) +let emotionalProfile = ColorPsychology.emotionalProfile(for: rgba) + +// Generate multi-emotion palettes +let palette = ColorPsychology.generatePalette(for: [.trustworthy, .creative], count: 8) +``` + +### 🌡️ Temperature & Advanced Gradients +Multiple interpolation methods and temperature-based color schemes: + +```swift +// Different interpolation methods +let linearGradient = GradientGenerator.generate(from: color1, to: color2, steps: 10, method: .linear) +let perceptualGradient = GradientGenerator.generate(from: color1, to: color2, steps: 10, method: .perceptual) +let hslGradient = GradientGenerator.generate(from: color1, to: color2, steps: 10, method: .hsl) + +// Temperature-based gradients +let thermal = TemperatureGradient.thermal(steps: 20) +let coolToWarm = TemperatureGradient.coolToWarm(steps: 15) ``` ### SwiftUI @@ -148,11 +272,31 @@ A tiny command‑line demo lives in `Example/ConsumerSample/`. - Run: `cd Example/ConsumerSample && swift run` - Shows: hex parsing, contrast ratio, palette generation, and color‑blindness simulation. +## 📱 Comprehensive iOS Demo App +Explore all ColorsKit features with our full-featured SwiftUI demo app in `Example/iOSAppDemo/`: + +### Features Showcased: +- **🎨 Color Harmony**: Generate complementary, analogous, triadic, and tetradic color schemes +- **🔬 Perceptual Colors**: Color space conversions, perceptual gradients, and Delta E analysis +- **🎭 Blending Modes**: 12 different blend modes with real-time preview +- **🧠 Color Psychology**: Emotion-based color generation and analysis +- **📊 Data Visualization**: Scientific colormaps, heatmaps, and specialized gradients +- **🌡️ Temperature Gradients**: Multiple interpolation methods and thermal imaging colors + +### Running the Demo: +```bash +cd Example/iOSAppDemo +open ColorsKitDemo.xcodeproj +# Build and run in Xcode or iOS Simulator +``` + +The demo app serves as both a showcase and reference implementation for integrating ColorsKit's advanced features into iOS applications. + ## Modules at a Glance -- `ColorCore`: `RGBA`, `HexColorFormatter`, `ColorMath`, `Accessibility`, `Theme`, `ThemeManager` -- `ColorUtilities`: `PaletteGenerator`, `AccessibilityUtils`, `ColorBlindnessSimulator` -- `ColorExtensions`: SwiftUI/UIColor helpers, gradients -- `ColorPalettes`: Predefined themes and palette helpers +- `ColorCore`: `RGBA`, `HexColorFormatter`, `ColorMath`, `Accessibility`, `Theme`, `ThemeManager`, `PerceptualColorMath`, `ColorSpaceConverter`, `ColorHarmony`, `BlendMode` +- `ColorUtilities`: `PaletteGenerator`, `AccessibilityUtils`, `ColorBlindnessSimulator`, `ColorPsychology`, `EmotionalCategory`, `GradientGenerator`, `TemperatureGradient`, `WhiteBalancePresets` +- `ColorExtensions`: SwiftUI/UIColor helpers, gradients, `GradientBuilder`, `SwiftUIGradientBuilder` +- `ColorPalettes`: `DataVisualizationPalettes`, predefined themes (`defaultLight`, `defaultDark`, `materialBlue`), scientific colormaps ## Platform Notes - `Color.dynamic` bridges to `Color(uiColor:)` on iOS/tvOS.