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
|
||||
// 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
|
||||
// does not shrink it — and its rotation handle sits on the +ry axis, so the
|
||||
// angle is simply the direction from the centre to the pointer, one quarter turn
|
||||
// back. A line has no angle to store, so turning one is moving its two ends
|
||||
// about its own middle by the same turn, which keeps the ramp's length and its
|
||||
// place — the gradient falls in the new direction and nowhere else changes.
|
||||
// 'move' takes the whole of it, the pin's own drag.
|
||||
// does not shrink it. A line has no angle to store, so turning one is moving its
|
||||
// two ends about its own middle by the same turn, which keeps the ramp's length
|
||||
// and its 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 = (
|
||||
m: GradientMask,
|
||||
part: MaskPart,
|
||||
p: { fx: number; fy: number },
|
||||
box: { width: number; height: number }
|
||||
box: { width: number; height: number },
|
||||
from: { fx: number; fy: number }
|
||||
): GradientMask => {
|
||||
const dx = (p.fx - m.x) * box.width;
|
||||
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 };
|
||||
}
|
||||
case 'rot': {
|
||||
if (m.kind === 'radial') return { ...m, angle: Math.atan2(dy, dx) - Math.PI / 2 };
|
||||
const cx = (m.x + m.ex) / 2;
|
||||
const cy = (m.y + m.ey) / 2;
|
||||
const pin = maskPin(m);
|
||||
// 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.
|
||||
const turn =
|
||||
Math.atan2((p.fy - cy) * box.height, (p.fx - cx) * box.width) -
|
||||
Math.atan2((m.ey - cy) * box.height, (m.ex - cx) * box.width);
|
||||
const bearing = (q: { fx: number; fy: number }) =>
|
||||
Math.atan2((q.fy - pin.y) * box.height, (q.fx - pin.x) * 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 sa = Math.sin(turn);
|
||||
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
|
||||
// own undo boundary, the same one the brush's grab keeps.
|
||||
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 maskDown = (e: React.PointerEvent<HTMLDivElement>) => {
|
||||
@@ -988,7 +994,7 @@ export function ImageStage({
|
||||
if (!onMaskEdit) return;
|
||||
e.stopPropagation();
|
||||
e.currentTarget.setPointerCapture(e.pointerId);
|
||||
maskGrab.current = { i, part };
|
||||
maskGrab.current = { i, part, from: brushPoint(e.clientX, e.clientY) };
|
||||
maskUndo.current = false;
|
||||
onMaskSel?.(i);
|
||||
};
|
||||
@@ -1000,7 +1006,8 @@ export function ImageStage({
|
||||
const p = brushPoint(e.clientX, e.clientY);
|
||||
if (!p) return;
|
||||
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);
|
||||
maskUndo.current = true;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user