├── .github └── workflows │ ├── build.yml │ └── release.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── build.gradle.kts ├── colormath ├── api │ └── colormath.api ├── build.gradle.kts ├── gradle.properties └── src │ ├── commonMain │ └── kotlin │ │ └── com │ │ └── github │ │ └── ajalt │ │ └── colormath │ │ ├── Color.kt │ │ ├── ColorSpace.kt │ │ ├── CssParse.kt │ │ ├── CssRender.kt │ │ ├── HueColor.kt │ │ ├── WhitePoint.kt │ │ ├── calculate │ │ ├── Contrast.kt │ │ ├── Difference.kt │ │ └── Gamut.kt │ │ ├── internal │ │ ├── ColorSpaceUtils.kt │ │ ├── Constants.kt │ │ ├── CssColors.kt │ │ ├── InternalMath.kt │ │ └── Matrix.kt │ │ ├── model │ │ ├── Ansi16.kt │ │ ├── Ansi256.kt │ │ ├── CMYK.kt │ │ ├── HPLuv.kt │ │ ├── HSL.kt │ │ ├── HSLuv.kt │ │ ├── HSV.kt │ │ ├── HWB.kt │ │ ├── ICtCp.kt │ │ ├── JzAzBz.kt │ │ ├── JzCzHz.kt │ │ ├── LAB.kt │ │ ├── LCHab.kt │ │ ├── LCHuv.kt │ │ ├── LUV.kt │ │ ├── Oklab.kt │ │ ├── Oklch.kt │ │ ├── RGB.kt │ │ ├── RGBColorSpaces.kt │ │ ├── RGBInt.kt │ │ ├── XYZ.kt │ │ └── xyY.kt │ │ └── transform │ │ ├── ChromaticAdapter.kt │ │ ├── EasingFunctions.kt │ │ ├── HueAdjustments.kt │ │ ├── Interpolate.kt │ │ ├── InterpolationMethod.kt │ │ ├── Mix.kt │ │ ├── Premultiply.kt │ │ ├── RGBToRGBConverter.kt │ │ └── Transform.kt │ └── commonTest │ └── kotlin │ └── com │ └── github │ └── ajalt │ └── colormath │ └── internal │ └── MatrixTest.kt ├── docs ├── colorspaces.md ├── css │ └── logo-styles.css ├── extensions.md ├── img │ ├── bad_hue_grad.svg │ ├── color_circle.svg │ ├── colormath_wordmark.svg │ ├── good_hue_grad.svg │ ├── palette_black_36dp.svg │ └── palette_white_24dp.svg └── usage.md ├── extensions ├── build.gradle.kts ├── colormath-ext-android-color │ ├── api │ │ └── colormath-ext-android-color.api │ ├── build.gradle.kts │ ├── gradle.properties │ └── src │ │ ├── androidMain │ │ └── kotlin │ │ │ └── com │ │ │ └── github │ │ │ └── ajalt │ │ │ └── colormath │ │ │ └── extensions │ │ │ └── android │ │ │ └── color │ │ │ └── ColorExtensions.kt │ │ └── androidUnitTest │ │ └── kotlin │ │ └── com │ │ └── github │ │ └── ajalt │ │ └── colormath │ │ └── extensions │ │ └── android │ │ └── color │ │ └── ColorExtensionsTest.kt ├── colormath-ext-android-colorint │ ├── api │ │ └── colormath-ext-android-colorint.api │ ├── build.gradle.kts │ ├── gradle.properties │ └── src │ │ ├── androidMain │ │ └── kotlin │ │ │ └── com │ │ │ └── github │ │ │ └── ajalt │ │ │ └── colormath │ │ │ └── extensions │ │ │ └── android │ │ │ └── colorint │ │ │ └── ColorIntExtensions.kt │ │ └── androidUnitTest │ │ └── kotlin │ │ └── com │ │ └── github │ │ └── ajalt │ │ └── colormath │ │ └── extensions │ │ └── android │ │ └── colorint │ │ └── ColorIntExtensionsTest.kt └── colormath-ext-jetpack-compose │ ├── api │ ├── android │ │ └── colormath-ext-jetpack-compose.api │ └── jvm │ │ └── colormath-ext-jetpack-compose.api │ ├── build.gradle.kts │ ├── gradle.properties │ └── src │ ├── commonMain │ └── kotlin │ │ └── com │ │ └── github │ │ └── ajalt │ │ └── colormath │ │ └── extensions │ │ └── android │ │ └── composecolor │ │ └── ComposeColorExtensions.kt │ └── jvmTest │ └── kotlin │ └── com │ └── github │ └── ajalt │ └── colormath │ └── extensions │ └── android │ └── colorint │ └── ComposeColorExtensionsTest.kt ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── mkdocs.yml ├── prepare_docs.sh ├── scripts ├── benchmarks │ ├── build.gradle.kts │ └── src │ │ └── jmh │ │ └── kotlin │ │ └── com │ │ └── github │ │ └── ajalt │ │ └── colormath │ │ └── benchmark │ │ └── ColorBenchmarks.kt └── generate_tests.py ├── settings.gradle.kts ├── test ├── build.gradle.kts ├── gradle.properties └── src │ └── commonTest │ └── kotlin │ └── com │ └── github │ └── ajalt │ └── colormath │ ├── CssParseTest.kt │ ├── CssRenderTest.kt │ ├── HueColorTest.kt │ ├── TestUtils.kt │ ├── calculate │ ├── ContrastTest.kt │ ├── DifferenceTest.kt │ └── GamutTest.kt │ ├── model │ ├── Ansi16Test.kt │ ├── Ansi256Test.kt │ ├── CMYKTest.kt │ ├── HPLuvTest.kt │ ├── HSLTest.kt │ ├── HSLuvTest.kt │ ├── HSVTest.kt │ ├── HWBTest.kt │ ├── ICtCpTest.kt │ ├── JzAzBzTest.kt │ ├── JzCzHzTest.kt │ ├── LABTest.kt │ ├── LCHabTest.kt │ ├── LCHuvTest.kt │ ├── LUVTest.kt │ ├── OklabTest.kt │ ├── OklchTest.kt │ ├── RGBColorSpacesConversionTest.kt │ ├── RGBColorSpacesTransferFunctionsTest.kt │ ├── RGBIntTest.kt │ ├── RGBTest.kt │ └── XYZTest.kt │ └── transform │ ├── EasingFunctionsTest.kt │ ├── HueAdjustmentsTest.kt │ ├── InterpolateTest.kt │ └── TransformTest.kt └── website ├── build.gradle.kts └── src ├── commonMain └── composeResources │ └── drawable │ └── colormath_wordmark.png └── wasmJsMain ├── kotlin ├── App.kt ├── ColorPickerViewModel.kt └── main.kt └── resources └── index.html /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | pull_request: 5 | paths-ignore: 6 | - 'docs/**' 7 | - 'samples/**' 8 | - '*.md' 9 | push: 10 | branches: 11 | - 'master' 12 | paths-ignore: 13 | - 'docs/**' 14 | - 'samples/**' 15 | - '*.md' 16 | jobs: 17 | test: 18 | strategy: 19 | matrix: 20 | os: [macos-latest, windows-latest, ubuntu-latest] 21 | include: 22 | - os: ubuntu-latest 23 | GRADLE_ARGS: >- 24 | apiCheck 25 | :test:check 26 | :colormath:compileKotlinLinuxArm64 27 | :colormath:compileKotlinWasmJs 28 | :extensions:colormath-ext-jetpack-compose:check 29 | :extensions:colormath-ext-android-colorint:check 30 | :extensions:colormath-ext-android-color:check 31 | --stacktrace 32 | - os: macos-latest 33 | GRADLE_ARGS: >- 34 | macosX64Test 35 | :colormath:compileKotlinMacosArm64 36 | iosX64Test 37 | tvosX64Test 38 | iosSimulatorArm64Test 39 | tvosSimulatorArm64Test 40 | watchosSimulatorArm64Test 41 | --stacktrace 42 | - os: windows-latest 43 | GRADLE_ARGS: mingwX64Test --stacktrace 44 | runs-on: ${{matrix.os}} 45 | steps: 46 | - uses: actions/checkout@v4 47 | - uses: actions/setup-java@v4 48 | with: 49 | distribution: 'zulu' 50 | java-version: 21 51 | - uses: gradle/actions/setup-gradle@v4 52 | - run: ./gradlew ${{matrix.GRADLE_ARGS}} 53 | - name: Upload the build report 54 | if: failure() 55 | uses: actions/upload-artifact@master 56 | with: 57 | name: build-report-${{ matrix.os }} 58 | path: '**/build/reports' 59 | publish: 60 | needs: test 61 | runs-on: ubuntu-latest 62 | if: ${{ github.ref == 'refs/heads/master' && github.repository == 'ajalt/colormath' }} 63 | steps: 64 | - uses: actions/checkout@v4 65 | - uses: actions/setup-java@v4 66 | with: 67 | distribution: 'zulu' 68 | java-version: 21 69 | - uses: gradle/actions/setup-gradle@v4 70 | - name: Deploy to sonatype 71 | # disable configuration cache due to https://github.com/gradle/gradle/issues/22779 72 | run: ./gradlew publishToMavenCentral -PsnapshotVersion=true --no-configuration-cache 73 | env: 74 | ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.ORG_GRADLE_PROJECT_mavenCentralPassword }} 75 | ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.ORG_GRADLE_PROJECT_signingInMemoryKey }} 76 | ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.ORG_GRADLE_PROJECT_mavenCentralUsername }} 77 | env: 78 | GRADLE_OPTS: -Dorg.gradle.configureondemand=true -Dorg.gradle.parallel=false -Dkotlin.incremental=false -Dorg.gradle.project.kotlin.incremental.multiplatform=false -Dorg.gradle.project.kotlin.native.disableCompilerDaemon=true -Dorg.gradle.jvmargs="-Dfile.encoding=UTF-8" 79 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '**' 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: actions/setup-java@v4 14 | with: 15 | distribution: 'zulu' 16 | java-version: 21 17 | - uses: gradle/actions/setup-gradle@v4 18 | # - uses: actions/setup-python@v5 19 | # with: 20 | # python-version: '3.12' 21 | - run: ./gradlew publishToMavenCentral 22 | env: 23 | ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.ORG_GRADLE_PROJECT_mavenCentralPassword }} 24 | ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.ORG_GRADLE_PROJECT_signingInMemoryKey }} 25 | ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.ORG_GRADLE_PROJECT_mavenCentralUsername }} 26 | # - name: Extract release notes 27 | # id: extract-release-notes 28 | # uses: ffurrer2/extract-release-notes@v2 29 | # - name: Create release 30 | # uses: ncipollo/release-action@v1 31 | # with: 32 | # body: ${{ steps.extract-release-notes.outputs.release_notes }} 33 | # - name: Dokka 34 | # uses: gradle/actions/setup-gradle@v3 35 | # with: 36 | # arguments: dokkaHtml :website:wasmJsBrowserDistribution 37 | # - run: ./prepare_docs.sh 38 | # - name: Build mkdocs 39 | # run: | 40 | # pip install mkdocs-material 41 | # mkdocs build 42 | # - name: Deploy docs to website 43 | # uses: JamesIves/github-pages-deploy-action@v4 44 | # with: 45 | # branch: gh-pages 46 | # folder: site 47 | env: 48 | GRADLE_OPTS: -Dorg.gradle.configureondemand=true -Dorg.gradle.parallel=false -Dkotlin.incremental=false -Dorg.gradle.project.kotlin.incremental.multiplatform=false -Dorg.gradle.project.kotlin.native.disableCompilerDaemon=true -Dorg.gradle.jvmargs="-Dfile.encoding=UTF-8" 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/ 5 | .DS_Store 6 | build/ 7 | captures/ 8 | .externalNativeBuild 9 | out/ 10 | docs/api/ 11 | docs/tryit/ 12 | docs/changelog.md 13 | docs/index.md 14 | site/ 15 | docs/js/gradient.js 16 | docs/js/converter.js 17 | kotlin-js-store/ 18 | .kotlin/ 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021 AJ Alt 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | Colormath is a Kotlin Multiplatform library for color manipulation and conversion. 6 | 7 | Colormath can: 8 | 9 | - Convert between color models and spaces 10 | - Manipulate colors with transformations such as mixing and chromatic adaptation 11 | - Calculate attributes such as WCAG contrast and perceptual color difference 12 | - Generate gradients with custom interpolation methods and easing functions 13 | - Parse and render colors as strings, including all representations from the CSS spec 14 | 15 | ```kotlin 16 | // Create an sRGB color 17 | val color = RGB("#ff23cc") 18 | 19 | // Interpolate with another color 20 | val mixed = color.interpolate(RGB(0.1, 0.4, 1), 0.5f) 21 | // RGB("#8c45e6") 22 | 23 | // Convert to a different color space 24 | val lab = mixed.toLAB() 25 | // LAB(46.3, 60.9, -70) 26 | 27 | // Change the transparency 28 | val labA = lab.copy(alpha = 0.25f) 29 | // LAB(46.3, 60.9, -70, 0.25) 30 | 31 | // Adapt white point 32 | val lab50 = labA.convertTo(LAB50) 33 | // LAB50(45, 55.1812, 72.5911, 0.25) 34 | 35 | // Render as a css color string 36 | println(lab50.formatCssString()) 37 | // "lab(45% 55.1812 -72.5911 / 0.25)" 38 | ``` 39 | 40 | ## Documentation 41 | 42 | The full documentation can be found on [the website](https://ajalt.github.io/colormath). 43 | 44 | You can also [try it online](https://ajalt.github.io/colormath/tryit/) 45 | 46 | ## Installation 47 | 48 | Colormath is distributed through [Maven Central](https://search.maven.org/artifact/com.github.ajalt.colormath/colormath/). 49 | 50 | ```groovy 51 | dependencies { 52 | implementation("com.github.ajalt.colormath:colormath:3.6.1") 53 | 54 | // optional extensions for interop with other platforms 55 | // 56 | // android.graphics.Color 57 | implementation("com.github.ajalt.colormath:colormath-ext-android-color:3.6.1") 58 | // androidx.annotation.ColorInt 59 | implementation("com.github.ajalt.colormath:colormath-ext-android-colorint:3.6.1") 60 | // androidx.compose.ui.graphics.Color 61 | implementation("com.github.ajalt.colormath:colormath-ext-jetpack-compose:3.6.1") 62 | } 63 | ``` 64 | 65 | ###### If you're using Maven instead of Gradle, use `colormath-jvm` 66 | 67 | #### Multiplatform 68 | 69 | Colormath publishes artifacts for all 70 | [Tier 1 and Tier 2](https://kotlinlang.org/docs/native-target-support.html) 71 | targets, as well as `mingwX64` and `wasmJs`. 72 | 73 | #### Snapshots 74 | 75 |
76 | Snapshot builds are also available 77 | 78 | 79 | 80 |

81 | You'll need to add the Sonatype snapshots repository: 82 | 83 | ```kotlin 84 | repositories { 85 | maven { 86 | url = uri("https://oss.sonatype.org/content/repositories/snapshots/") 87 | } 88 | } 89 | ``` 90 |

91 |
92 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.android.build.gradle.BaseExtension 2 | import com.vanniktech.maven.publish.JavadocJar 3 | import com.vanniktech.maven.publish.KotlinMultiplatform 4 | import com.vanniktech.maven.publish.MavenPublishBaseExtension 5 | import org.jetbrains.dokka.gradle.DokkaTask 6 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 7 | import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile 8 | import java.io.ByteArrayOutputStream 9 | 10 | plugins { 11 | kotlin("multiplatform").version(libs.versions.kotlin).apply(false) 12 | alias(libs.plugins.android.library).apply(false) 13 | alias(libs.plugins.dokka).apply(false) 14 | alias(libs.plugins.publish).apply(false) 15 | alias(libs.plugins.jetbrainsCompose).apply(false) 16 | alias(libs.plugins.compose.compiler) apply false 17 | alias(libs.plugins.kotlinBinaryCompatibilityValidator) 18 | } 19 | 20 | apiValidation { 21 | // https://github.com/Kotlin/binary-compatibility-validator/issues/3 22 | project("scripts").subprojects.mapTo(ignoredProjects) { it.name } 23 | project("test").subprojects.mapTo(ignoredProjects) { it.name } 24 | ignoredProjects.add("website") 25 | ignoredProjects.add("test") 26 | } 27 | 28 | 29 | fun getPublishVersion(): String { 30 | val versionName = project.property("VERSION_NAME") as String 31 | // Call gradle with -PsnapshotVersion to set the version as a snapshot. 32 | // Otherwise, we skip it to save time. 33 | if (!project.hasProperty("snapshotVersion")) return versionName 34 | val buildNumber = System.getenv("GITHUB_RUN_NUMBER") ?: "0" 35 | return "$versionName.$buildNumber-SNAPSHOT" 36 | } 37 | 38 | 39 | subprojects { 40 | project.setProperty("VERSION_NAME", getPublishVersion()) 41 | 42 | tasks.withType().configureEach { 43 | compilerOptions { 44 | jvmTarget.set(JvmTarget.JVM_1_8) 45 | } 46 | } 47 | 48 | plugins.withType().configureEach { 49 | configure { 50 | compileOptions { 51 | sourceCompatibility = JavaVersion.VERSION_1_8 52 | targetCompatibility = JavaVersion.VERSION_1_8 53 | } 54 | } 55 | } 56 | 57 | pluginManager.withPlugin("com.vanniktech.maven.publish") { 58 | apply(plugin = "org.jetbrains.dokka") 59 | extensions.configure("mavenPublishing") { 60 | @Suppress("UnstableApiUsage") 61 | configure(KotlinMultiplatform(JavadocJar.Empty())) 62 | } 63 | tasks.named("dokkaHtml") { 64 | val dir = if (project.name == "colormath") "" else "/${project.name}" 65 | outputDirectory.set(rootProject.rootDir.resolve("docs/api$dir")) 66 | val rootPath = rootProject.rootDir.toPath() 67 | val logoCss = rootPath.resolve("docs/css/logo-styles.css").toString().replace('\\', '/') 68 | val paletteSvg = rootPath.resolve("docs/img/palette_black_36dp.svg").toString() 69 | .replace('\\', '/') 70 | pluginsMapConfiguration.set( 71 | mapOf( 72 | "org.jetbrains.dokka.base.DokkaBase" to """{ 73 | "customStyleSheets": ["$logoCss"], 74 | "customAssets": ["$paletteSvg"], 75 | "footerMessage": "Copyright © 2021 AJ Alt" 76 | }""" 77 | ) 78 | ) 79 | dokkaSourceSets.configureEach { 80 | skipDeprecated.set(true) 81 | } 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /colormath/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl 2 | 3 | plugins { 4 | kotlin("multiplatform") 5 | alias(libs.plugins.publish) 6 | } 7 | 8 | repositories { 9 | mavenCentral() 10 | } 11 | 12 | kotlin { 13 | jvm() 14 | js { nodejs() } 15 | 16 | @OptIn(ExperimentalWasmDsl::class) 17 | wasmJs { nodejs() } 18 | 19 | linuxX64() 20 | linuxArm64() 21 | mingwX64() 22 | macosX64() 23 | macosArm64() 24 | iosX64() 25 | iosArm64() 26 | iosSimulatorArm64() 27 | tvosX64() 28 | tvosArm64() 29 | tvosSimulatorArm64() 30 | watchosX64() 31 | watchosArm32() 32 | watchosArm64() 33 | watchosSimulatorArm64() 34 | 35 | sourceSets { 36 | val commonTest by getting { 37 | dependencies { 38 | implementation(kotlin("test")) 39 | } 40 | } 41 | } 42 | } 43 | 44 | tasks.withType().configureEach { 45 | manifest { 46 | attributes("Automatic-Module-Name" to "com.github.ajalt.colormath") 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /colormath/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_ARTIFACT_ID=colormath 2 | POM_NAME=Colormath 3 | -------------------------------------------------------------------------------- /colormath/src/commonMain/kotlin/com/github/ajalt/colormath/Color.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.colormath 2 | 3 | import com.github.ajalt.colormath.model.* 4 | import com.github.ajalt.colormath.model.LCHuvColorSpaces.LCHuv65 5 | 6 | /** 7 | * A color that can be converted to other representations. 8 | * 9 | * The conversion functions can return the object they're called on if it is already in the 10 | * correct format. 11 | * 12 | * Note that there is not a direct conversion between every pair of representations. In those cases, 13 | * the values may be converted through one or more intermediate representations. 14 | * 15 | * All colors have an [alpha] value, which is the opacity of the color as a fraction between 0 and 16 | * 1. The [alpha] will be 1 if the value is unspecified or the color model doesn't 17 | * support transparency. 18 | */ 19 | interface Color { 20 | /** The opacity of this color, in the range `[0, 1]`, or [NaN][Float.NaN] if the opacity is unspecified */ 21 | val alpha: Float 22 | 23 | /** The color space describing this color */ 24 | val space: ColorSpace<*> 25 | 26 | /** Convert this color to [sRGB][RGBColorSpaces.SRGB] */ 27 | fun toSRGB(): RGB 28 | 29 | /** Convert this color to HSL */ 30 | fun toHSL(): HSL = toSRGB().toHSL() 31 | 32 | /** Convert this color to HSV */ 33 | fun toHSV(): HSV = toSRGB().toHSV() 34 | 35 | /** Convert this color to a 16-color ANSI code */ 36 | fun toAnsi16(): Ansi16 = toSRGB().toAnsi16() 37 | 38 | /** Convert this color to a 256-color ANSI code */ 39 | fun toAnsi256(): Ansi256 = toSRGB().toAnsi256() 40 | 41 | /** Convert this color to device-independent CMYK */ 42 | fun toCMYK(): CMYK = toSRGB().toCMYK() 43 | 44 | /** Convert this color to CIE XYZ */ 45 | fun toXYZ(): XYZ = toSRGB().toXYZ() 46 | 47 | /** Convert this color to CIE LAB */ 48 | fun toLAB(): LAB = toXYZ().toLAB() 49 | 50 | /** Convert this color to CIE LCh(ab) */ 51 | fun toLCHab(): LCHab = toLAB().toLCHab() 52 | 53 | /** Convert this color to CIE LUV */ 54 | fun toLUV(): LUV = toXYZ().toLUV() 55 | 56 | /** Convert this color to CIE LCh(uv) */ 57 | fun toLCHuv(): LCHuv = toLUV().toLCHuv() 58 | 59 | /** Convert this color to HWB */ 60 | fun toHWB(): HWB = toSRGB().toHWB() 61 | 62 | /** Convert this color to Oklab */ 63 | fun toOklab(): Oklab = toXYZ().toOklab() 64 | 65 | /** Convert this color to Oklch */ 66 | fun toOklch(): Oklch = toOklab().toOklch() 67 | 68 | /** Convert this color to JzAzBz */ 69 | fun toJzAzBz(): JzAzBz = toXYZ().toJzAzBz() 70 | 71 | /** Convert this color to JzCzHz */ 72 | fun toJzCzHz(): JzCzHz = toJzAzBz().toJzCzHz() 73 | 74 | /** Convert this color to ICtCp */ 75 | fun toICtCp(): ICtCp = toXYZ().toICtCp() 76 | 77 | /** Convert this color to HSLuv */ 78 | fun toHSLuv(): HSLuv = convertTo(LCHuv65).toHSLuv() 79 | 80 | /** Convert this color to HPLuv */ 81 | fun toHPLuv(): HPLuv = convertTo(LCHuv65).toHPLuv() 82 | 83 | /** Create a [FloatArray] containing all components of this color, with the [alpha] as the last component */ 84 | fun toArray(): FloatArray 85 | 86 | /** 87 | * Return a copy of this color with all component values in their reference range. 88 | * 89 | * No gamut mapping is performed: out-of-gamut values are truncated. 90 | */ 91 | fun clamp(): Color { 92 | val values = toArray() 93 | var clamped = false 94 | for (i in values.indices) { 95 | val info = space.components[i] 96 | if (values[i] !in info.min..info.max) { 97 | clamped = true 98 | values[i] = when { 99 | info.isPolar -> values[i] % 360 100 | else -> values[i].coerceIn(info.min, info.max) 101 | } 102 | } 103 | } 104 | return if (clamped) space.create(values) else this 105 | } 106 | 107 | companion object // enables extensions on the interface 108 | } 109 | 110 | /** 111 | * Convert this color to a given [space]. 112 | */ 113 | fun Color.convertTo(space: ColorSpace): T = space.convert(this) 114 | -------------------------------------------------------------------------------- /colormath/src/commonMain/kotlin/com/github/ajalt/colormath/ColorSpace.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.colormath 2 | 3 | interface ColorSpace { 4 | /** The name of this color */ 5 | val name: String 6 | 7 | /** 8 | * Information about the components (sometimes called channels) of this color. 9 | * 10 | * The list of components is the same size and order as the values returned from [Color.toArray] 11 | */ 12 | val components: List 13 | 14 | /** Convert a [color] to this space */ 15 | fun convert(color: Color): T 16 | 17 | /** 18 | * Create a new instance of a color in this space from an array of [components]. 19 | * 20 | * The [components] array must have a size equal to either the size of this 21 | * [space's components][ColorSpace.components], or one less, 22 | * in which case alpha will default to 1. 23 | */ 24 | fun create(components: FloatArray): T 25 | } 26 | 27 | class ColorComponentInfo( 28 | /** 29 | * The name of this component 30 | */ 31 | val name: String, 32 | 33 | /** 34 | * `true` if this component uses polar coordinates (e.g. a hue), and `false` if it's 35 | * rectangular. 36 | */ 37 | val isPolar: Boolean, 38 | 39 | /** 40 | * The minimum of the reference range for this component. 41 | * 42 | * Note that some color models have components with no strict limits. For those components, this 43 | * is the limit of typical values. 44 | */ 45 | val min: Float, 46 | 47 | /** 48 | * The maximum of the reference range for this component. 49 | * 50 | * Note that some color models have components with no strict limits. For those components, this 51 | * is the limit of typical values. 52 | */ 53 | val max: Float, 54 | ) { 55 | @Deprecated( 56 | "Use the constructor with a max and min", 57 | ReplaceWith("ColorComponentInfo(name, isPolar, 0f, 1f)"), 58 | ) 59 | constructor(name: String, isPolar: Boolean) : this(name, isPolar, 0f, 1f) 60 | 61 | init { 62 | require(min <= max) { "min must be less than or equal to max" } 63 | } 64 | } 65 | 66 | /** A color space that is defined with a reference [whitePoint]. */ 67 | interface WhitePointColorSpace : ColorSpace { 68 | /** The white point that colors in this space are calculated relative to. */ 69 | val whitePoint: WhitePoint 70 | } 71 | -------------------------------------------------------------------------------- /colormath/src/commonMain/kotlin/com/github/ajalt/colormath/HueColor.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.colormath 2 | 3 | import com.github.ajalt.colormath.internal.degToGrad 4 | import com.github.ajalt.colormath.internal.degToRad 5 | import com.github.ajalt.colormath.internal.degToTurns 6 | 7 | interface HueColor : Color { 8 | /** The hue, as degrees in the range `[0, 360)` */ 9 | val h: Float 10 | } 11 | 12 | /** Convert this color's hue to gradians (360° == 400 gradians) */ 13 | fun HueColor.hueAsGrad(): Float = h.degToGrad() 14 | 15 | /** Convert this color's hue to radians (360° == 2π radians) */ 16 | fun HueColor.hueAsRad(): Float = h.degToRad() 17 | 18 | /** Convert this color's hue to turns (360° == 1 turn) */ 19 | fun HueColor.hueAsTurns(): Float = h.degToTurns() 20 | 21 | /** 22 | * If this color's hue is [NaN][Float.NaN] (meaning the hue is undefined), return [whenNaN]. 23 | * Otherwise, return the hue unchanged. 24 | */ 25 | fun HueColor.hueOr(whenNaN: Number): Float = if (h.isNaN()) whenNaN.toFloat() else h 26 | -------------------------------------------------------------------------------- /colormath/src/commonMain/kotlin/com/github/ajalt/colormath/WhitePoint.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.colormath 2 | 3 | import com.github.ajalt.colormath.model.xyY 4 | 5 | /** A named chromaticity */ 6 | data class WhitePoint(val name: String, val chromaticity: xyY) { 7 | override fun toString(): String = name 8 | } 9 | 10 | /** 11 | * Standard CIE Illuminants under the 2° observer 12 | */ 13 | object Illuminant { 14 | /** 15 | * CIE 1931 2° Standard Illuminant A 16 | * 17 | * This illuminant has a CCT of 2856K 18 | */ 19 | val A = WhitePoint("A", xyY(0.44758, 0.40745)) 20 | 21 | /** 22 | * CIE 1931 2° Standard Illuminant B 23 | * 24 | * This illuminant has a CCT of 4874K 25 | */ 26 | val B = WhitePoint("B", xyY(0.34842, 0.35161)) 27 | 28 | /** 29 | * CIE 1931 2° Standard Illuminant C 30 | * 31 | * This illuminant has a CCT of 6774K 32 | */ 33 | val C = WhitePoint("C", xyY(0.31006, 0.31616)) 34 | 35 | /** 36 | * CIE 1931 2° Standard Illuminant D50 37 | * 38 | * This illuminant has a CCT of 5003K 39 | */ 40 | val D50 = WhitePoint("D50", xyY(0.34570, 0.35850)) 41 | 42 | /** 43 | * CIE 1931 2° Standard Illuminant D55 44 | * 45 | * This illuminant has a CCT of 5503K 46 | */ 47 | val D55 = WhitePoint("D55", xyY(0.33243, 0.34744)) 48 | 49 | /** 50 | * CIE 1931 2° Standard Illuminant D65 51 | * 52 | * This illuminant has a CCT of 6504K 53 | */ 54 | val D65 = WhitePoint("D65", xyY(0.31270, 0.32900)) 55 | 56 | /** 57 | * CIE 1931 2° Standard Illuminant D75 58 | * 59 | * This illuminant has a CCT of 7504K 60 | */ 61 | val D75 = WhitePoint("D75", xyY(0.29903, 0.31488)) 62 | 63 | /** 64 | * CIE 1931 2° Standard Illuminant E 65 | * 66 | * This illuminant has a CCT of 5454K 67 | */ 68 | val E = WhitePoint("E", xyY(1.0 / 3.0, 1.0 / 3.0)) 69 | } 70 | -------------------------------------------------------------------------------- /colormath/src/commonMain/kotlin/com/github/ajalt/colormath/calculate/Contrast.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.colormath.calculate 2 | 3 | import com.github.ajalt.colormath.Color 4 | import com.github.ajalt.colormath.convertTo 5 | import com.github.ajalt.colormath.model.RGB 6 | import com.github.ajalt.colormath.model.RGBColorSpaces 7 | import kotlin.math.max 8 | import kotlin.math.min 9 | 10 | /** 11 | * Calculate the relative luminance of this color according to the 12 | * [Web Content Accessibility Guidelines](https://www.w3.org/TR/WCAG21/#dfn-relative-luminance) 13 | * 14 | * @return The relative luminance of this color, which ranges from 0 to 1 for in-gamut sRGB colors 15 | */ 16 | fun Color.wcagLuminance(): Float { 17 | val (rs, gs, bs) = convertTo(RGBColorSpaces.LinearSRGB) 18 | return (0.2126 * rs + 0.7152 * gs + 0.0722 * bs).toFloat() 19 | } 20 | 21 | /** 22 | * Calculate the contrast ration of this color with [other] according to the 23 | * [Web Content Accessibility Guidelines](https://www.w3.org/TR/WCAG21/#dfn-contrast-ratio) 24 | * 25 | * @return The contrast ratio of this color with [other], which ranges from 1 to 21 for in-gamut sRGB colors 26 | */ 27 | fun Color.wcagContrastRatio(other: Color): Float { 28 | val l = wcagLuminance() 29 | val r = other.wcagLuminance() 30 | return ((max(l, r) + 0.05) / (min(l, r) + 0.05)).toFloat() 31 | } 32 | 33 | /** 34 | * Return the [color][colors] with the highest [contrast ratio][wcagContrastRatio] against this color. 35 | * 36 | * This implements the `color-contrast` functionality specified in the 37 | * [CSS Color 5 Spec](https://www.w3.org/TR/css-color-5/#colorcontrast) 38 | */ 39 | fun Color.mostContrasting(vararg colors: Color): Color { 40 | require(colors.isNotEmpty()) { "colors cannot be empty" } 41 | return colors.maxByOrNull { wcagContrastRatio(it) }!! 42 | } 43 | 44 | /** 45 | * Return the first [color][colors] with a [contrast ratio][wcagContrastRatio] greater or equal to the [targetContrast] 46 | * against this color, or `null` if no color meets the target. 47 | * 48 | * This implements the `color-contrast` functionality specified in the 49 | * [CSS Color 5 Spec](https://www.w3.org/TR/css-color-5/#colorcontrast) 50 | */ 51 | fun Color.firstWithContrastOrNull(vararg colors: Color, targetContrast: Float): Color? { 52 | require(colors.isNotEmpty()) { "colors cannot be empty" } 53 | return colors.firstOrNull { wcagContrastRatio(it) >= targetContrast } 54 | } 55 | 56 | /** 57 | * Return the first [color][colors] with a [contrast ratio][wcagContrastRatio] exceeding the [targetContrast] against 58 | * this color. If no color meets the target, black or white will be returned, whichever has the most contrast. 59 | * 60 | * This implements the `color-contrast` functionality specified in the 61 | * [CSS Color 5 Spec](https://www.w3.org/TR/css-color-5/#colorcontrast) 62 | */ 63 | fun Color.firstWithContrast(vararg colors: Color, targetContrast: Float): Color { 64 | require(colors.isNotEmpty()) { "colors cannot be empty" } 65 | return colors.firstOrNull { wcagContrastRatio(it) >= targetContrast } 66 | ?: listOf(RGB(0f, 0f, 0f), RGB(1f, 1f, 1f)).maxByOrNull { wcagContrastRatio(it) }!! 67 | } 68 | -------------------------------------------------------------------------------- /colormath/src/commonMain/kotlin/com/github/ajalt/colormath/calculate/Gamut.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.colormath.calculate 2 | 3 | import com.github.ajalt.colormath.Color 4 | 5 | /** 6 | * Return `true` if all channels of this color, when converted to sRGB, lie in the range `[0, 1]` 7 | */ 8 | fun Color.isInSRGBGamut(): Boolean = toSRGB().let { 9 | it.r in 0f..1f && it.g in 0f..1f && it.b in 0f..1f 10 | } 11 | -------------------------------------------------------------------------------- /colormath/src/commonMain/kotlin/com/github/ajalt/colormath/internal/ColorSpaceUtils.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.colormath.internal 2 | 3 | import com.github.ajalt.colormath.Color 4 | import com.github.ajalt.colormath.ColorComponentInfo 5 | import com.github.ajalt.colormath.ColorSpace 6 | import com.github.ajalt.colormath.Illuminant.D65 7 | import com.github.ajalt.colormath.WhitePointColorSpace 8 | import com.github.ajalt.colormath.model.XYZColorSpace 9 | 10 | 11 | internal inline fun ColorSpace.withValidComps( 12 | components: FloatArray, 13 | block: (FloatArray) -> T, 14 | ): T { 15 | val size = this.components.size 16 | require(components.size in (size - 1)..size) { 17 | "Invalid component array length: ${components.size}, expected ${size - 1} or $size" 18 | } 19 | return block(components) 20 | } 21 | 22 | internal inline fun ColorSpace.doCreate( 23 | components: FloatArray, 24 | init: (Float, Float, Float, Float) -> T, 25 | ): T { 26 | return withValidComps(components) { 27 | init(components[0], components[1], components[2], components.getOrElse(3) { 1f }) 28 | } 29 | } 30 | 31 | internal inline fun WhitePointColorSpace.adaptToThis( 32 | color: Color, 33 | convert: (Color) -> T, 34 | ): T { 35 | return if (((color.space as? WhitePointColorSpace<*>)?.whitePoint 36 | ?: D65) == whitePoint 37 | ) convert(color) 38 | else convert(color.toXYZ().adaptTo(XYZColorSpace(whitePoint))) 39 | } 40 | 41 | private val alphaInfo = ColorComponentInfo("alpha", false, 0f, 1f) 42 | 43 | internal fun componentInfoList(vararg c: ColorComponentInfo): List { 44 | return listOf(*c, alphaInfo) 45 | } 46 | 47 | internal fun threeComponentInfo( 48 | n1: String, l1: Float, r1: Float, 49 | n2: String, l2: Float, r2: Float, 50 | n3: String, l3: Float, r3: Float, 51 | ): List { 52 | return componentInfoList( 53 | ColorComponentInfo(n1, false, l1, r1), 54 | ColorComponentInfo(n2, false, l2, r2), 55 | ColorComponentInfo(n3, false, l3, r3), 56 | ) 57 | } 58 | 59 | internal fun zeroOneComponentInfo(name: String): List { 60 | return buildList { 61 | name.mapTo(this) { ColorComponentInfo(it.toString(), false, 0f, 1f) } 62 | add(alphaInfo) 63 | } 64 | } 65 | 66 | internal fun polarComponentInfo( 67 | name: String, l: Float, r: Float, 68 | ): List { 69 | return buildList { 70 | name.mapTo(this) { 71 | ColorComponentInfo( 72 | name = it.toString(), 73 | isPolar = it == 'H', 74 | min = if (it == 'H') 0f else l, 75 | max = if (it == 'H') 360f else r 76 | ) 77 | } 78 | add(alphaInfo) 79 | } 80 | } 81 | 82 | internal inline fun T.clamp3( 83 | v1: Float, 84 | v2: Float, 85 | v3: Float, 86 | alpha: Float, 87 | copy: (v1: Float, v2: Float, v3: Float, alpha: Float) -> T, 88 | ): T { 89 | val (c1, c2, c3) = space.components 90 | return when { 91 | v1 in c1.min..c1.max 92 | && v2 in c2.min..c2.max 93 | && v3 in c3.min..c3.max 94 | && alpha in 0f..1f -> this 95 | 96 | else -> copy( 97 | v1.coerceIn(c1.min, c1.max), 98 | v2.coerceIn(c2.min, c2.max), 99 | v3.coerceIn(c3.min, c3.max), 100 | alpha.coerceIn(0f, 1f) 101 | ) 102 | } 103 | } 104 | 105 | internal inline fun T.clampLeadingHue( 106 | v1: Float, 107 | v2: Float, 108 | v3: Float, 109 | alpha: Float, 110 | copy: (v1: Float, v2: Float, v3: Float, alpha: Float) -> T, 111 | ): T { 112 | val (c1, c2, c3) = space.components 113 | return when { 114 | v1 in c1.min..c1.max 115 | && v2 in c2.min..c2.max 116 | && v3 in c3.min..c3.max 117 | && alpha in 0f..1f -> this 118 | 119 | else -> copy( 120 | v1 % 360, 121 | v2.coerceIn(c2.min, c2.max), 122 | v3.coerceIn(c3.min, c3.max), 123 | alpha.coerceIn(0f, 1f) 124 | ) 125 | } 126 | } 127 | 128 | internal inline fun T.clampTrailingHue( 129 | v1: Float, 130 | v2: Float, 131 | v3: Float, 132 | alpha: Float, 133 | copy: (v1: Float, v2: Float, v3: Float, alpha: Float) -> T, 134 | ): T { 135 | val (c1, c2, c3) = space.components 136 | return when { 137 | v1 in c1.min..c1.max 138 | && v2 in c2.min..c2.max 139 | && v3 in c3.min..c3.max 140 | && alpha in 0f..1f -> this 141 | 142 | else -> copy( 143 | v1.coerceIn(c1.min, c1.max), 144 | v2.coerceIn(c2.min, c2.max), 145 | v3 % 360, 146 | alpha.coerceIn(0f, 1f) 147 | ) 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /colormath/src/commonMain/kotlin/com/github/ajalt/colormath/internal/Constants.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.colormath.internal 2 | 3 | // Constants used in LAB and LUV conversions. 4 | // http://www.brucelindbloom.com/index.html?LContinuity.html 5 | /** ϵ = (6/29)^3 */ 6 | internal const val CIE_E = 216.0 / 24389.0 7 | 8 | /** κ = (29/3)^3 */ 9 | internal const val CIE_K = 24389.0 / 27.0 10 | 11 | /** ϵ × κ */ 12 | internal const val CIE_E_times_K = 8.0 13 | 14 | /** The CIECAM02 transform matrix for XYZ -> LMS */ 15 | // https://en.wikipedia.org/wiki/CIECAM02#CAT02 16 | internal val CAT02_XYZ_TO_LMS = Matrix( 17 | +0.7328f, +0.4296f, -0.1624f, 18 | -0.7036f, +1.6975f, +0.0061f, 19 | +0.0030f, +0.0136f, +0.9834f, 20 | ) 21 | 22 | internal val CAT02_LMS_TO_XYZ = CAT02_XYZ_TO_LMS.inverse() 23 | -------------------------------------------------------------------------------- /colormath/src/commonMain/kotlin/com/github/ajalt/colormath/internal/InternalMath.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.colormath.internal 2 | 3 | import kotlin.math.* 4 | 5 | internal fun Float.degToRad(): Float = toDouble().degToRad().toFloat() 6 | internal fun Float.radToDeg(): Float = toDouble().radToDeg().toFloat() 7 | internal fun Float.gradToDeg(): Float = this * .9f 8 | internal fun Float.turnToDeg(): Float = this * 360f 9 | internal fun Float.degToGrad(): Float = this * 200f / 180f 10 | internal fun Float.degToTurns(): Float = this / 360f 11 | 12 | internal fun Double.radToDeg(): Double = (this * 180.0 / PI) 13 | internal fun Double.degToRad(): Double = (this * PI / 180.0) 14 | 15 | internal fun cosDeg(deg: Double) = cos(deg.degToRad()) 16 | internal fun sinDeg(deg: Double) = sin(deg.degToRad()) 17 | 18 | 19 | // formula from https://www.w3.org/TR/css-color-4/#hue-interpolation 20 | /** Return this value shifted to lie in [0, 360] */ 21 | internal fun Float.normalizeDeg(): Float = ((this % 360f) + 360f) % 360f 22 | internal fun Double.normalizeDeg(): Double = ((this % 360.0) + 360.0) % 360.0 23 | 24 | internal fun Float.nanToOne(): Float = if (isNaN()) 1f else this 25 | 26 | // Used for LAB <-> LCHab, LUV <-> LCHuv, Oklab <-> Oklch, JAB <-> JCH 27 | // https://www.w3.org/TR/css-color-4/#lab-to-lch 28 | // https://bottosson.github.io/posts/oklab/#the-oklab-color-space 29 | // https://en.wikipedia.org/wiki/CIELUV#Cylindrical_representation_.28CIELCH.29 30 | internal inline fun toPolarModel(a: Float, b: Float, block: (c: Float, h: Float) -> T): T { 31 | val c = sqrt(a * a + b * b) 32 | val h = if (c > -1e-7 && c < 1e-7) Float.NaN else atan2(b, a).radToDeg() 33 | return block(c, h.normalizeDeg()) 34 | } 35 | 36 | internal inline fun fromPolarModel(c: Float, h: Float, block: (a: Float, b: Float) -> T): T { 37 | val hDegrees = if (h.isNaN()) 0f else h.degToRad() 38 | val a = c * cos(hDegrees) 39 | val b = c * sin(hDegrees) 40 | return block(a, b) 41 | } 42 | 43 | /** 44 | * return `sign(a) * |a|^p`, which avoids NaN when `this` is negative 45 | */ 46 | internal fun Double.spow(p: Double): Double = absoluteValue.pow(p).withSign(this) 47 | internal fun Float.spow(p: Double): Double = toDouble().spow(p) 48 | 49 | internal fun sqrtSumSq(a: Float, b: Float, c: Float): Float = sqrt(a.pow(2) + b.pow(2) + c.pow(2)) 50 | internal fun sqrtSumSq(a: Double, b: Double): Double = sqrt(a.pow(2) + b.pow(2)) 51 | internal fun sqrtSumSq(a: Double, b: Double, c: Double): Double = 52 | sqrt(a.pow(2) + b.pow(2) + c.pow(2)) 53 | 54 | internal fun scaleRange(l1: Float, r1: Float, l2: Float, r2: Float, t: Float): Float { 55 | return if (r1 == l1) t else (r2 - l2) * (t - l1) / (r1 - l1) + l2 56 | } 57 | -------------------------------------------------------------------------------- /colormath/src/commonMain/kotlin/com/github/ajalt/colormath/internal/Matrix.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("LocalVariableName") 2 | 3 | package com.github.ajalt.colormath.internal 4 | 5 | import kotlin.jvm.JvmInline 6 | 7 | @JvmInline 8 | internal value class Matrix(val rowMajor: FloatArray) { 9 | constructor( 10 | v00: Float, v10: Float, v20: Float, 11 | v01: Float, v11: Float, v21: Float, 12 | v02: Float, v12: Float, v22: Float, 13 | ) : this( 14 | floatArrayOf( 15 | v00, v10, v20, 16 | v01, v11, v21, 17 | v02, v12, v22, 18 | ) 19 | ) 20 | 21 | fun copy() = Matrix(rowMajor.copyOf()) 22 | 23 | operator fun get(x: Int, y: Int): Float = rowMajor[y * 3 + x] 24 | 25 | operator fun set(x: Int, y: Int, value: Float) { 26 | rowMajor[y * 3 + x] = value 27 | } 28 | 29 | operator fun set(x: Int, y: Int, value: Double) = set(x, y, value.toFloat()) 30 | 31 | override fun toString(): String { 32 | return """Mat3( 33 | | ${get(0, 0)}, ${get(1, 0)}, ${get(2, 0)}, 34 | | ${get(0, 1)}, ${get(1, 1)}, ${get(2, 1)}, 35 | | ${get(0, 2)}, ${get(1, 2)}, ${get(2, 2)}, 36 | |) 37 | """.trimMargin() 38 | } 39 | } 40 | 41 | internal fun Matrix.inverse(inPlace: Boolean = false): Matrix { 42 | val a = get(0, 0).toDouble() 43 | val b = get(1, 0).toDouble() 44 | val c = get(2, 0).toDouble() 45 | val d = get(0, 1).toDouble() 46 | val e = get(1, 1).toDouble() 47 | val f = get(2, 1).toDouble() 48 | val g = get(0, 2).toDouble() 49 | val h = get(1, 2).toDouble() 50 | val i = get(2, 2).toDouble() 51 | 52 | val A = e * i - h * f 53 | val B = h * c - b * i 54 | val C = b * f - e * c 55 | 56 | val det = a * A + d * B + g * C 57 | 58 | val out = if (inPlace) this else copy() 59 | out[0, 0] = A / det 60 | out[0, 1] = (g * f - d * i) / det 61 | out[0, 2] = (d * h - g * e) / det 62 | out[1, 0] = B / det 63 | out[1, 1] = (a * i - g * c) / det 64 | out[1, 2] = (g * b - a * h) / det 65 | out[2, 0] = C / det 66 | out[2, 1] = (d * c - a * f) / det 67 | out[2, 2] = (a * e - d * b) / det 68 | return out 69 | } 70 | 71 | internal inline fun Matrix.dot( 72 | v0: Float, 73 | v1: Float, 74 | v2: Float, 75 | block: (Float, Float, Float) -> T, 76 | ): T { 77 | return block( 78 | get(0, 0) * v0 + get(1, 0) * v1 + get(2, 0) * v2, 79 | get(0, 1) * v0 + get(1, 1) * v1 + get(2, 1) * v2, 80 | get(0, 2) * v0 + get(1, 2) * v1 + get(2, 2) * v2, 81 | ) 82 | } 83 | 84 | internal fun Matrix.dot(v0: Float, v1: Float, v2: Float): Vector = dot(v0, v1, v2, ::Vector) 85 | 86 | internal fun Matrix.dot(other: Matrix): Matrix { 87 | fun f(x: Int, y: Int): Float { 88 | return this[0, y] * other[x, 0] + this[1, y] * other[x, 1] + this[2, y] * other[x, 2] 89 | } 90 | 91 | return Matrix( 92 | f(0, 0), f(1, 0), f(2, 0), 93 | f(0, 1), f(1, 1), f(2, 1), 94 | f(0, 2), f(1, 2), f(2, 2), 95 | ) 96 | } 97 | 98 | /** Return the dot product of this matrix with a diagonal matrix, with the three arguments as the diagonal */ 99 | internal fun Matrix.dotDiagonal(v0: Float, v1: Float, v2: Float): Matrix { 100 | return Matrix( 101 | get(0, 0) * v0, get(1, 0) * v1, get(2, 0) * v2, 102 | get(0, 1) * v0, get(1, 1) * v1, get(2, 1) * v2, 103 | get(0, 2) * v0, get(1, 2) * v1, get(2, 2) * v2, 104 | ) 105 | } 106 | 107 | @JvmInline 108 | internal value class Vector(val values: FloatArray) { 109 | constructor(v0: Float, v1: Float, v2: Float) : this(floatArrayOf(v0, v1, v2)) 110 | 111 | operator fun get(i: Int): Float = values[i] 112 | 113 | operator fun set(i: Int, value: Float) { 114 | values[i] = value 115 | } 116 | 117 | val r get() = values[0] 118 | val g get() = values[1] 119 | val b get() = values[2] 120 | 121 | val x get() = values[0] 122 | val y get() = values[1] 123 | val z get() = values[2] 124 | 125 | val l get() = values[0] 126 | val m get() = values[1] 127 | val s get() = values[2] 128 | 129 | operator fun component1() = values[0] 130 | operator fun component2() = values[1] 131 | operator fun component3() = values[2] 132 | } 133 | 134 | internal fun Matrix.scalarDiv(x: Float, inPlace: Boolean = false): Matrix { 135 | val out = (if (inPlace) this else copy()).rowMajor 136 | for (i in out.indices) { 137 | out[i] /= x 138 | } 139 | return Matrix(out) 140 | } 141 | -------------------------------------------------------------------------------- /colormath/src/commonMain/kotlin/com/github/ajalt/colormath/model/Ansi16.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.colormath.model 2 | 3 | import com.github.ajalt.colormath.Color 4 | import com.github.ajalt.colormath.ColorComponentInfo 5 | import com.github.ajalt.colormath.ColorSpace 6 | import com.github.ajalt.colormath.internal.componentInfoList 7 | import com.github.ajalt.colormath.internal.withValidComps 8 | 9 | /** 10 | * A 4-bit, 16 color ANSI code 11 | * 12 | * Conversions to [Ansi16] will always use foreground color codes. Conversions from [Ansi16] to [RGB] use the Windows XP 13 | * Console palette. 14 | * 15 | * ## Valid codes 16 | * 17 | * | Color | Foreground | Background | Bright FG | Bright BG | 18 | * | ------ | ---------- | ---------- | --------- | --------- | 19 | * | black | 30 | 40 | 90 | 100 | 20 | * | red | 31 | 41 | 91 | 101 | 21 | * | green | 32 | 42 | 92 | 102 | 22 | * | yellow | 33 | 43 | 93 | 103 | 23 | * | blue | 34 | 44 | 94 | 104 | 24 | * | purple | 35 | 45 | 95 | 105 | 25 | * | cyan | 36 | 46 | 96 | 106 | 26 | * | white | 37 | 47 | 97 | 107 | 27 | */ 28 | data class Ansi16(val code: Int) : Color { 29 | /** Default constructors for the [Ansi16] color model. */ 30 | companion object : ColorSpace { 31 | override val name: String get() = "Ansi16" 32 | override val components: List = componentInfoList( 33 | ColorComponentInfo("code", false, 30f, 107f), 34 | ) 35 | 36 | override fun convert(color: Color): Ansi16 = color.toAnsi16() 37 | override fun create(components: FloatArray): Ansi16 = withValidComps(components) { 38 | Ansi16(it[0].toInt()) 39 | } 40 | } 41 | 42 | override val alpha: Float get() = 1f 43 | override val space: ColorSpace get() = Ansi16 44 | 45 | override fun toSRGB(): RGB { 46 | // grayscale 47 | when (code) { 48 | 30, 40 -> return RGB(0f, 0f, 0f) 49 | 90, 100 -> return RGB.from255(128, 128, 128) 50 | 37, 47 -> return RGB.from255(192, 192, 192) 51 | 97, 107 -> return RGB(1.0f, 1.0f, 1.0f) 52 | } 53 | 54 | // color 55 | val color = code % 10 56 | val mul = if (code > 50) 1f else 0.5f 57 | val r = ((color % 2) * mul) 58 | val g = (((color / 2) % 2) * mul) 59 | val b = (((color / 4) % 2) * mul) 60 | 61 | return RGB(r, g, b) 62 | } 63 | 64 | override fun toAnsi256() = when { 65 | code >= 90 -> Ansi256(code - 90 + 8) 66 | else -> Ansi256(code - 30) 67 | } 68 | 69 | override fun toAnsi16() = this 70 | override fun toArray(): FloatArray = floatArrayOf(code.toFloat(), alpha) 71 | override fun clamp(): Color { 72 | return when { 73 | code < 30 -> Ansi16(30) 74 | code in 38..39 -> Ansi16(40) 75 | code in 48..89 -> Ansi16(40) 76 | code in 98..99 -> Ansi16(100) 77 | code > 107 -> Ansi16(107) 78 | else -> this 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /colormath/src/commonMain/kotlin/com/github/ajalt/colormath/model/Ansi256.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.colormath.model 2 | 3 | import com.github.ajalt.colormath.Color 4 | import com.github.ajalt.colormath.ColorComponentInfo 5 | import com.github.ajalt.colormath.ColorSpace 6 | import com.github.ajalt.colormath.internal.componentInfoList 7 | import com.github.ajalt.colormath.internal.withValidComps 8 | import kotlin.math.floor 9 | 10 | /** 11 | * An 8-bit, 256 color ANSI code 12 | * 13 | * Unlike [Ansi16], these codes don't have separate values for foreground and background. 14 | * 15 | * ## Valid codes 16 | * 17 | * - `0-7`: Standard colors corresponding to [Ansi16] codes `30-37` 18 | * - `8-15`: Bright colors corresponding to [Ansi16] codes `90-97` 19 | * - `16-231`: 216 colors encoded in a 6×6×6 cube 20 | * - `232-255`: Grayscale colors 21 | */ 22 | data class Ansi256(val code: Int) : Color { 23 | /** Default constructors for the [Ansi256] color model. */ 24 | companion object : ColorSpace { 25 | override val name: String get() = "Ansi256" 26 | override val components: List = componentInfoList( 27 | ColorComponentInfo("code", false, 0f, 255f), 28 | ) 29 | 30 | override fun convert(color: Color): Ansi256 = color.toAnsi256() 31 | override fun create(components: FloatArray): Ansi256 = withValidComps(components) { 32 | Ansi256(it[0].toInt()) 33 | } 34 | } 35 | 36 | 37 | override val alpha: Float get() = 1f 38 | override val space: ColorSpace get() = Ansi256 39 | 40 | override fun toSRGB(): RGB { 41 | // ansi16 colors 42 | if (code < 16) return toAnsi16().toSRGB() 43 | 44 | // grayscale 45 | if (code >= 232) { 46 | val c = (code - 232) * 10 + 8 47 | return RGB.from255(c, c, c) 48 | } 49 | 50 | // color 51 | val c = code - 16 52 | val rem = c % 36 53 | val r = floor(c / 36.0) / 5.0 54 | val g = floor(rem / 6.0) / 5.0 55 | val b = (rem % 6) / 5.0 56 | return RGB(r, g, b) 57 | } 58 | 59 | // 0-7 are standard ansi16 colors 60 | // 8-15 are bright ansi16 colors 61 | override fun toAnsi16() = when { 62 | code < 8 -> Ansi16(code + 30) 63 | code < 16 -> Ansi16(code - 8 + 90) 64 | else -> toSRGB().toAnsi16() 65 | } 66 | 67 | override fun toAnsi256() = this 68 | override fun toArray(): FloatArray = floatArrayOf(code.toFloat(), alpha) 69 | override fun clamp(): Color { 70 | return when { 71 | code < 0 -> Ansi256(0) 72 | code > 255 -> Ansi256(255) 73 | else -> this 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /colormath/src/commonMain/kotlin/com/github/ajalt/colormath/model/CMYK.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.colormath.model 2 | 3 | import com.github.ajalt.colormath.Color 4 | import com.github.ajalt.colormath.ColorComponentInfo 5 | import com.github.ajalt.colormath.ColorSpace 6 | import com.github.ajalt.colormath.internal.withValidComps 7 | import com.github.ajalt.colormath.internal.zeroOneComponentInfo 8 | 9 | /** 10 | * A color in the CMYK (cyan, magenta, yellow, and key) color model. 11 | * 12 | * Conversions to and from this model use the device-independent ("naive") formulas. 13 | * 14 | * | Component | Description | Range | 15 | * | ---------- | ----------- | -------- | 16 | * | [c] | cyan | `[0, 1]` | 17 | * | [m] | magenta | `[0, 1]` | 18 | * | [y] | yellow | `[0, 1]` | 19 | * | [k] | key / black | `[0, 1]` | 20 | */ 21 | data class CMYK( 22 | val c: Float, 23 | val m: Float, 24 | val y: Float, 25 | val k: Float, 26 | override val alpha: Float = 1f, 27 | ) : Color { 28 | /** Default constructors for the [CMYK] color model. */ 29 | companion object : ColorSpace { 30 | override val name: String get() = "CMYK" 31 | override val components: List = zeroOneComponentInfo("CMYK") 32 | override fun convert(color: Color): CMYK = color.toCMYK() 33 | override fun create(components: FloatArray): CMYK = withValidComps(components) { 34 | CMYK(it[0], it[1], it[2], it[3], it.getOrElse(4) { 1f }) 35 | } 36 | } 37 | 38 | constructor (c: Number, m: Number, y: Number, k: Number, alpha: Number = 1f) 39 | : this(c.toFloat(), m.toFloat(), y.toFloat(), k.toFloat(), alpha.toFloat()) 40 | 41 | /** 42 | * Construct a CMYK instance from Int values, with the color channels as percentages in the range `[0, 100]`. 43 | */ 44 | constructor(c: Int, m: Int, y: Int, k: Int, alpha: Float = 1f) 45 | : this(c / 100f, m / 100f, y / 100f, k / 100f, alpha) 46 | 47 | override val space: ColorSpace get() = CMYK 48 | 49 | override fun toSRGB(): RGB { 50 | val r = (1 - c) * (1 - k) 51 | val g = (1 - m) * (1 - k) 52 | val b = (1 - y) * (1 - k) 53 | return RGB(r, g, b, alpha) 54 | } 55 | 56 | override fun toCMYK() = this 57 | override fun toArray(): FloatArray = floatArrayOf(c, m, y, k, alpha) 58 | override fun clamp(): Color { 59 | return when { 60 | c in 0f..1f && m in 0f..1f && y in 0f..1f && k in 0f..1f && alpha in 0f..1f -> this 61 | else -> copy( 62 | c = c.coerceIn(0f, 1f), 63 | m = m.coerceIn(0f, 1f), 64 | y = y.coerceIn(0f, 1f), 65 | k = k.coerceIn(0f, 1f), 66 | alpha = alpha.coerceIn(0f, 1f) 67 | ) 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /colormath/src/commonMain/kotlin/com/github/ajalt/colormath/model/HPLuv.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.colormath.model 2 | 3 | import com.github.ajalt.colormath.Color 4 | import com.github.ajalt.colormath.ColorComponentInfo 5 | import com.github.ajalt.colormath.ColorSpace 6 | import com.github.ajalt.colormath.HueColor 7 | import com.github.ajalt.colormath.internal.clampLeadingHue 8 | import com.github.ajalt.colormath.internal.doCreate 9 | import com.github.ajalt.colormath.internal.polarComponentInfo 10 | 11 | /** 12 | * HPLuv color space, an alternative to [HSLuv] that preserves as many colors as it can without distorting chroma. 13 | * 14 | * | Component | Description | Range | 15 | * | ---------- | ----------------------------------------- | ---------- | 16 | * | [h] | hue, degrees, `NaN` for monochrome colors | `[0, 360)` | 17 | * | [p] | percentage saturation | `[0, 100]` | 18 | * | [l] | lightness | `[0, 100]` | 19 | * 20 | * ### References 21 | * - [HSLuv homepage](https://www.hsluv.org/) 22 | */ 23 | data class HPLuv( 24 | override val h: Float, 25 | val p: Float, 26 | val l: Float, 27 | override val alpha: Float = 1f, 28 | ) : HueColor { 29 | /** Default constructors for the [HPLuv] color model. */ 30 | companion object : ColorSpace { 31 | override val name: String get() = "HPLuv" 32 | override val components: List = polarComponentInfo("HPL", 0f, 100f) 33 | override fun convert(color: Color): HPLuv = color.toHPLuv() 34 | override fun create(components: FloatArray): HPLuv = doCreate(components, ::HPLuv) 35 | } 36 | 37 | constructor (h: Number, p: Number, l: Number, alpha: Number = 1f) 38 | : this(h.toFloat(), p.toFloat(), l.toFloat(), alpha.toFloat()) 39 | 40 | override val space: ColorSpace get() = HPLuv 41 | 42 | override fun toLCHuv(): LCHuv { 43 | if (l > 99.9999) return LCHuv(100f, 0f, h, alpha) 44 | if (l < 0.00001) return LCHuv(0f, 0f, h, alpha) 45 | val max = HUSLColorConverter.maxSafeChromaForL(l.toDouble()) 46 | val c = max / 100 * p 47 | return LCHuv(l, c.toFloat(), h, alpha) 48 | } 49 | 50 | override fun toSRGB(): RGB = toXYZ().toSRGB() 51 | override fun toLUV(): LUV = toLCHuv().toLUV() 52 | override fun toXYZ(): XYZ = toLCHuv().toXYZ() 53 | override fun toHPLuv(): HPLuv = this 54 | override fun toArray(): FloatArray = floatArrayOf(h, p, l, alpha) 55 | override fun clamp(): HPLuv = clampLeadingHue(h, p, l, alpha, ::copy) 56 | } 57 | -------------------------------------------------------------------------------- /colormath/src/commonMain/kotlin/com/github/ajalt/colormath/model/HSL.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.colormath.model 2 | 3 | import com.github.ajalt.colormath.* 4 | import com.github.ajalt.colormath.internal.clampLeadingHue 5 | import com.github.ajalt.colormath.internal.doCreate 6 | import com.github.ajalt.colormath.internal.normalizeDeg 7 | import com.github.ajalt.colormath.internal.polarComponentInfo 8 | import kotlin.math.min 9 | 10 | /** 11 | * A color model represented with Hue, Saturation, and Lightness. 12 | * 13 | * This is a cylindrical representation of the sRGB space used in [RGB]. 14 | * 15 | * | Component | Description | Range | 16 | * | ---------- | ----------------------------------------- | ---------- | 17 | * | [h] | hue, degrees, `NaN` for monochrome colors | `[0, 360)` | 18 | * | [s] | saturation | `[0, 1]` | 19 | * | [l] | lightness | `[0, 1]` | 20 | */ 21 | data class HSL(override val h: Float, val s: Float, val l: Float, override val alpha: Float = 1f) : 22 | HueColor { 23 | /** Default constructors for the [HSL] color model. */ 24 | companion object : ColorSpace { 25 | override val name: String get() = "HSL" 26 | override val components: List = polarComponentInfo("HSL", 0f, 1f) 27 | override fun convert(color: Color): HSL = color.toHSL() 28 | override fun create(components: FloatArray): HSL = doCreate(components, ::HSL) 29 | } 30 | 31 | constructor (h: Number, s: Number, l: Number, alpha: Number = 1f) 32 | : this(h.toFloat(), s.toFloat(), l.toFloat(), alpha.toFloat()) 33 | 34 | override val space: ColorSpace get() = HSL 35 | 36 | override fun toSRGB(): RGB { 37 | if (s < 1e-7) return RGB(l, l, l, alpha) 38 | 39 | val h = (hueOr(0).normalizeDeg() / 30.0) 40 | val s = s.toDouble() 41 | val l = l.toDouble() 42 | 43 | fun f(n: Int): Float { 44 | val k = (n + h) % 12.0 45 | val a = s * min(l, 1 - l) 46 | return (l - a * minOf(k - 3, 9 - k, 1.0).coerceAtLeast(-1.0)).toFloat() 47 | } 48 | 49 | return SRGB(f(0), f(8), f(4), alpha) 50 | } 51 | 52 | override fun toHSV(): HSV { 53 | var s = this.s 54 | var l = this.l 55 | var smin = s 56 | val lmin = maxOf(l, 0.01f) 57 | 58 | l *= 2 59 | s *= if (l <= 1) l else 2 - l 60 | smin *= if (lmin <= 1) lmin else 2 - lmin 61 | val v = (l + s) / 2 62 | val sv = if (l == 0f) (2 * smin) / (lmin + smin) else (2 * s) / (l + s) 63 | 64 | return HSV(h, sv, v, alpha) 65 | } 66 | 67 | override fun toHSL() = this 68 | override fun toArray(): FloatArray = floatArrayOf(h, s, l, alpha) 69 | override fun clamp(): HSL = clampLeadingHue(h, s, l, alpha, ::copy) 70 | } 71 | -------------------------------------------------------------------------------- /colormath/src/commonMain/kotlin/com/github/ajalt/colormath/model/HSLuv.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.colormath.model 2 | 3 | import com.github.ajalt.colormath.Color 4 | import com.github.ajalt.colormath.ColorComponentInfo 5 | import com.github.ajalt.colormath.ColorSpace 6 | import com.github.ajalt.colormath.HueColor 7 | import com.github.ajalt.colormath.internal.* 8 | import kotlin.math.* 9 | 10 | 11 | /** 12 | * HSLuv color space, a human friendly alternative to [HSL]. 13 | * 14 | * | Component | Description | Range | 15 | * | ---------- | ----------------------------------------- | ---------- | 16 | * | [h] | hue, degrees, `NaN` for monochrome colors | `[0, 360)` | 17 | * | [s] | saturation | `[0, 100]` | 18 | * | [l] | lightness | `[0, 100]` | 19 | * 20 | * ### References 21 | * - [HSLuv homepage](https://www.hsluv.org/) 22 | */ 23 | data class HSLuv( 24 | override val h: Float, 25 | val s: Float, 26 | val l: Float, 27 | override val alpha: Float = 1f, 28 | ) : HueColor { 29 | /** Default constructors for the [HSLuv] color model. */ 30 | companion object : ColorSpace { 31 | override val name: String get() = "HSLuv" 32 | override val components: List = polarComponentInfo("HSL", 0f, 100f) 33 | override fun convert(color: Color): HSLuv = color.toHSLuv() 34 | override fun create(components: FloatArray): HSLuv = doCreate(components, ::HSLuv) 35 | } 36 | 37 | constructor (h: Number, s: Number, l: Number, alpha: Number = 1f) 38 | : this(h.toFloat(), s.toFloat(), l.toFloat(), alpha.toFloat()) 39 | 40 | override val space: ColorSpace get() = HSLuv 41 | 42 | override fun toLCHuv(): LCHuv { 43 | if (l > 99.9999) return LCHuv(100f, 0f, h, alpha) 44 | if (l < 0.00001) return LCHuv(0f, 0f, h, alpha) 45 | val max = HUSLColorConverter.maxChromaForLH(l.toDouble(), h.toDouble()) 46 | val c = max / 100 * s 47 | return LCHuv(l, c.toFloat(), h, alpha) 48 | } 49 | 50 | override fun toSRGB(): RGB = toXYZ().toSRGB() 51 | override fun toLUV(): LUV = toLCHuv().toLUV() 52 | override fun toXYZ(): XYZ = toLCHuv().toXYZ() 53 | override fun toHSLuv(): HSLuv = this 54 | override fun toArray(): FloatArray = floatArrayOf(h, s, l, alpha) 55 | override fun clamp(): HSLuv = clampLeadingHue(h, s, l, alpha, ::copy) 56 | } 57 | 58 | 59 | internal object HUSLColorConverter { 60 | fun maxSafeChromaForL(L: Double): Double { 61 | return getBounds(L).minOf { (m1, b1) -> 62 | val x = intersectLineLine(m1, b1, -1 / m1, 0.0) 63 | distanceFromPole(x, b1 + x * m1) 64 | } 65 | } 66 | 67 | fun maxChromaForLH(L: Double, H: Double): Double { 68 | val hrad: Double = H / 360 * PI * 2 69 | return getBounds(L).minOf { (mi, hi) -> 70 | lengthOfRayUntilIntersect(hrad, mi, hi).let { if (it < 0) Double.MAX_VALUE else it } 71 | } 72 | } 73 | 74 | private fun getBounds(L: Double): List> { 75 | val result = ArrayList>(6) 76 | val sub1: Double = (L + 16).pow(3) / 1560896 77 | val sub2 = if (sub1 > CIE_E) sub1 else L / CIE_K 78 | for (c in 0..2) { 79 | val m1 = Matrix(SRGB.matrixFromXyz)[0, c] 80 | val m2 = Matrix(SRGB.matrixFromXyz)[1, c] 81 | val m3 = Matrix(SRGB.matrixFromXyz)[2, c] 82 | for (t in 0..1) { 83 | val top1 = (284517 * m1 - 94839 * m3) * sub2 84 | val top2 = (838422 * m3 + 769860 * m2 + 731718 * m1) * L * sub2 - 769860 * t * L 85 | val bottom = (632260 * m3 - 126452 * m2) * sub2 + 126452 * t 86 | result.add(top1 / bottom to top2 / bottom) 87 | } 88 | } 89 | return result 90 | } 91 | 92 | private fun intersectLineLine(x1: Double, y1: Double, x2: Double, y2: Double): Double { 93 | return (y1 - y2) / (x2 - x1) 94 | } 95 | 96 | private fun distanceFromPole(x: Double, y: Double): Double { 97 | return sqrt(x.pow(2) + y.pow(2)) 98 | } 99 | 100 | private fun lengthOfRayUntilIntersect(theta: Double, a: Double, b: Double): Double { 101 | return b / (sin(theta) - a * cos(theta)) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /colormath/src/commonMain/kotlin/com/github/ajalt/colormath/model/HSV.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.colormath.model 2 | 3 | import com.github.ajalt.colormath.Color 4 | import com.github.ajalt.colormath.ColorComponentInfo 5 | import com.github.ajalt.colormath.ColorSpace 6 | import com.github.ajalt.colormath.HueColor 7 | import com.github.ajalt.colormath.internal.clampLeadingHue 8 | import com.github.ajalt.colormath.internal.doCreate 9 | import com.github.ajalt.colormath.internal.normalizeDeg 10 | import com.github.ajalt.colormath.internal.polarComponentInfo 11 | import kotlin.math.max 12 | 13 | /** 14 | * A color model represented with Hue, Saturation, and Value. 15 | * 16 | * This is a cylindrical representation of the sRGB space used in [RGB]. 17 | * 18 | * | Component | Description | Range | 19 | * | ---------- | ----------------------------------------- | ---------- | 20 | * | [h] | hue, degrees, `NaN` for monochrome colors | `[0, 360)` | 21 | * | [s] | saturation | `[0, 1]` | 22 | * | [v] | value | `[0, 1]` | 23 | */ 24 | data class HSV(override val h: Float, val s: Float, val v: Float, override val alpha: Float = 1f) : 25 | HueColor { 26 | /** Default constructors for the [HSV] color model. */ 27 | companion object : ColorSpace { 28 | override val name: String get() = "HSV" 29 | override val components: List = polarComponentInfo("HSV", 0f, 1f) 30 | override fun convert(color: Color): HSV = color.toHSV() 31 | override fun create(components: FloatArray): HSV = doCreate(components, ::HSV) 32 | } 33 | 34 | constructor (h: Number, s: Number, v: Number, alpha: Number = 1f) 35 | : this(h.toFloat(), s.toFloat(), v.toFloat(), alpha.toFloat()) 36 | 37 | override val space: ColorSpace get() = HSV 38 | 39 | override fun toSRGB(): RGB { 40 | if (h.isNaN() || s.isNaN() || s < 1e-7) return RGB(v, v, v, alpha) 41 | val v = v.toDouble() 42 | val h = (h.normalizeDeg() / 60.0) 43 | val s = s.toDouble() 44 | 45 | fun f(n: Int): Float { 46 | val k = (n + h) % 6 47 | return (v - v * s * minOf(k, 4 - k, 1.0).coerceAtLeast(0.0)).toFloat() 48 | } 49 | return SRGB(f(5), f(3), f(1), alpha) 50 | } 51 | 52 | override fun toHSL(): HSL { 53 | val vmin = max(v, 0.01f) 54 | val l = ((2 - s) * v) / 2 55 | val lmin = (2 - s) * vmin 56 | val sl = if (lmin == 2f) 0f else (s * vmin) / (if (lmin <= 1) lmin else 2 - lmin) 57 | return HSL(h, sl, l, alpha) 58 | } 59 | 60 | override fun toHSV() = this 61 | override fun toArray(): FloatArray = floatArrayOf(h, s, v, alpha) 62 | override fun clamp(): HSV = clampLeadingHue(h, s, v, alpha, ::copy) 63 | } 64 | -------------------------------------------------------------------------------- /colormath/src/commonMain/kotlin/com/github/ajalt/colormath/model/HWB.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.colormath.model 2 | 3 | import com.github.ajalt.colormath.Color 4 | import com.github.ajalt.colormath.ColorComponentInfo 5 | import com.github.ajalt.colormath.ColorSpace 6 | import com.github.ajalt.colormath.HueColor 7 | import com.github.ajalt.colormath.internal.clampLeadingHue 8 | import com.github.ajalt.colormath.internal.doCreate 9 | import com.github.ajalt.colormath.internal.polarComponentInfo 10 | 11 | /** 12 | * A color model represented with Hue, Whiteness, and Blackness. 13 | * 14 | * This is a cylindrical representation of the sRGB space used in [RGB]. 15 | * 16 | * | Component | Description | Range | 17 | * | ---------- | ------------ | ---------- | 18 | * | [h] | hue, degrees | `[0, 360)` | 19 | * | [w] | whiteness | `[0, 1]` | 20 | * | [b] | blackness | `[0, 1]` | 21 | */ 22 | data class HWB(override val h: Float, val w: Float, val b: Float, override val alpha: Float = 1f) : 23 | Color, 24 | HueColor { 25 | /** Default constructors for the [HWB] color model. */ 26 | companion object : ColorSpace { 27 | override val name: String get() = "HWB" 28 | override val components: List = polarComponentInfo("HWB", 0f, 1f) 29 | override fun convert(color: Color): HWB = color.toHWB() 30 | override fun create(components: FloatArray): HWB = doCreate(components, ::HWB) 31 | } 32 | 33 | constructor(h: Number, w: Number, b: Number, alpha: Number = 1f) 34 | : this(h.toFloat(), w.toFloat(), b.toFloat(), alpha.toFloat()) 35 | 36 | override val space: ColorSpace get() = HWB 37 | 38 | override fun toSRGB(): RGB { 39 | // Algorithm from Smith and Lyons, http://alvyray.com/Papers/CG/HWB_JGTv208.pdf, Appendix B 40 | 41 | val h = this.h / 60f // Smith defines hue as normalized to [0, 6] for some reason 42 | val w = this.w 43 | val b = this.b 44 | val a = this.alpha 45 | 46 | // Smith just declares that w + b must be <= 1. We use the fast-exit from 47 | // https://www.w3.org/TR/css-color-4/#hwb-to-rgb rather than normalizing. 48 | if (w + b >= 1) { 49 | val gray = (w / (w + b)) 50 | return RGB(gray, gray, gray, a) 51 | } 52 | 53 | val v = 1 - b 54 | val i = h.toInt() 55 | val f = when { 56 | i % 2 == 1 -> 1 - (h - i) 57 | else -> h - i 58 | } 59 | val n = w + f * (v - w) // linear interpolation between w and v 60 | return when (i) { 61 | 1 -> RGB(n, v, w, a) 62 | 2 -> RGB(w, v, n, a) 63 | 3 -> RGB(w, n, v, a) 64 | 4 -> RGB(n, w, v, a) 65 | 5 -> RGB(v, w, n, a) 66 | else -> RGB(v, n, w, a) 67 | } 68 | } 69 | 70 | override fun toHSV(): HSV { 71 | // http://alvyray.com/Papers/CG/HWB_JGTv208.pdf, Page 3 72 | val w = this.w / 100 73 | val b = this.b / 100 74 | val s = 1 - w / (1 - b) 75 | val v = 1 - b 76 | return HSV(h, s * 100, v * 100, alpha) 77 | } 78 | 79 | override fun toHWB(): HWB = this 80 | override fun toArray(): FloatArray = floatArrayOf(h, w, b, alpha) 81 | override fun clamp(): HWB = clampLeadingHue(h, w, b, alpha, ::copy) 82 | } 83 | -------------------------------------------------------------------------------- /colormath/src/commonMain/kotlin/com/github/ajalt/colormath/model/ICtCp.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.colormath.model 2 | 3 | import com.github.ajalt.colormath.Color 4 | import com.github.ajalt.colormath.ColorComponentInfo 5 | import com.github.ajalt.colormath.ColorSpace 6 | import com.github.ajalt.colormath.internal.* 7 | import com.github.ajalt.colormath.model.RGBColorSpaces.BT2020 8 | import com.github.ajalt.colormath.model.XYZColorSpaces.XYZ65 9 | 10 | /** 11 | * The ICtCp color space, designed for high dynamic range and wide color gamut imagery. 12 | * 13 | * | Component | Description | Range | 14 | * | ---------- | -------------------- | ------------- | 15 | * | [i] | intensity | `[0, 1]` | 16 | * | [ct] | Tritan (blue-yellow) | `[-0.5, 0.5]` | 17 | * | [cp] | Protan (red-green) | `[-0.5, 0.5]` | 18 | * 19 | * ### References 20 | * - [Rec. ITU-R BT.2100-2](https://www.itu.int/rec/R-REC-BT.2100-2-201807-I/en) 21 | * - [Dolby ICtCp whitepaper](https://professional.dolby.com/siteassets/pdfs/ictcp_dolbywhitepaper_v071.pdf) 22 | */ 23 | data class ICtCp(val i: Float, val ct: Float, val cp: Float, override val alpha: Float = 1f) : 24 | Color { 25 | /** Default constructors for the [ICtCp] color model. */ 26 | companion object : ColorSpace { 27 | override val name: String get() = "ICtCp" 28 | override val components: List = threeComponentInfo( 29 | "I", 0f, 1f, "Ct", -.5f, .5f, "Cp", -.5f, .5f 30 | ) 31 | 32 | override fun convert(color: Color): ICtCp = color.toICtCp() 33 | override fun create(components: FloatArray): ICtCp = doCreate(components, ::ICtCp) 34 | } 35 | 36 | constructor (i: Number, ct: Number, cp: Number, alpha: Number = 1f) 37 | : this(i.toFloat(), ct.toFloat(), cp.toFloat(), alpha.toFloat()) 38 | 39 | override val space: ColorSpace get() = ICtCp 40 | 41 | /** Convert this color to [BT.2020 RGB][RGBColorSpaces.BT2020] */ 42 | fun toBT2020(): RGB { 43 | val fo = BT2020.transferFunctions 44 | return ICTCP_ICTCP_to_LMS.dot(i, ct, cp) { l, m, s -> 45 | ICTCP_LMS_to_RGB.dot( 46 | PqNonlinearity.eotf(l), 47 | PqNonlinearity.eotf(m), 48 | PqNonlinearity.eotf(s) 49 | ) { r, g, b -> 50 | BT2020(fo.oetf(r), fo.oetf(g), fo.oetf(b), alpha) 51 | } 52 | } 53 | } 54 | 55 | override fun toXYZ(): XYZ { 56 | return ICTCP_ICTCP_to_LMS.dot(i, ct, cp) { l, m, s -> 57 | ICTCP_LMS_TO_XYZ.dot( 58 | PqNonlinearity.eotf(l), 59 | PqNonlinearity.eotf(m), 60 | PqNonlinearity.eotf(s) 61 | ) { x, y, z -> 62 | XYZ65(x, y, z, alpha) 63 | } 64 | } 65 | } 66 | 67 | override fun toSRGB(): RGB = toXYZ().toSRGB() 68 | override fun toICtCp(): ICtCp = this 69 | override fun toArray(): FloatArray = floatArrayOf(i, ct, cp, alpha) 70 | override fun clamp(): ICtCp = clamp3(i, ct, cp, alpha, ::copy) 71 | } 72 | 73 | internal fun convertBT2020ToICtCp(rgb: RGB): ICtCp { 74 | val fe = BT2020.transferFunctions 75 | return ICTCP_RGB_TO_LMS.dot(fe.eotf(rgb.r), fe.eotf(rgb.g), fe.eotf(rgb.b)) { l, m, s -> 76 | ICTCP_LMS_TO_ICTCP.dot( 77 | PqNonlinearity.oetf(l), 78 | PqNonlinearity.oetf(m), 79 | PqNonlinearity.oetf(s) 80 | ) { i, ct, cp -> 81 | ICtCp(i, ct, cp, rgb.alpha) 82 | } 83 | } 84 | } 85 | 86 | internal fun convertXYZToICtCp(xyz: XYZ): ICtCp { 87 | return ICTCP_XYZ_TO_LMS.dot(xyz.x, xyz.y, xyz.z) { l, m, s -> 88 | ICTCP_LMS_TO_ICTCP.dot( 89 | PqNonlinearity.oetf(l), 90 | PqNonlinearity.oetf(m), 91 | PqNonlinearity.oetf(s) 92 | ) { i, ct, cp -> 93 | ICtCp(i, ct, cp, xyz.alpha) 94 | } 95 | } 96 | } 97 | 98 | /** The SMPTE ST 2084 EOTF as defined in the ICtCp whitepaper cited above */ 99 | private object PqNonlinearity : RGBColorSpace.TransferFunctions { 100 | private const val m1 = 2610.0 / 16384.0 101 | private const val m2 = 2523.0 / 4096.0 * 128.0 102 | private const val c1 = 3424.0 / 4096.0 103 | private const val c2 = 2413.0 / 4096.0 * 32.0 104 | private const val c3 = 2392.0 / 4096.0 * 32.0 105 | private const val lp = 10000.0 106 | private const val m1d = 1 / m1 107 | private const val m2d = 1 / m2 108 | 109 | override fun eotf(x: Float): Float { 110 | val vp = x.spow(m2d) 111 | val n = (vp - c1).coerceAtLeast(0.0) 112 | val l = (n / (c2 - c3 * vp)).spow(m1d) 113 | return (lp * l).toFloat() 114 | } 115 | 116 | override fun oetf(x: Float): Float { 117 | val yp = (x / lp).spow(m1) 118 | return ((c1 + c2 * yp) / (1.0 + c3 * yp)).spow(m2).toFloat() 119 | } 120 | } 121 | 122 | private val ICTCP_RGB_TO_LMS = Matrix( 123 | 1688f, 2146f, 262f, 124 | 683f, 2951f, 462f, 125 | 99f, 309f, 3688f, 126 | ).scalarDiv(4096f, inPlace = true) 127 | 128 | private val ICTCP_LMS_TO_ICTCP = Matrix( 129 | 2048f, 2048f, 0f, 130 | 6610f, -13613f, 7003f, 131 | 17933f, -17390f, -543f, 132 | ).scalarDiv(4096f, inPlace = true) 133 | 134 | private val ICTCP_LMS_to_RGB = ICTCP_RGB_TO_LMS.inverse() 135 | 136 | private val ICTCP_ICTCP_to_LMS = ICTCP_LMS_TO_ICTCP.inverse() 137 | 138 | // ICtCp defines the XYZ to LMS matrix by multiplying a crosstalk matrix with the old 139 | // Hunt-Pointer-Estevez transform. It's not clear why they use HPE rather than one of the newer 140 | // transforms. 141 | private val ICTCP_CROSSTALK = Matrix( 142 | 0.92f, 0.04f, 0.04f, 143 | 0.04f, 0.92f, 0.04f, 144 | 0.04f, 0.04f, 0.92f, 145 | ) 146 | 147 | private val HPE_XYZ_TO_LMS = Matrix( 148 | 0.4002f, 0.7076f, -0.0808f, 149 | -0.2263f, 1.1653f, 0.0457f, 150 | 0f, 0f, 0.9182f, 151 | ) 152 | 153 | private val ICTCP_XYZ_TO_LMS = ICTCP_CROSSTALK.dot(HPE_XYZ_TO_LMS) 154 | 155 | private val ICTCP_LMS_TO_XYZ = ICTCP_XYZ_TO_LMS.inverse() 156 | -------------------------------------------------------------------------------- /colormath/src/commonMain/kotlin/com/github/ajalt/colormath/model/JzAzBz.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.colormath.model 2 | 3 | import com.github.ajalt.colormath.Color 4 | import com.github.ajalt.colormath.ColorComponentInfo 5 | import com.github.ajalt.colormath.ColorSpace 6 | import com.github.ajalt.colormath.calculate.differenceEz 7 | import com.github.ajalt.colormath.internal.clamp3 8 | import com.github.ajalt.colormath.internal.doCreate 9 | import com.github.ajalt.colormath.internal.threeComponentInfo 10 | import com.github.ajalt.colormath.internal.toPolarModel 11 | import kotlin.math.pow 12 | 13 | /** 14 | * The JzAzBz color space: a perceptually uniform space where euclidean distance predicts perceptual difference. 15 | * 16 | * This color space is always calculated relative to [Illuminant.D65]. 17 | * 18 | * The JzAzBz color difference ΔEz between two colors can be calculated with [differenceEz]. 19 | * 20 | * | Component | Description | Range | 21 | * | --------- | ----------- | --------- | 22 | * | [j] | lightness | `[0, 1]` | 23 | * | [a] | green-red | `[-1, 1]` | 24 | * | [b] | blue-yellow | `[-1, 1]` | 25 | * 26 | * #### Reference 27 | * M. Safdar, G. Cui, Y. Kim, and M. Luo, "Perceptually uniform color space for image signals including high dynamic 28 | * range and wide gamut," Opt. Express 25, 15131-15151 (2017). 29 | */ 30 | data class JzAzBz(val j: Float, val a: Float, val b: Float, override val alpha: Float = 1f) : 31 | Color { 32 | /** Default constructors for the [JzAzBz] color model. */ 33 | companion object : ColorSpace { 34 | override val name: String get() = "JzAzBz" 35 | override val components: List = threeComponentInfo( 36 | "Jz", 0f, 1f, "Az", -1f, 1f, "Bz", -1f, 1f 37 | ) 38 | 39 | override fun convert(color: Color): JzAzBz = color.toJzAzBz() 40 | override fun create(components: FloatArray): JzAzBz = doCreate(components, ::JzAzBz) 41 | 42 | internal const val d0: Double = 1.6295499532821566e-11 43 | } 44 | 45 | constructor (j: Number, a: Number, b: Number, alpha: Number = 1f) 46 | : this(j.toFloat(), a.toFloat(), b.toFloat(), alpha.toFloat()) 47 | 48 | override val space: ColorSpace get() = JzAzBz 49 | 50 | override fun toSRGB(): RGB = when (j) { 51 | 0f -> RGB(0f, 0f, 0f, alpha) 52 | else -> toXYZ().toSRGB() 53 | } 54 | 55 | // Combined matrix values from https://observablehq.com/@jrus/jzazbz, which seems to be the values that most 56 | // implementations (such as ImageMagik) use. 57 | override fun toXYZ(): XYZ { 58 | fun pqInv(x: Double): Double { 59 | val xx = x.pow(7.460772656268214e-03) 60 | val v = 1e4 * ((0.8359375 - xx) / (18.6875 * xx - 18.8515625)).pow(6.277394636015326) 61 | return if (v.isNaN()) 0.0 else v 62 | } 63 | 64 | val jz = j + d0 65 | val iz = jz / (0.44 + 0.56 * jz) 66 | val l = pqInv(iz + 1.386050432715393e-1 * a + 5.804731615611869e-2 * b) 67 | val m = pqInv(iz - 1.386050432715393e-1 * a - 5.804731615611891e-2 * b) 68 | val s = pqInv(iz - 9.601924202631895e-2 * a - 8.118918960560390e-1 * b) 69 | return XYZ( 70 | x = +1.661373055774069e+00 * l - 9.145230923250668e-01 * m + 2.313620767186147e-01 * s, 71 | y = -3.250758740427037e-01 * l + 1.571847038366936e+00 * m - 2.182538318672940e-01 * s, 72 | z = -9.098281098284756e-02 * l - 3.127282905230740e-01 * m + 1.522766561305260e+00 * s, 73 | alpha = alpha 74 | ) 75 | } 76 | 77 | override fun toJzCzHz(): JzCzHz = toPolarModel(a, b) { c, h -> JzCzHz(j, c, h, alpha) } 78 | override fun toJzAzBz(): JzAzBz = this 79 | override fun toArray(): FloatArray = floatArrayOf(j, a, b, alpha) 80 | override fun clamp(): JzAzBz = clamp3(j, a, b, alpha, ::copy) 81 | } 82 | -------------------------------------------------------------------------------- /colormath/src/commonMain/kotlin/com/github/ajalt/colormath/model/JzCzHz.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.colormath.model 2 | 3 | import com.github.ajalt.colormath.Color 4 | import com.github.ajalt.colormath.ColorComponentInfo 5 | import com.github.ajalt.colormath.ColorSpace 6 | import com.github.ajalt.colormath.HueColor 7 | import com.github.ajalt.colormath.internal.clamp3 8 | import com.github.ajalt.colormath.internal.doCreate 9 | import com.github.ajalt.colormath.internal.fromPolarModel 10 | import com.github.ajalt.colormath.internal.threeComponentInfo 11 | 12 | /** 13 | * The JzCzHz color model, the cylindrical representation of [JzAzBz]. 14 | * 15 | * | Component | Description | Range | 16 | * | ---------- | ----------------------------------------- | ---------- | 17 | * | [j] | lightness | `[0, 1]` | 18 | * | [c] | chroma | `[-1, 1]` | 19 | * | [h] | hue, degrees, `NaN` for monochrome colors | `[0, 360)` | 20 | * 21 | * #### Reference 22 | * M. Safdar, G. Cui, Y. Kim, and M. Luo, "Perceptually uniform color space for image signals including high dynamic 23 | * range and wide gamut," Opt. Express 25, 15131-15151 (2017). 24 | */ 25 | data class JzCzHz( 26 | val j: Float, 27 | val c: Float, 28 | override val h: Float, 29 | override val alpha: Float = 1f, 30 | ) : Color, 31 | HueColor { 32 | /** Default constructors for the [JzCzHz] color model. */ 33 | companion object : ColorSpace { 34 | override val name: String get() = "JzCzHz" 35 | override val components: List = threeComponentInfo( 36 | "J", 0f, 1f, "C", -1f, 1f, "H", 0f, 360f, 37 | ) 38 | 39 | override fun convert(color: Color): JzCzHz = color.toJzCzHz() 40 | override fun create(components: FloatArray): JzCzHz = doCreate(components, ::JzCzHz) 41 | } 42 | 43 | constructor(l: Number, c: Number, h: Number, alpha: Number = 1f) 44 | : this(l.toFloat(), c.toFloat(), h.toFloat(), alpha.toFloat()) 45 | 46 | override val space: ColorSpace get() = JzCzHz 47 | 48 | override fun toSRGB(): RGB = when (j) { 49 | 0f -> RGB(0f, 0f, 0f, alpha) 50 | else -> toJzAzBz().toSRGB() 51 | } 52 | 53 | override fun toXYZ(): XYZ = toJzAzBz().toXYZ() 54 | override fun toJzAzBz(): JzAzBz = fromPolarModel(c, h) { a, b -> return JzAzBz(j, a, b, alpha) } 55 | override fun toJzCzHz(): JzCzHz = this 56 | override fun toArray(): FloatArray = floatArrayOf(j, c, h, alpha) 57 | override fun clamp(): JzCzHz = clamp3(j, c, h, alpha, ::copy) 58 | } 59 | -------------------------------------------------------------------------------- /colormath/src/commonMain/kotlin/com/github/ajalt/colormath/model/LAB.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.colormath.model 2 | 3 | import com.github.ajalt.colormath.* 4 | import com.github.ajalt.colormath.internal.* 5 | import com.github.ajalt.colormath.model.LAB.Companion.whitePoint 6 | import com.github.ajalt.colormath.model.LUV.Companion.whitePoint 7 | import com.github.ajalt.colormath.model.XYZ.Companion.whitePoint 8 | import kotlin.math.pow 9 | 10 | 11 | /** 12 | * The color space describing colors in the [LAB] model. 13 | */ 14 | interface LABColorSpace : WhitePointColorSpace { 15 | operator fun invoke(l: Float, a: Float, b: Float, alpha: Float = 1f): LAB 16 | operator fun invoke(l: Number, a: Number, b: Number, alpha: Number = 1f): LAB = 17 | invoke(l.toFloat(), a.toFloat(), b.toFloat(), alpha.toFloat()) 18 | } 19 | 20 | /** Create a new [LABColorSpace] that will be calculated relative to the given [whitePoint] */ 21 | fun LABColorSpace(whitePoint: WhitePoint): LABColorSpace = when (whitePoint) { 22 | Illuminant.D65 -> LABColorSpaces.LAB65 23 | Illuminant.D50 -> LABColorSpaces.LAB50 24 | else -> LABColorSpaceImpl(whitePoint) 25 | } 26 | 27 | private data class LABColorSpaceImpl(override val whitePoint: WhitePoint) : LABColorSpace { 28 | override val name: String get() = "LAB" 29 | override val components: List = threeComponentInfo( 30 | "L", 0f, 100f, "A", -128f, 128f, "B", -128f, 128f 31 | ) 32 | 33 | override fun convert(color: Color): LAB = adaptToThis(color) { it.toLAB() } 34 | override fun create(components: FloatArray): LAB = doCreate(components, ::invoke) 35 | override fun toString(): String = "LABColorSpace($whitePoint)" 36 | override operator fun invoke(l: Float, a: Float, b: Float, alpha: Float): LAB = 37 | LAB(l, a, b, alpha, this) 38 | 39 | override fun hashCode(): Int = whitePoint.hashCode() 40 | override fun equals(other: Any?): Boolean { 41 | return other is LABColorSpace && whitePoint == other.whitePoint 42 | } 43 | } 44 | 45 | object LABColorSpaces { 46 | /** An [LAB] color space calculated relative to [Illuminant.D65] */ 47 | val LAB65: LABColorSpace = LABColorSpaceImpl(Illuminant.D65) 48 | 49 | /** An [LAB] color space calculated relative to [Illuminant.D50] */ 50 | val LAB50: LABColorSpace = LABColorSpaceImpl(Illuminant.D50) 51 | } 52 | 53 | /** 54 | * CIE LAB color space, also referred to as `CIE 1976 L*a*b*`. 55 | * 56 | * The cylindrical representation of this space is [LCHab]. 57 | * 58 | * [LAB] is calculated relative to a [given][space] [whitePoint], which defaults to [Illuminant.D65]. 59 | * 60 | * | Component | Description | Range | 61 | * |-----------|-------------|---------------| 62 | * | L | lightness | `[0, 100]` | 63 | * | a* | green-red | `[-128, 128]` | 64 | * | b* | blue-yellow | `[-128, 128]` | 65 | */ 66 | data class LAB( 67 | val l: Float, 68 | val a: Float, 69 | val b: Float, 70 | override val alpha: Float, 71 | override val space: LABColorSpace, 72 | ) : Color { 73 | /** Default constructors for the [LAB] color model: the [LAB65][LABColorSpaces.LAB65] space. */ 74 | companion object : LABColorSpace by LABColorSpaces.LAB65 { 75 | override fun equals(other: Any?): Boolean = LABColorSpaces.LAB65 == other 76 | override fun hashCode(): Int = LABColorSpaces.LAB65.hashCode() 77 | } 78 | 79 | override fun toSRGB(): RGB = when (l) { 80 | 0f -> RGB(0f, 0f, 0f, alpha) 81 | else -> toXYZ().toSRGB() 82 | } 83 | 84 | override fun toXYZ(): XYZ { 85 | // http://www.brucelindbloom.com/Eqn_Lab_to_XYZ.html 86 | val xyzSpace = XYZColorSpace(space.whitePoint) 87 | if (l == 0f) return xyzSpace(0.0, 0.0, 0.0) 88 | 89 | val fy = (l + 16) / 116.0 90 | val fz = fy - b / 200.0 91 | val fx = a / 500.0 + fy 92 | 93 | val yr = if (l > CIE_E_times_K) fy.pow(3) else l / CIE_K 94 | val zr = fz.pow(3).let { if (it > CIE_E) it else (116 * fz - 16) / CIE_K } 95 | val xr = fx.pow(3).let { if (it > CIE_E) it else (116 * fx - 16) / CIE_K } 96 | 97 | val wp = space.whitePoint.chromaticity 98 | return xyzSpace(xr * wp.X, yr * wp.Y, zr * wp.Z, alpha) 99 | } 100 | 101 | override fun toLCHab(): LCHab = 102 | toPolarModel(a, b) { c, h -> LCHabColorSpace(space.whitePoint)(l, c, h, alpha) } 103 | 104 | override fun toLAB(): LAB = this 105 | 106 | override fun toArray(): FloatArray = floatArrayOf(l, a, b, alpha) 107 | override fun clamp(): LAB = clamp3(l, a, b, alpha, ::copy) 108 | } 109 | -------------------------------------------------------------------------------- /colormath/src/commonMain/kotlin/com/github/ajalt/colormath/model/LCHab.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("FunctionName") 2 | 3 | package com.github.ajalt.colormath.model 4 | 5 | import com.github.ajalt.colormath.* 6 | import com.github.ajalt.colormath.internal.* 7 | 8 | /** 9 | * The color space describing colors in the [LCHab] model. 10 | */ 11 | interface LCHabColorSpace : WhitePointColorSpace { 12 | operator fun invoke(l: Float, c: Float, h: Float, alpha: Float = 1f): LCHab 13 | operator fun invoke(l: Number, c: Number, h: Number, alpha: Number = 1f): LCHab = 14 | invoke(l.toFloat(), c.toFloat(), h.toFloat(), alpha.toFloat()) 15 | } 16 | 17 | /** Create a new [LCHabColorSpace] that will be calculated relative to the given [whitePoint] */ 18 | fun LCHabColorSpace(whitePoint: WhitePoint): LCHabColorSpace = when (whitePoint) { 19 | Illuminant.D65 -> LCHabColorSpaces.LCHab65 20 | Illuminant.D50 -> LCHabColorSpaces.LCHab50 21 | else -> LCHabColorSpaceImpl(whitePoint) 22 | } 23 | 24 | private data class LCHabColorSpaceImpl(override val whitePoint: WhitePoint) : LCHabColorSpace { 25 | override val name: String get() = "LCHab" 26 | override val components: List = componentInfoList( 27 | ColorComponentInfo("L", false, 0f, 100f), 28 | ColorComponentInfo("C", false, 0f, 150f), 29 | ColorComponentInfo("H", true, 0f, 360f), 30 | ) 31 | 32 | override fun convert(color: Color): LCHab = adaptToThis(color) { it.toLCHab() } 33 | override fun create(components: FloatArray): LCHab = doCreate(components, ::invoke) 34 | override fun toString(): String = "LCHabColorSpace($whitePoint)" 35 | override operator fun invoke(l: Float, c: Float, h: Float, alpha: Float): LCHab = 36 | LCHab(l, c, h, alpha, this) 37 | 38 | override fun hashCode(): Int = whitePoint.hashCode() 39 | override fun equals(other: Any?): Boolean { 40 | return other is LCHabColorSpace && whitePoint == other.whitePoint 41 | } 42 | } 43 | 44 | object LCHabColorSpaces { 45 | /** An [LCHab] color space calculated relative to [Illuminant.D65] */ 46 | val LCHab65: LCHabColorSpace = LCHabColorSpaceImpl(Illuminant.D65) 47 | 48 | /** An [LCHab] color space calculated relative to [Illuminant.D50] */ 49 | val LCHab50: LCHabColorSpace = LCHabColorSpaceImpl(Illuminant.D50) 50 | } 51 | 52 | /** 53 | * `CIE LCh(ab)` color model, a.k.a. `LCH`, the cylindrical representation of [LAB]. 54 | * 55 | * | Component | Description | Range | 56 | * |-----------|-------------------------------------------|------------| 57 | * | L | lightness | `[0, 100]` | 58 | * | c | chroma | `[0, 150]` | 59 | * | h | hue, degrees, `NaN` for monochrome colors | `[0, 360)` | 60 | */ 61 | data class LCHab( 62 | val l: Float, 63 | val c: Float, 64 | override val h: Float, 65 | override val alpha: Float, 66 | override val space: LCHabColorSpace, 67 | ) : HueColor { 68 | /** Default constructors for the [LCHab] color model: the [LCHab65][LCHabColorSpaces.LCHab65] space. */ 69 | companion object : LCHabColorSpace by LCHabColorSpaces.LCHab65 { 70 | override fun equals(other: Any?): Boolean = LCHabColorSpaces.LCHab65 == other 71 | override fun hashCode(): Int = LCHabColorSpaces.LCHab65.hashCode() 72 | } 73 | 74 | override fun toSRGB(): RGB = toLAB().toSRGB() 75 | override fun toXYZ(): XYZ = toLAB().toXYZ() 76 | override fun toLAB(): LAB = 77 | fromPolarModel(c, h) { a, b -> LABColorSpace(space.whitePoint)(l, a, b, alpha) } 78 | 79 | override fun toLCHab(): LCHab = this 80 | override fun toArray(): FloatArray = floatArrayOf(l, c, h, alpha) 81 | override fun clamp(): LCHab = clampTrailingHue(l, c, h, alpha, ::copy) 82 | } 83 | -------------------------------------------------------------------------------- /colormath/src/commonMain/kotlin/com/github/ajalt/colormath/model/LCHuv.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.colormath.model 2 | 3 | import com.github.ajalt.colormath.* 4 | import com.github.ajalt.colormath.internal.* 5 | 6 | 7 | /** 8 | * The color space describing colors in the [LCHuv] model. 9 | */ 10 | interface LCHuvColorSpace : WhitePointColorSpace { 11 | operator fun invoke(l: Float, c: Float, h: Float, alpha: Float = 1f): LCHuv 12 | operator fun invoke(l: Number, c: Number, h: Number, alpha: Number = 1f): LCHuv = 13 | invoke(l.toFloat(), c.toFloat(), h.toFloat(), alpha.toFloat()) 14 | } 15 | 16 | /** Create a new [LCHuvColorSpace] that will be calculated relative to the given [whitePoint] */ 17 | fun LCHuvColorSpace(whitePoint: WhitePoint): LCHuvColorSpace = when (whitePoint) { 18 | Illuminant.D65 -> LCHuvColorSpaces.LCHuv65 19 | Illuminant.D50 -> LCHuvColorSpaces.LCHuv50 20 | else -> LCHuvColorSpaceImpl(whitePoint) 21 | } 22 | 23 | private data class LCHuvColorSpaceImpl(override val whitePoint: WhitePoint) : LCHuvColorSpace { 24 | override val name: String get() = "LCHuv" 25 | override val components: List = polarComponentInfo("LCH", 0f, 100f) 26 | override fun convert(color: Color): LCHuv = adaptToThis(color) { it.toLCHuv() } 27 | override fun create(components: FloatArray): LCHuv = doCreate(components, ::invoke) 28 | override fun toString(): String = "LCHuvColorSpace($whitePoint)" 29 | override operator fun invoke(l: Float, c: Float, h: Float, alpha: Float): LCHuv = 30 | LCHuv(l, c, h, alpha, this) 31 | 32 | override fun hashCode(): Int = whitePoint.hashCode() 33 | override fun equals(other: Any?): Boolean { 34 | return other is LCHuvColorSpace && whitePoint == other.whitePoint 35 | } 36 | } 37 | 38 | object LCHuvColorSpaces { 39 | /** An [LCHuv] color space calculated relative to [Illuminant.D65] */ 40 | val LCHuv65: LCHuvColorSpace = LCHuvColorSpaceImpl(Illuminant.D65) 41 | 42 | /** An [LCHuv] color space calculated relative to [Illuminant.D50] */ 43 | val LCHuv50: LCHuvColorSpace = LCHuvColorSpaceImpl(Illuminant.D50) 44 | } 45 | 46 | /** 47 | * CIE LCh(uv) color model, a.k.a. `HCL`, the cylindrical representation of [LUV]. 48 | * 49 | * | Component | Description | Range | 50 | * | ---------- | ----------------------------------------- | ---------- | 51 | * | [l] | lightness | `[0, 100]` | 52 | * | [c] | chroma | `[0, 100]` | 53 | * | [h] | hue, degrees, `NaN` for monochrome colors | `[0, 360)` | 54 | */ 55 | data class LCHuv( 56 | val l: Float, 57 | val c: Float, 58 | override val h: Float, 59 | override val alpha: Float, 60 | override val space: LCHuvColorSpace, 61 | ) : HueColor { 62 | /** Default constructors for the [LCHuv] color model: the [LCHLCHuv65ab65][LCHuvColorSpaces.LCHuv65] space. */ 63 | companion object : LCHuvColorSpace by LCHuvColorSpaces.LCHuv65 { 64 | override fun equals(other: Any?): Boolean = LCHuvColorSpaces.LCHuv65 == other 65 | override fun hashCode(): Int = LCHuvColorSpaces.LCHuv65.hashCode() 66 | } 67 | 68 | override fun toHSLuv(): HSLuv { 69 | if (l > 99.9999) return HSLuv(h, 0f, 100f, alpha) 70 | if (l < 0.00001) return HSLuv(h, 0f, 0f, alpha) 71 | val max = HUSLColorConverter.maxChromaForLH(l.toDouble(), h.toDouble()) 72 | val s = c / max * 100 73 | return HSLuv(h, s.toFloat(), l, alpha) 74 | } 75 | 76 | override fun toHPLuv(): HPLuv { 77 | if (l > 99.9999) return HPLuv(h, 0f, 100f, alpha) 78 | if (l < 0.00001) return HPLuv(h, 0f, 0f, alpha) 79 | val max = HUSLColorConverter.maxSafeChromaForL(l.toDouble()) 80 | val s = c / max * 100 81 | return HPLuv(h, s.toFloat(), l, alpha) 82 | } 83 | 84 | override fun toSRGB(): RGB = toLUV().toSRGB() 85 | override fun toXYZ(): XYZ = toLUV().toXYZ() 86 | override fun toLUV(): LUV = 87 | fromPolarModel(c, h) { u, v -> LUVColorSpace(space.whitePoint)(l, u, v, alpha) } 88 | 89 | override fun toLCHuv(): LCHuv = this 90 | override fun toArray(): FloatArray = floatArrayOf(l, c, h, alpha) 91 | override fun clamp(): LCHuv = clampTrailingHue(l, c, h, alpha, ::copy) 92 | } 93 | -------------------------------------------------------------------------------- /colormath/src/commonMain/kotlin/com/github/ajalt/colormath/model/LUV.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.colormath.model 2 | 3 | import com.github.ajalt.colormath.* 4 | import com.github.ajalt.colormath.internal.* 5 | import com.github.ajalt.colormath.model.LUV.Companion.whitePoint 6 | import com.github.ajalt.colormath.model.XYZ.Companion.whitePoint 7 | import kotlin.math.pow 8 | 9 | /** 10 | * The color space describing colors in the [LUV] model. 11 | */ 12 | interface LUVColorSpace : WhitePointColorSpace { 13 | operator fun invoke(l: Float, u: Float, v: Float, alpha: Float = 1f): LUV 14 | operator fun invoke(l: Number, u: Number, v: Number, alpha: Number = 1f): LUV = 15 | invoke(l.toFloat(), u.toFloat(), v.toFloat(), alpha.toFloat()) 16 | } 17 | 18 | /** Create a new [LUVColorSpace] that will be calculated relative to the given [whitePoint] */ 19 | fun LUVColorSpace(whitePoint: WhitePoint): LUVColorSpace = when (whitePoint) { 20 | Illuminant.D65 -> LUVColorSpaces.LUV65 21 | Illuminant.D50 -> LUVColorSpaces.LUV50 22 | else -> LUVColorSpaceImpl(whitePoint) 23 | } 24 | 25 | private data class LUVColorSpaceImpl(override val whitePoint: WhitePoint) : LUVColorSpace { 26 | override val name: String get() = "LUV" 27 | override val components: List = threeComponentInfo( 28 | "L", 0f, 100f, "U", -100f, 100f, "V", -100f, 100f 29 | ) 30 | 31 | override fun convert(color: Color): LUV = adaptToThis(color) { it.toLUV() } 32 | override fun create(components: FloatArray): LUV = doCreate(components, ::invoke) 33 | override fun toString(): String = "LUVColorSpace($whitePoint)" 34 | override operator fun invoke(l: Float, u: Float, v: Float, alpha: Float): LUV = 35 | LUV(l, u, v, alpha, this) 36 | 37 | override fun hashCode(): Int = whitePoint.hashCode() 38 | override fun equals(other: Any?): Boolean { 39 | return other is LUVColorSpace && whitePoint == other.whitePoint 40 | } 41 | } 42 | 43 | object LUVColorSpaces { 44 | /** An [LUV] color space calculated relative to [Illuminant.D65] */ 45 | val LUV65: LUVColorSpace = LUVColorSpaceImpl(Illuminant.D65) 46 | 47 | /** An [LUV] color space calculated relative to [Illuminant.D50] */ 48 | val LUV50: LUVColorSpace = LUVColorSpaceImpl(Illuminant.D50) 49 | } 50 | 51 | /** 52 | * The CIE LUV color space, also referred to as `CIE 1976 L*u*v*`. 53 | * 54 | * [LUV] is calculated relative to a [given][space] [whitePoint], which defaults to [Illuminant.D65]. 55 | * 56 | * | Component | Description | Range | 57 | * | ---------- | ------------ | ------------- | 58 | * | [l] | lightness | `[0, 100]` | 59 | * | [u] | | `[-100, 100]` | 60 | * | [v] | | `[-100, 100]` | 61 | */ 62 | data class LUV( 63 | val l: Float, 64 | val u: Float, 65 | val v: Float, 66 | override val alpha: Float, 67 | override val space: LUVColorSpace, 68 | ) : Color { 69 | /** Default constructors for the [LUV] color model: the [LCHab65][LCHabColorSpaces.LCHab65] space. */ 70 | companion object : LUVColorSpace by LUVColorSpaces.LUV65 { 71 | override fun equals(other: Any?): Boolean = LUVColorSpaces.LUV65 == other 72 | override fun hashCode(): Int = LUVColorSpaces.LUV65.hashCode() 73 | } 74 | 75 | override fun toSRGB(): RGB = when (l) { 76 | 0f -> RGB(0f, 0f, 0f, alpha) 77 | else -> toXYZ().toSRGB() 78 | } 79 | 80 | override fun toXYZ(): XYZ { 81 | val xyzSpace = XYZColorSpace(space.whitePoint) 82 | // http://www.brucelindbloom.com/Eqn_Luv_to_XYZ.html 83 | if (l == 0f) return xyzSpace(0.0f, 0.0f, 0.0f) 84 | 85 | val wp = space.whitePoint.chromaticity 86 | val denominator0 = wp.X + 15.0 * wp.Y + 3.0 * wp.Z 87 | val u0 = 4.0 * wp.X / denominator0 88 | val v0 = 9.0 * wp.Y / denominator0 89 | 90 | val y = if (l > CIE_E_times_K) ((l + 16.0) / 116.0).pow(3) else l / CIE_K 91 | 92 | val a = (52 * l / (u + 13 * l * u0) - 1) / 3 93 | val b = -5 * y 94 | val c = -1.0 / 3 95 | val d = y * ((39 * l) / (v + 13 * l * v0) - 5) 96 | 97 | val x = (d - b) / (a - c) 98 | val z = x * a + b 99 | 100 | return xyzSpace(x, y, z, alpha) 101 | } 102 | 103 | override fun toLCHuv(): LCHuv = 104 | toPolarModel(u, v) { c, h -> LCHuvColorSpace(space.whitePoint)(l, c, h, alpha) } 105 | 106 | override fun toLUV(): LUV = this 107 | override fun toArray(): FloatArray = floatArrayOf(l, u, v, alpha) 108 | override fun clamp(): LUV = clamp3(l, u, v, alpha, ::copy) 109 | } 110 | -------------------------------------------------------------------------------- /colormath/src/commonMain/kotlin/com/github/ajalt/colormath/model/Oklab.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.colormath.model 2 | 3 | import com.github.ajalt.colormath.Color 4 | import com.github.ajalt.colormath.ColorComponentInfo 5 | import com.github.ajalt.colormath.ColorSpace 6 | import com.github.ajalt.colormath.Illuminant 7 | import com.github.ajalt.colormath.internal.clamp3 8 | import com.github.ajalt.colormath.internal.doCreate 9 | import com.github.ajalt.colormath.internal.threeComponentInfo 10 | import com.github.ajalt.colormath.internal.toPolarModel 11 | 12 | /** 13 | * The Oklab color space: a perceptual color space for image processing. 14 | * 15 | * This color space is always calculated relative to [Illuminant.D65]. 16 | * 17 | * | Component | Description | Range | 18 | * |-----------|-------------|---------------| 19 | * | L | lightness | `[0, 1]` | 20 | * | a | green-red | `[-0.4, 0.4]` | 21 | * | b | blue-yellow | `[-0.4, 0.4]` | 22 | */ 23 | data class Oklab(val l: Float, val a: Float, val b: Float, override val alpha: Float = 1f) : Color { 24 | /** Default constructors for the [Oklab] color model. */ 25 | companion object : ColorSpace { 26 | override val name: String get() = "Oklab" 27 | override val components: List = threeComponentInfo( 28 | "l", 0f, 1f, "a", -.4f, .4f, "b", -.4f, .4f 29 | ) 30 | 31 | override fun convert(color: Color): Oklab = color.toOklab() 32 | override fun create(components: FloatArray): Oklab = doCreate(components, ::Oklab) 33 | } 34 | 35 | constructor (l: Number, a: Number, b: Number, alpha: Number = 1f) 36 | : this(l.toFloat(), a.toFloat(), b.toFloat(), alpha.toFloat()) 37 | 38 | override val space: ColorSpace get() = Oklab 39 | 40 | // https://bottosson.github.io/posts/oklab/#converting-from-linear-srgb-to-oklab 41 | override fun toSRGB(): RGB = calculateConeResponse { l, m, s -> 42 | val r = +4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s 43 | val g = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s 44 | val b = -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s 45 | val f = RGB.transferFunctions 46 | return RGB(f.oetf(r.toFloat()), f.oetf(g.toFloat()), f.oetf(b.toFloat()), alpha) 47 | } 48 | 49 | // https://bottosson.github.io/posts/oklab/#converting-from-xyz-to-oklab 50 | // Note that Ottosson doesn't provide values for M₂⁻¹, so they were calculated with `numpy.linalg.inv` 51 | // and truncated to the same precision as used by Ottosson 52 | override fun toXYZ(): XYZ = calculateConeResponse { l, m, s -> 53 | return XYZ( 54 | x = +1.2270138511 * l - 0.5577999807 * m + 0.2812561490 * s, 55 | y = -0.0405801784 * l + 1.1122568696 * m - 0.0716766787 * s, 56 | z = -0.0763812845 * l - 0.4214819784 * m + 1.5861632204 * s, 57 | alpha = alpha 58 | ) 59 | } 60 | 61 | private inline fun calculateConeResponse(block: (l: Double, m: Double, s: Double) -> T): T { 62 | val ll = l + 0.3963377774 * a + 0.2158037573 * b 63 | val mm = l - 0.1055613458 * a - 0.0638541728 * b 64 | val ss = l - 0.0894841775 * a - 1.2914855480 * b 65 | 66 | val l = ll * ll * ll 67 | val m = mm * mm * mm 68 | val s = ss * ss * ss 69 | 70 | return block(l, m, s) 71 | } 72 | 73 | override fun toOklch(): Oklch = toPolarModel(a, b) { c, h -> Oklch(l, c, h, alpha) } 74 | override fun toOklab(): Oklab = this 75 | override fun toArray(): FloatArray = floatArrayOf(l, a, b, alpha) 76 | override fun clamp(): Oklab = clamp3(l, a, b, alpha, ::copy) 77 | } 78 | -------------------------------------------------------------------------------- /colormath/src/commonMain/kotlin/com/github/ajalt/colormath/model/Oklch.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.colormath.model 2 | 3 | import com.github.ajalt.colormath.Color 4 | import com.github.ajalt.colormath.ColorComponentInfo 5 | import com.github.ajalt.colormath.ColorSpace 6 | import com.github.ajalt.colormath.HueColor 7 | import com.github.ajalt.colormath.internal.clampTrailingHue 8 | import com.github.ajalt.colormath.internal.componentInfoList 9 | import com.github.ajalt.colormath.internal.doCreate 10 | import com.github.ajalt.colormath.internal.fromPolarModel 11 | 12 | /** 13 | * Oklch color model, the cylindrical representation of [Oklab]. 14 | * 15 | * | Component | Description | Range | 16 | * |-----------|-------------------------------------------|------------| 17 | * | L | lightness | `[0, 1]` | 18 | * | c | chroma | `[0, 0.4]` | 19 | * | h | hue, degrees, `NaN` for monochrome colors | `[0, 360)` | 20 | */ 21 | data class Oklch( 22 | val l: Float, val c: Float, override val h: Float, override val alpha: Float = 1f, 23 | ) : Color, 24 | HueColor { 25 | /** Default constructors for the [Oklch] color model. */ 26 | companion object : ColorSpace { 27 | override val name: String get() = "Oklch" 28 | override val components: List = componentInfoList( 29 | ColorComponentInfo("l", false, 0f, 1f), 30 | ColorComponentInfo("c", false, 0f, 0.4f), 31 | ColorComponentInfo("h", true, 0f, 360f), 32 | ) 33 | 34 | override fun convert(color: Color): Oklch = color.toOklch() 35 | override fun create(components: FloatArray): Oklch = doCreate(components, ::Oklch) 36 | } 37 | 38 | constructor(l: Number, c: Number, h: Number, alpha: Number = 1f) 39 | : this(l.toFloat(), c.toFloat(), h.toFloat(), alpha.toFloat()) 40 | 41 | override val space: ColorSpace get() = Oklch 42 | 43 | override fun toSRGB(): RGB = toOklab().toSRGB() 44 | override fun toXYZ(): XYZ = toOklab().toXYZ() 45 | override fun toOklab(): Oklab = fromPolarModel(c, h) { a, b -> Oklab(l, a, b, alpha) } 46 | override fun toOklch(): Oklch = this 47 | override fun toArray(): FloatArray = floatArrayOf(l, c, h, alpha) 48 | override fun clamp(): Oklch = clampTrailingHue(l, c, h, alpha, ::copy) 49 | } 50 | -------------------------------------------------------------------------------- /colormath/src/commonMain/kotlin/com/github/ajalt/colormath/model/RGBInt.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.colormath.model 2 | 3 | import com.github.ajalt.colormath.Color 4 | import com.github.ajalt.colormath.ColorComponentInfo 5 | import com.github.ajalt.colormath.ColorSpace 6 | import com.github.ajalt.colormath.RenderCondition 7 | import com.github.ajalt.colormath.RenderCondition.AUTO 8 | import com.github.ajalt.colormath.internal.doCreate 9 | import com.github.ajalt.colormath.internal.threeComponentInfo 10 | import kotlin.jvm.JvmInline 11 | import kotlin.math.roundToInt 12 | 13 | /** 14 | * A representation of [RGB] that packs color components into a single integer. 15 | * 16 | * This is an inline value class stores the color as a packed [argb] integer, such as those returned from 17 | * `android.graphics.Color.argb` or `java.awt.image.BufferedImage.getRGB`. 18 | * 19 | * This color always uses the sRGB color space. 20 | * 21 | * You can destructure this class into [r], [g], [b], and [a] components: `val (r, g, b, a) = RGBInt(0xaa112233u)` 22 | * 23 | * | Component | Description | Range | 24 | * | ---------- | ----------- | ---------- | 25 | * | [r] | red | `[0, 255]` | 26 | * | [g] | green | `[0, 255]` | 27 | * | [b] | blue | `[0, 255]` | 28 | */ 29 | @JvmInline 30 | value class RGBInt(val argb: UInt) : Color { 31 | /** Default constructors for [RGBInt]. */ 32 | companion object : ColorSpace { 33 | override val name: String get() = "RGBInt" 34 | override val components: List = threeComponentInfo( 35 | "R", 0f, 255f, "G", 0f, 255f, "B", 0f, 255f 36 | ) 37 | 38 | override fun convert(color: Color): RGBInt = color.toSRGB().toRGBInt() 39 | override fun create(components: FloatArray): RGBInt = doCreate(components) { r, g, b, a -> 40 | RGBInt(r.toInt(), g.toInt(), b.toInt(), a.toInt()) 41 | } 42 | 43 | /** Create an [RGBInt] from an integer packed in RGBA order */ 44 | fun fromRGBA(rgba: UInt): RGBInt = RGBInt((rgba shr 8) or (rgba shl 24)) 45 | } 46 | 47 | constructor(r: UByte, g: UByte, b: UByte, alpha: UByte = 0xff.toUByte()) : this( 48 | r.toInt(), g.toInt(), b.toInt(), alpha.toInt() 49 | ) 50 | 51 | constructor(r: Int, g: Int, b: Int, alpha: Int = 0xff) : this( 52 | (alpha.toUInt() shl 24) or (r.toUInt() shl 16) or (g.toUInt() shl 8) or b.toUInt() 53 | ) 54 | 55 | /** 56 | * Construct an [RGBInt] instance from Float value in the range `[0, 1]` 57 | */ 58 | constructor(r: Float, g: Float, b: Float, alpha: Float = 1f) : this( 59 | r = if (r.isNaN()) 0 else (r * 255).roundToInt().coerceIn(0, 255), 60 | g = if (b.isNaN()) 0 else (g * 255).roundToInt().coerceIn(0, 255), 61 | b = if (g.isNaN()) 0 else (b * 255).roundToInt().coerceIn(0, 255), 62 | alpha = (alpha * 255).roundToInt().coerceIn(0, 255), 63 | ) 64 | 65 | override val alpha: Float get() = (a.toFloat() / 255f) 66 | override val space: ColorSpace get() = RGBInt 67 | 68 | /** The red component, in the range `[0, 255]` */ 69 | val r: UByte get() = (argb shr 16).toUByte() 70 | 71 | /** The green component, in the range `[0, 255]` */ 72 | val g: UByte get() = (argb shr 8).toUByte() 73 | 74 | /** The blue component, in the range `[0, 255]` */ 75 | val b: UByte get() = (argb shr 0).toUByte() 76 | 77 | /** The [alpha] component scaled to `[0, 255]` */ 78 | val a: UByte get() = (argb shr 24).toUByte() 79 | 80 | /** The red component as a Float in the range `[0, 1]` */ 81 | val redFloat: Float get() = r.toInt() / 255f 82 | 83 | /** The green component as a Float in the range `[0, 1]` */ 84 | val greenFloat: Float get() = g.toInt() / 255f 85 | 86 | /** The blue component as a Float in the range `[0, 1]` */ 87 | val blueFloat: Float get() = b.toInt() / 255f 88 | 89 | /** Convert this color to an integer packed in RGBA order. */ 90 | fun toRGBA(): UInt = (argb shl 8) or (argb shr 24) 91 | 92 | override fun toSRGB(): RGB = RGB(redFloat, greenFloat, blueFloat, alpha) 93 | 94 | /** 95 | * Convert this color to an RGB hex string. 96 | * 97 | * If [renderAlpha] is `ALWAYS`, the [alpha] value will be added e.g. the `aa` in `#ffffffaa`. 98 | * If it's `NEVER`, the [alpha] will be omitted. If it's `AUTO`, then the [alpha] will be added 99 | * if it's less than 1. 100 | * 101 | * @return A string in the form `"#ffffff"` if [withNumberSign] is true, 102 | * or in the form `"ffffff"` otherwise. 103 | */ 104 | fun toHex(withNumberSign: Boolean = true, renderAlpha: RenderCondition = AUTO): String = 105 | buildString(9) { 106 | if (withNumberSign) append('#') 107 | append(r.renderHex()).append(g.renderHex()).append(b.renderHex()) 108 | if (renderAlpha == RenderCondition.ALWAYS || renderAlpha == AUTO && a < 255u) { 109 | append(a.renderHex()) 110 | } 111 | } 112 | 113 | operator fun component1() = r 114 | operator fun component2() = g 115 | operator fun component3() = b 116 | operator fun component4() = a 117 | override fun toArray(): FloatArray = 118 | floatArrayOf(r.toFloat(), g.toFloat(), b.toFloat(), a.toFloat()) 119 | 120 | override fun clamp(): RGBInt = this 121 | 122 | private fun UByte.renderHex() = toString(16).padStart(2, '0') 123 | } 124 | -------------------------------------------------------------------------------- /colormath/src/commonMain/kotlin/com/github/ajalt/colormath/model/xyY.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("FunctionName", "ClassName", "PropertyName") 2 | 3 | package com.github.ajalt.colormath.model 4 | 5 | import kotlin.jvm.JvmName 6 | 7 | /** 8 | * The `CIE xyY` color space, also used to store `xy` chromaticity coordinates by setting [Y] to 1. 9 | * 10 | * [x], [y], and [z] are relative values. [X], [Y], and [Z] are absolute. 11 | */ 12 | data class xyY( 13 | val x: Float, 14 | val y: Float, 15 | @get:JvmName("getAbsoluteY") 16 | val Y: Float = 1f, 17 | ) { 18 | constructor(x: Number, y: Number, Y: Number = 1.0) : this(x.toFloat(), y.toFloat(), Y.toFloat()) 19 | 20 | val z: Float get() = 1 - x - y 21 | 22 | @get:JvmName("getAbsoluteX") 23 | val X: Float 24 | get() = x * Y / y 25 | 26 | @get:JvmName("getAbsoluteZ") 27 | val Z: Float 28 | get() = (1 - x - y) * Y / y 29 | } 30 | -------------------------------------------------------------------------------- /colormath/src/commonMain/kotlin/com/github/ajalt/colormath/transform/ChromaticAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.colormath.transform 2 | 3 | import com.github.ajalt.colormath.Color 4 | import com.github.ajalt.colormath.Illuminant 5 | import com.github.ajalt.colormath.internal.Matrix 6 | import com.github.ajalt.colormath.internal.dot 7 | import com.github.ajalt.colormath.model.* 8 | import com.github.ajalt.colormath.model.RGBColorSpaces.SRGB 9 | import com.github.ajalt.colormath.model.XYZColorSpaces.XYZ65 10 | 11 | /** 12 | * Create a chromatic adapter that will adapt colors from a given [sourceWhite] to this color space's 13 | * [reference white][RGBColorSpace.whitePoint] 14 | */ 15 | fun RGBColorSpace.createChromaticAdapter(sourceWhite: Color): ChromaticAdapterRGB { 16 | return createChromaticAdapter(sourceWhite.toXYZ().toCIExyY()) 17 | } 18 | 19 | /** 20 | * Create a chromatic adapter that will adapt colors from a given [sourceWhite] to this color space's 21 | * [reference white][RGBColorSpace.whitePoint] 22 | */ 23 | fun RGBColorSpace.createChromaticAdapter(sourceWhite: xyY): ChromaticAdapterRGB { 24 | val xyzTransform = XYZColorSpace(whitePoint).chromaticAdaptationMatrix(sourceWhite) 25 | return ChromaticAdapterRGB(this, xyzToSrgb.dot(xyzTransform).dot(srgbToXYZ)) 26 | } 27 | 28 | /** Create a chromatic adapter that will adapt [RGBInt] colors from a given [sourceWhite] to [D65][Illuminant.D65] */ 29 | fun RGBInt.Companion.createChromaticAdapter(sourceWhite: Color): ChromaticAdapterRGBInt { 30 | return createChromaticAdapter(sourceWhite.toXYZ().toCIExyY()) 31 | } 32 | 33 | /** Create a chromatic adapter that will adapt [RGBInt] colors from a given [sourceWhite] to [D65][Illuminant.D65] */ 34 | fun RGBInt.Companion.createChromaticAdapter(sourceWhite: xyY): ChromaticAdapterRGBInt { 35 | val xyzTransform = XYZ65.chromaticAdaptationMatrix(sourceWhite) 36 | return ChromaticAdapterRGBInt(xyzToSrgb.dot(xyzTransform).dot(srgbToXYZ)) 37 | } 38 | 39 | class ChromaticAdapterRGB internal constructor( 40 | private val space: RGBColorSpace, 41 | private val transform: Matrix, 42 | ) { 43 | /** Adapt an sRGB [color] to this white point */ 44 | fun adapt(color: RGB): RGB { 45 | return doAdapt(transform, color.r, color.g, color.b) { r, g, b -> 46 | space(r, g, b, color.alpha) 47 | } 48 | } 49 | } 50 | 51 | class ChromaticAdapterRGBInt internal constructor(private val transform: Matrix) { 52 | /** Adapt an sRGB [color] to this white point */ 53 | fun adapt(color: RGBInt): RGBInt { 54 | return doAdapt(transform, color.redFloat, color.greenFloat, color.blueFloat) { r, g, b -> 55 | RGBInt(r, g, b, color.alpha) 56 | } 57 | } 58 | 59 | /** Apply this adaptation in-place to all `argb` integers in an array of [colors] */ 60 | fun adaptAll(colors: IntArray) { 61 | for (i in colors.indices) { 62 | colors[i] = adapt(RGBInt(colors[i].toUInt())).argb.toInt() 63 | } 64 | } 65 | } 66 | 67 | private inline fun doAdapt( 68 | transform: Matrix, 69 | r: Float, 70 | g: Float, 71 | b: Float, 72 | block: (Float, Float, Float) -> T, 73 | ): T { 74 | val f = SRGB.transferFunctions 75 | return transform.dot(f.eotf(r), f.eotf(g), f.eotf(b)) { rr, gg, bb -> 76 | block(f.oetf(rr), f.oetf(gg), f.oetf(bb)) 77 | } 78 | } 79 | 80 | private val xyzToSrgb = Matrix(SRGB.matrixFromXyz) 81 | 82 | private val srgbToXYZ = Matrix(SRGB.matrixToXyz) 83 | -------------------------------------------------------------------------------- /colormath/src/commonMain/kotlin/com/github/ajalt/colormath/transform/HueAdjustments.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.colormath.transform 2 | 3 | import com.github.ajalt.colormath.internal.normalizeDeg 4 | import kotlin.math.absoluteValue 5 | import kotlin.math.withSign 6 | 7 | object HueAdjustments { 8 | /** Angles are adjusted so that their difference is in `[-180, 180]` */ 9 | val shorter: ComponentAdjustment = deltaAdjustment { 10 | if (it.absoluteValue <= 180) it else it - 360f.withSign(it) 11 | } 12 | 13 | /** Angles are adjusted so that their difference is 0 or is in `[180, 360)` */ 14 | val longer: ComponentAdjustment = deltaAdjustment { 15 | if (it == 0f || it.absoluteValue >= 180) it else it - 360f.withSign(it) 16 | } 17 | 18 | /** Angles are adjusted so that their difference is in `[0, 360)` */ 19 | val increasing: ComponentAdjustment = deltaAdjustment { 20 | if (it >= 0) it else it + 360f 21 | } 22 | 23 | /** Angles are adjusted so that their difference is in `(-360, 0]` */ 24 | val decreasing: ComponentAdjustment = deltaAdjustment { 25 | if (it <= 0) it else it - 360f 26 | } 27 | 28 | /** 29 | * Leave all angles unchanged 30 | */ 31 | val specified: ComponentAdjustment = { it } 32 | } 33 | 34 | private inline fun deltaAdjustment(crossinline adj: (delta: Float) -> Float): ComponentAdjustment = 35 | { hues -> 36 | hues.toMutableList().also { h -> 37 | h[0] = h[0].normalizeDeg() 38 | for (i in 1..h.lastIndex) { 39 | val hue = h[i] 40 | val prev = h[i - 1] 41 | if (hue.isNaN() || prev.isNaN()) continue 42 | h[i] = prev + adj(hue.normalizeDeg() - prev.normalizeDeg()) 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /colormath/src/commonMain/kotlin/com/github/ajalt/colormath/transform/Mix.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.colormath.transform 2 | 3 | import com.github.ajalt.colormath.Color 4 | import com.github.ajalt.colormath.ColorSpace 5 | import com.github.ajalt.colormath.internal.nanToOne 6 | 7 | fun ColorSpace.mix( 8 | color1: Color, 9 | color2: Color, 10 | hueAdjustment: ComponentAdjustment = HueAdjustments.shorter, 11 | ): Color = mix(color1, .5f, color2, .5f, hueAdjustment) 12 | 13 | fun ColorSpace.mix( 14 | color1: Color, 15 | amount1: Number, 16 | color2: Color, 17 | hueAdjustment: ComponentAdjustment = HueAdjustments.shorter, 18 | ): Color = mix(color1, amount1, color2, 1f - amount1.toFloat(), hueAdjustment) 19 | 20 | fun ColorSpace.mix( 21 | color1: Color, 22 | color2: Color, 23 | amount2: Number, 24 | hueAdjustment: ComponentAdjustment = HueAdjustments.shorter, 25 | ): Color = mix(color1, 1f - amount2.toFloat(), color2, amount2, hueAdjustment) 26 | 27 | /** 28 | * Mix [amount1] of [color1] and [amount2] of [color2] in this color space. 29 | * 30 | * The sum of the amounts is greater than one, they will be normalized so the sum equals one. If the sum is less than 31 | * one, they will be normalized and the final alpha value will be multiplied by their sum. 32 | * 33 | * This implements the `color-mix` functionality specified in 34 | * [CSS Color Module 5](https://www.w3.org/TR/css-color-5/#color-mix) 35 | * 36 | * @param amount1 The amount of [color1] to mix. A fraction in `[0, 1]`. If omitted, defaults to `1 - amount2` 37 | * @param amount2 The amount of [color2] to mix. A fraction in `[0, 1]`. If omitted, defaults to `1 - amount1` 38 | * @param hueAdjustment An optional adjustment to the hue components of the colors, if there is one. Defaults to [HueAdjustments.shorter]. 39 | */ 40 | fun ColorSpace.mix( 41 | color1: Color, 42 | amount1: Number, 43 | color2: Color, 44 | amount2: Number, 45 | hueAdjustment: ComponentAdjustment = HueAdjustments.shorter, 46 | ): Color { 47 | val sum = amount1.toFloat() + amount2.toFloat() 48 | require(sum != 0f) { "mix amounts cannot sum to 0" } 49 | val c = convert(color1).interpolate(color2, amount2.toFloat() / sum, true, hueAdjustment) 50 | return if (sum < 1f) c.map { comps -> 51 | comps.also { 52 | it[it.lastIndex] = it.last().nanToOne() * sum 53 | } 54 | } else c 55 | } 56 | -------------------------------------------------------------------------------- /colormath/src/commonMain/kotlin/com/github/ajalt/colormath/transform/Premultiply.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.colormath.transform 2 | 3 | import com.github.ajalt.colormath.Color 4 | import com.github.ajalt.colormath.ColorComponentInfo 5 | import com.github.ajalt.colormath.ColorSpace 6 | 7 | /** 8 | * Multiply this color's components by its alpha value. 9 | * 10 | * [Polar components][ColorComponentInfo.isPolar] and the alpha value itself are not changed. 11 | */ 12 | fun T.multiplyAlpha() = map { components -> 13 | components.also { multiplyAlphaInPlace(this, it) } 14 | } 15 | 16 | internal fun multiplyAlphaInPlace(space: ColorSpace<*>, components: FloatArray) { 17 | val a = components.last() 18 | if (a.isNaN() || a == 1f) return 19 | for (i in 0 until components.lastIndex) { 20 | if (space.components[i].isPolar) continue 21 | components[i] = components[i] * a 22 | } 23 | } 24 | 25 | /** 26 | * Divide this color's components by its alpha value. 27 | * 28 | * This is the inverse of [multiplyAlpha]. 29 | * 30 | * [Polar components][ColorComponentInfo.isPolar] and the alpha value itself are not changed. 31 | * If `alpha == 0`, all components are left unchanged. 32 | */ 33 | fun T.divideAlpha(): T = map { components -> 34 | components.also { divideAlphaInPlace(this, it) } 35 | } 36 | 37 | internal fun divideAlphaInPlace(space: ColorSpace<*>, components: FloatArray) { 38 | val a = components.last() 39 | if (a.isNaN() || a == 0f || a == 1f) return 40 | for (i in 0 until components.lastIndex) { 41 | if (space.components[i].isPolar) continue 42 | components[i] = components[i] / a 43 | } 44 | } 45 | 46 | -------------------------------------------------------------------------------- /colormath/src/commonMain/kotlin/com/github/ajalt/colormath/transform/RGBToRGBConverter.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.colormath.transform 2 | 3 | import com.github.ajalt.colormath.internal.Matrix 4 | import com.github.ajalt.colormath.internal.dot 5 | import com.github.ajalt.colormath.model.RGB 6 | import com.github.ajalt.colormath.model.RGBColorSpace 7 | import com.github.ajalt.colormath.model.XYZColorSpace 8 | import com.github.ajalt.colormath.model.chromaticAdaptationMatrix 9 | 10 | 11 | /** 12 | * A converter that transforms [RGB] colors from one [RGBColorSpace] to another. 13 | * 14 | * You can also convert a color directly with [RGB.convertTo], but using a converter is more efficient 15 | * if you need to convert more than one color at a time. 16 | */ 17 | interface RGBToRGBConverter { 18 | fun convert(rgb: RGB): RGB 19 | } 20 | 21 | /** 22 | * Create an [RGBToRGBConverter] that transforms [RGB] colors from this [RGBColorSpace] to the [destination] space. 23 | */ 24 | fun RGBColorSpace.converterTo(destination: RGBColorSpace): RGBToRGBConverter { 25 | return RGBToRGBConverterImpl(this, destination, rgbToRgbMatrix(this, destination)) 26 | } 27 | 28 | private class RGBToRGBConverterImpl( 29 | private val src: RGBColorSpace, 30 | private val dst: RGBColorSpace, 31 | private val transform: Matrix, 32 | ) : RGBToRGBConverter { 33 | override fun convert(rgb: RGB): RGB { 34 | require(rgb.space == src) { "invalid rgb space: ${rgb.space}, expected $src" } 35 | val fsrc = src.transferFunctions 36 | val fdst = dst.transferFunctions 37 | return transform.dot(fsrc.eotf(rgb.r), fsrc.eotf(rgb.g), fsrc.eotf(rgb.b)) { rr, gg, bb -> 38 | dst(fdst.oetf(rr), fdst.oetf(gg), fdst.oetf(bb)) 39 | } 40 | } 41 | } 42 | 43 | internal fun rgbToRgbMatrix(src: RGBColorSpace, dst: RGBColorSpace): Matrix { 44 | return if (src.whitePoint == dst.whitePoint) { 45 | Matrix(dst.matrixFromXyz).dot(Matrix(src.matrixToXyz)) 46 | } else { 47 | val adaptation = 48 | XYZColorSpace(dst.whitePoint).chromaticAdaptationMatrix(src.whitePoint.chromaticity) 49 | Matrix(dst.matrixFromXyz).dot(adaptation).dot(Matrix(src.matrixToXyz)) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /colormath/src/commonMain/kotlin/com/github/ajalt/colormath/transform/Transform.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.colormath.transform 2 | 3 | import com.github.ajalt.colormath.Color 4 | import com.github.ajalt.colormath.ColorSpace 5 | 6 | /** 7 | * A mapping function used with [map]. 8 | * 9 | * ### Parameters 10 | * - `space`: The [ColorSpace] of the color being mapped 11 | * - `components`: The [components][Color.toArray] of the color to map. 12 | * 13 | * ### Returns 14 | * The new color components. You may alter and return `components` directly, or a new array the same size as 15 | * `components`. 16 | */ 17 | typealias ColorMapper = ColorSpace.(components: FloatArray) -> FloatArray 18 | 19 | /** 20 | * Return an new color in the same color space that is the result of applying [transform] to the components of this 21 | * color. 22 | */ 23 | @Suppress("UNCHECKED_CAST") 24 | fun T.map(transform: ColorMapper): T { 25 | return space.create(transform(space as ColorSpace, toArray())) as T 26 | } 27 | -------------------------------------------------------------------------------- /colormath/src/commonTest/kotlin/com/github/ajalt/colormath/internal/MatrixTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.colormath.internal 2 | 3 | import kotlin.js.JsName 4 | import kotlin.test.Test 5 | import kotlin.test.assertEquals 6 | 7 | class MatrixTest { 8 | @[Test JsName("matrix_times_matrix")] 9 | fun `matrix times matrix`() { 10 | val l = Matrix( 11 | -2f, 5f, 6f, 12 | -1f, -3f, 2f, 13 | 0f, 7f, 4f, 14 | ) 15 | val r = Matrix( 16 | 8f, 1f, -2f, 17 | 3f, 5f, 6f, 18 | -4f, -3f, -1f, 19 | ) 20 | (l.dot(r)).rowMajor shouldBe Matrix( 21 | -25f, 5f, 28f, 22 | -25f, -22f, -18f, 23 | 5f, 23f, 38f, 24 | ).rowMajor 25 | } 26 | 27 | @Test 28 | fun inverse() { 29 | val orig = Matrix( 30 | 9f, 13f, 14f, 31 | 12f, 11f, 6f, 32 | 3f, 5f, 15f, 33 | ) 34 | val copy = orig.copy() 35 | val ex = Matrix( 36 | -0.2631579f, +0.24366471f, +0.14814815f, 37 | +0.31578946f, -0.18128654f, -0.22222222f, 38 | -0.05263158f, +0.0116959065f, +0.11111111f, 39 | ) 40 | orig.inverse().rowMajor shouldBe ex.rowMajor 41 | orig.rowMajor shouldBe copy.rowMajor 42 | 43 | copy.inverse(inPlace = true).rowMajor shouldBe ex.rowMajor 44 | copy.rowMajor shouldBe ex.rowMajor 45 | } 46 | 47 | @[Test JsName("matrix_times_vec")] 48 | fun `matrix times vec`() { 49 | val l = Matrix( 50 | 1f, 2f, 3f, 51 | 4f, 5f, 6f, 52 | 7f, 8f, 9f, 53 | ) 54 | l.dot(10f, 20f, 30f).values shouldBe floatArrayOf(140f, 320f, 500f) 55 | } 56 | } 57 | 58 | // TODO(kotest): go back to kotest once is supports wasm 59 | private infix fun FloatArray.shouldBe(other: FloatArray) { 60 | for (i in indices) { 61 | assertEquals(this[i], other[i], 0.00000001f, "index $i") 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /docs/css/logo-styles.css: -------------------------------------------------------------------------------- 1 | /* Custom CSS for Dokka docs */ 2 | 3 | #logo { 4 | background-image: url('../images/palette_black_36dp.svg'); 5 | } 6 | 7 | /* Add the project name to the logo */ 8 | #logo::before { 9 | content: "Colormath"; 10 | margin-left: 80px; 11 | color: black; 12 | font-size: 20px; 13 | font-weight: 600; 14 | } 15 | 16 | /* Remove the "What's on this Page" tab that covers up the scroll bar */ 17 | .page-summary { 18 | display: none; 19 | } 20 | -------------------------------------------------------------------------------- /docs/extensions.md: -------------------------------------------------------------------------------- 1 | # Colormath extensions 2 | 3 | Colormath provides extensions for converting to and from other platform's color representations. 4 | Each set of extensions is published as a separate maven package. 5 | 6 | ## Android ColorInt 7 | 8 | ```kotlin 9 | dependencies { 10 | implementation("com.github.ajalt.colormath:colormath-ext-android-colorint:$colormathVersion") 11 | } 12 | ``` 13 | 14 | [API docs][colorint] 15 | 16 | These extensions convert between Android's packed ARGB integers, which are commonly annotated with `@ColorInt`. 17 | 18 | This package supports Android API 16+. 19 | 20 | ```kotlin 21 | val redPercent = RGBInt.fromColorInt(textView.currentTextColor).redFloat 22 | val textColor = RGB.fromColorInt(textView.currentTextColor) 23 | textView.highlightColor = textColor.toColorInt() 24 | ``` 25 | 26 | ## Android Color objects 27 | 28 | ```kotlin 29 | dependencies { 30 | implementation("com.github.ajalt.colormath:colormath-ext-android-color:$colormathVersion") 31 | } 32 | ``` 33 | 34 | [API docs][android-color] 35 | 36 | These extensions convert between the color objects introduced in Android 26. 37 | 38 | This package supports Android API 26+. 39 | 40 | ```kotlin 41 | import android.graphics.ColorSpace 42 | import android.graphics.Color as AndroidColor 43 | 44 | val c: AndroidColor = RGB("#f0f").toAndroidColor() 45 | val rgb: RGB = c.toColormathSRGB() 46 | val lab = AndroidColor.valueOf(0f, 1f, 0f, 1f, ColorSpace.get(ColorSpace.Named.CIE_LAB)).toColormathColor() 47 | ``` 48 | 49 | ## Jetpack Compose Color objects 50 | 51 | ```kotlin 52 | dependencies { 53 | implementation("com.github.ajalt.colormath:colormath-ext-jetpack-compose:$colormathVersion") 54 | } 55 | ``` 56 | 57 | [API docs][jetpack-compose] 58 | 59 | 60 | These extensions convert between the color objects used in `androidx.compose`. 61 | 62 | This package supports Android API 21+. 63 | 64 | [android-color]: api/colormath-ext-android-color/colormath-ext-android-color/com.github.ajalt.colormath.extensions.android.color/index.html 65 | [colorint]: api/colormath-ext-android-colorint/colormath-ext-android-colorint/com.github.ajalt.colormath.extensions.android.colorint/index.html 66 | [jetpack-compose]: api/colormath-ext-jetpack-compose/colormath-ext-jetpack-compose/com.github.ajalt.colormath.extensions.android.composecolor/index.html 67 | -------------------------------------------------------------------------------- /docs/img/bad_hue_grad.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /docs/img/colormath_wordmark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/img/good_hue_grad.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /docs/img/palette_black_36dp.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/img/palette_white_24dp.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /extensions/build.gradle.kts: -------------------------------------------------------------------------------- 1 | subprojects { 2 | group = "com.github.ajalt.colormath.extensions" 3 | } 4 | -------------------------------------------------------------------------------- /extensions/colormath-ext-android-color/api/colormath-ext-android-color.api: -------------------------------------------------------------------------------- 1 | public final class com/github/ajalt/colormath/extensions/android/color/ColorExtensionsKt { 2 | public static final fun toAndroidColor (Lcom/github/ajalt/colormath/Color;)Landroid/graphics/Color; 3 | public static final fun toColormathColor (Landroid/graphics/Color;)Lcom/github/ajalt/colormath/Color; 4 | public static final fun toColormathSRGB (Landroid/graphics/Color;)Lcom/github/ajalt/colormath/model/RGB; 5 | } 6 | 7 | -------------------------------------------------------------------------------- /extensions/colormath-ext-android-color/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.library") 3 | kotlin("multiplatform") 4 | alias(libs.plugins.publish) 5 | } 6 | 7 | 8 | repositories { 9 | mavenCentral() 10 | google() 11 | } 12 | 13 | kotlin { 14 | androidTarget { 15 | publishLibraryVariants("release") 16 | } 17 | 18 | sourceSets { 19 | val androidMain by getting { 20 | dependencies { 21 | api(project(":colormath")) 22 | } 23 | } 24 | val androidUnitTest by getting { 25 | dependencies { 26 | implementation(libs.junit) 27 | implementation(libs.robolectric) 28 | } 29 | } 30 | } 31 | } 32 | 33 | android { 34 | namespace = "com.github.ajalt.colormath.extensions.android.color" 35 | compileSdk = 33 36 | defaultConfig.minSdk = 26 // Color instances were added in 26 37 | buildFeatures.buildConfig = false 38 | } 39 | -------------------------------------------------------------------------------- /extensions/colormath-ext-android-color/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_ARTIFACT_ID=colormath-ext-android-color 2 | POM_NAME=Android Color Extensions for Colormath 3 | -------------------------------------------------------------------------------- /extensions/colormath-ext-android-color/src/androidMain/kotlin/com/github/ajalt/colormath/extensions/android/color/ColorExtensions.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.colormath.extensions.android.color 2 | 3 | import android.graphics.ColorSpace 4 | import com.github.ajalt.colormath.Color 5 | import com.github.ajalt.colormath.model.LABColorSpaces.LAB50 6 | import com.github.ajalt.colormath.model.RGB 7 | import com.github.ajalt.colormath.model.RGBColorSpaces.ACES 8 | import com.github.ajalt.colormath.model.RGBColorSpaces.ACEScg 9 | import com.github.ajalt.colormath.model.RGBColorSpaces.AdobeRGB 10 | import com.github.ajalt.colormath.model.RGBColorSpaces.BT2020 11 | import com.github.ajalt.colormath.model.RGBColorSpaces.BT709 12 | import com.github.ajalt.colormath.model.RGBColorSpaces.DCI_P3 13 | import com.github.ajalt.colormath.model.RGBColorSpaces.DisplayP3 14 | import com.github.ajalt.colormath.model.RGBColorSpaces.LinearSRGB 15 | import com.github.ajalt.colormath.model.RGBColorSpaces.ROMM_RGB 16 | import com.github.ajalt.colormath.model.RGBInt 17 | import com.github.ajalt.colormath.model.SRGB 18 | import com.github.ajalt.colormath.model.XYZColorSpaces.XYZ50 19 | import android.graphics.Color as AndroidColor 20 | 21 | 22 | /** 23 | * Convert this color to a Colormath [Color]. 24 | * 25 | * If this color's space is built in to Colormath, the returned color will be in the space space. 26 | * Otherwise, this is equivalent to [toColormathColor]. 27 | */ 28 | fun AndroidColor.toColormathColor(): Color { 29 | return when (colorSpace) { 30 | ColorSpace.get(ColorSpace.Named.SRGB) -> RGB(red(), green(), blue(), alpha()) 31 | ColorSpace.get(ColorSpace.Named.LINEAR_SRGB) -> LinearSRGB(red(), green(), blue(), alpha()) 32 | ColorSpace.get(ColorSpace.Named.BT709) -> BT709(red(), green(), blue(), alpha()) 33 | ColorSpace.get(ColorSpace.Named.BT2020) -> BT2020(red(), green(), blue(), alpha()) 34 | ColorSpace.get(ColorSpace.Named.DCI_P3) -> DCI_P3(red(), green(), blue(), alpha()) 35 | ColorSpace.get(ColorSpace.Named.DISPLAY_P3) -> DisplayP3(red(), green(), blue(), alpha()) 36 | ColorSpace.get(ColorSpace.Named.ADOBE_RGB) -> AdobeRGB(red(), green(), blue(), alpha()) 37 | ColorSpace.get(ColorSpace.Named.PRO_PHOTO_RGB) -> ROMM_RGB(red(), green(), blue(), alpha()) 38 | ColorSpace.get(ColorSpace.Named.ACES) -> ACES(red(), green(), blue(), alpha()) 39 | ColorSpace.get(ColorSpace.Named.ACESCG) -> ACEScg(red(), green(), blue(), alpha()) 40 | ColorSpace.get(ColorSpace.Named.CIE_XYZ) -> XYZ50(red(), green(), blue(), alpha()) 41 | ColorSpace.get(ColorSpace.Named.CIE_LAB) -> LAB50(red(), green(), blue(), alpha()) 42 | else -> toColormathSRGB() 43 | } 44 | } 45 | 46 | /** 47 | * Convert this color to a Colormath [SRGB] instance. 48 | */ 49 | fun AndroidColor.toColormathSRGB(): RGB { 50 | return SRGB.create(ColorSpace.connect(colorSpace).transform(components)) 51 | } 52 | 53 | /** 54 | * Convert this color to an Android [Color][android.graphics.Color] 55 | */ 56 | fun Color.toAndroidColor(): AndroidColor { 57 | if (this is RGBInt) return AndroidColor.valueOf(argb.toInt()) 58 | val s = when { 59 | space == SRGB -> ColorSpace.get(ColorSpace.Named.SRGB) 60 | space === LinearSRGB -> ColorSpace.get(ColorSpace.Named.LINEAR_SRGB) 61 | space === BT709 -> ColorSpace.get(ColorSpace.Named.BT709) 62 | space === BT2020 -> ColorSpace.get(ColorSpace.Named.BT2020) 63 | space === DCI_P3 -> ColorSpace.get(ColorSpace.Named.DCI_P3) 64 | space === DisplayP3 -> ColorSpace.get(ColorSpace.Named.DISPLAY_P3) 65 | space === AdobeRGB -> ColorSpace.get(ColorSpace.Named.ADOBE_RGB) 66 | space === ROMM_RGB -> ColorSpace.get(ColorSpace.Named.PRO_PHOTO_RGB) 67 | space === ACES -> ColorSpace.get(ColorSpace.Named.ACES) 68 | space === ACEScg -> ColorSpace.get(ColorSpace.Named.ACESCG) 69 | space === XYZ50 -> ColorSpace.get(ColorSpace.Named.CIE_XYZ) 70 | space === LAB50 -> ColorSpace.get(ColorSpace.Named.CIE_LAB) 71 | else -> null 72 | } 73 | 74 | return if (s == null) { 75 | val (r, g, b, a) = toSRGB() 76 | AndroidColor.valueOf(r, g, b, a) 77 | } else { 78 | AndroidColor.valueOf(toArray(), s) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /extensions/colormath-ext-android-color/src/androidUnitTest/kotlin/com/github/ajalt/colormath/extensions/android/color/ColorExtensionsTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.colormath.extensions.android.color 2 | 3 | import android.graphics.Color 4 | import org.junit.Assert.assertEquals 5 | import org.junit.Test 6 | import org.junit.runner.RunWith 7 | import org.robolectric.RobolectricTestRunner 8 | import org.robolectric.annotation.Config 9 | 10 | @RunWith(RobolectricTestRunner::class) 11 | @Config(sdk = [26]) 12 | class ColorExtensionsTest { 13 | 14 | private val androidBlue = Color.valueOf(Color.BLUE) 15 | private val colormathBlue = com.github.ajalt.colormath.model.RGB(0, 0, 1, 1) 16 | 17 | @Test 18 | fun toColormathColor() { 19 | assertEquals(colormathBlue, androidBlue.toColormathColor()) 20 | } 21 | 22 | @Test 23 | fun toColormathSRGB() { 24 | assertEquals(colormathBlue, androidBlue.toColormathSRGB()) 25 | } 26 | 27 | @Test 28 | fun toAndroidColor() { 29 | assertEquals(androidBlue, colormathBlue.toAndroidColor()) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /extensions/colormath-ext-android-colorint/api/colormath-ext-android-colorint.api: -------------------------------------------------------------------------------- 1 | public final class com/github/ajalt/colormath/extensions/android/colorint/ColorIntExtensionsKt { 2 | public static final fun fromColorInt (Lcom/github/ajalt/colormath/model/RGB$Companion;I)Lcom/github/ajalt/colormath/model/RGB; 3 | public static final fun fromColorInt (Lcom/github/ajalt/colormath/model/RGBInt$Companion;I)I 4 | public static final fun toColorInt (Lcom/github/ajalt/colormath/Color;)I 5 | } 6 | 7 | -------------------------------------------------------------------------------- /extensions/colormath-ext-android-colorint/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.library") 3 | kotlin("multiplatform") 4 | alias(libs.plugins.publish) 5 | } 6 | 7 | repositories { 8 | mavenCentral() 9 | google() 10 | } 11 | 12 | kotlin { 13 | androidTarget { 14 | publishLibraryVariants("release") 15 | } 16 | 17 | sourceSets { 18 | val androidMain by getting { 19 | dependencies { 20 | api(project(":colormath")) 21 | api(libs.androidx.annotation) 22 | } 23 | } 24 | val androidUnitTest by getting { 25 | dependencies { 26 | implementation(libs.junit) 27 | implementation(libs.robolectric) 28 | } 29 | } 30 | } 31 | } 32 | 33 | android { 34 | namespace = "com.github.ajalt.colormath.extensions.android.colorint" 35 | compileSdk = 33 36 | defaultConfig.minSdk = 21 37 | buildFeatures.buildConfig = false 38 | } 39 | -------------------------------------------------------------------------------- /extensions/colormath-ext-android-colorint/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_ARTIFACT_ID=colormath-ext-android-colorint 2 | POM_NAME=Android ColorInt Extensions for Colormath 3 | -------------------------------------------------------------------------------- /extensions/colormath-ext-android-colorint/src/androidMain/kotlin/com/github/ajalt/colormath/extensions/android/colorint/ColorIntExtensions.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.colormath.extensions.android.colorint 2 | 3 | import androidx.annotation.ColorInt 4 | import com.github.ajalt.colormath.Color 5 | import com.github.ajalt.colormath.model.RGB 6 | import com.github.ajalt.colormath.model.RGBInt 7 | import com.github.ajalt.colormath.model.SRGB 8 | 9 | 10 | /** 11 | * Convert this color to a packed argb [color int][ColorInt]. 12 | */ 13 | @ColorInt 14 | fun Color.toColorInt(): Int { 15 | return toSRGB().toRGBInt().argb.toInt() 16 | } 17 | 18 | /** 19 | * Create an [SRGB] instance from a packed argb [color int][ColorInt]. 20 | */ 21 | fun RGB.Companion.fromColorInt(@ColorInt argb: Int): RGB { 22 | return RGBInt(argb.toUInt()).toSRGB() 23 | } 24 | 25 | /** 26 | * Create an [RGBInt] instance from a packed argb [color int][ColorInt]. 27 | */ 28 | fun RGBInt.Companion.fromColorInt(@ColorInt argb: Int): RGBInt { 29 | return RGBInt(argb.toUInt()) 30 | } 31 | -------------------------------------------------------------------------------- /extensions/colormath-ext-android-colorint/src/androidUnitTest/kotlin/com/github/ajalt/colormath/extensions/android/colorint/ColorIntExtensionsTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.colormath.extensions.android.colorint 2 | 3 | import android.graphics.Color 4 | import org.junit.Assert.* 5 | import org.junit.runner.RunWith 6 | import org.robolectric.RobolectricTestRunner 7 | import org.robolectric.annotation.Config 8 | import com.github.ajalt.colormath.extensions.android.colorint.fromColorInt 9 | import com.github.ajalt.colormath.extensions.android.colorint.toColorInt 10 | import com.github.ajalt.colormath.model.RGB 11 | import com.github.ajalt.colormath.model.RGBInt 12 | import com.github.ajalt.colormath.model.SRGB 13 | 14 | @RunWith(RobolectricTestRunner::class) 15 | @Config(sdk = [26]) 16 | class ColorIntExtensionsTest { 17 | private val colormathBlue = com.github.ajalt.colormath.model.RGB(0, 0, 1, 1) 18 | 19 | @org.junit.Test 20 | fun toColorInt() { 21 | assertEquals(Color.BLUE, colormathBlue.toColorInt()) 22 | } 23 | 24 | @org.junit.Test 25 | fun fromColorIntRGB() { 26 | assertEquals(RGB.fromColorInt(Color.BLUE), colormathBlue) 27 | } 28 | 29 | @org.junit.Test 30 | fun fromColorIntRGBInt() { 31 | assertEquals(RGBInt.fromColorInt(Color.BLUE), colormathBlue.toRGBInt()) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /extensions/colormath-ext-jetpack-compose/api/android/colormath-ext-jetpack-compose.api: -------------------------------------------------------------------------------- 1 | public final class com/github/ajalt/colormath/extensions/android/composecolor/ComposeColorExtensionsKt { 2 | public static final fun toColormathColor-8_81llA (J)Lcom/github/ajalt/colormath/Color; 3 | public static final fun toColormathSRGB-8_81llA (J)Lcom/github/ajalt/colormath/model/RGB; 4 | public static final fun toComposeColor (Lcom/github/ajalt/colormath/Color;)J 5 | } 6 | 7 | -------------------------------------------------------------------------------- /extensions/colormath-ext-jetpack-compose/api/jvm/colormath-ext-jetpack-compose.api: -------------------------------------------------------------------------------- 1 | public final class com/github/ajalt/colormath/extensions/android/composecolor/ComposeColorExtensionsKt { 2 | public static final fun toColormathColor-8_81llA (J)Lcom/github/ajalt/colormath/Color; 3 | public static final fun toColormathSRGB-8_81llA (J)Lcom/github/ajalt/colormath/model/RGB; 4 | public static final fun toComposeColor (Lcom/github/ajalt/colormath/Color;)J 5 | } 6 | 7 | -------------------------------------------------------------------------------- /extensions/colormath-ext-jetpack-compose/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.vanniktech.maven.publish.tasks.JavadocJar 2 | import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl 3 | import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootExtension 4 | 5 | plugins { 6 | id("com.android.library") 7 | kotlin("multiplatform") 8 | alias(libs.plugins.publish) 9 | } 10 | 11 | repositories { 12 | mavenCentral() 13 | google() 14 | } 15 | 16 | kotlin { 17 | androidTarget { 18 | publishLibraryVariants("release") 19 | } 20 | 21 | jvm() 22 | js { nodejs() } 23 | 24 | @OptIn(ExperimentalWasmDsl::class) 25 | wasmJs { nodejs() } 26 | 27 | iosX64() 28 | iosArm64() 29 | iosSimulatorArm64() 30 | 31 | sourceSets { 32 | commonMain.dependencies { 33 | api(project(":colormath")) 34 | api(libs.compose.ui.graphics) 35 | } 36 | commonTest.dependencies { 37 | implementation(kotlin("test")) 38 | } 39 | } 40 | } 41 | 42 | android { 43 | namespace = "com.github.ajalt.colormath.extensions.android.composecolor" 44 | compileSdk = 33 45 | defaultConfig.minSdk = 21 46 | buildFeatures.buildConfig = false 47 | } 48 | 49 | // workaround for https://github.com/Kotlin/dokka/issues/1833 50 | tasks.withType().configureEach { 51 | dependsOn(project.tasks.getByPath(":colormath:dokkaHtml")) 52 | } 53 | -------------------------------------------------------------------------------- /extensions/colormath-ext-jetpack-compose/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_ARTIFACT_ID=colormath-ext-jetpack-compose 2 | POM_NAME=Jetpack Compose Extensions for Colormath 3 | -------------------------------------------------------------------------------- /extensions/colormath-ext-jetpack-compose/src/commonMain/kotlin/com/github/ajalt/colormath/extensions/android/composecolor/ComposeColorExtensions.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.colormath.extensions.android.composecolor 2 | 3 | import androidx.compose.ui.graphics.colorspace.ColorSpaces 4 | import com.github.ajalt.colormath.Color 5 | import com.github.ajalt.colormath.model.LABColorSpaces.LAB50 6 | import com.github.ajalt.colormath.model.Oklab 7 | import com.github.ajalt.colormath.model.RGB 8 | import com.github.ajalt.colormath.model.RGBColorSpaces.ACES 9 | import com.github.ajalt.colormath.model.RGBColorSpaces.ACEScg 10 | import com.github.ajalt.colormath.model.RGBColorSpaces.AdobeRGB 11 | import com.github.ajalt.colormath.model.RGBColorSpaces.BT2020 12 | import com.github.ajalt.colormath.model.RGBColorSpaces.BT709 13 | import com.github.ajalt.colormath.model.RGBColorSpaces.DCI_P3 14 | import com.github.ajalt.colormath.model.RGBColorSpaces.DisplayP3 15 | import com.github.ajalt.colormath.model.RGBColorSpaces.LinearSRGB 16 | import com.github.ajalt.colormath.model.RGBColorSpaces.ROMM_RGB 17 | import com.github.ajalt.colormath.model.RGBInt 18 | import com.github.ajalt.colormath.model.SRGB 19 | import com.github.ajalt.colormath.model.XYZColorSpaces.XYZ50 20 | import kotlin.jvm.JvmOverloads 21 | import androidx.compose.ui.graphics.Color as ComposeColor 22 | 23 | 24 | /** 25 | * Convert this color to a Colormath [Color] instance. 26 | */ 27 | fun ComposeColor.toColormathColor(): Color { 28 | return when (colorSpace) { 29 | ColorSpaces.Srgb -> SRGB(red, green, blue, alpha) 30 | ColorSpaces.Aces -> ACES(red, green, blue, alpha) 31 | ColorSpaces.Acescg -> ACEScg(red, green, blue, alpha) 32 | ColorSpaces.AdobeRgb -> AdobeRGB(red, green, blue, alpha) 33 | ColorSpaces.Bt2020 -> BT2020(red, green, blue, alpha) 34 | ColorSpaces.Bt709 -> BT709(red, green, blue, alpha) 35 | ColorSpaces.CieLab -> LAB50(red, green, blue, alpha) 36 | ColorSpaces.CieXyz -> XYZ50(red, green, blue, alpha) 37 | ColorSpaces.DciP3 -> DCI_P3(red, green, blue, alpha) 38 | ColorSpaces.DisplayP3 -> DisplayP3(red, green, blue, alpha) 39 | ColorSpaces.LinearSrgb -> LinearSRGB(red, green, blue, alpha) 40 | ColorSpaces.ProPhotoRgb -> ROMM_RGB(red, green, blue, alpha) 41 | else -> convert(ColorSpaces.Srgb).let { SRGB(it.red, it.green, it.blue, it.alpha) } 42 | } 43 | } 44 | 45 | /** 46 | * Convert this color to a Colormath [SRGB] instance. 47 | */ 48 | fun ComposeColor.toColormathSRGB(): RGB { 49 | return convert(ColorSpaces.Srgb).let { SRGB(it.red, it.green, it.blue, it.alpha) } 50 | } 51 | 52 | /** 53 | * Convert this color to a Jetpack Compose [Color][androidx.compose.ui.graphics.Color] instance. 54 | */ 55 | fun Color.toComposeColor(): ComposeColor { 56 | if (this is RGBInt) return ComposeColor(argb.toInt()) 57 | val s = when { 58 | space == SRGB -> ColorSpaces.Srgb 59 | space === ACES -> ColorSpaces.Aces 60 | space === ACEScg -> ColorSpaces.Acescg 61 | space === AdobeRGB -> ColorSpaces.AdobeRgb 62 | space === BT2020 -> ColorSpaces.Bt2020 63 | space === BT709 -> ColorSpaces.Bt709 64 | space === LAB50 -> ColorSpaces.CieLab 65 | space === XYZ50 -> ColorSpaces.CieXyz 66 | space === DCI_P3 -> ColorSpaces.DciP3 67 | space === DisplayP3 -> ColorSpaces.DisplayP3 68 | space === LinearSRGB -> ColorSpaces.LinearSrgb 69 | space === ROMM_RGB -> ColorSpaces.ProPhotoRgb 70 | space == Oklab -> ColorSpaces.Oklab 71 | else -> null 72 | } 73 | 74 | return if (s == null) { 75 | val (r, g, b, a) = toSRGB().clamp() 76 | ComposeColor(r, g, b, a) 77 | } else { 78 | val (r, g, b, a) = clamp().toArray() 79 | ComposeColor(r, g, b, a, s) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /extensions/colormath-ext-jetpack-compose/src/jvmTest/kotlin/com/github/ajalt/colormath/extensions/android/colorint/ComposeColorExtensionsTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.colormath.extensions.android.colorint 2 | 3 | import androidx.compose.ui.graphics.Color 4 | import com.github.ajalt.colormath.ColorSpace 5 | import com.github.ajalt.colormath.convertTo 6 | import com.github.ajalt.colormath.extensions.android.composecolor.toColormathColor 7 | import com.github.ajalt.colormath.extensions.android.composecolor.toColormathSRGB 8 | import com.github.ajalt.colormath.extensions.android.composecolor.toComposeColor 9 | import com.github.ajalt.colormath.model.JzAzBz 10 | import com.github.ajalt.colormath.model.LABColorSpaces.LAB50 11 | import com.github.ajalt.colormath.model.Oklab 12 | import com.github.ajalt.colormath.model.RGB 13 | import com.github.ajalt.colormath.model.RGBColorSpaces.ACES 14 | import com.github.ajalt.colormath.model.RGBColorSpaces.ACEScg 15 | import com.github.ajalt.colormath.model.RGBColorSpaces.AdobeRGB 16 | import com.github.ajalt.colormath.model.RGBColorSpaces.BT2020 17 | import com.github.ajalt.colormath.model.RGBColorSpaces.BT709 18 | import com.github.ajalt.colormath.model.RGBColorSpaces.DCI_P3 19 | import com.github.ajalt.colormath.model.RGBColorSpaces.DisplayP3 20 | import com.github.ajalt.colormath.model.RGBColorSpaces.LinearSRGB 21 | import com.github.ajalt.colormath.model.RGBColorSpaces.ROMM_RGB 22 | import com.github.ajalt.colormath.model.RGBColorSpaces.SRGB 23 | import com.github.ajalt.colormath.model.XYZ 24 | import com.github.ajalt.colormath.model.XYZColorSpaces.XYZ50 25 | import kotlin.test.Test 26 | import kotlin.test.assertEquals 27 | import kotlin.test.fail 28 | 29 | class ComposeColorExtensionsTest { 30 | private val colormathBlue = RGB(0, 0, 1, 1) 31 | 32 | // TODO(kotest): once kotest is released, go back to using it 33 | @Test 34 | fun toColormathColor() { 35 | assertEquals(colormathBlue, Color.Blue.toColormathColor()) 36 | } 37 | 38 | @Test 39 | fun toColormathSRGB() { 40 | assertEquals(colormathBlue, Color.Blue.toColormathSRGB()) 41 | } 42 | 43 | @Test 44 | fun toComposeColor() { 45 | assertEquals(Color.Blue, colormathBlue.toComposeColor()) 46 | } 47 | 48 | @Test 49 | fun outOfGamut() = listOf>( 50 | SRGB, 51 | ACES, 52 | ACEScg, 53 | AdobeRGB, 54 | BT2020, 55 | BT709, 56 | LAB50, 57 | XYZ50, 58 | DCI_P3, 59 | DisplayP3, 60 | LinearSRGB, 61 | ROMM_RGB, 62 | XYZ, 63 | Oklab, 64 | JzAzBz, 65 | ).forEach { space: ColorSpace<*> -> 66 | val color = RGB(9, 9, 9, 9).convertTo(space) 67 | 68 | // shouldNotThrow 69 | 70 | color.clamp().toComposeColor() 71 | 72 | // shouldThrow 73 | try { 74 | color.toComposeColor() 75 | } catch (e: IllegalArgumentException) { 76 | // expected 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | VERSION_NAME=3.6.1 2 | kotlin.mpp.stability.nowarn=true 3 | android.useAndroidX=true 4 | 5 | org.jetbrains.compose.experimental.wasm.enabled=true 6 | kotlin.native.enableKlibsCrossCompilation=true 7 | 8 | # https://kotlinlang.org/docs/whatsnew18.html#configuration-and-setup 9 | kotlin.mpp.androidSourceSetLayoutVersion=2 10 | 11 | # gradle-maven-publish configuration 12 | SONATYPE_HOST=DEFAULT 13 | RELEASE_SIGNING_ENABLED=true 14 | GROUP=com.github.ajalt.colormath 15 | POM_DESCRIPTION=Multiplatform color space conversions for Kotlin 16 | POM_INCEPTION_YEAR=2021 17 | POM_URL=https://github.com/ajalt/colormath/ 18 | POM_LICENSE_NAME=The MIT License 19 | POM_LICENSE_URL=https://opensource.org/licenses/MIT 20 | POM_LICENSE_DIST=repo 21 | POM_SCM_URL=https://github.com/ajalt/colormath/ 22 | POM_SCM_CONNECTION=scm:git:git://github.com/ajalt/colormath.git 23 | POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/ajalt/colormath.git 24 | POM_DEVELOPER_ID=ajalt 25 | POM_DEVELOPER_NAME=AJ Alt 26 | POM_DEVELOPER_URL=https://github.com/ajalt/ 27 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | kotlin = "2.1.0" 3 | compose = "1.7.3" 4 | compose-plugin = "1.7.3" 5 | 6 | [libraries] 7 | 8 | # used in extensions 9 | androidx-annotation = "androidx.annotation:annotation:1.7.0" 10 | compose-ui-graphics = { module = "org.jetbrains.compose.ui:ui-graphics", version.ref = "compose" } 11 | 12 | # used in tests 13 | kotest = "io.kotest:kotest-assertions-core:5.9.1" 14 | junit = "junit:junit:4.13.2" 15 | robolectric = "org.robolectric:robolectric:4.14.1" 16 | 17 | # used in samples 18 | compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" } 19 | compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "compose" } 20 | 21 | 22 | [plugins] 23 | dokka = "org.jetbrains.dokka:2.0.0" 24 | publish = "com.vanniktech.maven.publish:0.30.0" 25 | kotlinBinaryCompatibilityValidator = "org.jetbrains.kotlinx.binary-compatibility-validator:0.17.0" 26 | 27 | # used in extensions 28 | android-library = "com.android.library:8.7.3" 29 | 30 | # used in samples 31 | jetbrainsCompose = { id = "org.jetbrains.compose", version.ref = "compose-plugin" } 32 | compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } 33 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajalt/colormath/5e776b66ea6ffc5dc5dc360ab88ca5aefbd2a310/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 1>&2 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 48 | echo. 1>&2 49 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 50 | echo location of your Java installation. 1>&2 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 1>&2 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 62 | echo. 1>&2 63 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 64 | echo location of your Java installation. 1>&2 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Colormath 2 | repo_name: Colormath 3 | repo_url: https://github.com/ajalt/colormath 4 | site_description: "Colormath: Multiplatform color space conversions for Kotlin" 5 | site_author: AJ Alt 6 | remote_branch: gh-pages 7 | 8 | copyright: 'Copyright © 2021 AJ Alt' 9 | 10 | theme: 11 | name: 'material' 12 | logo: img/palette_white_24dp.svg 13 | favicon: img/favicon.ico 14 | palette: 15 | scheme: slate 16 | primary: deep purple 17 | accent: purple 18 | icon: 19 | repo: fontawesome/brands/github 20 | 21 | markdown_extensions: 22 | - smarty 23 | - codehilite: 24 | guess_lang: false 25 | - footnotes 26 | - meta 27 | - toc: 28 | permalink: true 29 | - pymdownx.betterem: 30 | smart_enable: all 31 | - pymdownx.caret 32 | - pymdownx.inlinehilite 33 | - pymdownx.magiclink 34 | - pymdownx.smartsymbols 35 | - pymdownx.superfences 36 | - pymdownx.tabbed 37 | - tables 38 | - admonition 39 | - md_in_html 40 | - attr_list 41 | 42 | nav: 43 | - 'Getting Started': index.md 44 | - 'Usage': usage.md 45 | - 'Color Spaces': colorspaces.md 46 | - 'Extensions': extensions.md 47 | - 'Try It Online': tryit/index.html 48 | - 'API Reference': api/colormath/com.github.ajalt.colormath/index.html 49 | - 'Releases': changelog.md 50 | -------------------------------------------------------------------------------- /prepare_docs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # The website is built using MkDocs with the Material theme. 4 | # https://squidfunk.github.io/mkdocs-material/ 5 | # It requires Python to run. 6 | # Install the packages with the following command: 7 | # pip install mkdocs mkdocs-material 8 | # Build the samples and api docs with 9 | # ./gradlew dokkaHtml :website:wasmJsBrowserDistribution 10 | # Then run this script to prepare the docs for the website. 11 | # Finally, run `mkdocs serve` to preview the site locally or `mkdocs build` to build the site. 12 | 13 | 14 | set -ex 15 | 16 | # Copy the changelog into the site, omitting the unreleased section 17 | cat CHANGELOG.md \ 18 | | grep -v '^## Unreleased' \ 19 | | sed '/^## /,$!d' \ 20 | > docs/changelog.md 21 | 22 | # Add the jinja frontmatter to the index 23 | cat > docs/index.md <<- EOM 24 | --- 25 | hide: 26 | - toc # Hide table of contents 27 | --- 28 | 29 | EOM 30 | 31 | # Copy the README into the index, omitting the license and fixing hrefs 32 | cat README.md \ 33 | | sed '/## License/Q' \ 34 | | sed -e '/## Documentation/,/Gradient generator/d' \ 35 | | sed 's!https://ajalt.github.io/colormath/!/!g' \ 36 | | sed 's!docs/img!img!g' \ 37 | >> docs/index.md 38 | 39 | # Copy the website js into the docs 40 | mkdir -p docs/tryit 41 | cp -r website/build/dist/wasmJs/productionExecutable/* docs/tryit/ 42 | -------------------------------------------------------------------------------- /scripts/benchmarks/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | kotlin("jvm") 3 | id( "me.champeau.jmh") version "0.6.5" 4 | } 5 | 6 | dependencies { 7 | api(kotlin("stdlib")) 8 | implementation(rootProject) 9 | } 10 | -------------------------------------------------------------------------------- /scripts/benchmarks/src/jmh/kotlin/com/github/ajalt/colormath/benchmark/ColorBenchmarks.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.colormath.benchmark 2 | 3 | import com.github.ajalt.colormath.* 4 | import com.github.ajalt.colormath.transform.EasingFunctions 5 | import com.github.ajalt.colormath.transform.createChromaticAdapter 6 | import com.github.ajalt.colormath.transform.interpolator 7 | import com.github.ajalt.colormath.transform.sequence 8 | import org.openjdk.jmh.annotations.* 9 | import java.util.concurrent.TimeUnit 10 | 11 | 12 | private val interpolator = RGB.interpolator(RGB("#000a"), RGB("#fffa"), premultiplyAlpha = false) 13 | private val interpolatorPrumult = RGB.interpolator(RGB("#000a"), RGB("#fffa")) 14 | private val adapter = RGBInt.createChromaticAdapter(RGBInt(200, 210, 220)) 15 | private val rgbInt = RGBInt(11, 222, 33) 16 | private val bezier = EasingFunctions.cubicBezier(0.3, -1, 0.7, 2) 17 | 18 | @Warmup(iterations = 3, time = 1) 19 | @Measurement(iterations = 3, time = 1) 20 | @OutputTimeUnit(TimeUnit.NANOSECONDS) 21 | @BenchmarkMode(Mode.AverageTime) 22 | @Fork(1) 23 | open class ColorBenchmarks { 24 | @Benchmark 25 | open fun interpolate(): RGB { 26 | return interpolator.interpolate(0.5f) 27 | } 28 | 29 | @Benchmark 30 | open fun interpolatePremultiply(): RGB { 31 | return interpolatorPrumult.interpolate(0.5f) 32 | } 33 | 34 | @Benchmark 35 | @OperationsPerInvocation(10) 36 | open fun sequence(): List { 37 | return interpolator.sequence(10).toList() 38 | } 39 | 40 | @Benchmark 41 | @OperationsPerInvocation(10) 42 | open fun sequencePremultiply(): List { 43 | return interpolatorPrumult.sequence(10).toList() 44 | } 45 | 46 | @Benchmark 47 | open fun cubicBezier(): Float { 48 | return bezier.ease(0.2f) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /scripts/generate_tests.py: -------------------------------------------------------------------------------- 1 | """ 2 | This script generates some of the color conversion test cases. It requires the `colour-science` library. 3 | """ 4 | 5 | import warnings 6 | 7 | warnings.filterwarnings("ignore") 8 | 9 | import colour 10 | from colour.colorimetry import CCS_ILLUMINANTS 11 | from colour.utilities.common import domain_range_scale 12 | from colour.models import * 13 | from functools import partial 14 | from oklab import XYZ_to_Oklab, Oklab_to_XYZ 15 | 16 | test_cases = [ 17 | [0, 0, 0], 18 | [0.18, 0.18, 0.18], 19 | [0.4, 0.5, 0.6], 20 | [1.0, 1.0, 1.0], 21 | ] 22 | 23 | cmyk_test_cases = [ 24 | [0, 0, 0, 0], 25 | [0.18, 0.18, 0.18, 0.18], 26 | [0.4, 0.5, 0.6, 0.7], 27 | [1.0, 1.0, 1.0, 1.0], 28 | ] 29 | 30 | ictcp_test_cases = [ 31 | [0, 0, 0], 32 | [0.08, 0, 0], 33 | [0.1, 0.01, -0.01], 34 | [0.15, 0, 0], 35 | ] 36 | 37 | illuminants = CCS_ILLUMINANTS['CIE 1931 2 Degree Standard Observer'] 38 | 39 | colour.RGB_COLOURSPACES['sRGB'].use_derived_transformation_matrices(True) 40 | 41 | 42 | def convert_rgb(input, output, c, decode=True, encode=True): 43 | i = colour.RGB_COLOURSPACES[input] 44 | i.use_derived_transformation_matrices(True) 45 | o = colour.RGB_COLOURSPACES[output] 46 | o.use_derived_transformation_matrices(True) 47 | return RGB_to_RGB(c, i, o, apply_cctf_decoding=decode, apply_cctf_encoding=encode, is_12_bits_system=True) 48 | 49 | 50 | def row(s1, v1, s2, v2): 51 | def f(n): 52 | s = f'{n:.8f}'.rstrip('0').replace('nan', 'NaN') 53 | return s + '0' if s.endswith('.') else s 54 | 55 | c1 = ', '.join(f'{v:.2f}' for v in v1) 56 | c2 = ', '.join(f(v) for v in v2) 57 | 58 | print(f' {s1}({c1}) to {s2}({c2}),') 59 | 60 | 61 | def cases(s1, s2, f, tests=test_cases, scale=()): 62 | for v in tests: 63 | with domain_range_scale('1'): 64 | v = v + list(f(v)) 65 | for i, s in scale: 66 | v[i] *= s 67 | row(s1, v[:int(len(v) / 2 + 0.5)], s2, v[int(len(v) / 2 + 0.5):]) 68 | 69 | 70 | def rgb_tests(): 71 | names_to_spaces = [ 72 | ['ACES2065-1', 'ACES'], 73 | ['ACEScc', 'ACEScc'], 74 | ['ACEScct', 'ACEScct'], 75 | ['ACEScg', 'ACEScg'], 76 | ['Adobe RGB (1998)', 'ADOBE_RGB'], 77 | ['ITU-R BT.2020', 'BT_2020'], 78 | ['ITU-R BT.709', 'BT_709'], 79 | ['DCI-P3', 'DCI_P3'], 80 | ['Display P3', 'DISPLAY_P3'], 81 | ['ROMM RGB', 'ROMM_RGB'], 82 | ] 83 | 84 | for (name, space) in names_to_spaces: 85 | print(f' @Test\n' 86 | f' fun {space}Test() = doTest(') 87 | cases('SRGB', space, partial(convert_rgb, 'sRGB', name)) 88 | cases(space, 'SRGB', partial(convert_rgb, name, 'sRGB')) 89 | 90 | print(' )\n') 91 | 92 | 93 | def LCHuv_to_HCL(lchuv): 94 | return [lchuv[2], lchuv[1], lchuv[0]] 95 | 96 | 97 | def compose(*fns): 98 | def f(c): 99 | val = c 100 | for fn in fns: 101 | val = fn(val) 102 | return val 103 | 104 | return f 105 | 106 | 107 | def color_tests(): 108 | tests = [ 109 | ['XYZ', 'JzAzBz', XYZ_to_JzAzBz], 110 | ['XYZ', 'Oklab', XYZ_to_Oklab], 111 | ['XYZ', 'LUV', XYZ_to_Luv], 112 | ['XYZ', 'LAB', XYZ_to_Lab], 113 | ['XYZ', 'RGB', XYZ_to_sRGB], 114 | ['RGB', 'HSV', RGB_to_HSV, [(3, 360)]], 115 | ['RGB', 'HSL', RGB_to_HSL, [(3, 360)]], 116 | ['RGB', 'XYZ', sRGB_to_XYZ], 117 | ['RGB', 'LAB', compose(sRGB_to_XYZ, XYZ_to_Lab)], 118 | ['RGB', 'LUV', compose(sRGB_to_XYZ, XYZ_to_Luv)], 119 | ['RGB', 'CMYK', compose(RGB_to_CMY, CMY_to_CMYK)], 120 | ['RGB', 'Oklab', compose(sRGB_to_XYZ, XYZ_to_Oklab)], 121 | ['CMYK', 'RGB', compose(CMYK_to_CMY, CMY_to_RGB), [], cmyk_test_cases], 122 | ['HCL', 'LUV', LCHuv_to_Luv, [(2, 360)]], 123 | ['HSL', 'RGB', HSL_to_RGB, [(0, 360)]], 124 | ['HSL', 'HSV', compose(HSL_to_RGB, RGB_to_HSV), [(0, 360), (3, 360)]], 125 | ['HSV', 'RGB', HSV_to_RGB, [(0, 360)]], 126 | ['HSV', 'HSL', compose(HSV_to_RGB, RGB_to_HSL), [(0, 360), (3, 360)]], 127 | ['JzCzHz', 'JzAzBz', JCh_to_Jab, [(2, 360)]], 128 | ['LAB', 'XYZ', Lab_to_XYZ, [(0, 100), (1, 100), (2, 100)]], 129 | ['LAB50', 'XYZ50', partial(Lab_to_XYZ, illuminant=illuminants['D50']), [(5, 360)]], 130 | ['LAB', 'LCH', Lab_to_LCHab, [(i, 360 if i == 5 else 100) for i in range(6)]], 131 | ['LCH', 'LAB', LCHab_to_Lab, [(i, 360 if i == 2 else 100) for i in range(6)]], 132 | ['LUV', 'XYZ', Luv_to_XYZ, [(i, 100) for i in range(3)]], 133 | ['LUV', 'HCL', compose(Luv_to_LCHuv, LCHuv_to_HCL), [(i, 360 if i == 3 else 100) for i in range(6)]], 134 | ['Oklab', 'XYZ', Oklab_to_XYZ], 135 | ['Oklab', 'RGB', compose(Oklab_to_XYZ, XYZ_to_sRGB)], 136 | ['ICtCp', 'BT_2020', compose(ICTCP_to_RGB, partial(eotf_inverse_BT2020, is_12_bits_system=True)), [], ictcp_test_cases], 137 | ['ICtCp', 'SRGB', compose(ICTCP_to_RGB, partial(convert_rgb, 'ITU-R BT.2020', 'sRGB', decode=False)), [], ictcp_test_cases], 138 | ] 139 | 140 | for test in tests: 141 | l, r, f = test[:3] 142 | s = test[3] if len(test) > 3 else [] 143 | c = test[4] if len(test) > 4 else test_cases 144 | print(f' @Test\n' 145 | f' fun {l}_to_{r}() = testColorConversions(') 146 | cases(l, r, f, scale=s, tests=c) 147 | print(' )\n') 148 | 149 | 150 | if __name__ == '__main__': 151 | rgb_tests() 152 | color_tests() 153 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | include("colormath") 2 | include("test") 3 | include("extensions:colormath-ext-android-color") 4 | include("extensions:colormath-ext-android-colorint") 5 | include("extensions:colormath-ext-jetpack-compose") 6 | include("website") 7 | include("scripts:benchmarks") 8 | 9 | rootProject.name = "colormath-root" 10 | 11 | // For compose web 12 | pluginManagement { 13 | repositories { 14 | gradlePluginPortal() 15 | google() 16 | mavenCentral() 17 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") 18 | } 19 | } 20 | 21 | @Suppress("UnstableApiUsage") 22 | dependencyResolutionManagement { 23 | repositories { 24 | mavenCentral() 25 | google() 26 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /test/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | kotlin("multiplatform") 3 | } 4 | 5 | repositories { 6 | mavenCentral() 7 | } 8 | 9 | kotlin { 10 | jvm() 11 | js { nodejs() } 12 | 13 | linuxX64() 14 | linuxArm64() 15 | mingwX64() 16 | macosX64() 17 | macosArm64() 18 | iosX64() 19 | iosArm64() 20 | iosSimulatorArm64() 21 | tvosX64() 22 | tvosArm64() 23 | tvosSimulatorArm64() 24 | watchosX64() 25 | watchosArm32() 26 | watchosArm64() 27 | watchosSimulatorArm64() 28 | 29 | sourceSets { 30 | val commonTest by getting { 31 | dependencies { 32 | api(project(":colormath")) 33 | implementation(libs.kotest) 34 | implementation(kotlin("test")) 35 | } 36 | } 37 | } 38 | } 39 | 40 | tasks.withType().configureEach { 41 | manifest { 42 | attributes("Automatic-Module-Name" to "com.github.ajalt.colormath") 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /test/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_ARTIFACT_ID=colormath 2 | POM_NAME=Colormath 3 | -------------------------------------------------------------------------------- /test/src/commonTest/kotlin/com/github/ajalt/colormath/HueColorTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.colormath 2 | 3 | import com.github.ajalt.colormath.model.HSL 4 | import io.kotest.assertions.assertSoftly 5 | import io.kotest.matchers.shouldBe 6 | import kotlin.math.PI 7 | import kotlin.test.Test 8 | 9 | class HueColorTest { 10 | @Test 11 | fun hueAsRad(): Unit = assertSoftly { 12 | HSL(0, 0, 0).hueAsRad() shouldBe 0f 13 | HSL(180, 0, 0).hueAsRad() shouldBe PI.toFloat() 14 | HSL(360, 0, 0).hueAsRad() shouldBe (2 * PI).toFloat() 15 | } 16 | 17 | @Test 18 | fun hueAsGrad(): Unit = assertSoftly { 19 | HSL(0, 0, 0).hueAsGrad() shouldBe 0f 20 | HSL(180, 0, 0).hueAsGrad() shouldBe 200f 21 | HSL(360, 0, 0).hueAsGrad() shouldBe 400f 22 | } 23 | 24 | @Test 25 | fun hueAsTurns(): Unit = assertSoftly { 26 | HSL(0, 0, 0).hueAsTurns() shouldBe 0f 27 | HSL(180, 0, 0).hueAsTurns() shouldBe .5f 28 | HSL(360, 0, 0).hueAsTurns() shouldBe 1f 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /test/src/commonTest/kotlin/com/github/ajalt/colormath/TestUtils.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.colormath 2 | 3 | import com.github.ajalt.colormath.model.SRGB 4 | import io.kotest.data.blocking.forAll 5 | import io.kotest.data.row 6 | import io.kotest.matchers.doubles.shouldBeNaN 7 | import io.kotest.matchers.floats.plusOrMinus 8 | import io.kotest.matchers.nulls.shouldBeNull 9 | import io.kotest.matchers.nulls.shouldNotBeNull 10 | import io.kotest.matchers.shouldBe 11 | 12 | fun testColorConversions( 13 | vararg rows: Pair, 14 | tolerance: Double = 5e-5, 15 | ignorePolar: Boolean = false, 16 | testInverse: Boolean = true, 17 | ) { 18 | val pairs = rows.map { row(it.first, it.second) } 19 | val inverse = if (testInverse) rows.map { row(it.second, it.first) } else emptyList() 20 | forAll(*(pairs + inverse).toTypedArray()) { l, r -> 21 | r.space.convert(l).shouldEqualColor(r, tolerance, ignorePolar) 22 | } 23 | } 24 | 25 | fun roundtripTest(vararg colors: T, intermediate: ColorSpace<*> = SRGB) { 26 | val rows = colors.flatMap { listOf(row(it, "self"), row(it, "intermediate"), row(it, "array")) } 27 | forAll(*rows.toTypedArray()) { it, case -> 28 | when (case) { 29 | "self" -> it.space.convert(it).shouldEqualColor(it) 30 | "intermediate" -> it.space.convert(intermediate.convert(it)).shouldEqualColor(it) 31 | "array" -> it.space.create(it.toArray()).shouldEqualColor(it) 32 | } 33 | } 34 | } 35 | 36 | // Test both directions to ensure that equals is symmetric 37 | fun companionTest(companion: ColorSpace<*>, actual: ColorSpace<*>) = forAll( 38 | row(companion, actual), 39 | row(actual, companion), 40 | ) { l, r -> 41 | l shouldBe r 42 | l.create(FloatArray(companion.components.size)).space shouldBe r 43 | } 44 | 45 | fun convertToSpaceTest(vararg spaces: ColorSpace<*>, to: ColorSpace<*>) { 46 | forAll(*spaces.map { row(it) }.toTypedArray()) { 47 | it.create(floatArrayOf(.1f, .2f, .3f)).convertTo(to).space shouldBe to 48 | } 49 | } 50 | 51 | fun Color?.shouldEqualColor( 52 | expected: Color?, 53 | tolerance: Double = 5e-4, 54 | ignorePolar: Boolean = false, 55 | ) { 56 | if (expected == null) { 57 | this.shouldBeNull() 58 | return 59 | } 60 | 61 | this.shouldNotBeNull() 62 | try { 63 | this::class shouldBe expected::class 64 | space shouldBe expected.space 65 | 66 | val l = toArray() 67 | val r = expected.toArray() 68 | l.size shouldBe r.size 69 | for (i in l.indices) { 70 | if (ignorePolar && space.components[i].isPolar) continue 71 | l[i].shouldBeFloat(r[i], tolerance) 72 | } 73 | } catch (e: AssertionError) { 74 | println("┌ ex ${expected.toSRGB().toHex()} $expected") 75 | println("└ ac ${this.toSRGB().toHex()} $this") 76 | throw e 77 | } 78 | } 79 | 80 | fun Float.shouldBeFloat(ex: Float, tolerance: Double = 0.0) { 81 | if (ex.isNaN()) toDouble().shouldBeNaN() 82 | else this shouldBe (ex plusOrMinus tolerance.toFloat()) 83 | } 84 | -------------------------------------------------------------------------------- /test/src/commonTest/kotlin/com/github/ajalt/colormath/calculate/ContrastTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.colormath.calculate 2 | 3 | import com.github.ajalt.colormath.model.RGB 4 | import io.kotest.data.blocking.forAll 5 | import io.kotest.data.row 6 | import io.kotest.matchers.floats.plusOrMinus 7 | import io.kotest.matchers.shouldBe 8 | import kotlin.test.Test 9 | 10 | // test cases from https://www.w3.org/TR/css-color-5/#colorcontrast 11 | class ContrastTest { 12 | @Test 13 | fun wcagLuminance() = forAll( 14 | row(RGB("#f5deb3"), 0.749), 15 | row(RGB("#d2b48c"), 0.482), 16 | row(RGB("#a0522d"), 0.137), 17 | row(RGB("#b22222"), 0.107), 18 | ) { c, ex -> 19 | c.wcagLuminance() shouldBe (ex.toFloat() plusOrMinus 0.001f) 20 | } 21 | 22 | @Test 23 | fun wcagContrastRatio() = forAll( 24 | row(RGB("#d2b48c"), 1.501), 25 | row(RGB("#a0522d"), 4.273), 26 | row(RGB("#b22222"), 5.081), 27 | ) { c, ex -> 28 | RGB("#f5deb3").wcagContrastRatio(c) shouldBe (ex.toFloat() plusOrMinus 0.001f) 29 | } 30 | 31 | @Test 32 | fun mostContrasting() { 33 | RGB("#f5deb3").mostContrasting( 34 | RGB("#d2b48c"), RGB("#a0522d"), RGB("#b22222"), RGB("#d2691e") 35 | ) shouldBe RGB("#b22222") 36 | } 37 | 38 | @Test 39 | fun firstWithContrastOrNull() = forAll( 40 | row(4.5, RGB("#006400")), 41 | row(5.8, RGB("#800000")), 42 | row(9.9, null), 43 | ) { r, ex -> 44 | RGB("#f5deb3").firstWithContrastOrNull( 45 | RGB("#ffe4c4"), RGB("#b8860b"), RGB("#808000"), RGB("#a0522d"), RGB("#006400"), RGB("#800000"), 46 | targetContrast = r.toFloat() 47 | ) shouldBe ex 48 | } 49 | 50 | @Test 51 | fun firstWithContrast() { 52 | RGB("#f5deb3").firstWithContrast( 53 | RGB("#ffe4c4"), RGB("#b8860b"), targetContrast = 99f 54 | ) shouldBe RGB("#000") 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /test/src/commonTest/kotlin/com/github/ajalt/colormath/calculate/DifferenceTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.colormath.calculate 2 | 3 | import com.github.ajalt.colormath.model.LAB 4 | import io.kotest.data.blocking.forAll 5 | import io.kotest.data.row 6 | import io.kotest.matchers.doubles.plusOrMinus 7 | import io.kotest.matchers.shouldBe 8 | import kotlin.test.Test 9 | 10 | class DifferenceTest { 11 | private val c1 = LAB(100.00000000, 21.57210357, 272.22819350) 12 | 13 | @Test 14 | fun cie76() = forAll( 15 | row(c1, 0.0), 16 | row(LAB(100.00000000, 426.67945353, 72.39590835), 451.713301974), 17 | row(LAB(100.00000000, 74.05216981, 276.45318193), 52.6498611564), 18 | row(LAB(100.00000000, 08.32281957, -73.58297716), 346.064891718), 19 | ) { c, ex -> 20 | c1.differenceCIE76(c).toDouble() shouldBe (ex plusOrMinus 1e-4) 21 | c1.euclideanDistance(c).toDouble() shouldBe (ex plusOrMinus 1e-4) 22 | } 23 | 24 | @Test 25 | fun cie94() = forAll( 26 | row(c1, 0.0, false), 27 | row(LAB(100.00000000, 426.67945353, 72.39590835), 83.77922550, false), 28 | row(LAB(100.00000000, 74.05216981, 276.45318193), 10.05393195, false), 29 | row(LAB(100.00000000, 08.32281957, -73.58297716), 57.53545370, false), 30 | row(LAB(100.00000000, 426.67945353, 72.39590835), 88.33555305, true), 31 | row(LAB(100.00000000, 74.05216981, 276.45318193), 10.61265789, true), 32 | row(LAB(100.00000000, 08.32281957, -73.58297716), 60.36868726, true), 33 | ) { c, ex, textiles -> 34 | c1.differenceCIE94(c, textiles).toDouble() shouldBe (ex plusOrMinus 1e-5) 35 | } 36 | 37 | @Test 38 | fun cie2000() = forAll( 39 | row(c1, 0.0), 40 | row(LAB(100.00000000, 426.67945353, 72.39590835), 94.0356490267), 41 | row(LAB(100.00000000, 74.05216981, 276.45318193), 14.8790641937), 42 | row(LAB(100.00000000, 08.32281957, -73.58297716), 68.2309487895), 43 | ) { c, ex -> 44 | c1.differenceCIE2000(c).toDouble() shouldBe (ex plusOrMinus 1e-5) 45 | } 46 | 47 | @Test 48 | fun cmc() = forAll( 49 | row(c1, 0.0, 1f), 50 | row(LAB(100.00000000, 426.67945353, 72.39590835), 172.7047712, 2f), 51 | row(LAB(100.00000000, 74.05216981, 276.45318193), 20.59732717, 2f), 52 | row(LAB(100.00000000, 08.32281957, -73.58297716), 121.7184147, 2f), 53 | row(LAB(100.00000000, 426.67945353, 72.39590835), 172.7047712, 1f), 54 | row(LAB(100.00000000, 74.05216981, 276.45318193), 20.59732717, 1f), 55 | row(LAB(100.00000000, 08.32281957, -73.58297716), 121.7184147, 1f), 56 | ) { c, ex, l -> 57 | c1.differenceCMC(c, l = l).toDouble() shouldBe (ex plusOrMinus 1e-5) 58 | } 59 | 60 | @Test 61 | fun deltaEz() = forAll( 62 | // test values from https://observablehq.com/@jrus/jzazbz 63 | row(c1, 0.0), 64 | row(LAB(100.00000000, 426.67945353, 72.39590835), 0.10972654), 65 | row(LAB(100.00000000, 74.05216981, 276.45318193), 0.01307468), 66 | row(LAB(100.00000000, 08.32281957, -73.58297716), 0.05543110), 67 | ) { c, ex -> 68 | c1.differenceEz(c).toDouble() shouldBe (ex plusOrMinus 1e-5) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /test/src/commonTest/kotlin/com/github/ajalt/colormath/calculate/GamutTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.colormath.calculate 2 | 3 | import com.github.ajalt.colormath.model.RGB 4 | import io.kotest.data.blocking.forAll 5 | import io.kotest.data.row 6 | import io.kotest.matchers.shouldBe 7 | import kotlin.test.Test 8 | 9 | class GamutTest { 10 | @Test 11 | fun isInSRGBGamut() = forAll( 12 | row(RGB("#f5deb3"), true), 13 | row(RGB("#000"), true), 14 | row(RGB("#fff"), true), 15 | row(RGB(-0.01, 0.0, 0.0), false), 16 | row(RGB(0.0, -0.01, 0.0), false), 17 | row(RGB(0.0, 0.0, -0.01), false), 18 | row(RGB(1.1f, 1.1f, 1.1f), false), 19 | ) { c, ex -> 20 | c.isInSRGBGamut() shouldBe ex 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test/src/commonTest/kotlin/com/github/ajalt/colormath/model/Ansi16Test.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.colormath.model 2 | 3 | import com.github.ajalt.colormath.roundtripTest 4 | import com.github.ajalt.colormath.testColorConversions 5 | import kotlin.js.JsName 6 | import kotlin.test.Test 7 | 8 | class Ansi16Test { 9 | @Test 10 | fun roundtrip() = roundtripTest(Ansi16(30)) 11 | 12 | @[Test JsName("Ansi16_to_RGB")] 13 | fun `Ansi16 to RGB`() = listOf( 14 | Ansi16(30) to RGBInt(0, 0, 0), 15 | Ansi16(31) to RGBInt(128, 0, 0), 16 | Ansi16(32) to RGBInt(0, 128, 0), 17 | Ansi16(33) to RGBInt(128, 128, 0), 18 | Ansi16(34) to RGBInt(0, 0, 128), 19 | Ansi16(35) to RGBInt(128, 0, 128), 20 | Ansi16(36) to RGBInt(0, 128, 128), 21 | Ansi16(37) to RGBInt(192, 192, 192), 22 | Ansi16(90) to RGBInt(128, 128, 128), 23 | Ansi16(91) to RGBInt(255, 0, 0), 24 | Ansi16(92) to RGBInt(0, 255, 0), 25 | Ansi16(93) to RGBInt(255, 255, 0), 26 | Ansi16(94) to RGBInt(0, 0, 255), 27 | Ansi16(95) to RGBInt(255, 0, 255), 28 | Ansi16(96) to RGBInt(0, 255, 255), 29 | Ansi16(97) to RGBInt(255, 255, 255), 30 | ).let { 31 | val tests = it + it.map { (l, r) -> Ansi16(l.code + 10) to r } 32 | testColorConversions(*tests.toTypedArray(), testInverse = false) 33 | } 34 | 35 | @[Test JsName("Ansi16_to_Ansi256")] 36 | fun `Ansi16 to Ansi256`() = testColorConversions( 37 | Ansi16(30) to Ansi256(0), 38 | Ansi16(31) to Ansi256(1), 39 | Ansi16(32) to Ansi256(2), 40 | Ansi16(33) to Ansi256(3), 41 | Ansi16(34) to Ansi256(4), 42 | Ansi16(35) to Ansi256(5), 43 | Ansi16(36) to Ansi256(6), 44 | Ansi16(37) to Ansi256(7), 45 | Ansi16(90) to Ansi256(8), 46 | Ansi16(91) to Ansi256(9), 47 | Ansi16(92) to Ansi256(10), 48 | Ansi16(93) to Ansi256(11), 49 | Ansi16(94) to Ansi256(12), 50 | Ansi16(95) to Ansi256(13), 51 | Ansi16(96) to Ansi256(14), 52 | Ansi16(97) to Ansi256(15), 53 | ) 54 | } 55 | -------------------------------------------------------------------------------- /test/src/commonTest/kotlin/com/github/ajalt/colormath/model/CMYKTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.colormath.model 2 | 3 | import com.github.ajalt.colormath.testColorConversions 4 | import kotlin.js.JsName 5 | import kotlin.test.Test 6 | 7 | class CMYKTest { 8 | @[Test JsName("CMYK_to_RGB")] 9 | fun `CMYK to RGB`() = testColorConversions( 10 | CMYK(0.00, 0.00, 0.00, 0.00) to RGB(1.0, 1.0, 1.0), 11 | CMYK(0.18, 0.18, 0.18, 0.18) to RGB(0.6724, 0.6724, 0.6724), 12 | CMYK(0.40, 0.50, 0.60, 0.70) to RGB(0.18, 0.15, 0.12), 13 | CMYK(100, 100, 100, 100) to RGB(0.0, 0.0, 0.0), 14 | testInverse = false 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /test/src/commonTest/kotlin/com/github/ajalt/colormath/model/HPLuvTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.colormath.model 2 | 3 | import com.github.ajalt.colormath.roundtripTest 4 | import com.github.ajalt.colormath.testColorConversions 5 | import kotlin.js.JsName 6 | import kotlin.test.Test 7 | 8 | class HPLuvTest { 9 | @Test 10 | fun roundtrip() = roundtripTest(HPLuv(0.1, 0.011, 0.012, 0.04), intermediate = LCHuv) 11 | 12 | @[Test JsName("LCHuv_to_HPLuv")] 13 | fun `LCHuv to HPLuv`() = testColorConversions( 14 | LCHuv(0.00, 0.00, Double.NaN) to HPLuv(Double.NaN, 0.0, 0.0), 15 | LCHuv(0.18, 0.18, 64.80) to HPLuv(64.8, 126.8934854430029, 0.18), 16 | LCHuv(0.40, 0.50, 216.00) to HPLuv(216.0, 158.6168568037536, 0.4), 17 | LCHuv(1.00, 1.00, 0.00) to HPLuv(0.0, 126.89348544300287, 1.0), 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /test/src/commonTest/kotlin/com/github/ajalt/colormath/model/HSLTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.colormath.model 2 | 3 | import com.github.ajalt.colormath.roundtripTest 4 | import com.github.ajalt.colormath.shouldEqualColor 5 | import com.github.ajalt.colormath.testColorConversions 6 | import io.kotest.data.blocking.forAll 7 | import io.kotest.data.row 8 | import io.kotest.matchers.types.shouldBeSameInstanceAs 9 | import kotlin.js.JsName 10 | import kotlin.test.Test 11 | 12 | class HSLTest { 13 | @Test 14 | fun roundtrip() = roundtripTest(HSL(0.01, 0.02, 0.03, 0.04)) 15 | 16 | @[Test JsName("HSL_to_RGB")] 17 | fun `HSL to RGB`() = testColorConversions( 18 | HSL(Double.NaN, 0.00, 0.00) to RGB(0.0, 0.0, 0.0), 19 | HSL(64.80, 0.18, 0.18) to RGB(0.207216, 0.2124, 0.1476), 20 | HSL(144.00, 0.50, 0.60) to RGB(0.4, 0.8, 0.56), 21 | HSL(Double.NaN, 0.00, 1.00) to RGB(1.0, 1.0, 1.0), 22 | ) 23 | 24 | @[Test JsName("HSL_to_HSV")] 25 | fun `HSL to HSV`() = testColorConversions( 26 | HSL(0.00, 0.00, 0.00) to HSV(0.0, 0.0, 0.0), 27 | HSL(64.80, 0.18, 0.18) to HSV(64.8, 0.30508475, 0.2124), 28 | HSL(144.00, 0.50, 0.60) to HSV(144.0, 0.5, 0.8), 29 | HSL(0.00, 0.00, 1.00) to HSV(0.0, 0.0, 1.0), 30 | ) 31 | 32 | @Test 33 | fun clamp() { 34 | forAll( 35 | row(HSL(0.0, 0.0, 0.0), HSL(0.0, 0.0, 0.0)), 36 | row(HSL(359, 1.0, 1.0), HSL(359, 1.0, 1.0)), 37 | row(HSL(361, 1.0, 1.0), HSL(1, 1.0, 1.0)), 38 | row(HSL(180, 2, 2), HSL(180, 1.0, 1.0)), 39 | ) { hsl, ex -> 40 | hsl.clamp().shouldEqualColor(ex) 41 | } 42 | val hsl = HSL(359, .9, .9, .9) 43 | hsl.clamp().shouldBeSameInstanceAs(hsl) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /test/src/commonTest/kotlin/com/github/ajalt/colormath/model/HSLuvTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.colormath.model 2 | 3 | import com.github.ajalt.colormath.roundtripTest 4 | import com.github.ajalt.colormath.testColorConversions 5 | import kotlin.js.JsName 6 | import kotlin.test.Test 7 | 8 | class HSLuvTest { 9 | @Test 10 | fun roundtrip() = roundtripTest(HSLuv(0.1, 0.011, 0.012, 0.04), intermediate = LCHuv) 11 | 12 | // Test cases generated from the reference implementation 13 | @[Test JsName("LCHuv_to_HSLuv")] 14 | fun `LCHuv to HSLuv`() = testColorConversions( 15 | LCHuv(0.00, 0.00, Double.NaN) to HSLuv(Double.NaN, 0.0, 0.0), 16 | LCHuv(0.18, 0.18, 64.80) to HSLuv(64.8, 86.24411410293375, 0.18), 17 | LCHuv(0.40, 0.50, 216.00) to HSLuv(216.0, 138.871331171819, 0.4), 18 | LCHuv(1.00, 1.00, 0.00) to HSLuv(0.0, 36.33223336102162, 1.0), 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /test/src/commonTest/kotlin/com/github/ajalt/colormath/model/HSVTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.colormath.model 2 | 3 | import com.github.ajalt.colormath.roundtripTest 4 | import com.github.ajalt.colormath.testColorConversions 5 | import kotlin.js.JsName 6 | import kotlin.test.Test 7 | 8 | class HSVTest { 9 | @Test 10 | fun roundtrip() = roundtripTest(HSV(0.01, 0.02, 0.03, 0.04)) 11 | 12 | @[Test JsName("HSV_to_RGB")] 13 | fun `HSV to RGB`() = testColorConversions( 14 | HSV(Double.NaN, 0.00, 0.00) to RGB(0.0, 0.0, 0.0), 15 | HSV(64.80, 0.18, 0.18) to RGB(0.177408, 0.18, 0.1476), 16 | HSV(144.00, 0.50, 0.60) to RGB(0.3, 0.6, 0.42), 17 | HSV(0.00, 1.00, 1.00) to RGB(1.0, 0.0, 0.0), 18 | ) 19 | 20 | @[Test JsName("HSV_to_RGB_NaN_S")] 21 | fun `HSV to RGB NaN S`() = testColorConversions( 22 | HSV(Double.NaN, Double.NaN, 0.00) to RGB(0.0, 0.0, 0.0), 23 | HSV(0.00, Double.NaN, 0.00) to RGB(0.0, 0.0, 0.0), 24 | HSV(Double.NaN, Double.NaN, 0.50) to RGB(0.5, 0.5, 0.5), 25 | testInverse = false, 26 | ) 27 | 28 | @[Test JsName("HSV_to_HSL")] 29 | fun `HSV to HSL`() = testColorConversions( 30 | HSV(0.00, 0.00, 0.00) to HSL(0.0, 0.0, 0.0), 31 | HSV(64.80, 0.18, 0.18) to HSL(64.8, 0.0989011, 0.1638), 32 | HSV(144.00, 0.50, 0.60) to HSL(144.0, 0.33333333, 0.45), 33 | HSV(0.00, 1.00, 1.00) to HSL(0.0, 1.0, 0.5), 34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /test/src/commonTest/kotlin/com/github/ajalt/colormath/model/HWBTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.colormath.model 2 | 3 | import com.github.ajalt.colormath.roundtripTest 4 | import com.github.ajalt.colormath.testColorConversions 5 | import kotlin.js.JsName 6 | import kotlin.test.Test 7 | 8 | class HWBTest { 9 | @Test 10 | fun roundtrip() = roundtripTest(HWB(0.01, 0.02, 0.03, 0.04)) 11 | 12 | @[Test JsName("HWB_to_RGB")] 13 | // https://www.w3.org/TR/css-color-4/#hwb-examples 14 | // At the time of this writing, no browsers implemented hwb. These tests are based on the 15 | // example colors from the working draft, and a few differ by one point on a single channel, 16 | // presumably due to the use of a different algorithm. 17 | fun `HWB to RGB`() = testColorConversions( 18 | HWB(000.0, .400, .400) to RGB("#996666"), 19 | HWB(030.0, .400, .400) to RGB("#998066"), 20 | HWB(060.0, .400, .400) to RGB("#999966"), 21 | HWB(090.0, .400, .400) to RGB("#809966"), 22 | HWB(120.0, .400, .400) to RGB("#669966"), 23 | HWB(150.0, .400, .400) to RGB("#669980"), 24 | HWB(180.0, .400, .400) to RGB("#669999"), 25 | HWB(210.0, .400, .400) to RGB("#668099"), 26 | HWB(240.0, .400, .400) to RGB("#666699"), 27 | HWB(270.0, .400, .400) to RGB("#806699"), 28 | HWB(300.0, .400, .400) to RGB("#996699"), 29 | HWB(330.0, .400, .400) to RGB("#996680"), 30 | 31 | HWB(90.0, .000, .000) to RGB("#80ff00"), 32 | HWB(90.0, .600, .200) to RGB("#b3cc99"), 33 | HWB(90.0, .200, .600) to RGB("#4c6633"), 34 | HWB(00.0, .400, .600) to RGB("#666666"), 35 | tolerance = 5e-3, 36 | testInverse = false 37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /test/src/commonTest/kotlin/com/github/ajalt/colormath/model/ICtCpTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.colormath.model 2 | 3 | import com.github.ajalt.colormath.model.RGBColorSpaces.BT2020 4 | import com.github.ajalt.colormath.roundtripTest 5 | import com.github.ajalt.colormath.testColorConversions 6 | import kotlin.js.JsName 7 | import kotlin.test.Test 8 | 9 | class ICtCpTest { 10 | @Test 11 | fun roundtrip() = roundtripTest(ICtCp(0.01, 0.011, 0.012, 0.04)) 12 | 13 | @[Test JsName("ICtCp_to_BT2020")] 14 | fun `ICtCp to BT2020`() = testColorConversions( 15 | ICtCp(0.00, 0.00, 0.00) to BT2020(0.0, 0.0, 0.0), 16 | ICtCp(0.08, 0.00, 0.00) to BT2020(0.41300407, 0.41300407, 0.41300407), 17 | ICtCp(0.10, 0.01, -0.01) to BT2020(0.51900627, 0.57112792, 0.64131823), 18 | ICtCp(0.15, 0.00, 0.00) to BT2020(1.00052666, 1.00052666, 1.00052666), 19 | ) 20 | 21 | @[Test JsName("ICtCp_to_sRGB")] 22 | fun `ICtCp to sRGB`() = testColorConversions( 23 | ICtCp(0.00, 0.00, 0.00) to SRGB(0.0, 0.0, 0.0), 24 | ICtCp(0.08, 0.00, 0.00) to SRGB(0.46526684, 0.46526684, 0.46526684), 25 | ICtCp(0.10, 0.01, -0.01) to SRGB(0.52319301, 0.6175152, 0.68473126), 26 | ICtCp(0.15, 0.00, 0.00) to SRGB(1.00046799, 1.00046799, 1.00046799), 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /test/src/commonTest/kotlin/com/github/ajalt/colormath/model/JzAzBzTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.colormath.model 2 | 3 | import com.github.ajalt.colormath.roundtripTest 4 | import com.github.ajalt.colormath.testColorConversions 5 | import kotlin.js.JsName 6 | import kotlin.test.Test 7 | 8 | class JzAzBzTest { 9 | @Test 10 | fun roundtrip() = roundtripTest(JzAzBz(0.01, 0.011, 0.012, 0.04)) 11 | 12 | @[Test JsName("JzAzBz_to_XYZ")] 13 | fun `JzAzBz to XYZ`() = testColorConversions( 14 | XYZ(0.00, 0.00, 0.00) to JzAzBz(0.0, 0.0, 0.0), 15 | XYZ(0.18, 0.18, 0.18) to JzAzBz(0.00594105, 0.00092704, 0.00074672), 16 | XYZ(0.40, 0.50, 0.60) to JzAzBz(0.01104753, -0.00494082, -0.00195568), 17 | XYZ(1.00, 1.00, 1.00) to JzAzBz(0.01777968, 0.00231107, 0.00187447), 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /test/src/commonTest/kotlin/com/github/ajalt/colormath/model/JzCzHzTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.colormath.model 2 | 3 | import com.github.ajalt.colormath.roundtripTest 4 | import com.github.ajalt.colormath.testColorConversions 5 | import kotlin.js.JsName 6 | import kotlin.test.Test 7 | 8 | 9 | class JzCzHzTest { 10 | @Test 11 | fun roundtrip() = roundtripTest(JzCzHz(0.01, 0.02, 0.03, 0.04)) 12 | 13 | @[Test JsName("JzCzHz_to_JzAzBz")] 14 | fun `JzCzHz to JzAzBz`() = testColorConversions( 15 | JzCzHz(0.00, 0.00, Double.NaN) to JzAzBz(0.0, 0.0, 0.0), 16 | JzCzHz(0.18, 0.18, 64.80) to JzAzBz(0.18, 0.07664027, 0.16286887), 17 | JzCzHz(0.40, 0.50, 216.00) to JzAzBz(0.4, -0.4045085, -0.29389263), 18 | JzCzHz(1.00, 1.00, 0.00) to JzAzBz(1.0, 1.0, -0.0), 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /test/src/commonTest/kotlin/com/github/ajalt/colormath/model/LABTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.colormath.model 2 | 3 | import com.github.ajalt.colormath.* 4 | import com.github.ajalt.colormath.model.LABColorSpaces.LAB50 5 | import com.github.ajalt.colormath.model.LABColorSpaces.LAB65 6 | import com.github.ajalt.colormath.model.LCHabColorSpaces.LCHab50 7 | import com.github.ajalt.colormath.model.LCHabColorSpaces.LCHab65 8 | import kotlin.js.JsName 9 | import kotlin.test.Test 10 | 11 | class LABTest { 12 | @Test 13 | fun roundtrip() = roundtripTest(LAB(0.01, 0.02, 0.03, 0.04)) 14 | 15 | @Test 16 | fun conversion() = convertToSpaceTest(LAB65, LCHab50, LCHab65, HSL, to = LAB50) 17 | 18 | @Test 19 | fun companion() = companionTest(LAB, LAB65) 20 | 21 | @[Test JsName("LAB_to_XYZ")] 22 | fun `LAB to XYZ`() = testColorConversions( 23 | LAB(0.00, 0.00, 0.00) to XYZ(0.0, 0.0, 0.0), 24 | LAB(18.00, 18.00, 18.00) to XYZ(0.0338789, 0.02518041, 0.0091147), 25 | LAB(40.00, 50.00, 60.00) to XYZ(0.18810403, 0.11250974, 0.00626937), 26 | LAB(100.00, 100.00, 100.00) to XYZ(1.64238784, 1.0, 0.13613222), 27 | ) 28 | 29 | @[Test JsName("LAB_to_LCHab")] 30 | fun `LAB to LCHab`() = testColorConversions( 31 | LAB(0.00, 0.00, 0.00) to LCHab(0.0, 0.0, Double.NaN), 32 | LAB(18.00, 18.00, 18.00) to LCHab(18.0, 25.45584412, 45.0), 33 | LAB(40.00, 50.00, 60.00) to LCHab(40.0, 78.10249676, 50.19442891), 34 | LAB(100.00, 100.00, 100.00) to LCHab(100.0, 141.42135624, 45.0), 35 | LAB50(100.00, 100.00, 100.00) to LCHab50(100.0, 141.42135624, 45.0), 36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /test/src/commonTest/kotlin/com/github/ajalt/colormath/model/LCHabTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.colormath.model 2 | 3 | import com.github.ajalt.colormath.companionTest 4 | import com.github.ajalt.colormath.convertToSpaceTest 5 | import com.github.ajalt.colormath.model.LABColorSpaces.LAB50 6 | import com.github.ajalt.colormath.model.LABColorSpaces.LAB65 7 | import com.github.ajalt.colormath.model.LCHabColorSpaces.LCHab50 8 | import com.github.ajalt.colormath.model.LCHabColorSpaces.LCHab65 9 | import com.github.ajalt.colormath.roundtripTest 10 | import com.github.ajalt.colormath.testColorConversions 11 | import kotlin.js.JsName 12 | import kotlin.test.Test 13 | 14 | 15 | class LCHabTest { 16 | @Test 17 | fun roundtrip() = roundtripTest(LCHab(0.1, 0.011, 0.015, 0.04), intermediate = LAB) 18 | 19 | @Test 20 | fun conversion() = convertToSpaceTest(LAB65, LCHab65, LAB50, HSL, to = LCHab50) 21 | 22 | @Test 23 | fun companion() = companionTest(LCHab, LCHab65) 24 | 25 | @[Test JsName("LCHab_to_LAB")] 26 | fun `LCHab to LAB`() = testColorConversions( 27 | LCHab(0.00, 0.00, Double.NaN) to LAB(0.0, 0.0, 0.0), 28 | LCHab(18.00, 18.00, 64.80) to LAB(18.0, 7.66402725, 16.28688694), 29 | LCHab(40.00, 50.00, 216.00) to LAB(40.0, -40.45084972, -29.38926261), 30 | LCHab(100.00, 100.00, 0.00) to LAB(100.0, 100.0, -0.0), 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /test/src/commonTest/kotlin/com/github/ajalt/colormath/model/LCHuvTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.colormath.model 2 | 3 | import com.github.ajalt.colormath.companionTest 4 | import com.github.ajalt.colormath.convertToSpaceTest 5 | import com.github.ajalt.colormath.model.LCHuvColorSpaces.LCHuv50 6 | import com.github.ajalt.colormath.model.LCHuvColorSpaces.LCHuv65 7 | import com.github.ajalt.colormath.model.LUVColorSpaces.LUV50 8 | import com.github.ajalt.colormath.model.LUVColorSpaces.LUV65 9 | import com.github.ajalt.colormath.roundtripTest 10 | import com.github.ajalt.colormath.testColorConversions 11 | import kotlin.js.JsName 12 | import kotlin.test.Test 13 | 14 | 15 | class LCHuvTest { 16 | @Test 17 | fun roundtrip() = roundtripTest(LCHuv(0.01, 0.02, 0.03, 0.04), intermediate = LUV) 18 | 19 | @Test 20 | fun conversion() = convertToSpaceTest(LCHuv65, LUV65, LUV50, HSL, to = LCHuv50) 21 | 22 | @Test 23 | fun companion() = companionTest(LCHuv, LCHuv65) 24 | 25 | @[Test JsName("LCHuv_to_LUV")] 26 | fun `LCHuv to LUV`() = testColorConversions( 27 | LCHuv(0.00, 0.00, Double.NaN) to LUV(0.0, 0.0, 0.0), 28 | LCHuv(0.18, 0.18, 64.80) to LUV(0.18, 0.07664027, 0.16286887), 29 | LCHuv(0.40, 0.50, 216.00) to LUV(0.4, -0.4045085, -0.29389263), 30 | LCHuv(1.00, 1.00, 0.00) to LUV(1.0, 1.0, -0.0), 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /test/src/commonTest/kotlin/com/github/ajalt/colormath/model/LUVTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.colormath.model 2 | 3 | import com.github.ajalt.colormath.companionTest 4 | import com.github.ajalt.colormath.convertToSpaceTest 5 | import com.github.ajalt.colormath.model.LCHuvColorSpaces.LCHuv50 6 | import com.github.ajalt.colormath.model.LCHuvColorSpaces.LCHuv65 7 | import com.github.ajalt.colormath.model.LUVColorSpaces.LUV50 8 | import com.github.ajalt.colormath.model.LUVColorSpaces.LUV65 9 | import com.github.ajalt.colormath.roundtripTest 10 | import com.github.ajalt.colormath.testColorConversions 11 | import kotlin.js.JsName 12 | import kotlin.test.Test 13 | 14 | class LUVTest { 15 | @Test 16 | fun roundtrip() = roundtripTest(LUV(0.01, 0.02, 0.03, 0.04)) 17 | 18 | @Test 19 | fun conversion() = convertToSpaceTest(LUV65, LCHuv65, LCHuv50, HSL, to = LUV50) 20 | 21 | @Test 22 | fun companion() = companionTest(LUV, LUV65) 23 | 24 | @[Test JsName("LUV_to_XYZ")] 25 | fun `LUV to XYZ`() = testColorConversions( 26 | LUV(0.00, 0.00, 0.00) to XYZ(0.0, 0.0, 0.0), 27 | LUV(18.00, 18.00, 18.00) to XYZ(0.02854945, 0.02518041, 0.00312744), 28 | LUV(40.00, 50.00, 60.00) to XYZ(0.12749789, 0.11250974, -0.02679452), 29 | LUV(100.00, 100.00, 100.00) to XYZ(1.13379604, 1.0, 0.12420117), 30 | tolerance = 5e-4, 31 | ) 32 | 33 | @[Test JsName("LUV_to_LCHuv")] 34 | fun `LUV to LCHuv`() = testColorConversions( 35 | LUV(0.00, 0.00, 0.00) to LCHuv(0.0, 0.0, Double.NaN), 36 | LUV(18.00, 18.00, 18.00) to LCHuv(18.0, 25.45584412, 45.0), 37 | LUV(40.00, 50.00, 60.00) to LCHuv(40.0, 78.10249676, 50.19442891), 38 | LUV(100.00, 100.00, 100.00) to LCHuv(100.0, 141.42135624, 45.0), 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /test/src/commonTest/kotlin/com/github/ajalt/colormath/model/OklabTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.colormath.model 2 | 3 | import com.github.ajalt.colormath.roundtripTest 4 | import com.github.ajalt.colormath.testColorConversions 5 | import kotlin.js.JsName 6 | import kotlin.test.Test 7 | 8 | class OklabTest { 9 | @Test 10 | fun roundtrip() = roundtripTest(Oklab(0.1, 0.011, 0.012, 0.04)) 11 | 12 | @[Test JsName("Oklab_to_XYZ")] 13 | fun `Oklab to XYZ`() = testColorConversions( 14 | Oklab(0.00, 0.00, 0.00) to XYZ(0.0, 0.0, 0.0), 15 | Oklab(0.18, 0.18, 0.18) to XYZ(0.02802839, 0.00274835, -0.0037864), 16 | Oklab(0.40, 0.50, 0.60) to XYZ(0.43551106, 0.02244794, -0.15905992), 17 | Oklab(1.00, 1.00, 1.00) to XYZ(4.80596605, 0.47125258, -0.6492456), 18 | ) 19 | 20 | @[Test JsName("Oklab_to_RGB")] 21 | fun `Oklab to RGB`() = testColorConversions( 22 | Oklab(0.00, 0.00, 0.00) to RGB(0.0, 0.0, 0.0), 23 | Oklab(0.18, 0.18, 0.18) to RGB(0.3291339, -0.28640902, -0.03880515), 24 | Oklab(0.40, 0.50, 0.60) to RGB(1.17887364, -4.99505886, -1.91827315), 25 | Oklab(1.00, 1.00, 1.00) to RGB(3.22136291, -49.10991381, -6.65383232), 26 | tolerance = 5e-2 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /test/src/commonTest/kotlin/com/github/ajalt/colormath/model/OklchTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.colormath.model 2 | 3 | import com.github.ajalt.colormath.roundtripTest 4 | import com.github.ajalt.colormath.shouldEqualColor 5 | import com.github.ajalt.colormath.testColorConversions 6 | import io.kotest.data.blocking.forAll 7 | import io.kotest.data.row 8 | import io.kotest.matchers.types.shouldBeSameInstanceAs 9 | import kotlin.js.JsName 10 | import kotlin.test.Test 11 | 12 | 13 | class OklchTest { 14 | @Test 15 | fun roundtrip() = roundtripTest(Oklch(0.01, 0.02, 0.03, 0.04), intermediate = Oklab) 16 | 17 | @[Test JsName("Oklab_to_Oklch")] 18 | fun `Oklab to Oklch`() = testColorConversions( 19 | Oklab(0.0, 0.0, 0.0) to Oklch(0.0, 0.0, Double.NaN), 20 | Oklab(0.18, 0.18, 0.18) to Oklch(0.18, 0.25455844, 45.0), 21 | Oklab(0.25, 0.5, 0.75) to Oklch(0.25, 0.90138782, 56.30993247), 22 | Oklab(1.0, 1.0, 1.0) to Oklch(1.0, 1.41421356, 45.0), 23 | ) 24 | 25 | @Test 26 | fun clamp() { 27 | forAll( 28 | row(Oklch(0.0, 0.0, 0.0), Oklch(0.0, 0.0, 0.0)), 29 | row(Oklch(-1, -1, 361, 3), Oklch(0.0, 0.0, 1)), 30 | ) { color, ex -> 31 | color.clamp().shouldEqualColor(ex) 32 | } 33 | val oklch = Oklch(.9, .2, 359, .9) 34 | oklch.clamp().shouldBeSameInstanceAs(oklch) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test/src/commonTest/kotlin/com/github/ajalt/colormath/model/RGBColorSpacesTransferFunctionsTest.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("TestFunctionName") 2 | 3 | package com.github.ajalt.colormath.model 4 | 5 | import com.github.ajalt.colormath.model.RGBColorSpaces.ACEScc 6 | import com.github.ajalt.colormath.model.RGBColorSpaces.ACEScct 7 | import com.github.ajalt.colormath.model.RGBColorSpaces.AdobeRGB 8 | import com.github.ajalt.colormath.model.RGBColorSpaces.BT2020 9 | import com.github.ajalt.colormath.model.RGBColorSpaces.BT709 10 | import com.github.ajalt.colormath.model.RGBColorSpaces.DCI_P3 11 | import com.github.ajalt.colormath.model.RGBColorSpaces.DisplayP3 12 | import com.github.ajalt.colormath.model.RGBColorSpaces.ROMM_RGB 13 | import io.kotest.data.blocking.forAll 14 | import io.kotest.data.row 15 | import io.kotest.matchers.doubles.plusOrMinus 16 | import io.kotest.matchers.shouldBe 17 | import kotlin.test.Test 18 | 19 | // Test values from https://github.com/colour-science/ 20 | class RGBColorSpacesTransferFunctionsTest { 21 | @Test 22 | fun SRGB() = doTest(SRGB, 0.01292, 0.46135612950044164) 23 | 24 | @Test 25 | fun ADOBE_RGB() = doTest(AdobeRGB, 0.043239356144868332, 0.45852946567989455) 26 | 27 | @Test 28 | fun BT_2020() = doTest(BT2020, 0.0045, 0.40884640249350368) 29 | 30 | @Test 31 | fun BT_709() = doTest(BT709, 0.0045, 0.409007728864150) 32 | 33 | @Test 34 | fun BT_709_extra() = forAll( 35 | row(0.015, 0.0675, "oetf"), 36 | row(0.0675, 0.015, "eotf"), 37 | ) { input, ex, func -> 38 | doSingleTest(BT709, input, ex, func) 39 | } 40 | 41 | @Test 42 | fun DCI_P3() = doTest(DCI_P3, 0.070170382867038292, 0.5170902489415321) 43 | 44 | @Test 45 | fun DISPLAY_P3() = doTest(DisplayP3, 0.01292, 0.46135612950044164) 46 | 47 | @Test 48 | fun ROMM_RGB() = doTest(ROMM_RGB, 0.016, 0.385711424751138) 49 | 50 | @Test 51 | fun ACEScc() = doTest(ACEScc, -0.01402878337112365, 0.413588402492442, -0.358447488584475, 0.554794520547945) 52 | 53 | @Test 54 | fun ACEScct() = doTest(ACEScct, 0.08344577193748999, 0.413588402492442, 0.072905534195835495, 0.554794520547945) 55 | 56 | private fun doTest(space: RGBColorSpace, zzOne: Double, eighteen: Double, zero: Double = 0.0, one: Double = 1.0) { 57 | forAll( 58 | row(0.0, zero, "oetf"), 59 | row(0.001, zzOne, "oetf"), 60 | row(0.18, eighteen, "oetf"), 61 | row(1.0, one, "oetf"), 62 | 63 | row(zero, 0.0, "eotf"), 64 | row(zzOne, 0.001, "eotf"), 65 | row(eighteen, 0.18, "eotf"), 66 | row(one, 1.0, "eotf"), 67 | ) { input, ex, func -> 68 | doSingleTest(space, input, ex, func) 69 | } 70 | } 71 | 72 | private fun doSingleTest( 73 | space: RGBColorSpace, 74 | input: Double, 75 | ex: Double, 76 | func: String, 77 | ) { 78 | val actual = when (func) { 79 | "oetf" -> space.transferFunctions.oetf(input.toFloat()) 80 | else -> space.transferFunctions.eotf(input.toFloat()) 81 | } 82 | actual.toDouble() shouldBe (ex plusOrMinus 1e-6) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /test/src/commonTest/kotlin/com/github/ajalt/colormath/model/RGBIntTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.colormath.model 2 | 3 | import com.github.ajalt.colormath.roundtripTest 4 | import com.github.ajalt.colormath.testColorConversions 5 | import io.kotest.matchers.shouldBe 6 | import kotlin.js.JsName 7 | import kotlin.test.Test 8 | 9 | class RGBIntTest { 10 | @Test 11 | fun roundtrip() = roundtripTest( 12 | RGBInt(128, 128, 128, 128), 13 | RGBInt(128u, 128u, 128u, 128u), 14 | RGBInt(128.toUByte(), 128.toUByte(), 128.toUByte(), 128.toUByte()), 15 | ) 16 | 17 | @Test 18 | fun rgba() { 19 | RGBInt(1, 2, 3, 4).toRGBA() shouldBe 0x01020304u 20 | RGBInt.fromRGBA(0x01020304u).argb shouldBe 0x04010203u 21 | } 22 | 23 | @[Test JsName("RGBInt_to_RGB")] 24 | fun `RGBInt to RGB`() = testColorConversions( 25 | RGBInt(0x00000000u) to RGB.from255(0, 0, 0, 0), 26 | RGBInt(0x00800000u) to RGB.from255(128, 0, 0, 0), 27 | RGBInt(0x00008000u) to RGB.from255(0, 128, 0, 0), 28 | RGBInt(0x00808000u) to RGB.from255(128, 128, 0, 0), 29 | RGBInt(0x00000080u) to RGB.from255(0, 0, 128, 0), 30 | RGBInt(0x00800080u) to RGB.from255(128, 0, 128, 0), 31 | RGBInt(0x00008080u) to RGB.from255(0, 128, 128, 0), 32 | RGBInt(0x0000ff00u) to RGB.from255(0, 255, 0, 0), 33 | RGBInt(0x00ffff00u) to RGB.from255(255, 255, 0, 0), 34 | RGBInt(0xffffffffu) to RGB.from255(255, 255, 255, 255), 35 | RGBInt(0x33aaaaaau) to RGB.from255(170, 170, 170, 51), 36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /test/src/commonTest/kotlin/com/github/ajalt/colormath/model/XYZTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.colormath.model 2 | 3 | import com.github.ajalt.colormath.companionTest 4 | import com.github.ajalt.colormath.model.LABColorSpaces.LAB50 5 | import com.github.ajalt.colormath.model.LUVColorSpaces.LUV50 6 | import com.github.ajalt.colormath.model.XYZColorSpaces.XYZ50 7 | import com.github.ajalt.colormath.model.XYZColorSpaces.XYZ65 8 | import com.github.ajalt.colormath.roundtripTest 9 | import com.github.ajalt.colormath.testColorConversions 10 | import kotlin.js.JsName 11 | import kotlin.test.Test 12 | 13 | class XYZTest { 14 | @Test 15 | fun roundtrip() = roundtripTest(XYZ(0.01, 0.02, 0.03, 0.04)) 16 | 17 | @Test 18 | fun companion() = companionTest(XYZ, XYZ65) 19 | 20 | @[Test JsName("XYZ_to_RGB")] 21 | fun `XYZ to SRGB`() = testColorConversions( 22 | XYZ(0.00, 0.00, 0.00) to RGB(0.0, 0.0, 0.0), 23 | XYZ(0.18, 0.18, 0.18) to RGB(0.50307213, 0.45005582, 0.44114606), 24 | XYZ(0.40, 0.50, 0.60) to RGB(0.51535521, 0.78288241, 0.77013935), 25 | XYZ(1.00, 1.00, 1.00) to RGB(1.08523261, 0.97691161, 0.95870753), 26 | ) 27 | 28 | @[Test JsName("XYZ_to_LAB")] 29 | fun `XYZ to LAB`() = testColorConversions( 30 | XYZ(0.00, 0.00, 0.00) to LUV(0.0, 0.0, 0.0), 31 | XYZ(0.18, 0.18, 0.18) to LUV(49.49610761, 8.16943249, 3.4516013), 32 | XYZ(0.40, 0.50, 0.60) to LUV(76.06926101, -32.51658072, -4.35360349), 33 | XYZ(1.00, 1.00, 1.00) to LUV(100.0, 16.50520189, 6.97348026), 34 | XYZ50(0.25, 0.5, 0.75) to LAB50(76.06926101, -78.02949711, -34.99756832), 35 | ) 36 | 37 | @[Test JsName("XYZ_to_LUV")] 38 | fun `XYZ to LUV`() = testColorConversions( 39 | XYZ(0.000, 0.000, 0.000) to LUV(0.0, 0.0, 0.0), 40 | XYZ(0.18, 0.18, 0.18) to LUV(49.49610761, 8.16943249, 3.4516013), 41 | XYZ(0.40, 0.50, 0.60) to LUV(76.06926101, -32.51658072, -4.35360349), 42 | XYZ(1.00, 1.00, 1.00) to LUV(100.0, 16.50520189, 6.97348026), 43 | XYZ50(0.25, 0.5, 0.75) to LUV50(76.06926101, -107.96735088, -37.65708044), 44 | ) 45 | 46 | @[Test JsName("XYZ_to_Oklab")] 47 | fun `XYZ to Oklab`() = testColorConversions( 48 | XYZ(0.00, 0.00, 0.00) to Oklab(0.0, 0.0, 0.0), 49 | XYZ(0.18, 0.18, 0.18) to Oklab(0.56645328, 0.01509528, 0.00832456), 50 | XYZ(0.40, 0.50, 0.60) to Oklab(0.78539542, -0.06758384, -0.01449969), 51 | XYZ(1.00, 1.00, 1.00) to Oklab(1.00324405, 0.02673522, 0.0147436), 52 | XYZ(0.18, 0.18, 0.18).adaptTo(XYZ50) to Oklab(0.56645328, 0.01509528, 0.00832456), 53 | // TODO(kotest): reenable this when kotest supports wasm 54 | // XYZ(0.18, 0.18, 0.18).adaptTo(XYZ50, CAT02_XYZ_TO_LMS) to Oklab( 55 | // 0.56645328, 0.01509528, 0.00832456 56 | // ), 57 | // XYZ(0.18, 0.18, 0.18).adaptTo(XYZ50, CAT02_XYZ_TO_LMS, CAT02_LMS_TO_XYZ.rowMajor) to Oklab( 58 | // 0.56645328, 0.01509528, 0.00832456 59 | // ), 60 | ) 61 | 62 | @[Test JsName("XYZ_to_JzAzBz")] 63 | fun `XYZ to JzAzBz`() = testColorConversions( 64 | XYZ(0.00, 0.00, 0.00) to JzAzBz(0.0, 0.0, 0.0), 65 | XYZ(0.18, 0.18, 0.18) to JzAzBz(0.00594105, 0.00092704, 0.00074672), 66 | XYZ(0.40, 0.50, 0.60) to JzAzBz(0.01104753, -0.00494082, -0.00195568), 67 | XYZ(1.00, 1.00, 1.00) to JzAzBz(0.01777968, 0.00231107, 0.00187447), 68 | XYZ(0.40, 0.50, 0.60).adaptTo(XYZ50) to JzAzBz(0.01104753, -0.00494082, -0.00195568), 69 | ) 70 | } 71 | -------------------------------------------------------------------------------- /test/src/commonTest/kotlin/com/github/ajalt/colormath/transform/EasingFunctionsTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.colormath.transform 2 | 3 | import io.kotest.data.blocking.forAll 4 | import io.kotest.data.row 5 | import io.kotest.matchers.doubles.plusOrMinus 6 | import io.kotest.matchers.shouldBe 7 | import kotlin.test.Test 8 | 9 | class EasingFunctionsTest { 10 | @Test 11 | fun cubicBezier() = forAll( 12 | row(0, 1, 1, 1, 1e-9, 0.0), 13 | row(0.3, -1, 0.7, 2, 0.0, 0.0), 14 | row(0.3, -1, 0.7, 2, 0.2, -0.1752580), 15 | row(0.3, -1, 0.7, 2, 0.4, 0.22073628), 16 | row(0.3, -1, 0.7, 2, 0.6, 0.77926371), 17 | row(0.3, -1, 0.7, 2, 0.8, 1.17525804), 18 | row(0.3, -1, 0.7, 2, 1.0, 1.0), 19 | ) { x1, y1, x2, y2, t, ex -> 20 | EasingFunctions.cubicBezier(x1, y1, x2, y2).ease(t.toFloat()).toDouble() shouldBe (ex plusOrMinus 1e-5) 21 | } 22 | 23 | @Test 24 | fun builtInEasings() = forAll( 25 | row(EasingFunctions.ease(), 0.2, 0.2952443), 26 | row(EasingFunctions.ease(), 0.8, 0.9756253), 27 | row(EasingFunctions.easeIn(), 0.2, 0.0622820), 28 | row(EasingFunctions.easeIn(), 0.8, 0.6916339), 29 | row(EasingFunctions.easeOut(), 0.2, 0.3083660), 30 | row(EasingFunctions.easeOut(), 0.8, 0.9377179), 31 | row(EasingFunctions.easeInOut(), 0.2, 0.0816598), 32 | row(EasingFunctions.easeInOut(), 0.8, 0.9183401), 33 | ) { fn, t, ex -> 34 | fn.ease(t.toFloat()).toDouble() shouldBe (ex plusOrMinus 1e-5) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test/src/commonTest/kotlin/com/github/ajalt/colormath/transform/HueAdjustmentsTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.colormath.transform 2 | 3 | import com.github.ajalt.colormath.model.HSL 4 | import com.github.ajalt.colormath.shouldBeFloat 5 | import io.kotest.matchers.shouldBe 6 | import kotlin.math.roundToInt 7 | import kotlin.test.Test 8 | 9 | class HueAdjustmentsTest { 10 | @Test 11 | fun nan() { 12 | val l = listOf(0f, Float.NaN, 10f, 20f, Float.NaN, Float.NaN) 13 | val ac = HueAdjustments.shorter(l) 14 | for ((a, ex) in ac.zip(l)) a.shouldBeFloat(ex) 15 | } 16 | 17 | @Test 18 | fun shorter() = doTest( 19 | HueAdjustments.shorter, 20 | listOf(0, 300, 50, 0, 100), 21 | listOf(0, -60, 50, 0, 100), 22 | ) 23 | 24 | @Test 25 | fun longer() = doTest( 26 | HueAdjustments.longer, 27 | listOf(0, 300, 50, 0, 100), 28 | listOf(0, 300, 50, 360, 100), 29 | ) 30 | 31 | @Test 32 | fun increasing() = doTest( 33 | HueAdjustments.increasing, 34 | listOf(0, 300, 50, 0, 100), 35 | listOf(0, 300, 410, 720, 820), 36 | ) 37 | 38 | @Test 39 | fun decreasing() = doTest( 40 | HueAdjustments.decreasing, 41 | listOf(0, 300, 50, 0, 100), 42 | listOf(0, -60, -310, -360, -620), 43 | ) 44 | 45 | @Test 46 | fun specified() = doTest( 47 | HueAdjustments.specified, 48 | listOf(0, 300, 50, 0, 100), 49 | listOf(0, 300, 50, 0, 100), 50 | ) 51 | 52 | private fun doTest(adj: ComponentAdjustment, before: List, expected: List) { 53 | val lerp = HSL.interpolator { 54 | componentAdjustment("h", adj) 55 | before.forEach { stop(HSL(it.toDouble(), .5, .5)) } 56 | } 57 | val actual = List(before.size) { lerp.interpolate(it / before.lastIndex.toDouble()).h.roundToInt() } 58 | actual shouldBe expected 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /test/src/commonTest/kotlin/com/github/ajalt/colormath/transform/TransformTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.colormath.transform 2 | 3 | import com.github.ajalt.colormath.Color 4 | import com.github.ajalt.colormath.model.LCHabColorSpaces.LCHab50 5 | import com.github.ajalt.colormath.model.RGB 6 | import com.github.ajalt.colormath.model.RGBInt 7 | import com.github.ajalt.colormath.model.xyY 8 | import com.github.ajalt.colormath.shouldEqualColor 9 | import io.kotest.data.blocking.forAll 10 | import io.kotest.data.row 11 | import kotlin.test.Test 12 | 13 | class TransformTest { 14 | @Test 15 | fun divideAlpha() = forAll( 16 | row(RGB(.1, .1, .1, 1), RGB(.1, .1, .1, 1)), 17 | row(RGB(.1, .1, .1, .5), RGB(.2, .2, .2, .5)), 18 | row(RGB(.1, .1, .1, 0), RGB(.1, .1, .1, 0)), 19 | ) { rgb, ex -> 20 | rgb.divideAlpha().shouldEqualColor(ex) 21 | } 22 | 23 | // most test cases from https://www.w3.org/TR/css-color-5/#color-mix 24 | @Test 25 | fun mix() { 26 | // Specifying colors manually since the W3 examples use the old bradford adaptation 27 | val purple = LCHab50(29.6920, 66.8302, 327.1094) 28 | val plum = LCHab50(73.3321, 37.6076, 324.5817) 29 | val mixed = LCHab50(51.51, 52.21, 325.8) 30 | forAll( 31 | row(LCHab50.mix(purple, .5, plum, .5), mixed), 32 | row(LCHab50.mix(purple, .5, plum), mixed), 33 | row(LCHab50.mix(purple, plum, .5), mixed), 34 | row(LCHab50.mix(purple, plum), mixed), 35 | row(LCHab50.mix(plum, purple), mixed), 36 | row(LCHab50.mix(purple, .8, plum, .8), mixed), 37 | row(LCHab50.mix(purple, .3, plum, .3), LCHab50(51.51, 52.21, 325.8, 0.6)), 38 | row(LCHab50.mix(LCHab50(62.253, 54.011, 63.677), .4, LCHab50(91.374, 31.406, 98.834)), 39 | LCHab50(79.7256, 40.448, 84.771)), 40 | row(LCHab50.mix(LCHab50(50, 50, 60), LCHab50(50, 50, 0), HueAdjustments.longer), 41 | LCHab50(50, 50, 210)), 42 | ) { actual, ex -> 43 | actual.shouldEqualColor(ex, 0.1) 44 | } 45 | } 46 | 47 | @Test 48 | fun chromaticAdapter() = forAll( 49 | row(RGB.createChromaticAdapter(RGB.from255(209, 215, 212)).adapt(RGB.from255(192, 202, 202)), 50 | RGB(r = 0.9202273, g = 0.94016844, b = 0.9533126)), 51 | row(RGB.createChromaticAdapter(RGB.from255(209, 215, 212).toChrom()).adapt(RGB.from255(192, 202, 202)), 52 | RGB(r = 0.9202273, g = 0.94016844, b = 0.9533126)), 53 | row(RGBInt.createChromaticAdapter(RGBInt(200, 210, 220)).adapt(RGBInt(11, 222, 33)), 54 | RGB(r = 0.29472744, g = 1.0578139, b = 0.073229484).toRGBInt()), 55 | row(RGBInt.createChromaticAdapter(RGBInt(200, 210, 220).toChrom()).adapt(RGBInt(11, 222, 33)), 56 | RGB(r = 0.29472744, g = 1.0578139, b = 0.073229484).toRGBInt()), 57 | ) { ac, ex -> 58 | ac.shouldEqualColor(ex) 59 | } 60 | 61 | @Test 62 | fun adaptAll() { 63 | val colors = intArrayOf(RGBInt(192, 202, 202).argb.toInt(), RGBInt(11, 222, 33).argb.toInt()) 64 | RGBInt.createChromaticAdapter(RGBInt(200, 210, 220)).adaptAll(colors) 65 | RGBInt(colors[0].toUInt()).shouldEqualColor(RGB(0.96045226, 0.9623541, 0.9181748).toRGBInt()) 66 | RGBInt(colors[1].toUInt()).shouldEqualColor(RGB(0.29472744, 1.0578139, 0.073229484).toRGBInt()) 67 | } 68 | } 69 | 70 | private fun Color.toChrom(): xyY = toXYZ().toCIExyY() 71 | -------------------------------------------------------------------------------- /website/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl 2 | 3 | plugins { 4 | kotlin("multiplatform") 5 | alias(libs.plugins.jetbrainsCompose) 6 | alias(libs.plugins.compose.compiler) 7 | } 8 | 9 | kotlin { 10 | @OptIn(ExperimentalWasmDsl::class) 11 | wasmJs { 12 | moduleName = "colormathApp" 13 | browser { 14 | commonWebpackConfig { 15 | outputFileName = "colormathApp.js" 16 | } 17 | } 18 | binaries.executable() 19 | } 20 | 21 | sourceSets { 22 | all { 23 | languageSettings { 24 | optIn("org.jetbrains.compose.resources.ExperimentalResourceApi") 25 | optIn("androidx.compose.foundation.layout.ExperimentalLayoutApi") 26 | optIn("androidx.compose.material3.ExperimentalMaterial3Api") 27 | optIn("androidx.compose.ui.ExperimentalComposeUiApi") 28 | } 29 | } 30 | commonMain.dependencies { 31 | implementation(compose.runtime) 32 | implementation(compose.foundation) 33 | implementation(compose.material3) 34 | implementation(compose.ui) 35 | implementation(compose.components.resources) 36 | implementation(project(":colormath")) 37 | implementation(project(":extensions:colormath-ext-jetpack-compose")) 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /website/src/commonMain/composeResources/drawable/colormath_wordmark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajalt/colormath/5e776b66ea6ffc5dc5dc360ab88ca5aefbd2a310/website/src/commonMain/composeResources/drawable/colormath_wordmark.png -------------------------------------------------------------------------------- /website/src/wasmJsMain/kotlin/main.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.ui.window.CanvasBasedWindow 2 | 3 | fun main() { 4 | CanvasBasedWindow(canvasElementId = "ComposeTarget") { App() } 5 | } 6 | -------------------------------------------------------------------------------- /website/src/wasmJsMain/resources/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Colormath Gradient Generator 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | --------------------------------------------------------------------------------