From b9ae002c5f36da4b4d3676c3f4d10d1e0ad9495b Mon Sep 17 00:00:00 2001 From: Robert Long Date: Thu, 12 Aug 2021 16:25:10 -0700 Subject: [PATCH] Improve grid demo --- package-lock.json | 33 +++++++- package.json | 3 +- src/GridDemo.jsx | 178 ++++++++++++++++++++++++++++++++-------- src/GridDemo.module.css | 11 +-- 4 files changed, 183 insertions(+), 42 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4e82ee0..a360337 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,8 @@ "react": "^17.0.0", "react-dom": "^17.0.0", "react-router-dom": "^5.2.0", - "react-use-gesture": "^9.1.3" + "react-use-gesture": "^9.1.3", + "react-use-measure": "^2.0.4" }, "devDependencies": { "@vitejs/plugin-react-refresh": "^1.3.1", @@ -729,6 +730,11 @@ "node": ">=0.10" } }, + "node_modules/debounce": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", + "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==" + }, "node_modules/debug": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", @@ -1401,6 +1407,18 @@ "react": ">= 16.8.0" } }, + "node_modules/react-use-measure": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.0.4.tgz", + "integrity": "sha512-7K2HIGaPMl3Q9ZQiEVjen3tRXl4UDda8LiTPy/QxP8dP2rl5gPBhf7mMH6MVjjRNv3loU7sNzey/ycPNnHVTxQ==", + "dependencies": { + "debounce": "^1.2.0" + }, + "peerDependencies": { + "react": ">=16.13", + "react-dom": ">=16.13" + } + }, "node_modules/regenerator-runtime": { "version": "0.13.7", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", @@ -2233,6 +2251,11 @@ "assert-plus": "^1.0.0" } }, + "debounce": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", + "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==" + }, "debug": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", @@ -2738,6 +2761,14 @@ "integrity": "sha512-CdqA2SmS/fj3kkS2W8ZU8wjTbVBAIwDWaRprX7OKaj7HlGwBasGEFggmk5qNklknqk9zK/h8D355bEJFTpqEMg==", "requires": {} }, + "react-use-measure": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.0.4.tgz", + "integrity": "sha512-7K2HIGaPMl3Q9ZQiEVjen3tRXl4UDda8LiTPy/QxP8dP2rl5gPBhf7mMH6MVjjRNv3loU7sNzey/ycPNnHVTxQ==", + "requires": { + "debounce": "^1.2.0" + } + }, "regenerator-runtime": { "version": "0.13.7", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", diff --git a/package.json b/package.json index c06ad03..1dbbe13 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,8 @@ "react": "^17.0.0", "react-dom": "^17.0.0", "react-router-dom": "^5.2.0", - "react-use-gesture": "^9.1.3" + "react-use-gesture": "^9.1.3", + "react-use-measure": "^2.0.4" }, "devDependencies": { "@vitejs/plugin-react-refresh": "^1.3.1", diff --git a/src/GridDemo.jsx b/src/GridDemo.jsx index 69652d8..40b2330 100644 --- a/src/GridDemo.jsx +++ b/src/GridDemo.jsx @@ -1,24 +1,35 @@ import React, { useCallback, useEffect, useRef, useState } from "react"; import classNames from "classnames"; import { useDrag } from "react-use-gesture"; -import { useSpring, useTransition, animated } from "@react-spring/web"; +import { useSprings, useTransition, animated } from "@react-spring/web"; import styles from "./GridDemo.module.css"; - -let tileIdx = 0; +import useMeasure from "react-use-measure"; export function GridDemo() { + const tileKey = useRef(0); const [stream, setStream] = useState(); const [tiles, setTiles] = useState([]); + const [tileStyles, setTileStyles] = useState({}); + + const [springs, api] = useSprings(tiles.length, (index) => ({ + from: { x: 0, y: 0, zIndex: 0, shadow: 1, scale: 1 }, + config: { + tension: 250, + }, + })); const startWebcam = useCallback(async () => { const stream = await navigator.mediaDevices.getUserMedia({ video: true }); setStream(stream); - setTiles([{ stream, key: tileIdx++ }]); + setTiles([{ stream, key: tileKey.current++ }]); }, []); const addTile = useCallback(() => { const newStream = stream.clone(); - setTiles((tiles) => [...tiles, { stream: newStream, key: tileIdx++ }]); + setTiles((tiles) => [ + ...tiles, + { stream: newStream, key: tileKey.current++ }, + ]); }, [stream]); const removeTile = useCallback(() => { @@ -29,14 +40,115 @@ export function GridDemo() { }); }, []); - useEffect(() => { - console.log(tiles); - }, [tiles]); + const [gridRef, gridBounds] = useMeasure(); - const tileTransitions = useTransition(tiles, { - from: { opacity: 0, scale: 0.5 }, - enter: { opacity: 1, scale: 1 }, - leave: { opacity: 0, scale: 0.5 }, + useEffect(() => { + const newTileStyles = {}; + const tileCount = tiles.length; + const { width: gridWidth, height: gridHeight } = gridBounds; + const gap = 8; + + if (tileCount > 0) { + const aspectRatio = gridWidth / gridHeight; + + let columnCount, rowCount; + + if (aspectRatio < 1) { + if (tileCount <= 4) { + columnCount = 1; + rowCount = tileCount; + } else if (tileCount <= 12) { + columnCount = 2; + rowCount = Math.ceil(tileCount / 2); + } + } else { + if (tileCount === 1) { + columnCount = 1; + rowCount = 1; + } else if (tileCount === 2) { + columnCount = 2; + rowCount = 1; + } else if (tileCount <= 4) { + columnCount = 2; + rowCount = 2; + } else if (tileCount <= 6) { + columnCount = 3; + rowCount = 2; + } else if (tileCount <= 8) { + columnCount = 4; + rowCount = 2; + } else if (tileCount <= 10) { + columnCount = 5; + rowCount = 2; + } else if (tileCount <= 12) { + columnCount = 4; + rowCount = 3; + } + } + + let tileHeight = Math.round( + (gridHeight - gap * (rowCount + 1)) / rowCount + ); + let tileWidth = Math.round( + (gridWidth - gap * (columnCount + 1)) / columnCount + ); + + const tileAspectRatio = tileWidth / tileHeight; + + if (tileAspectRatio > 16 / 9) { + tileWidth = (16 * tileHeight) / 9; + } + + for (let i = 0; i < tiles.length; i++) { + const tile = tiles[i]; + + const verticalIndex = Math.floor(i / columnCount); + const top = verticalIndex * tileHeight + (verticalIndex + 1) * gap; + + let rowItemCount; + + if (verticalIndex + 1 === rowCount && tileCount % rowCount !== 0) { + rowItemCount = Math.floor(tileCount / rowCount); + } else { + rowItemCount = Math.ceil(tileCount / rowCount); + } + + const horizontalIndex = i % columnCount; + const totalRowGapWidth = (rowItemCount + 1) * gap; + const totalRowTileWidth = rowItemCount * tileWidth; + const rowLeftMargin = Math.round( + (gridWidth - (totalRowTileWidth + totalRowGapWidth)) / 2 + ); + const left = + tileWidth * horizontalIndex + + rowLeftMargin + + (horizontalIndex + 1) * gap; + + newTileStyles[tile.key] = { + width: tileWidth, + height: tileHeight, + transform: `translate(${left}px, ${top}px)`, + }; + } + } + + setTileStyles(newTileStyles); + }, [gridBounds, tiles]); + + const bind = useDrag(({ args: [index], down, movement: [x, y] }) => { + api.start((springIndex) => { + const dragging = springIndex === index && down; + return { + x: dragging ? x : 0, + y: dragging ? y : 0, + scale: dragging ? 1.1 : 1, + zIndex: dragging ? 1 : 0, + shadow: dragging ? 15 : 1, + immediate: dragging + ? (key) => key === "zIndex" || key === "x" || key === "y" + : false, + }; + }); }); return ( @@ -46,16 +158,31 @@ export function GridDemo() { {stream && } {stream && } -
- {tileTransitions((style, tile) => ( - - ))} +
+ {springs.map(({ shadow, ...springStyles }, i) => { + const tile = tiles[i]; + + return ( + `rgba(0, 0, 0, 0.5) 0px ${s}px ${2 * s}px 0px` + ), + }} + {...tile} + /> + ); + })}
); } -function ParticipantTile({ style, stream }) { +function ParticipantTile({ style, stream, ...rest }) { const videoRef = useRef(); useEffect(() => { @@ -67,23 +194,8 @@ function ParticipantTile({ style, stream }) { } }, [stream]); - const [{ x, y }, api] = useSpring(() => ({ - from: { x: 0, y: 0 }, - config: { - tension: 250, - }, - })); - - const bind = useDrag(({ down, movement: [mx, my] }) => { - api.start({ x: down ? mx : 0, y: down ? my : 0 }); - }); - return ( - + ); diff --git a/src/GridDemo.module.css b/src/GridDemo.module.css index a655e52..72d23cd 100644 --- a/src/GridDemo.module.css +++ b/src/GridDemo.module.css @@ -12,17 +12,14 @@ .grid { position: relative; overflow: hidden; - display: grid; - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); - grid-auto-flow: dense; - grid-gap: 8px; - justify-items: stretch; - padding: 8px; flex: 1; } .participantTile { - will-change: transform, opacity; + position: absolute; + will-change: transform, width, height, opacity; + border-radius: 24px; + overflow: hidden; } .participantTile video {