80 lines
2.8 KiB
TypeScript
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>
|
|
);
|
|
}
|