@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,372 @@
1
+ --!nolint LocalShadow
2
+ local CollectionService = game:GetService("CollectionService")
3
+
4
+ local attr = require("@mod/attributes")
5
+ local shape = require("@mod/shape")
6
+ local tween = require("@mod/tween")
7
+ local utility = require("@mod/utility")
8
+
9
+ local Oklab = require("@mod/color/Oklab")
10
+ local Bezier = require("@obj/Bezier")
11
+
12
+ local common = {}
13
+
14
+ common.drawFuncMap = {
15
+ Box = {
16
+ Volume = shape.getPointWithinBox,
17
+ Surface = shape.getPointOnBox,
18
+ },
19
+
20
+ Cylinder = {
21
+ Volume = function(seed, cframe, size, normal, partial)
22
+ return shape.getPointWithinCylinder(seed, 0, partial, cframe, size, normal)
23
+ end,
24
+
25
+ Surface = function(seed, cframe, size, normal, partial)
26
+ return shape.getPointWithinCylinder(seed, 1, partial, cframe, size, normal)
27
+ end,
28
+ },
29
+
30
+ Sphere = {
31
+ Volume = function(seed, cframe, size, normal, partial)
32
+ return shape.getPointWithinSphere(seed, 0, partial, cframe, size, normal)
33
+ end,
34
+
35
+ Surface = function(seed, cframe, size, normal, partial)
36
+ return shape.getPointWithinSphere(seed, 1, partial, cframe, size, normal)
37
+ end,
38
+ },
39
+
40
+ Disc = {
41
+ Volume = function(seed, cframe, size, normal, partial)
42
+ return shape.getPointWithinDisc(seed, 0, partial, cframe, size, normal)
43
+ end,
44
+
45
+ Surface = function(seed, cframe, size, normal, partial)
46
+ return shape.getPointWithinDisc(seed, 1, partial, cframe, size, normal)
47
+ end,
48
+ },
49
+ }
50
+
51
+ function common.getColorAtTime(sequence: ColorSequence, time: number)
52
+ local keypoints = sequence.Keypoints
53
+
54
+ if time <= keypoints[1].Time then
55
+ return keypoints[1].Value
56
+ end
57
+
58
+ if time >= keypoints[#keypoints].Time then
59
+ return keypoints[#keypoints].Value
60
+ end
61
+
62
+ local closestBelow
63
+ local closestAbove
64
+
65
+ for i = 1, #keypoints do
66
+ local kp = keypoints[i]
67
+ if kp.Time == time then
68
+ return kp.Value
69
+ elseif kp.Time < time then
70
+ closestBelow = kp
71
+ elseif kp.Time > time then
72
+ closestAbove = kp
73
+ break
74
+ end
75
+ end
76
+
77
+ if not closestBelow or not closestAbove then
78
+ return keypoints[1].Value
79
+ end
80
+
81
+ local alpha = (time - closestBelow.Time) / (closestAbove.Time - closestBelow.Time)
82
+ return closestBelow.Value:Lerp(closestAbove.Value, alpha)
83
+ end
84
+
85
+ function common.getColorAtTimeOklab(sequence: ColorSequence, time: number)
86
+ local keypoints = sequence.Keypoints
87
+
88
+ if time <= keypoints[1].Time then
89
+ return keypoints[1].Value
90
+ end
91
+
92
+ if time >= keypoints[#keypoints].Time then
93
+ return keypoints[#keypoints].Value
94
+ end
95
+
96
+ local closestBelow
97
+ local closestAbove
98
+
99
+ for i = 1, #keypoints do
100
+ local kp = keypoints[i]
101
+ if kp.Time == time then
102
+ return kp.Value
103
+ elseif kp.Time < time then
104
+ closestBelow = kp
105
+ elseif kp.Time > time then
106
+ closestAbove = kp
107
+ break
108
+ end
109
+ end
110
+
111
+ if not closestBelow or not closestAbove then
112
+ return keypoints[1].Value
113
+ end
114
+
115
+ local alpha = (time - closestBelow.Time) / (closestAbove.Time - closestBelow.Time)
116
+
117
+ local labA = Oklab.fromSRGB(closestBelow.Value)
118
+ local labB = Oklab.fromSRGB(closestAbove.Value)
119
+
120
+ local blendedLab = labA:Lerp(labB, alpha)
121
+
122
+ return Oklab.toSRGB(blendedLab)
123
+ end
124
+
125
+ function common.getColorWithEasingOklab(sequence: ColorSequence, time: number, easingBezier)
126
+ local easedTime = 1 - easingBezier:getEase(math.clamp(time, 0, 1)).y
127
+ return common.getColorAtTimeOklab(sequence, math.clamp(easedTime, 0, 1))
128
+ end
129
+
130
+ export type HitboxConfig = {
131
+ enabled: boolean,
132
+ collisionGroup: string,
133
+ filterTag: string,
134
+ filterType: string,
135
+ ignoreCanCollide: boolean,
136
+ }
137
+
138
+ function common.createHitboxParams(config: HitboxConfig, parent: Instance, root: Attachment?): OverlapParams
139
+ local params = OverlapParams.new()
140
+ params.MaxParts = 1
141
+ params.FilterType = Enum.RaycastFilterType[config.filterType]
142
+ params.CollisionGroup = config.collisionGroup
143
+ params.RespectCanCollide = not config.ignoreCanCollide
144
+
145
+ params:AddToFilter(CollectionService:GetTagged(config.filterTag))
146
+
147
+ if config.filterType == "Exclude" then
148
+ local ancestorPart = root and root:FindFirstAncestorOfClass("Part")
149
+ params:AddToFilter({ workspace.Terrain, parent, ancestorPart })
150
+ end
151
+
152
+ return params
153
+ end
154
+
155
+ export type EmissionConfig = {
156
+ face: "InAndOut" | "Inward" | "Outward",
157
+ spreadAngle: Vector3,
158
+ mirror: boolean,
159
+ mirrorRot: Vector3,
160
+ partial: number,
161
+ emissionDirection: Enum.NormalId,
162
+ }
163
+
164
+ function common.calculateEmissionCFrame(
165
+ originCFrame: CFrame,
166
+ originSize: Vector3,
167
+ config: EmissionConfig,
168
+ drawFunc: ((...any) -> CFrame)?,
169
+ rng: Random,
170
+ endPoint: Attachment?,
171
+ isAttachment: boolean
172
+ )
173
+ local cf: CFrame
174
+
175
+ if isAttachment then
176
+ cf = originCFrame * CFrame.new(Vector3.zero, Vector3.FromNormalId(config.emissionDirection)).Rotation
177
+ elseif drawFunc then
178
+ cf = drawFunc(nil, originCFrame, originSize, config.emissionDirection, config.partial)
179
+ else
180
+ cf = originCFrame
181
+ end
182
+
183
+ local normal = Vector3.FromNormalId(config.emissionDirection)
184
+ local pitchAxis = normal:Cross(originCFrame.LookVector)
185
+
186
+ if pitchAxis.Magnitude < 0.001 then
187
+ pitchAxis = normal:Cross(originCFrame.UpVector)
188
+ end
189
+
190
+ local spread = CFrame.fromAxisAngle(normal, math.rad(rng:NextNumber(-config.spreadAngle.X, config.spreadAngle.X)))
191
+ * CFrame.fromAxisAngle(pitchAxis, math.rad(rng:NextNumber(-config.spreadAngle.Y, config.spreadAngle.Y)))
192
+
193
+ cf *= spread
194
+
195
+ if config.face == "Inward" or (config.face == "InAndOut" and rng:NextInteger(0, 1) == 1) then
196
+ cf *= CFrame.fromOrientation(0, math.pi, 0)
197
+ end
198
+
199
+ if endPoint and config.mirror and (cf.Position - endPoint.WorldPosition).Unit:Dot(cf.RightVector) >= 0 then
200
+ local rot = config.mirrorRot * utility.DEG_TO_RAD
201
+ cf *= CFrame.fromOrientation(rot.X, rot.Y, rot.Z)
202
+ end
203
+
204
+ return cf
205
+ end
206
+
207
+ function common.createBezierWithEndpoint(points: { vector }, cf: CFrame, endPoint: Attachment?, endT1: Attachment?)
208
+ if not endPoint then
209
+ return Bezier.new(points)
210
+ end
211
+
212
+ local pts = {}
213
+
214
+ for j, pt in points do
215
+ table.insert(
216
+ pts,
217
+ if j == (#points - 1)
218
+ then endT1 and endT1.WorldPosition or endPoint.WorldPosition
219
+ else if j == #points then endPoint.WorldPosition else cf * (pt - points[1])
220
+ )
221
+ end
222
+
223
+ return Bezier.new(pts)
224
+ end
225
+
226
+ function common.createPosGetter(
227
+ bezier,
228
+ points: { vector },
229
+ cf: CFrame,
230
+ endPoint: Attachment?,
231
+ useArcSpace: boolean?
232
+ ): (number) -> Vector3
233
+ return function(alpha: number): Vector3
234
+ local p = if useArcSpace ~= false then bezier:getPositionArcSpace(alpha) else bezier:getPosition(alpha)
235
+
236
+ if endPoint then
237
+ return p
238
+ else
239
+ return cf * (p - points[1])
240
+ end
241
+ end
242
+ end
243
+
244
+ function common.readCommonAttributes(ref: Attachment)
245
+ return {
246
+ -- timing
247
+ emitDelay = attr.get(ref, "EmitDelay", 0),
248
+ emitCount = attr.get(ref, "EmitCount", 1),
249
+ emitDuration = attr.get(ref, "EmitDuration", 0),
250
+ destroyDelay = attr.get(ref, "DestroyDelay", 0),
251
+ duration = attr.getRange(ref, "Duration", NumberRange.new(1, 1), NumberRange.new(0, math.huge)),
252
+
253
+ -- shape/emission
254
+ shapeType = attr.getEnum(ref, "Shape", "Box", { "Box", "Cylinder", "Sphere", "Disc" }),
255
+ shapeStyle = attr.getEnum(ref, "ShapeStyle", "Volume", { "Volume", "Surface" }),
256
+ emissionDirection = Enum.NormalId[attr.getEnum(
257
+ ref,
258
+ "EmissionDirection",
259
+ "Top",
260
+ { "Top", "Bottom", "Left", "Right", "Front", "Back" }
261
+ )],
262
+ face = attr.getEnum(ref, "ShapeFace", "Outward", { "InAndOut", "Inward", "Outward" }),
263
+ spreadAngle = attr.get(ref, "SpreadAngle", vector.zero),
264
+ partial = attr.get(ref, "ShapePartial", 1),
265
+
266
+ -- path
267
+ syncPosition = attr.get(ref, "SyncPosition", false),
268
+ mirror = attr.get(ref, "MirrorPaths", true),
269
+ mirrorRot = attr.get(ref, "MirrorRotation", vector.create(0, 0, 180)),
270
+
271
+ -- projectile
272
+ projectileEnabled = attr.get(ref, "ProjectileEnabled", false),
273
+ projectileMatchEnd = attr.get(ref, "MatchEndDirection", false),
274
+ projectileSpeed = attr.get(ref, "ProjectileSpeed", 30),
275
+ projectileLifetime = attr.getRange(ref, "ProjectileLifetime", NumberRange.new(1, 1), NumberRange.new(0, math.huge)),
276
+
277
+ -- hitbox
278
+ hitboxEnabled = attr.get(ref, "HitboxEnabled", false),
279
+ hitboxCollisionGroup = attr.get(ref, "HitboxCollisionGroup", "Default"),
280
+ hitboxFilterTag = attr.get(ref, "HitboxFilterTag", ""),
281
+ hitboxFilterType = attr.get(ref, "HitboxFilterType", "Exclude"),
282
+ hitboxIgnoreCanCollide = attr.get(ref, "HitboxIgnoreCanCollide", false),
283
+
284
+ -- speed
285
+ speedStart = attr.get(ref, "Speed_Start", 1),
286
+ speedEnd = attr.get(ref, "Speed_End", 1),
287
+ }
288
+ end
289
+
290
+ function common.getCurrentOriginCFrame(parent: Instance, fallback: CFrame)
291
+ if parent:IsA("BasePart") then
292
+ return parent.CFrame
293
+ elseif parent:IsA("Attachment") then
294
+ return parent.WorldCFrame
295
+ end
296
+
297
+ return fallback
298
+ end
299
+
300
+ function common.findEndAttachments(ref: Attachment): (Attachment?, Attachment?)
301
+ local endPoint = ref:FindFirstChild("End")
302
+ local endT1 = endPoint and endPoint:FindFirstChild("T1")
303
+
304
+ if endPoint and not endPoint:IsA("Attachment") then
305
+ endPoint = nil
306
+ end
307
+
308
+ if endT1 and not endT1:IsA("Attachment") then
309
+ endT1 = nil
310
+ end
311
+
312
+ return endPoint, endT1
313
+ end
314
+
315
+ function common.validateParent(ref: Attachment): Instance?
316
+ local parent = ref.Parent
317
+
318
+ if not parent then
319
+ return nil
320
+ end
321
+
322
+ if not parent:IsA("BasePart") and not parent:IsA("Attachment") then
323
+ parent = ref
324
+ end
325
+
326
+ return parent
327
+ end
328
+
329
+ function common.getPerpendicularVectors(tangent: Vector3)
330
+ local right = tangent:Cross(Vector3.yAxis)
331
+
332
+ if right.Magnitude < 0.001 then
333
+ right = tangent:Cross(Vector3.zAxis)
334
+ end
335
+
336
+ right = right.Unit
337
+
338
+ local up = tangent:Cross(right).Unit
339
+
340
+ return right, up
341
+ end
342
+
343
+ function common.createPropertyTween(
344
+ scope: any,
345
+ ref: Attachment,
346
+ attrName: string,
347
+ tweenDuration: number,
348
+ startVal: number,
349
+ endVal: number,
350
+ setter: (number) -> (),
351
+ getSpeed: () -> number,
352
+ speedTween: any?
353
+ )
354
+ if startVal == endVal then
355
+ return
356
+ end
357
+
358
+ table.insert(
359
+ scope,
360
+ tween.fromParams(
361
+ attr.get(ref, attrName .. "_Curve", utility.default_bezier),
362
+ tweenDuration,
363
+ function(alpha, deltaTime)
364
+ setter(utility.lerp(startVal, endVal, alpha))
365
+ return deltaTime * getSpeed()
366
+ end,
367
+ speedTween
368
+ )
369
+ )
370
+ end
371
+
372
+ return common
@@ -0,0 +1,102 @@
1
+ local RunService = game:GetService("RunService")
2
+ local CollectionService = game:GetService("CollectionService")
3
+
4
+ local common = {}
5
+
6
+ function common.serialize(ids: { number })
7
+ local buf = buffer.create(#ids * 8)
8
+
9
+ for i, id in ids do
10
+ buffer.writef64(buf, (i - 1) * 8, id)
11
+ end
12
+
13
+ return buf
14
+ end
15
+
16
+ function common.deserialize(data: string | buffer)
17
+ local buf = typeof(data) == "string" and buffer.fromstring(data) or data
18
+ local count = buffer.len(buf) / 8
19
+
20
+ local ids = {}
21
+
22
+ for i = 0, count - 1 do
23
+ table.insert(ids, buffer.readf64(buf, i * 8))
24
+ end
25
+
26
+ return ids
27
+ end
28
+
29
+ function common.isLocalFlipbook(ref: Instance)
30
+ if not RunService:IsStudio() then
31
+ return false
32
+ end
33
+
34
+ for _, tag in CollectionService:GetTags(ref) do
35
+ if tag:match("^_local_flipbook_") then
36
+ return true
37
+ end
38
+ end
39
+
40
+ return false
41
+ end
42
+
43
+ function common.getTexturePrefix(ref: Instance)
44
+ return common.isLocalFlipbook(ref) and "rbxtemp://" or "rbxassetid://"
45
+ end
46
+
47
+ function common.getFlipbookData(ref: Instance): { number }?
48
+ if not ref:GetAttribute("FlipbookEnabled") then
49
+ return nil
50
+ end
51
+
52
+ local data = ref:GetAttribute("FlipbookTextures")
53
+
54
+ if not data then
55
+ return nil
56
+ end
57
+
58
+ local frames = common.deserialize(data)
59
+
60
+ if #frames == 0 then
61
+ return nil
62
+ end
63
+
64
+ return frames
65
+ end
66
+
67
+ export type FlipbookConfig = {
68
+ ref: Instance,
69
+ curve: string,
70
+ speedTween: any?,
71
+
72
+ duration: number,
73
+ effectDuration: number,
74
+
75
+ frames: { number },
76
+
77
+ getSpeed: () -> number,
78
+ setTexture: (texture: string) -> (),
79
+ }
80
+
81
+ function common.getChangeDuration(config: FlipbookConfig)
82
+ local changeDuration = config.duration
83
+
84
+ if config.ref:GetAttribute("SyncDuration") then
85
+ changeDuration = config.effectDuration
86
+ end
87
+
88
+ return changeDuration
89
+ end
90
+
91
+ function common.createUpdateCallback(config: FlipbookConfig)
92
+ local prefix = common.getTexturePrefix(config.ref)
93
+
94
+ return function(alpha, deltaTime)
95
+ local index = math.max(math.round(#config.frames * alpha), 1)
96
+ config.setTexture(`{prefix}{config.frames[index]}`)
97
+
98
+ return deltaTime * config.getSpeed()
99
+ end
100
+ end
101
+
102
+ return common
@@ -0,0 +1,210 @@
1
+ local Oklab = require("@mod/color/Oklab")
2
+
3
+ local MAX_KEYPOINTS = 20
4
+
5
+ local lerp = {}
6
+
7
+ function lerp.number(a: number, b: number, t: number)
8
+ return a + (b - a) * t
9
+ end
10
+
11
+ function lerp.Vector3(a: Vector3, b: Vector3, t: number)
12
+ return a:Lerp(b, t)
13
+ end
14
+
15
+ function lerp.Vector2(a: Vector2, b: Vector2, t: number)
16
+ return a:Lerp(b, t)
17
+ end
18
+
19
+ function lerp.CFrame(a: CFrame, b: CFrame, t: number)
20
+ return a:Lerp(b, t)
21
+ end
22
+
23
+ function lerp.UDim2(a: UDim2, b: UDim2, t: number)
24
+ return a:Lerp(b, t)
25
+ end
26
+
27
+ function lerp.UDim(a: UDim, b: UDim, t: number)
28
+ -- stylua: ignore
29
+ return UDim.new(
30
+ lerp.number(a.Scale, b.Scale, t),
31
+ lerp.number(a.Offset, b.Offset, t)
32
+ )
33
+ end
34
+
35
+ function lerp.NumberRange(a: NumberRange, b: NumberRange, t: number)
36
+ -- stylua: ignore
37
+ return NumberRange.new(
38
+ lerp.number(a.Min, b.Min, t),
39
+ lerp.number(a.Max, b.Max, t)
40
+ )
41
+ end
42
+
43
+ function lerp.Color3(a: Color3, b: Color3, t: number)
44
+ -- stylua: ignore
45
+ return Oklab.toSRGB(
46
+ Oklab.fromSRGB(a):Lerp(Oklab.fromSRGB(b), t)
47
+ )
48
+ end
49
+
50
+ function lerp.PhysicalProperties(a: PhysicalProperties, b: PhysicalProperties, t: number)
51
+ return PhysicalProperties.new(
52
+ lerp.number(a.Density, b.Density, t),
53
+ lerp.number(a.Friction, b.Friction, t),
54
+ lerp.number(a.Elasticity, b.Elasticity, t),
55
+ lerp.number(a.FrictionWeight, b.FrictionWeight, t),
56
+ lerp.number(a.ElasticityWeight, b.ElasticityWeight, t)
57
+ )
58
+ end
59
+
60
+ function lerp.Rect(a: Rect, b: Rect, t: number)
61
+ return Rect.new(a.Min:Lerp(b.Min, t), a.Max:Lerp(b.Max, t))
62
+ end
63
+
64
+ -- credit https://github.com/boatbomber/BoatTween
65
+ function lerp.NumberSequence(a: NumberSequence, b: NumberSequence, t: number)
66
+ local keypoints = {}
67
+ local addedTimes = {}
68
+
69
+ local keylength = 0
70
+
71
+ for _, ap in a.Keypoints do
72
+ local closestAbove, closestBelow
73
+
74
+ for _, bp in b.Keypoints do
75
+ if bp.Time == ap.Time then
76
+ closestAbove, closestBelow = bp, bp
77
+ break
78
+ elseif bp.Time < ap.Time and (closestBelow == nil or bp.Time > closestBelow.Time) then
79
+ closestBelow = bp
80
+ elseif bp.Time > ap.Time and (closestAbove == nil or bp.Time < closestAbove.Time) then
81
+ closestAbove = bp
82
+ end
83
+ end
84
+
85
+ local bValue, bEnvelope
86
+
87
+ if closestAbove == closestBelow then
88
+ bValue, bEnvelope = closestAbove.Value, closestAbove.Envelope
89
+ else
90
+ local p = (ap.Time - closestBelow.Time) / (closestAbove.Time - closestBelow.Time)
91
+ bValue = (closestAbove.Value - closestBelow.Value) * p + closestBelow.Value
92
+ bEnvelope = (closestAbove.Envelope - closestBelow.Envelope) * p + closestBelow.Envelope
93
+ end
94
+
95
+ keylength += 1
96
+ keypoints[keylength] = NumberSequenceKeypoint.new(
97
+ ap.Time,
98
+ (bValue - ap.Value) * t + ap.Value,
99
+ (bEnvelope - ap.Envelope) * t + ap.Envelope
100
+ )
101
+
102
+ addedTimes[ap.Time] = true
103
+ end
104
+
105
+ for _, bp in b.Keypoints do
106
+ if not addedTimes[bp.Time] then
107
+ local closestAbove, closestBelow
108
+
109
+ for _, ap in a.Keypoints do
110
+ if ap.Time == bp.Time then
111
+ closestAbove, closestBelow = ap, ap
112
+ break
113
+ elseif ap.Time < bp.Time and (closestBelow == nil or ap.Time > closestBelow.Time) then
114
+ closestBelow = ap
115
+ elseif ap.Time > bp.Time and (closestAbove == nil or ap.Time < closestAbove.Time) then
116
+ closestAbove = ap
117
+ end
118
+ end
119
+
120
+ local aValue, aEnvelope
121
+
122
+ if closestAbove == closestBelow then
123
+ aValue, aEnvelope = closestAbove.Value, closestAbove.Envelope
124
+ else
125
+ local p = (bp.Time - closestBelow.Time) / (closestAbove.Time - closestBelow.Time)
126
+ aValue = (closestAbove.Value - closestBelow.Value) * p + closestBelow.Value
127
+ aEnvelope = (closestAbove.Envelope - closestBelow.Envelope) * p + closestBelow.Envelope
128
+ end
129
+
130
+ keylength += 1
131
+ keypoints[keylength] =
132
+ NumberSequenceKeypoint.new(bp.Time, (bp.Value - aValue) * t + aValue, (bp.Envelope - aEnvelope) * t + aEnvelope)
133
+ end
134
+ end
135
+
136
+ table.sort(keypoints, function(a, b)
137
+ return a.Time < b.Time
138
+ end)
139
+
140
+ local finalKeypoints
141
+
142
+ if #keypoints > MAX_KEYPOINTS then
143
+ finalKeypoints = {}
144
+ local step = (#keypoints - 1) / (MAX_KEYPOINTS - 1)
145
+
146
+ for i = 0, MAX_KEYPOINTS - 1 do
147
+ local index = math.floor(i * step + 1)
148
+ table.insert(finalKeypoints, keypoints[index])
149
+ end
150
+
151
+ if finalKeypoints[#finalKeypoints].Time < keypoints[#keypoints].Time then
152
+ finalKeypoints[#finalKeypoints] = keypoints[#keypoints]
153
+ end
154
+ else
155
+ finalKeypoints = keypoints
156
+ end
157
+
158
+ return NumberSequence.new(finalKeypoints)
159
+ end
160
+
161
+ local function getColorAtTime(sequence: ColorSequence, time: number)
162
+ local keypoints = sequence.Keypoints
163
+
164
+ if time <= keypoints[1].Time then
165
+ return keypoints[1].Value
166
+ end
167
+
168
+ if time >= keypoints[#keypoints].Time then
169
+ return keypoints[#keypoints].Value
170
+ end
171
+
172
+ local closestBelow
173
+ local closestAbove
174
+
175
+ for i = 1, #keypoints do
176
+ local kp = keypoints[i]
177
+ if kp.Time == time then
178
+ return kp.Value
179
+ elseif kp.Time < time then
180
+ closestBelow = kp
181
+ elseif kp.Time > time then
182
+ closestAbove = kp
183
+ break
184
+ end
185
+ end
186
+
187
+ local alpha = (time - closestBelow.Time) / (closestAbove.Time - closestBelow.Time)
188
+
189
+ return closestBelow.Value:Lerp(closestAbove.Value, alpha)
190
+ end
191
+
192
+ function lerp.ColorSequence(a: ColorSequence, b: ColorSequence, t: number)
193
+ local newKeypoints = {}
194
+
195
+ for _, bp in ipairs(b.Keypoints) do
196
+ local aValueAtBTime = getColorAtTime(a, bp.Time)
197
+ -- use linear space because color sequences are in linear space anyway
198
+ local finalColor = Oklab.toSRGB(Oklab.fromSRGB(aValueAtBTime:Lerp(bp.Value, t)))
199
+
200
+ table.insert(newKeypoints, ColorSequenceKeypoint.new(bp.Time, finalColor))
201
+ end
202
+
203
+ return ColorSequence.new(newKeypoints)
204
+ end
205
+
206
+ function lerp.Other(a: any, b: any, t: number)
207
+ return t < 0.5 and a or b
208
+ end
209
+
210
+ return lerp
@@ -0,0 +1,20 @@
1
+ local function msg(...: string)
2
+ return `[Forge Emit API]: {table.concat({ ... }, " ")}`
3
+ end
4
+
5
+ local logger = {}
6
+
7
+ function logger.error(...)
8
+ error(msg(..., "\n"))
9
+ end
10
+
11
+ function logger.warn(...)
12
+ warn(msg(...))
13
+ warn(msg(debug.traceback("stack trace:")))
14
+ end
15
+
16
+ function logger.info(...)
17
+ print(msg(...))
18
+ end
19
+
20
+ return logger