// carousel.js — pure carousel-swipe and crop-nudge logic (plan Step 5 + // red-team D4). The simulator component wires pointer events through these // so the swipe threshold, clamping, and keyboard nudge are unit-testable. import { clampCropRect } from './cropViewport'; /** * Clamp a slide index into [0, n-1]. */ export function clampIndex(index, n) { if (!Number.isInteger(n) || n < 1) return 0; return Math.min(Math.max(index, 0), n - 1); } /** * Resolve the slide index after a horizontal drag. * * @param {number} index current slide index * @param {number} dragDeltaX total pointer delta in px (positive = dragged * right / finger moved right) * @param {number} threshold px distance that triggers a slide change * @param {number} n total slides * @returns {number} new clamped index * * Dragging LEFT (deltaX < 0) reveals the NEXT slide (index+1); dragging * RIGHT (deltaX > 0) goes back (index-1). Sub-threshold drags stay put. */ export function resolveIndex(index, dragDeltaX, threshold, n) { const base = clampIndex(index, n); if (dragDeltaX <= -threshold) return clampIndex(base + 1, n); if (dragDeltaX >= threshold) return clampIndex(base - 1, n); return base; } /** * Nudge a crop rect by whole pixels (red-team D4 keyboard positioning). * Delegates clamping to clampCropRect so the crop can never leave the * source. step lets Shift-arrow move in larger increments. */ export function nudgeCrop(crop, dx, dy, imgW, imgH, step = 1) { return clampCropRect( { ...crop, x: crop.x + dx * step, y: crop.y + dy * step, }, imgW, imgH ); }