Skip to content

Commit 7b042ea

Browse files
authored
Fixes HCL color interpolation (#195)
1 parent 6219ddf commit 7b042ea

7 files changed

Lines changed: 99 additions & 21 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@ tests/VerifyExamples
44
.DS_Store
55
node_modules/
66
build/
7+
.puppeteer-cache/
78
*.swp
89
.idea

elm.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
"elmcraft/core-extra": "1.0.0 <= v < 3.0.0",
3131
"folkertdev/one-true-path-experiment": "5.0.0 <= v < 7.0.0",
3232
"gampleman/elm-rosetree": "1.0.0 <= v < 2.0.0",
33-
"ianmackenzie/elm-geometry": "3.6.0 <= v < 4.0.0",
33+
"ianmackenzie/elm-geometry": "3.6.0 <= v < 5.0.0",
3434
"ianmackenzie/elm-units-prefixed": "2.0.0 <= v < 3.0.0",
3535
"justinmimbs/time-extra": "1.0.1 <= v < 2.0.0",
3636
"rtfeldman/elm-hex": "1.0.0 <= v < 2.0.0",

netlify.toml

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,28 @@
77
# directory between builds with netlify-plugin-cache.
88

99
[build]
10-
command = "npm run build-docs"
10+
# The publisher screenshots examples with puppeteer, which needs a Chrome
11+
# binary. Puppeteer normally downloads it in a postinstall hook, but Netlify
12+
# caches node_modules: when the lockfile is unchanged npm reports "up to date"
13+
# and skips postinstall, so Chrome never lands. We install it explicitly here
14+
# (a no-op once it's in the cached PUPPETEER_CACHE_DIR below) before building.
15+
# The publisher launches with headless: "shell", so chrome-headless-shell is
16+
# the binary it actually needs.
17+
command = "npx puppeteer browsers install chrome-headless-shell && npm run build-docs"
1118
publish = "build"
1219

1320
[build.environment]
1421
# The publisher requires Node 22+ (it also reads .nvmrc).
1522
NODE_VERSION = "22.20.0"
23+
# Put puppeteer's browser download inside the repo so netlify-plugin-cache can
24+
# persist it (the default ~/.cache/puppeteer is outside the cached paths).
25+
PUPPETEER_CACHE_DIR = "/opt/build/repo/.puppeteer-cache"
1626

17-
# Restore build/ before the build and save it afterwards, so the incremental
18-
# cache and prior outputs survive across deploys.
27+
# Restore build/ and the puppeteer browser cache before the build and save them
28+
# afterwards, so the incremental cache, prior outputs, and Chrome all survive
29+
# across deploys.
1930
[[plugins]]
2031
package = "netlify-plugin-cache"
2132

2233
[plugins.inputs]
23-
paths = ["build"]
34+
paths = ["build", ".puppeteer-cache"]

src/Color/Lab.elm

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -197,13 +197,19 @@ toHcl color =
197197
toLab color
198198
in
199199
if a == 0 && b == 0 then
200+
-- A grey has an undefined hue (you cannot take the angle of a
201+
-- zero-length vector). Midtone greys have a well-defined chroma of 0;
202+
-- pure black and white have an undefined chroma as well. This matches
203+
-- d3-color's `hclConvert`. (A previous version of this code had the
204+
-- chroma condition inverted, which turned any gradient touching a grey
205+
-- entirely grey — see issue #151.)
200206
{ hue = nan
201207
, chroma =
202208
if 0 < l && l < 100 then
203-
nan
209+
0
204210

205211
else
206-
0
212+
nan
207213
, luminance = l
208214
, alpha = alpha
209215
}

src/Interpolation.elm

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -333,7 +333,7 @@ hcl =
333333
-}
334334
hclLong : Color -> Color -> Interpolator Color
335335
hclLong =
336-
hclImpl float
336+
hclImpl labChannel
337337

338338

