feat(photos): инструмент «Цвет» с WebGL-превью и пипеткой

- неразрушающие правки цвета: баланс белого, вибрация/насыщенность,
  цветовой микшер, колористика и поканальная коррекция
- серверный рендер цвета в Go и паритетная реализация в WASM
- живое превью на WebGL2 (кадр считает GPU), CPU/WASM-фолбэк
- пипетка баланса белого по нейтральной точке
- новые контролы: слайдеры с градиентами, цветовые колёса,
  двойной клик по контролу сбрасывает значение
- тесты: параметры, рендер, паритет JS/WASM, e2e сохранения и сброса
This commit is contained in:
iu
2026-10-23 21:57:48 +03:00
parent c886c6f22c
commit d791b9a572
19 changed files with 3375 additions and 144 deletions
+75
View File
@@ -0,0 +1,75 @@
package e2e
import (
"context"
"path/filepath"
"testing"
"time"
"galo/internal/albums"
"galo/internal/photos"
)
// A color edit must survive the round trip: the parameters are stored in the
// photo metadata, the edited version is rendered, and a reset returns the
// photo to its original look.
func TestColorEditRoundTrip(t *testing.T) {
ctx := context.Background()
st, closeDB := openStack(t, filepath.Join(t.TempDir(), "galo.db"), t.TempDir())
defer closeDB()
user, _, err := st.auth.Register(ctx, "color@test.local", "", "password123", "C")
if err != nil {
t.Fatalf("register: %v", err)
}
album, err := st.albums.Create(ctx, user.ID, albums.CreateInput{Title: "Color"})
if err != nil {
t.Fatalf("create album: %v", err)
}
p, err := st.photos.Upload(ctx, photos.UploadInput{
AlbumID: album.ID, UserID: user.ID, Filename: "c.png", Data: makePNG(t),
})
if err != nil {
t.Fatalf("upload: %v", err)
}
w := photos.NewWorker(st.photos, time.Second, 10, time.Minute, 2)
w.ProcessOnce(ctx)
color := photos.ColorParams{Temperature: 25, Vibrance: 20}
color.Mixer.Red.Saturation = 30
color.Grading.Shadows = photos.ColorWheel{Hue: 220, Saturation: 40}
edited, err := st.photos.EditPhoto(ctx, p.ID, user.ID, photos.EditParams{
Light: photos.LightParams{Exposure: 0.3},
Color: color,
})
if err != nil {
t.Fatalf("edit: %v", err)
}
if edited.EditParams == nil {
t.Fatal("edit returned no parameters")
}
w.ProcessOnce(ctx)
got, err := st.photos.Get(ctx, p.ID, user.ID)
if err != nil {
t.Fatalf("get: %v", err)
}
if got.EditPreviewURL == "" {
t.Fatalf("no edited preview url: %+v", got)
}
if _, ok := got.EditParams["color"]; !ok {
t.Fatalf("color params not stored: %+v", got.EditParams)
}
reset, err := st.photos.ResetEdit(ctx, p.ID, user.ID)
if err != nil {
t.Fatalf("reset: %v", err)
}
if len(reset.EditParams) != 0 {
t.Fatalf("reset kept params: %+v", reset.EditParams)
}
if reset.EditURL != "" || reset.EditPreviewURL != "" {
t.Fatalf("reset kept the edited version: %+v", reset)
}
}