@rbxts/vfx-forge 2.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/LICENSE +82 -0
  2. package/README.md +39 -0
  3. package/out/effects/beam.luau +312 -0
  4. package/out/effects/bezier.luau +392 -0
  5. package/out/effects/camera_shake.luau +200 -0
  6. package/out/effects/lightning.luau +1183 -0
  7. package/out/effects/mesh.luau +466 -0
  8. package/out/effects/particle.luau +64 -0
  9. package/out/effects/randomizer.luau +110 -0
  10. package/out/effects/screen.luau +61 -0
  11. package/out/effects/shockwave_debris.luau +277 -0
  12. package/out/effects/shockwave_line.luau +356 -0
  13. package/out/effects/shockwave_ring.luau +252 -0
  14. package/out/effects/sound.luau +311 -0
  15. package/out/effects/spin.luau +88 -0
  16. package/out/effects/tweener.luau +122 -0
  17. package/out/emitters.luau +387 -0
  18. package/out/index.d.ts +341 -0
  19. package/out/init.luau +279 -0
  20. package/out/mod/attributes.luau +227 -0
  21. package/out/mod/color/Oklab.luau +93 -0
  22. package/out/mod/color/sRGB.luau +71 -0
  23. package/out/mod/common/bezier.luau +372 -0
  24. package/out/mod/common/flipbook.luau +102 -0
  25. package/out/mod/lerp.luau +210 -0
  26. package/out/mod/logger.luau +20 -0
  27. package/out/mod/shape.luau +207 -0
  28. package/out/mod/tween.luau +161 -0
  29. package/out/mod/utility.luau +707 -0
  30. package/out/obj/Bezier.luau +268 -0
  31. package/out/obj/ObjectCache.luau +289 -0
  32. package/out/services/caches.luau +62 -0
  33. package/out/services/effects.luau +234 -0
  34. package/out/services/enabled_effects.luau +120 -0
  35. package/out/services/texture_loader.luau +174 -0
  36. package/out/tsconfig.tsbuildinfo +1 -0
  37. package/out/types.luau +43 -0
  38. package/package.json +63 -0
@@ -0,0 +1,707 @@
1
+ --!native
2
+ --!nolint LocalShadow
3
+ local RunService = game:GetService("RunService")
4
+ local PhysicsService = game:GetService("PhysicsService")
5
+
6
+ local attr = require("@mod/attributes")
7
+ local logger = require("@mod/logger")
8
+
9
+ local Promise = require("@pkg/Promise")
10
+
11
+ local utility = {}
12
+
13
+ utility.PLUGIN_CONTEXT = script:FindFirstAncestorOfClass("Plugin")
14
+ or (RunService:IsStudio() and not RunService:IsRunning())
15
+
16
+ utility.SERVER_CONTEXT = RunService:IsServer() or utility.PLUGIN_CONTEXT
17
+
18
+ utility.DEG_TO_RAD = math.pi / 180
19
+
20
+ utility.BEZIER_TAG = "BezierParticle"
21
+ utility.LIGHTNING_TAG = "LightningBolt"
22
+ utility.SHOCKWAVE_TAG = "Shockwave"
23
+ utility.SCREENSHAKE_TAG = "CameraShake"
24
+
25
+ utility.PROPERTY_TWEENER_TAG = "PropertyTweener"
26
+ utility.PROPERTY_RANDOMIZER_TAG = "PropertyRandomizer"
27
+
28
+ utility.ATTRIBUTE_TWEENER_TAG = "AttributeTweener"
29
+ utility.ATTRIBUTE_RANDOMIZER_TAG = "AttributeRandomizer"
30
+
31
+ utility.ENABLED_VFX_TAG = "ConstantVFX"
32
+ utility.TEXTURE_LOAD_TAG = "LoadVFXTextures"
33
+
34
+ utility.CLEANUP_TAG = "__forge__cleanupOnExit"
35
+ utility.EMIT_EXCLUDE_TAG = "__forge_excludeFromEmit"
36
+
37
+ utility.RENDER_PRIORITY = Enum.RenderPriority.Camera.Value + 1
38
+
39
+ utility.COLLISION_GROUPS = {
40
+ StudioSelectable = {},
41
+
42
+ ForgeDebris = {
43
+ ForgeDebris = false,
44
+ },
45
+
46
+ ForgeMouseIgnore = {
47
+ StudioSelectable = false,
48
+ },
49
+ }
50
+
51
+ utility.COPY_SPECIALMESH_PROPERTIES = {
52
+ "MeshId",
53
+ "MeshType",
54
+ "Offset",
55
+ "Scale",
56
+ "TextureId",
57
+ "VertexColor",
58
+ }
59
+
60
+ utility.COPY_PART_PROPERTIES = {
61
+ "CastShadow",
62
+
63
+ "Color",
64
+
65
+ "Material",
66
+ "MaterialVariant",
67
+ "Reflectance",
68
+
69
+ "Shape",
70
+
71
+ "FrontSurface",
72
+ "BackSurface",
73
+ "LeftSurface",
74
+ "RightSurface",
75
+ "TopSurface",
76
+ "BottomSurface",
77
+ }
78
+
79
+ utility.COPY_EXTENDED_PART_PROPERTIES = {
80
+ "Size",
81
+ "Transparency",
82
+
83
+ "CustomPhysicalProperties",
84
+
85
+ "CanCollide",
86
+ "CanQuery",
87
+ "CanTouch",
88
+
89
+ "CollisionGroup",
90
+ }
91
+
92
+ local LOCKS = setmetatable({} :: { [Instance]: boolean }, { __mode = "k" })
93
+
94
+ function utility.lock(ref: Instance)
95
+ if LOCKS[ref] then
96
+ return true
97
+ end
98
+
99
+ LOCKS[ref] = coroutine.running()
100
+
101
+ return false
102
+ end
103
+
104
+ function utility.unlock(ref: Instance, key: thread?)
105
+ local thread = LOCKS[ref]
106
+
107
+ if coroutine.running() ~= thread and key ~= thread then
108
+ logger.error("attempt to unlock an instance owned by a different thread")
109
+ end
110
+
111
+ LOCKS[ref] = nil
112
+ end
113
+
114
+ function utility.setCollisionGroups(groups)
115
+ local rules = {}
116
+
117
+ for name, list in groups do
118
+ PhysicsService:RegisterCollisionGroup(name)
119
+
120
+ if not name:match("Studio") or RunService:IsStudio() then
121
+ rules[name] = list
122
+ end
123
+ end
124
+
125
+ for name, list in rules do
126
+ for group, collidable in list do
127
+ if group:match("Studio") and not RunService:IsStudio() then
128
+ continue
129
+ end
130
+
131
+ PhysicsService:CollisionGroupSetCollidable(name, group, collidable)
132
+ end
133
+ end
134
+ end
135
+
136
+ local last_id = 0
137
+
138
+ function utility.getRandomId()
139
+ last_id += 1
140
+ return tostring(last_id)
141
+ end
142
+
143
+ function utility.copyProperties(from: Instance, to: Instance, list: { string })
144
+ for _, k in list do
145
+ to[k] = from[k]
146
+ end
147
+ end
148
+
149
+ function utility.lerp(a: number, b: number, t: number)
150
+ return a + (b - a) * t
151
+ end
152
+
153
+ function utility.try<R...>(msg: string, func: (...any) -> R..., ...: any): (boolean, R...)
154
+ local res = { xpcall(func, function(err)
155
+ logger.warn(string.format(msg, err))
156
+ end, ...) }
157
+
158
+ return res[1], table.unpack(res, 2)
159
+ end
160
+
161
+ function utility.reboundfn<A>(sec: number, func: (...A) -> never): (...A) -> never
162
+ local lastRunThread
163
+
164
+ return function(...)
165
+ if lastRunThread then
166
+ task.cancel(lastRunThread)
167
+ end
168
+
169
+ local args = { ... }
170
+
171
+ lastRunThread = task.delay(sec, function()
172
+ lastRunThread = nil
173
+ func(table.unpack(args))
174
+ end)
175
+ end
176
+ end
177
+
178
+ function utility.randomUnitVector(min: Vector3, max: Vector3, rng: Random?)
179
+ local rng = rng or Random.new()
180
+ return Vector3.new(rng:NextNumber(min.X, max.X), rng:NextNumber(min.Y, max.Y), rng:NextNumber(min.Z, max.Z))
181
+ end
182
+
183
+ function utility.getImpulseForce(start: Vector3, goal: Vector3, duration: number)
184
+ return (goal - start) / duration + Vector3.new(0, workspace.Gravity * duration * 0.5, 0)
185
+ end
186
+
187
+ function utility.isMeshVFX(obj: Instance)
188
+ return obj
189
+ and obj:IsA("Model")
190
+ and obj:FindFirstChild("Start")
191
+ and (obj :: any).Start:IsA("BasePart")
192
+ and obj:FindFirstChild("End")
193
+ and (obj :: any).End:IsA("BasePart")
194
+ end
195
+
196
+ function utility.isSpinModelStatic(model: Model)
197
+ return attr.get(model, "SpinRotation", vector.zero, true) == vector.zero
198
+ and attr.get(model, "Scale_Start", 1, true) == 1
199
+ and attr.get(model, "Scale_End", 1, true) == 1
200
+ and (attr.get(model, "SyncPosition", false, true) == false)
201
+ end
202
+
203
+ function utility.shouldSkipNested(obj: Instance)
204
+ return obj:IsA("Beam")
205
+ or obj:HasTag(utility.BEZIER_TAG)
206
+ or obj:HasTag(utility.LIGHTNING_TAG)
207
+ or utility.isMeshVFX(obj)
208
+ or (obj:IsA("BasePart") and utility.findFirstClassWithTag(obj, "Attachment", utility.SHOCKWAVE_TAG) ~= nil)
209
+ end
210
+
211
+ function utility.getTarget(obj: Instance): Instance?
212
+ local objectValue = obj:FindFirstChildOfClass("ObjectValue")
213
+
214
+ if objectValue and objectValue.Value then
215
+ return objectValue.Value
216
+ end
217
+
218
+ local parent = obj.Parent
219
+
220
+ if not parent then
221
+ return nil
222
+ end
223
+
224
+ while parent:IsA("Folder") do
225
+ parent = parent.Parent
226
+
227
+ if not parent then
228
+ return nil
229
+ end
230
+ end
231
+
232
+ return parent
233
+ end
234
+
235
+ function utility.createEmitPromise(effects: any, instance: Instance, effectDepth: number, emitFunc: any)
236
+ return Promise.new(function(resolve)
237
+ local scope = {}
238
+ scope.depth = effectDepth
239
+ scope.effects = effects
240
+
241
+ emitFunc(scope)
242
+
243
+ utility.cleanupScope(scope)
244
+
245
+ resolve()
246
+ end)
247
+ end
248
+
249
+ function utility.cleanupScope(scope: { unknown })
250
+ for k, v in scope do
251
+ -- Skip scope metadata fields
252
+ if k == "depth" or k == "effects" then
253
+ continue
254
+ end
255
+
256
+ local t = typeof(v)
257
+
258
+ if t == "Instance" then
259
+ v:Destroy()
260
+ elseif t == "RBXScriptConnection" then
261
+ v:Disconnect()
262
+ elseif t == "thread" then
263
+ if coroutine.status(v) ~= "dead" then
264
+ task.cancel(v)
265
+ end
266
+ elseif t == "function" then
267
+ task.spawn(v)
268
+ elseif t == "table" then
269
+ utility.cleanupScope(v)
270
+ end
271
+ end
272
+
273
+ table.clear(scope)
274
+ end
275
+
276
+ function utility.protectParent(scope: { unknown }, parent: Instance)
277
+ table.insert(
278
+ scope,
279
+ parent.AncestryChanged:Connect(function(_, new)
280
+ if parent.Parent == workspace.Terrain then
281
+ return
282
+ end
283
+
284
+ parent.Parent = workspace.Terrain
285
+ end)
286
+ )
287
+ end
288
+
289
+ function utility.findFirstClassWithTag(obj: Instance?, class: string, tag: string)
290
+ if not obj or obj.Parent == game then
291
+ return
292
+ end
293
+
294
+ if obj.ClassName == class and obj:HasTag(tag) then
295
+ return obj
296
+ else
297
+ return utility.findFirstClassWithTag(obj.Parent, class, tag)
298
+ end
299
+ end
300
+
301
+ function utility.cloneParticleAncestry(
302
+ particle: ParticleEmitter,
303
+ map: { [Instance?]: Instance }
304
+ ): (Instance?, Instance?)
305
+ if not particle:FindFirstAncestorWhichIsA("BasePart") and not particle:FindFirstAncestorOfClass("Attachment") then
306
+ return
307
+ end
308
+
309
+ local function findAncestor(obj: Instance?)
310
+ if not obj then
311
+ return
312
+ end
313
+
314
+ local ancestor = obj
315
+
316
+ if obj.Parent and (obj.Parent:IsA("BasePart") or obj.Parent:IsA("Attachment")) then
317
+ ancestor = findAncestor(obj.Parent)
318
+ end
319
+
320
+ return ancestor
321
+ end
322
+
323
+ local function recurse(obj: Instance?)
324
+ if not obj then
325
+ return
326
+ end
327
+
328
+ local created = map[obj]
329
+
330
+ if created then
331
+ return created, if obj == particle then map[obj.Parent] else map[findAncestor(obj.Parent)] or map[obj]
332
+ end
333
+
334
+ local clone = Instance.fromExisting(obj)
335
+ clone.Archivable = false
336
+
337
+ if clone:IsA("BasePart") then
338
+ clone.Locked = true
339
+ end
340
+
341
+ local ancestor = clone
342
+
343
+ if map then
344
+ map[obj] = clone
345
+ end
346
+
347
+ if obj.Parent and (obj.Parent:IsA("BasePart") or obj.Parent:IsA("Attachment")) then
348
+ local parent, pa = recurse(obj.Parent)
349
+
350
+ if pa then
351
+ ancestor = pa
352
+ end
353
+
354
+ if parent then
355
+ clone.Parent = parent
356
+ end
357
+ end
358
+
359
+ return clone, ancestor
360
+ end
361
+
362
+ return recurse(particle.Parent)
363
+ end
364
+
365
+ function utility.getTransformedOriginExtents(parent: Instance)
366
+ local originCFrame, originSize = CFrame.identity, vector.zero
367
+
368
+ if parent:IsA("BasePart") then
369
+ originSize = parent.Size
370
+ originCFrame = parent.CFrame
371
+ elseif parent:IsA("Attachment") then
372
+ originCFrame = parent.WorldCFrame
373
+
374
+ local newParent = parent.Parent
375
+
376
+ -- step 1: find the full origin cframe by walking up the tree, and processing the upmost attachment
377
+ local positionScale = attr.get(parent, "PositionScale", vector.zero, true)
378
+
379
+ if newParent then
380
+ if newParent:IsA("BasePart") and positionScale ~= vector.zero then
381
+ local offset = (newParent.Size / 2) * positionScale
382
+ originCFrame = originCFrame + offset
383
+ end
384
+ end
385
+
386
+ -- step 2: apply rotation on final origin cframe
387
+ local overrideWorldRotation = attr.get(parent, "OverrideWorldRotation", false, true)
388
+ local worldRotation = attr.get(parent, "WorldRotation", vector.zero, true)
389
+
390
+ if overrideWorldRotation then
391
+ local rotRad = worldRotation * utility.DEG_TO_RAD
392
+ local position = originCFrame.Position
393
+
394
+ originCFrame = CFrame.new(position) * CFrame.fromOrientation(rotRad.X, rotRad.Y, rotRad.Z)
395
+ end
396
+ end
397
+
398
+ return originCFrame, originSize
399
+ end
400
+
401
+ function utility.getMeshDecals(ref: Model, start: BasePart)
402
+ local legacy_isFlipbook = attr.get(ref, "Flipbook", false, true)
403
+
404
+ local decals = {}
405
+ local flipbooks = {}
406
+ local fromToMap = {}
407
+
408
+ local function filter(list: { Instance }): { Decal }
409
+ local filtered = {}
410
+
411
+ for _, v in list do
412
+ if v:IsA("Decal") then
413
+ table.insert(filtered, v)
414
+ end
415
+ end
416
+
417
+ return filtered
418
+ end
419
+
420
+ if legacy_isFlipbook then
421
+ decals = filter(start:GetChildren())
422
+ else
423
+ local originalEnd = ref:FindFirstChild("End")
424
+ local originalStart = ref:FindFirstChild("Start")
425
+
426
+ if originalEnd then
427
+ for _, v in start:GetChildren() do
428
+ if not v:IsA("Decal") then
429
+ continue
430
+ end
431
+
432
+ local from = v
433
+
434
+ local to = originalEnd:FindFirstChild(v.Name)
435
+
436
+ if not to or not to:IsA("Decal") then
437
+ continue
438
+ end
439
+
440
+ table.insert(decals, v)
441
+
442
+ fromToMap[from] = to
443
+
444
+ if not v:GetAttribute("FlipbookEnabled") then
445
+ table.insert(decals, v)
446
+ else
447
+ flipbooks[v] = utility.deserializeFlipbook(v:GetAttribute("FlipbookTextures"))
448
+ end
449
+ end
450
+
451
+ for _, v in originalStart:GetChildren() do
452
+ if not v:IsA("Decal") then
453
+ continue
454
+ end
455
+
456
+ -- convert children decals to flipbooks
457
+ local children = v:GetChildren()
458
+
459
+ do
460
+ local offset = 0
461
+
462
+ for i = 1, #children do
463
+ i -= offset
464
+
465
+ local obj = children[i]
466
+
467
+ if not obj:IsA("Decal") then
468
+ table.remove(children, i)
469
+ offset += 1
470
+ end
471
+ end
472
+ end
473
+
474
+ if #children ~= 0 then
475
+ local function idx(str: string)
476
+ return tonumber(str:match("%d+")) or 0
477
+ end
478
+
479
+ table.sort(children, function(a, b)
480
+ return idx(a.Name) < idx(b.Name)
481
+ end)
482
+
483
+ local ids = {}
484
+
485
+ for _, obj in children do
486
+ table.insert(ids, idx(obj.Texture))
487
+ obj:Destroy()
488
+ end
489
+
490
+ local buf = utility.serializeFlipbook(ids)
491
+
492
+ v:SetAttribute("FlipbookEnabled", true)
493
+ v:SetAttribute("FlipbookTextures", buffer.tostring(buf))
494
+ end
495
+ end
496
+ end
497
+ end
498
+
499
+ return decals, flipbooks, fromToMap
500
+ end
501
+
502
+ function utility.assembleMeshVFX(start: BasePart, scope: { any }, shared_part_cache: any)
503
+ local id = utility.getRandomId()
504
+
505
+ if start:IsA("Part") then
506
+ local objAbstr = shared_part_cache:get(id)
507
+ objAbstr.CFrame = start.CFrame
508
+
509
+ local obj = objAbstr._getReal()
510
+
511
+ utility.copyProperties(start, obj, utility.COPY_PART_PROPERTIES)
512
+ utility.copyProperties(start, obj, utility.COPY_EXTENDED_PART_PROPERTIES)
513
+
514
+ local clone = start:Clone()
515
+
516
+ for _, child in clone:GetChildren() do
517
+ child.Parent = obj
518
+ end
519
+
520
+ clone:Destroy()
521
+
522
+ table.insert(scope, function()
523
+ shared_part_cache:free(id)
524
+ end)
525
+
526
+ return objAbstr
527
+ else
528
+ local clone = start:Clone()
529
+ clone.Archivable = false
530
+ clone.Locked = true
531
+ clone.Parent = workspace.Terrain
532
+
533
+ clone:AddTag(utility.CLEANUP_TAG)
534
+
535
+ table.insert(scope, clone)
536
+
537
+ return clone
538
+ end
539
+ end
540
+
541
+ function utility.getBezierPoints(root: Attachment, metadata: boolean?)
542
+ local objs = root:GetChildren()
543
+
544
+ table.sort(objs, function(a, b)
545
+ return tonumber(a.Name) < tonumber(b.Name)
546
+ end)
547
+
548
+ local points: { vector } = {}
549
+ local attachments: { Attachment } = {}
550
+ local attachmentToPointMap: { [Attachment]: vector } = {}
551
+
552
+ local function vec(p: Attachment)
553
+ local w = root.WorldCFrame:PointToObjectSpace(p.WorldPosition)
554
+ local pos = vector.create(w.X, w.Y, w.Z)
555
+
556
+ table.insert(points, pos)
557
+
558
+ if metadata then
559
+ attachmentToPointMap[p] = pos
560
+ table.insert(attachments, p)
561
+ end
562
+
563
+ return pos
564
+ end
565
+
566
+ for i, p in objs do
567
+ local t0 = p:FindFirstChild("T0")
568
+ local t1 = p:FindFirstChild("T1")
569
+
570
+ if i == 1 then
571
+ vec(p)
572
+ end
573
+
574
+ -- left
575
+ if i ~= 1 then
576
+ if t1 then
577
+ vec(t1)
578
+ else
579
+ vec(p)
580
+ end
581
+ end
582
+
583
+ if i ~= 1 and i ~= #objs then
584
+ vec(p)
585
+ end
586
+
587
+ -- right
588
+ if i ~= #objs then
589
+ if t0 then
590
+ vec(t0)
591
+ else
592
+ vec(p)
593
+ end
594
+ end
595
+
596
+ if i == #objs then
597
+ vec(p)
598
+ end
599
+ end
600
+
601
+ return points, attachments, attachmentToPointMap
602
+ end
603
+
604
+ function utility.scaleNumberSequence(
605
+ seq: NumberSequence,
606
+ scale: number | (value: number, envelope: number) -> (number, number)
607
+ )
608
+ if scale == 1 then
609
+ return seq
610
+ end
611
+
612
+ local scaled = {}
613
+
614
+ for _, keypoint in seq.Keypoints do
615
+ local value, envelope
616
+
617
+ if typeof(scale) == "function" then
618
+ value, envelope = scale(keypoint.Value, keypoint.Envelope)
619
+ else
620
+ value, envelope = keypoint.Value * scale, keypoint.Envelope * scale
621
+ end
622
+
623
+ table.insert(scaled, NumberSequenceKeypoint.new(keypoint.Time, value, envelope))
624
+ end
625
+
626
+ return NumberSequence.new(scaled)
627
+ end
628
+
629
+ function utility.serializePath(points: { Path2DControlPoint })
630
+ local buf = buffer.create(#points * 4 * 6)
631
+
632
+ local offset = 0
633
+
634
+ for i, p in points do
635
+ local px = p.Position.X.Scale
636
+ local py = p.Position.Y.Scale
637
+
638
+ if i ~= 1 then
639
+ buffer.writef32(buf, offset, px + p.LeftTangent.X.Scale)
640
+ buffer.writef32(buf, offset + 4, py + p.LeftTangent.Y.Scale)
641
+
642
+ offset += 8
643
+ end
644
+
645
+ buffer.writef32(buf, offset, px)
646
+ buffer.writef32(buf, offset + 4, py)
647
+
648
+ offset += 8
649
+
650
+ if i ~= #points then
651
+ buffer.writef32(buf, offset, px + p.RightTangent.X.Scale)
652
+ buffer.writef32(buf, offset + 4, py + p.RightTangent.Y.Scale)
653
+
654
+ offset += 8
655
+ end
656
+ end
657
+
658
+ return buf
659
+ end
660
+
661
+ function utility.deserializePath(data: string | buffer)
662
+ local buf = typeof(data) == "string" and buffer.fromstring(data) or data
663
+ local count = buffer.len(buf) / 24
664
+
665
+ local points = {}
666
+
667
+ for i = 0, count - 1 do
668
+ local offset = i * 4 * 6
669
+
670
+ local px = buffer.readf32(buf, offset)
671
+ local py = buffer.readf32(buf, offset + 4)
672
+
673
+ table.insert(points, vector.create(px, py))
674
+
675
+ if i ~= count - 1 then
676
+ local t1x = buffer.readf32(buf, offset + 8)
677
+ local t1y = buffer.readf32(buf, offset + 12)
678
+
679
+ local t2x = buffer.readf32(buf, offset + 16)
680
+ local t2y = buffer.readf32(buf, offset + 20)
681
+
682
+ table.insert(points, vector.create(t1x, t1y))
683
+ table.insert(points, vector.create(t2x, t2y))
684
+ end
685
+ end
686
+
687
+ return points
688
+ end
689
+
690
+ local flipbook_common = require("@mod/common/flipbook")
691
+
692
+ utility.serializeFlipbook = flipbook_common.serialize
693
+ utility.deserializeFlipbook = flipbook_common.deserialize
694
+
695
+ -- out cubic
696
+ utility.default_bezier = buffer.tostring(utility.serializePath({
697
+ Path2DControlPoint.new(UDim2.fromScale(0, 1), UDim2.new(), UDim2.fromScale(0.215, -0.61)),
698
+ Path2DControlPoint.new(UDim2.fromScale(1, 0), UDim2.fromScale(-0.645, 0), UDim2.new()),
699
+ }))
700
+
701
+ -- linear
702
+ utility.linear_bezier = buffer.tostring(utility.serializePath({
703
+ Path2DControlPoint.new(UDim2.fromScale(0, 1)),
704
+ Path2DControlPoint.new(UDim2.fromScale(1, 0)),
705
+ }))
706
+
707
+ return utility