Skip to content

Instantly share code, notes, and snippets.

@LoueeD
Last active February 28, 2025 08:07
Show Gist options
  • Save LoueeD/fe938b186e7868bcbe445353f53d5516 to your computer and use it in GitHub Desktop.
Save LoueeD/fe938b186e7868bcbe445353f53d5516 to your computer and use it in GitHub Desktop.
tiny vue render overlay inspired by react-scan
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();
@LoueeD
Copy link
Author

LoueeD commented Nov 24, 2024

To try this out, head to vuejs or vuetify, paste the code in devtools and click around the site.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment