fix(web): гистограмма пересчитывается при правках

Гистограмма считалась по базовому превью один раз и не менялась при движении
ползунков и кривой. Теперь по уменьшенной копии фото накапливается сэмпл
пикселей, а сама гистограмма пересчитывается с применением текущих правок, так
что график реагирует на манипуляции сразу.
This commit is contained in:
iu
2026-10-21 01:32:14 +03:00
parent b64574b921
commit 48ed0bb775
@@ -2,6 +2,7 @@
import { photosApi } from '$lib/api'; import { photosApi } from '$lib/api';
import Icon from './Icon.svelte'; import Icon from './Icon.svelte';
import { edit } from '$lib/stores/edit.svelte'; import { edit } from '$lib/stores/edit.svelte';
import { applyLight } from '$lib/light-wasm';
import { import {
CHANNEL_COLORS, CHANNEL_COLORS,
CHANNEL_LABELS, CHANNEL_LABELS,
@@ -53,21 +54,22 @@
}; };
}); });
// Гистограмма каналов считается по базовому превью фото. // Гистограмма каналов считается по базовому превью фото и пересчитывается
// с учётом текущих правок, поэтому реагирует на движение ползунков и кривой.
type Histogram = { red: number[]; green: number[]; blue: number[] }; type Histogram = { red: number[]; green: number[]; blue: number[] };
let histogram = $state<Histogram | null>(null); let sample = $state<Uint8ClampedArray | null>(null);
$effect(() => { $effect(() => {
const url = photo.preview_url; const url = photo.preview_url;
if (!url) { if (!url) {
histogram = null; sample = null;
return; return;
} }
let cancelled = false; let cancelled = false;
const img = new Image(); const img = new Image();
img.onload = () => { img.onload = () => {
if (cancelled) return; if (cancelled) return;
histogram = computeHistogram(img); sample = sampleImage(img);
}; };
img.src = url; img.src = url;
return () => { return () => {
@@ -75,8 +77,8 @@
}; };
}); });
/** Считает гистограмму каналов по уменьшенной копии фото. */ /** Уменьшенная копия фото: пиксели для расчёта гистограммы. */
function computeHistogram(img: HTMLImageElement): Histogram | null { function sampleImage(img: HTMLImageElement): Uint8ClampedArray | null {
const width = 256; const width = 256;
const height = Math.max(1, Math.round((img.naturalHeight / img.naturalWidth) * width)); const height = Math.max(1, Math.round((img.naturalHeight / img.naturalWidth) * width));
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
@@ -85,7 +87,15 @@
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
if (!ctx) return null; if (!ctx) return null;
ctx.drawImage(img, 0, 0, width, height); ctx.drawImage(img, 0, 0, width, height);
const data = ctx.getImageData(0, 0, width, height).data; return new Uint8ClampedArray(ctx.getImageData(0, 0, width, height).data);
}
/** Гистограмма с учётом текущих правок. */
const histogram = $derived.by<Histogram | null>(() => {
const pixels = sample;
if (!pixels) return null;
const data = new Uint8ClampedArray(pixels);
applyLight(data, params.light);
const r = new Array<number>(256).fill(0); const r = new Array<number>(256).fill(0);
const g = new Array<number>(256).fill(0); const g = new Array<number>(256).fill(0);
const b = new Array<number>(256).fill(0); const b = new Array<number>(256).fill(0);
@@ -101,7 +111,7 @@
if (!max) return null; if (!max) return null;
const norm = (values: number[]) => values.map((v) => v / max); const norm = (values: number[]) => values.map((v) => v / max);
return { red: norm(r), green: norm(g), blue: norm(b) }; return { red: norm(r), green: norm(g), blue: norm(b) };
} });
/** Каналы, которые показываем на графике: у мастер-канала — все три. */ /** Каналы, которые показываем на графике: у мастер-канала — все три. */
const visibleChannels = $derived<CurveChannel[]>( const visibleChannels = $derived<CurveChannel[]>(