Last active
April 17, 2025 13:44
-
-
Save themorgantown/2662fdd67e3d3fdb15ee6c05c6e2e315 to your computer and use it in GitHub Desktop.
detect clicks on a non-transparent image (webp, png, avif, gif) in Hype. Multiple layered images supported. https://forums.tumult.com/t/detect-click-on-non-transparent-area-of-png/24769/
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* Clickable Transparency Library | |
* -------------------------------- | |
* This script enables non-transparent parts of an image to be clickable. | |
* It works for both <img> elements and any element with a background image. | |
* For any element with the "clickable-transparency" class: | |
* - It changes the mouse cursor to 'pointer' over non-transparent areas and 'default' over transparent areas. | |
* - It prevents click propagation if a transparent area is clicked. | |
* | |
* Usage: | |
* 1. Add the "clickable-transparency" class to your elements. | |
* For <img> elements, use the src attribute. | |
* For other elements, set a background-image in CSS. | |
* | |
* Example for <img>: | |
* <img src="your-image.png" alt="Your Image" class="clickable-transparency"> | |
* | |
* Example for a background element: | |
* <div class="HYPE_element clickable-transparency" | |
* style="background-image: url('your-image.png'); background-size: 100% 100%; width: 253px; height: 226px;"> | |
* </div> | |
* Note: this is meant for Tumult Hype. | |
*/ | |
// Threshold for “transparent enough” (α ≤ 0.1) | |
const TRANSPARENCY_THRESHOLD = 0.999; | |
(function() { | |
const registry = []; | |
// Note: The CanvasSettings object also has a willReadFrequently boolean. | |
// When a CanvasSettings object's willReadFrequently is true, the user agent | |
// may optimize the canvas for readback operations. | |
// | |
// We pass { willReadFrequently: true } to getContext so that reading pixels | |
// back (via getImageData) is as efficient as possible. | |
// 1) Gather every .clickable-transparency and build its off‑screen canvas: | |
document.querySelectorAll('.clickable-transparency').forEach(el => { | |
let src, natW, natH; | |
if (el.tagName === 'IMG') { | |
src = el.src; | |
natW = el.naturalWidth; | |
natH = el.naturalHeight; | |
} else { | |
const bg = getComputedStyle(el).backgroundImage.match(/url\(([^)]+)\)/); | |
if (!bg) return; | |
src = bg[1].replace(/['"]/g,''); | |
} | |
const img = new Image(); | |
img.crossOrigin = 'anonymous'; | |
img.src = src; | |
img.onload = () => { | |
const w = natW || img.naturalWidth; | |
const h = natH || img.naturalHeight; | |
const canvas = document.createElement('canvas'); | |
canvas.width = w; | |
canvas.height = h; | |
// create context optimized for frequent readbacks: | |
const ctx = canvas.getContext('2d', { willReadFrequently: true }); | |
ctx.drawImage(img, 0, 0, w, h); | |
registry.push({ el, canvas, ctx }); | |
}; | |
}); | |
// 2) On every pointer/touch event, sample the pixel and toggle pointer-events: | |
function hitTest(e) { | |
const x = e.clientX, y = e.clientY; | |
registry.forEach(({ el, canvas, ctx }) => { | |
const r = el.getBoundingClientRect(); | |
if (x < r.left || x > r.right || y < r.top || y > r.bottom) { | |
// outside → restore normal behavior | |
el.style.pointerEvents = 'auto'; | |
return; | |
} | |
// inside → map to canvas pixels | |
const px = Math.floor((x - r.left) * canvas.width / r.width); | |
const py = Math.floor((y - r.top ) * canvas.height / r.height); | |
const alpha = ctx.getImageData(px, py, 1, 1).data[3] / 255; | |
if (alpha <= TRANSPARENCY_THRESHOLD) { | |
el.style.pointerEvents = 'none'; // click falls through | |
} else { | |
el.style.pointerEvents = 'auto'; // click catches here | |
} | |
}); | |
} | |
// 3) Use Pointer Events where supported (covers mouse/touch), else fall back: | |
if (window.PointerEvent) { | |
document.addEventListener('pointermove', hitTest, { passive: true }); | |
document.addEventListener('pointerdown', hitTest, { passive: true }); | |
} else { | |
document.addEventListener('touchmove', hitTest, { passive: true }); | |
document.addEventListener('touchstart', hitTest, { passive: true }); | |
} | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment