Недавно я столкнулся с проблемой программного осветления цветов, и это оказалось необычайной кроличьей норой. Проблема в том, что наивное решение, заключающееся в простом добавлении некоторого значения дельты к значениям RGB по отдельности, выглядит ужасно и меняет оттенок цвета после клипа самого яркого компонента. Это очевидно, если у вас есть красный светодиод и, например, R = 240, G = 60, B = 60, и вы хотите добавить 20, тогда вам нужно обрезать R = 255 максимум, но G = 80 и B = 80продолжают увеличиваться. Чем больше вы добавите, тем больше свет изменит свой оттенок, и тогда вы получите белый свет, который снова выглядит реалистично. Конечно, вы можете добавить столько цвета, сколько у вас есть. Но результат некрасивый.

Один из способов получить желаемый эффект — преобразовать цвет из RGB в HSL, цветовое пространство, в котором цвета кодируются оттенком, насыщенностью и яркостью. Затем вы можете увеличить яркость, что выглядит довольно аккуратно. Цветовое пространство Lab еще лучше подходит для этого. Но для этого требуются некоторые библиотеки, и, как я, к своему ужасу, понял, самостоятельно работать с цветовыми пространствами ужасно сложно. К сожалению, я программировал на Go, и все библиотеки цветов, которые я нашел, очень затрудняли преобразование в стандартный color.Color, который мне нужен в качестве вывода.

К счастью, я нашел хороший stackoverflow post, в котором Марк Рэнсом объясняет проблему и то, как ее исправить в RGB путем перераспределения цветов. Ключ в том, чтобы перераспределить равномерно, как только цвет начинает обрезаться. Код был на Python, поэтому я преобразовал его в Go:

func minMax(array ...int) (int, int) {
	var max int = array[0]
	var min int = array[0]
	for _, value := range array {
		if max < value {
			max = value
		}
		if min > value {
			min = value
		}
	}
	return min, max
}
func redistributeRGB(r, g, b float64) (int, int, int) {
	threshold := 255.999
	_, m := minMax(int(r), int(g), int(b))
	if float64(m) <= threshold {
		return int(r), int(g), int(b)
	}
	total := r + g + b
	if total >= 3*threshold {
		return int(r), int(g), int(b)
	}
	x := (3*threshold - total) / (3*float64(m) - total)
	gray := threshold - x*float64(m)
	return int(gray + x*r), int(gray + x*g), int(gray + x*b)
}

Затем в моем виджете я могу вычислить цвет, умножив цвет на 1 + напряжение моего светодиода из [0, 1] и перераспределив значения RGB:

func (s *SignalLightWidget) computeColor(volt float64, alpha uint8) color.Color {
	r, g, b, _ := s.offColor.RGBA()
	fr := float64(r) * (1 + volt)
	fg := float64(g) * (1 + volt)
	fb := float64(b) * (1 + volt)
	nr, ng, nb := redistributeRGB(fr, fg, fb)
	return color.RGBA{uint8(nr), uint8(ng), uint8(nb), alpha}
}

Я добавил немного дополнительного альфа-смешивания вокруг света и увеличил его, когда напряжение выше 0,9. Результат выглядит нормально: