Skip to content

Commit

Permalink
overcomplicated score wheel update
Browse files Browse the repository at this point in the history
  • Loading branch information
danikova committed Dec 3, 2023
1 parent be1d6a8 commit 6b74350
Show file tree
Hide file tree
Showing 5 changed files with 242 additions and 30 deletions.
104 changes: 104 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"@radix-ui/react-toggle": "^1.0.3",
"@radix-ui/react-toggle-group": "^1.0.4",
"@radix-ui/react-tooltip": "^1.0.7",
"@react-spring/web": "^9.7.3",
"axios": "^1.6.2",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
Expand Down Expand Up @@ -58,4 +59,4 @@
"typescript": "^5.2.2",
"vite": "^5.0.0"
}
}
}
35 changes: 7 additions & 28 deletions src/components/counter.tsx
Original file line number Diff line number Diff line change
@@ -1,42 +1,18 @@
import { useMemo } from "react";
import { DateTime } from "luxon";
import { cn } from "@/lib/utils";
import { ScoreWheel } from "./score-wheel";
import { useFormData, useRemainingTime } from "@/lib/hooks";
import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";

const numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];

function CounterDisplaySingleDigit({ num }: { num: number }) {
const offset = useMemo(() => {
if (num < 0) return 0;
else if (num > 9) return 9;
return num;
}, [num]);

const getNumber = (n: number) => (
<div key={n} className="w-[45px] h-[5rem] flex justify-center items-center">
<span className="text-[5rem]">{n}</span>
</div>
);

return (
<div className="w-[45px] h-[5rem] flex flex-col overflow-hidden">
<div
className="transition-[margin] duration-500"
style={{ marginTop: `calc(${-offset} * 5rem)` }}
>
{numbers.map((n) => getNumber(n))}
</div>
</div>
);
}

function CounterDisplayNumber({
num,
max,
tooltip,
className,
}: {
num: number;
max?: number[];
tooltip?: string;
className?: string;
}) {
Expand All @@ -53,7 +29,7 @@ function CounterDisplayNumber({
<TooltipTrigger disabled={!tooltip} asChild>
<div className={cn("flex", className)}>
{nums.map((num, i) => (
<CounterDisplaySingleDigit key={`${i}`} num={num} />
<ScoreWheel key={`${i}`} num={num} max={max ? max[i] : 9} />
))}
</div>
</TooltipTrigger>
Expand Down Expand Up @@ -104,6 +80,7 @@ export function Counter() {
<CounterDisplayNumber
key="h"
num={remaining.hours}
max={[2, 9]}
tooltip={`${isTMinus ? "Remaining" : "Elapsed"} hours`}
className="px-2"
/>
Expand All @@ -113,6 +90,7 @@ export function Counter() {
<CounterDisplayNumber
key="m"
num={remaining.minutes}
max={[5, 9]}
tooltip={`${isTMinus ? "Remaining" : "Elapsed"} minutes`}
className="px-2"
/>
Expand All @@ -122,6 +100,7 @@ export function Counter() {
<CounterDisplayNumber
key="s"
num={remaining.seconds}
max={[5, 9]}
tooltip={`${isTMinus ? "Remaining" : "Elapsed"} seconds`}
className="px-2"
/>
Expand Down
120 changes: 120 additions & 0 deletions src/components/score-wheel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { usePrev } from "../lib/hooks";
import { useEffect, useMemo } from "react";
import { animated, useSpring, to } from "@react-spring/web";

const OFFSET = -0.1;
const ITEM_HEIGHT = 80;
/* keep ~80x45 aspect ratio */
const ITEM_WIDTH = ITEM_HEIGHT * 0.56;

type RotationMap = {
[k in number]: {
spawnPosition: number;
position: number;
};
};

function getParams(itemCount: number) {
const itemAngle = 360 / itemCount;

const alpha = itemAngle / 2;
const a = ITEM_HEIGHT / 2;
const c = a / Math.sin((alpha * Math.PI) / 180);
const b = Math.sqrt(c * c - a * a);

return {
itemCount,
angle: itemAngle,
diameter: b * 2 + OFFSET,
radius: b + OFFSET,
};
}

function calculateRotationDelta(fromAngle: number, toAngle: number) {
const normalizedFrom = ((fromAngle % 360) + 360) % 360;
const normalizedTo = ((toAngle % 360) + 360) % 360;

const clockwiseDiff = (normalizedTo - normalizedFrom + 360) % 360;
const counterclockwiseDiff = (normalizedFrom - normalizedTo + 360) % 360;

return clockwiseDiff < counterclockwiseDiff
? clockwiseDiff
: -counterclockwiseDiff;
}

export function ScoreWheel({
num: nextNum,
max = 9,
}: {
num: number;
max?: number;
}) {
nextNum = Math.min(Math.max(nextNum, 0), max);
const params = useMemo(() => getParams(max + 1), [max]);
const rotationMap = useMemo(() => {
const nums = Array.from({ length: params.itemCount }).map((_, i) => i);
const _rotationMap: RotationMap = {};
for (let i = 0; i < nums.length; i++) {
const num = nums[i];
_rotationMap[num] = {
spawnPosition: params.angle * (nums.length - i),
position: params.angle * i,
};
}
return _rotationMap;
}, [params]);
const num = usePrev(nextNum) ?? 0;

const [styles, api] = useSpring(() => ({ rotateX: 0 }), []);

useEffect(() => {
if (num !== nextNum) {
const numAngle = rotationMap[num].position;
const nextNumAngle = rotationMap[nextNum].position;
const rotationDelta = calculateRotationDelta(numAngle, nextNumAngle);
api.start({
from: { rotateX: numAngle },
to: { rotateX: numAngle + rotationDelta },
});
}
}, [num, nextNum, api, rotationMap]);

return (
<div
className="flex justify-center items-center box-border overflow-hidden"
style={{
height: ITEM_HEIGHT,
width: ITEM_WIDTH,
}}
>
<div>
<animated.div
className="relative [transform-style:preserve-3d] box-border"
style={{
transformOrigin: `50% calc(50% + ${ITEM_HEIGHT / 2}px)`,
marginTop: `-${ITEM_HEIGHT}px`,
height: ITEM_HEIGHT,
width: ITEM_WIDTH,
transform: to([styles.rotateX], (x) => {
return `rotateX(${x}deg)`;
}),
}}
>
{Object.entries(rotationMap).map(([num, rotation]) => (
<div
key={`${num}`}
className="flex justify-center items-center h-[100%] w-[100%] text-[5rem] absolute top-[50%] box-border [backface-visibility:hidden]"
style={{
transform: `rotateX(${rotation.spawnPosition}deg) translateZ(${params.radius}px)`,
height: ITEM_HEIGHT,
width: ITEM_WIDTH,
}}
>
<span>{num}</span>
</div>
))}
</animated.div>
</div>
</div>
);
}
10 changes: 9 additions & 1 deletion src/lib/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { DateTime } from "luxon";
import { useSearchParams } from "react-router-dom";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";

export function useRemainingTime() {
const [{ endDate: targetDate }] = useFormData();
Expand Down Expand Up @@ -117,3 +117,11 @@ export function useFormData(): [FormData, (values: Partial<FormData>) => void] {

return [{ endDate, digits, title, imageId }, setData];
}

export function usePrev<T>(state: T): T | undefined {
const ref = useRef<T>();
useEffect(() => {
ref.current = state;
});
return ref.current;
}

0 comments on commit 6b74350

Please sign in to comment.