-
-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* oval shape * 여유좌표, overflow처리 * circular * scale opacity * content setup * direction * chore: minWidth 760 * refactor: oval offsetPath * rotate logic * offset rotate * fix: rotate logic * fix: duration tune * refactor: DockContent * gradient utils * fix: dock content transition, content bg * fix: initial exit animation AnimatePresence custom property * fix: content * fix: static image * fine tuing * svg path save
- Loading branch information
1 parent
7df7eb6
commit d5780cf
Showing
19 changed files
with
732 additions
and
0 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,158 @@ | ||
// like radix-ui Tab.Content | ||
|
||
import { AnimatePresence, motion, Variants } from 'framer-motion'; | ||
import { forwardRef, ReactElement } from 'react'; | ||
|
||
import { styled } from '../../../../stitches.config'; | ||
|
||
import { useMenuDockContext } from './context'; | ||
|
||
// index -> value is better | ||
export interface DockContentProps { | ||
children: React.ReactNode; | ||
index: number; | ||
bottomAddon?: ReactElement; | ||
} | ||
const xAmount = 290; | ||
const yAmount = 40; | ||
const yRotate = 18; | ||
const x = { | ||
left: -1.08 * xAmount, | ||
right: xAmount, | ||
}; | ||
|
||
const rotateY = { | ||
right: yRotate, | ||
left: -1 * yRotate, | ||
}; | ||
interface AnimateParams { | ||
direction: number; | ||
} | ||
|
||
const variants: Variants = { | ||
enter: ({ direction }: AnimateParams) => { | ||
return { | ||
x: direction > 0 ? x.left : x.right, | ||
rotateY: direction > 0 ? rotateY.left : rotateY.right, | ||
y: yAmount, | ||
opacity: 0, | ||
scale: 0.85, | ||
transformPerspective: 400, | ||
}; | ||
}, | ||
center: { | ||
zIndex: 1, | ||
x: 0, | ||
y: 0, | ||
rotateY: 0, | ||
opacity: 1, | ||
scale: 1, | ||
transformPerspective: 400, | ||
}, | ||
exit: ({ direction }: AnimateParams) => { | ||
return { | ||
zIndex: 0, | ||
x: direction > 0 ? x.right : x.left, | ||
y: yAmount * 1.1, | ||
rotateY: direction < 0 ? rotateY.left : rotateY.right, | ||
opacity: 0, | ||
scale: 0.85, | ||
transformPerspective: 400, | ||
}; | ||
}, | ||
}; | ||
export const DockContentImpl = forwardRef<HTMLDivElement, DockContentProps>( | ||
(props, ref) => { | ||
const { children, index, bottomAddon, ...restProps } = props; | ||
const { activeIndex, direction } = useMenuDockContext('DockContent'); | ||
const visible = activeIndex === index; | ||
const animationDirection = direction === 'clockwise' ? 1 : -1; | ||
|
||
return ( | ||
<AnimatePresence | ||
initial={false} | ||
custom={{ direction: animationDirection }} | ||
> | ||
{visible ? ( | ||
<MotionRoot | ||
ref={ref} | ||
key={index} | ||
custom={{ direction: animationDirection }} | ||
variants={variants} | ||
initial="enter" | ||
animate="center" | ||
exit="exit" | ||
transition={{ | ||
ease: [0.45, 0, 0.55, 1], | ||
duration: 0.5, | ||
opacity: { duration: 0.3 }, | ||
}} | ||
{...restProps} | ||
> | ||
{children} | ||
<Fog>{bottomAddon}</Fog> | ||
</MotionRoot> | ||
) : null} | ||
</AnimatePresence> | ||
); | ||
} | ||
); | ||
|
||
const BottomAddonRoot = styled('div', { | ||
display: 'flex', | ||
flexDirection: 'column', | ||
justifyContent: 'flex-end', | ||
height: '100%', | ||
padding: '0 0 26px 26px', | ||
color: 'rgb(29,29,31)', | ||
fontSize: '14px', | ||
}); | ||
|
||
const Title = styled('div', { | ||
fontWeight: 600, | ||
}); | ||
|
||
const Caption = styled('div', { | ||
marginTop: 4, | ||
}); | ||
|
||
const Fog = styled('div', { | ||
width: '100%', | ||
height: '46%', | ||
zIndex: 1, | ||
|
||
position: 'absolute', | ||
bottom: 0, | ||
left: 0, | ||
borderRadius: '0px 0px 32px 32px', | ||
|
||
background: | ||
'linear-gradient(transparent, rgba(255, 255, 255, 0.416) 58%, rgb(255, 255, 255, 0.52) 100%)', | ||
}); | ||
|
||
const MotionRoot = styled(motion.div, { | ||
height: 240, | ||
width: 240, | ||
position: 'absolute', | ||
borderRadius: 32, | ||
display: 'flex', | ||
alignItems: 'center', | ||
boxShadow: '0 0 8px rgba(177, 177, 177, 0.25)', | ||
|
||
'&::after': { | ||
content: '', | ||
width: '100%', | ||
height: '100%', | ||
position: 'absolute', | ||
top: 0, | ||
left: 0, | ||
boxShadow: 'inset 0 0 1px rgba(0,0,0,0.12)', | ||
borderRadius: 'inherit', | ||
}, | ||
}); | ||
|
||
export const DockContent = Object.assign(DockContentImpl, { | ||
BottomAddonRoot, | ||
Title, | ||
Caption, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
import { composeEventHandlers } from '@radix-ui/primitive'; | ||
import { motion } from 'framer-motion'; | ||
import { ButtonHTMLAttributes, ReactNode, useMemo } from 'react'; | ||
|
||
import { styled } from '../../../../stitches.config'; | ||
import { usePrevious } from '../../../hooks/usePrevious'; | ||
|
||
import { useMenuDockContext } from './context'; | ||
|
||
export interface DockItemProps | ||
extends Omit< | ||
ButtonHTMLAttributes<HTMLButtonElement>, | ||
'onDrag' | 'onDragEnd' | 'onDragStart' | 'onAnimationStart' | ||
> { | ||
children: ReactNode; | ||
index: number; | ||
} | ||
|
||
const origin = [0, 1, 2, 3, 4]; | ||
const pathLength = [7, 17, 27, 37, 47]; | ||
const midIndex = Math.floor(pathLength.length / 2); | ||
// 개수는 5개 기준으로 작성 | ||
export function DockItem({ | ||
children, | ||
index, | ||
onClick, | ||
...restProps | ||
}: DockItemProps) { | ||
const { onActiveIndexChange, activeIndex, direction, onDirectionChange } = | ||
useMenuDockContext('DockItem'); | ||
const offset = Math.abs(activeIndex - midIndex); | ||
const orderedIndex = | ||
offset === 0 | ||
? origin | ||
: activeIndex > 2 | ||
? //next | ||
[...origin.slice(offset), ...origin.slice(0, offset)] | ||
: //prev | ||
[ | ||
...origin.slice(origin.length - offset), | ||
...origin.slice(0, origin.length - offset), | ||
]; | ||
|
||
const order = orderedIndex.findIndex(i => i === index); | ||
|
||
const nextPathLengthValueCandidate = pathLength[order]; | ||
const prevPathLengthValue = usePrevious(pathLength[order]); | ||
|
||
const nextPathLengthValue = useMemo(() => { | ||
if (direction === 'counterclockwise') { | ||
return nextPathLengthValueCandidate > prevPathLengthValue | ||
? nextPathLengthValueCandidate - 100 | ||
: nextPathLengthValueCandidate; | ||
} | ||
|
||
return nextPathLengthValueCandidate < prevPathLengthValue | ||
? 100 + nextPathLengthValueCandidate | ||
: nextPathLengthValueCandidate; | ||
}, [direction, nextPathLengthValueCandidate, prevPathLengthValue]); | ||
|
||
return ( | ||
<Item | ||
onClick={composeEventHandlers(onClick, () => { | ||
const direction = order < 2 ? 'clockwise' : 'counterclockwise'; | ||
onDirectionChange(direction); | ||
onActiveIndexChange(index); | ||
})} | ||
initial={false} | ||
animate={{ | ||
offsetDistance: `${nextPathLengthValue}%`, | ||
scale: order === 2 ? 1.3 : 1 - Math.abs(offset) * 0.05, | ||
opacity: activeIndex === index ? 1 : 1 - Math.abs(offset) * 0.1, | ||
transitionEnd: { | ||
offsetDistance: `${adjustInRange(nextPathLengthValue, 0, 100)}%`, | ||
}, | ||
}} | ||
transition={{ | ||
ease: [0.45, 0, 0.55, 1], | ||
duration: 0.6, | ||
scale: { | ||
duration: 0.3, | ||
}, | ||
}} | ||
{...restProps} | ||
> | ||
{children} | ||
</Item> | ||
); | ||
} | ||
|
||
function adjustInRange(number: number, min: number, max: number): number { | ||
if (number < min) { | ||
// 주어진 숫자가 최소값보다 작으면 최대값을 더해줌 | ||
return number + max; | ||
} else if (number > max) { | ||
// 주어진 숫자가 최대값보다 크면 최대값을 뺌 | ||
return number - max; | ||
} else { | ||
// 아무 조정이 필요 없는 경우 | ||
return number; | ||
} | ||
} | ||
|
||
const Item = styled(motion.button, { | ||
position: 'absolute', | ||
top: 0, | ||
left: 0, | ||
offsetPath: | ||
'path("M80.5 93.0032V68.2395C102.179 50.8165 127.503 33.912 163.6 21.3472C199.805 8.74474 246.872 0.5 312 0.5C442.114 0.5 510.116 28.4174 551.5 68.2127V95.9968L80.5 93.0032Z")', | ||
offsetRotate: '0deg', | ||
background: 'transparent', | ||
borderRadius: 12, | ||
border: '1px solid rgba(255, 255, 255, 0.9)', | ||
height: 48, | ||
width: 48, | ||
filter: 'saturate(0.9) brightness(0.9)', | ||
transition: 'transform 0.25s ease', | ||
display: 'flex', | ||
alignItems: 'center', | ||
justifyContent: 'center', | ||
}); |
Oops, something went wrong.