导入 XuXuCheck 工程(iOS 干化学尿液试纸分析 App)

从桌面工程复制而来,作为本仓库的 Xuxucheck/ 子目录。

为控制仓库体积做了两处删减:
- 移除 Frameworks/opencv2.xcframework 下的两个 visionOS 切片
  (xros-arm64、xros-arm64-simulator,合计约 106 MB),并同步删掉该
  xcframework 内 Info.plist 里对应的两条 AvailableLibraries 记录,
  只保留 ios-arm64 与 ios-arm64_x86_64-simulator。
- 移除全部 .DS_Store 与 xcuserdata/。

已用 xcodebuild 从本目录完整编译验证:BUILD SUCCEEDED,
ProcessXCFramework 以 --platform ios 成功选中框架,产物含 3699 个 cv:: 符号
(OpenCV 确实链入),嵌入 framework 3 个,与移除无用 pod 之后的状态一致。

仓库约 206 MB,其中 OpenCV 203 MB、其余源码与依赖约 3 MB。
This commit is contained in:
2026-09-23 14:17:31 +08:00
commit d6aeede2bc
901 changed files with 327267 additions and 0 deletions
@@ -0,0 +1,39 @@
/*
* DynamicColor
*
* Copyright 2015-present Yannick Loriot.
* http://yannickloriot.com
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
*/
import Foundation
/**
Convenient extension for color array to work as a DynamicGradient.
*/
public extension Array where Element: DynamicColor {
/**
Gradient representation of the array.
*/
var gradient: DynamicGradient {
return DynamicGradient(colors: self)
}
}
@@ -0,0 +1,60 @@
//
// ContrastDisplayContext.swift
// DynamicColorExample
//
// Created by Yannick LORIOT on 26/11/2016.
// Copyright © 2016 Yannick LORIOT. All rights reserved.
//
import Foundation
#if os(iOS) || os(tvOS) || os(watchOS)
import UIKit
#elseif os(OSX)
import AppKit
#endif
extension DynamicColor {
/**
Used to describe the context of display of 2 colors.
Based on WCAG: https://www.w3.org/TR/2008/REC-WCAG20-20081211/#visual-audio-contrast-contrast
*/
public enum ContrastDisplayContext {
/**
A standard text in a normal context.
*/
case standard
/**
A large text in a normal context.
You can look here for the definition of "large text":
https://www.w3.org/TR/2008/REC-WCAG20-20081211/#larger-scaledef
*/
case standardLargeText
/**
A standard text in an enhanced context.
Enhanced means that you want to be accessible (and AAA compliant in WCAG)
*/
case enhanced
/**
A large text in an enhanced context.
Enhanced means that you want to be accessible (and AAA compliant in WCAG)
You can look here for the definition of "large text":
https://www.w3.org/TR/2008/REC-WCAG20-20081211/#larger-scaledef
*/
case enhancedLargeText
var minimumContrastRatio: CGFloat {
switch self {
case .standard:
return 4.5
case .standardLargeText:
return 3.0
case .enhanced:
return 7.0
case .enhancedLargeText:
return 4.5
}
}
}
}
@@ -0,0 +1,137 @@
/*
* DynamicColor
*
* Copyright 2015-present Yannick Loriot.
* http://yannickloriot.com
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
*/
#if os(iOS) || os(tvOS) || os(watchOS)
import UIKit
#elseif os(OSX)
import AppKit
#endif
// MARK: Deriving Colors
public extension DynamicColor {
/**
Creates and returns a color object with the hue rotated along the color wheel by the given amount.
- parameter amount: A float representing the number of degrees as ratio (usually between -360.0 degree and 360.0 degree).
- returns: A DynamicColor object with the hue changed.
*/
final func adjustedHue(amount: CGFloat) -> DynamicColor {
return HSL(color: self).adjustedHue(amount: amount).toDynamicColor()
}
/**
Creates and returns the complement of the color object.
This is identical to adjustedHue(180).
- returns: The complement DynamicColor.
- seealso: adjustedHueColor:
*/
final func complemented() -> DynamicColor {
return adjustedHue(amount: 180.0)
}
/**
Creates and returns a color object with the lightness increased by the given amount.
- parameter amount: CGFloat between 0.0 and 1.0. Default value is 0.2.
- returns: A lighter DynamicColor.
*/
final func lighter(amount: CGFloat = 0.2) -> DynamicColor {
return HSL(color: self).lighter(amount: amount).toDynamicColor()
}
/**
Creates and returns a color object with the lightness decreased by the given amount.
- parameter amount: Float between 0.0 and 1.0. Default value is 0.2.
- returns: A darker DynamicColor.
*/
final func darkened(amount: CGFloat = 0.2) -> DynamicColor {
return HSL(color: self).darkened(amount: amount).toDynamicColor()
}
/**
Creates and returns a color object with the saturation increased by the given amount.
- parameter amount: CGFloat between 0.0 and 1.0. Default value is 0.2.
- returns: A DynamicColor more saturated.
*/
final func saturated(amount: CGFloat = 0.2) -> DynamicColor {
return HSL(color: self).saturated(amount: amount).toDynamicColor()
}
/**
Creates and returns a color object with the saturation decreased by the given amount.
- parameter amount: CGFloat between 0.0 and 1.0. Default value is 0.2.
- returns: A DynamicColor less saturated.
*/
final func desaturated(amount: CGFloat = 0.2) -> DynamicColor {
return HSL(color: self).desaturated(amount: amount).toDynamicColor()
}
/**
Creates and returns a color object converted to grayscale.
- returns: A grayscale DynamicColor.
- seealso: desaturated:
*/
final func grayscaled(mode: GrayscalingMode = .lightness) -> DynamicColor {
let (r, g, b, a) = self.toRGBAComponents()
let l: CGFloat
switch mode {
case .luminance:
l = (0.299 * r) + (0.587 * g) + (0.114 * b)
case .lightness:
l = 0.5 * (max(r, g, b) + min(r, g, b))
case .average:
l = (1.0 / 3.0) * (r + g + b)
case .value:
l = max(r, g, b)
}
return HSL(hue: 0.0, saturation: 0.0, lightness: l, alpha: a).toDynamicColor()
}
/**
Creates and return a color object where the red, green, and blue values are inverted, while the alpha channel is left alone.
- returns: An inverse (negative) of the original color.
*/
final func inverted() -> DynamicColor {
let rgba = toRGBAComponents()
let invertedRed = 1.0 - rgba.r
let invertedGreen = 1.0 - rgba.g
let invertedBlue = 1.0 - rgba.b
return DynamicColor(red: invertedRed, green: invertedGreen, blue: invertedBlue, alpha: rgba.a)
}
}
@@ -0,0 +1,88 @@
/*
* DynamicColor
*
* Copyright 2015-present Yannick Loriot.
* http://yannickloriot.com
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
*/
#if os(iOS) || os(tvOS) || os(watchOS)
import UIKit
#elseif os(OSX)
import AppKit
#endif
// MARK: HSB Color Space
extension DynamicColor {
// MARK: - Getting the HSB Components
/**
Returns the HSB (hue, saturation, brightness) components.
- returns: The HSB components as a tuple (h, s, b).
*/
public final func toHSBComponents() -> (h: CGFloat, s: CGFloat, b: CGFloat) {
var h: CGFloat = 0.0
var s: CGFloat = 0.0
var b: CGFloat = 0.0
#if os(iOS) || os(tvOS) || os(watchOS)
getHue(&h, saturation: &s, brightness: &b, alpha: nil)
return (h: h, s: s, b: b)
#elseif os(OSX)
if isEqual(DynamicColor.black) {
return (0.0, 0.0, 0.0)
}
else if isEqual(DynamicColor.white) {
return (0.0, 0.0, 1.0)
}
getHue(&h, saturation: &s, brightness: &b, alpha: nil)
return (h: h, s: s, b: b)
#endif
}
#if os(iOS) || os(tvOS) || os(watchOS)
/**
The hue component as CGFloat between 0.0 to 1.0.
*/
public final var hueComponent: CGFloat {
return toHSBComponents().h
}
/**
The saturation component as CGFloat between 0.0 to 1.0.
*/
public final var saturationComponent: CGFloat {
return toHSBComponents().s
}
/**
The brightness component as CGFloat between 0.0 to 1.0.
*/
public final var brightnessComponent: CGFloat {
return toHSBComponents().b
}
#endif
}
@@ -0,0 +1,65 @@
/*
* DynamicColor
*
* Copyright 2015-present Yannick Loriot.
* http://yannickloriot.com
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
*/
#if os(iOS) || os(tvOS) || os(watchOS)
import UIKit
#elseif os(OSX)
import AppKit
#endif
// MARK: HSL Color Space
extension DynamicColor {
/**
Initializes and returns a color object using the specified opacity and HSL component values.
- parameter hue: The hue component of the color object, specified as a value from 0.0 to 360.0 degree.
- parameter saturation: The saturation component of the color object, specified as a value from 0.0 to 1.0.
- parameter lightness: The lightness component of the color object, specified as a value from 0.0 to 1.0.
- parameter alpha: The opacity value of the color object, specified as a value from 0.0 to 1.0. Default to 1.0.
*/
public convenience init(hue: CGFloat, saturation: CGFloat, lightness: CGFloat, alpha: CGFloat = 1) {
let color = HSL(hue: hue, saturation: saturation, lightness: lightness, alpha: alpha).toDynamicColor()
let components = color.toRGBAComponents()
self.init(red: components.r, green: components.g, blue: components.b, alpha: components.a)
}
// MARK: - Getting the HSL Components
/**
Returns the HSL (hue, saturation, lightness) components.
Notes that the hue value is between 0.0 and 360.0 degree.
- returns: The HSL components as a tuple (h, s, l).
*/
public final func toHSLComponents() -> (h: CGFloat, s: CGFloat, l: CGFloat) {
let hsl = HSL(color: self)
return (hsl.h * 360.0, hsl.s, hsl.l)
}
}
@@ -0,0 +1,92 @@
/*
* DynamicColor
*
* Copyright 2015-present Yannick Loriot.
* http://yannickloriot.com
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
*/
#if os(iOS) || os(tvOS) || os(watchOS)
import UIKit
#elseif os(OSX)
import AppKit
#endif
// MARK: CIE L*a*b* Color Space
public extension DynamicColor {
/**
Initializes and returns a color object using CIE XYZ color space component values with an observer at 2° and a D65 illuminant.
Notes that values out of range are clipped.
- parameter L: The lightness, specified as a value from 0 to 100.0.
- parameter a: The red-green axis, specified as a value from -128.0 to 127.0.
- parameter b: The yellow-blue axis, specified as a value from -128.0 to 127.0.
- parameter alpha: The opacity value of the color object, specified as a value from 0.0 to 1.0. Default to 1.0.
*/
convenience init(L: CGFloat, a: CGFloat, b: CGFloat, alpha: CGFloat = 1) {
let clippedL = clip(L, 0.0, 100.0)
let clippedA = clip(a, -128.0, 127.0)
let clippedB = clip(b, -128.0, 127.0)
let normalized = { (c: CGFloat) -> CGFloat in
pow(c, 3) > 0.008856 ? pow(c, 3) : (c - (16 / 116)) / 7.787
}
let preY = (clippedL + 16.0) / 116.0
let preX = (clippedA / 500.0) + preY
let preZ = preY - (clippedB / 200.0)
let X = 95.05 * normalized(preX)
let Y = 100.0 * normalized(preY)
let Z = 108.9 * normalized(preZ)
self.init(X: X, Y: Y, Z: Z, alpha: alpha)
}
// MARK: - Getting the L*a*b* Components
/**
Returns the Lab (lightness, red-green axis, yellow-blue axis) components.
It is based on the CIE XYZ color space with an observer at 2° and a D65 illuminant.
Notes that L values are between 0 to 100.0, a values are between -128 to 127.0 and b values are between -128 to 127.0.
- returns: The L*a*b* components as a tuple (L, a, b).
*/
final func toLabComponents() -> (L: CGFloat, a: CGFloat, b: CGFloat) {
let normalized = { (c: CGFloat) -> CGFloat in
c > 0.008856 ? pow(c, 1.0 / 3.0) : (7.787 * c) + (16.0 / 116.0)
}
let xyz = toXYZComponents()
let normalizedX = normalized(xyz.X / 95.05)
let normalizedY = normalized(xyz.Y / 100.0)
let normalizedZ = normalized(xyz.Z / 108.9)
let L = roundDecimal((116.0 * normalizedY) - 16.0, precision: 1000)
let a = roundDecimal(500.0 * (normalizedX - normalizedY), precision: 1000)
let b = roundDecimal(200.0 * (normalizedY - normalizedZ), precision: 1000)
return (L: L, a: a, b: b)
}
}
@@ -0,0 +1,143 @@
/*
* DynamicColor
*
* Copyright 2015-present Yannick Loriot.
* http://yannickloriot.com
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
*/
#if os(iOS) || os(tvOS) || os(watchOS)
import UIKit
#elseif os(OSX)
import AppKit
#endif
// MARK: Mixing Colors
public extension DynamicColor {
/**
Mixes the given color object with the receiver.
Specifically, takes the average of each of the RGB components, optionally weighted by the given percentage.
- Parameter color: A color object to mix with the receiver.
- Parameter weight: The weight specifies the amount of the given color object (between 0 and 1).
The default value is 0.5, which means that half the given color and half the receiver color object should be used.
0.25 means that a quarter of the given color object and three quarters of the receiver color object should be used.
- Parameter colorspace: The color space used to mix the colors. By default it uses the RBG color space.
- Returns: A color object corresponding to the two colors object mixed together.
*/
final func mixed(withColor color: DynamicColor, weight: CGFloat = 0.5, inColorSpace colorspace: DynamicColorSpace = .rgb) -> DynamicColor {
let normalizedWeight = clip(weight, 0.0, 1.0)
switch colorspace {
case .lab:
return mixedLab(withColor: color, weight: normalizedWeight)
case .hsl:
return mixedHSL(withColor: color, weight: normalizedWeight)
case .hsb:
return mixedHSB(withColor: color, weight: normalizedWeight)
case .rgb:
return mixedRGB(withColor: color, weight: normalizedWeight)
}
}
/**
Creates and returns a color object corresponding to the mix of the receiver and an amount of white color, which increases lightness.
- Parameter amount: Float between 0.0 and 1.0. The default amount is equal to 0.2.
- Returns: A lighter DynamicColor.
*/
final func tinted(amount: CGFloat = 0.2) -> DynamicColor {
return mixed(withColor: .white, weight: amount)
}
/**
Creates and returns a color object corresponding to the mix of the receiver and an amount of black color, which reduces lightness.
- Parameter amount: Float between 0.0 and 1.0. The default amount is equal to 0.2.
- Returns: A darker DynamicColor.
*/
final func shaded(amount: CGFloat = 0.2) -> DynamicColor {
return mixed(withColor: DynamicColor(red: 0, green: 0, blue: 0, alpha: 1), weight: amount)
}
// MARK: - Convenient Internal Methods
func mixedLab(withColor color: DynamicColor, weight: CGFloat) -> DynamicColor {
let c1 = toLabComponents()
let c2 = color.toLabComponents()
let L = c1.L + (weight * (c2.L - c1.L))
let a = c1.a + (weight * (c2.a - c1.a))
let b = c1.b + (weight * (c2.b - c1.b))
let alpha = alphaComponent + (weight * (color.alphaComponent - alphaComponent))
return DynamicColor(L: L, a: a, b: b, alpha: alpha)
}
func mixedHSL(withColor color: DynamicColor, weight: CGFloat) -> DynamicColor {
let c1 = toHSLComponents()
let c2 = color.toHSLComponents()
let h = c1.h + (weight * mixedHue(source: c1.h, target: c2.h))
let s = c1.s + (weight * (c2.s - c1.s))
let l = c1.l + (weight * (c2.l - c1.l))
let alpha = alphaComponent + (weight * (color.alphaComponent - alphaComponent))
return DynamicColor(hue: h, saturation: s, lightness: l, alpha: alpha)
}
func mixedHSB(withColor color: DynamicColor, weight: CGFloat) -> DynamicColor {
let c1 = toHSBComponents()
let c2 = color.toHSBComponents()
let h = c1.h + (weight * mixedHue(source: c1.h, target: c2.h))
let s = c1.s + (weight * (c2.s - c1.s))
let b = c1.b + (weight * (c2.b - c1.b))
let alpha = alphaComponent + (weight * (color.alphaComponent - alphaComponent))
return DynamicColor(hue: h, saturation: s, brightness: b, alpha: alpha)
}
func mixedRGB(withColor color: DynamicColor, weight: CGFloat) -> DynamicColor {
let c1 = toRGBAComponents()
let c2 = color.toRGBAComponents()
let red = c1.r + (weight * (c2.r - c1.r))
let green = c1.g + (weight * (c2.g - c1.g))
let blue = c1.b + (weight * (c2.b - c1.b))
let alpha = alphaComponent + (weight * (color.alphaComponent - alphaComponent))
return DynamicColor(red: red, green: green, blue: blue, alpha: alpha)
}
func mixedHue(source: CGFloat, target: CGFloat) -> CGFloat {
if target > source && target - source > 180.0 {
return target - source + 360.0
}
else if target < source && source - target > 180.0 {
return target + 360.0 - source
}
return target - source
}
}
@@ -0,0 +1,119 @@
/*
* DynamicColor
*
* Copyright 2015-present Yannick Loriot.
* http://yannickloriot.com
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
*/
#if os(iOS) || os(tvOS) || os(watchOS)
import UIKit
#elseif os(OSX)
import AppKit
#endif
// MARK: RGBA Color Space
public extension DynamicColor {
/**
Initializes and returns a color object using the specified opacity and RGB component values.
Notes that values out of range are clipped.
- Parameter r: The red component of the color object, specified as a value from 0.0 to 255.0.
- Parameter g: The green component of the color object, specified as a value from 0.0 to 255.0.
- Parameter b: The blue component of the color object, specified as a value from 0.0 to 255.0.
- Parameter a: The opacity value of the color object, specified as a value from 0.0 to 255.0. The default value is 255.
*/
convenience init(r: CGFloat, g: CGFloat, b: CGFloat, a: CGFloat = 255) {
self.init(red: clip(r, 0, 255) / 255, green: clip(g, 0, 255) / 255, blue: clip(b, 0, 255) / 255, alpha: clip(a, 0, 255) / 255)
}
// MARK: - Getting the RGBA Components
/**
Returns the RGBA (red, green, blue, alpha) components.
- returns: The RGBA components as a tuple (r, g, b, a).
*/
final func toRGBAComponents() -> (r: CGFloat, g: CGFloat, b: CGFloat, a: CGFloat) {
var r: CGFloat = 0, g: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0
#if os(iOS) || os(tvOS) || os(watchOS)
getRed(&r, green: &g, blue: &b, alpha: &a)
return (r, g, b, a)
#elseif os(OSX)
guard let rgbaColor = self.usingColorSpace(.deviceRGB) else {
fatalError("Could not convert color to RGBA.")
}
rgbaColor.getRed(&r, green: &g, blue: &b, alpha: &a)
return (r, g, b, a)
#endif
}
#if os(iOS) || os(tvOS) || os(watchOS)
/**
The red component as CGFloat between 0.0 to 1.0.
*/
final var redComponent: CGFloat {
return toRGBAComponents().r
}
/**
The green component as CGFloat between 0.0 to 1.0.
*/
final var greenComponent: CGFloat {
return toRGBAComponents().g
}
/**
The blue component as CGFloat between 0.0 to 1.0.
*/
final var blueComponent: CGFloat {
return toRGBAComponents().b
}
/**
The alpha component as CGFloat between 0.0 to 1.0.
*/
final var alphaComponent: CGFloat {
return toRGBAComponents().a
}
#endif
// MARK: - Setting the RGBA Components
/**
Creates and returns a color object with the alpha increased by the given amount.
- parameter amount: CGFloat between 0.0 and 1.0.
- returns: A color object with its alpha channel modified.
*/
final func adjustedAlpha(amount: CGFloat) -> DynamicColor {
let components = toRGBAComponents()
let normalizedAlpha = clip(components.a + amount, 0.0, 1.0)
return DynamicColor(red: components.r, green: components.g, blue: components.b, alpha: normalizedAlpha)
}
}
@@ -0,0 +1,89 @@
/*
* DynamicColor
*
* Copyright 2015-present Yannick Loriot.
* http://yannickloriot.com
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
*/
#if os(iOS) || os(tvOS) || os(watchOS)
import UIKit
#elseif os(OSX)
import AppKit
#endif
// MARK: CIE XYZ Color Space
public extension DynamicColor {
/**
Initializes and returns a color object using CIE XYZ color space component values with an observer at 2° and a D65 illuminant.
Notes that values out of range are clipped.
- parameter X: The mix of cone response curves, specified as a value from 0 to 95.05.
- parameter Y: The luminance, specified as a value from 0 to 100.0.
- parameter Z: The quasi-equal to blue stimulation, specified as a value from 0 to 108.9.
- parameter alpha: The opacity value of the color object, specified as a value from 0.0 to 1.0. Default to 1.0.
*/
convenience init(X: CGFloat, Y: CGFloat, Z: CGFloat, alpha: CGFloat = 1) {
let clippedX = clip(X, 0.0, 95.05) / 100.0
let clippedY = clip(Y, 0.0, 100) / 100.0
let clippedZ = clip(Z, 0.0, 108.9) / 100.0
let toRGB = { (c: CGFloat) -> CGFloat in
let rgb = c > 0.0031308 ? 1.055 * pow(c, 1.0 / 2.4) - 0.055 : c * 12.92
return abs(roundDecimal(rgb, precision: 1000.0))
}
let red = toRGB((clippedX * 3.2406) + (clippedY * -1.5372) + (clippedZ * -0.4986))
let green = toRGB((clippedX * -0.9689) + (clippedY * 1.8758) + (clippedZ * 0.0415))
let blue = toRGB((clippedX * 0.0557) + (clippedY * -0.2040) + (clippedZ * 1.0570))
self.init(red: red, green: green, blue: blue, alpha: alpha)
}
// MARK: - Getting the XYZ Components
/**
Returns the XYZ (mix of cone response curves, luminance, quasi-equal to blue stimulation) components with an observer at 2° and a D65 illuminant.
Notes that X values are between 0 to 95.05, Y values are between 0 to 100.0 and Z values are between 0 to 108.9.
- returns: The XYZ components as a tuple (X, Y, Z).
*/
final func toXYZComponents() -> (X: CGFloat, Y: CGFloat, Z: CGFloat) {
let toSRGB = { (c: CGFloat) -> CGFloat in
c > 0.04045 ? pow((c + 0.055) / 1.055, 2.4) : c / 12.92
}
let rgba = toRGBAComponents()
let red = toSRGB(rgba.r)
let green = toSRGB(rgba.g)
let blue = toSRGB(rgba.b)
let X = roundDecimal(((red * 0.4124) + (green * 0.3576) + (blue * 0.1805)) * 100.0, precision: 1000.0)
let Y = roundDecimal(((red * 0.2126) + (green * 0.7152) + (blue * 0.0722)) * 100.0, precision: 1000.0)
let Z = roundDecimal(((red * 0.0193) + (green * 0.1192) + (blue * 0.9505)) * 100.0, precision: 1000.0)
return (X: X, Y: Y, Z: Z)
}
}
@@ -0,0 +1,226 @@
/*
* DynamicColor
*
* Copyright 2015-present Yannick Loriot.
* http://yannickloriot.com
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
*/
#if os(iOS) || os(tvOS) || os(watchOS)
import UIKit
/**
Extension to manipulate colours easily.
It allows you to work hexadecimal strings and value, HSV and RGB components, derivating colours, and many more...
*/
public typealias DynamicColor = UIColor
#elseif os(OSX)
import AppKit
/**
Extension to manipulate colours easily.
It allows you to work hexadecimal strings and value, HSV and RGB components, derivating colours, and many more...
*/
public typealias DynamicColor = NSColor
#endif
public extension DynamicColor {
// MARK: - Manipulating Hexa-decimal Values and Strings
/**
Creates a color from an hex string (e.g. "#3498db"). The RGBA string are also supported (e.g. "#3498dbff").
If the given hex string is invalid the initialiser will create a black color.
- parameter hexString: A hexa-decimal color string representation.
*/
convenience init(hexString: String) {
let hexString = hexString.trimmingCharacters(in: .whitespacesAndNewlines)
let scanner = Scanner(string: hexString)
scanner.charactersToBeSkipped = CharacterSet(charactersIn: "#")
var color: UInt64 = 0
if scanner.scanHexInt64(&color) {
self.init(hex: color, useAlpha: hexString.count > 7)
}
else {
self.init(hex: 0x000000)
}
}
/**
Creates a color from an hex integer (e.g. 0x3498db).
- parameter hex: A hexa-decimal UInt64 that represents a color.
- parameter alphaChannel: If true the given hex-decimal UInt64 includes the alpha channel (e.g. 0xFF0000FF).
*/
convenience init(hex: UInt64, useAlpha alphaChannel: Bool = false) {
let mask = UInt64(0xFF)
let cappedHex = !alphaChannel && hex > 0xffffff ? 0xffffff : hex
let r = cappedHex >> (alphaChannel ? 24 : 16) & mask
let g = cappedHex >> (alphaChannel ? 16 : 8) & mask
let b = cappedHex >> (alphaChannel ? 8 : 0) & mask
let a = alphaChannel ? cappedHex & mask : 255
let red = CGFloat(r) / 255.0
let green = CGFloat(g) / 255.0
let blue = CGFloat(b) / 255.0
let alpha = CGFloat(a) / 255.0
self.init(red: red, green: green, blue: blue, alpha: alpha)
}
/**
Returns the color representation as hexadecimal string.
- returns: A string similar to this pattern "#f4003b".
*/
final func toHexString() -> String {
return String(format: "#%06x", toHex())
}
/**
Returns the color representation as an integer (without the alpha channel).
- returns: A UInt32 that represents the hexa-decimal color.
*/
final func toHex() -> UInt32 {
let rgba = toRGBAComponents()
return roundToHex(rgba.r) << 16 | roundToHex(rgba.g) << 8 | roundToHex(rgba.b)
}
/**
Returns the RGBA color representation.
- returns: A UInt32 that represents the color as an RGBA value.
*/
func toRGBA() -> UInt32 {
let rgba = toRGBAComponents()
return roundToHex(rgba.r) << 24 | roundToHex(rgba.g) << 16 | roundToHex(rgba.b) << 8 | roundToHex(rgba.a)
}
/**
Returns the AGBR color representation.
- returns: A UInt32 that represents the color as an AGBR value.
*/
func toAGBR() -> UInt32 {
let rgba = toRGBAComponents()
return roundToHex(rgba.a) << 24 | roundToHex(rgba.b) << 16 | roundToHex(rgba.g) << 8 | roundToHex(rgba.r)
}
// MARK: - Identifying and Comparing Colors
/**
Returns a boolean value that indicates whether the receiver is equal to the given hexa-decimal string.
- parameter hexString: A hexa-decimal color number representation to be compared to the receiver.
- returns: true if the receiver and the string are equals, otherwise false.
*/
func isEqual(toHexString hexString: String) -> Bool {
return self.toHexString() == hexString
}
/**
Returns a boolean value that indicates whether the receiver is equal to the given hexa-decimal integer.
- parameter hex: A UInt32 that represents the hexa-decimal color.
- returns: true if the receiver and the integer are equals, otherwise false.
*/
func isEqual(toHex hex: UInt32) -> Bool {
return self.toHex() == hex
}
// MARK: - Querying Colors
/**
Determines if the color object is dark or light.
It is useful when you need to know whether you should display the text in black or white.
- returns: A boolean value to know whether the color is light. If true the color is light, dark otherwise.
*/
func isLight() -> Bool {
let components = toRGBAComponents()
let brightness = ((components.r * 299.0) + (components.g * 587.0) + (components.b * 114.0)) / 1000.0
return brightness >= 0.5
}
/**
A float value representing the luminance of the current color. May vary from 0 to 1.0.
We use the formula described by W3C in WCAG 2.0. You can read more here: https://www.w3.org/TR/WCAG20/#relativeluminancedef.
*/
var luminance: CGFloat {
let components = toRGBAComponents()
let componentsArray = [components.r, components.g, components.b].map { (val) -> CGFloat in
guard val <= 0.03928 else { return pow((val + 0.055) / 1.055, 2.4) }
return val / 12.92
}
return (0.2126 * componentsArray[0]) + (0.7152 * componentsArray[1]) + (0.0722 * componentsArray[2])
}
/**
Returns a float value representing the contrast ratio between 2 colors.
We use the formula described by W3C in WCAG 2.0. You can read more here: https://www.w3.org/TR/WCAG20-TECHS/G18.html
NB: the contrast ratio is a relative value. So the contrast between Color1 and Color2 is exactly the same between Color2 and Color1.
- returns: A CGFloat representing contrast value.
*/
func contrastRatio(with otherColor: DynamicColor) -> CGFloat {
let otherLuminance = otherColor.luminance
let l1 = max(luminance, otherLuminance)
let l2 = min(luminance, otherLuminance)
return (l1 + 0.05) / (l2 + 0.05)
}
/**
Indicates if two colors are contrasting, regarding W3C's WCAG 2.0 recommendations.
You can read it here: https://www.w3.org/TR/2008/REC-WCAG20-20081211/#visual-audio-contrast-contrast
The acceptable contrast ratio depends on the context of display. Most of the time, the default context (.Standard) is enough.
You can look at ContrastDisplayContext for more options.
- parameter otherColor: The other color to compare with.
- parameter context: An optional context to determine the minimum acceptable contrast ratio. Default value is .Standard.
- returns: true is the contrast ratio between 2 colors exceed the minimum acceptable ratio.
*/
func isContrasting(with otherColor: DynamicColor, inContext context: ContrastDisplayContext = .standard) -> Bool {
return self.contrastRatio(with: otherColor) > context.minimumContrastRatio
}
}
@@ -0,0 +1,105 @@
/*
* DynamicColor
*
* Copyright 2015-present Yannick Loriot.
* http://yannickloriot.com
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
*/
#if os(iOS) || os(tvOS) || os(watchOS)
import UIKit
#elseif os(OSX)
import AppKit
#endif
/**
Object representing a gradient object. It allows you to manipulate colors inside different gradients and color spaces.
*/
final public class DynamicGradient {
let colors: [DynamicColor]
/**
Initializes and creates a gradient from a color array.
- Parameter colors: An array of colors.
*/
public init(colors: [DynamicColor]) {
self.colors = colors
}
/**
Returns the color palette of `amount` elements by grabbing equidistant colors.
- Parameter amount: An amount of colors to return. 2 by default.
- Parameter colorspace: The color space used to mix the colors. By default it uses the RBG color space.
- Returns: An array of DynamicColor objects with equi-distant space in the gradient.
*/
public func colorPalette(amount: UInt = 2, inColorSpace colorspace: DynamicColorSpace = .rgb) -> [DynamicColor] {
guard amount > 0 && colors.count > 0 else {
return []
}
guard colors.count > 1 else {
return (0 ..< amount).map { _ in colors[0] }
}
let increment = 1.0 / CGFloat(amount - 1)
return (0 ..< amount).map { pickColorAt(scale: CGFloat($0) * increment, inColorSpace: colorspace) }
}
/**
Picks up and returns the color at the given scale by interpolating the colors.
For example, given this color array `[red, green, blue]` and a scale of `0.25` you will get a kaki color.
- Parameter scale: A float value between 0.0 and 1.0.
- Parameter colorspace: The color space used to mix the colors. By default it uses the RBG color space.
- Returns: A DynamicColor object corresponding to the color at the given scale.
*/
public func pickColorAt(scale: CGFloat, inColorSpace colorspace: DynamicColorSpace = .rgb) -> DynamicColor {
guard colors.count > 1 else {
return colors.first ?? .black
}
let clippedScale = clip(scale, 0.0, 1.0)
let positions = (0 ..< colors.count).map { CGFloat($0) / CGFloat(colors.count - 1) }
var color: DynamicColor = .black
for (index, position) in positions.enumerated() {
guard clippedScale <= position else { continue }
guard clippedScale != 0.0 && clippedScale != 1.0 else {
return colors[index]
}
let previousPosition = positions[index - 1]
let weight = (clippedScale - previousPosition) / (position - previousPosition)
color = colors[index - 1].mixed(withColor: colors[index], weight: weight, inColorSpace: colorspace)
break
}
return color
}
}
@@ -0,0 +1,39 @@
/*
* DynamicColor
*
* Copyright 2015-present Yannick Loriot.
* http://yannickloriot.com
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
*/
/**
Defines the supported color spaces.
*/
public enum DynamicColorSpace {
/// The RGB color space
case rgb
/// The HSL color space
case hsl
/// The HSB color space
case hsb
/// The Cie L*a*b* color space
case lab
}
@@ -0,0 +1,41 @@
/*
* DynamicColor
*
* Copyright 2015-present Yannick Loriot.
* http://yannickloriot.com
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
*/
/**
Defines the mode (i.e color space) used for grayscaling.
[More info](https://en.wikipedia.org/wiki/Lightness#Lightness_and_human_perception)
*/
public enum GrayscalingMode {
/// XYZ luminance
case luminance
/// HSL lightness
case lightness
/// RGB average
case average
/// HSV value
case value
}
@@ -0,0 +1,196 @@
/*
* DynamicColor
*
* Copyright 2015-present Yannick Loriot.
* http://yannickloriot.com
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
*/
import SwiftUI
#if os(iOS) || os(tvOS) || os(watchOS)
import UIKit
#elseif os(OSX)
import AppKit
#endif
/// Hue-saturation-lightness structure to make the color manipulation easier.
internal struct HSL {
/// Hue value between 0.0 and 1.0 (0.0 = 0 degree, 1.0 = 360 degree).
var h: CGFloat = 0.0
/// Saturation value between 0.0 and 1.0.
var s: CGFloat = 0.0
/// Lightness value between 0.0 and 1.0.
var l: CGFloat = 0.0
/// Alpha value between 0.0 and 1.0.
var a: CGFloat = 1.0
// MARK: - Initializing HSL Colors
/**
Initializes and creates a HSL color from the hue, saturation, lightness and alpha components.
- parameter h: The hue component of the color object, specified as a value between 0.0 and 360.0 degree.
- parameter s: The saturation component of the color object, specified as a value between 0.0 and 1.0.
- parameter l: The lightness component of the color object, specified as a value between 0.0 and 1.0.
- parameter a: The opacity component of the color object, specified as a value between 0.0 and 1.0.
*/
init(hue: CGFloat, saturation: CGFloat, lightness: CGFloat, alpha: CGFloat = 1.0) {
h = hue.truncatingRemainder(dividingBy: 360.0) / 360.0
s = clip(saturation, 0.0, 1.0)
l = clip(lightness, 0.0, 1.0)
a = clip(alpha, 0.0, 1.0)
}
/**
Initializes and creates a HSL (hue, saturation, lightness) color from a DynamicColor object.
- parameter color: A DynamicColor object.
*/
init(color: DynamicColor) {
let rgba = color.toRGBAComponents()
let maximum = max(rgba.r, max(rgba.g, rgba.b))
let minimum = min(rgba.r, min(rgba.g, rgba.b))
let delta = maximum - minimum
h = 0.0
s = 0.0
l = (maximum + minimum) / 2.0
if delta != 0.0 {
if l < 0.5 {
s = delta / (maximum + minimum)
}
else {
s = delta / (2.0 - maximum - minimum)
}
if rgba.r == maximum {
h = ((rgba.g - rgba.b) / delta) + (rgba.g < rgba.b ? 6.0 : 0.0)
}
else if rgba.g == maximum {
h = ((rgba.b - rgba.r) / delta) + 2.0
}
else if rgba.b == maximum {
h = ((rgba.r - rgba.g) / delta) + 4.0
}
}
h /= 6.0
a = rgba.a
}
// MARK: - Transforming HSL Color
/**
Returns the DynamicColor representation from the current HSV color.
- returns: A DynamicColor object corresponding to the current HSV color.
*/
func toDynamicColor() -> DynamicColor {
let (r, g, b, a) = rgbaComponents()
return DynamicColor(red: r, green: g, blue: b, alpha: a)
}
/// Returns the RGBA components from the current HSV color.
func rgbaComponents() -> (CGFloat, CGFloat, CGFloat, CGFloat) {
let m2 = l <= 0.5 ? l * (s + 1.0) : (l + s) - (l * s)
let m1 = (l * 2.0) - m2
let r = hueToRGB(m1: m1, m2: m2, h: h + (1.0 / 3.0))
let g = hueToRGB(m1: m1, m2: m2, h: h)
let b = hueToRGB(m1: m1, m2: m2, h: h - (1.0 / 3.0))
return (r, g, b, CGFloat(a))
}
/// Hue to RGB helper function
private func hueToRGB(m1: CGFloat, m2: CGFloat, h: CGFloat) -> CGFloat {
let hue = moda(h, m: 1)
if hue * 6 < 1.0 {
return m1 + ((m2 - m1) * hue * 6.0)
}
else if hue * 2.0 < 1.0 {
return m2
}
else if hue * 3.0 < 1.9999 {
return m1 + ((m2 - m1) * ((2.0 / 3.0) - hue) * 6.0)
}
return m1
}
// MARK: - Deriving the Color
/**
Returns a color with the hue rotated along the color wheel by the given amount.
- parameter amount: A float representing the number of degrees as ratio (usually between -360.0 degree and 360.0 degree).
- returns: A HSL color with the hue changed.
*/
func adjustedHue(amount: CGFloat) -> HSL {
return HSL(hue: (h * 360.0) + amount, saturation: s, lightness: l, alpha: a)
}
/**
Returns a color with the lightness increased by the given amount.
- parameter amount: CGFloat between 0.0 and 1.0.
- returns: A lighter HSL color.
*/
func lighter(amount: CGFloat) -> HSL {
return HSL(hue: h * 360.0, saturation: s, lightness: l + amount, alpha: a)
}
/**
Returns a color with the lightness decreased by the given amount.
- parameter amount: CGFloat between 0.0 and 1.0.
- returns: A darker HSL color.
*/
func darkened(amount: CGFloat) -> HSL {
return lighter(amount: amount * -1.0)
}
/**
Returns a color with the saturation increased by the given amount.
- parameter amount: CGFloat between 0.0 and 1.0.
- returns: A HSL color more saturated.
*/
func saturated(amount: CGFloat) -> HSL {
return HSL(hue: h * 360.0, saturation: s + amount, lightness: l, alpha: a)
}
/**
Returns a color with the saturation decreased by the given amount.
- parameter amount: CGFloat between 0.0 and 1.0.
- returns: A HSL color less saturated.
*/
func desaturated(amount: CGFloat) -> HSL {
return saturated(amount: amount * -1.0)
}
}
@@ -0,0 +1,73 @@
/*
* DynamicColor
*
* Copyright 2015-present Yannick Loriot.
* http://yannickloriot.com
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
*/
#if os(iOS) || os(tvOS) || os(watchOS)
import UIKit
#elseif os(OSX)
import AppKit
#endif
/**
Clips the values in an interval.
Given an interval, values outside the interval are clipped to the interval
edges. For example, if an interval of [0, 1] is specified, values smaller than
0 become 0, and values larger than 1 become 1.
- Parameter v: The value to clipped.
- Parameter minimum: The minimum edge value.
- Parameter maximum: The maximum edgevalue.
*/
internal func clip<T: Comparable>(_ v: T, _ minimum: T, _ maximum: T) -> T {
return max(min(v, maximum), minimum)
}
/**
Returns the absolute value of the modulo operation.
- Parameter x: The value to compute.
- Parameter m: The modulo.
*/
internal func moda(_ x: CGFloat, m: CGFloat) -> CGFloat {
return (x.truncatingRemainder(dividingBy: m) + m).truncatingRemainder(dividingBy: m)
}
/**
Rounds the given float to a given decimal precision.
- Parameter x: The value to round.
- Parameter m: The precision. Default to 10000.
*/
internal func roundDecimal(_ x: CGFloat, precision: CGFloat = 10000.0) -> CGFloat {
return CGFloat(Int(round(x * precision))) / precision
}
internal func roundToHex(_ x: CGFloat) -> UInt32 {
guard x > 0 else { return 0 }
let rounded: CGFloat = round(x * 255.0)
return UInt32(rounded)
}
@@ -0,0 +1,77 @@
/*
* DynamicColor
*
* Copyright 2015-present Yannick Loriot.
* http://yannickloriot.com
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
*/
import SwiftUI
@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *)
public extension Color {
// MARK: - Manipulating Hexa-decimal Values and Strings
/**
Creates a color from an hex string (e.g. "#3498db"). The RGBA string are also supported (e.g. "#3498dbff").
If the given hex string is invalid the initialiser will create a black color.
- parameter hexString: A hexa-decimal color string representation.
*/
init(hexString: String) {
let hexString = hexString.trimmingCharacters(in: .whitespacesAndNewlines)
let scanner = Scanner(string: hexString)
scanner.charactersToBeSkipped = CharacterSet(charactersIn: "#")
var color: UInt64 = 0
if scanner.scanHexInt64(&color) {
self.init(hex: color, useOpacity: hexString.count > 7)
}
else {
self.init(hex: 0x000000)
}
}
/**
Creates a color from an hex integer (e.g. 0x3498db).
- parameter hex: A hexa-decimal UInt64 that represents a color.
- parameter opacityChannel: If true the given hex-decimal UInt64 includes the opacity channel (e.g. 0xFF0000FF).
*/
init(hex: UInt64, useOpacity opacityChannel: Bool = false) {
let mask = UInt64(0xFF)
let cappedHex = !opacityChannel && hex > 0xffffff ? 0xffffff : hex
let r = cappedHex >> (opacityChannel ? 24 : 16) & mask
let g = cappedHex >> (opacityChannel ? 16 : 8) & mask
let b = cappedHex >> (opacityChannel ? 8 : 0) & mask
let o = opacityChannel ? cappedHex & mask : 255
let red = Double(r) / 255.0
let green = Double(g) / 255.0
let blue = Double(b) / 255.0
let opacity = Double(o) / 255.0
self.init(red: red, green: green, blue: blue, opacity: opacity)
}
}
@@ -0,0 +1,46 @@
/*
* DynamicColor
*
* Copyright 2015-present Yannick Loriot.
* http://yannickloriot.com
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
*/
import SwiftUI
#if os(iOS) || os(tvOS) || os(watchOS)
import UIKit
#elseif os(OSX)
import AppKit
#endif
/// Convert a DynamicColor to a SwiftUI color
extension DynamicColor {
/**
Returns the Color from an Dynamic Color.
- returns: A Color (SwiftUI).
*/
@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *)
func toColor() -> Color {
return Color(self)
}
}
@@ -0,0 +1,37 @@
/*
* DynamicColor
*
* Copyright 2015-present Yannick Loriot.
* http://yannickloriot.com
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
*/
import SwiftUI
extension HSL {
@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *)
func toColor() -> Color {
let (r, g, b, a) = rgbaComponents()
return SwiftUI.Color(red: Double(r), green: Double(g), blue: Double(b), opacity: Double(a))
}
}