alpha-mask-events
Version:
Enable click‑through on transparent regions of images (PNG, WebP, AVIF, GIF, SVG) using alpha masks.
3 lines (2 loc) • 19.8 kB
JavaScript
;Object.defineProperty(exports,"__esModule",{value:!0});const e=new Map,t={png:{hasAlpha:!0,browserSupport:"universal"},webp:{hasAlpha:!0,browserSupport:"modern"},avif:{hasAlpha:!0,browserSupport:"latest"},gif:{hasAlpha:"limited",browserSupport:"universal"},jpg:{hasAlpha:!1,browserSupport:"universal"},jpeg:{hasAlpha:!1,browserSupport:"universal"},bmp:{hasAlpha:!1,browserSupport:"limited"},tiff:{hasAlpha:"limited",browserSupport:"limited"},ico:{hasAlpha:"limited",browserSupport:"limited"},svg:{hasAlpha:!0,browserSupport:"modern"}};class s{constructor({threshold:e=.999,log:t=!1,useIntersectionObserver:s=!0,intersectionRootMargin:o="100px"}={}){this.threshold=e,this.log=t,this.useIntersectionObserver=s,this.intersectionRootMargin=o,this.registry=new Map,this._handler=this._onPointerEvent.bind(this),this._rafPending=!1,this._lastEvent=null,this._mutationObserver=null,this._resizeObservers=new WeakMap,this._intersectionObserver=null,this._intersectionElements=new Set,this._compatibilityWarningShown=!1,this._listenersAttached=!1}scan(){this.log&&this._showBrowserCompatibilityWarning(),document.querySelectorAll(".alpha-mask-events").forEach(e=>this.add(e)),this._observeMutations(),this._setupIntersectionObserver()}add(s,o={}){const r="string"==typeof s?document.querySelector(s):s;if(!r||!(r instanceof HTMLElement)||this.registry.has(r))return void(this.log&&r&&this.registry.has(r)?console.log("AME: Element already registered",r):this.log&&!r?console.warn("AME: Element not found for selector",s):this.log&&console.warn("AME: Invalid element provided",r));const n=o.threshold??this.threshold,i=getComputedStyle(r);let a;if("IMG"===r.tagName)a=r.currentSrc||r.src;else{const e=i.backgroundImage.match(/url\((['"]?)(.*?)\1\)/);if(!e||!e[2])return void(this.log&&console.warn("AME: No background-image URL found for element",r));a=e[2]}if(!a)return void(this.log&&console.warn("AME: Could not determine image source for element",r));const l=function(e){const s=e.split("?")[0].split("#")[0],o=s.split(".").pop()?.toLowerCase();if(!o)return{format:"unknown",info:null,warning:"Unable to detect image format from URL"};const r=t[o];if(!r)return{format:o,info:null,warning:`Unsupported or unknown format: ${o}. Transparency detection may not work.`};const n=[];return"modern"===r.browserSupport?n.push(`${o.toUpperCase()} requires modern browser support`):"latest"===r.browserSupport&&n.push(`${o.toUpperCase()} requires very recent browser support`),!1===r.hasAlpha?n.push(`${o.toUpperCase()} format does not support transparency`):"limited"===r.hasAlpha&&n.push(`${o.toUpperCase()} has limited transparency support`),{format:o,info:r,warning:n.length>0?n.join("; "):null}}(a);if(this.log&&l.warning&&console.warn(`AME: Format warning for ${a}: ${l.warning}`),this.log&&(console.log(`AME: Detected format: ${l.format?.toUpperCase()||"unknown"} for ${a}`),l.info)){const e=!0===l.info.hasAlpha?"✅ Full alpha":"limited"===l.info.hasAlpha?"⚠️ Limited alpha":"❌ No alpha";console.log(`AME: Format capabilities - ${e}, Browser: ${l.info.browserSupport}`)}const h=document.createElement("canvas"),c=h.getContext("2d",{willReadFrequently:!0});if(!c)return void console.error("AME: Failed to get 2D context for canvas. Alpha masking disabled for element.",r);const d=r.style.pointerEvents||i.pointerEvents;r.style.pointerEvents="none";const m={el:r,canvas:h,ctx:c,threshold:n,originalPointerEvents:d,img:null,imageLoaded:!1,currentSrc:a,isVisible:!0,_lastOpaqueState:null,_transformCache:null,_lastTransform:i.transform};if(this.registry.set(r,m),this.log&&console.log("AME: Registering element",r,"with src:",a,"threshold:",n),e.has(a)){const t=e.get(a);if(m.img=t,m.imageLoaded=!0,this._drawBackgroundToCanvas(m),"ResizeObserver"in window){const e=new ResizeObserver(()=>this._updateCanvas(m));e.observe(r),this._resizeObservers.set(r,e)}else this.log&&console.warn("AME: ResizeObserver not supported. Layout changes might affect accuracy.");this.useIntersectionObserver&&(this._intersectionObserver||this._setupIntersectionObserver(),this._intersectionObserver&&!this._intersectionElements.has(r)&&(this._intersectionObserver.observe(r),this._intersectionElements.add(r),m.isVisible=!0))}else{const t=new window.Image;t.crossOrigin="Anonymous",t.onload=()=>{if(this.log&&console.log("AME: Image loaded successfully for",r,a),m.img=t,m.imageLoaded=!0,this._drawBackgroundToCanvas(m),"ResizeObserver"in window){const e=new ResizeObserver(()=>this._updateCanvas(m));e.observe(r),this._resizeObservers.set(r,e)}else this.log&&console.warn("AME: ResizeObserver not supported. Layout changes might affect accuracy.");this.useIntersectionObserver&&(this._intersectionObserver||this._setupIntersectionObserver(),this._intersectionObserver&&!this._intersectionElements.has(r)&&(this._intersectionObserver.observe(r),this._intersectionElements.add(r),m.isVisible=!0)),e.set(a,t)},t.onerror=e=>{const t={element:r,src:a,error:e.type||"unknown",format:l.format,timestamp:(new Date).toISOString()};console.error("AME: Image loading failed - implementing recovery strategies",t),console.group("AME: Image Loading Recovery Guide"),console.info("🔧 Troubleshooting Steps:"),console.info("1. Check if the image URL is accessible:",a),console.info("2. Verify CORS headers if cross-origin:",a.startsWith("http")&&!a.startsWith(window.location.origin)?"⚠️ Cross-origin detected":"✓ Same-origin"),console.info("3. Image format compatibility:"),l.format&&l.info?(console.info(` • Format: ${l.format.toUpperCase()}`),console.info(` • Browser support: ${l.info.browserSupport}`),console.info(" • Alpha support: "+(!0===l.info.hasAlpha?"Full":"limited"===l.info.hasAlpha?"Limited":"None")),"webp"===l.format?console.info(" 💡 WebP: Ensure browser supports WebP (Chrome 23+, Firefox 65+, Safari 14+)"):"avif"===l.format?console.info(" 💡 AVIF: Requires very recent browser (Chrome 85+, Firefox 93+, Safari 16.4+)"):l.info.hasAlpha||console.info(" ⚠️ This format doesn't support transparency")):(console.info(" ⚠️ Unknown or unsupported format detected"),console.info(" 📋 Fully supported: PNG, WebP, AVIF, GIF"),console.info(" 📋 Partially supported: SVG, JPEG (no transparency), BMP, TIFF")),console.info("4. Check network connectivity and server availability"),console.info("💡 Alternative Solutions:"),console.info("• Use the CLI tool to pre-generate masks: npx ame-generate-masks"),console.info("• Implement server-side image processing"),console.info("• Use PNG format for maximum compatibility"),console.info("• Use same-origin images when possible"),console.groupEnd(),this.remove(r)},t.src=a,e.set(a,t)}}remove(e){const t="string"==typeof e?document.querySelector(e):e;if(!t||!this.registry.has(t))return void(this.log&&!t?console.warn("AME: Element not found for removal",e):this.log&&console.log("AME: Element not registered, cannot remove",t));const s=this.registry.get(t);t.style.pointerEvents=s.originalPointerEvents;const o=this._resizeObservers.get(t);o&&(o.disconnect(),this._resizeObservers.delete(t)),this._intersectionObserver&&this._intersectionElements.has(t)&&(this._intersectionObserver.unobserve(t),this._intersectionElements.delete(t)),this.registry.delete(t),this.log&&console.log("AME: Unregistered element",t)}setThreshold(e,t){const s=Math.max(0,Math.min(1,e));if(t){const e="string"==typeof t?document.querySelector(t):t;e&&this.registry.has(e)?(this.registry.get(e).threshold=s,this.log&&console.log("AME: Updated threshold for specific element",e,s)):this.log&&console.warn("AME: Element not found or not registered for setThreshold",t)}else this.threshold=s,this.registry.forEach(e=>{e.threshold=s}),this.log&&console.log("AME: Updated global threshold",s)}attachListeners(){this._listenersAttached||(window.PointerEvent?(document.addEventListener("pointermove",this._handler,{passive:!0}),document.addEventListener("pointerdown",this._handler,{passive:!0}),document.addEventListener("pointerover",this._handler,{passive:!0})):(document.addEventListener("mousemove",this._handler,{passive:!0}),document.addEventListener("touchmove",this._handler,{passive:!0}),document.addEventListener("mousedown",this._handler,{passive:!0}),document.addEventListener("touchstart",this._handler,{passive:!0}),document.addEventListener("mouseover",this._handler,{passive:!0})),this._listenersAttached=!0,this.log&&console.log("AME: Attached global listeners"))}detachListeners(){this._listenersAttached&&(window.PointerEvent?(document.removeEventListener("pointermove",this._handler),document.removeEventListener("pointerdown",this._handler),document.removeEventListener("pointerover",this._handler)):(document.removeEventListener("mousemove",this._handler),document.removeEventListener("touchmove",this._handler),document.removeEventListener("mousedown",this._handler),document.removeEventListener("touchstart",this._handler),document.removeEventListener("mouseover",this._handler)),this._mutationObserver&&(this._mutationObserver.disconnect(),this._mutationObserver=null),this._intersectionObserver&&(this._intersectionObserver.disconnect(),this._intersectionObserver=null,this._intersectionElements.clear()),this.registry.forEach((e,t)=>{this.remove(t)}),this.registry.clear(),this._resizeObservers=new WeakMap,this._listenersAttached=!1,this.log&&console.log("AME: Detached global listeners and cleaned up observers/registry"))}_onPointerEvent(e){this._lastEvent=e,this._rafPending||(this._rafPending=!0,requestAnimationFrame(()=>{this._rafPending=!1,this._lastEvent&&this._hitTest(this._lastEvent)}))}_hitTest(e){let t,s;if(e.touches&&e.touches.length>0)t=e.touches[0].clientX,s=e.touches[0].clientY;else{if(void 0===e.clientX||void 0===e.clientY)return;t=e.clientX,s=e.clientY}this.registry.forEach(e=>{const{el:o,canvas:r,ctx:n,threshold:i,originalPointerEvents:a,imageLoaded:l,isVisible:h}=e;if(this.useIntersectionObserver&&!1===h)return;if(!l||!n)return void("none"!==o.style.pointerEvents&&(o.style.pointerEvents="none"));const c=o.getBoundingClientRect();if(t>=c.left&&t<=c.right&&s>=c.top&&s<=c.bottom){const{canvasX:a,canvasY:l}=this._mapPointerToCanvasCoordinates(t,s,o,c,r,e);let h=0;if(a>=0&&a<r.width&&l>=0&&l<r.height)try{h=n.getImageData(a,l,1,1).data[3]/255}catch(r){this.log&&!e._loggedImageDataError&&(console.warn("AME: getImageData failed - implementing fallback strategy.",{element:o,error:r.message,src:e.currentSrc,fallback:"Using bounding box approximation"}),console.info('AME: CORS Recovery Tips:\n• Ensure images have crossOrigin="anonymous" attribute\n• Verify server sends Access-Control-Allow-Origin headers\n• Consider using the CLI tool for pre-generated masks\n• Use same-origin images when possible'),e._loggedImageDataError=!0),h=this._approximateAlphaFromBounds(o,t,s,c)}else this.log&&console.log("AME: Calculated coords outside canvas bounds",{canvasX:a,canvasY:l,canvasW:r.width,canvasH:r.height}),h=0;const d=h>i?"auto":"none",m=h>i;e._lastOpaqueState!==m&&(m?this._dispatchAlphaMaskEvent(o,"alpha-mask-over",{alpha:h,coordinates:{x:a,y:l},threshold:i,element:o}):this._dispatchAlphaMaskEvent(o,"alpha-mask-out",{alpha:h,coordinates:{x:a,y:l},threshold:i,element:o}),e._lastOpaqueState=m),o.style.pointerEvents!==d&&(o.style.pointerEvents=d,this.log>1&&console.log(`AME: Set pointerEvents=${d} (alpha=${h.toFixed(3)}) on`,o))}else o.style.pointerEvents!==a&&(o.style.pointerEvents=a,this.log>1&&console.log(`AME: Pointer left bounds, restored pointerEvents=${a} on`,o)),!0===e._lastOpaqueState&&(this._dispatchAlphaMaskEvent(o,"alpha-mask-out",{alpha:0,coordinates:{x:-1,y:-1},threshold:i,element:o}),e._lastOpaqueState=null)})}_dispatchAlphaMaskEvent(e,t,s){try{const o=new CustomEvent(t,{detail:s,bubbles:!1,cancelable:!1});e.dispatchEvent(o),this.log>1&&console.log(`AME: Dispatched ${t} on`,e,"with detail:",s)}catch(e){this.log&&console.warn(`AME: Failed to dispatch ${t} event:`,e)}}_observeMutations(){"MutationObserver"in window&&!this._mutationObserver&&(this._mutationObserver=new MutationObserver(e=>{e.forEach(e=>{if(e.addedNodes.forEach(e=>{e.nodeType===Node.ELEMENT_NODE&&(e.matches&&e.matches(".alpha-mask-events")&&this.add(e),e.querySelectorAll&&e.querySelectorAll(".alpha-mask-events").forEach(e=>this.add(e)))}),e.removedNodes.forEach(e=>{e.nodeType===Node.ELEMENT_NODE&&(this.registry.has(e)&&this.remove(e),e.querySelectorAll&&e.querySelectorAll("*").forEach(e=>{this.registry.has(e)&&this.remove(e)}))}),"attributes"===e.type){const t=e.target;if(t.nodeType===Node.ELEMENT_NODE){const s=this.registry.has(t),o=t.classList.contains("alpha-mask-events"),r="IMG"===t.tagName?t.currentSrc||t.src:getComputedStyle(t).backgroundImage.match(/url\((['"]?)(.*?)\1\)/)?.[2],n=this.registry.get(t);if(o&&!s)this.add(t);else if(!o&&s)this.remove(t);else if(s&&r&&n&&r!==n.currentSrc)this.log&&console.log("AME: Image source changed, re-processing element",t),this.remove(t),this.add(t);else if(s&&n&&"style"===e.attributeName){const e=getComputedStyle(t).transform;n._transformCache&&n._lastTransform!==e&&(this.log>1&&console.log("AME: Transform changed, invalidating cache",t),n._transformCache=null,n._lastTransform=e)}}}})}),this._mutationObserver.observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:["class","src","style"]}),this.log&&console.log("AME: MutationObserver attached"))}_setupIntersectionObserver(){this.useIntersectionObserver&&"IntersectionObserver"in window&&!this._intersectionObserver&&(this._intersectionObserver=new IntersectionObserver(e=>{e.forEach(e=>{const t=e.target,s=this.registry.get(t);if(!s)return;const o=e.isIntersecting;s.isVisible=o,this.log>1&&console.log(`AME: Element ${o?"entered":"left"} viewport`,t),o||(t.style.pointerEvents=s.originalPointerEvents)})},{rootMargin:this.intersectionRootMargin,threshold:[0,.1]}),this.log&&console.log("AME: IntersectionObserver setup complete"))}_updateCanvas(e){if(!(e&&e.imageLoaded&&e.el&&e.canvas))return void(this.log&&console.warn("AME: _updateCanvas called with invalid or incomplete entry",e));const{el:t,canvas:s}=e,o=t.getBoundingClientRect(),r=Math.round(o.width),n=Math.round(o.height);if(s.width!==r||s.height!==n){if(r<=0||n<=0)return this.log&&console.log("AME: Element resized to zero or negative dimensions, skipping canvas update",t),s.width=1,s.height=1,void e.ctx.clearRect(0,0,1,1);this.log&&console.log("AME: Resizing canvas for element",t,`from ${s.width}x${s.height} to ${r}x${n}`),s.width=r,s.height=n,this._drawBackgroundToCanvas(e)}}_drawBackgroundToCanvas(e){const{el:t,canvas:s,ctx:o,img:r}=e;if(!r||!o||!s||s.width<=0||s.height<=0)return void(this.log&&console.warn("AME: Cannot draw background, missing image, context, or canvas dimensions are invalid",e));const n=s.width,i=s.height,a=r.naturalWidth,l=r.naturalHeight;if(a<=0||l<=0)return void(this.log&&console.warn("AME: Image has zero dimensions, cannot draw.",r));const h=getComputedStyle(t),c=h.backgroundSize,d=h.backgroundPosition;let m,u;o.clearRect(0,0,n,i);const p=a/l,g=n/i;if("cover"===c)p>g?(u=i,m=u*p):(m=n,u=m/p);else if("contain"===c)p>g?(m=n,u=m/p):(u=i,m=u*p);else if("auto"===c||"auto auto"===c)m=a,u=l;else{const e=c.split(" "),t=this._parseCssDimension(e[0],n,a),s=this._parseCssDimension(e[1]||e[0],i,l);"auto"===e[0]&&e[1]&&"auto"!==e[1]?(u=s,m=u*p):"auto"===e[1]&&e[0]&&"auto"!==e[0]?(m=t,u=m/p):(m=t,u=s)}let v,f;m=Math.max(1,Math.round(m)),u=Math.max(1,Math.round(u));const b=d.split(" "),w=this._parseCssPosition(b[0],n,m),E=this._parseCssPosition(b[1]||b[0],i,u);v=Math.round(w),f=Math.round(E);try{o.drawImage(r,0,0,a,l,v,f,m,u),this.log>1&&console.log(`AME: Drew image to canvas for ${t.id||t.tagName}`,{dx:v,dy:f,dw:m,dh:u,canvasW:n,canvasH:i}),e._loggedImageDataError=!1}catch(e){console.error("AME: Error during ctx.drawImage:",e,{el:t,img:r.src,dx:v,dy:f,dw:m,dh:u})}}_parseCssDimension(e,t,s){return e&&"auto"!==e?e.endsWith("%")?parseFloat(e)/100*t:(e.endsWith("px"),parseFloat(e)):s}_parseCssPosition(e,t,s){if(!e)return 0;switch(e){case"left":case"top":return 0;case"center":return(t-s)/2;case"right":case"bottom":return t-s}return e.endsWith("%")?(t-s)*(parseFloat(e)/100):(e.endsWith("px"),parseFloat(e))}_approximateAlphaFromBounds(e,t,s,o){const r=(t-o.left)/o.width,n=(s-o.top)/o.height,i=Math.sqrt(Math.pow(r-.5,2)+Math.pow(n-.5,2)),a=.35;if(i<=a)return 1;{const e=(i-a)/.357;return Math.max(0,1-1.5*e)}}_mapPointerToCanvasCoordinates(e,t,s,o,r,n){const i=e-o.left,a=t-o.top,l=getComputedStyle(s).transform;if(!l||"none"===l)return{canvasX:Math.floor(i*(r.width/o.width)),canvasY:Math.floor(a*(r.height/o.height))};const h=`${l}_${o.width}_${o.height}`;if(n._transformCache?.key===h){const{inverseMatrix:e}=n._transformCache,t=this._applyInverseTransform(i-o.width/2,a-o.height/2,e);return{canvasX:Math.floor((t.x+o.width/2)*(r.width/o.width)),canvasY:Math.floor((t.y+o.height/2)*(r.height/o.height))}}try{const e=this._parseTransformMatrix(l),t=this._invertMatrix(e);n._transformCache={key:h,matrix:e,inverseMatrix:t};const s=this._applyInverseTransform(i-o.width/2,a-o.height/2,t);return{canvasX:Math.floor((s.x+o.width/2)*(r.width/o.width)),canvasY:Math.floor((s.y+o.height/2)*(r.height/o.height))}}catch(e){return this.log&&console.warn("AME: Transform parsing failed, using simple coordinate mapping",{element:s,transform:l,error:e.message}),{canvasX:Math.floor(i*(r.width/o.width)),canvasY:Math.floor(a*(r.height/o.height))}}}_parseTransformMatrix(e){if(e.includes("matrix3d")){const t=e.match(/matrix3d\(([-\d.\s,]+)\)/);if(t){const e=t[1].split(",").map(e=>parseFloat(e.trim()));return[e[0],e[1],e[4],e[5],e[12],e[13]]}}if(e.includes("matrix")){const t=e.match(/matrix\(([-\d.\s,]+)\)/);if(t){return t[1].split(",").map(e=>parseFloat(e.trim()))}}if(e.includes("rotate")||e.includes("scale")||e.includes("skew")||e.includes("translate")){const t=document.createElement("div");t.style.transform=e,t.style.position="absolute",t.style.visibility="hidden",document.body.appendChild(t);try{const e=getComputedStyle(t).transform;if(document.body.removeChild(t),e&&"none"!==e)return this._parseTransformMatrix(e)}catch(e){throw document.body.removeChild(t),e}}return[1,0,0,1,0,0]}_invertMatrix(e){const[t,s,o,r,n,i]=e,a=t*r-s*o;if(Math.abs(a)<1e-10)return this.log&&console.warn("AME: Transform matrix is singular (non-invertible), using identity"),[1,0,0,1,0,0];const l=1/a;return[r*l,-s*l,-o*l,t*l,(o*i-r*n)*l,(s*n-t*i)*l]}_applyInverseTransform(e,t,s){const[o,r,n,i,a,l]=s;return{x:o*e+n*t+a,y:r*e+i*t+l}}_showBrowserCompatibilityWarning(){if(this._compatibilityWarningShown)return;this._compatibilityWarningShown=!0;const e=[],t=[];"ResizeObserver"in window||(e.push("ResizeObserver"),t.push("https://github.com/que-etc/resize-observer-polyfill")),"IntersectionObserver"in window||(e.push("IntersectionObserver"),t.push("https://github.com/w3c/IntersectionObserver/tree/main/polyfill")),"PointerEvent"in window||(e.push("PointerEvent"),console.info("AME: Falling back to mouse/touch events (PointerEvent not supported)")),e.length>0&&(console.group("AME: Browser Compatibility Notice"),console.warn(`Missing features: ${e.join(", ")}`),console.info("🌐 Browser Support:"),console.info("• Chrome 50+ ✓"),console.info("• Firefox 50+ ✓"),console.info("• Safari 11+ ✓"),console.info("• Edge 18+ ✓"),console.info("📦 Recommended Polyfills:"),t.forEach(e=>console.info(`• ${e}`)),console.info("💡 For optimal performance, consider upgrading your browser"),console.groupEnd())}}let o=null;function r(e={}){return o||(o=new s(e),o.scan(),o.attachListeners()),o}function n(e,t={}){o||r(),o.add(e,t)}function i(e){o&&o.remove(e)}function a(e){o&&o.setThreshold(e)}var l={init:r,register:n,unregister:i,setThreshold:a};exports.default=l,exports.init=r,exports.register=n,exports.setThreshold=a,exports.unregister=i;
//# sourceMappingURL=index.cjs.min.js.map