diff --git a/src/app/components/OutfitMovieLayer.js b/src/app/components/OutfitMovieLayer.js index edb0a7f..64cbfc2 100644 --- a/src/app/components/OutfitMovieLayer.js +++ b/src/app/components/OutfitMovieLayer.js @@ -10,6 +10,7 @@ function OutfitMovieLayer({ height, isPaused = false, onLoad = null, + onLowFps = null, }) { const [stage, setStage] = React.useState(null); const [library, setLibrary] = React.useState(null); @@ -165,6 +166,8 @@ function OutfitMovieLayer({ return; } + const targetFps = library.properties.fps; + let lastFpsLoggedAtInMs = performance.now(); let numFramesSinceLastLogged = 0; const intervalId = setInterval(() => { @@ -181,16 +184,20 @@ function OutfitMovieLayer({ const roundedFps = Math.round(fps * 100) / 100; console.debug( - `[OutfitMovieLayer] FPS: ${roundedFps} (Target: ${library.properties.fps}, ${numFramesSinceLastLogged}, ${timeSinceLastFpsLoggedAtInSec}) (${libraryUrl})` + `[OutfitMovieLayer] FPS: ${roundedFps} (Target: ${targetFps}) (${libraryUrl})` ); + if (onLowFps && fps < 2) { + onLowFps(fps); + } + lastFpsLoggedAtInMs = now; numFramesSinceLastLogged = 0; } - }, 1000 / library.properties.fps); + }, 1000 / targetFps); return () => clearInterval(intervalId); - }, [libraryUrl, stage, updateStage, movieClip, library, isPaused]); + }, [libraryUrl, stage, updateStage, movieClip, library, isPaused, onLowFps]); // This effect keeps the `movieClip` scaled correctly, based on the canvas // size and the `library`'s natural size declaration. (If the canvas size diff --git a/src/app/components/OutfitPreview.js b/src/app/components/OutfitPreview.js index 5198c1b..da3804a 100644 --- a/src/app/components/OutfitPreview.js +++ b/src/app/components/OutfitPreview.js @@ -1,5 +1,12 @@ import React from "react"; -import { Box, DarkMode, Flex, Text, useColorModeValue } from "@chakra-ui/react"; +import { + Box, + DarkMode, + Flex, + Text, + useColorModeValue, + useToast, +} from "@chakra-ui/react"; import LRU from "lru-cache"; import { WarningIcon } from "@chakra-ui/icons"; import { ClassNames } from "@emotion/react"; @@ -54,6 +61,9 @@ export function useOutfitPreview({ onChangeHasAnimations = null, ...props }) { + const [isPaused, setIsPaused] = useLocalStorage("DTIOutfitIsPaused", true); + const toast = useToast(); + const appearance = useOutfitAppearance({ speciesId, colorId, @@ -70,7 +80,26 @@ export function useOutfitPreview({ layersHaveAnimations, } = usePreloadLayers(visibleLayers); - const [isPaused] = useLocalStorage("DTIOutfitIsPaused", true); + const onLowFps = React.useCallback( + (fps) => { + setIsPaused(true); + console.warn(`[OutfitPreview] Pausing due to low FPS: ${fps}`); + + if (!toast.isActive("low-fps-warning")) { + toast({ + id: "low-fps-warning", + status: "warning", + title: "Sorry, the animation was lagging, so we paused it! 😖", + description: + "We do this to help make sure your machine doesn't lag too much! " + + "You can unpause the preview to try again.", + duration: null, + isClosable: true, + }); + } + }, + [setIsPaused, toast] + ); React.useEffect(() => { if (onChangeHasAnimations) { @@ -100,6 +129,7 @@ export function useOutfitPreview({ loadingDelayMs={loadingDelayMs} spinnerVariant={spinnerVariant} onChangeHasAnimations={onChangeHasAnimations} + onLowFps={onLowFps} doTransitions isPaused={isPaused} {...props} @@ -122,6 +152,7 @@ export function OutfitLayers({ spinnerVariant = "overlay", doTransitions = false, isPaused = true, + onLowFps = null, ...props }) { const [hiResMode] = useLocalStorage("DTIHiResMode", false); @@ -228,6 +259,7 @@ export function OutfitLayers({ width={canvasSize} height={canvasSize} isPaused={isPaused} + onLowFps={onLowFps} /> ) : (