├── .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 |
27 |
--------------------------------------------------------------------------------
/docs/img/colormath_wordmark.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/img/good_hue_grad.svg:
--------------------------------------------------------------------------------
1 |
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 |
--------------------------------------------------------------------------------