perf(photos): ускорить рендер правок в 9 раз

Расчёт правок считал math.Pow для каждого канала каждого пикселя, из-за чего
большое фото обрабатывалось больше секунды. Факторы правок теперь считаются
один раз на рендер, квадраты считаются умножением, а строки обрабатываются
параллельно.

Обработка кадра 4000×3000 сократилась с 1.23 с до 0.14 с. Задержка от
сохранения правок до появления отредактированной версии в сетке и просмотре:
около 0.15 с для небольшого фото и 0.6 с для большого (полноразмерная
версия догружается следом, через 1.4 с).
This commit is contained in:
iu
2026-10-20 23:19:03 +03:00
parent cd3d17c0f0
commit 6d3d7cbdd4
3 changed files with 121 additions and 22 deletions
+44 -17
View File
@@ -212,31 +212,58 @@ func curveEval(points []CurvePoint, x float64) float64 {
return last.Y
}
// adjustChannel applies the light adjustments to a single channel value and
// then maps it through the tone curves: the master curve first, the channel
// curve second. The order of the operations is fixed and mirrored by the
// client-side preview, so the live preview and the rendered result match.
func adjustChannel(v float64, p LightParams, master, channel *[256]uint8) float64 {
// lightAdjuster holds the precomputed factors of the light adjustments. The
// factors are calculated once per render, so the per-pixel loop stays cheap.
type lightAdjuster struct {
exposure float64
contrast float64
highlights float64
shadows float64
whites float64
blacks float64
}
func newLightAdjuster(p LightParams) lightAdjuster {
return lightAdjuster{
exposure: math.Pow(2, p.Exposure),
contrast: 1 + p.Contrast/100,
highlights: p.Highlights / 100 * 80,
shadows: p.Shadows / 100 * 80,
whites: p.Whites / 100 * 80,
blacks: p.Blacks / 100 * 80,
}
}
// apply maps a single channel value through the adjustments and the tone
// curves: the master curve first, the channel curve second. The order of the
// operations is fixed and mirrored by the client-side preview, so the live
// preview and the rendered result match.
func (a lightAdjuster) apply(v float64, master, channel *[256]uint8) float64 {
// Экспозиция (стопы).
v *= math.Pow(2, p.Exposure)
v *= a.exposure
// Контраст вокруг средней точки.
k := 1 + p.Contrast/100
v = 128 + (v-128)*k
v = 128 + (v-128)*a.contrast
// Света: верхняя часть диапазона.
w := math.Pow(clamp01((v-128)/127), 2)
v += p.Highlights/100*80*w
w := clamp01((v - 128) / 127)
v += a.highlights * w * w
// Тени: нижняя часть диапазона.
w = math.Pow(clamp01((128-v)/128), 2)
v += p.Shadows/100*80*w
w = clamp01((128 - v) / 128)
v += a.shadows * w * w
// Белые: самый верх диапазона.
w = clamp01((v - 192) / 63)
v += p.Whites/100*80*w
v += a.whites * w
// Чёрные: самый низ диапазона.
w = clamp01((64 - v) / 64)
v += p.Blacks/100*80*w
v += a.blacks * w
// Тоновая кривая: общая, затем канальная.
idx := clampInt(int(math.Round(v)), 0, 255)
idx := clampInt(int(v+0.5), 0, 255)
v = float64(master[idx])
idx = clampInt(int(math.Round(v)), 0, 255)
return clampFloat(float64(channel[idx]), 0, 255)
idx = clampInt(int(v+0.5), 0, 255)
return float64(channel[idx])
}
// adjustChannel applies the light adjustments to a single channel value. It
// is the per-pixel helper of the renderer.
func adjustChannel(v float64, p LightParams, master, channel *[256]uint8) float64 {
return newLightAdjuster(p).apply(v, master, channel)
}