Add hi-res mode setting, default off
We're just having too many glitchy SVGs for my taste, esp since TNT seems to just be using PNGs for now? This change defaults us to using PNGs for users by default, with the option to use SVGs as a new "hi-res mode" setting. This is our first ever setting, wow! I'm also envisioning that like, if we get Fastly Image Optimizer set up, this could be a way to tune the quality of the incoming images. We could also consider a setting to turn off animations altogether—like, just download the PNG instead of the movie, whereas right now we download the movie on the assumption that you might play it at any time.
This commit is contained in:
parent
efa8a4d499
commit
bed525d3ff
2 changed files with 122 additions and 34 deletions
|
@ -5,10 +5,20 @@ import {
|
|||
Button,
|
||||
DarkMode,
|
||||
Flex,
|
||||
FormControl,
|
||||
FormHelperText,
|
||||
FormLabel,
|
||||
HStack,
|
||||
IconButton,
|
||||
ListItem,
|
||||
Popover,
|
||||
PopoverArrow,
|
||||
PopoverBody,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
Portal,
|
||||
Stack,
|
||||
Switch,
|
||||
Tooltip,
|
||||
UnorderedList,
|
||||
useClipboard,
|
||||
|
@ -17,8 +27,10 @@ import {
|
|||
import {
|
||||
ArrowBackIcon,
|
||||
CheckIcon,
|
||||
ChevronDownIcon,
|
||||
DownloadIcon,
|
||||
LinkIcon,
|
||||
SettingsIcon,
|
||||
} from "@chakra-ui/icons";
|
||||
import { MdPause, MdPlayArrow } from "react-icons/md";
|
||||
import { Link } from "react-router-dom";
|
||||
|
@ -151,13 +163,20 @@ function OutfitControls({
|
|||
<Box gridArea="back" onClick={maybeUnlockFocus}>
|
||||
<BackButton outfitState={outfitState} />
|
||||
</Box>
|
||||
{showAnimationControls && (
|
||||
<Box gridArea="play-pause" display="flex" justifyContent="center">
|
||||
<DarkMode>
|
||||
<PlayPauseButton />
|
||||
</DarkMode>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box
|
||||
gridArea="play-pause"
|
||||
// HACK: Better visual centering with other controls
|
||||
paddingTop="0.3rem"
|
||||
>
|
||||
<HStack spacing="2" align="center" justify="center">
|
||||
{showAnimationControls && <PlayPauseButton />}
|
||||
<SettingsButton
|
||||
onLockFocus={onLockFocus}
|
||||
onUnlockFocus={onUnlockFocus}
|
||||
/>
|
||||
</HStack>
|
||||
</Box>
|
||||
<Stack
|
||||
gridArea="sharing"
|
||||
alignSelf="flex-end"
|
||||
|
@ -379,7 +398,6 @@ function PlayPauseButton() {
|
|||
<PlayPauseButtonContent
|
||||
isPaused={isPaused}
|
||||
setIsPaused={setIsPaused}
|
||||
marginTop="0.3rem" // to center-align with buttons (not sure on amt?)
|
||||
ref={buttonRef}
|
||||
/>
|
||||
{blinkInState.type === "started" && (
|
||||
|
@ -432,36 +450,95 @@ function PlayPauseButton() {
|
|||
const PlayPauseButtonContent = React.forwardRef(
|
||||
({ isPaused, setIsPaused, ...props }, ref) => {
|
||||
return (
|
||||
<Button
|
||||
<TranslucentButton
|
||||
ref={ref}
|
||||
leftIcon={isPaused ? <MdPause /> : <MdPlayArrow />}
|
||||
size="sm"
|
||||
color="gray.100"
|
||||
variant="outline"
|
||||
borderColor="gray.200"
|
||||
borderRadius="full"
|
||||
backgroundColor="blackAlpha.600"
|
||||
boxShadow="md"
|
||||
position="absolute"
|
||||
_hover={{
|
||||
backgroundColor: "gray.600",
|
||||
borderColor: "gray.50",
|
||||
color: "gray.50",
|
||||
}}
|
||||
_focus={{
|
||||
backgroundColor: "gray.600",
|
||||
borderColor: "gray.50",
|
||||
color: "gray.50",
|
||||
}}
|
||||
onClick={() => setIsPaused(!isPaused)}
|
||||
{...props}
|
||||
>
|
||||
{isPaused ? <>Paused</> : <>Playing</>}
|
||||
</Button>
|
||||
</TranslucentButton>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
function SettingsButton({ onLockFocus, onUnlockFocus }) {
|
||||
return (
|
||||
<Popover onOpen={onLockFocus} onClose={onUnlockFocus}>
|
||||
<PopoverTrigger>
|
||||
<TranslucentButton size="xs" aria-label="Settings">
|
||||
<SettingsIcon />
|
||||
<Box width="1" />
|
||||
<ChevronDownIcon />
|
||||
</TranslucentButton>
|
||||
</PopoverTrigger>
|
||||
<Portal>
|
||||
<PopoverContent width="25ch">
|
||||
<PopoverArrow />
|
||||
<PopoverBody>
|
||||
<HiResModeSetting />
|
||||
</PopoverBody>
|
||||
</PopoverContent>
|
||||
</Portal>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
function HiResModeSetting() {
|
||||
const [hiResMode, setHiResMode] = useLocalStorage("DTIHiResMode", false);
|
||||
|
||||
return (
|
||||
<FormControl>
|
||||
<Flex>
|
||||
<Box>
|
||||
<FormLabel htmlFor="hi-res-mode-setting" fontSize="sm" margin="0">
|
||||
Hi-res mode (SVG)
|
||||
</FormLabel>
|
||||
<FormHelperText marginTop="0" fontSize="xs">
|
||||
Crisper at higher resolutions, but not always accurate
|
||||
</FormHelperText>
|
||||
</Box>
|
||||
<Box width="2" />
|
||||
<Switch
|
||||
id="hi-res-mode-setting"
|
||||
size="sm"
|
||||
marginTop="0.1rem"
|
||||
isChecked={hiResMode}
|
||||
onChange={(e) => setHiResMode(e.target.checked)}
|
||||
/>
|
||||
</Flex>
|
||||
</FormControl>
|
||||
);
|
||||
}
|
||||
|
||||
const TranslucentButton = React.forwardRef(({ children, ...props }, ref) => {
|
||||
return (
|
||||
<Button
|
||||
ref={ref}
|
||||
size="sm"
|
||||
color="gray.100"
|
||||
variant="outline"
|
||||
borderColor="gray.200"
|
||||
borderRadius="full"
|
||||
backgroundColor="blackAlpha.600"
|
||||
boxShadow="md"
|
||||
_hover={{
|
||||
backgroundColor: "gray.600",
|
||||
borderColor: "gray.50",
|
||||
color: "gray.50",
|
||||
}}
|
||||
_focus={{
|
||||
backgroundColor: "gray.600",
|
||||
borderColor: "gray.50",
|
||||
color: "gray.50",
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* ControlButton is a UI helper to render the cute round buttons we use in
|
||||
* OutfitControls!
|
||||
|
@ -493,6 +570,8 @@ function ControlButton({ icon, "aria-label": ariaLabel, ...props }) {
|
|||
* image URL.
|
||||
*/
|
||||
function useDownloadableImage(visibleLayers) {
|
||||
const [hiResMode] = useLocalStorage("DTIHiResMode", false);
|
||||
|
||||
const [downloadImageUrl, setDownloadImageUrl] = React.useState(null);
|
||||
const [preparedForLayerIds, setPreparedForLayerIds] = React.useState([]);
|
||||
const toast = useToast();
|
||||
|
@ -511,8 +590,12 @@ function useDownloadableImage(visibleLayers) {
|
|||
|
||||
setDownloadImageUrl(null);
|
||||
|
||||
// NOTE: You could argue that we may as well just always use PNGs here,
|
||||
// regardless of hi-res mode… but using the same src will help both
|
||||
// performance (can use cached SVG), and predictability (image will
|
||||
// look like what you see here).
|
||||
const imagePromises = visibleLayers.map((layer) =>
|
||||
loadImage(getBestImageUrlForLayer(layer))
|
||||
loadImage(getBestImageUrlForLayer(layer, { hiResMode }))
|
||||
);
|
||||
|
||||
let images;
|
||||
|
@ -545,7 +628,7 @@ function useDownloadableImage(visibleLayers) {
|
|||
);
|
||||
setDownloadImageUrl(canvas.toDataURL("image/png"));
|
||||
setPreparedForLayerIds(layerIds);
|
||||
}, [preparedForLayerIds, visibleLayers, toast]);
|
||||
}, [preparedForLayerIds, visibleLayers, toast, hiResMode]);
|
||||
|
||||
return [downloadImageUrl, prepareDownload];
|
||||
}
|
||||
|
|
|
@ -124,6 +124,8 @@ export function OutfitLayers({
|
|||
isPaused = true,
|
||||
...props
|
||||
}) {
|
||||
const [hiResMode] = useLocalStorage("DTIHiResMode", false);
|
||||
|
||||
const containerRef = React.useRef(null);
|
||||
const [canvasSize, setCanvasSize] = React.useState(0);
|
||||
const [loadingDelayHasPassed, setLoadingDelayHasPassed] = React.useState(
|
||||
|
@ -230,13 +232,16 @@ export function OutfitLayers({
|
|||
) : (
|
||||
<Box
|
||||
as="img"
|
||||
src={getBestImageUrlForLayer(layer).src}
|
||||
src={getBestImageUrlForLayer(layer, { hiResMode }).src}
|
||||
// The crossOrigin prop isn't strictly necessary for loading
|
||||
// here (<img> tags are always allowed through CORS), but
|
||||
// this means we make the same request that the Download
|
||||
// button makes, so it can use the cached version of this
|
||||
// image instead of requesting it again with crossOrigin!
|
||||
crossOrigin={getBestImageUrlForLayer(layer).crossOrigin}
|
||||
crossOrigin={
|
||||
getBestImageUrlForLayer(layer, { hiResMode })
|
||||
.crossOrigin
|
||||
}
|
||||
alt=""
|
||||
objectFit="contain"
|
||||
maxWidth="100%"
|
||||
|
@ -306,8 +311,8 @@ export function FullScreenCenter({ children, ...otherProps }) {
|
|||
);
|
||||
}
|
||||
|
||||
export function getBestImageUrlForLayer(layer) {
|
||||
if (layer.svgUrl) {
|
||||
export function getBestImageUrlForLayer(layer, { hiResMode = false } = {}) {
|
||||
if (hiResMode && layer.svgUrl) {
|
||||
return { src: safeImageUrl(layer.svgUrl), crossOrigin: "anonymous" };
|
||||
} else {
|
||||
return { src: safeImageUrl(layer.imageUrl), crossOrigin: "anonymous" };
|
||||
|
|
Loading…
Reference in a new issue