Last active
February 28, 2025 08:07
-
-
Save LoueeD/fe938b186e7868bcbe445353f53d5516 to your computer and use it in GitHub Desktop.
tiny vue render overlay inspired by react-scan
This file contains 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
function createDOMHighlighter() { | |
const canvas = document.createElement('canvas'); | |
const ctx = canvas.getContext('2d'); | |
Object.assign(canvas.style, { | |
position: 'fixed', | |
inset: '0', | |
zIndex: '99999999', | |
pointerEvents: 'none', | |
width: '100%', | |
height: '100%' | |
}); | |
document.body.appendChild(canvas); | |
const animations = new Set(); | |
let animationFrameId = null; | |
const resizeCanvas = () => { | |
const dpr = window.devicePixelRatio || 1; | |
const rect = canvas.getBoundingClientRect(); | |
canvas.width = rect.width * dpr; | |
canvas.height = rect.height * dpr; | |
ctx.scale(dpr, dpr); | |
}; | |
resizeCanvas(); | |
window.addEventListener('resize', resizeCanvas); | |
// Find closest Vue component ID by traversing up the DOM tree | |
const getVueId = (element) => { | |
let current = element; | |
while (current) { | |
for (const attr of current.attributes || []) { | |
if (attr.name.startsWith('data-v-')) { | |
return attr.name; | |
} | |
} | |
current = current.parentElement; | |
} | |
return null; | |
}; | |
// Render text with background | |
const drawLabel = (text, x, y, alpha) => { | |
ctx.save(); | |
// Set font and measure text | |
ctx.font = '12px monospace'; | |
const metrics = ctx.measureText(text); | |
const padding = 4; | |
const boxWidth = metrics.width + (padding * 2); | |
const boxHeight = 20; | |
// Draw background rect | |
ctx.fillStyle = `rgba(0, 0, 0, ${alpha * 0.8})`; | |
ctx.fillRect( | |
x, | |
y - boxHeight, | |
boxWidth, | |
boxHeight | |
); | |
// Draw text | |
ctx.fillStyle = `rgba(255, 255, 255, ${alpha})`; | |
ctx.fillText(text, x + padding, y - 6); | |
ctx.restore(); | |
}; | |
const animate = () => { | |
const dpr = window.devicePixelRatio || 1; | |
ctx.clearRect(0, 0, canvas.width / dpr, canvas.height / dpr); | |
for (const anim of animations) { | |
const progress = (Date.now() - anim.startTime) / 600; // 600ms duration | |
if (progress >= 1) { | |
animations.delete(anim); | |
continue; | |
} | |
const fillAlpha = 0.2 * (1 - progress); | |
const strokeAlpha = 0.5 * (1 - progress); | |
// Draw highlight rectangle | |
ctx.fillStyle = `rgba(0, 255, 0, ${fillAlpha})`; | |
ctx.fillRect( | |
anim.rect.x, | |
anim.rect.y, | |
anim.rect.width, | |
anim.rect.height | |
); | |
ctx.strokeStyle = `rgba(0, 180, 0, ${strokeAlpha})`; | |
ctx.lineWidth = 2; | |
ctx.strokeRect( | |
anim.rect.x, | |
anim.rect.y, | |
anim.rect.width, | |
anim.rect.height | |
); | |
// Draw label if we have a Vue ID | |
if (anim.vueId) { | |
drawLabel( | |
anim.vueId, | |
anim.rect.x, | |
anim.rect.y, | |
1 - progress | |
); | |
} | |
} | |
if (animations.size > 0) { | |
animationFrameId = requestAnimationFrame(animate); | |
} else { | |
animationFrameId = null; | |
} | |
}; | |
const observer = new MutationObserver(mutations => { | |
for (const mutation of mutations) { | |
// Handle added nodes | |
for (const node of mutation.addedNodes) { | |
if (node.nodeType === Node.ELEMENT_NODE) { | |
const rect = node.getBoundingClientRect(); | |
const vueId = getVueId(node); | |
animations.add({ | |
startTime: Date.now(), | |
rect, | |
vueId | |
}); | |
} | |
} | |
// Handle attributes changes | |
if (mutation.type === 'attributes' && mutation.target.nodeType === Node.ELEMENT_NODE) { | |
const rect = mutation.target.getBoundingClientRect(); | |
const vueId = getVueId(mutation.target); | |
animations.add({ | |
startTime: Date.now(), | |
rect, | |
vueId | |
}); | |
} | |
} | |
if (!animationFrameId && animations.size > 0) { | |
animationFrameId = requestAnimationFrame(animate); | |
} | |
}); | |
observer.observe(document.body, { | |
childList: true, | |
subtree: true, | |
attributes: true | |
}); | |
return () => { | |
observer.disconnect(); | |
window.removeEventListener('resize', resizeCanvas); | |
if (animationFrameId) { | |
cancelAnimationFrame(animationFrameId); | |
} | |
canvas.remove(); | |
}; | |
} | |
// Usage: | |
const cleanup = createDOMHighlighter(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
To try this out, head to vuejs or vuetify, paste the code in devtools and click around the site.