feat(web): гистограммы каналов в редакторе кривой
График тональной кривой теперь показывает гистограмму фото: выбранный канал — своим цветом, мастер-канал (RGB) — всеми тремя слоями. Гистограмма считается в браузере по уменьшенной копии базового превью и не влияет на скорость перерисовки.
This commit is contained in:
@@ -53,6 +53,80 @@
|
||||
};
|
||||
});
|
||||
|
||||
// Гистограмма каналов считается по базовому превью фото.
|
||||
type Histogram = { red: number[]; green: number[]; blue: number[] };
|
||||
let histogram = $state<Histogram | null>(null);
|
||||
|
||||
$effect(() => {
|
||||
const url = photo.preview_url;
|
||||
if (!url) {
|
||||
histogram = null;
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
if (cancelled) return;
|
||||
histogram = computeHistogram(img);
|
||||
};
|
||||
img.src = url;
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
});
|
||||
|
||||
/** Считает гистограмму каналов по уменьшенной копии фото. */
|
||||
function computeHistogram(img: HTMLImageElement): Histogram | null {
|
||||
const width = 256;
|
||||
const height = Math.max(1, Math.round((img.naturalHeight / img.naturalWidth) * width));
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return null;
|
||||
ctx.drawImage(img, 0, 0, width, height);
|
||||
const data = ctx.getImageData(0, 0, width, height).data;
|
||||
const r = new Array<number>(256).fill(0);
|
||||
const g = new Array<number>(256).fill(0);
|
||||
const b = new Array<number>(256).fill(0);
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
r[data[i]]++;
|
||||
g[data[i + 1]]++;
|
||||
b[data[i + 2]]++;
|
||||
}
|
||||
let max = 0;
|
||||
for (const values of [r, g, b]) {
|
||||
for (const v of values) if (v > max) max = v;
|
||||
}
|
||||
if (!max) return null;
|
||||
const norm = (values: number[]) => values.map((v) => v / max);
|
||||
return { red: norm(r), green: norm(g), blue: norm(b) };
|
||||
}
|
||||
|
||||
/** Каналы, которые показываем на графике: у мастер-канала — все три. */
|
||||
const visibleChannels = $derived<CurveChannel[]>(
|
||||
channel === 'rgb' ? ['red', 'green', 'blue'] : [channel]
|
||||
);
|
||||
|
||||
/** Значения гистограммы для канала. */
|
||||
function histogramValues(ch: CurveChannel): number[] {
|
||||
if (!histogram) return [];
|
||||
if (ch === 'rgb') return histogram.red;
|
||||
return histogram[ch];
|
||||
}
|
||||
|
||||
/** Гистограмма канала в виде вертикальных столбиков. */
|
||||
function histogramPath(values: number[]): string {
|
||||
const parts: string[] = [];
|
||||
const last = values.length - 1;
|
||||
for (let i = 0; i <= last; i++) {
|
||||
const x = ((i / last) * 100).toFixed(2);
|
||||
const y = (100 - values[i] * 100).toFixed(2);
|
||||
parts.push(`M${x},100 L${x},${y}`);
|
||||
}
|
||||
return parts.join(' ');
|
||||
}
|
||||
|
||||
// Правки уходят на сервер не сразу: пока пользователь крутит ползунки,
|
||||
// фото перерисовывает браузер, а серверу достаётся уже settled-версия.
|
||||
const SAVE_DELAY = 10_000;
|
||||
@@ -347,6 +421,16 @@
|
||||
onpointerleave={onPointUp}
|
||||
>
|
||||
<rect x="0" y="0" width="100" height="100" class="bg-bg" />
|
||||
{#if histogram}
|
||||
{#each visibleChannels as ch (ch)}
|
||||
<path
|
||||
d={histogramPath(histogramValues(ch))}
|
||||
fill="none"
|
||||
stroke-width="0.4"
|
||||
style="stroke: {CHANNEL_COLORS[ch]}; opacity: {ch === 'rgb' ? 0.45 : 0.7}"
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
{#each [25, 50, 75] as step (step)}
|
||||
<line x1={step} y1="0" x2={step} y2="100" class="stroke-border" stroke-width="0.4" />
|
||||
<line x1="0" y1={step} x2="100" y2={step} class="stroke-border" stroke-width="0.4" />
|
||||
|
||||
@@ -779,10 +779,11 @@ SHALL очищаться. При отсутствии сети приложен
|
||||
### Requirement: Вкладка «Редактирование»
|
||||
Правая панель фото SHALL содержать вкладку «Редактирование» с инструментом «Свет»:
|
||||
ползунки экспозиции, контраста, светов, теней, белых и чёрных, а также точечную кривую
|
||||
с выбором канала. Правки SHALL применяться к фото сразу и отправляться на сервер после
|
||||
десяти секунд без изменений либо при переключении фото. Переключатель «Сброс» SHALL
|
||||
удалять параметры правок и отредактированную версию фото. Сетка и просмотр SHALL
|
||||
показывать отредактированную версию фото, когда она отрендерена.
|
||||
с выбором канала. График кривой SHALL показывать гистограмму каналов фото: выбранный
|
||||
канал — своим цветом, мастер-канал — всеми тремя. Правки SHALL применяться к фото сразу и
|
||||
отправляться на сервер после десяти секунд без изменений либо при переключении фото.
|
||||
Переключатель «Сброс» SHALL удалять параметры правок и отредактированную версию фото.
|
||||
Сетка и просмотр SHALL показывать отредактированную версию фото, когда она отрендерена.
|
||||
|
||||
#### Scenario: Отложенная отправка
|
||||
- **WHEN** пользователь меняет ползунок и не трогает панель десять секунд
|
||||
|
||||
Reference in New Issue
Block a user