feat(studio): a finger across the photo is the open ruler's own track
This commit is contained in:
@@ -4720,6 +4720,16 @@ export function Workspace() {
|
||||
canCompare={!!source}
|
||||
onSaveOriginal={() => void saveOriginal()}
|
||||
onClear={() => setClearing(true)}
|
||||
// The open ruler, handed to the stage as the track the photo itself
|
||||
// becomes: a chip's ruler stands at the foot of a phone's screen, and
|
||||
// the corner being judged is at the top of it, so a finger dragged
|
||||
// across the picture moves the knob instead. STRAIGHTEN and TONE CURVE
|
||||
// open no row (see `slider`), and neither hands anything over.
|
||||
rulerDrag={
|
||||
slider
|
||||
? { value: slider.value, min: slider.min, max: slider.max, step: slider.step, onChange: slider.onChange }
|
||||
: null
|
||||
}
|
||||
picking={picking}
|
||||
onPickColor={pickColor}
|
||||
brushTool={brushTool}
|
||||
|
||||
@@ -60,6 +60,20 @@ export type WmMark = {
|
||||
onMove: (p: { x?: number; y?: number; size?: number }) => void;
|
||||
};
|
||||
|
||||
// A ruler that is open while the photo is under the hand. A phone has no wheel
|
||||
// to notch and the row the ruler stands in is off at the foot of the screen,
|
||||
// away from the thing being judged: with one of these in hand, the photo itself
|
||||
// is the track — a finger dragged across it moves the value the same distance
|
||||
// the drag covers, so the number can be found on the picture it is changing.
|
||||
// The range travels the whole width, the same travel the row's own slider has.
|
||||
export type RulerDrag = {
|
||||
value: number;
|
||||
min: number;
|
||||
max: number;
|
||||
step: number;
|
||||
onChange: (value: number) => void;
|
||||
};
|
||||
|
||||
// One mark's box, from the numbers the engine draws it with. The width of the
|
||||
// text is the face's own when the mark carries a family (measureMark), and the
|
||||
// monospace em above otherwise; the ascent/descent come back with it, because
|
||||
@@ -270,6 +284,7 @@ export function ImageStage({
|
||||
marks = [],
|
||||
straightenRuler = null,
|
||||
curvePanel = null,
|
||||
rulerDrag = null,
|
||||
// FX's brush: the tool that takes the pointer on the photo, and the size a
|
||||
// spot is laid down at. One spot is a circle — HEAL's the area it fills,
|
||||
// MOSAIC's the cell it hides. Either way they are fractions of the render,
|
||||
@@ -351,6 +366,13 @@ export function ImageStage({
|
||||
// rather than in a column beside it. It is anchored, not draggable — the graph
|
||||
// is a fixed square and the photo moves under it.
|
||||
curvePanel?: ReactNode;
|
||||
// The ruler a chip has open, when one is. While it is here a finger dragged
|
||||
// across the photo moves that knob instead of the view — the value travels
|
||||
// with the hand's own direction, right for up the scale and left for down it
|
||||
// — and two fingers are still the phone's own zoom, because a pinch is not a
|
||||
// value (see the listeners on the stage's box). Null leaves the photo's
|
||||
// gestures exactly as they were.
|
||||
rulerDrag?: RulerDrag | null;
|
||||
// FX's brush, HEAL or MOSAIC: the circle under the cursor is the size of the
|
||||
// spot a gesture lays down (the wheel sets it), and one gesture reports its
|
||||
// spots together — one gesture is one undo step however many spots it laid.
|
||||
@@ -998,8 +1020,25 @@ export function ImageStage({
|
||||
// the pinch is what two fingers mean, and what is left of the stroke is not
|
||||
// laid down (the pointer releases the stroke rather than repainting it with
|
||||
// the finger after).
|
||||
//
|
||||
// The same listeners carry the ruler: a chip's ruler stands in a row at the
|
||||
// foot of a phone's screen, which is a long way from the corner a reader is
|
||||
// looking at, and one finger is not spoken for while a ruler is open (opening
|
||||
// one puts FIX's brush and the mask tool down). So one finger there is that
|
||||
// ruler's track and two are still the zoom — the pinch wins the moment its
|
||||
// second finger lands.
|
||||
const fingersRef = useRef(new Map<number, { x: number; y: number }>());
|
||||
const pinchRef = useRef<{ d: number; s: number; px: number; py: number } | null>(null);
|
||||
// The open ruler, read through a ref so the listener below is bound once per
|
||||
// stage rather than once per render: the value it is on belongs to the parent
|
||||
// and changes on every move of the very drag it is measuring.
|
||||
const rulerDragRef = useRef(rulerDrag);
|
||||
rulerDragRef.current = rulerDrag;
|
||||
// The ruler's own drag, in hand: which finger started it, where it began and
|
||||
// what the value was then. Read against its own beginning rather than against
|
||||
// the last move, so a step the pointer snaps to is never counted twice and the
|
||||
// number under the finger never drifts away from it.
|
||||
const rulerRef = useRef<{ id: number; x: number; value: number } | null>(null);
|
||||
useEffect(() => {
|
||||
const wrap = wrapRef.current;
|
||||
if (!wrap) return;
|
||||
@@ -1019,7 +1058,18 @@ export function ImageStage({
|
||||
const down = (e: PointerEvent) => {
|
||||
if (e.pointerType !== 'touch') return;
|
||||
fingersRef.current.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
||||
// The first finger is the open ruler's, when there is one: where it went
|
||||
// down and what the knob read at that moment. Nothing is changed yet — a
|
||||
// tap on the photo is not a move of the number.
|
||||
const r = rulerDragRef.current;
|
||||
if (fingersRef.current.size === 1) {
|
||||
if (r) rulerRef.current = { id: e.pointerId, x: e.clientX, value: r.value };
|
||||
return;
|
||||
}
|
||||
if (fingersRef.current.size !== 2) return;
|
||||
// The second finger is the zoom, and a pinch is not a value: whatever the
|
||||
// first finger had hold of is let go the moment this one lands.
|
||||
rulerRef.current = null;
|
||||
const m = pinch();
|
||||
if (!m || !(m.d > 0)) return;
|
||||
// The photo's own point under the fingers, and the size the stage is at:
|
||||
@@ -1035,7 +1085,25 @@ export function ImageStage({
|
||||
finger.y = e.clientY;
|
||||
}
|
||||
const p = pinchRef.current;
|
||||
if (!p) return;
|
||||
if (!p) {
|
||||
// One finger, no pinch: the ruler's own drag. The photo's width is the
|
||||
// track — the range travels it end to end, the same travel the row's
|
||||
// slider has — and the value is read from where this drag began, so a
|
||||
// hand that comes back the way it went lands on the number it left.
|
||||
const r = rulerRef.current;
|
||||
const d = rulerDragRef.current;
|
||||
if (!r || !d || r.id !== e.pointerId) return;
|
||||
const width = wrap.getBoundingClientRect().width;
|
||||
if (!(width > 0) || !(d.max > d.min)) return;
|
||||
e.preventDefault();
|
||||
const step = d.step > 0 ? d.step : 1;
|
||||
const raw = r.value + ((e.clientX - r.x) / width) * (d.max - d.min);
|
||||
const next = Math.min(d.max, Math.max(d.min, Number((Math.round(raw / step) * step).toFixed(4))));
|
||||
// A move that lands on the same step as the last one is not an edit:
|
||||
// firing it would lay a second undo step over a drag that never left.
|
||||
if (next !== d.value) d.onChange(next);
|
||||
return;
|
||||
}
|
||||
// What the first finger had begun — a stroke, a repair in hand, a pan — is
|
||||
// dropped here rather than at the second `pointerdown`, because the
|
||||
// brush's own handler on that layer runs after this listener does.
|
||||
@@ -1051,6 +1119,7 @@ export function ImageStage({
|
||||
setView({ s, ...clampPan(s, m.x - s * p.px, m.y - s * p.py) });
|
||||
};
|
||||
const up = (e: PointerEvent) => {
|
||||
if (rulerRef.current?.id === e.pointerId) rulerRef.current = null;
|
||||
fingersRef.current.delete(e.pointerId);
|
||||
// One finger left is not a pinch: the stage stays where the gesture put it
|
||||
// and the finger that is left paints nothing until it is lifted and put
|
||||
@@ -1068,6 +1137,7 @@ export function ImageStage({
|
||||
wrap.removeEventListener('pointercancel', up);
|
||||
fingersRef.current.clear();
|
||||
pinchRef.current = null;
|
||||
rulerRef.current = null;
|
||||
};
|
||||
// `previewUrl` because the wrap is only on the stage once a frame is: the
|
||||
// listener has to follow the node it is bound to.
|
||||
@@ -1329,6 +1399,10 @@ export function ImageStage({
|
||||
|
||||
const startPan = (e: React.PointerEvent<HTMLImageElement>) => {
|
||||
if (cropEditing || compare || e.button !== 0) return;
|
||||
// A ruler that is open owns the finger: dragging the photo is moving that
|
||||
// knob, which is the same drag the listener on the wrap reads (see above).
|
||||
// Two fingers still pan and zoom, so the view is never out of reach.
|
||||
if (rulerDrag && e.pointerType === 'touch') return;
|
||||
if (frameId === 'custom' && onCustomFramePan) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
Reference in New Issue
Block a user