web: give a mask its knobs as rulers and let the ramp be turned
A mask's knobs were a card of small sliders side by side, three abreast under the colour they were moving, which is the shape the mixer needs because its whole point is reading three bands at once. A mask has nothing to read against: its knobs are a list, and the strip beside the photo has always had the shape for a list — the ruler, one parameter to a row, with its own name, its own value and the width to aim with. So the card goes and the rows come: EXPOSURE, CONTRAST, SATURATION, and FEATHER below them for the shape that fades over one, each the same ruler the FX panel opens, stacked in the mask's own column next to the chip that says which shape is chosen. Only the ramp could be moved and not aimed. An ellipse is stored as an angle, so turning it is writing a new one; a line is stored as the two points it falls between, and turning one is moving both of them about their own middle by the same turn — which is the point of doing it that way: the length survives, the middle survives, and only the direction the gradient falls in changes. The turn handle a chosen ramp now wears hangs clear of its middle along the ramp's own normal, so it never sits on the pin the shape is dragged by, and the angle the hand asks for is measured in the photo's own pixels, so a quarter turn of the hand is a quarter turn of the ramp however the photo is shaped or zoomed. Both handles for both kinds now come out of one list and one map, which is how a ramp and an ellipse ended up wearing the same class, the round one that says "this turns me". ponytail: the ruler row takes no data-key of its own — the parameter's key is already on the chip that opened it, and the panel's fourth column opens a ruler under the same name a strip chip answers to, so a second element carrying it would make an existing selector ambiguous. The mask's rows are found by the label they print; if a probe ever needs to hook a row directly, the key belongs on the row and the panel's ruler needs its own name first. Highlights/Shadows and per-mask invert and range are still out, as before. Verified: tsc clean; mask-probe 42/0 on the dev server and again on 8090 — the three rows share an edge and stack, a linear mask carries no feather row, a radial one carries it fourth, and dragging the ramp's turn handle stands the gradient up the photo: 61 at the top and 244 at the bottom where it was 61 -> 244 across — brush-edit 33/0, heal-idle 23/0, heal-zoom-drag 28/0, landing/pro-gate/ award-column/otp-code/tone-curve all ALL PASS, backend 180/0.
This commit is contained in:
+69
-43
@@ -2233,6 +2233,69 @@ export function Workspace() {
|
||||
|| (Object.keys(DEFAULT_GPS_STYLE) as (keyof GpsStyle)[]).some((k) => gpsStyle[k] !== DEFAULT_GPS_STYLE[k]);
|
||||
}, [recipe, simId, frameId, crop, rotation, straighten, markOn, markText, markStyle, useGeotag, gpsStyle]);
|
||||
|
||||
// The chosen mask's knobs, as the app's own rulers: one parameter to a row,
|
||||
// the same row every other number in the app is edited with. EXPOSURE is the
|
||||
// spec's EV (-5..+5, its own `pow(2.0, e)`); CONTRAST and SATURATION are the
|
||||
// app's -10..+10, which the shader reads as the spec's -1..+1; and a radial
|
||||
// mask adds the feather it fades over — the percentage of its own axis the
|
||||
// alpha holds full before it dies at the rim.
|
||||
const maskRulers: SliderDef[] = !selMask
|
||||
? []
|
||||
: [
|
||||
{
|
||||
key: 'mask-exposure',
|
||||
label: 'EXPOSURE',
|
||||
value: selMask.exposure,
|
||||
min: -MASK_EXPOSURE_MAX,
|
||||
max: MASK_EXPOSURE_MAX,
|
||||
step: 0.1,
|
||||
display: `${selMask.exposure > 0 ? '+' : ''}${selMask.exposure.toFixed(1)}`,
|
||||
edited: selMask.exposure !== 0,
|
||||
onChange: (v) => setMaskKnob({ exposure: v }),
|
||||
onReset: () => setMaskKnob({ exposure: 0 }),
|
||||
},
|
||||
{
|
||||
key: 'mask-contrast',
|
||||
label: 'CONTRAST',
|
||||
value: selMask.contrast,
|
||||
min: -10,
|
||||
max: 10,
|
||||
step: 1,
|
||||
display: selMask.contrast > 0 ? `+${selMask.contrast}` : String(selMask.contrast),
|
||||
edited: selMask.contrast !== 0,
|
||||
onChange: (v) => setMaskKnob({ contrast: Math.round(v) }),
|
||||
onReset: () => setMaskKnob({ contrast: 0 }),
|
||||
},
|
||||
{
|
||||
key: 'mask-saturation',
|
||||
label: 'SATURATION',
|
||||
value: selMask.saturation,
|
||||
min: -10,
|
||||
max: 10,
|
||||
step: 1,
|
||||
display: selMask.saturation > 0 ? `+${selMask.saturation}` : String(selMask.saturation),
|
||||
edited: selMask.saturation !== 0,
|
||||
onChange: (v) => setMaskKnob({ saturation: Math.round(v) }),
|
||||
onReset: () => setMaskKnob({ saturation: 0 }),
|
||||
},
|
||||
...(selMask.kind === 'radial'
|
||||
? [
|
||||
{
|
||||
key: 'mask-feather',
|
||||
label: 'FEATHER',
|
||||
value: Math.round(selMask.feather * 100),
|
||||
min: 0,
|
||||
max: 100,
|
||||
step: 5,
|
||||
display: `${Math.round(selMask.feather * 100)}%`,
|
||||
edited: selMask.feather !== MASK_DEFAULT_FEATHER,
|
||||
onChange: (v: number) => setMaskKnob({ feather: v / 100 }),
|
||||
onReset: () => setMaskKnob({ feather: MASK_DEFAULT_FEATHER }),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
|
||||
const resetChip = (): ChipDef => ({
|
||||
key: 'reset-all',
|
||||
label: resetDirty ? 'RESET *' : 'RESET',
|
||||
@@ -2768,9 +2831,9 @@ export function Workspace() {
|
||||
</div>
|
||||
|
||||
{/* column 2 — FX's GRADIENT MASK, while one is chosen: the shape's own
|
||||
name, the way to take it off the photo, and the three knobs it
|
||||
grades with. A radial mask adds the feather it fades over, since
|
||||
that is the number its rim is made of. */}
|
||||
name, the way to take it off the photo, and its knobs as the app's
|
||||
own rulers — one parameter to a row. A radial mask adds the feather
|
||||
it fades over, since that is the number its rim is made of. */}
|
||||
{selMask ? (
|
||||
<div className="col col-sub strip" data-col="mask">
|
||||
<ChipRow
|
||||
@@ -2779,46 +2842,9 @@ export function Workspace() {
|
||||
{ key: 'mask-delete', label: 'DELETE', onClick: deleteMask },
|
||||
]}
|
||||
/>
|
||||
<div className="hsl-card-knobs">
|
||||
<MiniSlider
|
||||
label="EXP"
|
||||
value={selMask.exposure}
|
||||
min={-MASK_EXPOSURE_MAX}
|
||||
max={MASK_EXPOSURE_MAX}
|
||||
step={0.1}
|
||||
prefix="mask-knob"
|
||||
format={(v) => `${v > 0 ? '+' : ''}${v.toFixed(1)}`}
|
||||
onChange={(v) => setMaskKnob({ exposure: v })}
|
||||
onReset={() => setMaskKnob({ exposure: 0 })}
|
||||
/>
|
||||
<MiniSlider
|
||||
label="CON"
|
||||
value={selMask.contrast}
|
||||
prefix="mask-knob"
|
||||
onChange={(v) => setMaskKnob({ contrast: Math.round(v) })}
|
||||
onReset={() => setMaskKnob({ contrast: 0 })}
|
||||
/>
|
||||
<MiniSlider
|
||||
label="SAT"
|
||||
value={selMask.saturation}
|
||||
prefix="mask-knob"
|
||||
onChange={(v) => setMaskKnob({ saturation: Math.round(v) })}
|
||||
onReset={() => setMaskKnob({ saturation: 0 })}
|
||||
/>
|
||||
{selMask.kind === 'radial' ? (
|
||||
<MiniSlider
|
||||
label="FEA"
|
||||
value={Math.round(selMask.feather * 100)}
|
||||
min={0}
|
||||
max={100}
|
||||
step={5}
|
||||
prefix="mask-knob"
|
||||
format={(v) => `${v}%`}
|
||||
onChange={(v) => setMaskKnob({ feather: v / 100 })}
|
||||
onReset={() => setMaskKnob({ feather: MASK_DEFAULT_FEATHER })}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
{maskRulers.map((def) => (
|
||||
<SliderRow key={def.key} slider={def} />
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user