feat(ui): modularize imagesize components

Canvas and non-canvas have separate width and height and need their own separate aspect ratios. In order to not duplicate a lot of aspect ratio logic, the components relating to image size have been modularized.
This commit is contained in:
psychedelicious
2024-01-02 12:01:23 +11:00
committed by Kent Keirsey
parent 011757c497
commit 4f43eda09b
27 changed files with 614 additions and 554 deletions

View File

@@ -1,64 +0,0 @@
import { Flex } from '@chakra-ui/layout';
import type { Meta, StoryObj } from '@storybook/react';
import { InvControl } from 'common/components/InvControl/InvControl';
import { InvSlider } from 'common/components/InvSlider/InvSlider';
import { useState } from 'react';
import { AspectRatioPreview } from './AspectRatioPreview';
const meta: Meta<typeof AspectRatioPreview> = {
title: 'Components/AspectRatioPreview',
tags: ['autodocs'],
component: AspectRatioPreview,
};
export default meta;
type Story = StoryObj<typeof InvControl>;
const MIN = 64;
const MAX = 1024;
const STEP = 64;
const FINE_STEP = 8;
const INITIAL = 512;
const MARKS = Array.from(
{ length: Math.floor(MAX / STEP) },
(_, i) => MIN + i * STEP
);
const Component = () => {
const [width, setWidth] = useState(INITIAL);
const [height, setHeight] = useState(INITIAL);
return (
<Flex w="full" flexDir="column">
<InvControl label="Width">
<InvSlider
value={width}
min={MIN}
max={MAX}
step={STEP}
fineStep={FINE_STEP}
onChange={setWidth}
marks={MARKS}
/>
</InvControl>
<InvControl label="Height">
<InvSlider
value={height}
min={MIN}
max={MAX}
step={STEP}
fineStep={FINE_STEP}
onChange={setHeight}
marks={MARKS}
/>
</InvControl>
<Flex h={96} w={96} p={4}>
<AspectRatioPreview width={width} height={height} />
</Flex>
</Flex>
);
};
export const AspectRatioWithSliderInvControls: Story = {
render: Component,
};

View File

@@ -1,60 +0,0 @@
import { Flex, Icon } from '@chakra-ui/react';
import { useSize } from '@chakra-ui/react-use-size';
import { AnimatePresence, motion } from 'framer-motion';
import { useRef } from 'react';
import { FaImage } from 'react-icons/fa';
import {
BOX_SIZE_CSS_CALC,
ICON_CONTAINER_STYLES,
MOTION_ICON_ANIMATE,
MOTION_ICON_EXIT,
MOTION_ICON_INITIAL,
} from './constants';
import { useAspectRatioPreviewState } from './hooks';
import type { AspectRatioPreviewProps } from './types';
export const AspectRatioPreview = (props: AspectRatioPreviewProps) => {
const { width: _width, height: _height, icon = FaImage } = props;
const containerRef = useRef<HTMLDivElement>(null);
const containerSize = useSize(containerRef);
const { width, height, shouldShowIcon } = useAspectRatioPreviewState({
width: _width,
height: _height,
containerSize,
});
return (
<Flex
w="full"
h="full"
alignItems="center"
justifyContent="center"
ref={containerRef}
>
<Flex
bg="blackAlpha.400"
borderRadius="base"
width={`${width}px`}
height={`${height}px`}
alignItems="center"
justifyContent="center"
>
<AnimatePresence>
{shouldShowIcon && (
<Flex
as={motion.div}
initial={MOTION_ICON_INITIAL}
animate={MOTION_ICON_ANIMATE}
exit={MOTION_ICON_EXIT}
style={ICON_CONTAINER_STYLES}
>
<Icon as={icon} color="base.700" boxSize={BOX_SIZE_CSS_CALC} />
</Flex>
)}
</AnimatePresence>
</Flex>
</Flex>
);
};

View File

@@ -1,23 +0,0 @@
// When the aspect ratio is between these two values, we show the icon (experimentally determined)
export const ICON_LOW_CUTOFF = 0.23;
export const ICON_HIGH_CUTOFF = 1 / ICON_LOW_CUTOFF;
export const ICON_SIZE_PX = 48;
export const ICON_PADDING_PX = 16;
export const BOX_SIZE_CSS_CALC = `min(${ICON_SIZE_PX}px, calc(100% - ${ICON_PADDING_PX}px))`;
export const MOTION_ICON_INITIAL = {
opacity: 0,
};
export const MOTION_ICON_ANIMATE = {
opacity: 1,
transition: { duration: 0.1 },
};
export const MOTION_ICON_EXIT = {
opacity: 0,
transition: { duration: 0.1 },
};
export const ICON_CONTAINER_STYLES = {
width: '100%',
height: '100%',
alignItems: 'center',
justifyContent: 'center',
};

View File

@@ -1,48 +0,0 @@
import { useMemo } from 'react';
import { ICON_HIGH_CUTOFF, ICON_LOW_CUTOFF } from './constants';
type Dimensions = {
width: number;
height: number;
};
type UseAspectRatioPreviewStateArg = {
width: number;
height: number;
containerSize?: Dimensions;
};
type UseAspectRatioPreviewState = (
arg: UseAspectRatioPreviewStateArg
) => Dimensions & { shouldShowIcon: boolean };
export const useAspectRatioPreviewState: UseAspectRatioPreviewState = ({
width: _width,
height: _height,
containerSize,
}) => {
const dimensions = useMemo(() => {
if (!containerSize) {
return { width: 0, height: 0, shouldShowIcon: false };
}
const aspectRatio = _width / _height;
let width = _width;
let height = _height;
if (_width > _height) {
width = containerSize.width;
height = width / aspectRatio;
} else {
height = containerSize.height;
width = height * aspectRatio;
}
const shouldShowIcon =
aspectRatio < ICON_HIGH_CUTOFF && aspectRatio > ICON_LOW_CUTOFF;
return { width, height, shouldShowIcon };
}, [_height, _width, containerSize]);
return dimensions;
};

View File

@@ -1,7 +0,0 @@
import type { IconType } from 'react-icons';
export type AspectRatioPreviewProps = {
width: number;
height: number;
icon?: IconType;
};