Ended up being *slightly* harder than expected

You’ll need this “ObjectPool” module I found (as a ModuleScript that’s a child of this script): https://github.com/Roblox/Core-Scripts/blob/master/CoreScriptsRoot/Modules/Common/ObjectPool.lua

And then just stick this inside whatever container like

```
-- converted to lua from https://codepen.io/jkiss/pen/OVEeqK by nicemike40
local canvas = script.Parent
local can_w = canvas.AbsoluteSize.X
local can_h = canvas.AbsoluteSize.Y
-- ADDED
local ObjectPool = require(script.ObjectPool)
local pool = ObjectPool.new(300) -- max 300 balls or 300 lines
local CIRCLE_IMG = "http://www.roblox.com/asset/?id=172650188"
local ball = {
x = 0,
y = 0,
vx = 0,
vy = 0,
r = 0,
alpha = 1,
phase = 0
}
local ball_color = Color3.new(207/255, 255/255, 4/255)
local R = 6
local guis = {} -- ADDED for guis
local min_v = 6
local max_v = 60
local alpha_f = 1.8
local alpha_phase = 0
-- Line
local link_line_width = 1
local dis_limit = 260
local add_mouse_point = true
local mouse_in = false
local mouse_ball = {
x = 0,
y = 0,
vx = 0,
vy = 0,
r = 0,
type = 'mouse'
}
local balls = {mouse_ball} -- CHANGED: always have mouse ball
-- ADDED to support GUIs
local function GetLine(x1, y1, x2, y2)
local line = pool:GetInstance("Frame")
line.BorderSizePixel = 0
line.AnchorPoint = Vector2.new(0.5, 0.5)
line.Visible = true
line.Parent = canvas
table.insert(guis, line)
return line
end
local function GetBallGui()
local gui = pool:GetInstance("ImageLabel")
gui.Image = CIRCLE_IMG
gui.Size = UDim2.fromOffset(R, R)
gui.BackgroundTransparency = 1
gui.ImageColor3 = ball_color
gui.Parent = canvas
gui.Visible = true
gui.AnchorPoint = Vector2.new(0.5, 0.5)
table.insert(guis, gui)
return gui
end
-- Random speed
local function randomNumFrom(min, max)
return math.random()*(max - min) + min;
end
local function getRandomSpeed(pos)
local min = -1
local max = 1
-- CHANGED: switch statement -> elseif
if pos == 'top' then
return randomNumFrom(-max_v, max_v), randomNumFrom(min_v, max_v)
elseif pos == 'right' then
return randomNumFrom(-max_v, -min_v), randomNumFrom(-max_v, max_v)
elseif pos == 'bottom' then
return randomNumFrom(-max_v, max_v), randomNumFrom(-max_v, -min_v)
elseif pos == 'left' then
return randomNumFrom(min_v, max_v), randomNumFrom(-max_v, max_v)
end
end
local function randomArrayItem(arr)
return arr[math.random(1, #arr)];
end
-- Random Ball
local DIRS = {'top', 'right', 'bottom', 'left'}
local function randomSidePos(length)
return math.random(0, length);
end
local function getRandomBall()
local pos = randomArrayItem(DIRS)
local vx, vy = getRandomSpeed(pos)
local ball = {
x = 0,
y = 0,
vx = vx,
vy = vy,
r = R,
alpha = 1,
phase = randomNumFrom(0, 10)
}
if pos == 'top' then
ball.x = randomSidePos(can_w)
ball.y = -R
elseif pos == 'right' then
ball.x = can_w + R
ball.y = randomSidePos(can_h)
elseif pos == 'bottom' then
ball.x = randomSidePos(can_w)
ball.y = can_h + R
elseif pos == 'left' then
ball.x = -R
ball.y = randomSidePos(can_h)
end
return ball
end
-- Draw Ball
local function renderBalls()
for i = 1, #balls do
local b = balls[i]
if b.type == nil then
local gui = GetBallGui()
gui.ImageTransparency = b.alpha
gui.Position = UDim2.fromOffset(b.x, b.y)
end
end
end
-- Update balls
local function updateBalls(dt)
local new_balls = {};
for i = 1, #balls do
local b = balls[i]
if b.type then
table.insert(new_balls, b)
elseif b.x > -(50) and b.x < (can_w+50) and b.y > -(50) and b.y < (can_h+50) then
table.insert(new_balls, b)
-- alpha change
b.phase += alpha_f * dt; -- CHANGED use the delta time to make motion consistent regardless of framerate
b.alpha = math.abs(math.cos(b.phase));
b.x += b.vx * dt;
b.y += b.vy * dt;
end
end
balls = new_balls;
end
-- calculate distance between two points
local function getDisOf(b1, b2)
local delta_x = math.abs(b1.x - b2.x)
local delta_y = math.abs(b1.y - b2.y)
return math.sqrt(delta_x*delta_x + delta_y*delta_y);
end
-- Draw lines
local function renderLines()
local fraction;
for i = 1, #balls do
for j = 1, #balls do
local dist = getDisOf(balls[i], balls[j])
fraction = dist / dis_limit;
if(fraction < 1) then
local x1, y1, x2, y2 = balls[i].x, balls[i].y, balls[j].x, balls[j].y
local l = GetLine()
l.BackgroundTransparency = fraction
l.Size = UDim2.fromOffset(dist, link_line_width)
l.Position = UDim2.fromOffset((x1+x2)/2, (y1+y2)/2)
l.Rotation = math.deg(math.atan2(y2-y1, x2-x1))
end
end
end
end
-- add balls if there a little balls
local function addBallIfy()
if(#balls < 20) then
table.insert(balls, getRandomBall());
end
end
-- Render
local function render(dt)
-- ADDED clean up all GUIs before rendering this frame
for i = 1, #guis do
local g = guis[i]
g.Visible = false
pool:ReturnInstance(g)
end
guis = {}
updateBalls(dt);
renderBalls();
renderLines();
addBallIfy();
end
-- Init Balls
local function initBalls(num)
for i = 1, num do
local vx, vy = getRandomSpeed('top')
table.insert(balls, {
x = randomSidePos(can_w),
y = randomSidePos(can_h),
vx = vx,
vy = vy,
r = R,
alpha = 1,
phase = randomNumFrom(0, 10)
});
end
end
-- REMOVED initCanvas and moved goMovie to end
-- Mouse effect CHANGED: always have mouse ball so delete mouse enter/leave stuff
game:GetService("UserInputService").InputChanged:Connect(function(input, gameProcessed)
if input.UserInputType == Enum.UserInputType.MouseMovement then
mouse_ball.x = input.Position.X - canvas.AbsolutePosition.X
mouse_ball.y = input.Position.Y - canvas.AbsolutePosition.Y
end
end)
initBalls(30)
game:GetService("RunService").Stepped:Connect(function(_t, dt)
render(dt)
end)
```

That was fun—left some comments around when I made big changes. It could still use some optimization and cleaning up (the original code does some weird stuff and had a few bugs). Have fun!