Skip to content

Instantly share code, notes, and snippets.

@littensy
Created August 5, 2023 04:52
Show Gist options
  • Save littensy/9e99f07aac1c77b967ab093399b22bba to your computer and use it in GitHub Desktop.
Save littensy/9e99f07aac1c77b967ab093399b22bba to your computer and use it in GitHub Desktop.
🔥 Mock rem units in React-TS
import { map, useDebounceState, useViewport } from "@rbxts/pretty-react-hooks";
import Roact, { createContext } from "@rbxts/roact";
export interface RemProviderProps extends Roact.PropsWithChildren {
baseRem?: number;
remOverride?: number;
minimumRem?: number;
maximumRem?: number;
}
export const DEFAULT_REM = 16;
export const MIN_REM = 8;
const BASE_RESOLUTION = new Vector2(1920, 1020);
const MAX_ASPECT_RATIO = 19 / 9;
export const RemContext = createContext<number>(DEFAULT_REM);
export function RemProvider({
baseRem = DEFAULT_REM,
minimumRem = MIN_REM,
maximumRem = math.huge,
remOverride,
children,
}: RemProviderProps) {
const [rem, setRem] = useDebounceState(DEFAULT_REM, { wait: 0.5, leading: true });
useViewport((viewport: Vector2) => {
if (remOverride !== undefined) {
return remOverride;
}
// wide screens should not scale beyond iPhone aspect ratio
const resolution = new Vector2(math.min(viewport.X, viewport.Y * MAX_ASPECT_RATIO), viewport.Y);
const scale = resolution.Magnitude / BASE_RESOLUTION.Magnitude;
const desktop = resolution.X > resolution.Y || scale >= 1;
// portrait mode should downscale slower than landscape
const factor = desktop ? scale : map(scale, 0, 1, 0.25, 1);
setRem(math.clamp(math.round(baseRem * factor), minimumRem, maximumRem));
});
return <RemContext.Provider value={rem}>{children}</RemContext.Provider>;
}
import { useCallback, useContext } from "@rbxts/roact";
import { DEFAULT_REM, RemContext } from "../providers/rem-provider";
export interface RemOptions {
minimum?: number;
maximum?: number;
}
interface RemCallback {
(value: number, mode?: RemScaleMode): number;
(value: UDim2, mode?: RemScaleMode): UDim2;
(value: UDim, mode?: RemScaleMode): UDim;
(value: Vector2, mode?: RemScaleMode): Vector2;
}
type RemScaleMode = "relative" | "unit";
const scaleFunctions = {
number: (value: number, rem: number): number => {
return value * rem;
},
UDim2: (value: UDim2, rem: number): UDim2 => {
return new UDim2(value.X.Scale, value.X.Offset * rem, value.Y.Scale, value.Y.Offset * rem);
},
UDim: (value: UDim, rem: number): UDim => {
return new UDim(value.Scale, value.Offset * rem);
},
Vector2: (value: Vector2, rem: number): Vector2 => {
return new Vector2(value.X * rem, value.Y * rem);
},
};
function useRemContext({ minimum = 0, maximum = math.huge }: RemOptions = {}) {
const rem = useContext(RemContext);
return math.clamp(rem, minimum, maximum);
}
export function useRem(options?: RemOptions): RemCallback {
const rem = useRemContext(options);
return useCallback(
<T>(value: T, mode: RemScaleMode = "unit"): T => {
const scale = scaleFunctions[typeOf(value) as never] as <T>(value: T, rem: number) => T;
if (!scale) {
return value;
}
return mode === "unit" ? scale(value, rem) : scale(value, rem / DEFAULT_REM);
},
[rem],
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment