Created
August 7, 2021 01:52
-
-
Save Reselim/9da53bbb3079da948512ccb8eae6416b to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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