Instantly share code, notes, and snippets.
Created
August 18, 2024 07:13
-
Star
1
(1)
You must be signed in to star a gist -
Fork
0
(0)
You must be signed in to fork a gist
-
Save 1216892614/69bc42c20b1a5d45ee06b812d21cf08d to your computer and use it in GitHub Desktop.
Apple Watch Menu like
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
import React, { useMemo, useRef, useState } from "react"; | |
import { animated, SpringValue, useSpringValue, to } from "@react-spring/web"; | |
const GOODS = [ | |
{ title: "0", color: "rgb(29, 69, 76)" }, | |
{ title: "1", color: "rgb(29, 69, 76)" }, | |
{ title: "2", color: "rgb(28, 66, 72)" }, | |
{ title: "3", color: "rgb(28, 66, 72)" }, | |
{ title: "4", color: "rgb(27, 63, 68)" }, | |
{ title: "5", color: "rgb(27, 63, 68)" }, | |
{ title: "6", color: "rgb(26, 60, 64)" }, | |
{ title: "7", color: "rgb(26, 60, 64)" }, | |
{ title: "8", color: "rgb(25, 57, 60)" }, | |
{ title: "9", color: "rgb(25, 57, 60)" }, | |
{ title: "10", color: "rgb(24, 54, 56)" }, | |
{ title: "11", color: "rgb(24, 54, 56)" }, | |
{ title: "12", color: "rgb(23, 51, 52)" }, | |
{ title: "13", color: "rgb(23, 51, 52)" }, | |
{ title: "14", color: "rgb(22, 48, 48)" }, | |
{ title: "15", color: "rgb(22, 48, 48)" }, | |
{ title: "16", color: "rgb(21, 45, 44)" }, | |
{ title: "17", color: "rgb(21, 45, 44)" }, | |
{ title: "18", color: "rgb(20, 42, 40)" }, | |
{ title: "19", color: "rgb(20, 42, 40)" }, | |
{ title: "20", color: "rgb(19, 39, 36)" }, | |
{ title: "21", color: "rgb(19, 39, 36)" }, | |
{ title: "22", color: "rgb(18, 36, 32)" }, | |
{ title: "23", color: "rgb(18, 36, 32)" }, | |
{ title: "24", color: "rgb(17, 33, 28)" }, | |
{ title: "25", color: "rgb(17, 33, 28)" }, | |
{ title: "26", color: "rgb(16, 30, 24)" }, | |
{ title: "27", color: "rgb(16, 30, 24)" }, | |
{ title: "28", color: "rgb(15, 27, 20)" }, | |
{ title: "29", color: "rgb(15, 27, 20)" }, | |
{ title: "30", color: "rgb(14, 24, 16)" }, | |
{ title: "31", color: "rgb(14, 24, 16)" }, | |
{ title: "32", color: "rgb(13, 21, 12)" }, | |
{ title: "33", color: "rgb(13, 21, 12)" }, | |
{ title: "34", color: "rgb(12, 18, 8)" }, | |
{ title: "35", color: "rgb(12, 18, 8)" }, | |
{ title: "36", color: "rgb(11, 15, 4)" }, | |
{ title: "37", color: "rgb(11, 15, 4)" }, | |
{ title: "38", color: "rgb(10, 12, 0)" }, | |
{ title: "39", color: "rgb(10, 12, 0)" }, | |
]; | |
const { sqrt, min, pow, abs } = Math; | |
const DIRECTIONS: [number, number][] = [ | |
[1, 0], | |
[0.5, sqrt(3) / 2], | |
[-0.5, sqrt(3) / 2], | |
[-1, 0], | |
[-0.5, -sqrt(3) / 2], | |
[0.5, -sqrt(3) / 2], | |
]; | |
const generateCirclePositionsFixed = ( | |
radius: number, | |
numCircles: number | |
): Array<[number, number]> => { | |
const positions: Array<[number, number]> = [[0, 0]]; // Start with the center circle | |
let n = 1; | |
while (positions.length < numCircles) { | |
for (let i = 0; i < 6; i++) { | |
for (let j = 0; j < n; j++) { | |
if (positions.length >= numCircles) break; | |
const x = | |
n * radius * 2 * DIRECTIONS[i][0] - | |
j * radius * 2 * (DIRECTIONS[i][0] - DIRECTIONS[(i + 1) % 6][0]); | |
const y = | |
n * radius * 2 * DIRECTIONS[i][1] - | |
j * radius * 2 * (DIRECTIONS[i][1] - DIRECTIONS[(i + 1) % 6][1]); | |
positions.push([x, y]); | |
} | |
} | |
n += 1; | |
} | |
return positions; | |
}; | |
const radius = 95; | |
const containerRect = 200; | |
const Item: React.FC<{ | |
coord: [number, number]; | |
offset: [SpringValue<number>, SpringValue<number>]; | |
color: string; | |
title: string; | |
}> = ({ coord, offset, color, title }) => { | |
const [x, y] = [ | |
offset[0].to((v) => v + coord[0]), | |
offset[1].to((v) => v + coord[1]), | |
]; | |
const distanceFromCenter = useMemo( | |
() => to([x, y], (x, y) => sqrt(pow(x, 2) + pow(y, 2))), | |
[x, y] | |
); | |
const itemRef = useRef<HTMLDivElement>(null); | |
const distanceFromContainerRect = to([x, y], (x, y) => | |
min(containerRect - abs(y), containerRect - abs(x)) | |
); | |
return ( | |
<animated.div | |
ref={itemRef} | |
className="absolute rounded-[50%] text-yellow-50 text-3xl flex justify-center items-center pointer-events-none" | |
style={{ | |
backgroundColor: color, | |
width: radius, | |
height: radius, | |
display: distanceFromContainerRect.to((v) => | |
v < 0 ? "none" : "unset" | |
), | |
left: x.to((v) => `calc(50% - ${v}px)`), | |
top: y.to((v) => `calc(50% - ${v}px)`), | |
transform: to( | |
[distanceFromContainerRect, distanceFromCenter], | |
(distanceFromContainerRect, distanceFromCenter) => | |
`translate(-50%, -50%) scale(${min( | |
(distanceFromContainerRect / radius) * 200, | |
radius - distanceFromCenter / 5 | |
)}%)` | |
), | |
}} | |
> | |
<span className="w-full h-full flex justify-center items-center"> | |
{title} | |
</span> | |
</animated.div> | |
); | |
}; | |
function App() { | |
const coords = useMemo( | |
() => generateCirclePositionsFixed(radius / 2, GOODS.length), | |
[] | |
); | |
const [isHold, setIsHold] = useState(false); | |
const offsetToRef = useRef([0, 0]); | |
const offsetX = useSpringValue(0); | |
const offsetY = useSpringValue(0); | |
return ( | |
<div | |
className="w-screen h-screen" | |
onMouseDown={() => setIsHold(true)} | |
onMouseUp={() => setIsHold(false)} | |
onMouseMove={(evt) => { | |
if (!isHold) return; | |
offsetToRef.current[0] -= evt.movementX; | |
offsetToRef.current[1] -= evt.movementY; | |
offsetX.start(offsetToRef.current[0]); | |
offsetY.start(offsetToRef.current[1]); | |
}} | |
> | |
<main className="relative rounded-lg w-[420px] h-[420px] bg-slate-200 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2"> | |
{GOODS.map(({ title, color }, idx) => ( | |
<Item | |
title={title} | |
color={color} | |
key={idx} | |
coord={coords[idx]} | |
offset={[offsetX, offsetY]} | |
/> | |
))} | |
</main> | |
</div> | |
); | |
} | |
export default App; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
deps: react tailwind @react-spring/web