├── .gitignore ├── .travis.yml ├── elm.json ├── README.md ├── LICENSE ├── src └── Color │ ├── Accessibility.elm │ ├── Interpolate.elm │ ├── Blending.elm │ ├── Gradient.elm │ ├── Manipulate.elm │ └── Convert.elm └── tests └── Tests.elm /.gitignore: -------------------------------------------------------------------------------- 1 | elm-stuff/ 2 | node_modules 3 | test/test.js 4 | .idea -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "8.11.0" 5 | 6 | cache: 7 | directories: 8 | - elm-stuff/ 9 | - /home/travis/.elm 10 | 11 | before_install: 12 | - if [ ${TRAVIS_OS_NAME} == "osx" ]; 13 | then brew update; brew install nvm; mkdir ~/.nvm; export NVM_DIR=~/.nvm; source $(brew --prefix nvm)/nvm.sh; 14 | fi 15 | install: 16 | - npm install -g elm elm-test@0.19.0-beta9 17 | 18 | script: elm-test 19 | -------------------------------------------------------------------------------- /elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "package", 3 | "name": "noahzgordon/elm-color-extra", 4 | "summary": "Additional color handling for Elm", 5 | "license": "MIT", 6 | "version": "1.0.2", 7 | "exposed-modules": [ 8 | "Color.Accessibility", 9 | "Color.Manipulate", 10 | "Color.Blending", 11 | "Color.Convert", 12 | "Color.Gradient", 13 | "Color.Interpolate" 14 | ], 15 | "elm-version": "0.19.0 <= v < 0.20.0", 16 | "dependencies": { 17 | "avh4/elm-color": "1.0.0 <= v < 2.0.0", 18 | "elm/core": "1.0.0 <= v < 2.0.0", 19 | "elm/regex": "1.0.0 <= v < 2.0.0", 20 | "fredcy/elm-parseint": "2.0.1 <= v < 3.0.0" 21 | }, 22 | "test-dependencies": { 23 | "elm-explorations/test": "1.1.0 <= v < 2.0.0" 24 | } 25 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # elm-color-extra 2 | Additional color handling for Elm 3 | 4 | [![Build Status](https://travis-ci.org/noahzgordon/elm-color-extra.svg?branch=master)](https://travis-ci.org/noahzgordon/elm-color-extra) 5 | 6 | __Note__: This repository was forked from eskimoblood/elm-color-extra. I am not the original creator, but will try my best to maintain this library moving forward! 7 | 8 | ## Color.Accessibility 9 | 10 | Functions to measure and maximize accessibility. 11 | 12 | ## Color.Manipulate 13 | 14 | Manipulate hue, saturation, lightning and alpha value of colors. 15 | 16 | ## Color.Blending 17 | 18 | Different methods to blend two colors. 19 | 20 | ## Color.Gradient 21 | 22 | Create gradient colors from color stops or list of colors. 23 | 24 | ## Color.Convert 25 | 26 | Convert colors to strings or strings to colors. 27 | 28 | ## Color.Interpolate 29 | 30 | Interpolate between two colors. 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Andreas Köberle 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Color/Accessibility.elm: -------------------------------------------------------------------------------- 1 | module Color.Accessibility exposing (contrastRatio, luminance, maximumContrast) 2 | 3 | {-| 4 | 5 | 6 | # Accessibility 7 | 8 | Functions to measure and maximize accessibility. 9 | 10 | @docs contrastRatio, luminance, maximumContrast 11 | 12 | -} 13 | 14 | import Color exposing (..) 15 | 16 | 17 | {-| Get the contrast ratio of two colors represented as a Float. 18 | 19 | Formula based on: 20 | 21 | 22 | contrastRatio Color.black Color.white -- 21.0 23 | 24 | contrastRatio Color.blue Color.blue -- 1.0 25 | 26 | -} 27 | contrastRatio : Color -> Color -> Float 28 | contrastRatio c1 c2 = 29 | let 30 | a = 31 | luminance c1 + 0.05 32 | 33 | b = 34 | luminance c2 + 0.05 35 | in 36 | if a > b then 37 | a / b 38 | 39 | else 40 | b / a 41 | 42 | 43 | {-| Get the relative luminance of a color represented as a Float. 44 | 45 | Formula based on: 46 | 47 | 48 | luminance Color.black -- 0.0 49 | 50 | luminance Color.white -- 1.0 51 | 52 | -} 53 | luminance : Color -> Float 54 | luminance cl = 55 | let 56 | ( r, g, b ) = 57 | cl |> toRgba |> (\a -> ( f a.red, f a.green, f a.blue )) 58 | 59 | f intensity = 60 | if intensity <= 0.03928 then 61 | intensity / 12.92 62 | 63 | else 64 | ((intensity + 0.055) / 1.055) ^ 2.4 65 | in 66 | 0.2126 * r + 0.7152 * g + 0.0722 * b 67 | 68 | 69 | {-| Returns the color with the highest contrast to the base color. 70 | 71 | bgColor = Color.darkBlue 72 | textOptions = [ Color.white, Color.purple, Color.black ] 73 | 74 | maximumContrast bgColor textOptions -- Just Color.white 75 | 76 | -} 77 | maximumContrast : Color -> List Color -> Maybe Color 78 | maximumContrast base options = 79 | let 80 | compareContrast c1 c2 = 81 | compare (contrastRatio base c2) (contrastRatio base c1) 82 | in 83 | options 84 | |> List.sortWith compareContrast 85 | |> List.head 86 | -------------------------------------------------------------------------------- /src/Color/Interpolate.elm: -------------------------------------------------------------------------------- 1 | module Color.Interpolate exposing (Space(..), interpolate) 2 | 3 | {-| 4 | 5 | 6 | # Interpolate 7 | 8 | Interpolate between two colors 9 | 10 | @docs Space, interpolate 11 | 12 | -} 13 | 14 | import Color exposing (Color, hsla, rgba, toHsla, toRgba) 15 | import Color.Convert exposing (colorToLab, labToColor) 16 | 17 | 18 | {-| The color space that is used for the interpolation 19 | -} 20 | type Space 21 | = RGB 22 | | HSL 23 | | LAB 24 | 25 | 26 | degree180 : Float 27 | degree180 = 28 | degrees 180 29 | 30 | 31 | degree360 : Float 32 | degree360 = 33 | degrees 360 34 | 35 | 36 | {-| Linear interpolation of two colors by a factor between `0` and `1`. 37 | -} 38 | interpolate : Space -> Color -> Color -> Float -> Color 39 | interpolate space cl1 cl2 t = 40 | let 41 | i = 42 | linear t 43 | in 44 | case space of 45 | RGB -> 46 | let 47 | cl1_ = 48 | toRgba cl1 49 | 50 | cl2_ = 51 | toRgba cl2 52 | in 53 | rgba (i cl1_.red cl2_.red) 54 | (i cl1_.green cl2_.green) 55 | (i cl1_.blue cl2_.blue) 56 | (i cl1_.alpha cl2_.alpha) 57 | 58 | HSL -> 59 | let 60 | cl1_ = 61 | toHsla cl1 62 | 63 | cl2_ = 64 | toHsla cl2 65 | 66 | h1 = 67 | cl1_.hue 68 | 69 | h2 = 70 | cl2_.hue 71 | 72 | dH = 73 | if h2 > h1 && h2 - h1 > degree180 then 74 | h2 - h1 + degree360 75 | 76 | else if h2 < h1 && h1 - h2 > degree180 then 77 | h2 + degree360 - h1 78 | 79 | else 80 | h2 - h1 81 | in 82 | hsla (h1 + t * dH) 83 | (i cl1_.saturation cl2_.saturation) 84 | (i cl1_.lightness cl2_.lightness) 85 | (i cl1_.alpha cl2_.alpha) 86 | 87 | LAB -> 88 | let 89 | lab1 = 90 | colorToLab cl1 91 | 92 | lab2 = 93 | colorToLab cl2 94 | in 95 | labToColor 96 | { l = i lab1.l lab2.l 97 | , a = i lab1.a lab2.a 98 | , b = i lab1.b lab2.b 99 | } 100 | 101 | 102 | linear : Float -> Float -> Float -> Float 103 | linear t i1 i2 = 104 | i1 + (i2 - i1) * t 105 | -------------------------------------------------------------------------------- /src/Color/Blending.elm: -------------------------------------------------------------------------------- 1 | module Color.Blending exposing (multiply, screen, overlay, difference, exclusion, hardlight, softlight, colorBurn, colorDodge, lighten, darken) 2 | 3 | {-| 4 | 5 | 6 | # Blending 7 | 8 | Based on the [Compositing and Blending Level 1](https://www.w3.org/TR/compositing-1/#blending) 9 | 10 | @docs multiply, screen, overlay, difference, exclusion, hardlight, softlight, colorBurn, colorDodge, lighten, darken 11 | 12 | -} 13 | 14 | import Color exposing (Color, rgba, toRgba) 15 | 16 | 17 | {-| The source color is multiplied by the destination color and replaces the destination. 18 | 19 | The resultant color is always at least as dark as either the source or destination color. 20 | Multiplying any color with black results in black. 21 | Multiplying any color with white preserves the original color. 22 | 23 | -} 24 | multiply : Color -> Color -> Color 25 | multiply clB clS = 26 | colorBlend (*) clB clS 27 | 28 | 29 | {-| Multiplies the complements of the backdrop and source color values, 30 | then complements the result. 31 | -} 32 | screen : Color -> Color -> Color 33 | screen clB clS = 34 | colorBlend screen_ clB clS 35 | 36 | 37 | {-| Multiplies or screens the colors, depending on the backdrop color value. 38 | -} 39 | overlay : Color -> Color -> Color 40 | overlay clB clS = 41 | colorBlend overlay_ clB clS 42 | 43 | 44 | {-| Selects the darker of the backdrop and source colors. 45 | -} 46 | darken : Color -> Color -> Color 47 | darken clB clS = 48 | colorBlend min clB clS 49 | 50 | 51 | {-| Selects the lighter of the backdrop and source colors. 52 | -} 53 | lighten : Color -> Color -> Color 54 | lighten clB clS = 55 | colorBlend max clB clS 56 | 57 | 58 | {-| Subtracts the darker of the two constituent colors from the lighter color. 59 | -} 60 | difference : Color -> Color -> Color 61 | difference clB clS = 62 | colorBlend (\cB cS -> abs (cB - cS)) clB clS 63 | 64 | 65 | {-| Produces an effect similar to that of the Difference mode but lower in contrast. 66 | Painting with white inverts the backdrop color; painting with black produces no change 67 | -} 68 | exclusion : Color -> Color -> Color 69 | exclusion clB clS = 70 | colorBlend (\cB cS -> cB + cS - 2 * cB * cS) clB clS 71 | 72 | 73 | {-| Multiplies or screens the colors, depending on the source color value. 74 | The effect is similar to shining a harsh spotlight on the backdrop. 75 | -} 76 | hardlight : Color -> Color -> Color 77 | hardlight clB clS = 78 | overlay clS clB 79 | 80 | 81 | {-| Darkens or lightens the colors, depending on the source color value. 82 | The effect is similar to shining a diffused spotlight on the backdrop. 83 | -} 84 | softlight : Color -> Color -> Color 85 | softlight clB clS = 86 | colorBlend softlight_ clB clS 87 | 88 | 89 | {-| Darkens the backdrop color to reflect the source color. 90 | Painting with white produces no change. 91 | -} 92 | colorBurn : Color -> Color -> Color 93 | colorBurn clB clS = 94 | colorBlend colorBurn_ clB clS 95 | 96 | 97 | {-| Brightens the backdrop color to reflect the source color. 98 | Painting with black produces no changes. 99 | -} 100 | colorDodge : Color -> Color -> Color 101 | colorDodge clB clS = 102 | colorBlend colorDodge_ clB clS 103 | 104 | 105 | colorBlend : (Float -> Float -> Float) -> Color -> Color -> Color 106 | colorBlend fn clB clS = 107 | let 108 | rgba1 = 109 | toRgba clB 110 | 111 | rgba2 = 112 | toRgba clS 113 | 114 | ar = 115 | rgba2.alpha + rgba1.alpha * (1 - rgba2.alpha) 116 | 117 | calc = 118 | calcChanel fn rgba1.alpha rgba2.alpha ar 119 | in 120 | rgba (calc rgba1.red rgba2.red) 121 | (calc rgba1.green rgba2.green) 122 | (calc rgba1.blue rgba2.blue) 123 | ar 124 | 125 | 126 | calcChanel : (Float -> Float -> Float) -> Float -> Float -> Float -> Float -> Float -> Float 127 | calcChanel fn aB aS ar cB cS = 128 | let 129 | cB_ = 130 | cB 131 | 132 | cS_ = 133 | cS 134 | 135 | cr = 136 | fn cB_ cS_ 137 | 138 | cr_ = 139 | if ar == 0 then 140 | cr 141 | 142 | else 143 | (aS * cS_ + aB * (cB_ - aS * (cB_ + cS_ - cr))) / ar 144 | in 145 | clampChannel cr_ 146 | 147 | 148 | clampChannel : number -> number 149 | clampChannel = 150 | clamp 0 1 151 | 152 | 153 | screen_ : Float -> Float -> Float 154 | screen_ cB cS = 155 | cB + cS - cB * cS 156 | 157 | 158 | overlay_ : Float -> Float -> Float 159 | overlay_ cB cS = 160 | let 161 | cB_ = 162 | cB * 2 163 | in 164 | if cB_ <= 1 then 165 | cB_ * cS 166 | 167 | else 168 | screen_ (cB_ - 1) cS 169 | 170 | 171 | softlight_ : Float -> Float -> Float 172 | softlight_ cB cS = 173 | let 174 | ( d, e ) = 175 | if cS > 0.5 then 176 | if cB > 0.25 then 177 | ( sqrt cB, 1 ) 178 | 179 | else 180 | ( ((16 * cB - 12) * cB + 4) * cB, 1 ) 181 | 182 | else 183 | ( 1, cB ) 184 | in 185 | cB - (1 - 2 * cS) * e * (d - cB) 186 | 187 | 188 | colorBurn_ : Float -> Float -> Float 189 | colorBurn_ cB cS = 190 | if cB == 1 then 191 | 1 192 | 193 | else if cS == 0 then 194 | 0 195 | 196 | else 197 | 1 - min 1 (1 - cB) / cS 198 | 199 | 200 | colorDodge_ : Float -> Float -> Float 201 | colorDodge_ cB cS = 202 | if cB == 0 then 203 | 0 204 | 205 | else if cS == 1 then 206 | 1 207 | 208 | else 209 | min 1 cB / (1 - cS) 210 | -------------------------------------------------------------------------------- /src/Color/Gradient.elm: -------------------------------------------------------------------------------- 1 | module Color.Gradient exposing (GradientStop, Gradient, Palette, linearGradient, linearGradientFromStops, CosineGradientSetting, cosineGradient) 2 | 3 | {-| 4 | 5 | 6 | # Gradient 7 | 8 | @docs GradientStop, Gradient, Palette, linearGradient, linearGradientFromStops, CosineGradientSetting, cosineGradient 9 | 10 | -} 11 | 12 | import Color exposing (Color) 13 | import Color.Interpolate as Interpolate exposing (Space(..), interpolate) 14 | import Maybe exposing (..) 15 | import Tuple exposing (first) 16 | 17 | 18 | {-| Create a new gradient `Palette` from a given `Palette`, with a given size. 19 | -} 20 | type alias Palette = 21 | List Color 22 | 23 | 24 | {-| A color and a stop value that indicates where the color appears in a gradient. 25 | The stop value must be between `0` and `1`. 26 | -} 27 | type alias GradientStop = 28 | ( Float, Color ) 29 | 30 | 31 | {-| -} 32 | type alias Gradient = 33 | List GradientStop 34 | 35 | 36 | type alias GradientInfo = 37 | { stop1 : GradientStop 38 | , stop2 : GradientStop 39 | , gradient : Gradient 40 | , palette : Palette 41 | } 42 | 43 | 44 | {-| Create a new `Palette` with gradient colors from a given `Palette`, 45 | with a given size. 46 | 47 | p1 : Palette 48 | p1 = 49 | [ rgb 200 0 200 50 | , rgb 0 100 100 51 | , rgb 100 0 0 52 | ] 53 | gradient RGB p1 5 -- [RGBA 200 0 200 1,RGBA 100 50 150 1,RGBA 0 100 100 1,RGBA 50 50 50 1,RGBA 100 0 0 1] 54 | 55 | -} 56 | linearGradient : Space -> Palette -> Int -> Palette 57 | linearGradient space palette size = 58 | let 59 | l = 60 | List.length palette - 1 61 | 62 | gr = 63 | List.map2 (\i cl -> ( toFloat i / toFloat l, cl )) (List.range 0 l) palette 64 | in 65 | linearGradientFromStops space gr size 66 | 67 | 68 | {-| Create a new `Palette` with gradient colors from a given `Gradient`, 69 | with a given size. 70 | 71 | g : Gradient 72 | g = 73 | [ (0, rgb 200 0 200) 74 | , (0.25, rgb 0 100 100) 75 | , (1, rgb 150 175 160) 76 | ] 77 | gradientFromStops RGB g 5 -- [RGBA 200 0 200 1,RGBA 0 100 100 1,RGBA 50 125 120 1,RGBA 100 150 140 1,RGBA 150 175 160 1] 78 | 79 | -} 80 | linearGradientFromStops : Space -> Gradient -> Int -> Palette 81 | linearGradientFromStops space stops size = 82 | let 83 | purifiedStops = 84 | stops 85 | |> List.filter (\( t, _ ) -> t >= 0 && t <= 1) 86 | |> List.sortBy (\( t, _ ) -> t) 87 | 88 | stop1 = 89 | List.head purifiedStops 90 | in 91 | case stop1 of 92 | Just s1 -> 93 | let 94 | l = 95 | size - 1 96 | 97 | stops_ = 98 | List.range 0 l |> List.map (\i -> toFloat i / toFloat l) 99 | 100 | currentStops = 101 | Maybe.withDefault [] (List.tail purifiedStops) 102 | 103 | ( s2, g ) = 104 | getNextGradientStop s1 currentStops 105 | in 106 | List.foldl (adjustGradient space) { stop1 = s1, stop2 = s2, gradient = g, palette = [] } stops_ 107 | |> (\{ palette } -> palette) 108 | |> List.reverse 109 | 110 | Nothing -> 111 | [] 112 | 113 | 114 | adjustGradient : Space -> Float -> GradientInfo -> GradientInfo 115 | adjustGradient space t { stop1, stop2, gradient, palette } = 116 | let 117 | newInfo = 118 | calculateGradient space stop1 stop2 gradient t 119 | in 120 | { stop1 = newInfo.stop1, stop2 = newInfo.stop2, gradient = newInfo.gradient, palette = newInfo.palette ++ palette } 121 | 122 | 123 | calculateGradient : Space -> GradientStop -> GradientStop -> Gradient -> Float -> GradientInfo 124 | calculateGradient space stop1 stop2 gradient t = 125 | if first stop2 < t then 126 | let 127 | stop1_ = 128 | stop2 129 | 130 | ( stop2_, gradient_ ) = 131 | getNextGradientStop stop2 gradient 132 | in 133 | { stop1 = stop1_, stop2 = stop2_, gradient = gradient_, palette = [ calculateColor space stop1_ stop2_ t ] } 134 | 135 | else 136 | { stop1 = stop1, stop2 = stop2, gradient = gradient, palette = [ calculateColor space stop1 stop2 t ] } 137 | 138 | 139 | calculateColor : Space -> GradientStop -> GradientStop -> Float -> Color 140 | calculateColor space ( t1, cl1 ) ( t2, cl2 ) t = 141 | if t == 0 then 142 | cl1 143 | 144 | else if t == 1 then 145 | cl2 146 | 147 | else 148 | interpolate space cl1 cl2 ((t - t1) / (t2 - t1)) 149 | 150 | 151 | getNextGradientStop : GradientStop -> Gradient -> ( GradientStop, Gradient ) 152 | getNextGradientStop currentStop gradient = 153 | let 154 | nextStop = 155 | List.head gradient 156 | in 157 | case nextStop of 158 | Just s -> 159 | ( s, Maybe.withDefault [] (List.tail gradient) ) 160 | 161 | Nothing -> 162 | ( currentStop, gradient ) 163 | 164 | 165 | {-| parameters for calculate RGB values for cosine gradients 166 | -} 167 | type alias CosineGradientSetting = 168 | ( Float, Float, Float ) 169 | 170 | 171 | calcCosine : Float -> Float -> Float -> Float -> Float -> Float 172 | calcCosine a b c d t = 173 | (a + b * cos (pi * 2 * (c * t + d))) 174 | |> clamp 0 1 175 | |> (*) 255 176 | 177 | 178 | calcCosineColor : CosineGradientSetting -> CosineGradientSetting -> CosineGradientSetting -> CosineGradientSetting -> Float -> Color 179 | calcCosineColor ( oX, oY, oZ ) ( aX, aY, aZ ) ( fX, fY, fZ ) ( pX, pY, pZ ) t = 180 | Color.rgb 181 | (calcCosine oX aX fX pX t) 182 | (calcCosine oY aY fY pY t) 183 | (calcCosine oZ aZ fZ pZ t) 184 | 185 | 186 | {-| Create a gradient based on the on an [idea by Iñigo Quílez](http://www.iquilezles.org/www/articles/palettes/palettes.htm) 187 | For an interactive example have a look at Karsten Schmidt's example from his [thi.ng library](http://dev.thi.ng/gradients/) 188 | -} 189 | cosineGradient : CosineGradientSetting -> CosineGradientSetting -> CosineGradientSetting -> CosineGradientSetting -> Int -> Palette 190 | cosineGradient offset amp fmod phase l = 191 | List.range 0 l 192 | |> List.map (toFloat >> (*) (1.0 / toFloat l) >> calcCosineColor offset amp fmod phase) 193 | -------------------------------------------------------------------------------- /src/Color/Manipulate.elm: -------------------------------------------------------------------------------- 1 | module Color.Manipulate exposing (darken, lighten, saturate, desaturate, rotateHue, fadeIn, fadeOut, grayscale, scaleHsl, scaleRgb, mix, weightedMix) 2 | 3 | {-| A library for creating and manipulating colors. 4 | 5 | 6 | # Color adjustment 7 | 8 | @docs darken, lighten, saturate, desaturate, rotateHue, fadeIn, fadeOut, grayscale, scaleHsl, scaleRgb, mix, weightedMix 9 | 10 | -} 11 | 12 | import Color exposing (Color, hsla, rgba, toHsla, toRgba) 13 | import Debug exposing (log) 14 | 15 | 16 | limit : Float -> Float 17 | limit = 18 | clamp 0 1 19 | 20 | 21 | {-| Decrease the lightning of a color 22 | -} 23 | darken : Float -> Color -> Color 24 | darken offset cl = 25 | let 26 | { hue, saturation, lightness, alpha } = 27 | toHsla cl 28 | in 29 | hsla hue saturation (limit (lightness - offset)) alpha 30 | 31 | 32 | {-| Increase the lightning of a color 33 | -} 34 | lighten : Float -> Color -> Color 35 | lighten offset cl = 36 | darken -offset cl 37 | 38 | 39 | {-| Increase the saturation of a color 40 | -} 41 | saturate : Float -> Color -> Color 42 | saturate offset cl = 43 | let 44 | { hue, saturation, lightness, alpha } = 45 | toHsla cl 46 | in 47 | hsla hue (limit (saturation + offset)) lightness alpha 48 | 49 | 50 | {-| Decrease the saturation of a color 51 | -} 52 | desaturate : Float -> Color -> Color 53 | desaturate offset cl = 54 | saturate -offset cl 55 | 56 | 57 | {-| Convert the color to a greyscale version, aka set saturation to 0 58 | -} 59 | grayscale : Color -> Color 60 | grayscale cl = 61 | saturate -1 cl 62 | 63 | 64 | {-| Increase the opacity of a color 65 | -} 66 | fadeIn : Float -> Color -> Color 67 | fadeIn offset cl = 68 | let 69 | { hue, saturation, lightness, alpha } = 70 | toHsla cl 71 | in 72 | hsla hue saturation lightness (limit (alpha + offset)) 73 | 74 | 75 | {-| Decrease the opacity of a color 76 | -} 77 | fadeOut : Float -> Color -> Color 78 | fadeOut offset cl = 79 | fadeIn -offset cl 80 | 81 | 82 | {-| Change the hue of a color. The angle value must be in degrees 83 | -} 84 | rotateHue : Float -> Color -> Color 85 | rotateHue angle cl = 86 | let 87 | { hue, saturation, lightness, alpha } = 88 | toHsla cl 89 | in 90 | hsla (hue + degrees angle) saturation lightness alpha 91 | 92 | 93 | {-| Fluidly scale saturation, lightness and alpha channel. 94 | 95 | That means that lightening an already-light color with `scaleHsl` won’t change the lightness much, but lightening 96 | a dark color by the same amount will change it more dramatically. 97 | 98 | For example, the lightness of a color can be anywhere between 0 and 1.0. If `scaleHsl { saturationScale = 0, lightnessScale = 0.4, alphaScale = 0 } color` is called, 99 | the resulting color’s lightness will be 40% of the way between its original lightness and 1.0. If 100 | `scaleHsl { saturationScale = 0, lightnessScale = -0.4, alphaScale = 0 } color` is called instead, the lightness will be 40% of the way between the original 101 | and 0. 102 | 103 | The values of the supplied tuple scale saturation, lightness, and opacity, respectively, and have a valid range of 104 | -1.0 to 1.0. 105 | 106 | This function is inspired by the Sass function [scale-color](http://sass-lang.com/documentation/Sass/Script/Functions.html#scale_color-instance_method). 107 | 108 | -} 109 | scaleHsl : { saturationScale : Float, lightnessScale : Float, alphaScale : Float } -> Color -> Color 110 | scaleHsl scaleBy color = 111 | let 112 | { saturationScale, lightnessScale, alphaScale } = 113 | scaleBy 114 | 115 | hsl = 116 | toHsla color 117 | in 118 | hsla hsl.hue 119 | (scale 1.0 saturationScale hsl.saturation) 120 | (scale 1.0 lightnessScale hsl.lightness) 121 | (scale 1.0 alphaScale hsl.alpha) 122 | 123 | 124 | {-| Fluidly scale red, green, blue, and alpha channels. 125 | 126 | That means that reddening a already-red color with `scaleRgb` won’t change the redness much, but reddening a color 127 | with little or no red by the same amount will change it more dramatically. 128 | 129 | For example, the redness of a color can be anywhere between 0 and 255. If `scaleRgb { redScale = 0.4, greenScale = 0, blueScale = 0, alphaScale = 0 } color` is called, 130 | the resulting color’s redness will be 40% of the way between its original redness and 255. If 131 | `scaleRgb { redScale = -0.4, greenScale = 0, blueScale = 0, alphaScale = 0 } color` is called instead, the redness will be 40% of the way between the original 132 | and 0. 133 | 134 | The values of the supplied tuple scale red, green, blue, and alpha channels, respectively, and have a valid range of 135 | -1.0 to 1.0. 136 | 137 | This function is inspired by the Sass function [scale-color](http://sass-lang.com/documentation/Sass/Script/Functions.html#scale_color-instance_method). 138 | 139 | -} 140 | scaleRgb : { redScale : Float, greenScale : Float, blueScale : Float, alphaScale : Float } -> Color -> Color 141 | scaleRgb scaleBy color = 142 | let 143 | { redScale, greenScale, blueScale, alphaScale } = 144 | scaleBy 145 | 146 | rgb = 147 | toRgba color 148 | in 149 | rgba 150 | (scale 1.0 redScale rgb.red) 151 | (scale 1.0 greenScale rgb.green) 152 | (scale 1.0 blueScale rgb.blue) 153 | (scale 1.0 alphaScale rgb.alpha) 154 | 155 | 156 | scale : Float -> Float -> Float -> Float 157 | scale max scaleAmount value = 158 | let 159 | clampedScale = 160 | clamp -1.0 1.0 scaleAmount 161 | 162 | clampedValue = 163 | clamp 0 max value 164 | 165 | diff = 166 | if clampedScale > 0 then 167 | max - clampedValue 168 | 169 | else 170 | clampedValue 171 | in 172 | clampedValue + diff * clampedScale 173 | 174 | 175 | {-| Mixes two colors together. 176 | 177 | This function takes the average of each of the RGB components, weighted by a provided value between 0 and 1.0. The 178 | opacity of the colors is also considered when weighting the components. 179 | 180 | The weight specifies the amount of the first color that should be included in the returned color. For example, a weight 181 | of 0.5 means that half the first color and half the second color should be used. A weight of 0.25 means that a quarter 182 | of the first color and three quarters of the second color should be used. 183 | 184 | This function uses the same algorithm as the [mix](http://sass-lang.com/documentation/Sass/Script/Functions.html#mix-instance_method) function in Sass. 185 | 186 | -} 187 | weightedMix : Color -> Color -> Float -> Color 188 | weightedMix color1 color2 weight = 189 | let 190 | clampedWeight = 191 | clamp 0 1 weight 192 | 193 | c1 = 194 | toRgba color1 195 | 196 | c2 = 197 | toRgba color2 198 | 199 | w = 200 | calculateWeight c1.alpha c2.alpha clampedWeight 201 | 202 | rMixed = 203 | mixChannel w c1.red c2.red 204 | 205 | gMixed = 206 | mixChannel w c1.green c2.green 207 | 208 | bMixed = 209 | mixChannel w c1.blue c2.blue 210 | 211 | alphaMixed = 212 | c1.alpha * clampedWeight + c2.alpha * (1 - clampedWeight) 213 | in 214 | rgba rMixed gMixed bMixed alphaMixed 215 | 216 | 217 | {-| Mixes two colors together. This is the same as calling `weightedMix` with a weight of 0.5. 218 | -} 219 | mix : Color -> Color -> Color 220 | mix c1 c2 = 221 | weightedMix c1 c2 0.5 222 | 223 | 224 | calculateWeight : Float -> Float -> Float -> Float 225 | calculateWeight a1 a2 weight = 226 | let 227 | a = 228 | a1 - a2 229 | 230 | w1 = 231 | weight * 2 - 1 232 | 233 | w2 = 234 | if w1 * a == -1 then 235 | w1 236 | 237 | else 238 | (w1 + a) / (1 + w1 * a) 239 | in 240 | (w2 + 1) / 2 241 | 242 | 243 | mixChannel : Float -> Float -> Float -> Float 244 | mixChannel weight c1 c2 = 245 | c1 * weight + c2 * (1 - weight) 246 | -------------------------------------------------------------------------------- /src/Color/Convert.elm: -------------------------------------------------------------------------------- 1 | module Color.Convert exposing 2 | ( colorToCssRgb, colorToCssRgba, colorToCssHsl, colorToCssHsla, colorToHex, colorToHexWithAlpha 3 | , hexToColor, colorToLab, labToColor 4 | ) 5 | 6 | {-| #Convert 7 | Convert colors to differnt string formats and hexadecimal strings to colors. 8 | 9 | @docs colorToCssRgb, colorToCssRgba, colorToCssHsl, colorToCssHsla, colorToHex, colorToHexWithAlpha 10 | @docs hexToColor, colorToLab, labToColor 11 | 12 | -} 13 | 14 | import Char 15 | import Color exposing (..) 16 | import ParseInt exposing (parseIntHex) 17 | import Regex 18 | import String 19 | 20 | 21 | type alias XYZ = 22 | { x : Float, y : Float, z : Float } 23 | 24 | 25 | type alias Lab = 26 | { l : Float, a : Float, b : Float } 27 | 28 | 29 | {-| Converts a color to an css rgb string. 30 | 31 | colorToCssRgb (rgb 255 0 0) -- "rgb(255, 0, 0)" 32 | 33 | -} 34 | colorToCssRgb : Color -> String 35 | colorToCssRgb cl = 36 | let 37 | { red, green, blue, alpha } = 38 | toRgba cl 39 | in 40 | cssColorString "rgb" 41 | [ String.fromFloat (red * 255) 42 | , String.fromFloat (green * 255) 43 | , String.fromFloat (blue * 255) 44 | ] 45 | 46 | 47 | {-| Converts a color to an css rgba string. 48 | 49 | colorToCssRgba (rgba 255 0 0 0.5) -- "rgba(255, 0, 0, 0.5)" 50 | 51 | -} 52 | colorToCssRgba : Color -> String 53 | colorToCssRgba cl = 54 | let 55 | { red, green, blue, alpha } = 56 | toRgba cl 57 | in 58 | cssColorString "rgba" 59 | [ String.fromFloat (red * 255) 60 | , String.fromFloat (green * 255) 61 | , String.fromFloat (blue * 255) 62 | , String.fromFloat alpha 63 | ] 64 | 65 | 66 | {-| Converts a color to an css hsl string. 67 | 68 | colorToCssHsl (hsl 1 1 0.5) -- "hsl(1, 1, 0.5)" 69 | 70 | -} 71 | colorToCssHsl : Color -> String 72 | colorToCssHsl cl = 73 | let 74 | { hue, saturation, lightness, alpha } = 75 | toHsla cl 76 | in 77 | cssColorString "hsl" 78 | [ hueToString hue 79 | , toPercentString saturation 80 | , toPercentString lightness 81 | ] 82 | 83 | 84 | {-| Converts a color to an css hsla string. 85 | 86 | colorToCssHsla (hsla 1 1 0.5 1) -- "hsla(56, 100%, 50%, 1)" 87 | 88 | -} 89 | colorToCssHsla : Color -> String 90 | colorToCssHsla cl = 91 | let 92 | { hue, saturation, lightness, alpha } = 93 | toHsla cl 94 | in 95 | cssColorString "hsla" 96 | [ hueToString hue 97 | , toPercentString saturation 98 | , toPercentString lightness 99 | , String.fromFloat alpha 100 | ] 101 | 102 | 103 | hueToString : Float -> String 104 | hueToString = 105 | (*) 360 >> round >> String.fromInt 106 | 107 | 108 | toPercentString : Float -> String 109 | toPercentString = 110 | (*) 100 >> round >> String.fromInt >> (\a -> (++) a "%") 111 | 112 | 113 | cssColorString : String -> List String -> String 114 | cssColorString kind values = 115 | kind ++ "(" ++ String.join ", " values ++ ")" 116 | 117 | 118 | {-| Converts a string to `Maybe` of color. 119 | 120 | hexToColor "#ff0000" -- "Ok (RGB 255 0 0)" 121 | 122 | hexToColor "#f00" -- "Ok (RGB 255 0 0)" 123 | 124 | hexToColor "#ff000080" -- "Ok (RGBA 255 0 0 0.5)" 125 | 126 | hexToColor "ff0000" -- "Ok (RGB 255 0 0)" 127 | 128 | hexToColor "f00" -- "Ok (RGB 255 0 0)" 129 | 130 | hexToColor "ff000080" -- "Ok (RGBA 255 0 0 0.5)" 131 | 132 | hexToColor "1234" -- "Err \"Parsing hex regex failed\"" 133 | 134 | -} 135 | hexToColor : String -> Result String Color 136 | hexToColor = 137 | let 138 | {- Converts "f" to "ff" and "ff" to "ff" -} 139 | extend : String -> String 140 | extend token = 141 | case String.toList token of 142 | [ token_ ] -> 143 | String.fromList [ token_, token_ ] 144 | 145 | _ -> 146 | token 147 | 148 | pattern = 149 | "" 150 | ++ "^" 151 | ++ "#?" 152 | ++ "(?:" 153 | -- RRGGBB 154 | ++ "(?:([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2}))" 155 | -- RGB 156 | ++ "|" 157 | ++ "(?:([a-f\\d])([a-f\\d])([a-f\\d]))" 158 | -- RRGGBBAA 159 | ++ "|" 160 | ++ "(?:([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2}))" 161 | -- RGBA 162 | ++ "|" 163 | ++ "(?:([a-f\\d])([a-f\\d])([a-f\\d])([a-f\\d]))" 164 | ++ ")" 165 | ++ "$" 166 | in 167 | String.toLower 168 | >> (\str -> Maybe.map (\regex -> Regex.findAtMost 1 regex str) (Regex.fromString pattern)) 169 | >> Maybe.andThen List.head 170 | >> Maybe.map .submatches 171 | >> Maybe.map (List.filterMap identity) 172 | >> Result.fromMaybe "Parsing hex regex failed" 173 | >> Result.andThen 174 | (\colors -> 175 | case List.map (extend >> parseIntHex >> Result.map toFloat) colors of 176 | [ Ok r, Ok g, Ok b, Ok a ] -> 177 | Ok <| rgba (r / 255) (g / 255) (b / 255) (roundToPlaces 2 (a / 255)) 178 | 179 | [ Ok r, Ok g, Ok b ] -> 180 | Ok <| rgb (r / 255) (g / 255) (b / 255) 181 | 182 | _ -> 183 | -- there could be more descriptive error cases per channel 184 | Err "Parsing ints from hex failed" 185 | ) 186 | 187 | 188 | roundToPlaces : Int -> Float -> Float 189 | roundToPlaces places number = 190 | let 191 | multiplier = 192 | toFloat (10 ^ places) 193 | in 194 | toFloat (round (number * multiplier)) / multiplier 195 | 196 | 197 | {-| Converts a color to a hexadecimal string. 198 | 199 | colorToHex (rgb 255 0 0) -- "#ff0000" 200 | 201 | colorToHex (rgba 255 0 0 1.0) -- "#ff0000" 202 | 203 | colorToHex (rgba 255 0 0 0.5) -- "#ff0000" 204 | 205 | colorToHex (rgba 255 0 0 0.0) -- "#ff0000" 206 | 207 | If you want support for colors with alpha transparency, either use `colorToCssRgba` or `colorToHexWithAlpha`. 208 | 209 | -} 210 | colorToHex : Color -> String 211 | colorToHex cl = 212 | let 213 | { red, green, blue } = 214 | toRgba cl 215 | in 216 | List.map (round >> toHex) [ red * 255, green * 255, blue * 255 ] 217 | |> (::) "#" 218 | |> String.join "" 219 | 220 | 221 | {-| Converts a color to a hexadecimal string. 222 | 223 | If the color has alpha transparency different from 1, it will use the `#RRGGBBAA` format. 224 | Note that the support for that is (as of March 2018) [missing](https://caniuse.com/#feat=css-rrggbbaa) on IE, Edge and some mobile browsers. 225 | It may be better to use `colorToCssRgba`, which has excellent support. 226 | 227 | colorToHexWithAlpha (rgb 255 0 0) -- "#ff0000" 228 | 229 | colorToHexWithAlpha (rgba 255 0 0 1.0) -- "#ff0000" 230 | 231 | colorToHexWithAlpha (rgba 255 0 0 0.5) -- "#ff000080" 232 | 233 | colorToHexWithAlpha (rgba 255 0 0 0.0) -- "#ff000000" 234 | 235 | -} 236 | colorToHexWithAlpha : Color -> String 237 | colorToHexWithAlpha color = 238 | let 239 | { red, green, blue, alpha } = 240 | toRgba color 241 | in 242 | if alpha == 1 then 243 | colorToHex color 244 | 245 | else 246 | List.map (round >> toHex) [ red * 255, green * 255, blue * 255, alpha * 255 ] 247 | |> (::) "#" 248 | |> String.join "" 249 | 250 | 251 | toHex : Int -> String 252 | toHex = 253 | toRadix >> String.padLeft 2 '0' 254 | 255 | 256 | toRadix : Int -> String 257 | toRadix n = 258 | let 259 | getChr c = 260 | if c < 10 then 261 | String.fromInt c 262 | 263 | else 264 | String.fromChar <| Char.fromCode (87 + c) 265 | in 266 | if n < 16 then 267 | getChr n 268 | 269 | else 270 | toRadix (n // 16) ++ getChr (modBy 16 n) 271 | 272 | 273 | {-| Convert color to CIELAB- color space 274 | -} 275 | colorToLab : Color -> { l : Float, a : Float, b : Float } 276 | colorToLab = 277 | colorToXyz >> xyzToLab 278 | 279 | 280 | colorToXyz : Color -> XYZ 281 | colorToXyz cl = 282 | let 283 | c ch = 284 | let 285 | ch_ = 286 | if ch > 4.045e-2 then 287 | ((ch + 5.5e-2) / 1.055) ^ 2.4 288 | 289 | else 290 | ch / 12.92 291 | in 292 | ch_ * 100 293 | 294 | { red, green, blue } = 295 | toRgba cl 296 | 297 | r = 298 | c red 299 | 300 | g = 301 | c green 302 | 303 | b = 304 | c blue 305 | in 306 | { x = r * 0.4124 + g * 0.3576 + b * 0.1805 307 | , y = r * 0.2126 + g * 0.7152 + b * 7.22e-2 308 | , z = r * 1.93e-2 + g * 0.1192 + b * 0.9505 309 | } 310 | 311 | 312 | xyzToLab : XYZ -> Lab 313 | xyzToLab { x, y, z } = 314 | let 315 | c ch = 316 | if ch > 8.856e-3 then 317 | ch ^ (1 / 3) 318 | 319 | else 320 | (7.787 * ch) + (16 / 116) 321 | 322 | x_ = 323 | c (x / 95.047) 324 | 325 | y_ = 326 | c (y / 100) 327 | 328 | z_ = 329 | c (z / 108.883) 330 | in 331 | { l = (116 * y_) - 16 332 | , a = 500 * (x_ - y_) 333 | , b = 200 * (y_ - z_) 334 | } 335 | 336 | 337 | {-| Convert a color in CIELAB- color space to Elm `Color` 338 | -} 339 | labToColor : { l : Float, a : Float, b : Float } -> Color 340 | labToColor = 341 | labToXyz >> xyzToColor 342 | 343 | 344 | labToXyz : Lab -> XYZ 345 | labToXyz { l, a, b } = 346 | let 347 | c ch = 348 | let 349 | ch_ = 350 | ch * ch * ch 351 | in 352 | if ch_ > 8.856e-3 then 353 | ch_ 354 | 355 | else 356 | (ch - 16 / 116) / 7.787 357 | 358 | y = 359 | (l + 16) / 116 360 | in 361 | { y = c y * 100 362 | , x = c (y + a / 500) * 95.047 363 | , z = c (y - b / 200) * 108.883 364 | } 365 | 366 | 367 | xyzToColor : XYZ -> Color 368 | xyzToColor { x, y, z } = 369 | let 370 | x_ = 371 | x / 100 372 | 373 | y_ = 374 | y / 100 375 | 376 | z_ = 377 | z / 100 378 | 379 | r = 380 | x_ * 3.2404542 + y_ * -1.5371385 + z_ * -0.4986 381 | 382 | g = 383 | x_ * -0.969266 + y_ * 1.8760108 + z_ * 4.1556e-2 384 | 385 | b = 386 | x_ * 5.56434e-2 + y_ * -0.2040259 + z_ * 1.0572252 387 | 388 | c ch = 389 | let 390 | ch_ = 391 | if ch > 3.1308e-3 then 392 | 1.055 * (ch ^ (1 / 2.4)) - 5.5e-2 393 | 394 | else 395 | 12.92 * ch 396 | in 397 | clamp 0 1 ch_ 398 | in 399 | rgb (c r) (c g) (c b) 400 | -------------------------------------------------------------------------------- /tests/Tests.elm: -------------------------------------------------------------------------------- 1 | module Tests exposing (accessibility, blending, c1, c2, convert, gradient, interpolation, lab1, manipulate, p1, p1Result, p2, p2Result) 2 | 3 | import Color exposing (Color, hsl, hsla, toRgba) 4 | import Color.Accessibility exposing (..) 5 | import Color.Blending as Ble exposing (..) 6 | import Color.Convert exposing (..) 7 | import Color.Gradient as Gra exposing (..) 8 | import Color.Interpolate as Int exposing (..) 9 | import Color.Manipulate as Man exposing (..) 10 | import Expect exposing (Expectation, FloatingPointTolerance(..)) 11 | import Test exposing (..) 12 | 13 | 14 | rgb : Float -> Float -> Float -> Color 15 | rgb r g b = 16 | Color.rgb (r / 255) (g / 255) (b / 255) 17 | 18 | 19 | rgba : Float -> Float -> Float -> Float -> Color 20 | rgba r g b a = 21 | Color.rgba (r / 255) (g / 255) (b / 255) a 22 | 23 | 24 | accessibility : Test 25 | accessibility = 26 | describe "Accessibility" 27 | [ test "Contrast ratio of black and white should be 21:1" <| 28 | \() -> 29 | Expect.within (Absolute 0.001) 30 | (contrastRatio Color.black Color.white) 31 | 21.0 32 | , test "Contrast ratio of equal colors should be 1:1" <| 33 | \() -> 34 | Expect.within (Absolute 0.001) 35 | (contrastRatio Color.blue Color.blue) 36 | 1.0 37 | , test "Contrast ratio color order does not matter" <| 38 | \() -> 39 | Expect.within (Absolute 0.001) 40 | (contrastRatio Color.green Color.blue) 41 | (contrastRatio Color.blue Color.green) 42 | , test "Luminance of black is the minimum possible" <| 43 | \() -> 44 | Expect.within (Absolute 0.001) 45 | (luminance Color.black) 46 | 0.0 47 | , test "Luminance of white is the maximum possible" <| 48 | \() -> 49 | Expect.within (Absolute 0.001) 50 | (luminance Color.white) 51 | 1.0 52 | , test "Maximum contrast" <| 53 | \() -> 54 | Expect.equal 55 | (maximumContrast Color.yellow 56 | [ Color.white 57 | , Color.darkBlue 58 | , Color.green 59 | ] 60 | ) 61 | (Just Color.darkBlue) 62 | ] 63 | 64 | 65 | convert : Test 66 | convert = 67 | describe "Convert" 68 | [ test "Color to rgb String" <| 69 | \() -> Expect.equal (colorToCssRgb (rgb 255 125 0)) "rgb(255, 125, 0)" 70 | , test "Color to rgba String" <| 71 | \() -> Expect.equal (colorToCssRgba (rgba 255 125 0 0.3)) "rgba(255, 125, 0, 0.3)" 72 | , test "Color to hsl String" <| 73 | \() -> Expect.equal (colorToCssHsl (hsl 0.4 0.2 0.2)) "hsl(144, 20%, 20%)" 74 | , test "Color to hsla String" <| 75 | \() -> Expect.equal (colorToCssHsla (hsla 0.4 0.2 0.2 1)) "hsla(144, 20%, 20%, 1)" 76 | , test "Color to hex String" <| 77 | \() -> Expect.equal (colorToHex (rgb 255 0 255)) "#ff00ff" 78 | , test "Color to hex String ignores alpha" <| 79 | \() -> Expect.equal (colorToHex (rgba 255 0 255 0)) "#ff00ff" 80 | , test "Color to hex String with alpha keeps #RRGGBB format when alpha = 1" <| 81 | \() -> Expect.equal (colorToHexWithAlpha (rgb 255 0 255)) "#ff00ff" 82 | , test "Color to hex String with alpha keeps #RRGGBB format when alpha = 1 (explicitly)" <| 83 | \() -> Expect.equal (colorToHexWithAlpha (rgba 255 0 255 1)) "#ff00ff" 84 | , test "Color to hex String with alpha uses #RRGGBBAA format when alpha /= 1" <| 85 | \() -> Expect.equal (colorToHexWithAlpha (rgba 255 0 255 0.5)) "#ff00ff80" 86 | , test "Hex string to hex color (#RRGGBB)" <| 87 | \() -> Expect.equal (hexToColor "#ff00ff") (Ok <| rgb 255 0 255) 88 | , test "Hex string to hex color (RRGGBB)" <| 89 | \() -> Expect.equal (hexToColor "ff00ff") (Ok <| rgb 255 0 255) 90 | , test "Hex string to hex color (#RGB)" <| 91 | \() -> Expect.equal (hexToColor "#f0f") (Ok <| rgb 255 0 255) 92 | , test "Hex string to hex color (RGB)" <| 93 | \() -> Expect.equal (hexToColor "0a0") (Ok <| rgb 0 170 0) 94 | , test "Hex string to hex color (#RRGGBBAA)" <| 95 | \() -> Expect.equal (hexToColor "#ff00ff80") (Ok <| rgba 255 0 255 0.5) 96 | , test "Hex string to hex color (RRGGBBAA)" <| 97 | \() -> Expect.equal (hexToColor "ff00ff80") (Ok <| rgba 255 0 255 0.5) 98 | , test "Hex string to hex color (#RGBA)" <| 99 | \() -> Expect.equal (hexToColor "#f0f0") (Ok <| rgba 255 0 255 0) 100 | , test "Hex string to hex color (RGBA)" <| 101 | \() -> Expect.equal (hexToColor "f0f0") (Ok <| rgba 255 0 255 0) 102 | , test "Hex string to hex color (fails)" <| 103 | \() -> Expect.equal (hexToColor "12345") (Err "Parsing hex regex failed") 104 | , test "Rgb to lab" <| 105 | \() -> Expect.equal lab1 (colorToLab (rgb 255 255 0)) 106 | , test "Lab to rgb" <| 107 | expectColorSimilarity (rgb 255 255 0) (labToColor lab1) 108 | ] 109 | 110 | 111 | lab1 : { l : Float, a : Float, b : Float } 112 | lab1 = 113 | { l = 97.13824698129729, a = -21.555908334832285, b = 94.48248544644461 } 114 | 115 | 116 | manipulate : Test 117 | manipulate = 118 | describe "Manipulate" 119 | [ test "Darken" <| 120 | expectColorSimilarity (Man.darken 0.5 (hsl 1 1 0.75)) (hsl 1 1 0.25) 121 | , test "Darken should be limit to 0" <| 122 | expectColorSimilarity (Man.darken 10 (hsl 1 1 0.75)) (hsl 1 1 0) 123 | , test "Lighten" <| 124 | expectColorSimilarity (Man.lighten 0.5 (hsl 1 1 0.2)) (hsl 1 1 0.7) 125 | , test "Lighten should be limit to 1" <| 126 | expectColorSimilarity (Man.lighten 10 (hsl 1 1 0)) (hsl 1 1 1) 127 | , test "Saturate" <| 128 | expectColorSimilarity (saturate 0.5 (hsl 1 0 1)) (hsl 1 0.5 1) 129 | , test "Saturate should be limit to 1" <| 130 | expectColorSimilarity (saturate 10 (hsl 1 1 1)) (hsl 1 1 1) 131 | , test "Desaturate" <| 132 | expectColorSimilarity (desaturate 0.5 (hsl 1 1 1)) (hsl 1 0.5 1) 133 | , test "Desaturate should be limit to 0" <| 134 | expectColorSimilarity (desaturate 10 (hsl 1 1 1)) (hsl 1 0 1) 135 | , test "Grayscale" <| 136 | expectColorSimilarity (Man.grayscale (hsl 1 1 1)) (hsl 1 0 1) 137 | , test "Fade in" <| 138 | expectColorSimilarity (fadeIn 0.2 (hsla 1 1 1 0.5)) (hsla 1 1 1 0.7) 139 | , test "Fade in should be limit to 1" <| 140 | expectColorSimilarity (fadeIn 10 (hsla 1 1 1 0.5)) (hsla 1 1 1 1) 141 | , test "Fade out" <| 142 | expectColorSimilarity (fadeOut 0.2 (hsla 1 1 1 0.5)) (hsla 1 1 1 0.3) 143 | , test "Fade out should be limit to 0" <| 144 | expectColorSimilarity (fadeOut 10 (hsla 1 1 1 0.5)) (hsla 1 1 1 0) 145 | , test "Rotate hue" <| 146 | expectColorSimilarity (rotateHue 90 (hsla 0 1 1 0)) (hsla (degrees 90) 1 1 0) 147 | , test "Rotate hue with negative value" <| 148 | expectColorSimilarity (rotateHue -90 (hsla 0 1 1 0)) (hsla (degrees 270) 1 1 0) 149 | , test "Rotate hue for more then 360°" <| 150 | expectColorSimilarity (rotateHue 270 (hsla (degrees 180) 1 1 0)) 151 | (hsla (degrees 90) 1 1 0) 152 | , test "Scale saturation with positive value" <| 153 | expectColorSimilarity (hsl (1 / 3) 0.51 0.9) 154 | (scaleHsl { saturationScale = 0.3, lightnessScale = 0, alphaScale = 0 } (hsl (1 / 3) 0.3 0.9)) 155 | , test "Scale saturation with negative value" <| 156 | expectColorSimilarity (hsl (1 / 3) 0.21 0.9) 157 | (scaleHsl { saturationScale = -0.3, lightnessScale = 0, alphaScale = 0 } (hsl (1 / 3) 0.3 0.9)) 158 | , test "Scale lightness with positive value" <| 159 | expectColorSimilarity (hsl (1 / 3) 0.3 0.915) 160 | (scaleHsl { saturationScale = 0, lightnessScale = 0.15, alphaScale = 0 } (hsl (1 / 3) 0.3 0.9)) 161 | , test "Scale lightness with negative value" <| 162 | expectColorSimilarity (hsl (1 / 3) 0.3 0.765) 163 | (scaleHsl { saturationScale = 0, lightnessScale = -0.15, alphaScale = 0 } (hsl (1 / 3) 0.3 0.9)) 164 | , test "Scale alpha with positive value" <| 165 | expectColorSimilarity (hsla (1 / 3) 0.3 0.9 0.14) 166 | (scaleHsl { saturationScale = 0, lightnessScale = 0, alphaScale = 0.14 } (hsla (1 / 3) 0.3 0.9 0)) 167 | , test "Scale alpha with negative value" <| 168 | expectColorSimilarity (hsla (1 / 3) 0.3 0.9 0.86) 169 | (scaleHsl { saturationScale = 0, lightnessScale = 0, alphaScale = -0.14 } (hsl (1 / 3) 0.3 0.9)) 170 | , test "Scale red channel with positive value" <| 171 | expectColorSimilarity (rgb 186.4 20 30) (scaleRgb { redScale = 0.3, greenScale = 0, blueScale = 0, alphaScale = 0 } (rgb 157 20 30)) 172 | , test "Scale red channel with negative value" <| expectColorSimilarity (rgb 109.9 20 30) (scaleRgb { redScale = -0.3, greenScale = 0, blueScale = 0, alphaScale = 0 } (rgb 157 20 30)) 173 | , test "Scale green channel with positive value" <| expectColorSimilarity (rgb 157 55.25 30) (scaleRgb { redScale = 0, greenScale = 0.15, blueScale = 0, alphaScale = 0 } (rgb 157 20 30)) 174 | , test "Scale green channel with negative value" <| expectColorSimilarity (rgb 157 17 30) (scaleRgb { redScale = 0, greenScale = -0.15, blueScale = 0, alphaScale = 0 } (rgb 157 20 30)) 175 | , test "Scale blue channel with positive value" <| expectColorSimilarity (rgb 157 20 61.5) (scaleRgb { redScale = 0, greenScale = 0, blueScale = 0.14, alphaScale = 0 } (rgb 157 20 30)) 176 | , test "Scale blue channel with negative value" <| expectColorSimilarity (rgb 157 20 25.8) (scaleRgb { redScale = 0, greenScale = 0, blueScale = -0.14, alphaScale = 0 } (rgb 157 20 30)) 177 | , test "Scale alpha channel with positive value" <| expectColorSimilarity (rgba 157 20 30 0.6) (scaleRgb { redScale = 0, greenScale = 0, blueScale = 0, alphaScale = 0.2 } (rgba 157 20 30 0.5)) 178 | , test "Scale alpha channel with negative value" <| expectColorSimilarity (rgba 157 20 30 0.4) (scaleRgb { redScale = 0, greenScale = 0, blueScale = 0, alphaScale = -0.2 } (rgba 157 20 30 0.5)) 179 | , test "Mix 1" <| expectColorSimilarity (rgb 127.5 0 127.5) (mix (rgb 255 0 0) (rgb 0 0 255)) 180 | , test "Mix 2" <| expectColorSimilarity (rgb 127.5 127.5 127.5) (mix (rgb 255 255 0) (rgb 0 0 255)) 181 | , test "Mix 3" <| expectColorSimilarity (rgb 127.5 144.5 85) (mix (rgb 255 119 0) (rgb 0 170 170)) 182 | , test "Mix 4" <| expectColorSimilarity (rgb 63.75 0 191.25) (weightedMix (rgb 255 0 0) (rgb 0 0 255) 0.25) 183 | , test "Mix 5" <| expectColorSimilarity (rgba 63.75 0 191.25 0.75) (mix (rgba 255 0 0 0.5) (rgb 0 0 255)) 184 | , test "Mix 6" <| expectColorSimilarity (rgb 255 0 0) (weightedMix (rgb 255 0 0) (rgb 0 0 255) 1) 185 | , test "Mix 7" <| expectColorSimilarity (rgb 0 0 255) (weightedMix (rgb 255 0 0) (rgb 0 0 255) 0) 186 | , test "Mix 8" <| expectColorSimilarity (rgba 255 0 0 0.5) (mix (rgb 255 0 0) (rgba 0 0 255 0)) 187 | , test "Mix 9" <| expectColorSimilarity (rgba 0 0 255 0.5) (mix (rgba 255 0 0 0) (rgb 0 0 255)) 188 | , test "Mix 10" <| expectColorSimilarity (rgb 255 0 0) (weightedMix (rgb 255 0 0) (rgba 0 0 255 0) 1) 189 | , test "Mix 11" <| expectColorSimilarity (rgb 0 0 255) (weightedMix (rgba 255 0 0 0) (rgb 0 0 255) 0) 190 | , test "Mix 12" <| expectColorSimilarity (rgba 0 0 255 0) (weightedMix (rgb 255 0 0) (rgba 0 0 255 0) 0) 191 | , test "Mix 13" <| expectColorSimilarity (rgba 255 0 0 0) (weightedMix (rgba 255 0 0 0) (rgb 0 0 255) 1) 192 | ] 193 | 194 | 195 | expectColorSimilarity : Color -> Color -> (() -> Expectation) 196 | expectColorSimilarity color1 color2 = 197 | let 198 | color1Rgb = 199 | toRgba color1 200 | 201 | color2Rgb = 202 | toRgba color2 203 | 204 | compare a b fn = 205 | \_ -> Expect.within (Absolute 0.01) (fn a) (fn b) 206 | in 207 | Expect.all (List.map (compare color1Rgb color2Rgb) [ .red, .green, .blue, .alpha ]) 208 | 209 | 210 | c1 : Color 211 | c1 = 212 | rgb 255 102 0 213 | 214 | 215 | c2 : Color 216 | c2 = 217 | rgb 0 255 0 218 | 219 | 220 | blending : Test 221 | blending = 222 | describe "Blending" 223 | [ test "Multiply" <| expectColorSimilarity (multiply c1 c2) (rgb 0 102 0) 224 | , test "Screen" <| expectColorSimilarity (screen c1 c2) (rgb 255 255 0) 225 | , test "Overlay" <| expectColorSimilarity (overlay c1 c2) (rgb 255 204 0) 226 | , test "Softlight" <| expectColorSimilarity (softlight c1 c2) (rgb 255 161.27 0) 227 | , test "Hardlight" <| expectColorSimilarity (hardlight c1 c2) c2 228 | , test "Difference" <| expectColorSimilarity (difference c1 c2) (rgb 255 153 0) 229 | , test "Exclusion" <| expectColorSimilarity (exclusion c1 c2) (rgb 255 153 0) 230 | , test "Darken" <| expectColorSimilarity (Ble.darken c1 c2) (rgb 0 102 0) 231 | , test "Lighten" <| expectColorSimilarity (Ble.lighten c1 c2) (rgb 255 255 0) 232 | ] 233 | 234 | 235 | interpolation : Test 236 | interpolation = 237 | describe "Interpolate" 238 | [ test "Mix" <| \() -> Expect.equal (interpolate RGB (rgba 0 0 0 0) (rgba 255 255 255 1) 0.5) (rgba 127.5 127.5 127.5 0.5) 239 | ] 240 | 241 | 242 | p1 : Palette 243 | p1 = 244 | [ rgb 200 0 200 245 | , rgb 0 100 100 246 | , rgb 100 0 0 247 | ] 248 | 249 | 250 | p1Result : Palette 251 | p1Result = 252 | [ rgb 200 0 200 253 | , rgb 100 50 150 254 | , rgb 0 100 100 255 | , rgb 50 50 50 256 | , rgb 100 0 0 257 | ] 258 | 259 | 260 | p2 : Gradient 261 | p2 = 262 | [ ( 0, rgb 200 0 200 ) 263 | , ( 0.25, rgb 0 100 100 ) 264 | , ( 1, rgb 150 175 160 ) 265 | ] 266 | 267 | 268 | p2Result : Palette 269 | p2Result = 270 | [ rgb 200 0 200 271 | , rgb 0 100 100 272 | , rgb 50 125 120 273 | , rgb 100 150 140 274 | , rgb 150 175 160 275 | ] 276 | 277 | 278 | gradient : Test 279 | gradient = 280 | describe "Gradient" 281 | [ test "Gradient from list" <| \() -> Expect.equal (Gra.linearGradient RGB p1 5) p1Result 282 | , test "Gradient from stops" <| \() -> Expect.equal (Gra.linearGradientFromStops RGB p2 5) p2Result 283 | ] 284 | --------------------------------------------------------------------------------