diff --git a/src/video-grid/NewVideoGrid.tsx b/src/video-grid/NewVideoGrid.tsx index aba499ea..810946bc 100644 --- a/src/video-grid/NewVideoGrid.tsx +++ b/src/video-grid/NewVideoGrid.tsx @@ -148,18 +148,27 @@ interface DragState { cursorY: number; } +/** + * An interactive, animated grid of video tiles. + */ export const NewVideoGrid: FC = ({ items, disableAnimations, children, }) => { + // Overview: This component lays out tiles by rendering an invisible template + // grid of "slots" for tiles to go in. Once rendered, it uses the DOM API to + // get the dimensions of each slot, feeding these numbers back into + // react-spring to let the actual tiles move freely atop the template. + + // To know when the rendered grid becomes consistent with the layout we've + // requested, we give it a data-generation attribute which holds the ID of the + // most recently rendered generation of the grid, and watch it with a + // MutationObserver. + const [slotGrid, setSlotGrid] = useState(null); const [slotGridGeneration, setSlotGridGeneration] = useState(0); - const [gridRef1, gridBounds] = useMeasure(); - const gridRef2 = useRef(null); - const gridRef = useMergedRefs(gridRef1, gridRef2); - useEffect(() => { if (slotGrid !== null) { setSlotGridGeneration( @@ -179,6 +188,10 @@ export const NewVideoGrid: FC = ({ } }, [slotGrid, setSlotGridGeneration]); + const [gridRef1, gridBounds] = useMeasure(); + const gridRef2 = useRef(null); + const gridRef = useMergedRefs(gridRef1, gridRef2); + const slotRects = useMemo(() => { if (slotGrid === null) return []; @@ -214,7 +227,7 @@ export const NewVideoGrid: FC = ({ const [tiles] = useReactiveState( (prevTiles) => { - // If React hasn't yet rendered the current generation of the layout, skip + // If React hasn't yet rendered the current generation of the grid, skip // the update, because grid and slotRects will be out of sync if (slotGridGeneration !== grid?.generation) return prevTiles ?? []; @@ -264,43 +277,6 @@ export const NewVideoGrid: FC = ({ // react-spring's types are bugged and can't infer the spring type ) as unknown as [TransitionFn, SpringRef]; - const slotGridStyle = useMemo(() => { - if (grid === null) return {}; - - const areas = new Array<(number | null)[]>( - Math.ceil(grid.cells.length / grid.columns) - ); - for (let i = 0; i < areas.length; i++) - areas[i] = new Array(grid.columns).fill(null); - - let slotId = 0; - for (let i = 0; i < grid.cells.length; i++) { - const cell = grid.cells[i]; - if (cell?.origin) { - const slotEnd = i + cell.columns - 1 + grid.columns * (cell.rows - 1); - forEachCellInArea( - i, - slotEnd, - grid, - (_c, j) => (areas[row(j, grid)][column(j, grid)] = slotId) - ); - slotId++; - } - } - - return { - gridTemplateAreas: areas - .map( - (row) => - `'${row - .map((slotId) => (slotId === null ? "." : `s${slotId}`)) - .join(" ")}'` - ) - .join(" "), - gridTemplateColumns: `repeat(${columns}, 1fr)`, - }; - }, [grid, columns]); - const animateDraggedTile = (endOfGesture: boolean) => { const { tileId, tileX, tileY, cursorX, cursorY } = dragState.current!; const tile = tiles.find((t) => t.item.id === tileId)!; @@ -357,6 +333,11 @@ export const NewVideoGrid: FC = ({ } }; + // Callback for useDrag. We could call useDrag here, but the default + // pattern of spreading {...bind()} across the children to bind the gesture + // ends up breaking memoization and ruining this component's performance. + // Instead, we pass this callback to each tile via a ref, to let them bind the + // gesture using the much more sensible ref-based method. const onTileDrag = ( tileId: string, { @@ -411,6 +392,43 @@ export const NewVideoGrid: FC = ({ { target: gridRef2 } ); + const slotGridStyle = useMemo(() => { + if (grid === null) return {}; + + const areas = new Array<(number | null)[]>( + Math.ceil(grid.cells.length / grid.columns) + ); + for (let i = 0; i < areas.length; i++) + areas[i] = new Array(grid.columns).fill(null); + + let slotId = 0; + for (let i = 0; i < grid.cells.length; i++) { + const cell = grid.cells[i]; + if (cell?.origin) { + const slotEnd = i + cell.columns - 1 + grid.columns * (cell.rows - 1); + forEachCellInArea( + i, + slotEnd, + grid, + (_c, j) => (areas[row(j, grid)][column(j, grid)] = slotId) + ); + slotId++; + } + } + + return { + gridTemplateAreas: areas + .map( + (row) => + `'${row + .map((slotId) => (slotId === null ? "." : `s${slotId}`)) + .join(" ")}'` + ) + .join(" "), + gridTemplateColumns: `repeat(${columns}, 1fr)`, + }; + }, [grid, columns]); + const slots = useMemo(() => { const slots = new Array(items.length); for (let i = 0; i < items.length; i++)