Skip to content

Commit 6bd00cf

Browse files
committed
Smooth the tonal lightness gradient
1 parent 7bd60d0 commit 6bd00cf

1 file changed

Lines changed: 48 additions & 10 deletions

File tree

lib/color/palette/tonal.ex

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -127,13 +127,44 @@ defmodule Color.Palette.Tonal do
127127
base_h = seed_oklch.h || 0.0
128128
base_c = seed_oklch.c || 0.0
129129

130-
# Generate one Oklch struct per stop, then gamut-map.
130+
seed_l = seed_oklch.l || 0.5
131+
num_stops = length(stops)
132+
133+
# First pass: find which stop the seed would snap to, and
134+
# compute the curve lightness at that position.
135+
snap_index = nearest_index(stops, light_anchor, dark_anchor, seed_l)
136+
snap_position = position_for(snap_index, num_stops)
137+
138+
# Second pass: generate every stop using a lightness curve
139+
# that passes through the seed's actual lightness at the snap
140+
# position. We do this by splitting the curve into two
141+
# segments — [light_anchor, seed_l] for stops above the snap,
142+
# [seed_l, dark_anchor] for stops at or below — so adjacent
143+
# stops transition smoothly through the seed instead of
144+
# hitting a cliff.
131145
generated =
132146
stops
133147
|> Enum.with_index()
134148
|> Enum.map(fn {label, index} ->
135-
position = position_for(index, length(stops))
136-
l = lerp(light_anchor, dark_anchor, position)
149+
position = position_for(index, num_stops)
150+
151+
l =
152+
cond do
153+
index < snap_index ->
154+
# Above the snap: lerp from light_anchor to seed_l.
155+
t = position / snap_position
156+
lerp(light_anchor, seed_l, t)
157+
158+
index == snap_index ->
159+
seed_l
160+
161+
true ->
162+
# Below the snap: lerp from seed_l to dark_anchor.
163+
remaining = 1.0 - snap_position
164+
t = if remaining == 0.0, do: 1.0, else: (position - snap_position) / remaining
165+
lerp(seed_l, dark_anchor, t)
166+
end
167+
137168
c = base_c * chroma_damping(l)
138169
h = if hue_drift?, do: drift_hue(base_h, position), else: base_h
139170

@@ -142,8 +173,7 @@ defmodule Color.Palette.Tonal do
142173
{label, mapped, l}
143174
end)
144175

145-
seed_l = seed_oklch.l || 0.5
146-
seed_stop_label = nearest_stop(generated, seed_l)
176+
seed_stop_label = Enum.at(stops, snap_index)
147177

148178
stops_map =
149179
Enum.into(generated, %{}, fn
@@ -480,11 +510,19 @@ defmodule Color.Palette.Tonal do
480510
defp clamp01(v) when v > 1.0, do: 1.0
481511
defp clamp01(v), do: v
482512

483-
defp nearest_stop(generated, target_l) do
484-
{label, _mapped, _l} =
485-
Enum.min_by(generated, fn {_label, _mapped, l} -> abs(l - target_l) end)
486-
487-
label
513+
# Returns the index in the stop list whose curve lightness is
514+
# closest to the seed's lightness.
515+
defp nearest_index(stops, light_anchor, dark_anchor, seed_l) do
516+
count = length(stops)
517+
518+
stops
519+
|> Enum.with_index()
520+
|> Enum.min_by(fn {_label, index} ->
521+
position = position_for(index, count)
522+
curve_l = lerp(light_anchor, dark_anchor, position)
523+
abs(curve_l - seed_l)
524+
end)
525+
|> elem(1)
488526
end
489527

490528
# ---- options validation -------------------------------------------------

0 commit comments

Comments
 (0)