Skip to content

Instantly share code, notes, and snippets.

@Reselim
Created August 7, 2021 01:52
Show Gist options
  • Save Reselim/9da53bbb3079da948512ccb8eae6416b to your computer and use it in GitHub Desktop.
Save Reselim/9da53bbb3079da948512ccb8eae6416b to your computer and use it in GitHub Desktop.
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local Workspace = game:GetService("Workspace")
local Llama = require(ReplicatedStorage.packages.Llama)
local PathCommandType = require(ReplicatedStorage.shared.enum.PathCommandType)
local Renderer = require(ReplicatedStorage.shared.Renderer)
local Toolbox = require(ReplicatedStorage.client.Game.Toolbox)
local SCALE = 1 / 150
local CURVE_QUALITY = 1 / 40
local FPS = 24
local frames = script.frames
local pointCounts = {
[PathCommandType.MoveTo] = 2,
[PathCommandType.LineTo] = 2,
[PathCommandType.HorizontalLineTo] = 1,
[PathCommandType.VerticalLineTo] = 1,
[PathCommandType.CurveTo] = 6,
[PathCommandType.SmoothCurveTo] = 4,
[PathCommandType.QuadraticBezierCurveTo] = 4,
[PathCommandType.SmoothQuadraticBezierCurveTo] = 2,
[PathCommandType.EllipticalArc] = 7,
[PathCommandType.ClosePath] = 0,
}
local function cubicBezier(A1, B1, C1, D1, alpha)
local A2 = A1:Lerp(B1, alpha)
local B2 = B1:Lerp(C1, alpha)
local C2 = C1:Lerp(D1, alpha)
local A3 = A2:Lerp(B2, alpha)
local B3 = B2:Lerp(C2, alpha)
return A3:Lerp(B3, alpha)
end
local function cubicBezierLength(A, B, C, D)
return (A - B).Magnitude + (B - C).Magnitude + (C - D).Magnitude
end
local function parseRawPath(path)
local commands = {}
for code, data in path:gmatch("([a-zA-Z])%s*([%d%s-]*)") do
local type = assert(PathCommandType.cast(code:upper()), ("Unknown command with code \"%s\""):format(code))
local relative = code == code:lower()
data = string.split(data, " ")
data = Llama.List.map(data, function(number)
if number ~= "" then
return tonumber(number)
else
return nil
end
end)
table.insert(commands, {
type = type,
relative = relative,
data = data,
})
end
return commands
end
local function decompressPath(commands)
local newCommands = {}
for _, command in ipairs(commands) do
local type = command.type
local relative = command.relative
local data = command.data
local pointCount = pointCounts[type]
local commandCount = math.ceil(#data / pointCount)
for index = 1, commandCount do
local rangeStart = (index - 1) * pointCount + 1
local rangeEnd = rangeStart + pointCount - 1
table.insert(newCommands, {
type = type,
relative = relative,
data = { table.unpack(data, rangeStart, rangeEnd) },
})
end
end
return newCommands
end
local function parsePath(path)
path = parseRawPath(path)
path = decompressPath(path)
local parsedCommands = {}
for _, command in ipairs(path) do
local type = command.type
local relative = command.relative
local data = command.data
local parsedCommand = {
type = type,
relative = relative,
}
if type == PathCommandType.MoveTo or type == PathCommandType.LineTo then
parsedCommand.position = Vector2.new(data[1], data[2])
elseif type == PathCommandType.HorizontalLineTo or type == PathCommandType.VerticalLineTo then
parsedCommand.position = data[1]
elseif type == PathCommandType.CurveTo then
parsedCommand.controlPositionA = Vector2.new(data[1], data[2])
parsedCommand.controlPositionB = Vector2.new(data[3], data[4])
parsedCommand.endPosition = Vector2.new(data[5], data[6])
elseif type ~= PathCommandType.ClosePath then
error(("Unsupported command %s"):format(tostring(type)))
end
table.insert(parsedCommands, parsedCommand)
end
return parsedCommands
end
local function normalizePath(commands)
local normalizedCommands = {}
local lastPosition = Vector2.new(0, 0)
for index = 1, #commands do
local command = commands[index]
local type = command.type
local relative = command.relative
local normalizedCommand = {
type = type,
relative = relative,
}
if type == PathCommandType.MoveTo or type == PathCommandType.LineTo then
normalizedCommand.position = relative and lastPosition + command.position or command.position
lastPosition = normalizedCommand.position
elseif type == PathCommandType.HorizontalLineTo or type == PathCommandType.VerticalLineTo then
local position
if type == PathCommandType.HorizontalLineTo then
position = Vector2.new(relative and lastPosition.X + command.position or command.position, lastPosition.Y)
elseif type == PathCommandType.VerticalLineTo then
position = Vector2.new(lastPosition.X, relative and lastPosition.Y + command.position or command.position)
end
normalizedCommand.type = PathCommandType.LineTo
normalizedCommand.position = position
lastPosition = position
elseif type == PathCommandType.CurveTo then
normalizedCommand.startPosition = lastPosition
if relative then
normalizedCommand.controlPositionA = lastPosition + command.controlPositionA
normalizedCommand.controlPositionB = lastPosition + command.controlPositionB
normalizedCommand.endPosition = lastPosition + command.endPosition
else
normalizedCommand.controlPositionA = command.controlPositionA
normalizedCommand.controlPositionB = command.controlPositionB
normalizedCommand.endPosition = command.endPosition
end
lastPosition = normalizedCommand.endPosition
elseif type ~= PathCommandType.ClosePath then
error(("Unsupported command %s"):format(tostring(type)))
end
table.insert(normalizedCommands, normalizedCommand)
end
return normalizedCommands
end
local function renderPath(commands)
local lines = {}
local points = {}
local function finishLine()
if #points >= 2 then
table.insert(lines, points)
end
points = {}
end
for index = 1, #commands do
local command = commands[index]
local type = command.type
if type == PathCommandType.MoveTo then
finishLine()
table.insert(points, command.position)
elseif type == PathCommandType.LineTo then
table.insert(points, command.position)
elseif type == PathCommandType.CurveTo then
local length = cubicBezierLength(
command.startPosition,
command.controlPositionA,
command.controlPositionB,
command.endPosition
)
local pointCount = math.floor(length * CURVE_QUALITY)
for pointIndex = 1, pointCount do
table.insert(points, cubicBezier(
command.startPosition,
command.controlPositionA,
command.controlPositionB,
command.endPosition,
pointIndex / pointCount
))
end
elseif type == PathCommandType.ClosePath then
finishLine()
else
error(("Unsupported command %s"):format(tostring(type)))
end
end
finishLine()
return lines
end
local function scaleLine(points, scale)
return Llama.List.map(points, function(point)
return point * scale
end)
end
local currentFolder
local function renderFrame(frame)
if currentFolder then
currentFolder:Destroy()
end
currentFolder = Instance.new("Folder")
local options = Toolbox.options:serialize()
for _, path in ipairs(frame) do
path = parsePath(path)
path = normalizePath(path)
path = renderPath(path)
for _, points in ipairs(path) do
local renderer = Renderer.new(options)
renderer:render(scaleLine(points, SCALE))
renderer:setParent(currentFolder)
end
end
currentFolder.Parent = Workspace
end
local function loadFrame(frameIndex)
local module = frames:FindFirstChild(frameIndex)
return require(module)
end
return function()
local startTime = os.clock()
local currentFrameIndex = 0
RunService.RenderStepped:Connect(function()
local timeElapsed = os.clock() - startTime
local frameIndex = math.clamp(math.ceil(timeElapsed * FPS), 1, 5258)
if currentFrameIndex ~= frameIndex then
currentFrameIndex = frameIndex
local frame = loadFrame(frameIndex)
renderFrame(frame)
end
end)
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment