fix(studio): keep the tone curve's channels, and drop the strip that repeats them
Two halves of one report: the R/G/B channel picks belong on the graph, and the chips down the sidebar were reading as a second copy of the sliders. - ToneCurvePanel.tsx + app.css: the channel row (RGB/R/G/B and RESET) and the .curve-tabs rules come back, so the card picks a channel again and bends that one. Undoes the row's removal; the graph itself is unchanged. - app.css: the strip a LIGHT panel opens is hidden on a desktop, with the row of panel chips it is opened from. Both name the seven rows the panel already stands in, so a desktop was drawing every knob twice — the strip beside the sliders, and the sliders. The phone keeps it: there the panels are the strip, not the column. Verified in Chrome at 1440x900 and 820x700 with a photo loaded: on the desktop the strip is gone and the card carries RGB/R/G/B/RESET, the R tab bends the red curve while the composite stays drawn beneath it; on the phone the strip still opens TONE -> EV..BLACK, TONE CURVE, and the card is the same.
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import type { ToneCurve, ToneCurvePoint } from '../../shared/types';
|
||||
import type { ToneCurve, ToneCurveChannel, ToneCurvePoint } from '../../shared/types';
|
||||
import { readHistogram, type Histogram } from './Histogram';
|
||||
import {
|
||||
CURVE_CHANNELS,
|
||||
CURVE_CHANNEL_COLOR,
|
||||
addCurvePoint,
|
||||
curvePoints,
|
||||
@@ -11,10 +12,9 @@ import {
|
||||
sampleCurve,
|
||||
} from '../../shared/utils/toneCurve';
|
||||
|
||||
// LIGHT > TONE CURVE: the graph that hangs on the photo. One curve — the
|
||||
// composite one, the `rgb` key — drawn over a grid, with the points the user
|
||||
// drags. The per-channel tabs are gone: the card is the graph and nothing else.
|
||||
// It reads and writes nothing but that one key of the `toneCurve` field, so the
|
||||
// LIGHT > TONE CURVE: the graph that hangs on the photo. Four curves — the
|
||||
// composite one and one per channel — drawn over a grid, with the points the
|
||||
// user drags. It reads and writes nothing but the `toneCurve` field, so the
|
||||
// render is the only other thing that has to know what a curve is (see
|
||||
// toneCurve.ts for the table it hands the renderer).
|
||||
//
|
||||
@@ -29,7 +29,12 @@ import {
|
||||
const SIZE = 224; // the graph's own square, in px
|
||||
const STEPS = 64; // how finely the line is drawn
|
||||
const HIT = 11; // px within which the pointer takes hold of a point
|
||||
const NEEDS = 0.002; // how far off the diagonal counts as a move, as in toneCurve.ts
|
||||
|
||||
const LABEL: Record<ToneCurveChannel, string> = { rgb: 'RGB', r: 'R', g: 'G', b: 'B' };
|
||||
|
||||
const clamp01 = (v: number) => (v < 0 ? 0 : v > 1 ? 1 : v);
|
||||
const offDiagonal = (pts: ToneCurvePoint[]) => pts.some((p) => Math.abs(p[1] - p[0]) > NEEDS);
|
||||
|
||||
export function ToneCurvePanel({
|
||||
curve,
|
||||
@@ -42,6 +47,7 @@ export function ToneCurvePanel({
|
||||
onChange: (next: ToneCurve) => void;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const [channel, setChannel] = useState<ToneCurveChannel>('rgb');
|
||||
const [hist, setHist] = useState<Histogram | null>(null);
|
||||
const [pos, setPos] = useState<{ x: number; y: number } | null>(null);
|
||||
const [dragging, setDragging] = useState(false);
|
||||
@@ -70,14 +76,15 @@ export function ToneCurvePanel({
|
||||
};
|
||||
}, [url]);
|
||||
|
||||
const pts = curvePoints(curve, 'rgb');
|
||||
const pts = curvePoints(curve, channel);
|
||||
const master = curvePoints(curve, 'rgb');
|
||||
|
||||
// A curve pulled back to the diagonal stores nothing: the recipe stays as
|
||||
// A channel pulled back to the diagonal stores nothing: the recipe stays as
|
||||
// small as the curve is flat, and an old recipe reads the same either way.
|
||||
const put = (next: ToneCurvePoint[]): ToneCurve => {
|
||||
const out: ToneCurve = { ...curve };
|
||||
if (isFlatCurve(next)) delete out.rgb;
|
||||
else out.rgb = next;
|
||||
if (isFlatCurve(next)) delete out[channel];
|
||||
else out[channel] = next;
|
||||
return out;
|
||||
};
|
||||
|
||||
@@ -224,9 +231,9 @@ export function ToneCurvePanel({
|
||||
|
||||
const grid = [0, 1, 2, 3, 4].map((i) => (i * SIZE) / 4);
|
||||
// What the graph is drawn against: the luminance of the picture on the grey
|
||||
// channel — the shape of the whole frame, since the composite curve is the
|
||||
// only one the card draws.
|
||||
const bins = hist ? hist.lum : null;
|
||||
// channel, and that channel's own distribution when a single channel is being
|
||||
// bent — the histogram follows the tab, the way Lightroom's does.
|
||||
const bins = hist ? (channel === 'rgb' ? hist.lum : hist[channel]) : null;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -249,6 +256,38 @@ export function ToneCurvePanel({
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<div className="curve-tabs">
|
||||
{CURVE_CHANNELS.map((ch) => (
|
||||
<button
|
||||
key={ch}
|
||||
type="button"
|
||||
className={`curve-tab${ch === channel ? ' on' : ''}${offDiagonal(curvePoints(curve, ch)) ? ' moved' : ''}`}
|
||||
data-key={`curve-tab-${ch}`}
|
||||
aria-pressed={ch === channel}
|
||||
onClick={() => {
|
||||
setChannel(ch);
|
||||
drag.current = null;
|
||||
}}
|
||||
>
|
||||
<span className="curve-dot" data-color={CURVE_CHANNEL_COLOR[ch]} style={{ background: CURVE_CHANNEL_COLOR[ch] }} />
|
||||
{LABEL[ch]}
|
||||
</button>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
className="curve-reset"
|
||||
data-key="curve-reset"
|
||||
onClick={() => {
|
||||
drag.current = null;
|
||||
// RESET is the whole graph, not the channel on screen: the button
|
||||
// sits with the tabs, and "the curve" is what the chip and the
|
||||
// pixels answer to, so a flat object means every channel is flat.
|
||||
onChange({});
|
||||
}}
|
||||
>
|
||||
RESET
|
||||
</button>
|
||||
</div>
|
||||
<svg
|
||||
ref={svgRef}
|
||||
className="curve-graph"
|
||||
@@ -294,10 +333,11 @@ export function ToneCurvePanel({
|
||||
{/* The diagonal is the neutral line: a curve that never leaves it does
|
||||
nothing, which is what makes "how far has this moved" readable. */}
|
||||
<line className="curve-diagonal" x1={0} y1={SIZE} x2={SIZE} y2={0} />
|
||||
{channel !== 'rgb' ? <path className="curve-line ghost" d={line(master)} /> : null}
|
||||
<path
|
||||
className="curve-line"
|
||||
data-key="curve-line"
|
||||
style={{ stroke: CURVE_CHANNEL_COLOR.rgb }}
|
||||
style={{ stroke: CURVE_CHANNEL_COLOR[channel] }}
|
||||
d={line(pts)}
|
||||
/>
|
||||
{pts.map((p, i) => (
|
||||
@@ -308,7 +348,7 @@ export function ToneCurvePanel({
|
||||
cx={p[0] * SIZE}
|
||||
cy={(1 - p[1]) * SIZE}
|
||||
r={i === 0 || i === pts.length - 1 ? 6 : 5}
|
||||
style={{ stroke: CURVE_CHANNEL_COLOR.rgb }}
|
||||
style={{ stroke: CURVE_CHANNEL_COLOR[channel] }}
|
||||
/>
|
||||
))}
|
||||
</svg>
|
||||
|
||||
Reference in New Issue
Block a user