* Parse a CSS object-position value into x/y offsets (top-left of the * rendered content relative to the CSS box top-left). * * Supports the common subset: keyword pairs, percentage pairs, pixel pairs, * and single-value shorthand. Mixed units (e.g. "50% 10px") are supported. * * Pure — no DOM
(
objectPosition: string,
box: { width: number; height: number },
content: { width: number; height: number },
)
| 193 | * Pure — no DOM access. |
| 194 | */ |
| 195 | function parseObjectPosition( |
| 196 | objectPosition: string, |
| 197 | box: { width: number; height: number }, |
| 198 | content: { width: number; height: number }, |
| 199 | ): { x: number; y: number } { |
| 200 | const raw = (objectPosition || "50% 50%").trim(); |
| 201 | const parts = raw.split(/\s+/); |
| 202 | |
| 203 | // Resolve a single token into a pixel offset along the given axis. |
| 204 | // `available` is the "slack" (box dimension - content dimension). |
| 205 | // fallow-ignore-next-line complexity |
| 206 | function resolveToken(token: string, available: number): number { |
| 207 | if (token === "left" || token === "top") return 0; |
| 208 | if (token === "right" || token === "bottom") return available; |
| 209 | if (token === "center") return available / 2; |
| 210 | if (token.endsWith("%")) { |
| 211 | const pct = parseFloat(token) / 100; |
| 212 | return isNaN(pct) ? available / 2 : pct * available; |
| 213 | } |
| 214 | if (token.endsWith("px")) { |
| 215 | const px = parseFloat(token); |
| 216 | return isNaN(px) ? available / 2 : px; |
| 217 | } |
| 218 | // Bare number — treat as px |
| 219 | const n = parseFloat(token); |
| 220 | return isNaN(n) ? available / 2 : n; |
| 221 | } |
| 222 | |
| 223 | const availX = box.width - content.width; |
| 224 | const availY = box.height - content.height; |
| 225 | |
| 226 | const isVert = (t: string) => t === "top" || t === "bottom"; |
| 227 | const isHoriz = (t: string) => t === "left" || t === "right"; |
| 228 | |
| 229 | if (parts.length === 1) { |
| 230 | const tokenX = parts[0] ?? "50%"; |
| 231 | // Single value: if it's a vertical keyword the x defaults to center |
| 232 | if (isVert(tokenX)) { |
| 233 | return { x: availX / 2, y: resolveToken(tokenX, availY) }; |
| 234 | } |
| 235 | return { x: resolveToken(tokenX, availX), y: availY / 2 }; |
| 236 | } |
| 237 | |
| 238 | // Keyword pairs may be given vertical-first ("bottom left"); normalize so the |
| 239 | // first token addresses the x-axis and the second the y-axis. |
| 240 | let xToken = parts[0] ?? "50%"; |
| 241 | let yToken = parts[1] ?? "50%"; |
| 242 | if (isVert(xToken) || isHoriz(yToken)) { |
| 243 | [xToken, yToken] = [yToken, xToken]; |
| 244 | } |
| 245 | return { |
| 246 | x: resolveToken(xToken, availX), |
| 247 | y: resolveToken(yToken, availY), |
| 248 | }; |
| 249 | } |
| 250 | |
| 251 | function clamp(v: number, min: number, max: number): number { |
| 252 | return v < min ? min : v > max ? max : v; |
no test coverage detected