Created
November 3, 2025 19:28
-
-
Save fitsum/c9282eafe1f27124cca5211ab1114706 to your computer and use it in GitHub Desktop.
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
| /* | |
| 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