OverlayFrame.tsx 1.7 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243
  1. import { useLayoutEffect, useRef, useState, type CSSProperties, type ReactNode } from 'react';
  2. import { OVERLAY_DIMENSIONS, type OverlayLayout } from '../utils/overlayLayout';
  3. // Render at the browser-source resolution, then scale the whole composition.
  4. // Text, camera and spacing therefore match in settings, OBS and standalone tabs.
  5. export function OverlayFrame({ layout, preview = false, children }: {
  6. layout: OverlayLayout;
  7. preview?: boolean;
  8. children: ReactNode;
  9. }) {
  10. const ref = useRef<HTMLDivElement>(null);
  11. const [scale, setScale] = useState(0);
  12. const { width, height } = OVERLAY_DIMENSIONS[layout];
  13. useLayoutEffect(() => {
  14. const element = ref.current;
  15. if (!element) return;
  16. const measure = () => setScale(Math.min(element.clientWidth / width, element.clientHeight / height));
  17. measure();
  18. if (typeof ResizeObserver === 'undefined') {
  19. window.addEventListener('resize', measure);
  20. return () => window.removeEventListener('resize', measure);
  21. }
  22. const observer = new ResizeObserver(measure);
  23. observer.observe(element);
  24. return () => observer.disconnect();
  25. }, [width, height]);
  26. const canvasStyle: CSSProperties & Record<'--overlay-vmin' | '--overlay-height', string> = {
  27. width, height,
  28. '--overlay-vmin': '10.8px',
  29. '--overlay-height': `${height}px`,
  30. position: 'absolute', top: '50%', left: '50%',
  31. transform: `translate(-50%, -50%) scale(${scale})`,
  32. transformOrigin: 'center',
  33. };
  34. return (
  35. <div ref={ref} className={preview ? 'relative w-full overflow-hidden' : 'fixed inset-0 overflow-hidden'}
  36. style={preview ? { aspectRatio: `${width} / ${height}` } : undefined}>
  37. <div style={canvasStyle}>{children}</div>
  38. </div>
  39. );
  40. }