Created
October 27, 2020 19:09
-
-
Save mattgperry/6281538936f963296211ff9a84f659b4 to your computer and use it in GitHub Desktop.
This file contains hidden or 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
import { | |
AnimatePresence, | |
AnimateSharedLayout, | |
motion, | |
useMotionValue, | |
useIsPresent, | |
} from "framer-motion"; | |
import * as React from "react"; | |
import { useEffect, useRef, useState } from "react"; | |
import { shuffle } from "lodash"; | |
import styled from "styled-components"; | |
import { animate, distance, clamp, linear, PlaybackControls } from "popmotion"; | |
import move from "array-move"; | |
const MobileContainer = styled.div` | |
height: 568px; | |
width: 320px; | |
background: var(--color-background); | |
border-radius: 40px; | |
display: flex; | |
flex-direction: column; | |
overflow: hidden; | |
position: relative; | |
`; | |
export const Example = styled.div` | |
grid-column: lo-start / ro-end !important; | |
background: var(--color-grey); | |
border-radius: 3px; | |
margin-bottom: 75px !important; | |
margin-top: 25px; | |
display: flex; | |
padding: 20px 0px; | |
justify-content: center; | |
`; | |
const CardContainerSpacing = styled.div` | |
height: 250px; | |
margin-bottom: 20px; | |
`; | |
const Image = styled(motion.div)` | |
background: ${({ color }) => `var(${color})`}; | |
height: 180px; | |
padding-top: 10px; | |
padding-left: 5px; | |
[data-isopen="true"] & { | |
height: 300px; | |
padding-top: 20px; | |
padding-left: 20px; | |
} | |
`; | |
const CardContainer = styled(motion.div)` | |
height: 100%; | |
cursor: pointer; | |
overflow: hidden; | |
position: relative; | |
background: var(--color-text); | |
will-change: transform; | |
[data-isopen="true"] & { | |
position: absolute; | |
top: 0; | |
left: 0; | |
right: 0; | |
bottom: 0; | |
z-index: 1; | |
} | |
`; | |
const Info = styled(motion.div)` | |
height: 70px; | |
position: absolute; | |
bottom: 0; | |
right: 0; | |
left: 0; | |
padding: 10px; | |
display: flex; | |
align-items: flex-start; | |
[data-isopen="true"] & { | |
top: 300px; | |
height: auto; | |
} | |
`; | |
const Icon = styled(motion.div)` | |
width: 50px; | |
height: 50px; | |
background: var(${({ color }) => color}); | |
[data-isopen="true"] & { | |
width: 120px; | |
height: 120px; | |
} | |
`; | |
const TextLine = styled.div` | |
height: 20px; | |
width: 180px; | |
border-radius: 10px; | |
background: white; | |
`; | |
const TextContainer = styled(motion.div)` | |
position: relative; | |
padding: 5px 0 0 10px; | |
`; | |
const Button = styled(motion.div)` | |
background: var(${({ color }) => color}); | |
width: 120px; | |
height: 30px; | |
border-radius: 25px; | |
`; | |
function Card({ color, layout = true, onClick }: any) { | |
const [isOpen, setIsOpen] = useState(false); | |
const zIndex = useMotionValue(0); | |
React.useEffect(() => { | |
if (isOpen) zIndex.set(1); | |
}); | |
return ( | |
<CardContainerSpacing data-isopen={isOpen}> | |
<CardContainer | |
layout={layout} | |
onClick={() => (onClick ? onClick() : setIsOpen(!isOpen))} | |
initial={false} | |
animate={{ borderRadius: isOpen ? 0 : 20 }} | |
style={{ zIndex }} | |
onAnimationComplete={() => { | |
if (!isOpen) zIndex.set(0); | |
}} | |
> | |
<Image layout={layout} color={color}> | |
<TextContainer layout={layout} style={{ width: 180, opacity: 0.4 }}> | |
<TextLine | |
style={{ | |
height: 18, | |
width: "100%", | |
marginBottom: 10, | |
}} | |
/> | |
<TextLine | |
style={{ | |
height: 18, | |
width: "80%", | |
}} | |
/> | |
</TextContainer> | |
</Image> | |
<Info layout={layout}> | |
<Icon | |
initial={false} | |
animate={{ borderRadius: isOpen ? 10 : 5 }} | |
color={color} | |
layout={layout} | |
/> | |
<TextContainer layout={layout} style={{ width: 180 }}> | |
<TextLine | |
style={{ | |
height: 14, | |
width: "100%", | |
marginBottom: 10, | |
opacity: 0.4, | |
}} | |
/> | |
<TextLine | |
style={{ | |
height: 14, | |
width: "80%", | |
opacity: 0.4, | |
}} | |
/> | |
<Button | |
layout={layout} | |
color={color} | |
animate={{ opacity: isOpen ? 1 : 0 }} | |
style={{ marginTop: 20 }} | |
/> | |
</TextContainer> | |
</Info> | |
</CardContainer> | |
</CardContainerSpacing> | |
); | |
} | |
function CardList({ layout, onClick, fromDock = false }: any) { | |
const isPresent = useIsPresent(); | |
const opacity = useMotionValue(fromDock ? 0 : 1); | |
useEffect(() => { | |
if (!fromDock) return; | |
let current: PlaybackControls; | |
opacity.attach((v, set) => { | |
if (isPresent) { | |
set(v); | |
current?.stop(); | |
} else if (!current && v < 0.1) { | |
current = animate({ | |
from: 1, | |
to: 0, | |
duration: 200, | |
ease: linear, | |
onUpdate: (v) => { | |
set(v); | |
}, | |
}); | |
} | |
}); | |
}, [isPresent]); | |
return ( | |
<motion.div | |
transition={{ duration: 0.35, ease: [0.2, 0.05, 0.48, 1] }} | |
layoutId="container" | |
style={{ | |
opacity, | |
padding: 20, | |
background: "var(--color-background)", | |
zIndex: 1, | |
}} | |
> | |
<Card color={"--color-a"} layout={layout} onClick={onClick} /> | |
<Card color={"--color-b"} layout={layout} onClick={onClick} /> | |
</motion.div> | |
); | |
} | |
export function ComplexPrototypeExample() { | |
return ( | |
<Example> | |
<MobileContainer> | |
<CardList /> | |
</MobileContainer> | |
</Example> | |
); | |
} | |
const AppListContainer = styled.div` | |
display: flex; | |
flex-wrap: wrap; | |
padding: 30px; | |
width: 100%; | |
position: absolute; | |
top: 0; | |
`; | |
const DummyIcon = styled.div` | |
border-radius: 15px; | |
background: var(--color-grey); | |
width: 57px; | |
height: 57px; | |
margin-bottom: 10px; | |
margin-right: 10px; | |
&:nth-child(4n) { | |
margin-right: 0; | |
} | |
`; | |
const ActualIcon = styled(DummyIcon)` | |
background: var(--color-a); | |
cursor: pointer; | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
`; | |
function AppList({ onClick }) { | |
return ( | |
<AppListContainer> | |
<DummyIcon /> | |
<DummyIcon /> | |
<DummyIcon /> | |
<DummyIcon /> | |
<DummyIcon /> | |
<DummyIcon /> | |
<ActualIcon | |
as={motion.div} | |
layoutId="container" | |
transition={{ duration: 0.35, ease: [0.2, 0.05, 0.48, 1] }} | |
onClick={onClick} | |
> | |
<div | |
style={{ | |
borderRadius: "50%", | |
border: "3px solid white", | |
width: 35, | |
height: 35, | |
}} | |
/> | |
</ActualIcon> | |
</AppListContainer> | |
); | |
} | |
export function AppIconExample({ layout = false }: any) { | |
const [isAppOpen, setIsOpen] = useState(false); | |
return ( | |
<Example> | |
<MobileContainer> | |
<AnimateSharedLayout type="crossfade"> | |
<AnimatePresence> | |
{!isAppOpen ? ( | |
<AppList key="list" onClick={() => setIsOpen(true)} /> | |
) : ( | |
<CardList | |
key="card" | |
layout={layout} | |
onClick={() => setIsOpen(false)} | |
fromDock | |
/> | |
)} | |
</AnimatePresence> | |
</AnimateSharedLayout> | |
</MobileContainer> | |
</Example> | |
); | |
} | |
export function AppIconExampleIncorrect() { | |
return <AppIconExample layout />; | |
} | |
const ShuffleButton = styled(motion.button)` | |
background: var(--color-a); | |
padding: 12px 18px; | |
display: inline-block; | |
border-radius: 10px; | |
color: white; | |
text-decoration: none; | |
width: auto; | |
box-shadow: 2px 2px 5px 0px rgba(0, 0, 0, 0.4); | |
&:focus { | |
outline: none; | |
box-shadow: 0 0 0px 2px var(--color-grey), 0 0 0px 4px var(--color-a); | |
} | |
`; | |
const OrderList = styled.ul` | |
padding: 0 !important; | |
margin: 0 !important; | |
list-style: none !important; | |
`; | |
const OrderListItem = styled(motion.li)` | |
margin-bottom: 10px !important; | |
padding: 0; | |
background: var(--color-b); | |
border-radius: 10px; | |
width: 250px; | |
list-style: none !important; | |
`; | |
export function ListReorder() { | |
const [order, setOrder] = useState([0, 1, 2, 3]); | |
return ( | |
<Example> | |
<div | |
style={{ | |
display: "flex", | |
flexDirection: "column", | |
justifyContent: "center", | |
}} | |
> | |
<OrderList> | |
{order.map((item) => ( | |
<OrderListItem | |
layout | |
transition={{ type: "spring", duration: 0.9, bounce: 0.4 }} | |
key={item} | |
style={{ height: 30 + item * 15 }} | |
/> | |
))} | |
</OrderList> | |
<ShuffleButton | |
whileTap={{ scale: 0.95 }} | |
transition={{ type: false }} | |
onClick={() => setOrder(shuffle(order))} | |
> | |
Shuffle items | |
</ShuffleButton> | |
</div> | |
</Example> | |
); | |
} | |
export function usePositionReorder(initialState) { | |
const [order, setOrder] = useState(initialState); | |
// We need to collect an array of height and position data for all of this component's | |
// `Item` children, so we can later us that in calculations to decide when a dragging | |
// `Item` should swap places with its siblings. | |
const positions = useRef([]).current; | |
const updatePosition = (i, offset) => (positions[i] = offset); | |
// Find the ideal index for a dragging item based on its position in the array, and its | |
// current drag offset. If it's different to its current index, we swap this item with that | |
// sibling. | |
const updateOrder = (i, dragOffset) => { | |
const targetIndex = findIndex(i, dragOffset, positions); | |
if (targetIndex !== i) setOrder(move(order, i, targetIndex)); | |
}; | |
return [order, updatePosition, updateOrder]; | |
} | |
const buffer = 30; | |
export const findIndex = (i, yOffset, positions) => { | |
let target = i; | |
const { top, height } = positions[i]; | |
const bottom = top + height; | |
// If moving down | |
if (yOffset > 0) { | |
const nextItem = positions[i + 1]; | |
if (nextItem === undefined) return i; | |
const swapOffset = | |
distance(bottom, nextItem.top + nextItem.height / 2) + buffer; | |
if (yOffset > swapOffset) target = i + 1; | |
// If moving up | |
} else if (yOffset < 0) { | |
const prevItem = positions[i - 1]; | |
if (prevItem === undefined) return i; | |
const prevBottom = prevItem.top + prevItem.height; | |
const swapOffset = distance(top, prevBottom - prevItem.height / 2) + buffer; | |
if (yOffset < -swapOffset) target = i - 1; | |
} | |
return clamp(0, positions.length, target); | |
}; | |
export function useMeasurePosition(update) { | |
// We'll use a `ref` to access the DOM element that the `motion.li` produces. | |
// This will allow us to measure its height and position, which will be useful to | |
// decide when a dragging element should switch places with its siblings. | |
const ref = useRef(null); | |
// Update the measured position of the item so we can calculate when we should rearrange. | |
useEffect(() => { | |
update({ | |
height: ref.current.offsetHeight, | |
top: ref.current.offsetTop, | |
}); | |
}); | |
return ref; | |
} | |
function DraggableItem({ i, height, updatePosition, updateOrder }) { | |
const [isDragging, setDragging] = useState(false); | |
const ref = useMeasurePosition((pos) => updatePosition(i, pos)); | |
return ( | |
<OrderListItem | |
ref={ref} | |
layout | |
initial={false} | |
transition={{ type: "spring", duration: 0.9, bounce: 0.4 }} | |
style={{ height, cursor: "pointer" }} | |
whileHover={{ | |
scale: 1.03, | |
boxShadow: "0px 3px 3px rgba(0,0,0,0.15)", | |
}} | |
whileTap={{ | |
scale: 1.12, | |
boxShadow: "0px 5px 5px rgba(0,0,0,0.1)", | |
}} | |
drag="y" | |
onDragStart={() => setDragging(true)} | |
onDragEnd={() => setDragging(false)} | |
onViewportBoxUpdate={(_, delta) => { | |
isDragging && updateOrder(i, delta.y.translate); | |
}} | |
/> | |
); | |
} | |
const items = [50, 80, 70, 100]; | |
export function ListDragToReorder() { | |
const [order, updatePosition, updateOrder] = usePositionReorder(items); | |
return ( | |
<Example> | |
<div | |
style={{ | |
display: "flex", | |
flexDirection: "column", | |
justifyContent: "center", | |
}} | |
> | |
<OrderList> | |
{order.map((height, i) => ( | |
<DraggableItem | |
key={height} | |
height={height} | |
i={i} | |
updatePosition={updatePosition} | |
updateOrder={updateOrder} | |
/> | |
))} | |
</OrderList> | |
</div> | |
</Example> | |
); | |
} | |
const ContentRow = styled.div` | |
width: 100%; | |
height: 8px; | |
background-color: #999; | |
border-radius: 10px; | |
margin-top: 12px; | |
`; | |
function Content() { | |
return ( | |
<motion.div | |
layout | |
initial={{ opacity: 0 }} | |
animate={{ opacity: 1 }} | |
exit={{ opacity: 0 }} | |
> | |
<ContentRow /> | |
<ContentRow /> | |
<ContentRow /> | |
</motion.div> | |
); | |
} | |
const AutoItem = styled(motion.li)` | |
background-color: rgba(214, 214, 214, 0.5); | |
border-radius: 10px; | |
display: block; | |
padding: 20px !important; | |
margin-bottom: 20px !important; | |
overflow: hidden; | |
cursor: pointer; | |
&:last-child { | |
margin-bottom: 0px !important; | |
} | |
`; | |
const Avatar = styled(motion.div)` | |
width: 40px; | |
height: 40px; | |
background-color: #666; | |
border-radius: 20px; | |
`; | |
function AutoHeightItem() { | |
const [isOpen, setIsOpen] = useState(false); | |
const toggleOpen = () => setIsOpen(!isOpen); | |
return ( | |
<AutoItem layout onClick={toggleOpen} initial={{ borderRadius: 10 }}> | |
<Avatar layout /> | |
<AnimatePresence>{isOpen && <Content />}</AnimatePresence> | |
</AutoItem> | |
); | |
} | |
const AutoContainer = styled(motion.ul)` | |
width: 300px; | |
display: flex; | |
flex-direction: column; | |
background: white; | |
padding: 20px; | |
border-radius: 25px; | |
margin: 0 !important; | |
`; | |
export function AnimateHeightAuto() { | |
return ( | |
<Example | |
style={{ | |
height: 400, | |
alignItems: "center", | |
background: "var(--color-b)", | |
}} | |
> | |
<AnimateSharedLayout> | |
<AutoContainer layout initial={{ borderRadius: 25 }}> | |
<AutoHeightItem /> | |
<AutoHeightItem /> | |
</AutoContainer> | |
</AnimateSharedLayout> | |
</Example> | |
); | |
} | |
const UnderlineContainer = styled.div` | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
transform: translateZ(0); | |
margin: 20px 0; | |
`; | |
const Underline = styled(motion.div)` | |
width: 100%; | |
height: 4px; | |
position: absolute; | |
bottom: -6px; | |
background: var(--color-a); | |
`; | |
const UnderlineItem = styled(motion.div)` | |
font-size: 32px; | |
margin-left: 20px; | |
position: relative; | |
cursor: pointer; | |
`; | |
export const screens = ["Home", "Calendar", "Mail"]; | |
export function Menu() { | |
const [selected, setSelected] = useState(0); | |
return ( | |
<Example> | |
<AnimateSharedLayout> | |
<UnderlineContainer> | |
{screens.map((title, i) => ( | |
<UnderlineItem | |
key={i} | |
isSelected={i === selected} | |
initial={false} | |
animate={{ | |
color: i === selected ? "var(--color-a)" : "var(--color-text)", | |
}} | |
className="hl" | |
onClick={() => setSelected(i)} | |
> | |
{i === selected && ( | |
<Underline layoutId="underline" style={{ borderRadius: 4 }} /> | |
)} | |
{title} | |
</UnderlineItem> | |
))} | |
</UnderlineContainer> | |
</AnimateSharedLayout> | |
</Example> | |
); | |
} | |
export function TextWidthExample() { | |
const [isOpen, setIsOpen] = useState(false); | |
return ( | |
<Example> | |
<div | |
style={{ | |
display: "flex", | |
flexDirection: "column", | |
height: 450, | |
justifyContent: "space-between", | |
alignItems: "center", | |
}} | |
> | |
<p | |
style={{ | |
width: isOpen ? 150 : 350, | |
background: "var(--color-text)", | |
color: "var(--color-background)", | |
padding: 20, | |
transition: "width 4s ease-out", | |
borderRadius: 5, | |
}} | |
> | |
I must not animate layout. I must not animate layout. I must not | |
animate layout. I must not animate layout. I must not animate layout. | |
</p> | |
<ShuffleButton | |
whileTap={{ scale: 0.95 }} | |
transition={{ type: false }} | |
onClick={() => setIsOpen(!isOpen)} | |
> | |
Toggle width | |
</ShuffleButton> | |
</div> | |
</Example> | |
); | |
} | |
export function TextWidthCrossfadeExample() { | |
const [isOpen, setIsOpen] = useState(false); | |
return ( | |
<Example> | |
<div | |
style={{ | |
display: "flex", | |
flexDirection: "column", | |
height: 450, | |
justifyContent: "space-between", | |
alignItems: "center", | |
position: "relative", | |
}} | |
> | |
<AnimateSharedLayout type="crossfade"> | |
<AnimatePresence> | |
<motion.p | |
key={isOpen ? 150 : 350} | |
style={{ | |
width: isOpen ? 150 : 350, | |
background: "var(--color-text)", | |
color: "var(--color-background)", | |
padding: 20, | |
transition: "width 4s ease-out", | |
borderRadius: 5, | |
position: "absolute", | |
top: 0, | |
}} | |
layoutId="text" | |
transition={{ duration: 0.6 }} | |
> | |
I must not animate layout. I must not animate layout. I must not | |
animate layout. I must not animate layout. | |
</motion.p> | |
</AnimatePresence> | |
</AnimateSharedLayout> | |
<ShuffleButton | |
whileTap={{ scale: 0.95 }} | |
transition={{ type: false }} | |
onClick={() => setIsOpen(!isOpen)} | |
style={{ position: "absolute", bottom: 0 }} | |
> | |
Toggle width | |
</ShuffleButton> | |
</div> | |
</Example> | |
); | |
} | |
const ScaleIndicator = styled(motion.div)` | |
background-position: 5px 5px; | |
border-radius: 50px; | |
`; | |
export function PositionExample() { | |
const [isOpen, setIsOpen] = useState(false); | |
return ( | |
<Example style={{ height: 300 }}> | |
<div | |
style={{ | |
display: "flex", | |
flexDirection: "column", | |
alignContent: "center", | |
}} | |
> | |
<ScaleIndicator | |
animate={{ x: isOpen ? 100 : 0 }} | |
style={{ | |
borderRadius: 50, | |
width: 200, | |
height: 200, | |
backgroundColor: "var(--color-b)", | |
marginBottom: 10, | |
padding: 20, | |
}} | |
> | |
<div | |
style={{ | |
width: 50, | |
height: 50, | |
borderRadius: "50%", | |
background: "var(--color-background)", | |
}} | |
/> | |
</ScaleIndicator> | |
<ShuffleButton | |
whileTap={{ scale: 0.95 }} | |
transition={{ type: false }} | |
onClick={() => setIsOpen(!isOpen)} | |
> | |
Toggle position | |
</ShuffleButton> | |
</div> | |
</Example> | |
); | |
} | |
export function ScaleExample() { | |
const [isOpen, setIsOpen] = useState(false); | |
const size = isOpen ? 200 : 100; | |
return ( | |
<Example style={{ height: 300 }}> | |
<div | |
style={{ | |
display: "flex", | |
flexDirection: "column", | |
justifyContent: "space-between", | |
width: 200, | |
}} | |
> | |
<ScaleIndicator | |
layout | |
transition={{ duration: 1 }} | |
style={{ | |
width: size, | |
height: size, | |
padding: 20, | |
backgroundColor: "var(--color-b)", | |
}} | |
onClick={() => setIsOpen(!isOpen)} | |
> | |
<div | |
style={{ | |
width: 50, | |
height: 50, | |
borderRadius: "50%", | |
background: "var(--color-background)", | |
}} | |
/> | |
</ScaleIndicator> | |
<ShuffleButton | |
whileTap={{ scale: 0.95 }} | |
transition={{ type: false }} | |
onClick={() => setIsOpen(!isOpen)} | |
> | |
Toggle size | |
</ShuffleButton> | |
</div> | |
</Example> | |
); | |
} | |
export function CorrectedScaleExample() { | |
const [isOpen, setIsOpen] = useState(false); | |
const size = isOpen ? 200 : 100; | |
return ( | |
<Example style={{ height: 300 }}> | |
<div | |
style={{ | |
display: "flex", | |
flexDirection: "column", | |
justifyContent: "space-between", | |
width: 200, | |
}} | |
> | |
<ScaleIndicator | |
layout | |
transition={{ duration: 1 }} | |
style={{ | |
width: size, | |
height: size, | |
padding: 20, | |
backgroundColor: "var(--color-b)", | |
borderRadius: 50, | |
}} | |
onClick={() => setIsOpen(!isOpen)} | |
> | |
<motion.div | |
layout | |
style={{ | |
width: 50, | |
height: 50, | |
borderRadius: "50%", | |
background: "var(--color-background)", | |
}} | |
/> | |
</ScaleIndicator> | |
<ShuffleButton | |
whileTap={{ scale: 0.95 }} | |
transition={{ type: false }} | |
onClick={() => setIsOpen(!isOpen)} | |
> | |
Toggle size | |
</ShuffleButton> | |
</div> | |
</Example> | |
); | |
} | |
export function CorrectedScaleExampleNoStyles() { | |
const [isOpen, setIsOpen] = useState(false); | |
const size = isOpen ? 200 : 100; | |
return ( | |
<Example style={{ height: 300 }}> | |
<div | |
style={{ | |
display: "flex", | |
flexDirection: "column", | |
justifyContent: "space-between", | |
width: 200, | |
}} | |
> | |
<ScaleIndicator | |
layout | |
transition={{ duration: 1 }} | |
style={{ | |
width: size, | |
height: size, | |
padding: 20, | |
backgroundColor: "var(--color-b)", | |
}} | |
onClick={() => setIsOpen(!isOpen)} | |
> | |
<motion.div | |
layout | |
style={{ | |
width: 50, | |
height: 50, | |
background: "var(--color-background)", | |
borderRadius: "50%", | |
}} | |
/> | |
</ScaleIndicator> | |
<ShuffleButton | |
whileTap={{ scale: 0.95 }} | |
transition={{ type: false }} | |
onClick={() => setIsOpen(!isOpen)} | |
> | |
Toggle size | |
</ShuffleButton> | |
</div> | |
</Example> | |
); | |
} | |
const DistortionParent = styled.div` | |
height: 100px; | |
width: 300px; | |
background: var(--color-b); | |
border-radius: 20px; | |
padding: 10px; | |
`; | |
const DistortionChild = styled.div` | |
width: 80px; | |
height: 80px; | |
background: var(--color-background); | |
transform: translateX(100px); | |
border-radius: 50%; | |
`; | |
export function CoordinateDistortion() { | |
return ( | |
<Example> | |
<div> | |
<code | |
style={{ marginBottom: 10, display: "block" }} | |
>{`scaleX: 1`}</code> | |
<DistortionParent | |
style={{ | |
marginBottom: 40, | |
}} | |
> | |
<DistortionChild /> | |
</DistortionParent> | |
<code | |
style={{ marginBottom: 10, display: "block" }} | |
>{`scaleX: 0.5`}</code> | |
<DistortionParent | |
style={{ | |
transform: "scaleX(0.5)", | |
transformOrigin: "0% 0%", | |
}} | |
> | |
<DistortionChild /> | |
</DistortionParent> | |
</div> | |
</Example> | |
); | |
} | |
export function DragExample() { | |
return ( | |
<Example> | |
<motion.div | |
drag | |
dragMomentum={false} | |
style={{ | |
borderRadius: 20, | |
background: "var(--color-b)", | |
cursor: "pointer", | |
width: 100, | |
height: 100, | |
userSelect: "none", | |
WebkitTouchCallout: "none", | |
WebkitUserSelect: "none", | |
}} | |
/> | |
</Example> | |
); | |
} | |
export function DragExampleIncorrect() { | |
const [state, setState] = useState(false); | |
useEffect(() => { | |
setState(true); | |
}, []); | |
return ( | |
<Example> | |
<motion.div | |
drag | |
layout | |
dragMomentum={false} | |
style={{ | |
borderRadius: 20, | |
background: "var(--color-b)", | |
cursor: "pointer", | |
width: 100, | |
height: 100, | |
display: "flex", | |
justifyContent: "center", | |
alignItems: "center", | |
}} | |
> | |
<motion.div | |
layout | |
style={{ | |
borderRadius: 10, | |
background: "var(--color-background)", | |
cursor: "pointer", | |
width: state === false ? 49 : 50, | |
height: 50, | |
}} | |
></motion.div> | |
</motion.div> | |
</Example> | |
); | |
} | |
export function useViewportWidth() { | |
const viewportWidth = useRef(0); | |
useEffect(() => { | |
const updateViewportWidth = () => { | |
viewportWidth.current = window.innerWidth; | |
}; | |
updateViewportWidth(); | |
window.addEventListener("resize", updateViewportWidth); | |
}, []); | |
return viewportWidth; | |
} | |
export function SharedDragExample() { | |
const viewportWidth = useViewportWidth(); | |
const [activeHalf, setActiveHalf] = useState("a"); | |
const onViewportBoxUpdate = ({ x }) => { | |
const halfViewport = viewportWidth.current / 2; | |
if (activeHalf === "a" && x.min > halfViewport) { | |
setActiveHalf("b"); | |
} else if (activeHalf === "b" && x.max < halfViewport) { | |
setActiveHalf("a"); | |
} | |
}; | |
return ( | |
<AnimateSharedLayout> | |
<Example style={{ height: 400 }}> | |
<Zone | |
color="var(--color-b)" | |
isSelected={activeHalf === "a"} | |
onViewportBoxUpdate={onViewportBoxUpdate} | |
/> | |
<Zone | |
color="var(--color-a)" | |
isSelected={activeHalf === "b"} | |
onViewportBoxUpdate={onViewportBoxUpdate} | |
/> | |
</Example> | |
</AnimateSharedLayout> | |
); | |
} | |
const HalfContainer = styled.div` | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
width: 50%; | |
height: 100%; | |
position: relative; | |
`; | |
const Overlay = styled(motion.div)` | |
position: absolute; | |
top: 30px; | |
left: 30px; | |
bottom: 30px; | |
right: 30px; | |
background: var(--color-background); | |
border-radius: 10px; | |
`; | |
const Box = styled(motion.div)` | |
width: 100px; | |
height: 100px; | |
border-radius: 20px; | |
position: relative; | |
z-index: 1; | |
`; | |
function Zone({ color, isSelected, onViewportBoxUpdate }) { | |
return ( | |
<HalfContainer> | |
<Overlay animate={{ scale: isSelected ? 1.05 : 1 }} /> | |
{isSelected && ( | |
<Box | |
layoutId="box" | |
initial={false} | |
animate={{ backgroundColor: color }} | |
drag | |
// Snap the box back to its center when we let go | |
dragConstraints={{ top: 0, left: 0, right: 0, bottom: 0 }} | |
// Allow full movememnt outside constraints | |
dragElastic={1} | |
onViewportBoxUpdate={onViewportBoxUpdate} | |
/> | |
)} | |
</HalfContainer> | |
); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment