a pixel-sized component that scales to fill its dynamic-sized parent component

Features

  • automatically scales (via transform) to fill available space
  • centers self within parent
  • supports both contain and cover fill modes
  • smooth transitions on prop changes

Dependencies

  • react-use for observe/measure hook
  • motion for animations
import React, { useMemo } from 'react'
import { useMeasure } from 'react-use'
import { motion } from 'motion/react'
 
const PixelSizedAutoScaler = function ({
	screen_pixels_horiz, screen_pixels_vert, 
	fill_mode, 
	color_bg_canvas, 
	children
}) {
	
	const [ ref, { width, height } ] = useMeasure()
	
	const pos_top = useMemo(() => {
		const pos_top = (height / 2) - (screen_pixels_vert / 2)
		return pos_top
	}, [ height, screen_pixels_vert ])
	
	const pos_left = useMemo(() => {
		const pos_left = (width / 2) - (screen_pixels_horiz / 2)
		return pos_left
	}, [ width, screen_pixels_horiz ])
	
	const scaleFactor = useMemo(() => {
		
		let m_func
		if (fill_mode === undefined || fill_mode === 'contain') {
			m_func = Math.min
		}
		else if (fill_mode === 'cover') {
			m_func = Math.max
		}
		else {
			console.error(`unexpected: fill_mode=${fill_mode}`)
			// fallback
			m_func = Math.min
		}
		
		return m_func(
			(1 / (screen_pixels_horiz / width)),
			(1 / (screen_pixels_vert / height)),
		)
		
	}, [ height, width, screen_pixels_horiz, screen_pixels_vert, fill_mode ])
	
	const jsx_inner = (width === 0 || height === 0) ? undefined : (
		<motion.div
			initial={false}
			animate={{
				top  : pos_top, 
				left : pos_left, 
				scale: scaleFactor,
			}}
			transition={{
				ease    : 'linear',
				duration: 1/8,
			}}
			style={{
				position: 'absolute',
				width   : `${screen_pixels_horiz}px`,
				height  : `${screen_pixels_vert}px`,
				
				backgroundColor: color_bg_canvas,
			}}
		>
			{children}
		</motion.div>
	)
	
	return (
		<div 
			ref={ref} 
			style={{
				height: '100%', width: '100%',
				position: 'relative',
			}}
		>
			{jsx_inner}
		</div>
	)
	
}
 
export { PixelSizedAutoScaler }