339339
{-| We do not want negative values in an rgb color
@@ -359,25 +359,51 @@ hclImpl hueInt from to =
359359
in
360360
map4 (\h c l alpha -> Lab.fromHcl { hue = h, chroma = c, luminance = l, alpha = alpha })
361361
(hueInt start.hue end.hue)
362-
(float start.chroma end.chroma)
363-
(float start.luminance end.luminance)
364-
(float start.alpha end.alpha)
362+
(labChannel start.chroma end.chroma)
363+
(labChannel start.luminance end.luminance)
364+
(labChannel start.alpha end.alpha)
365365
>> forcePositive
366366

367367

368+
{-| Interpolates a single Lab/Hcl color channel. Mirrors d3-interpolate's
369+
color-channel interpolator (`nogamma`): if either endpoint is undefined (`NaN` —
370+
as happens for the hue of any grey, or the chroma of pure black/white), the
371+
channel holds the defined endpoint constant rather than letting the `NaN`
372+
propagate across the whole gradient. Without this, a single grey endpoint turns
373+
an entire HCL gradient grey (issue #151).
374+
-}
375+
labChannel : Float -> Float -> Interpolator Float
376+
labChannel from to =
377+
if isNaN from then
378+
always to
379+
380+
else if isNaN to then
381+
always from
382+
383+
else
384+
float from to
385+
386+
368387
hue : Float -> Float -> Interpolator Float
369388
hue from to =
370-
let
371-
d =
372-
to - from
373-
in
374-
float from
375-
(if d > 0.5 || d < -0.5 then
376-
from + (d - toFloat (round d))
389+
if isNaN from then
390+
always to
377391

378-
else
379-
to
380-
)
392+
else if isNaN to then
393+
always from
394+
395+
else
396+
let
397+
d =
398+
to - from
399+
in
400+
float from
401+
(if d > 0.5 || d < -0.5 then
402+
from + (d - toFloat (round d))
403+
404+
else
405+
to
406+
)
381407

382408

383409
hue360 : Float -> Float -> Interpolator Float

tests/Color/LabTests.elm

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,21 @@ suite =
5151
Color.rgb255 170 187 204
5252
|> Lab.toHcl
5353
|> expectHclEqual { hue = 252.37145234745182, chroma = 11.223567114593477, luminance = 74.96879980931759, alpha = 1 }
54+
, test "a midtone grey has chroma 0 and an undefined (NaN) hue (#151)" <|
55+
\() ->
56+
Color.rgb255 145 145 145
57+
|> Lab.toHcl
58+
|> expectHclEqual { hue = nan, chroma = 0, luminance = 60.17214765137845, alpha = 1 }
59+
, test "pure black has an undefined chroma and hue" <|
60+
\() ->
61+
Color.rgb255 0 0 0
62+
|> Lab.toHcl
63+
|> expectHclEqual { hue = nan, chroma = nan, luminance = 0, alpha = 1 }
64+
, test "pure white has an undefined chroma and hue" <|
65+
\() ->
66+
Color.rgb255 255 255 255
67+
|> Lab.toHcl
68+
|> expectHclEqual { hue = nan, chroma = nan, luminance = 100, alpha = 1 }
5469
, test "hcl converts to proper RGB" <|
5570
\() ->
5671
Lab.fromHcl { hue = 120, chroma = 30, luminance = 50, alpha = 0.4 }
@@ -105,3 +120,8 @@ areHclCoordsEqual expected actual =
105120
areRgbCoordsEqual : Float -> Float -> Bool
106121
areRgbCoordsEqual expected actual =
107122
round (expected * 255) == round (actual * 255)
123+
124+
125+
nan : Float
126+
nan =
127+
0 / 0

tests/InterpolationTests.elm

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,20 @@ suite =
116116
\() ->
117117
interpolateColorTest Interpolation.hcl
118118
{ red = 106, green = 121, blue = 206 }
119+
, test "a grey endpoint fades chroma while holding the other hue, instead of greying out the whole gradient (#151)" <|
120+
\() ->
121+
-- #00ff00 -> #919191. A grey has an undefined hue, so the
122+
-- green should smoothly desaturate to grey. Expected values
123+
-- are taken from d3-interpolate's interpolateHcl, which this
124+
-- module ports, and match it exactly.
125+
Interpolation.hcl (Color.rgb255 0 255 0) (Color.rgb255 145 145 145)
126+
|> equalsSamples equalsColor
127+
[ Color.rgb255 0 255 0
128+
, Color.rgb255 92 228 73
129+
, Color.rgb255 121 201 103
130+
, Color.rgb255 136 173 126
131+
, Color.rgb255 145 145 145
132+
]
119133
]
120134
, describe "hclLong" <|
121135
[ test "interpolates in hcl" <|

0 commit comments

Comments
 (0)