From b64574b9215e9f7e5e6202750f0626b10829391f Mon Sep 17 00:00:00 2001 From: Iu Date: Wed, 21 Oct 2026 01:29:35 +0300 Subject: [PATCH] =?UTF-8?q?feat(web):=20=D0=B3=D0=B8=D1=81=D1=82=D0=BE?= =?UTF-8?q?=D0=B3=D1=80=D0=B0=D0=BC=D0=BC=D1=8B=20=D0=BA=D0=B0=D0=BD=D0=B0?= =?UTF-8?q?=D0=BB=D0=BE=D0=B2=20=D0=B2=20=D1=80=D0=B5=D0=B4=D0=B0=D0=BA?= =?UTF-8?q?=D1=82=D0=BE=D1=80=D0=B5=20=D0=BA=D1=80=D0=B8=D0=B2=D0=BE=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit График тональной кривой теперь показывает гистограмму фото: выбранный канал — своим цветом, мастер-канал (RGB) — всеми тремя слоями. Гистограмма считается в браузере по уменьшенной копии базового превью и не влияет на скорость перерисовки. --- .../src/lib/components/PhotoEditPanel.svelte | 84 +++++++++++++++++++ openspec/specs/ui-workspace/spec.md | 9 +- 2 files changed, 89 insertions(+), 4 deletions(-) diff --git a/frontend/src/lib/components/PhotoEditPanel.svelte b/frontend/src/lib/components/PhotoEditPanel.svelte index bfbcbe9..13370cc 100644 --- a/frontend/src/lib/components/PhotoEditPanel.svelte +++ b/frontend/src/lib/components/PhotoEditPanel.svelte @@ -53,6 +53,80 @@ }; }); + // Гистограмма каналов считается по базовому превью фото. + type Histogram = { red: number[]; green: number[]; blue: number[] }; + let histogram = $state(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(256).fill(0); + const g = new Array(256).fill(0); + const b = new Array(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( + 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} > + {#if histogram} + {#each visibleChannels as ch (ch)} + + {/each} + {/if} {#each [25, 50, 75] as step (step)} diff --git a/openspec/specs/ui-workspace/spec.md b/openspec/specs/ui-workspace/spec.md index 7a71fda..8f34cf8 100644 --- a/openspec/specs/ui-workspace/spec.md +++ b/openspec/specs/ui-workspace/spec.md @@ -779,10 +779,11 @@ SHALL очищаться. При отсутствии сети приложен ### Requirement: Вкладка «Редактирование» Правая панель фото SHALL содержать вкладку «Редактирование» с инструментом «Свет»: ползунки экспозиции, контраста, светов, теней, белых и чёрных, а также точечную кривую -с выбором канала. Правки SHALL применяться к фото сразу и отправляться на сервер после -десяти секунд без изменений либо при переключении фото. Переключатель «Сброс» SHALL -удалять параметры правок и отредактированную версию фото. Сетка и просмотр SHALL -показывать отредактированную версию фото, когда она отрендерена. +с выбором канала. График кривой SHALL показывать гистограмму каналов фото: выбранный +канал — своим цветом, мастер-канал — всеми тремя. Правки SHALL применяться к фото сразу и +отправляться на сервер после десяти секунд без изменений либо при переключении фото. +Переключатель «Сброс» SHALL удалять параметры правок и отредактированную версию фото. +Сетка и просмотр SHALL показывать отредактированную версию фото, когда она отрендерена. #### Scenario: Отложенная отправка - **WHEN** пользователь меняет ползунок и не трогает панель десять секунд