1/** 2 * Smart Focal Point 3 * 4 * For every <img data-smart-focal> on the page: 5 * - If the attribute value is "X,Y" (explicit percentages), apply object-position 6 * immediately. This is the editor-override path. 7 * - If the value is "auto" (or empty), load smartcrop.js once, analyze the image, 8 * and set object-position to the centroid of the most-salient crop region. 9 * 10 * Paired with CSS `object-fit: cover` on the same image or its wrapper, 11 * this keeps the important region visible at any crop aspect. 12 */ 13 14(function () { 15 'use strict'; 16 17 const SMARTCROP_SRC = 'https://unpkg.com/[email protected]/smartcrop.js'; 18 19 function loadSmartCrop() { 20 return new Promise(function (resolve, reject) { 21 if (window.SmartCrop) { resolve(window.SmartCrop); return; } 22 let s = document.querySelector('script[data-smartcrop]'); 23 if (s) { 24 s.addEventListener('load', function () { resolve(window.SmartCrop); }); 25 s.addEventListener('error', reject); 26 return; 27 } 28 s = document.createElement('script'); 29 s.src = SMARTCROP_SRC; 30 s.setAttribute('data-smartcrop', ''); 31 s.addEventListener('load', function () { resolve(window.SmartCrop); }); 32 s.addEventListener('error', reject); 33 document.head.appendChild(s); 34 }); 35 } 36 37 function applyFocal(img, xPct, yPct) { 38 const clamp = function (v) { return Math.max(0, Math.min(100, v)); }; 39 img.style.objectPosition = clamp(xPct) + '% ' + clamp(yPct) + '%'; 40 img.setAttribute('data-smart-focal-applied', '1'); 41 } 42 43 function whenLoaded(img) { 44 return new Promise(function (resolve) { 45 if (img.complete && img.naturalWidth > 0) { resolve(); return; } 46 img.addEventListener('load', function () { resolve(); }, { once: true }); 47 img.addEventListener('error', function () { resolve(); }, { once: true }); 48 }); 49 } 50 51 function targetAspect(img) { 52 // Use the container/displayed aspect so SmartCrop searches for the most 53 // interesting subregion that matches what object-fit: cover will actually show. 54 const rW = img.clientWidth || img.offsetWidth; 55 const rH = img.clientHeight || img.offsetHeight; 56 if (rW > 0 && rH > 0) return { w: rW, h: rH }; 57 // Container not laid out â fall back to the parent's rect. 58 const p = img.parentElement; 59 if (p) { 60 const r = p.getBoundingClientRect(); 61 if (r.width > 0 && r.height > 0) return { w: r.width, h: r.height }; 62 } 63 // Last resort â bias toward a landscape 16:9 search so SmartCrop returns 64 // a meaningful subregion instead of the whole square image. 65 return { w: 160, h: 90 }; 66 } 67 68 /** 69 * Face detection â uses the browser's built-in FaceDetector API where 70 * available (Chrome behind a flag), otherwise lazy-loads @vladmandic/face-api 71 * (a face-api.js fork) with the tinyFaceDetector model. Both return faces 72 * as bounding boxes; we take the area-weighted centroid so a prominent face 73 * dominates over a tiny face in the background. 74 */ 75 const FACE_API_URL = 'https://cdn.jsdelivr.net/npm/@vladmandic/[email protected]/dist/face-api.min.js'; 76 const FACE_MODEL_URI = 'https://cdn.jsdelivr.net/npm/@vladmandic/[email protected]/model'; 77 let _faceApiPromise = null; 78 79 function loadFaceApi() { 80 if (_faceApiPromise) return _faceApiPromise; 81 _faceApiPromise = new Promise(function (resolve, reject) { 82 if (window.faceapi && window.faceapi.nets && window.faceapi.nets.tinyFaceDetector) { 83 if (window.faceapi.nets.tinyFaceDetector.params) return resolve(window.faceapi); 84 } 85 const finishLoad = function () { 86 window.faceapi.nets.tinyFaceDetector.loadFromUri(FACE_MODEL_URI) 87 .then(function () { resolve(window.faceapi); }) 88 .catch(reject); 89 }; 90 if (window.faceapi) { finishLoad(); return; } 91 const s = document.createElement('script'); 92 s.src = FACE_API_URL; 93 s.onload = finishLoad; 94 s.onerror = reject; 95 document.head.appendChild(s); 96 }); 97 return _faceApiPromise; 98 } 99 100 /** Returns area-weighted centroid in image-space %, or null. */ 101 function centroidFromBoxes(boxes, naturalW, naturalH) { 102 let totalArea = 0, cx = 0, cy = 0; 103 for (const b of boxes) { 104 if (!b || !b.width || !b.height) continue; 105 const a = b.width * b.height; 106 totalArea += a; 107 cx += (b.x + b.width / 2) * a;
108 cy += (b.y + b.height / 2) * a; 109 } 110 if (!totalArea) return null; 111 return { 112 xPct: (cx / totalArea / naturalW) * 100, 113 yPct: (cy / totalArea / naturalH) * 100, 114 }; 115 } 116 117 async function runFaceDetector(img) { 118 if (!img.naturalWidth || !img.naturalHeight) return null; 119 120 // Path 1: browser-native FaceDetector (Chrome flag, Edge). 121 if (typeof window.FaceDetector !== 'undefined') { 122 try { 123 const fd = new window.FaceDetector({ fastMode: true, maxDetectedFaces: 5 }); 124 const faces = await fd.detect(img); 125 if (faces && faces.length) { 126 const centroid = centroidFromBoxes(faces.map(f => f.boundingBox), img.naturalWidth, img.naturalHeight); 127 if (centroid) return centroid; 128 } 129 } catch (_e) { /* fall through */ } 130 } 131 132 // Path 2: face-api.js with tinyFaceDetector â works in all modern browsers. 133 try { 134 const faceapi = await loadFaceApi(); 135 const opts = new faceapi.TinyFaceDetectorOptions({ inputSize: 320, scoreThreshold: 0.45 }); 136 const detections = await faceapi.detectAllFaces(img, opts); 137 if (!detections || !detections.length) return null; 138 const boxes = detections.map(d => ({ 139 x: d.box.x, y: d.box.y, width: d.box.width, height: d.box.height, 140 })); 141 return centroidFromBoxes(boxes, img.naturalWidth, img.naturalHeight); 142 } catch (e) { 143 console.warn('[smart-focal] face-api skipped', img.src, e && e.message); 144 return null; 145 } 146 } 147 148 async function runSmartCrop(img) { 149 if (!img.naturalWidth || !img.naturalHeight) return; 150 try { 151 const SmartCrop = await loadSmartCrop(); 152 const t = targetAspect(img); 153 // Subject-at-top boost: weight the upper third of the image more so the 154 // crop centers on heads / busts / building peaks for typical compositions. 155 // SmartCrop's saliency analysis still finds the topCrop; we just nudge it. 156 // Weight is moderate (0.6) â strong enough to lift a faces-at-top shot, 157 // weak enough that a clearly-bottom-focused composition still wins. 158 const boosts = [{ 159 x: 0, 160 y: 0, 161 width: img.naturalWidth, 162 height: Math.floor(img.naturalHeight * 0.45), 163 weight: 0.6, 164 }]; 165 const result = await SmartCrop.crop(img, { width: t.w, height: t.h, boost: boosts }); 166 if (!result || !result.topCrop) return; 167 const c = result.topCrop; 168 const coversAll = ( 169 c.x === 0 && c.y === 0 && 170 c.width === img.naturalWidth && c.height === img.naturalHeight 171 ); 172 if (coversAll) return; 173 let xPct = ((c.x + c.width / 2) / img.naturalWidth) * 100; 174 let yPct = ((c.y + c.height / 2) / img.naturalHeight) * 100; 175 // Safety clamp: for landscape images, if the chosen Y is in the extreme 176 // bottom (rare for "subject of interest"), pull it up to the upper third. 177 // This catches cases like bronze sculptures with pedestal-heavy saliency. 178 const aspect = img.naturalWidth / img.naturalHeight; 179 if (aspect >= 1.0 && yPct > 70) { 180 yPct = 35; 181 } 182 applyFocal(img, xPct, yPct); 183 } catch (e) { 184 // Silent â CORS / decode failures just fall back to default center crop. 185 console.warn('[smart-focal] skipped', img.src, e && e.message); 186 } 187 } 188 189 /** Auto-detect: faces first (where supported), then smartcrop saliency fallback. */ 190 async function runAutoFocal(img) { 191 const face = await runFaceDetector(img); 192 if (face) { 193 applyFocal(img, face.xPct, face.yPct); 194 return; 195 } 196 await runSmartCrop(img); 197 } 198 199 function parseExplicit(value) { 200 if (!value || value === 'auto' || value === '1') return null; 201 const m = value.match(/^(-?\d+(?:\.\d+)?)[,\s]+(-?\d+(?:\.\d+)?)$/); 202 if (!m) return null; 203 return { x: parseFloat(m[1]), y: parseFloat(m[2]) }; 204 } 205 206 function process(img) { 207 if (img.getAttribute('data-smart-focal-applied') === '1') return; 208 const explicit = parseExplicit(img.dataset.smartFocal); 209 if (explicit) { 210 applyFocal(img, explicit.x, explicit.y); 211 return; 212 } 213 whenLoaded(img).then(function () { runAutoFocal(img); }); 214 } 215 216 let intersectionObserver = null; 217 218 function watch(img) { 219 if (!('IntersectionObserver' in window)) { process(img); return; } 220 if (!intersectionObserver) { 221 intersectionObserver = new IntersectionObserver(function (entries) {
222 entries.forEach(function (entry) { 223 if (entry.isIntersecting) { 224 process(entry.target); 225 intersectionObserver.unobserve(entry.target); 226 } 227 }); 228 }, { rootMargin: '200px' }); 229 } 230 intersectionObserver.observe(img); 231 } 232 233 function scanRoot(root) { 234 const imgs = root.querySelectorAll('img[data-smart-focal]'); 235 Array.prototype.forEach.call(imgs, watch); 236 } 237 238 function observeAdditions() { 239 if (!('MutationObserver' in window)) return; 240 const mo = new MutationObserver(function (records) { 241 records.forEach(function (r) { 242 r.addedNodes.forEach(function (node) { 243 if (node.nodeType !== 1) return; 244 if (node.tagName === 'IMG' && node.hasAttribute('data-smart-focal')) { 245 watch(node); 246 } else if (node.querySelectorAll) { 247 scanRoot(node); 248 } 249 }); 250 }); 251 }); 252 mo.observe(document.body, { childList: true, subtree: true }); 253 } 254 255 function init() { 256 scanRoot(document); 257 observeAdditions(); 258 } 259 260 if (document.readyState === 'loading') { 261 document.addEventListener('DOMContentLoaded', init); 262 } else { 263 init(); 264 } 265})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.