Skip to content

Instantly share code, notes, and snippets.

@1216892614
Created August 18, 2024 07:13
Show Gist options
  • Save 1216892614/69bc42c20b1a5d45ee06b812d21cf08d to your computer and use it in GitHub Desktop.
Save 1216892614/69bc42c20b1a5d45ee06b812d21cf08d to your computer and use it in GitHub Desktop.
Apple Watch Menu like
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;
@1216892614
Copy link
Author

deps: react tailwind @react-spring/web

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment