@@ -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+ in dex == 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