(
rect: { left: number; top: number; width: number; height: number },
natural: { width: number; height: number },
objectFit: string,
objectPosition: string,
point: { x: number; y: number },
)
| 115 | */ |
| 116 | // fallow-ignore-next-line complexity |
| 117 | export function mapPointToImagePixel( |
| 118 | rect: { left: number; top: number; width: number; height: number }, |
| 119 | natural: { width: number; height: number }, |
| 120 | objectFit: string, |
| 121 | objectPosition: string, |
| 122 | point: { x: number; y: number }, |
| 123 | ): { px: number; py: number } | null { |
| 124 | // Local coords within the CSS box |
| 125 | const lx = point.x - rect.left; |
| 126 | const ly = point.y - rect.top; |
| 127 | |
| 128 | if (lx < 0 || ly < 0 || lx > rect.width || ly > rect.height) return null; |
| 129 | |
| 130 | const fit = objectFit || "fill"; |
| 131 | |
| 132 | // For fill (or unrecognized values): the natural image is stretched to the |
| 133 | // box; direct linear mapping. |
| 134 | if (fit !== "cover" && fit !== "contain" && fit !== "none") { |
| 135 | if (rect.width === 0 || rect.height === 0) return null; |
| 136 | const px = Math.floor((lx / rect.width) * natural.width); |
| 137 | const py = Math.floor((ly / rect.height) * natural.height); |
| 138 | return { px: clamp(px, 0, natural.width - 1), py: clamp(py, 0, natural.height - 1) }; |
| 139 | } |
| 140 | |
| 141 | // For none: image is drawn at its natural size; no scaling. |
| 142 | if (fit === "none") { |
| 143 | const pos = parseObjectPosition(objectPosition, rect, natural); |
| 144 | const ox = pos.x; |
| 145 | const oy = pos.y; |
| 146 | const px = Math.floor(lx - ox); |
| 147 | const py = Math.floor(ly - oy); |
| 148 | if (px < 0 || py < 0 || px >= natural.width || py >= natural.height) return null; |
| 149 | return { px, py }; |
| 150 | } |
| 151 | |
| 152 | // cover: scale uniformly so the image covers the box; may clip edges. |
| 153 | // contain: scale uniformly so the image fits within the box; may letterbox. |
| 154 | if (natural.width === 0 || natural.height === 0) return null; |
| 155 | const scaleX = rect.width / natural.width; |
| 156 | const scaleY = rect.height / natural.height; |
| 157 | const scale = fit === "cover" ? Math.max(scaleX, scaleY) : Math.min(scaleX, scaleY); |
| 158 | |
| 159 | const renderedW = natural.width * scale; |
| 160 | const renderedH = natural.height * scale; |
| 161 | |
| 162 | const pos = parseObjectPosition(objectPosition, rect, { |
| 163 | width: renderedW, |
| 164 | height: renderedH, |
| 165 | }); |
| 166 | |
| 167 | // Offset of the rendered image's top-left within the CSS box |
| 168 | const imgLeft = pos.x; |
| 169 | const imgTop = pos.y; |
| 170 | |
| 171 | // Local coords relative to the rendered image's top-left |
| 172 | const rx = lx - imgLeft; |
| 173 | const ry = ly - imgTop; |
| 174 |
no test coverage detected