Skip to content

Instantly share code, notes, and snippets.

@littensy
Last active February 15, 2024 05:18
Show Gist options
  • Save littensy/448afc110479fc9aab1deff8f97003f5 to your computer and use it in GitHub Desktop.
Save littensy/448afc110479fc9aab1deff8f97003f5 to your computer and use it in GitHub Desktop.
Roblox-TS React background blur component
import { useCamera, useEventListener } from "@rbxts/pretty-react-hooks";
import Roact, { Portal, useRef, useState } from "@rbxts/roact";
import { GuiService, RunService } from "@rbxts/services";
import { useParentRadius } from "client/hooks/use-parent-radius";
import { usePx } from "client/hooks/use-px";
import { useQualityLevel } from "client/hooks/use-quality-level";
const DISTANCE = 0.005;
const BLUR_QUALITY_LEVEL = 8;
const CYLINDER_ANGLE = CFrame.Angles(math.rad(90), 0, 0);
const [GUI_INSET] = GuiService.GetGuiInset();
const partProps: Partial<InstanceProperties<Part>> = {
Anchored: true,
CanCollide: false,
CanQuery: false,
CanTouch: false,
CastShadow: false,
Material: Enum.Material.Glass,
Transparency: 0.999,
};
function insetContainer(size: Vector2, position: Vector2, inset: number) {
const paddedSize = size.add(new Vector2(-inset, -inset));
const paddedPosition = position.sub(new Vector2(-inset / 2, -inset / 2));
return $tuple(paddedSize, paddedPosition);
}
interface BackgroundBlurProps {
roundedCorners?: boolean;
enabled?: boolean;
}
export function BackgroundBlur({ roundedCorners = false, enabled = true }: BackgroundBlurProps) {
const px = usePx();
const camera = useCamera();
const [model, setModel] = useState<Model>();
const [container, setContainer] = useState<Frame>();
const shouldRender = useQualityLevel() >= BLUR_QUALITY_LEVEL && enabled;
const cornerRadius = useParentRadius(roundedCorners ? container : undefined);
const scheduled = useRef(false);
const cameraCFrame = useRef(camera.CFrame);
// Vertical and horizontal parts to fill space between corners
const [vertical, setVertical] = useState<Part>();
const [horizontal, setHorizontal] = useState<Part>();
// Cylinder parts to create rounded corners
const [corner0, setCorner0] = useState<Part>();
const [corner1, setCorner1] = useState<Part>();
const [corner2, setCorner2] = useState<Part>();
const [corner3, setCorner3] = useState<Part>();
const active = container && model && vertical && horizontal && corner0 && corner1 && corner2 && corner3;
const getPointInWorldSpace = (point: Vector2) => {
const ray = camera.ViewportPointToRay(point.X, point.Y);
return ray.Origin.add(ray.Direction.mul(DISTANCE));
};
const getPointsInWorldSpace = () => {
assert(active, "BackgroundBlur is not active");
const [size, position] = insetContainer(
container.AbsoluteSize,
container.AbsolutePosition.add(GUI_INSET),
px(22),
);
const radiusPixels = cornerRadius.Offset + cornerRadius.Scale * math.min(size.X, size.Y) - px(11);
const getCornerDimensions = (cornerUnit: Vector2, radiusDirection: number) => {
if (radiusPixels <= 0) {
return {
position: getPointInWorldSpace(position.add(size.mul(cornerUnit))),
radius: 0,
};
}
const cornerPoint = position.add(size.mul(cornerUnit));
const cornerPosition = getPointInWorldSpace(cornerPoint);
const radiusOffset = getPointInWorldSpace(cornerPoint.add(new Vector2(radiusPixels * radiusDirection, 0)));
return {
position: cornerPosition,
radius: radiusOffset.sub(cornerPosition).Magnitude,
};
};
return {
topLeft: getCornerDimensions(new Vector2(0, 0), 1),
topRight: getCornerDimensions(new Vector2(1, 0), -1),
bottomLeft: getCornerDimensions(new Vector2(0, 1), 1),
bottomRight: getCornerDimensions(new Vector2(1, 1), -1),
center: getPointInWorldSpace(position.add(size.div(2))),
};
};
const update = () => {
if (!active) return;
debug.profilebegin("BackgroundBlur.update");
const { topLeft, topRight, bottomLeft, bottomRight, center } = getPointsInWorldSpace();
const objectTopLeft = camera.CFrame.VectorToObjectSpace(topLeft.position);
const objectTopRight = camera.CFrame.VectorToObjectSpace(topRight.position);
const objectBottomLeft = camera.CFrame.VectorToObjectSpace(bottomLeft.position);
const width = math.abs(objectTopRight.X - objectTopLeft.X);
const height = math.abs(objectBottomLeft.Y - objectTopLeft.Y);
vertical.Size = new Vector3(width - topLeft.radius - topRight.radius, height, 0);
vertical.CFrame = CFrame.lookAlong(center, camera.CFrame.LookVector);
if (cornerRadius !== new UDim()) {
horizontal.Size = new Vector3(width, height - topLeft.radius - bottomLeft.radius, 0);
horizontal.CFrame = CFrame.lookAlong(center, camera.CFrame.LookVector);
corner0.Size = new Vector3(2 * topLeft.radius, 0, 2 * topLeft.radius);
corner0.CFrame = CFrame.lookAlong(topLeft.position, camera.CFrame.LookVector)
.mul(new CFrame(topLeft.radius, -topLeft.radius, 0))
.mul(CYLINDER_ANGLE);
corner1.Size = new Vector3(2 * topRight.radius, 0, 2 * topRight.radius);
corner1.CFrame = CFrame.lookAlong(topRight.position, camera.CFrame.LookVector)
.mul(new CFrame(-topRight.radius, -topRight.radius, 0))
.mul(CYLINDER_ANGLE);
corner2.Size = new Vector3(2 * bottomLeft.radius, 0, 2 * bottomLeft.radius);
corner2.CFrame = CFrame.lookAlong(bottomLeft.position, camera.CFrame.LookVector)
.mul(new CFrame(bottomLeft.radius, bottomLeft.radius, 0))
.mul(CYLINDER_ANGLE);
corner3.Size = new Vector3(2 * bottomRight.radius, 0, 2 * bottomRight.radius);
corner3.CFrame = CFrame.lookAlong(bottomRight.position, camera.CFrame.LookVector)
.mul(new CFrame(-bottomRight.radius, bottomRight.radius, 0))
.mul(CYLINDER_ANGLE);
}
// Apply rotation of the container
if (container.AbsoluteRotation !== 0) {
model.PrimaryPart = vertical;
model.PivotTo(model.GetPivot().mul(CFrame.Angles(0, 0, -math.rad(container.AbsoluteRotation))));
}
debug.profileend();
};
const scheduleUpdate = () => {
scheduled.current = true;
};
useEventListener(RunService.RenderStepped, () => {
if (scheduled.current || cameraCFrame.current !== camera.CFrame) {
scheduled.current = false;
cameraCFrame.current = camera.CFrame;
update();
}
});
useEventListener(camera.GetPropertyChangedSignal("ViewportSize"), scheduleUpdate);
useEventListener(camera.GetPropertyChangedSignal("FieldOfView"), scheduleUpdate);
useEventListener(container?.GetPropertyChangedSignal("AbsoluteSize"), scheduleUpdate);
useEventListener(container?.GetPropertyChangedSignal("AbsolutePosition"), scheduleUpdate);
if (!shouldRender) {
return <></>;
}
return (
<frame ref={setContainer} BackgroundTransparency={1} Size={new UDim2(1, 0, 1, 0)}>
<Portal target={camera}>
<model key="BackgroundBlur" ref={setModel}>
<part ref={setVertical} {...partProps}>
<blockmesh Scale={new Vector3(1, 1, 0)} />
</part>
<part ref={setHorizontal} {...partProps}>
<blockmesh Scale={new Vector3(1, 1, 0)} />
</part>
<part ref={setCorner0} {...partProps}>
<cylindermesh Scale={new Vector3(1, 0, 1)} />
</part>
<part ref={setCorner1} {...partProps}>
<cylindermesh Scale={new Vector3(1, 0, 1)} />
</part>
<part ref={setCorner2} {...partProps}>
<cylindermesh Scale={new Vector3(1, 0, 1)} />
</part>
<part ref={setCorner3} {...partProps}>
<cylindermesh Scale={new Vector3(1, 0, 1)} />
</part>
</model>
</Portal>
</frame>
);
}
import { useEventListener } from "@rbxts/pretty-react-hooks";
import { useEffect, useState } from "@rbxts/roact";
/**
* Returns the corner radius applied to the parent of the given instance.
*/
export function useParentRadius(ref?: Instance) {
const [parent, setParent] = useState(ref?.Parent);
const [corner, setCorner] = useState(parent?.FindFirstChildWhichIsA("UICorner"));
const [cornerRadius, setCornerRadius] = useState(corner ? corner.CornerRadius : new UDim());
useEffect(() => {
setParent(ref?.Parent);
}, [ref]);
useEffect(() => {
setCorner(parent?.FindFirstChildWhichIsA("UICorner"));
}, [parent]);
useEffect(() => {
setCornerRadius(corner ? corner.CornerRadius : new UDim());
}, [corner]);
useEventListener(ref?.GetPropertyChangedSignal("Parent"), () => {
setParent(ref?.Parent);
});
useEventListener(parent?.ChildAdded, (child) => {
if (child.IsA("UICorner")) {
setCorner(child);
setCornerRadius(child.CornerRadius);
}
});
useEventListener(corner?.GetPropertyChangedSignal("CornerRadius"), () => {
setCornerRadius(corner!.CornerRadius);
});
return cornerRadius;
}
import { useEventListener } from "@rbxts/pretty-react-hooks";
import { useState } from "@rbxts/roact";
const userSettings = UserSettings();
const userGameSettings = userSettings.GetService("UserGameSettings");
export function useQualityLevel() {
const [qualityLevel, setQualityLevel] = useState(userGameSettings.SavedQualityLevel.Value);
useEventListener(userGameSettings.GetPropertyChangedSignal("SavedQualityLevel"), () => {
setQualityLevel(userGameSettings.SavedQualityLevel.Value);
});
return qualityLevel;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment