Files
RecipesCam/docker/frontend/src/ui/StraightenRuler.tsx
T

80 lines
2.8 KiB
TypeScript

import { useRef } from 'react';
// FRAME > ROTATE > STRAIGHTEN's ruler is not one of the chip row's stacked
// knobs: it is a graduated scale lying across the foot of the photo, so the
// angle is read off the picture it is levelling. Dragging anywhere on the scale
// turns the photo by whole degrees; leaving the tool (another tab or button)
// keeps the angle and takes the scale away — see App's dismiss effect.
const RANGE = 180; // degrees either side of 0, the whole circle across the width
const TICK = 5; // minor tick spacing
const MAJOR = 45; // labelled tick spacing
export function StraightenRuler({
value,
onChange,
onReset,
}: {
value: number;
onChange: (v: number) => void;
onReset: () => void;
}) {
const scaleRef = useRef<HTMLDivElement>(null);
const dragging = useRef(false);
// A pixel of travel is well under a degree at any sane width, so the reading
// snaps to whole degrees however far the pointer has been dragged.
const angleAt = (clientX: number) => {
const r = scaleRef.current?.getBoundingClientRect();
if (!r || !(r.width > 0)) return value;
const pct = (clientX - r.left) / r.width;
return Math.max(-RANGE, Math.min(RANGE, Math.round(pct * 2 * RANGE - RANGE)));
};
const ticks: number[] = [];
for (let d = -RANGE; d <= RANGE; d += TICK) ticks.push(d);
const pctOf = (d: number) => ((d + RANGE) / (2 * RANGE)) * 100;
return (
<div className="straighten-ruler" data-key="straighten-ruler" onPointerDown={(e) => e.stopPropagation()}>
<div className="straighten-head">
<span className="straighten-label">STRAIGHTEN</span>
<b className={value !== 0 ? 'amber' : ''} data-key="straighten-value">
{value > 0 ? `+${value}` : value}°
</b>
</div>
<div
ref={scaleRef}
className="straighten-scale"
data-key="straighten-scale"
onPointerDown={(e) => {
dragging.current = true;
e.currentTarget.setPointerCapture(e.pointerId);
onChange(angleAt(e.clientX));
}}
onPointerMove={(e) => {
if (dragging.current) onChange(angleAt(e.clientX));
}}
onPointerUp={(e) => {
dragging.current = false;
e.currentTarget.releasePointerCapture(e.pointerId);
}}
onPointerCancel={() => {
dragging.current = false;
}}
onDoubleClick={onReset}
>
{ticks.map((d) => (
<span
key={d}
className={`straighten-tick${d === 0 ? ' zero' : d % MAJOR === 0 ? ' major' : ''}`}
style={{ left: `${pctOf(d)}%` }}
>
{d % MAJOR === 0 ? <i>{d > 0 ? `+${d}` : d}</i> : null}
</span>
))}
<span className="straighten-needle" data-key="straighten-needle" style={{ left: `${pctOf(value)}%` }} />
</div>
</div>
);
}