Keep the photo's colour when the highlight and shadow sliders move
Highlight/Shadow scaled R, G and B by one luma gain. That keeps the ratio but crushes absolute chroma, so -SH turned a saturated blue into near-black and -HL turned bright colours grey, and both sliders only bit at the very ends of the range (knees 0.80..1.00 / 0.00..0.30) — they read as dead on any photo without true whites or blacks. The curve now moves the luma and carries the colour difference (rgb - luma) along at clamp(gain, 0.55, 1.35), so hue survives darkening and lifting instead of collapsing to black or white. Both knobs are pure additive luma shifts with soft knees over the upper/lower half: the 0.50 midpoint moves under 3%, and the curve stays monotonic (the old multiplicative form was not — with hl=-1 a grey 0.73 came out darker than 0.80). Kotlin applyTone port and the native bench probe gates follow. Verified on device (4200x2800 probe: grey ramp + colour patches), library export at ratio 4:3: -10 highlight leaves the darks alone and drops white 243->189 with the sky still blue; -10 shadow keeps blue as dark blue (0,0,254)->(0,3,146), never black, ramp stays monotonic. Export keeps the source 4200x2800 as well, so nothing is cropped.
This commit is contained in:
+12
-19
@@ -485,29 +485,22 @@ private fun applyTone(bmp: Bitmap, dr: Float, hl: Float, sh: Float) {
|
||||
val g = ((c shr 8) and 0xFF) / 255f
|
||||
val b = (c and 0xFF) / 255f
|
||||
val t = (0.2126f * r + 0.7152f * g + 0.0722f * b).coerceIn(0f, 1f)
|
||||
val hlMask = smoothstep(0.80f, 1.00f, t)
|
||||
val shMask = 1f - smoothstep(0.00f, 0.30f, t)
|
||||
var o = t
|
||||
// Highlight: bright end only, multiplicative on both signs (symmetric region).
|
||||
if (hl >= 0f) {
|
||||
o *= 1f + hl * 0.30f * hlMask
|
||||
} else {
|
||||
o *= 1f + hl * 0.45f * hlMask
|
||||
}
|
||||
// Shadow: dark end only.
|
||||
if (sh >= 0f) {
|
||||
o += sh * 0.80f * shMask * (1f - t)
|
||||
} else {
|
||||
o *= 1f + sh * 0.80f * shMask
|
||||
}
|
||||
// Wide, soft knees (see TONE_SKSL): HL rides the upper half, SH the lower.
|
||||
val hlMask = smoothstep(0.45f, 1.00f, t)
|
||||
val shMask = 1f - smoothstep(0.00f, 0.55f, t)
|
||||
// Pure additive luma shifts -> monotonic curve (no reversal on a ramp).
|
||||
var o = t + hl * 0.32f * hlMask + sh * 0.34f * shMask
|
||||
// Dynamic range: gentle shadow lift + highlight roll (protect brights).
|
||||
o += dr * 0.12f * shMask * (1f - t)
|
||||
o -= dr * 0.18f * hlMask * t
|
||||
val f = (o / max(t, 0.0004f)).coerceIn(0f, 4f)
|
||||
o = o.coerceIn(0f, 1f)
|
||||
// Lightness takes the curve, hue stays: the colour difference is gained
|
||||
// part-way so darkening cannot collapse a saturated colour to black.
|
||||
val cg = (o / max(t, 0.0004f)).coerceIn(0.55f, 1.35f)
|
||||
val alpha = (c ushr 24) and 0xFF
|
||||
val nr = ((r * f).coerceIn(0f, 1f) * 255f).roundToInt()
|
||||
val ng = ((g * f).coerceIn(0f, 1f) * 255f).roundToInt()
|
||||
val nb = ((b * f).coerceIn(0f, 1f) * 255f).roundToInt()
|
||||
val nr = ((o + (r - t) * cg).coerceIn(0f, 1f) * 255f).roundToInt()
|
||||
val ng = ((o + (g - t) * cg).coerceIn(0f, 1f) * 255f).roundToInt()
|
||||
val nb = ((o + (b - t) * cg).coerceIn(0f, 1f) * 255f).roundToInt()
|
||||
px[i] = (alpha shl 24) or (nr shl 16) or (ng shl 8) or nb
|
||||
}
|
||||
bmp.setPixels(px, 0, w, 0, 0, w, h)
|
||||
|
||||
Reference in New Issue
Block a user