Skip to content

Instantly share code, notes, and snippets.

@Flaque
Created December 5, 2020 00:37
Show Gist options
  • Save Flaque/e56898ddcabd3d6bfd1d46533e19c518 to your computer and use it in GitHub Desktop.
Save Flaque/e56898ddcabd3d6bfd1d46533e19c518 to your computer and use it in GitHub Desktop.
import {
DetailedHTMLProps,
CanvasHTMLAttributes,
useLayoutEffect,
useEffect,
useRef,
createContext,
useContext,
Ref,
MutableRefObject,
useState,
} from "react";
import lerp from "lerp";
const CanvasReactContext = createContext<{
subscribe: (ref: Ref<any>) => any;
}>({
subscribe: () => {},
});
type FrameFn = (ctx: CanvasRenderingContext2D, delta: number) => void;
function useAnimationFrame(render: (delta: number) => any) {
// Use useRef for mutable variables that we want to persist
// without triggering a re-render on their change
const requestRef = useRef<any>();
const previousTimeRef = useRef<any>();
const animate = (time) => {
if (previousTimeRef.current != undefined) {
const deltaTime = time - previousTimeRef.current;
// Pass on a function to the setter of the state
// to make sure we always have the latest state
render(deltaTime);
}
previousTimeRef.current = time;
requestRef.current = requestAnimationFrame(animate);
};
useEffect(() => {
requestRef.current = requestAnimationFrame(animate);
return () => cancelAnimationFrame(requestRef.current);
}, []); // Make sure the effect runs only once
}
export function useFrame(fn: FrameFn) {
const canvas = useContext(CanvasReactContext);
const ref = useRef<FrameFn>(fn);
useLayoutEffect(() => void (ref.current = fn), [fn]);
useEffect(() => {
const unsubscribe = canvas.subscribe(ref);
return () => unsubscribe();
}, [fn]);
}
export function useLerp(oldValue: number) {
const ref = useRef<number>(oldValue);
function next(newValue: number, alpha: number) {
ref.current = lerp(ref.current, newValue, alpha);
return ref.current;
}
return next;
}
export function Canvas(
props: DetailedHTMLProps<
CanvasHTMLAttributes<HTMLCanvasElement>,
HTMLCanvasElement
>
) {
const canvasRef = useRef<HTMLCanvasElement>();
const internal = useRef<{
subscribers: Array<MutableRefObject<FrameFn>>;
}>({
subscribers: [],
});
function renderFrame(delta: number) {
const ctx = canvasRef.current.getContext("2d") as CanvasRenderingContext2D;
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
for (let fn of internal.current.subscribers) {
fn.current(ctx, delta);
}
}
function subscribe(ref: MutableRefObject<FrameFn>) {
internal.current.subscribers.push(ref);
return () => {
if (internal.current?.subscribers) {
internal.current.subscribers = internal.current.subscribers.filter(
(s) => s !== ref
);
}
};
}
useAnimationFrame(renderFrame);
return (
<CanvasReactContext.Provider value={{ subscribe }}>
<canvas {...props} ref={canvasRef} />
{props.children}
</CanvasReactContext.Provider>
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment