web: the mixer's card can be dragged off the colour it was read at
The card hangs on the point the eyedropper read, which is exactly where the user wants to watch the band move — so it covers the patch it is editing. Its body now takes a drag: the offset is a fraction of the photo, which is the layer the card lives in, so a zoom keeps it where it was put and a fresh pick drops it back on its own point. The knobs and the close button keep the pointer to themselves, and the anchor cannot leave the photo, so the card is always half in reach of a drag back.
This commit is contained in:
@@ -357,6 +357,10 @@ input[type="range"] { width: 100%; accent-color: var(--accent); }
|
||||
.hsl-panel {
|
||||
position: absolute;
|
||||
pointer-events: auto;
|
||||
/* Dragged by its body to a clear patch of photo; the knobs and the close
|
||||
button keep the pointer to themselves. */
|
||||
cursor: grab;
|
||||
touch-action: none;
|
||||
width: 248px;
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
|
||||
@@ -154,6 +154,16 @@ export function ImageStage({
|
||||
// Where the eyedropper's own icon is drawing itself inside the photo: the
|
||||
// layer is the photo's box, so plain pixels are enough.
|
||||
const [pickPos, setPickPos] = useState<{ x: number; y: number } | null>(null);
|
||||
// The colour that was read is rarely a clear patch of photo, so the mixer's
|
||||
// card can be dragged off the point it hangs on. The offset is a fraction of
|
||||
// the photo — which is what the layer is — so a zoom keeps the card where it
|
||||
// was put; a fresh pick starts it back on its point.
|
||||
const [panelAt, setPanelAt] = useState({ x: 0, y: 0 });
|
||||
const panelDrag = useRef<{ id: number; px: number; py: number; ox: number; oy: number; w: number; h: number } | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setPanelAt({ x: 0, y: 0 });
|
||||
}, [pickPanelAt]);
|
||||
|
||||
// \ toggles PEEK; the key is the app's own shortcut, kept identical on web.
|
||||
useEffect(() => {
|
||||
@@ -206,6 +216,32 @@ export function ImageStage({
|
||||
return () => window.removeEventListener('resize', measure);
|
||||
}, [cropEditing, compare, picking, pickPanelAt, wmCount, showStraighten, measure, previewUrl]);
|
||||
|
||||
const grabPanel = (e: React.PointerEvent<HTMLDivElement>) => {
|
||||
e.stopPropagation(); // the photo must not pan under the card
|
||||
if ((e.target as HTMLElement).closest('input, button, .mini-slider')) return; // those keep their own drag
|
||||
const rect = e.currentTarget.parentElement?.getBoundingClientRect();
|
||||
if (!rect || !(rect.width > 0 && rect.height > 0)) return;
|
||||
e.currentTarget.setPointerCapture(e.pointerId);
|
||||
panelDrag.current = { id: e.pointerId, px: e.clientX, py: e.clientY, ox: panelAt.x, oy: panelAt.y, w: rect.width, h: rect.height };
|
||||
};
|
||||
|
||||
const movePanel = (e: React.PointerEvent<HTMLDivElement>) => {
|
||||
const d = panelDrag.current;
|
||||
if (!d || d.id !== e.pointerId || !pickPanelAt) return;
|
||||
e.preventDefault();
|
||||
// The anchor stays on the photo, so half the card is always within reach of
|
||||
// a drag back however far it has been pushed.
|
||||
const fit = (v: number, at: number) => Math.max(-at, Math.min(1 - at, v));
|
||||
setPanelAt({
|
||||
x: fit(d.ox + (e.clientX - d.px) / d.w, pickPanelAt.fx),
|
||||
y: fit(d.oy + (e.clientY - d.py) / d.h, pickPanelAt.fy),
|
||||
});
|
||||
};
|
||||
|
||||
const endPanel = (e: React.PointerEvent<HTMLDivElement>) => {
|
||||
if (panelDrag.current?.id === e.pointerId) panelDrag.current = null;
|
||||
};
|
||||
|
||||
// Eyedropper. The decode is done on the click, not when PICK is armed: a
|
||||
// bitmap made up front would be stale after the first band move, and re-making
|
||||
// it on every render would decode a fresh JPEG per slider step. One bitmap is
|
||||
@@ -684,10 +720,13 @@ export function ImageStage({
|
||||
<div
|
||||
className="hsl-panel"
|
||||
data-key="hsl-panel"
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
onPointerDown={grabPanel}
|
||||
onPointerMove={movePanel}
|
||||
onPointerUp={endPanel}
|
||||
onPointerCancel={endPanel}
|
||||
style={{
|
||||
left: `${pickPanelAt.fx * 100}%`,
|
||||
top: `${pickPanelAt.fy * 100}%`,
|
||||
left: `${(pickPanelAt.fx + panelAt.x) * 100}%`,
|
||||
top: `${(pickPanelAt.fy + panelAt.y) * 100}%`,
|
||||
// Below the point, or above it when the point sits low enough
|
||||
// that the panel would fall off the photo.
|
||||
transform: pickPanelAt.fy > 0.55 ? 'translate(-50%, calc(-100% - 14px))' : 'translate(-50%, 14px)',
|
||||
|
||||
Reference in New Issue
Block a user