Skip to content

Instantly share code, notes, and snippets.

@littensy
Created June 29, 2023 03:38
Show Gist options
  • Save littensy/d5ae1123a37daaffa6198f4e92ddf956 to your computer and use it in GitHub Desktop.
Save littensy/d5ae1123a37daaffa6198f4e92ddf956 to your computer and use it in GitHub Desktop.
A bunch of components I like to use often
import { useBindingListener, useCamera } from "@rbxts/pretty-roact-hooks";
import Roact from "@rbxts/roact";
import { useState } from "@rbxts/roact-hooked";
interface BackgroundBlurProps {
blurSize?: number | Roact.Binding<number>;
}
/**
* Wraps a BlurEffect
*/
export function BackgroundBlur({ blurSize }: BackgroundBlurProps) {
const camera = useCamera();
const [visible, setVisible] = useState(false);
useBindingListener(blurSize, (size = 0) => {
setVisible(size > 0);
});
return <Roact.Portal target={camera}>{visible && <blureffect Size={blurSize} />}</Roact.Portal>;
}
import { mapBinding, useBindingState } from "@rbxts/pretty-roact-hooks";
import Roact from "@rbxts/roact";
import { useEffect, useMemo, useState } from "@rbxts/roact-hooked";
import { CanvasGroup, CanvasGroupProps } from "./canvas-group";
import { Frame } from "./frame";
import { Group } from "./group";
type CanvasOrFrameProps = CanvasGroupProps & {
ref?: Roact.RefPropertyOrFunction<Frame | CanvasGroup>;
event?: Roact.JsxInstanceEvents<Frame | CanvasGroup>;
change?: Roact.JsxInstanceChangeEvents<Frame | CanvasGroup>;
directChildren?: Roact.Element;
};
const EPSILON = 0.03;
/**
* Render a CanvasGroup when animating GroupTransparency, render
* a Frame when it's zero or close to zero. Great for transitions
* that don't need to permanently use a CanvasGroup.
*/
export function CanvasOrFrame(props: CanvasOrFrameProps) {
const propsWithoutChildren = {
...props,
[Roact.Children]: undefined,
};
const isCanvasBinding = useMemo(() => {
return mapBinding(props.groupTransparency, (t = 0) => t > EPSILON);
}, [props.groupTransparency]);
const isCanvas = useBindingState(isCanvasBinding);
const [canvas, canvasRef] = useState<CanvasGroup>();
const [frame, frameRef] = useState<Frame>();
const portalTarget = useMemo(() => {
// Hack to avoid changing the 'target' prop of Portal, since
// that forces children to re-mount, which is bad
const frame = new Instance("Frame");
frame.Name = "smart-canvas-target";
frame.Size = UDim2.fromScale(1, 1);
frame.BackgroundTransparency = 1;
return frame;
}, []);
useEffect(() => {
portalTarget.Parent = isCanvas ? canvas : frame;
}, [isCanvas, canvas, frame]);
useEffect(() => {
return () => {
portalTarget.Destroy();
};
}, []);
return (
<>
<Roact.Portal Key="portal" target={portalTarget}>
{props[Roact.Children]}
</Roact.Portal>
<Group>
<CanvasGroup {...propsWithoutChildren} visible={isCanvas} ref={canvasRef}>
{props.directChildren}
</CanvasGroup>
<Frame {...propsWithoutChildren} visible={!isCanvas} ref={frameRef}>
{props.directChildren}
</Frame>
</Group>
</>
);
}
import Roact from "@rbxts/roact";
import { useEffect, useState } from "@rbxts/roact-hooked";
import { setTimeout } from "@rbxts/set-timeout";
interface DelayRenderProps extends Roact.PropsWithChildren {
shouldRender: boolean;
mountDelay?: number;
unmountDelay?: number;
}
/**
* Delay the mount or unmount of a component with a prop
*/
export function DelayRender({
shouldRender,
mountDelay = 0,
unmountDelay = 0,
[Roact.Children]: children,
}: DelayRenderProps) {
const [render, setRender] = useState(mountDelay === 0 ? shouldRender : false);
useEffect(() => {
if (shouldRender) {
return setTimeout(() => setRender(true), mountDelay);
} else {
return setTimeout(() => setRender(false), unmountDelay);
}
}, [shouldRender]);
return <>{render && children}</>;
}
import { useComposedRef, useDeferState } from "@rbxts/pretty-roact-hooks";
import Roact from "@rbxts/roact";
import { useEffect, useState } from "@rbxts/roact-hooked";
import { Group } from "./group";
interface RenderInViewProps extends Roact.PropsWithChildren {
ref?: (rbx?: Frame) => void;
change?: Roact.JsxInstanceChangeEvents<Frame>;
event?: Roact.JsxInstanceEvents<Frame>;
container?: GuiObject;
containerMargin?: Vector2;
size?: UDim2 | Roact.Binding<UDim2>;
position?: UDim2 | Roact.Binding<UDim2>;
anchorPoint?: Vector2 | Roact.Binding<Vector2>;
zIndex?: number | Roact.Binding<number>;
layoutOrder?: number | Roact.Binding<number>;
}
/**
* Unmount the children if this invisible container frame is
* outside of the `container` object's bounds plus the margin
*/
export function RenderInView({
ref,
change = {},
event = {},
container,
containerMargin = Vector2.zero,
size,
position,
anchorPoint,
zIndex,
layoutOrder,
[Roact.Children]: children,
}: RenderInViewProps) {
const [frame, setFrame] = useState<Frame>();
const [shouldRender, setShouldRender] = useDeferState(false);
useEffect(() => {
if (!frame || !container) return;
// Set shouldRender to 'true' if any part of the frame is inside the container
const updateShouldRender = () => {
const framePosition = frame.AbsolutePosition;
const frameSize = frame.AbsoluteSize;
const containerPosition = container.AbsolutePosition.sub(containerMargin.div(2));
const containerSize = container.AbsoluteSize.add(containerMargin);
const inFrame =
framePosition.X + frameSize.X > containerPosition.X &&
framePosition.X < containerPosition.X + containerSize.X &&
framePosition.Y + frameSize.Y > containerPosition.Y &&
framePosition.Y < containerPosition.Y + containerSize.Y;
setShouldRender(inFrame);
};
const connections = [
frame.GetPropertyChangedSignal("AbsolutePosition").Connect(updateShouldRender),
frame.GetPropertyChangedSignal("AbsoluteSize").Connect(updateShouldRender),
container.GetPropertyChangedSignal("AbsolutePosition").Connect(updateShouldRender),
container.GetPropertyChangedSignal("AbsoluteSize").Connect(updateShouldRender),
];
updateShouldRender();
return () => {
for (const connection of connections) {
connection.Disconnect();
}
};
}, [frame, container, containerMargin]);
return (
<Group
ref={useComposedRef(setFrame, ref)}
change={change}
event={event}
size={size}
position={position}
anchorPoint={anchorPoint}
zIndex={zIndex}
layoutOrder={layoutOrder}
>
{shouldRender && children}
</Group>
);
}
import Roact from "@rbxts/roact";
import { IS_EDIT } from "shared/utils/constants";
import { Group } from "./group";
interface RootProps extends Roact.PropsWithChildren {
displayOrder?: number;
}
/**
* Create a ScreenGui if this is a live game, otherwise render
* an invisible container frame
* `IS_EDIT = RunService.IsStudio() && !RunService.IsRunning()`
*/
export function Root({ displayOrder, [Roact.Children]: children }: RootProps) {
return IS_EDIT ? (
<Group zIndex={displayOrder}>{children}</Group>
) : (
<screengui ResetOnSpawn={false} DisplayOrder={displayOrder} IgnoreGuiInset ZIndexBehavior="Sibling">
{children}
</screengui>
);
}
import { Linear, Spring, useMotor, useProperty } from "@rbxts/pretty-roact-hooks";
import Roact from "@rbxts/roact";
import { useEffect } from "@rbxts/roact-hooked";
import { setInterval } from "@rbxts/set-timeout";
import { Dictionary } from "@rbxts/sift";
import { useRem } from "client/app/hooks";
import { Text, TextProps } from "./text";
interface TextTruncatedProps extends TextProps {}
const GRADIENT = new NumberSequence([
new NumberSequenceKeypoint(0, 0),
new NumberSequenceKeypoint(0.75, 0),
new NumberSequenceKeypoint(1, 1),
]);
/**
* Blurs out the right-edge of a text label and adds automatic
* scrolling to view the entire string, like how some music apps
* scroll the title of a song if it's too long
*/
export function TextTruncated(props: TextTruncatedProps) {
const textProps = Dictionary.removeKeys(props, Roact.Children);
const rem = useRem();
const [bounds = Vector2.one, size = Vector2.one, change] = useProperty("TextBounds", "AbsoluteSize");
const [offset, setOffset] = useMotor(0);
const distance = size.X - bounds.X - 2 * rem;
useEffect(() => {
setOffset(new Spring(0));
if (bounds.X < size.X + rem) {
return;
}
let toggle = false;
return setInterval(() => {
toggle = !toggle;
if (toggle) {
setOffset(new Linear(distance, { velocity: 5 * rem }));
} else {
setOffset(new Linear(0, { velocity: 5 * rem }));
}
}, 5);
}, [bounds, size, rem]);
return (
<Text {...textProps} clipsDescendants change={{ ...props.change, ...change }}>
<uigradient Key="truncate-gradient" Transparency={GRADIENT} />
<uipadding Key="truncate-offset" PaddingLeft={offset.map((x) => new UDim(0, math.round(x)))} />
{props[Roact.Children]}
</Text>
);
}
import Roact from "@rbxts/roact";
import { useState } from "@rbxts/roact-hooked";
import { FrameProps } from "./frame";
interface ViewportFrameProps extends FrameProps<ViewportFrame> {
camera?: CFrame | Roact.Binding<CFrame>;
fieldOfView?: number | Roact.Binding<number>;
ambient?: Color3 | Roact.Binding<Color3>;
lightColor?: Color3 | Roact.Binding<Color3>;
lightDirection?: Vector3 | Roact.Binding<Vector3>;
imageColor?: Color3 | Roact.Binding<Color3>;
imageTransparency?: number | Roact.Binding<number>;
useWorldModel?: boolean;
}
/**
* Make working with viewport frames a bit easier
*/
export function ViewportFrame(props: ViewportFrameProps) {
const [currentCamera, setCurrentCamera] = useState<Camera>();
return (
<viewportframe
Ref={props.ref}
CurrentCamera={currentCamera}
Ambient={props.ambient}
LightColor={props.lightColor}
LightDirection={props.lightDirection}
ImageColor3={props.imageColor}
ImageTransparency={props.imageTransparency}
Size={props.size}
Position={props.position}
AnchorPoint={props.anchorPoint}
BackgroundColor3={props.backgroundColor}
BackgroundTransparency={props.backgroundTransparency}
ClipsDescendants={props.clipsDescendants}
Visible={props.visible}
ZIndex={props.zIndex}
LayoutOrder={props.layoutOrder}
BorderSizePixel={0}
Event={props.event || {}}
Change={props.change || {}}
>
<camera Ref={setCurrentCamera} CFrame={props.camera} FieldOfView={props.fieldOfView} />
{props.cornerRadius && <uicorner CornerRadius={props.cornerRadius} />}
{props.useWorldModel ? <worldmodel>{props[Roact.Children]}</worldmodel> : <>{props[Roact.Children]}</>}
</viewportframe>
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment