Something went wrong. Try again.
slices images for the bsky.app photo carousel (min 5 images)
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209// geometry.js — pure slice-geometry functions (plan Step 2)//// The Bluesky carousel requires every slide to have byte-identical// dimensions or the frame resizes mid-swipe and the panorama illusion// breaks. All geometry here enforces that invariant by construction:// the crop width is made divisible by N, then each slice is exactly// cropWidth/N wide and cropHeight tall, on integer pixel boundaries.
export const MIN_SLICES = 5;export const MAX_SLICES = 10;export const MAX_SLICE_DIMENSION = 4000;
/** * Validate the slice count against the carousel constraint (5-10). * Throws RangeError otherwise. */export function assertSliceCount(n) { if (!Number.isInteger(n) || n < MIN_SLICES || n > MAX_SLICES) { throw new RangeError( `slice count must be an integer between ${MIN_SLICES} and ${MAX_SLICES}, got ${n}` ); }
return n;}
/** * Validate that a crop rect lies within the source bounds. * Throws RangeError otherwise. */function assertCropInBounds(srcWidth, srcHeight, cropRect) { const { x, y, width, height } = cropRect;
if ( x < 0 || y < 0 || width <= 0 || height <= 0 || x + width > srcWidth || y + height > srcHeight ) { throw new RangeError( `cropRect ${JSON.stringify(cropRect)} is outside source ${srcWidth}x${srcHeight}` ); }}
/** * Make `width` divisible by `n` by trimming up to n-1 px symmetrically. * Returns the adjusted { x, width }. */function trimToDivisible(x, width, n) { const remainder = width % n;
if (remainder === 0) return { x, width }; const trimLeft = Math.floor(remainder / 2); const trimmedWidth = width - remainder;
return { x: x + trimLeft, width: trimmedWidth };}
/** * Compute the N equal-width slices for a crop. * * @param {Object} args * @param {number} args.srcWidth source image width * @param {number} args.srcHeight source image height * @param {{x:number,y:number,width:number,height:number}} args.cropRect * requested crop in source coordinates * @param {number} args.n slice count (5-10) * @returns {{ * slices: Array<{sx:number,sy:number,sw:number,sh:number}>, * sliceWidth: number, * sliceHeight: number, * effectiveCrop: {x:number,y:number,width:number,height:number} * }} * slices are in SOURCE coordinates; effectiveCrop is the crop after * symmetric trimming (width is the trimmed, N-divisible width). */export function computeSliceGrid({ srcWidth, srcHeight, cropRect, n }) { assertSliceCount(n); assertCropInBounds(srcWidth, srcHeight, cropRect);
const { x, y, height } = cropRect; const trimmed = trimToDivisible(x, cropRect.width, n); const effectiveWidth = trimmed.width;
if (effectiveWidth < n) { throw new RangeError( `crop width ${effectiveWidth} cannot be split into ${n} slices` ); }
const sliceWidth = effectiveWidth / n; const sliceHeight = height; const slices = [];
for (let i = 0; i < n; i++) { slices.push({ sx: trimmed.x + i * sliceWidth, sy: y, sw: sliceWidth, sh: sliceHeight, }); }
return { slices, sliceWidth, sliceHeight, effectiveCrop: { x: trimmed.x, y, width: effectiveWidth, height }, };}
/** * Normalize a target slice aspect to an integer ratio {w, h}. * Accepts "4:5", "16:9", "1:1", {w,h}, or a number (w/h). */export function normalizeAspect(targetSliceAspect) { // Boundary parse: aspect is a "W:H" string or a number. // oxlint-disable-next-line anti-slop/no-runtime-typeof if (typeof targetSliceAspect === 'string') { const m = targetSliceAspect.match(/^(\d+):(\d+)$/);
if (!m) throw new RangeError(`invalid aspect string: ${targetSliceAspect}`);
return { w: Number(m[1]), h: Number(m[2]) }; }
if ( targetSliceAspect && Number.isInteger(targetSliceAspect.w) && Number.isInteger(targetSliceAspect.h) && targetSliceAspect.w > 0 && targetSliceAspect.h > 0 ) { return { w: targetSliceAspect.w, h: targetSliceAspect.h }; }
// Boundary parse: numeric aspect branch. // oxlint-disable-next-line anti-slop/no-runtime-typeof if (typeof targetSliceAspect === 'number' && targetSliceAspect > 0) { // Best-effort rational approximation of a numeric w/h ratio. const g = 10000; const w = Math.round(targetSliceAspect * g); const h = g; const d = gcd(w, h);
return { w: w / d, h: h / d }; }
throw new RangeError(`invalid targetSliceAspect: ${targetSliceAspect}`);}
function gcd(a, b) { return b === 0 ? a : gcd(b, a % b);}
/** * Suggest the LARGEST centered crop for N slices at a target slice aspect. * * With slice aspect w:h, the crop is W' x H' where * sliceWidth = W'/n = w*t * sliceHeight = H' = h*t * for some integer t. Maximizing area = maximize t under * W' <= srcWidth, H' <= srcHeight. * W' = n*w*t is divisible by n by construction, so no trimming is needed. * The crop is centered with floor padding. Never upscales (min:true * semantics — the CDN will not upscale either). * * @returns {{ * cropRect: {x:number,y:number,width:number,height:number}, * sliceWidth: number, * sliceHeight: number, * warnings: string[] * } | null} null when no valid crop fits the source. */export function suggestCrop({ srcWidth, srcHeight, n, targetSliceAspect }) { assertSliceCount(n); const { w, h } = normalizeAspect(targetSliceAspect);
const tMax = Math.floor(Math.min( Math.floor(srcHeight / h), Math.floor(srcWidth / (n * w)) ));
if (tMax < 1) return null;
const cropWidth = n * w * tMax; const cropHeight = h * tMax; const x = Math.floor((srcWidth - cropWidth) / 2); const y = Math.floor((srcHeight - cropHeight) / 2);
const sliceWidth = w * tMax; const sliceHeight = h * tMax;
const warnings = [];
if (sliceWidth > MAX_SLICE_DIMENSION || sliceHeight > MAX_SLICE_DIMENSION) { warnings.push( `slice ${sliceWidth}x${sliceHeight} exceeds ${MAX_SLICE_DIMENSION}px ` + `on an axis — CDN re-encode may fail (4000x4000 max); reduce N or the slice aspect` ); }
return { cropRect: { x, y, width: cropWidth, height: cropHeight }, sliceWidth, sliceHeight, warnings, };}