Hey y’all!
It’s Ditch New Year’s Resolution Day where you’re finally free from the burden of that impossible promise you made to yourself! In seriousness, I guess this happens to the best of us–our temptations get hold of us and we often stray from our goals.
Anyways, this is what we’re going to be creating today:
Intro
The contraption in question is called a comparison slider, which is used to compare two images (typically for a before-and-after effect). The two images are overlapped, but the left image is cropped so that it reaches the slider, which means the right image will show from that slider onwards. This allows you to see both images at the same time while keeping the “overall” image intact.
For example, if you want to compare a cityscape image’s contrast, you can have the original at the left, and the adjusted contrast one at the right. However, the entire image still resembles the original cityscape with the buildings and everything in the same place (aka, the image positions and sizes do not change, just the cropping).
I hope this makes sense! Here’s a video:
The images are from the DevHub post-processing effects article. They used to have comparison sliders before they added the “tab” system, which is how I got the idea.
Of course, all the “UI jazz” (e.g. tweens, UICorners, etc.) will be excluded from this tutorial to prevent overcomplexity.
Application on Roblox
Now, why may comparison sliders be useful in a Roblox game? Well, not only are they cool and simple, but with a game that allows for lighting effects to be changed, it is good to give players a good before-after comparison to see if it’s worth it to compromise performance.
What’s really neat about comparison sliders as opposed to a simple “switch-back-and-forth” is that you can control which element you want to compare. If you get a glace at the entire image immediately, some minor discrepancies may be overlooked.
Structure
Depending on which method you use (there are two, which will follow), it’s either very structure-based or script/concept-based. The first uses a “cropper” frame while the second uses ImageRectSize and both crop the left image accordingly.
Method 1
- Frame is just for container purposes. It has a UIAspectRatioConstraint to keep it in compliance with the images’ aspect ratio.
- Image1 and Image2 are the before and after images, respectively.
- Image1Frame serves as the “cropper” of Image1 using the ClipsDescendants property.
- Slider is just a 2px thick frame that will be our slider.
Here’s what the UI looks like:
Note: Image1 and Image2 are the same absolute size as Frame itself, I just labeled them in their corresponding areas where they’re visible.
Method 2
Everything’s the same here except having Image1
in Frame
directly–there is no “cropper” frame.
Again, these explorer layouts above simplified–there is no toggle button at the bottom left, title text at the top, dragger button at the slider, etc. all of which are purely extras.
Scripting
There are technically two methods to accomplish this, each with its pros and cons:
Method 1 | Method 2 | |
---|---|---|
Pros | Easy; structural | No extra “cropper” frame |
Cons | Extra “cropper” frame | Just a bit more diffcult |
Both output the same effect, but I prefer Method 2 because it avoids the extra frame, plus I liked using the ImageRectSize, which I normally never use. I’d call it a creative approach, as opposed to the cliche Method 1.
Please start with Method 1 first to see the entire script, because I will skip certain sections that remain the same in Method 2.
Method 1
As I said, the scripting is easy, but I’ll still explain the bits.
First, declare variables, of course:
--uis for the slider mechanics
local uis = game:GetService('UserInputService')
local ui = script.Parent --screenGui
local frame = ui.Frame
local image1Frame = frame.Image1Frame
local img1, img2 = image1Frame.Image1, frame.Image2
local sliderBorder = frame.SliderBorder
Technically, Image2 never needs to be cropped, that’s why it doesn’t have it’s “cropper” frame. The reason we need it is to use its AbsoluteSize to set Image1’s, which needs to stay stagnant as its parent’s size changes with the slider’s position.
Here is the function that will run every time a “drag” happens:
--factor is the X scale position of the mouse
local function position(factor)
sliderBorder.Position = UDim2.new(factor, 0, 0, 0)
image1Frame.Size = UDim2.new(factor, 0, 1, 0)
img1.Size = UDim2.new(0, img2.AbsoluteSize.X, 0, img2.AbsoluteSize.Y)
end
As you see above, we update the slider’s position according to the mouse’s X scale position (which is passed as a parameter). The size of the “cropper” frame is also adjusted to it, but we need Image1 to stay the same size, which can only be done with a pixel size. We need both images to fit perfectly, and since Image2’s size will stay constant, we can use Image2’s AbsoluteSize.
Now, this is just a simple function to return true
if the given mouse position mp
is inside of Frame’s bounds:
local function isInside(mp)
local p = frame.AbsolutePosition
local s = frame.AbsoluteSize
return mp.X >= p.X and mp.Y >= p.Y and mp.X <= p.X + s.X and mp.Y <= p.Y + s.Y
end
Then, we have the UserInputService detect mouse dragging using a combination of MouseMovement and checking if the mouse button is pressed at the time:
uis.InputChanged:Connect(function(input)
if input.UserInputType == Enum.UserInputType.MouseMovement then
if uis:IsMouseButtonPressed(Enum.UserInputType.MouseButton1) then
--remeber the deprecated Mouse.X and Mouse.Y? it almost like that
local mousePos = uis:GetMouseLocation()
--use the function to verify whether the mouse is inside Frame or not
if isInside(mousePos) then
--position relative from the top-left corner of the FRAME
local pos = mousePos.X - frame.AbsolutePosition.X
local scale = pos / frame.AbsoluteSize.X --convert pixel to scale
scale = math.clamp(scale, 0, 1) --just in case, clamp it from 0-1
position(scale) --call the function to update the elements
end
end
end
end)
Instead of the UserInputService, you can use a TextButton or something that covers the entire Frame and that object listens of InputBegan
. But, I find UserInputService a bit more dynamic and any changes made are easy and efficient.
Now you can stop here, or you can add a function to update Image1’s size whenever Frame’s size changes, so that if the player adjusted their window size, Image1 can instantly update:
frame:GetPropertyChangedSignal('AbsoluteSize'):Connect(function()
img1.Size = UDim2.new(0, img2.AbsoluteSize.X, 0, img2.AbsoluteSize.Y)
end)
And that’s it. I told you it was easy, the main battle was just the structure.
Method 2
So, Method 2 is using ImageRectSize and some math. For those who don’t know, ImageRectSize allows us to set the “area” (in pixels) that is going to be shown. If it’s (0, 0), which is the default, the area is the entire image. When it’s combined with ImageRectOffset, you can shift that imaginary “rectangle” around. So an ImageRectOffset of (50, 50), would move that box 50px right and 50px down from the image’s top-left corner:
For example, this is a 900x900px image, and to show only the red circle in the middle, you need to set the ImageRectSize to that area (300x300px). This will focus on the top-left circle, so we need to shift the “area” 300 to the right and 300 down by setting ImageRectOffset to (300, 300).
(Note: We’re not using ImageRectOffset in this one since a combination of ImageRectSize and the Size property achieve the desired result.)
This is simplified. Since the area being focused on it different from the image’s size property, the ScaleType will kick in and try to fill the entirety of the image bounds with the resulting area. This can distort or crop the image undesirably, but there’s an easy fix! You can manually resize the image element so no filling is necessary.
Read more about the two properties here:
Let’s start coding! Declare the variables:
local uis = game:GetService('UserInputService')
local ui = script.Parent
local frame= ui.Frame
local img1, img2 = frame.Image1, frame.Image2
local sliderBorder,= frame.SliderBorder
--see explanation below for these two vars
local COMP_SIZE = 1024
local SIZE = Vector2.new(COMP_SIZE, COMP_SIZE / (1200 / 450))
When you upload an image to Roblox, it gets compressed to a maximum of 1024x1024px. This means that if your image is larger, you cannot use those pixel values for the ImageRectSize since it’d be based on the compressed size, not the original. Now SIZE
represents the compressed pixel dimension of the image. Of course, if it’s wide, then the X dimension reaches the max of 1024 and the Y is any value to keep the same aspect ratio. For example, the images we’re using right now are 1200x450px before compression, doing some math, the compressed image is:
aspectRatio = 1200 / 450
sizeY = 1024 / aspectRatio
SIZE = (1024, sizeY)
This is the key! SIZE
will be used in the ImageRectSize, and I’ll explain later why it needs to be constant.
Next, we have the position()
function, which is a tad bit different:
local function position(factor)
sliderBorder.Position = UDim2.new(factor, 0, 0, 0)
--[[set the X as a fraction of the dragger's X scale position,
so the image extends to the dragger]]
img1.ImageRectSize = Vector2.new(SIZE.X * factor, SIZE.Y)
--set the size to avoid any distortions
img1.Size = UDim2.new(0, frame.AbsoluteSize.X * factor, 0, frame.AbsoluteSize.Y)
end
Now, why do we need a constant ImageRectSize apart from the factor
multiplication? Well, when you resize an image, it’s internal pixel dimension stays constant. Even if you set a 1000x500 image’s size property to a 1x1px space then it’s still being recognized as a 1000x500px when using ImageRectSize (and ImageRectOffset). This means that if the window is resized or Frame
is somehow smaller, then the ImageRectSize needs to comply with the original compressed size. If you set the X to frame.AbsoluteSize.X * scale
, then the image will actually stay large when the UI shrinks, which will not fit well with Image2
.
Please reach out if that blurb was confusing!
Anyways, the rest until the last connection is the same (I skipped those two to preserve space):
--isInside() function
--UIS input changed
frame:GetPropertyChangedSignal('AbsoluteSize'):Connect(function()
--we need to rerun the entire function to allow Image1 to accomodate
postion(dragger.Position.X.Scale)
end)
And we’re done with Method 2!
Download the UI
This is the ScreenGui that is in the video, with all the “UI jazz.” Feel free to explore the script and the structure.
Method 1: Comparison_Slider.rbxm (13.3 KB)
Method 2: Comparison_Slider_2.rbxm (13.3 KB)
Closing Remarks
I know this is rather simple, but hey, anyone can learn anything at any stage. Plus, it’s a nice addition to your settings UI!
How would you rate this tutorial?
Anything unclear, overlooked, etc.
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
0 voters
How applicable is this to your game/project?
i.e. How likely are you to use this?
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
0 voters
Thanks y’all,
And have a great time!