feat(photos): добавить пресеты кривой и починить переключение фото
- PCHIP для предпросмотра и серверного рендера - пресеты всех каналов в профиле, импорт и экспорт - мерж настроек auth без потери разделов - сохранение вкладки и правок текущего фото при переключении
This commit is contained in:
@@ -317,8 +317,9 @@ func clampInt(v, min, max int) int {
|
||||
}
|
||||
|
||||
// curveLUT builds a 256-entry lookup table from the control points of a
|
||||
// curve. The first and last points extend to the edges of the range, and the
|
||||
// values in between are interpolated linearly.
|
||||
// curve. The first and last points extend to the edges of the range as flat
|
||||
// shelves, and the values in between are interpolated with a monotone cubic
|
||||
// Hermite spline (PCHIP, Fritsch–Carlson).
|
||||
func curveLUT(points []CurvePoint) [256]uint8 {
|
||||
var lut [256]uint8
|
||||
pts := normalizeCurve(points)
|
||||
@@ -328,16 +329,71 @@ func curveLUT(points []CurvePoint) [256]uint8 {
|
||||
}
|
||||
return lut
|
||||
}
|
||||
tangents := curveTangents(pts)
|
||||
for i := 0; i < 256; i++ {
|
||||
x := float64(i) / 255
|
||||
lut[i] = uint8(math.Round(clamp01(curveEval(pts, x)) * 255))
|
||||
lut[i] = uint8(math.Round(clamp01(curveEvalWithTangents(pts, tangents, x)) * 255))
|
||||
}
|
||||
return lut
|
||||
}
|
||||
|
||||
// curveEval evaluates the curve at x using linear interpolation between the
|
||||
// surrounding control points.
|
||||
// curveTangents returns the PCHIP slopes at the control points. Interior
|
||||
// tangents are weighted harmonic means of the neighbouring slopes; endpoint
|
||||
// tangents use the one-sided three-point formula with the Fritsch–Carlson
|
||||
// shape-preserving clamp.
|
||||
func curveTangents(points []CurvePoint) []float64 {
|
||||
n := len(points)
|
||||
if n < 2 {
|
||||
return nil
|
||||
}
|
||||
h := make([]float64, n-1)
|
||||
delta := make([]float64, n-1)
|
||||
for i := 0; i < n-1; i++ {
|
||||
h[i] = points[i+1].X - points[i].X
|
||||
delta[i] = (points[i+1].Y - points[i].Y) / h[i]
|
||||
}
|
||||
m := make([]float64, n)
|
||||
if n == 2 {
|
||||
m[0] = delta[0]
|
||||
m[1] = delta[0]
|
||||
return m
|
||||
}
|
||||
m[0] = endpointTangent(h[0], h[1], delta[0], delta[1])
|
||||
m[n-1] = endpointTangent(h[n-2], h[n-3], delta[n-2], delta[n-3])
|
||||
for i := 1; i < n-1; i++ {
|
||||
if delta[i-1]*delta[i] <= 0 {
|
||||
m[i] = 0
|
||||
continue
|
||||
}
|
||||
w1 := 2*h[i] + h[i-1]
|
||||
w2 := h[i] + 2*h[i-1]
|
||||
m[i] = (w1 + w2) / (w1/delta[i-1] + w2/delta[i])
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// endpointTangent is the one-sided endpoint slope of the PCHIP spline.
|
||||
func endpointTangent(h0, h1, d0, d1 float64) float64 {
|
||||
m := ((2*h0+h1)*d0 - h0*d1) / (h0 + h1)
|
||||
if m*d0 <= 0 {
|
||||
return 0
|
||||
}
|
||||
if d0*d1 < 0 && math.Abs(m) > math.Abs(3*d0) {
|
||||
return 3 * d0
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// curveEval evaluates the curve at x with a monotone cubic Hermite spline.
|
||||
// Outside the first and last points the value is constant.
|
||||
func curveEval(points []CurvePoint, x float64) float64 {
|
||||
if len(points) < 2 {
|
||||
return x
|
||||
}
|
||||
return curveEvalWithTangents(points, curveTangents(points), x)
|
||||
}
|
||||
|
||||
func curveEvalWithTangents(points []CurvePoint, tangents []float64, x float64) float64 {
|
||||
if x <= points[0].X {
|
||||
return points[0].Y
|
||||
}
|
||||
@@ -345,19 +401,23 @@ func curveEval(points []CurvePoint, x float64) float64 {
|
||||
if x >= last.X {
|
||||
return last.Y
|
||||
}
|
||||
for i := 1; i < len(points); i++ {
|
||||
if x > points[i].X {
|
||||
continue
|
||||
}
|
||||
x0, y0 := points[i-1].X, points[i-1].Y
|
||||
x1, y1 := points[i].X, points[i].Y
|
||||
if x1-x0 < 1e-9 {
|
||||
return y1
|
||||
}
|
||||
t := (x - x0) / (x1 - x0)
|
||||
return y0 + t*(y1-y0)
|
||||
i := 1
|
||||
for i < len(points)-1 && x > points[i].X {
|
||||
i++
|
||||
}
|
||||
return last.Y
|
||||
p0, p1 := points[i-1], points[i]
|
||||
h := p1.X - p0.X
|
||||
if h < 1e-9 {
|
||||
return p1.Y
|
||||
}
|
||||
t := (x - p0.X) / h
|
||||
t2 := t * t
|
||||
t3 := t2 * t
|
||||
h00 := 2*t3 - 3*t2 + 1
|
||||
h10 := t3 - 2*t2 + t
|
||||
h01 := -2*t3 + 3*t2
|
||||
h11 := t3 - t2
|
||||
return h00*p0.Y + h10*h*tangents[i-1] + h01*p1.Y + h11*h*tangents[i]
|
||||
}
|
||||
|
||||
// lightAdjuster holds the precomputed factors of the light adjustments. The
|
||||
|
||||
@@ -91,6 +91,70 @@ func TestCurveLUTSteepens(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// The client builds the same 256-entry LUT (frontend/src/lib/edit.ts). The
|
||||
// hard-coded samples keep the Go and TypeScript implementations in sync.
|
||||
func TestCurveLUTMatchesClient(t *testing.T) {
|
||||
samples := []int{0, 32, 64, 128, 192, 224, 255}
|
||||
cases := []struct {
|
||||
name string
|
||||
points []CurvePoint
|
||||
want []int
|
||||
}{
|
||||
{"linear", []CurvePoint{{0, 0}, {1, 1}}, []int{0, 32, 64, 128, 192, 224, 255}},
|
||||
{
|
||||
"medium",
|
||||
[]CurvePoint{{0, 0}, {0.25, 0.2}, {0.5, 0.5}, {0.75, 0.8}, {1, 1}},
|
||||
[]int{0, 23, 51, 128, 205, 233, 255},
|
||||
},
|
||||
{
|
||||
"strong",
|
||||
[]CurvePoint{{0, 0}, {0.2, 0.12}, {0.5, 0.5}, {0.8, 0.88}, {1, 1}},
|
||||
[]int{0, 17, 43, 128, 213, 239, 255},
|
||||
},
|
||||
{
|
||||
"inner",
|
||||
[]CurvePoint{{0.1, 0.05}, {0.4, 0.3}, {0.9, 0.95}},
|
||||
[]int{13, 17, 42, 104, 186, 234, 242},
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
lut := curveLUT(tc.points)
|
||||
for i, index := range samples {
|
||||
if int(lut[index]) != tc.want[i] {
|
||||
t.Fatalf("%s lut[%d] = %d, want %d", tc.name, index, lut[index], tc.want[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurveLUTIsMonotone(t *testing.T) {
|
||||
for _, points := range [][]CurvePoint{
|
||||
{{0, 0}, {0.25, 0.2}, {0.5, 0.5}, {0.75, 0.8}, {1, 1}},
|
||||
{{0, 0}, {0.2, 0.12}, {0.5, 0.5}, {0.8, 0.88}, {1, 1}},
|
||||
} {
|
||||
lut := curveLUT(points)
|
||||
for i := 1; i < len(lut); i++ {
|
||||
if lut[i] < lut[i-1] {
|
||||
t.Fatalf("lut[%d] = %d < lut[%d] = %d", i, lut[i], i-1, lut[i-1])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurveLUTFlatOutsideEndpoints(t *testing.T) {
|
||||
lut := curveLUT([]CurvePoint{{0.1, 0.05}, {0.4, 0.3}, {0.9, 0.95}})
|
||||
for i := 0; i <= int(math.Floor(0.1*255)); i++ {
|
||||
if lut[i] != 13 {
|
||||
t.Fatalf("lut[%d] = %d, want flat 13", i, lut[i])
|
||||
}
|
||||
}
|
||||
for i := 230; i < len(lut); i++ {
|
||||
if lut[i] != 242 {
|
||||
t.Fatalf("lut[%d] = %d, want flat 242", i, lut[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyLightBrightensAndKeepsSource(t *testing.T) {
|
||||
src := image.NewNRGBA(image.Rect(0, 0, 2, 2))
|
||||
for i := range src.Pix {
|
||||
|
||||
Reference in New Issue
Block a user