Zoom and pan the preview with the wheel, and step a parameter from its ruler

Cursor over the image: the wheel zooms about the pointer, and a drag pans,
both clamped to the contain-fit of the loaded bitmap so the photo can never
be lost off-screen. Cursor over an open parameter ruler: the wheel moves that
parameter by one step, clamped to its min/max. While the crop frame is being
dragged the wheel keeps resizing the crop instead, and APPLY puts the view
back on the fit.
This commit is contained in:
2026-09-17 20:57:00 +07:00
parent 6e8a600a82
commit 014b3e5a4b
3 changed files with 166 additions and 2 deletions
+22 -1
View File
@@ -1,3 +1,5 @@
import { useEffect, useRef } from 'react';
// One strip at a time — the same shape as the phone's AdjustmentPanel: a ruler
// row for the open slider, an options strip for the open group, then the tab's
// chip row. Chip labels are the phone's own strings (they are not translated
@@ -53,8 +55,27 @@ export function ChipRow({ chips }: { chips: ChipDef[] }) {
}
export function SliderRow({ slider }: { slider: SliderDef }) {
// The wheel belongs to the open ruler: one notch is one step, held inside the
// parameter's own range. The listener is native because React's is passive —
// the panel must not scroll away under the cursor while the value moves.
const rowRef = useRef<HTMLDivElement>(null);
const live = useRef(slider);
live.current = slider;
useEffect(() => {
const el = rowRef.current;
if (!el) return;
const onWheel = (e: WheelEvent) => {
e.preventDefault();
const s = live.current;
const step = s.step || 1;
const next = Math.min(s.max, Math.max(s.min, s.value + (e.deltaY < 0 ? step : -step)));
if (next !== s.value) s.onChange(next);
};
el.addEventListener('wheel', onWheel, { passive: false });
return () => el.removeEventListener('wheel', onWheel);
}, []);
return (
<div className="slider-row">
<div className="slider-row" ref={rowRef}>
<button type="button" className="back" onClick={slider.onBack} aria-label="back">&lt;</button>
<div className="ruler">
<div className="head">