Skip to content

Instantly share code, notes, and snippets.

@fabiospampinato
Created May 1, 2023 17:34
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save fabiospampinato/c0c5e53d4d7c4230155ebf5a93e7706e to your computer and use it in GitHub Desktop.
Save fabiospampinato/c0c5e53d4d7c4230155ebf5a93e7706e to your computer and use it in GitHub Desktop.
A little Voby hook for rendering hit boxes around each element on the page
/* IMPORT */
import {$$} from 'voby';
import {useEffect} from '~/hooks';
/* MAIN */
const useResizeObserver = ( ref: $<Element | undefined>, fn: ResizeObserverCallback, options: ResizeObserverOptions = {} ): void => {
useEffect ( () => {
const target = $$(ref);
if ( !target ) return;
const observer = new ResizeObserver ( fn );
observer.observe ( target, options );
return () => {
observer.disconnect ();
};
});
};
/* EXPORT */
export default useResizeObserver;
/* IMPORT */
import _ from '_';
import {$, $$} from 'voby';
import {useEffect, useResizeObserver} from '~/hooks';
/* MAIN */
const useDimensions = ( ref: $<Element | undefined> ): ObservableReadonly<{ width: number; height: number }> => {
const dimensions = $({ width: 0, height: 0 }, { equals: _.isEqual });
useEffect ( () => {
const target = $$(ref);
if ( !target ) return;
const get = () => ({ width: target.clientWidth, height: target.clientHeight });
const update = () => dimensions ( get () );
update ();
useResizeObserver ( target, update );
});
return dimensions;
};
/* EXPORT */
export default useDimensions;
/* IMPORT */
import {useDimensions, useEffect} from '~/hooks';
/* MAIN */
const useCanvasOverlay = (): HTMLCanvasElement => {
const canvas = document.createElement ( 'canvas' );
const context = canvas.getContext ( '2d' );
const dimensions = useDimensions ( document.body );
canvas.style.position = 'fixed';
canvas.style.inset = '0';
canvas.style.zIndex = '100000';
canvas.style.pointerEvents = 'none';
useEffect ( () => {
const width = dimensions ().width;
const height = dimensions ().height;
const ratio = Math.min ( 2, window.devicePixelRatio );
canvas.width = width * ratio;
canvas.height = height * ratio;
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;
context?.scale ( ratio, ratio );
});
useEffect ( () => {
document.body.appendChild ( canvas );
return () => {
document.body.removeChild ( canvas );
};
});
return canvas;
};
/* EXPORT */
export default useCanvasOverlay;
/* IMPORT */
import _ from '_';
import {$$} from 'voby';
import DOM from '@lib/dom';
import {useAnimationLoop, useCanvasOverlay} from '~/hooks';
/* HELPERS */
const BACKGROUNDS = ['#220a4f', '#004fd0', '#18b817', '#998f00', '#995400', '#900000'];
const FOREGROUNDS = ['#ffffff', '#ffffff', '#ffffff', '#ffffff', '#ffffff', '#ffffff'];
/* MAIN */
const useBoxer = ( ref: $<Element | undefined> ): void => {
const canvas = useCanvasOverlay ();
const ctx = canvas.getContext ( '2d' );
if ( !ctx ) return;
const clear = (): void => {
ctx.clearRect ( 0, 0, Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER );
};
const paint = (): void => {
const target = $$(ref);
if ( !target ) return;
const elements = Array.from ( target.querySelectorAll ( '*' ) );
const leaves = elements.filter ( element => !element.childElementCount );
const traversed = new Set ();
const paint = ( elements: Element[], level: number ): void => {
if ( !elements.length ) return;
const background = BACKGROUNDS[level % BACKGROUNDS.length];
const foreground = FOREGROUNDS[level % FOREGROUNDS.length];
for ( let i = 0, l = elements.length; i < l; i++ ) {
const element = elements[i];
if ( traversed.has ( element ) ) continue;
traversed.add ( element );
const rect = DOM.getRect ( element );
if ( !rect.width || !rect.height ) continue;
ctx.strokeStyle = background;
ctx.strokeRect ( rect.left, rect.top, rect.width, rect.height );
const nr = element.querySelectorAll ( '*' ).length;
const label = `${nr}`;
if ( nr < 1 ) continue;
ctx.font = '10px sans-serif';
const measure = ctx.measureText ( label );
const width = measure.width;
const height = 10;
ctx.fillStyle = background;
ctx.fillRect ( rect.left, rect.top, width + 2, height + 4 );
ctx.fillStyle = foreground;
ctx.fillText ( label, rect.left + 1, rect.top + height );
}
const parents = elements.map ( leaf => leaf.parentElement ).filter ( _.isTruthy ).filter ( parent => target.contains ( parent ) );
if ( !parents.length ) return;
paint ( parents, level + 1 );
};
paint ( leaves, 0 );
};
useAnimationLoop ( () => {
clear ();
paint ();
});
};
/* EXPORT */
export default useBoxer;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment