728x90
프로젝트를 하다가 아래와 같이 회전하는 Dial모양의 컴포넌트를 만들게 되었다.
우선 위 컴포넌트를 만들어낸 방식은 아래와 같다.
실제로는 아래와 같이 미리 렌더링을 하고 , 빨간색 박스 부분만 보여지도록 제어를 해주면 된다.
이때 회전을 주기위해서 rotate 효과를 활용할 수 있다.
const [rotation, setRotation] = useState(0);
따라서 위와같이 회전값을 지정해준 후 버튼이 눌릴때마다 회전을 시켜주면 된다.
그런데 단순하게 위 그림대로 만들어 두고 회전하는 방식으로는 정확하게 180도씩 회전하지 않았다.
위 gif에서 검은색 박스를 보자. rotate 으로 각도를 변화시키면 영역을 감싸고 있는 큰 박스자체가 회전을 하게 된다.
이때 세개의 작은 Dial들이 있는 부분을 absolute속성으로 위치시켰다면 회전시켰을때 정확히 서로의 위치에 가게 제어하는 것이 어려웠다.
따라서 한개의 큰 영역을 회전시켜서 맞추는 것이 아닌, 아래와 같은 영역을 개개별로 3개를 만들어서 미리 회전시켜두었다.
이 경우 시작점이 세개의 회전 Dial 모두 0도에서 시작하기 때문에 회전을 할때 정확하게 서로의 위치가 맞게 된다!!
/** @jsxImportSource @emotion/react */
import React, { useState } from "react";
import styled from "styled-components";
import CircleTyle from "./CircleTyle";
import Dial from "./Dial";
import { circleType, searchType, tmpSelectType } from "types/home";
import { waitSecond } from "utils/time";
import { homeColor } from "styles/color";
interface CircleProp {
rotation: number;
circleType: circleType;
isAnimate: boolean;
}
interface Props {
windowWidth: number;
}
const circleDegree = {
main: 0,
before: 240,
after: 120,
};
const Wheel = ({ windowWidth }: Props) => {
const [rotation, setRotation] = useState(0);
const [dialRotation, setDialRotation] = useState(0);
const [dialMainType, setDailMainType] = useState<tmpSelectType>("searchType");
const [isAnimate, setIsAnimate] = useState(true);
// 현재 다이얼에 표시될 메뉴의 순서 (메인 , 이전 , 이후 순서)
const [menuList, setMenuList] = useState<searchType[]>([
"views",
"popular",
"followers",
]);
const menuOrder: circleType[] = ["main", "before", "after"];
const degreeOrder = [0, -120, 120];
/**
* 휠을 오른쪽으로 회전시키는 함수
* @param isturnRight
*/
const rotateWheelRight = async (isturnRight: boolean) => {
setRotation(rotation + (isturnRight ? 120 : -120));
await waitSecond(1); // 애니메이션 시간 대기
setIsAnimate(false); // 애니메이션을 끄고 새로운 정보로 대체
setRotation(0);
setDialRotation(0);
setDailMainType("searchType");
if (isturnRight) setMenuList([menuList[1], menuList[2], menuList[0]]);
else setMenuList([menuList[2], menuList[0], menuList[1]]);
await waitSecond(0.01); // 다시 애니메이션이 적용될 수 있도록 적용
setIsAnimate(true);
};
return (
<Wrapper>
<WheelIntroduce>
<Title>
<p>
<span>Tyle</span>의 카테고리별
</p>
<p>랭킹 확인</p>
</Title>
<Ment>
<p>원판을 돌려 더많은 순위정보를</p>
<p>확인해 보세요!</p>
</Ment>
</WheelIntroduce>
<WheelWrapper>
<Dial
searchType={menuList[0]}
beforeType={menuList[1]}
afterType={menuList[2]}
rotateWheelRight={rotateWheelRight}
rotation={dialRotation}
setRotation={setDialRotation}
mainType={dialMainType}
setMainType={setDailMainType}
isAnimate={isAnimate}
windowWidth={windowWidth}
></Dial>
{menuList.map((menu, idx) => (
<BigCircleArea
isAnimate={isAnimate}
rotation={rotation + degreeOrder[idx]}
key={idx}
>
<BigCircle>
<Circles>
<CircleTyleArea
isAnimate={isAnimate}
rotation={rotation}
circleType={menuOrder[idx]}
>
<CircleTyle
width={windowWidth * 0.2}
userType="silver"
searchType={menu}
></CircleTyle>
</CircleTyleArea>
<CircleTyleArea
isAnimate={isAnimate}
rotation={rotation}
circleType={menuOrder[idx]}
>
<CircleTyle
width={windowWidth * 0.2}
userType="gold"
searchType={menu}
></CircleTyle>
</CircleTyleArea>
<CircleTyleArea
isAnimate={isAnimate}
rotation={rotation}
circleType={menuOrder[idx]}
>
<CircleTyle
width={windowWidth * 0.2}
userType="bronze"
searchType={menu}
></CircleTyle>
</CircleTyleArea>
</Circles>
</BigCircle>
</BigCircleArea>
))}
</WheelWrapper>
</Wrapper>
);
};
/* STYLE */
const Wrapper = styled.div``;
const WheelWrapper = styled.div`
position: relative;
width: 100vw;
height: 50vw;
overflow: hidden;
`;
const WheelIntroduce = styled.div`
padding: 8rem 2rem;
& span {
color: ${homeColor.empahsis};
}
`;
const Title = styled.div`
font-size: 50px;
font-weight: 700;
p {
margin-bottom: 0.5rem;
}
`;
const Ment = styled.div`
font-size: 22px;
font-weight: 700;
color: ${homeColor.wheelIntroduceGray};
`;
const BigCircleArea = styled.div<{ rotation: number; isAnimate: boolean }>`
position: absolute;
top: -25%;
left: -50%;
width: 100%;
height: 100%;
transform: rotate(${({ rotation }) => rotation}deg);
transition: ${({ isAnimate }) => (isAnimate ? "1s" : "0s")};
`;
const BigCircle = styled.div`
width: 100%;
height: 100%;
position: relative;
`;
const Circles = styled.div`
position: absolute;
display: flex;
width: 100%;
gap: 1rem;
top: 43%;
left: 80%;
`;
const CircleTyleArea = styled.div<CircleProp>`
rotate: ${({ rotation, circleType }) =>
360 - rotation - circleDegree[circleType]}deg;
transition: ${({ isAnimate }) => (isAnimate ? "1s" : "0s")};
`;
export default Wheel;
큰 Dial안의 영역이 바뀌는 것 또한 똑같은 원리를 한번 더 적용해서 만들어주었다.
/** @jsxImportSource @emotion/react */
import React from "react";
import styled, { css } from "styled-components";
import { homeColor } from "styles/color";
import { homeZIdx } from "styles/zIndex";
import { searchType, tmpSelectType } from "types/home";
interface Props {
searchType: searchType;
beforeType: searchType;
afterType: searchType;
rotateWheelRight: (isturnRight: boolean) => void;
rotation: number;
setRotation: React.Dispatch<React.SetStateAction<number>>;
mainType: tmpSelectType;
setMainType: React.Dispatch<React.SetStateAction<tmpSelectType>>;
isAnimate: boolean;
windowWidth: number;
}
interface TmpSelectTypeProp {
rotation: number;
isAnimate: boolean;
windowWidth: number;
}
const typeMent = {
views: "조회순",
popular: "인기순",
followers: "구독자순",
};
const Dial = ({
searchType,
beforeType,
afterType,
rotateWheelRight,
rotation,
setRotation,
mainType,
setMainType,
isAnimate,
windowWidth,
}: Props) => {
const rotateDialRight = (isturnRight: boolean) => {
setRotation(rotation + (isturnRight ? 60 : -60));
};
const clickBeforeType = () => {
rotateWheelRight(true);
rotateDialRight(true);
setMainType("beforeType");
};
const clickAfterType = () => {
rotateWheelRight(false);
rotateDialRight(false);
setMainType("afterType");
};
const typeObj = {
searchType,
beforeType,
afterType,
};
return (
<Wrapper>
<DialBtn
searchType={typeObj[mainType]}
rotation={rotation - 120}
isAnimate={isAnimate}
>
<TmpSelectType
rotation={rotation - 120}
isAnimate={isAnimate}
windowWidth={windowWidth}
>
{typeMent[afterType]}
</TmpSelectType>
</DialBtn>
<DialBtn
searchType={typeObj[mainType]}
rotation={rotation - 60}
isAnimate={isAnimate}
>
<DialMenu
rotation={rotation - 60}
isSelect={mainType === "beforeType" ? true : false}
windowWidth={windowWidth}
searchType={typeObj[mainType]}
isAnimate={isAnimate}
onClick={clickBeforeType}
>
{typeMent[beforeType]}
</DialMenu>
</DialBtn>
<MainDialBtn
searchType={typeObj[mainType]}
rotation={rotation}
isAnimate={isAnimate}
>
<SelectType
rotation={rotation}
searchType={typeObj[mainType]}
windowWidth={windowWidth}
isTurn={mainType === "searchType" ? false : true}
isAnimate={isAnimate}
>
{typeMent[searchType]}
</SelectType>
</MainDialBtn>
<DialBtn
searchType={typeObj[mainType]}
rotation={rotation + 60}
isAnimate={isAnimate}
>
<DialMenu
rotation={rotation + 60}
isSelect={mainType === "afterType" ? true : false}
isAnimate={isAnimate}
windowWidth={windowWidth}
searchType={typeObj[mainType]}
onClick={clickAfterType}
>
{typeMent[afterType]}
</DialMenu>
</DialBtn>
<DialBtn
searchType={typeObj[mainType]}
rotation={rotation + 120}
isAnimate={isAnimate}
>
<TmpSelectType
rotation={rotation + 120}
isAnimate={isAnimate}
windowWidth={windowWidth}
>
{typeMent[beforeType]}
</TmpSelectType>
</DialBtn>
</Wrapper>
);
};
/* STYLE */
const Wrapper = styled.div``;
const MainTypeCSS = css<{ searchType: searchType; windowWidth: number }>`
font-weight: 700;
text-shadow: 3px 4px 4px ${({ searchType }) => homeColor[searchType]};
font-size: ${({ windowWidth }) => `${windowWidth * 0.06}px`};
`;
const SubTypeCSS = css<{ windowWidth: number }>`
font-weight: 700;
color: ${homeColor.subSelectTypeGray};
font-size: ${({ windowWidth }) => `${windowWidth * 0.03}px`};
`;
const DialBtn = styled.div<{
searchType: searchType;
rotation: number;
isAnimate: boolean;
}>`
position: absolute;
left: -25%;
width: 50%;
height: 100%;
transform: rotate(${({ rotation }) => rotation}deg);
transition: ${({ isAnimate }) => (isAnimate ? "1s" : "0s")};
pointer-events: none;
z-index: ${homeZIdx.dial};
`;
const MainDialBtn = styled(DialBtn)`
border-radius: 9999px;
box-shadow: 4px 4px 20px 0px ${({ searchType }) => homeColor[searchType]};
`;
const noneSearchType = styled.div<TmpSelectTypeProp>`
position: absolute;
cursor: pointer;
rotate: ${({ rotation }) => 360 - rotation}deg;
transition: ${({ isAnimate }) => (isAnimate ? "1s" : "0s")};
right: 5%;
pointer-events: auto;
top: calc(50% - ${({ windowWidth }) => `${windowWidth * 0.03}px`});
`;
const TmpSelectType = styled(noneSearchType)`
${SubTypeCSS}
`;
const DialMenu = styled(noneSearchType)<{
isSelect: boolean;
searchType: searchType;
}>`
${({ isSelect }) => (isSelect ? MainTypeCSS : SubTypeCSS)}
`;
const SelectType = styled(noneSearchType)<{
searchType: searchType;
isTurn: boolean;
}>`
${({ isTurn }) => (isTurn ? SubTypeCSS : MainTypeCSS)}
`;
export default Dial;
728x90
'프로젝트 > 소규모프로젝트들' 카테고리의 다른 글
[express] 라즈베리파이 이미지서버 사용 (0) | 2023.08.18 |
---|---|
라즈베리파이로 웹 서버 만들기 (3) - 배포하기 (0) | 2023.01.27 |
라즈베리파이로 웹 서버 만들기 (2) - 라파 DB 서버로 사용하기 (1) | 2023.01.24 |
라즈베리파이로 웹서버 만들기 (1) - 밖에서 라파 접속하기 (3) | 2023.01.24 |
mdx Editor만들기 (마크다운 에디터) (1) | 2022.11.27 |