Skip to content

Instantly share code, notes, and snippets.

@fitsum
Created November 3, 2025 19:28
Show Gist options
  • Select an option

  • Save fitsum/c9282eafe1f27124cca5211ab1114706 to your computer and use it in GitHub Desktop.

Select an option

Save fitsum/c9282eafe1f27124cca5211ab1114706 to your computer and use it in GitHub Desktop.
/*
Web component code gen from Gemini Pro
Original code: https://codepen.io/cassidoo/pen/RNrqOOO
Related social media post: https://bsky.app/profile/did:plc:bhdap3w2bseikypfnjmaskzf/post/3m4pb4lz6ss24
*/
class RevealImage extends HTMLElement {
// --- Define default values ---
radius = 100;
bigRadius = 200;
currentRadius = 100;
targetRadius = 100;
animating = false;
mouseX = 0;
mouseY = 0;
// --- References to internal elements ---
clearImg = null;
container = null;
constructor() {
super();
// Create a Shadow DOM to encapsulate styles and markup
this.attachShadow({ mode: 'open' });
}
// --- Main lifecycle method: Runs when the element is added to the page ---
connectedCallback() {
// --- 1. Get component attributes from the HTML ---
const imgSrc = this.getAttribute('src');
// Stop if the most important attribute is missing
if (!imgSrc) {
console.error('Error: <reveal-image> component requires an "src" attribute.');
return;
}
// Allow overriding the radii via HTML attributes
this.radius = parseInt(this.getAttribute('radius'), 10) || this.radius;
this.bigRadius = parseInt(this.getAttribute('big-radius'), 10) || this.bigRadius;
this.currentRadius = this.radius;
this.targetRadius = this.radius;
// --- 2. Define the component's internal HTML and CSS ---
this.shadowRoot.innerHTML = `
<style>
.image-container {
position: relative;
/* Use inline-block so container fits the image */
display: inline-block;
/* Hide the mouse cursor, as the circle acts as the cursor */
cursor: none;
/* Fix for extra space below inline images */
line-height: 0;
}
/* This blurred image defines the component's size */
#blurred {
filter: blur(15px);
max-width: 100%;
/* Fix for potential inline spacing issues */
display: block;
}
/* This clear image is layered on top */
#clear {
position: absolute;
top: 0;
left: 0;
max-width: 100%;
/* Start with the reveal circle hidden */
clip-path: circle(0px at 0 0);
}
</style>
<div class="image-container">
<img id="blurred" src="${imgSrc}" alt="Blurred image" />
<img id="clear" src="${imgSrc}" alt="Revealed image" />
</div>
`;
// --- 3. Get references to the internal elements ---
// We must search within the component's 'shadowRoot'
this.clearImg = this.shadowRoot.getElementById('clear');
this.container = this.shadowRoot.querySelector('.image-container');
// --- 4. Attach event listeners ---
// We must .bind(this) so that 'this' inside the handler
// refers to the class instance, not the element.
this.container.addEventListener('mousemove', this.handleMouseMove.bind(this));
this.container.addEventListener('mouseleave', this.handleMouseLeave.bind(this));
this.container.addEventListener('mousedown', this.handleMouseDown.bind(this));
this.container.addEventListener('mouseup', this.handleMouseUp.bind(this));
}
// --- 5. Component Methods (Event Handlers & Logic) ---
handleMouseMove(e) {
// Get mouse position relative to the container
const rect = this.container.getBoundingClientRect();
this.mouseX = e.clientX - rect.left;
this.mouseY = e.clientY - rect.top;
// Update the clip-path if not animating
if (!this.animating) {
this.updateClipPath();
}
}
handleMouseLeave() {
// Hide the circle and reset animation state
this.animating = false;
this.clearImg.style.clipPath = 'circle(0px at 0 0)';
this.currentRadius = this.radius;
this.targetRadius = this.radius;
}
handleMouseDown() {
this.targetRadius = this.bigRadius;
this.startAnimation();
}
handleMouseUp() {
this.targetRadius = this.radius;
this.startAnimation();
}
startAnimation() {
if (!this.animating) {
this.animating = true;
// Use requestAnimationFrame for a smooth animation loop
this.animateCircle();
}
}
animateCircle() {
if (!this.animating) return;
// Smoothly interpolate the radius (Lerp)
this.currentRadius += (this.targetRadius - this.currentRadius) * 0.2;
this.updateClipPath();
// Stop the loop if we're "close enough" to the target
if (Math.abs(this.currentRadius - this.targetRadius) > 0.5) {
requestAnimationFrame(this.animateCircle.bind(this));
} else {
// Snap to the final value and stop
this.currentRadius = this.targetRadius;
this.updateClipPath();
this.animating = false;
}
}
// Helper function to apply the clip-path
updateClipPath() {
this.clearImg.style.clipPath = `circle(${this.currentRadius}px at ${this.mouseX}px ${this.mouseY}px)`;
}
}
// --- 6. Register the new HTML tag ---
// This tells the browser that <reveal-image> should be handled by our class
customElements.define('reveal-image', RevealImage);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment