216 lines
5.8 KiB
TypeScript
216 lines
5.8 KiB
TypeScript
import type { ComponentPropsWithoutRef, ReactNode } from "react";
|
|
import { Fragment, createContext, useCallback, useContext, useRef, useState } from "react";
|
|
import { inverseLerp, lerp } from "~/utils/lerp";
|
|
|
|
interface MousePosition {
|
|
x: number;
|
|
y: number;
|
|
}
|
|
const MousePositionContext = createContext<MousePosition | undefined>(undefined);
|
|
export function MousePositionProvider({ children }: { children: ReactNode }) {
|
|
const ref = useRef<HTMLDivElement>(null);
|
|
const [position, setPosition] = useState<MousePosition | undefined>(undefined);
|
|
|
|
const handleMouseMove = useCallback(
|
|
(e: React.MouseEvent) => {
|
|
if (!ref.current) {
|
|
setPosition(undefined);
|
|
return;
|
|
}
|
|
|
|
const { top, left, width, height } = ref.current.getBoundingClientRect();
|
|
const x = (e.clientX - left) / width;
|
|
const y = (e.clientY - top) / height;
|
|
|
|
if (x < 0 || x > 1 || y < 0 || y > 1) {
|
|
setPosition(undefined);
|
|
return;
|
|
}
|
|
|
|
setPosition({ x, y });
|
|
},
|
|
[ref.current]
|
|
);
|
|
|
|
return (
|
|
<div
|
|
ref={ref}
|
|
onMouseEnter={handleMouseMove}
|
|
onMouseLeave={() => setPosition(undefined)}
|
|
onMouseMove={handleMouseMove}
|
|
style={{ width: "100%", height: "100%" }}
|
|
>
|
|
<MousePositionContext.Provider value={position}>{children}</MousePositionContext.Provider>
|
|
</div>
|
|
);
|
|
}
|
|
export const useMousePosition = () => {
|
|
return useContext(MousePositionContext);
|
|
};
|
|
|
|
type TimelineContextState = {
|
|
startMs: number;
|
|
durationMs: number;
|
|
};
|
|
const TimelineContext = createContext<TimelineContextState>({} as TimelineContextState);
|
|
|
|
function useTimeline() {
|
|
return useContext(TimelineContext);
|
|
}
|
|
|
|
type TimelineMousePositionContextState = { x: number; y: number } | undefined;
|
|
const _TimelineMousePositionContext = createContext<TimelineMousePositionContextState>(undefined);
|
|
export type RootProps = {
|
|
/** If the timeline doesn't start at zero. Doesn't impact layout but gives you the times back */
|
|
startMs?: number;
|
|
durationMs: number;
|
|
/** A number between 0 and 1, determines the width between min and max */
|
|
scale: number;
|
|
minWidth: number;
|
|
maxWidth: number;
|
|
children?: ReactNode;
|
|
className?: string;
|
|
};
|
|
|
|
/** The main element that determines the dimensions for all sub-elements */
|
|
export function Root({
|
|
startMs = 0,
|
|
durationMs,
|
|
scale,
|
|
minWidth,
|
|
maxWidth,
|
|
children,
|
|
className,
|
|
}: RootProps) {
|
|
const pixelWidth = calculatePixelWidth(minWidth, maxWidth, scale);
|
|
|
|
return (
|
|
<TimelineContext.Provider value={{ startMs, durationMs }}>
|
|
<div
|
|
className={className}
|
|
style={{
|
|
position: "relative",
|
|
width: `${pixelWidth}px`,
|
|
}}
|
|
>
|
|
<MousePositionProvider>{children}</MousePositionProvider>
|
|
</div>
|
|
</TimelineContext.Provider>
|
|
);
|
|
}
|
|
|
|
export type RowProps = ComponentPropsWithoutRef<"div">;
|
|
|
|
/** This simply acts as a container, with position relative.
|
|
* This allows you to nest "Rows" and put heights on them */
|
|
export function Row({ className, children, ...props }: RowProps) {
|
|
return (
|
|
<div {...props} className={className} style={{ ...props.style, position: "relative" }}>
|
|
{children}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export type PointProps = {
|
|
ms: number;
|
|
className?: string;
|
|
children?: (ms: number) => ReactNode;
|
|
};
|
|
|
|
/** A point in time, it has no duration */
|
|
export function Point({ ms, className, children }: PointProps) {
|
|
const { startMs, durationMs } = useTimeline();
|
|
const position = inverseLerp(startMs, startMs + durationMs, ms);
|
|
return (
|
|
<div
|
|
className={className}
|
|
style={{
|
|
position: "absolute",
|
|
left: `${position * 100}%`,
|
|
}}
|
|
>
|
|
{children && children(ms)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export type SpanProps = {
|
|
startMs: number;
|
|
durationMs: number;
|
|
className?: string;
|
|
children?: ReactNode;
|
|
};
|
|
|
|
/** As span of time with a start and duration */
|
|
export function Span({ startMs, durationMs, className, children }: SpanProps) {
|
|
const { startMs: rootStartMs, durationMs: rootDurationMs } = useTimeline();
|
|
const position = inverseLerp(rootStartMs, rootStartMs + rootDurationMs, startMs);
|
|
const width =
|
|
inverseLerp(rootStartMs, rootStartMs + rootDurationMs, startMs + durationMs) - position;
|
|
return (
|
|
<div
|
|
className={className}
|
|
style={{
|
|
position: "absolute",
|
|
left: `${position * 100}%`,
|
|
width: `${width * 100}%`,
|
|
}}
|
|
>
|
|
{children}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export type EquallyDistributeProps = {
|
|
count: number;
|
|
children: (ms: number, index: number) => ReactNode;
|
|
};
|
|
|
|
/** Render a child equally distributed across the duration */
|
|
export function EquallyDistribute({ count, children }: EquallyDistributeProps) {
|
|
const { startMs, durationMs } = useTimeline();
|
|
|
|
return (
|
|
<>
|
|
{Array.from({ length: count }).map((_, index) => {
|
|
const ms = startMs + (durationMs / (count - 1)) * index;
|
|
return <Fragment key={index}>{children(ms, index)}</Fragment>;
|
|
})}
|
|
</>
|
|
);
|
|
}
|
|
|
|
export type FollowCursorProps = {
|
|
children: (ms: number) => ReactNode;
|
|
};
|
|
|
|
/** Renders a child that follows the cursor */
|
|
export function FollowCursor({ children }: FollowCursorProps) {
|
|
const { startMs, durationMs } = useTimeline();
|
|
const relativeMousePosition = useMousePosition();
|
|
const ms = relativeMousePosition?.x
|
|
? lerp(startMs, startMs + durationMs, relativeMousePosition.x)
|
|
: undefined;
|
|
|
|
if (ms === undefined) return null;
|
|
|
|
return (
|
|
<div
|
|
style={{
|
|
position: "absolute",
|
|
top: 0,
|
|
left: relativeMousePosition ? `${relativeMousePosition?.x * 100}%` : 0,
|
|
height: "100%",
|
|
pointerEvents: "none",
|
|
}}
|
|
>
|
|
{children(ms)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/** Gives the total width of the root */
|
|
function calculatePixelWidth(minWidth: number, maxWidth: number, scale: number) {
|
|
return lerp(minWidth, maxWidth, scale);
|
|
}
|