Created
May 1, 2023 17:34
-
-
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
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
/* 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