Something went wrong. Try again.
slices images for the bsky.app photo carousel (min 5 images)
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125// cropViewport.js — pure viewport/crop math for the crop UI (plan Step 4)//// Viewport state: { scale, ox, oy } where (ox, oy) is the SOURCE-pixel// coordinate at the viewport's top-left corner and scale is screen px per// source px. All functions are pure and unit-testable; the React component// just wires pointer/wheel events through them.
import { assertSliceCount } from './geometry';
// Clamp a scale into [minScale, maxScale].export function clampScale(scale, minScale, maxScale) { return Math.min(Math.max(scale, minScale), maxScale);}
// Scale that fits the whole image in the viewport (never below 1px/px... no:// fit can legitimately be < 1 for large images; guard only against 0 dims).export function fitScale(viewW, viewH, imgW, imgH) { if (!viewW || !viewH || !imgW || !imgH) return 1;
return Math.min(viewW / imgW, viewH / imgH);}
// Clamp the viewport offset so the viewport never shows empty space beyond// the image: when the scaled image is smaller than the viewport it is// centered; otherwise the offset stays within [0, scaled - view].export function clampOffset(ox, oy, scale, viewW, viewH, imgW, imgH) { const scaledW = imgW * scale; const scaledH = imgH * scale;
let cx = ox; let cy = oy;
if (scaledW <= viewW) { cx = -(viewW - scaledW) / (2 * scale); } else { cx = Math.min(Math.max(ox, 0), (scaledW - viewW) / scale); }
if (scaledH <= viewH) { cy = -(viewH - scaledH) / (2 * scale); } else { cy = Math.min(Math.max(oy, 0), (scaledH - viewH) / scale); }
return { ox: cx, oy: cy };}
// Initial viewport: fit the image, centered.export function initialViewport(viewW, viewH, imgW, imgH) { const scale = fitScale(viewW, viewH, imgW, imgH);
return { scale, ...clampOffset(0, 0, scale, viewW, viewH, imgW, imgH) };}
// Zoom about a screen-space point (cx, cy), keeping the source pixel under// the cursor fixed. Returns a clamped { scale, ox, oy }.export function zoomAt(viewport, cx, cy, factor, minScale, maxScale, viewW, viewH, imgW, imgH) { const newScale = clampScale(viewport.scale * factor, minScale, maxScale); const srcX = viewport.ox + cx / viewport.scale; const srcY = viewport.oy + cy / viewport.scale; const ox = srcX - cx / newScale; const oy = srcY - cy / newScale;
return { scale: newScale, ...clampOffset(ox, oy, newScale, viewW, viewH, imgW, imgH) };}
// Pan by a screen-space delta. Positive dx/dy = content follows the pointer.export function panBy(viewport, dx, dy, viewW, viewH, imgW, imgH) { return { scale: viewport.scale, ...clampOffset( viewport.ox - dx / viewport.scale, viewport.oy - dy / viewport.scale, viewport.scale, viewW, viewH, imgW, imgH ), };}
// Screen (px, py) -> source pixel. Returns { x, y } in source pixels.export function screenToSource(viewport, px, py) { return { x: viewport.ox + px / viewport.scale, y: viewport.oy + py / viewport.scale, };}
// Clamp a crop rect so it stays fully inside the source (plan Step 4// acceptance: "crop cannot be dragged outside source bounds").export function clampCropRect(crop, imgW, imgH) { const maxX = Math.max(0, imgW - crop.width); const maxY = Math.max(0, imgH - crop.height);
return { ...crop, x: Math.min(Math.max(crop.x, 0), maxX), y: Math.min(Math.max(crop.y, 0), maxY), };}
// Source-space x of the N-1 vertical slice boundary lines inside a crop.export function getSliceGridLines(crop, n) { assertSliceCount(n); const lines = [];
for (let i = 1; i < n; i++) { lines.push(crop.x + (crop.width * i) / n); }
return lines;}
// Convert a source-space rect to screen-space using the viewport.export function sourceRectToScreen(rect, viewport) { const x = (rect.x - viewport.ox) * viewport.scale; const y = (rect.y - viewport.oy) * viewport.scale;
return { x, y, width: rect.width * viewport.scale, height: rect.height * viewport.scale, };}