web: turn a mask by the turn the hand makes
A press anywhere on a mask's outline takes hold of it to turn it, and the turn it asked for was read as an absolute bearing: the direction from the shape's pin to wherever the finger now happened to be. The outline is a grip a hand lands on wherever it likes — a ramp's line runs the whole way across the photo — so the moment a press was set down on it the shape swung round to face that finger. A press on the edge of a ramp lying across the photo stood it upright on a ten-pixel move, and a shape already turned snapped to the angle of the press before it had been dragged at all. The turn is now what the hand turns: the change in bearing about the pin since the press, which on the first move is the press itself. A finger that lands on the outline and stays there leaves the angle exactly as it found it; one that carries the shape round by a quarter turn turns it by a quarter turn, whatever angle it was at to begin with. An ellipse adds that turn to the angle it stores, a ramp spins its two ends about its own middle, and both are measured in the photo's own pixels so the two bearings are the same kind of thing. ponytail: The turn is read against the previous pointer position the drag already keeps, so it costs one subtraction and no state. A pointer that leaves the photo mid-turn keeps the angle it stood at — the rule the brush already follows — and the next move on the picture resumes from the last position that was on it. Verified: tsc clean; mask-probe 52/0 on the dev server and again on 8090 — a press on a ramp's own edge that travels a hundredth of the photo's width leaves the ramp at the angle it was taken hold of at (ends 0.500 and 0.500), and on a shape already turned the same press keeps it upright (0.562,0.260 and 0.558,0.860); a quarter turn of the hand on the edge turns the ramp a quarter of the way round (ends 0.200 and 0.800, one pin, no second shape laid); the pin still takes the shape with the hand to where the hand went (0.560,0.560, no sideways throw); the rim of an ellipse still turns with the hand on it (1 pin); another chip still leaves 0 mask columns and the mask chip brings 1 back; DELETE still takes the chosen shape off the photo with its grade (64 -> 255, then back to 61). Against the code before this change the same probe fails 9, both angle checks among them — the ramp lands at 0.850,0.560 and 0.860,0.562, that is, wherever the finger was. brush-edit 33/0, heal-idle 23/0, heal-zoom-drag 28/0, landing/pro-gate/ award-column/otp-code/tone-curve/hsl-panel/grain-controls/chip-edge/chips-desk/ slider-reset/temp-swatch all ALL PASS, backend 180/0.
This commit is contained in:
@@ -169,17 +169,22 @@ const maskFromDrag = (
|
|||||||
// One handle's move, as the shape it leaves. The two ends of a linear ramp are
|
// One handle's move, as the shape it leaves. The two ends of a linear ramp are
|
||||||
// themselves; a radial mask resizes along the ellipse's OWN axes — the pointer
|
// themselves; a radial mask resizes along the ellipse's OWN axes — the pointer
|
||||||
// is projected onto the axis the handle rides, so a hand that wanders across it
|
// is projected onto the axis the handle rides, so a hand that wanders across it
|
||||||
// does not shrink it — and its rotation handle sits on the +ry axis, so the
|
// does not shrink it. A line has no angle to store, so turning one is moving its
|
||||||
// angle is simply the direction from the centre to the pointer, one quarter turn
|
// two ends about its own middle by the same turn, which keeps the ramp's length
|
||||||
// back. A line has no angle to store, so turning one is moving its two ends
|
// and its place — the gradient falls in the new direction and nowhere else
|
||||||
// about its own middle by the same turn, which keeps the ramp's length and its
|
// changes. 'move' takes the whole of it, the pin's own drag.
|
||||||
// place — the gradient falls in the new direction and nowhere else changes.
|
//
|
||||||
// 'move' takes the whole of it, the pin's own drag.
|
// Turning is RELATIVE to where the hand took hold: the turn is how far it has
|
||||||
|
// travelled about the pin since then, not the direction it now points. A mask's
|
||||||
|
// edge is a grip you can land on anywhere, so an absolute angle would snap the
|
||||||
|
// shape to face the finger the instant it was brushed. `from` is the previous
|
||||||
|
// pointer position, the press itself on the first move.
|
||||||
const maskDragged = (
|
const maskDragged = (
|
||||||
m: GradientMask,
|
m: GradientMask,
|
||||||
part: MaskPart,
|
part: MaskPart,
|
||||||
p: { fx: number; fy: number },
|
p: { fx: number; fy: number },
|
||||||
box: { width: number; height: number }
|
box: { width: number; height: number },
|
||||||
|
from: { fx: number; fy: number }
|
||||||
): GradientMask => {
|
): GradientMask => {
|
||||||
const dx = (p.fx - m.x) * box.width;
|
const dx = (p.fx - m.x) * box.width;
|
||||||
const dy = (p.fy - m.y) * box.height;
|
const dy = (p.fy - m.y) * box.height;
|
||||||
@@ -199,14 +204,15 @@ const maskDragged = (
|
|||||||
return { ...m, x: m.x + ddx, y: m.y + ddy, ex: m.ex + ddx, ey: m.ey + ddy };
|
return { ...m, x: m.x + ddx, y: m.y + ddy, ex: m.ex + ddx, ey: m.ey + ddy };
|
||||||
}
|
}
|
||||||
case 'rot': {
|
case 'rot': {
|
||||||
if (m.kind === 'radial') return { ...m, angle: Math.atan2(dy, dx) - Math.PI / 2 };
|
const pin = maskPin(m);
|
||||||
const cx = (m.x + m.ex) / 2;
|
|
||||||
const cy = (m.y + m.ey) / 2;
|
|
||||||
// The turn is asked for in the photo's own pixels, so the angle the hand
|
// The turn is asked for in the photo's own pixels, so the angle the hand
|
||||||
// asks for and the angle the ramp lies at are measured the same way.
|
// asks for and the angle the ramp lies at are measured the same way.
|
||||||
const turn =
|
const bearing = (q: { fx: number; fy: number }) =>
|
||||||
Math.atan2((p.fy - cy) * box.height, (p.fx - cx) * box.width) -
|
Math.atan2((q.fy - pin.y) * box.height, (q.fx - pin.x) * box.width);
|
||||||
Math.atan2((m.ey - cy) * box.height, (m.ex - cx) * box.width);
|
const turn = bearing(p) - bearing(from);
|
||||||
|
if (m.kind === 'radial') return { ...m, angle: m.angle + turn };
|
||||||
|
const cx = (m.x + m.ex) / 2;
|
||||||
|
const cy = (m.y + m.ey) / 2;
|
||||||
const ca = Math.cos(turn);
|
const ca = Math.cos(turn);
|
||||||
const sa = Math.sin(turn);
|
const sa = Math.sin(turn);
|
||||||
const spin = (x: number, y: number) => {
|
const spin = (x: number, y: number) => {
|
||||||
@@ -952,7 +958,7 @@ export function ImageStage({
|
|||||||
// the shape can be seen before it is let go; the flags below are the gesture's
|
// the shape can be seen before it is let go; the flags below are the gesture's
|
||||||
// own undo boundary, the same one the brush's grab keeps.
|
// own undo boundary, the same one the brush's grab keeps.
|
||||||
const [draft, setDraft] = useState<{ a: BrushPoint; b: BrushPoint } | null>(null);
|
const [draft, setDraft] = useState<{ a: BrushPoint; b: BrushPoint } | null>(null);
|
||||||
const maskGrab = useRef<{ i: number; part: MaskPart } | null>(null);
|
const maskGrab = useRef<{ i: number; part: MaskPart; from: { fx: number; fy: number } | null } | null>(null);
|
||||||
const maskUndo = useRef(false);
|
const maskUndo = useRef(false);
|
||||||
|
|
||||||
const maskDown = (e: React.PointerEvent<HTMLDivElement>) => {
|
const maskDown = (e: React.PointerEvent<HTMLDivElement>) => {
|
||||||
@@ -988,7 +994,7 @@ export function ImageStage({
|
|||||||
if (!onMaskEdit) return;
|
if (!onMaskEdit) return;
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.currentTarget.setPointerCapture(e.pointerId);
|
e.currentTarget.setPointerCapture(e.pointerId);
|
||||||
maskGrab.current = { i, part };
|
maskGrab.current = { i, part, from: brushPoint(e.clientX, e.clientY) };
|
||||||
maskUndo.current = false;
|
maskUndo.current = false;
|
||||||
onMaskSel?.(i);
|
onMaskSel?.(i);
|
||||||
};
|
};
|
||||||
@@ -1000,7 +1006,8 @@ export function ImageStage({
|
|||||||
const p = brushPoint(e.clientX, e.clientY);
|
const p = brushPoint(e.clientX, e.clientY);
|
||||||
if (!p) return;
|
if (!p) return;
|
||||||
const next = masks.slice();
|
const next = masks.slice();
|
||||||
next[g.i] = maskDragged(was, g.part, p, box);
|
next[g.i] = maskDragged(was, g.part, p, box, g.from ?? p);
|
||||||
|
g.from = p;
|
||||||
onMaskEdit(next, !maskUndo.current);
|
onMaskEdit(next, !maskUndo.current);
|
||||||
maskUndo.current = true;
|
maskUndo.current = true;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user