@el4cteo/rbx-studio-mcp 0.3.0 → 0.3.5
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.
- package/README.md +20 -26
- package/dist/index.js +1 -1
- package/dist/lib/format.js +18 -2
- package/dist/lib/format.js.map +1 -1
- package/dist/lib/pluginbuild.js +23 -13
- package/dist/lib/pluginbuild.js.map +1 -1
- package/dist/tools/api.js +20 -1
- package/dist/tools/api.js.map +1 -1
- package/dist/tools/debug.js +18 -8
- package/dist/tools/debug.js.map +1 -1
- package/dist/tools/discover.js +5 -0
- package/dist/tools/discover.js.map +1 -1
- package/dist/tools/instances.js +49 -2
- package/dist/tools/instances.js.map +1 -1
- package/dist/tools/perf.js +11 -1
- package/dist/tools/perf.js.map +1 -1
- package/dist/tools/scripts.js +27 -3
- package/dist/tools/scripts.js.map +1 -1
- package/dist/tools/session.js +16 -0
- package/dist/tools/session.js.map +1 -1
- package/package.json +1 -1
- package/plugin/src/Config.luau +1 -1
- package/plugin/src/Console.luau +257 -116
- package/plugin/src/Paths.luau +138 -69
- package/plugin/src/Serialize.luau +43 -3
- package/plugin/src/ThemePicker.luau +458 -0
- package/plugin/src/Themes/Aurora.luau +147 -0
- package/plugin/src/Themes/Blueprint.luau +213 -0
- package/plugin/src/Themes/Draw.luau +177 -0
- package/plugin/src/Themes/Lattice.luau +304 -0
- package/plugin/src/Themes/Nebula.luau +182 -0
- package/plugin/src/Themes/Observatory.luau +200 -0
- package/plugin/src/Themes/Orbit.luau +268 -0
- package/plugin/src/Themes/Phosphor.luau +200 -0
- package/plugin/src/Themes/Theme.luau +121 -0
- package/plugin/src/Themes/Void.luau +230 -0
- package/plugin/src/Themes/init.luau +116 -0
- package/plugin/src/Visuals.luau +788 -907
- package/plugin/src/handlers/Discover.luau +75 -1
- package/plugin/src/handlers/Exec.luau +17 -5
- package/plugin/src/handlers/Instances.luau +35 -3
- package/plugin/src/handlers/Scripts.luau +58 -2
- package/plugin/src/init.server.luau +386 -354
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--[[
|
|
3
|
+
The preset drawer: a tab on the panel's right edge that opens on hover.
|
|
4
|
+
|
|
5
|
+
Kept out of the header on purpose. The header is where connection facts live
|
|
6
|
+
and it is already carrying a status light, an address, a client badge and
|
|
7
|
+
three buttons; a ninth control there would push something off the edge on a
|
|
8
|
+
narrow dock. A theme is also not something anyone changes twice in a
|
|
9
|
+
session, so it has no claim on permanent width -- an edge tab costs fourteen
|
|
10
|
+
pixels and is invisible until you go looking for it.
|
|
11
|
+
|
|
12
|
+
Hover rather than click to open, because the drawer is a preview as much as
|
|
13
|
+
a menu: each row wears its own preset's background, text colour and cell
|
|
14
|
+
miniature, so running the pointer down the list shows you the eight themes
|
|
15
|
+
rather than eight names. Clicking picks; leaving closes.
|
|
16
|
+
|
|
17
|
+
The grace period on close is the part that makes it usable. Without it, the
|
|
18
|
+
pointer crossing the one-pixel gap between the tab and the drawer counts as
|
|
19
|
+
leaving, and the menu shuts under the hand reaching for it.
|
|
20
|
+
]]
|
|
21
|
+
|
|
22
|
+
local TweenService = game:GetService("TweenService")
|
|
23
|
+
|
|
24
|
+
local Themes = require(script.Parent.Themes)
|
|
25
|
+
|
|
26
|
+
local ThemePicker = {}
|
|
27
|
+
|
|
28
|
+
local TAB_WIDTH = 14
|
|
29
|
+
local TAB_HEIGHT = 48
|
|
30
|
+
local DRAWER_WIDTH = 196
|
|
31
|
+
local ROW_HEIGHT = 34
|
|
32
|
+
local PADDING = 6
|
|
33
|
+
|
|
34
|
+
-- Long enough to cross the gap between tab and drawer without thinking about
|
|
35
|
+
-- it, short enough that a drawer left open behind the pointer still closes.
|
|
36
|
+
local GRACE = 0.4
|
|
37
|
+
|
|
38
|
+
--[[
|
|
39
|
+
Explicit depths for every layer of the drawer.
|
|
40
|
+
|
|
41
|
+
A plugin widget draws with ZIndexBehavior.Global, where a child does NOT
|
|
42
|
+
inherit its parent's depth -- so the rows, built with the default ZIndex of
|
|
43
|
+
1, rendered UNDER the drawer's own background at 30 and the menu opened
|
|
44
|
+
empty. Nothing about that is visible in the code unless you already know the
|
|
45
|
+
rule, so the depths are named rather than sprinkled as numbers.
|
|
46
|
+
]]
|
|
47
|
+
local Z_DRAWER = 30
|
|
48
|
+
local Z_LIST = 31
|
|
49
|
+
local Z_ROW = 32
|
|
50
|
+
local Z_ROW_CONTENT = 33
|
|
51
|
+
local Z_TAB = 36
|
|
52
|
+
local Z_VEIL = 50
|
|
53
|
+
|
|
54
|
+
--[[
|
|
55
|
+
Stamps a depth on an Instance and everything inside it.
|
|
56
|
+
|
|
57
|
+
Needed because a preset draws its own swatch and has no reason to know it is
|
|
58
|
+
being drawn inside a menu. Rather than adding a ZIndex argument to the theme
|
|
59
|
+
contract -- which would put a menu detail into eight renderers -- the swatch
|
|
60
|
+
is drawn first and pushed to the right depth afterwards.
|
|
61
|
+
]]
|
|
62
|
+
local function stack(instance: Instance, depth: number)
|
|
63
|
+
if instance:IsA("GuiObject") then
|
|
64
|
+
instance.ZIndex = depth
|
|
65
|
+
end
|
|
66
|
+
for _, child in instance:GetDescendants() do
|
|
67
|
+
if child:IsA("GuiObject") then
|
|
68
|
+
child.ZIndex = depth
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
local OPEN = TweenInfo.new(0.22, Enum.EasingStyle.Quint, Enum.EasingDirection.Out)
|
|
74
|
+
local SHUT = TweenInfo.new(0.16, Enum.EasingStyle.Quint, Enum.EasingDirection.In)
|
|
75
|
+
|
|
76
|
+
type Runtime = {
|
|
77
|
+
drawer: Frame?,
|
|
78
|
+
tab: TextButton?,
|
|
79
|
+
chevron: TextLabel?,
|
|
80
|
+
rows: { { frame: Frame, id: string, marker: TextLabel } },
|
|
81
|
+
open: boolean,
|
|
82
|
+
-- Bumped on every hover, so a grace timer from an earlier one cannot close
|
|
83
|
+
-- a drawer the pointer has since come back to.
|
|
84
|
+
generation: number,
|
|
85
|
+
root: Frame?,
|
|
86
|
+
onPicked: ((string) -> ())?,
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
local runtime: Runtime = {
|
|
90
|
+
drawer = nil,
|
|
91
|
+
tab = nil,
|
|
92
|
+
chevron = nil,
|
|
93
|
+
rows = {},
|
|
94
|
+
open = false,
|
|
95
|
+
generation = 0,
|
|
96
|
+
root = nil,
|
|
97
|
+
onPicked = nil,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
local function drawerHeight(): number
|
|
101
|
+
return #Themes.list() * ROW_HEIGHT + PADDING * 2
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
local function setOpen(open: boolean)
|
|
105
|
+
local drawer = runtime.drawer
|
|
106
|
+
local chevron = runtime.chevron
|
|
107
|
+
if drawer == nil then
|
|
108
|
+
return
|
|
109
|
+
end
|
|
110
|
+
if runtime.open == open then
|
|
111
|
+
return
|
|
112
|
+
end
|
|
113
|
+
runtime.open = open
|
|
114
|
+
|
|
115
|
+
--[[
|
|
116
|
+
Size and position tween together, with the drawer anchored to its own
|
|
117
|
+
right edge at the tab's height. That is what makes it appear to grow out
|
|
118
|
+
of the tab rather than slide in from off-screen: at width zero it is
|
|
119
|
+
exactly where the pointer is.
|
|
120
|
+
]]
|
|
121
|
+
local height = drawerHeight()
|
|
122
|
+
TweenService:Create(drawer, if open then OPEN else SHUT, {
|
|
123
|
+
Size = UDim2.fromOffset(if open then DRAWER_WIDTH else 0, height),
|
|
124
|
+
}):Play()
|
|
125
|
+
|
|
126
|
+
if chevron then
|
|
127
|
+
-- Points the way it will move: into the panel when it will open, back
|
|
128
|
+
-- out at the edge when it will close.
|
|
129
|
+
TweenService:Create(chevron, if open then OPEN else SHUT, {
|
|
130
|
+
Rotation = if open then 180 else 0,
|
|
131
|
+
}):Play()
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
--[[
|
|
136
|
+
Closes after the grace period, unless the pointer came back.
|
|
137
|
+
]]
|
|
138
|
+
local function scheduleClose()
|
|
139
|
+
runtime.generation += 1
|
|
140
|
+
local generation = runtime.generation
|
|
141
|
+
task.delay(GRACE, function()
|
|
142
|
+
if runtime.generation == generation then
|
|
143
|
+
setOpen(false)
|
|
144
|
+
end
|
|
145
|
+
end)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
local function hold()
|
|
149
|
+
runtime.generation += 1
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
--[[
|
|
153
|
+
Fades the panel through its own background while the palette changes.
|
|
154
|
+
|
|
155
|
+
A theme switch repaints thirty Instances and re-renders three hundred log
|
|
156
|
+
rows, and doing that in one frame reads as a flicker -- the eye catches the
|
|
157
|
+
half-repainted state even though there is not one. A third of a second of
|
|
158
|
+
the outgoing background over the top hides the seam and, more usefully, says
|
|
159
|
+
that something deliberate just happened.
|
|
160
|
+
]]
|
|
161
|
+
local function crossfade(root: Frame, from: Color3, apply: () -> ())
|
|
162
|
+
local veil = Instance.new("Frame")
|
|
163
|
+
veil.Name = "ThemeFade"
|
|
164
|
+
veil.Size = UDim2.fromScale(1, 1)
|
|
165
|
+
veil.BackgroundColor3 = from
|
|
166
|
+
veil.BackgroundTransparency = 0.15
|
|
167
|
+
veil.BorderSizePixel = 0
|
|
168
|
+
veil.ZIndex = Z_VEIL
|
|
169
|
+
veil.Parent = root
|
|
170
|
+
|
|
171
|
+
apply()
|
|
172
|
+
|
|
173
|
+
local fade = TweenService:Create(
|
|
174
|
+
veil,
|
|
175
|
+
TweenInfo.new(0.3, Enum.EasingStyle.Quad, Enum.EasingDirection.Out),
|
|
176
|
+
{ BackgroundTransparency = 1 }
|
|
177
|
+
)
|
|
178
|
+
fade.Completed:Connect(function()
|
|
179
|
+
veil:Destroy()
|
|
180
|
+
end)
|
|
181
|
+
fade:Play()
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
local function markActive()
|
|
185
|
+
local activeId = Themes.activeId()
|
|
186
|
+
for _, row in runtime.rows do
|
|
187
|
+
row.marker.Text = if row.id == activeId then "\u{25C6}" else ""
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
--[[
|
|
192
|
+
One row: the preset's own background, its cell miniature, its name.
|
|
193
|
+
|
|
194
|
+
Drawn in the preset's palette rather than the console's, which is the whole
|
|
195
|
+
reason the drawer is worth opening. A list of eight words in the current
|
|
196
|
+
theme's colours would tell you nothing you could not have guessed.
|
|
197
|
+
]]
|
|
198
|
+
local function buildRow(parent: Instance, theme: Themes.Theme, order: number)
|
|
199
|
+
local palette = theme.palette
|
|
200
|
+
|
|
201
|
+
local row = Instance.new("TextButton")
|
|
202
|
+
row.Name = theme.id
|
|
203
|
+
row.Text = ""
|
|
204
|
+
row.AutoButtonColor = false
|
|
205
|
+
row.BackgroundColor3 = palette.background
|
|
206
|
+
row.BorderSizePixel = 0
|
|
207
|
+
row.Size = UDim2.new(1, 0, 0, ROW_HEIGHT - 2)
|
|
208
|
+
row.LayoutOrder = order
|
|
209
|
+
row.ZIndex = Z_ROW
|
|
210
|
+
row.Parent = parent
|
|
211
|
+
|
|
212
|
+
local corner = Instance.new("UICorner")
|
|
213
|
+
corner.CornerRadius = UDim.new(0, 3)
|
|
214
|
+
corner.Parent = row
|
|
215
|
+
|
|
216
|
+
local stroke = Instance.new("UIStroke")
|
|
217
|
+
stroke.Color = palette.rule
|
|
218
|
+
stroke.Transparency = 0.75
|
|
219
|
+
stroke.Parent = row
|
|
220
|
+
|
|
221
|
+
--[[
|
|
222
|
+
The miniature. Clipped, because a preset draws to the edges of whatever
|
|
223
|
+
it is given and a swatch that spills over the row would land on its
|
|
224
|
+
neighbour.
|
|
225
|
+
]]
|
|
226
|
+
local swatch = Instance.new("Frame")
|
|
227
|
+
swatch.Name = "Swatch"
|
|
228
|
+
swatch.AnchorPoint = Vector2.new(0, 0.5)
|
|
229
|
+
swatch.Position = UDim2.new(0, 6, 0.5, 0)
|
|
230
|
+
swatch.Size = UDim2.fromOffset(40, 24)
|
|
231
|
+
swatch.BackgroundColor3 = palette.surface
|
|
232
|
+
swatch.BorderSizePixel = 0
|
|
233
|
+
swatch.ClipsDescendants = true
|
|
234
|
+
swatch.ZIndex = Z_ROW_CONTENT
|
|
235
|
+
swatch.Parent = row
|
|
236
|
+
|
|
237
|
+
local swatchCorner = Instance.new("UICorner")
|
|
238
|
+
swatchCorner.CornerRadius = UDim.new(0, 2)
|
|
239
|
+
swatchCorner.Parent = swatch
|
|
240
|
+
|
|
241
|
+
-- A preset that cannot draw its own thumbnail must not take the menu with
|
|
242
|
+
-- it; an empty swatch is a bad row, a broken drawer is a broken plugin.
|
|
243
|
+
local ok, err = pcall(theme.swatch, swatch, palette)
|
|
244
|
+
if not ok then
|
|
245
|
+
warn(string.format("[rbx-studio] theme %q failed to draw a swatch: %s", theme.id, tostring(err)))
|
|
246
|
+
end
|
|
247
|
+
-- Whatever the preset just built is at depth 1 and would be invisible.
|
|
248
|
+
stack(swatch, Z_ROW_CONTENT)
|
|
249
|
+
|
|
250
|
+
local label = Instance.new("TextLabel")
|
|
251
|
+
label.Name = "Label"
|
|
252
|
+
label.BackgroundTransparency = 1
|
|
253
|
+
label.Position = UDim2.new(0, 52, 0, 4)
|
|
254
|
+
label.Size = UDim2.new(1, -70, 0, 13)
|
|
255
|
+
label.Font = Enum.Font.Code
|
|
256
|
+
label.TextSize = 12
|
|
257
|
+
label.TextColor3 = palette.text
|
|
258
|
+
label.TextXAlignment = Enum.TextXAlignment.Left
|
|
259
|
+
label.Text = theme.label
|
|
260
|
+
label.ZIndex = Z_ROW_CONTENT
|
|
261
|
+
label.Parent = row
|
|
262
|
+
|
|
263
|
+
local blurb = Instance.new("TextLabel")
|
|
264
|
+
blurb.Name = "Blurb"
|
|
265
|
+
blurb.BackgroundTransparency = 1
|
|
266
|
+
blurb.Position = UDim2.new(0, 52, 0, 16)
|
|
267
|
+
blurb.Size = UDim2.new(1, -70, 0, 12)
|
|
268
|
+
blurb.Font = Enum.Font.Code
|
|
269
|
+
blurb.TextSize = 10
|
|
270
|
+
blurb.TextColor3 = palette.dim
|
|
271
|
+
blurb.TextXAlignment = Enum.TextXAlignment.Left
|
|
272
|
+
blurb.TextTruncate = Enum.TextTruncate.AtEnd
|
|
273
|
+
blurb.Text = theme.blurb
|
|
274
|
+
blurb.ZIndex = Z_ROW_CONTENT
|
|
275
|
+
blurb.Parent = row
|
|
276
|
+
|
|
277
|
+
local marker = Instance.new("TextLabel")
|
|
278
|
+
marker.Name = "Marker"
|
|
279
|
+
marker.AnchorPoint = Vector2.new(1, 0.5)
|
|
280
|
+
marker.Position = UDim2.new(1, -6, 0.5, 0)
|
|
281
|
+
marker.Size = UDim2.fromOffset(12, 12)
|
|
282
|
+
marker.BackgroundTransparency = 1
|
|
283
|
+
marker.Font = Enum.Font.Code
|
|
284
|
+
marker.TextSize = 11
|
|
285
|
+
marker.TextColor3 = palette.rule
|
|
286
|
+
marker.Text = ""
|
|
287
|
+
marker.ZIndex = Z_ROW_CONTENT
|
|
288
|
+
marker.Parent = row
|
|
289
|
+
|
|
290
|
+
row.MouseEnter:Connect(function()
|
|
291
|
+
hold()
|
|
292
|
+
stroke.Transparency = 0.2
|
|
293
|
+
end)
|
|
294
|
+
row.MouseLeave:Connect(function()
|
|
295
|
+
stroke.Transparency = 0.75
|
|
296
|
+
end)
|
|
297
|
+
|
|
298
|
+
row.MouseButton1Click:Connect(function()
|
|
299
|
+
local root = runtime.root
|
|
300
|
+
if root == nil then
|
|
301
|
+
return
|
|
302
|
+
end
|
|
303
|
+
-- Re-picking the current theme is a no-op rather than a free repaint of
|
|
304
|
+
-- three hundred log rows.
|
|
305
|
+
if not Themes.use(theme.id) then
|
|
306
|
+
setOpen(false)
|
|
307
|
+
return
|
|
308
|
+
end
|
|
309
|
+
local previous = root.BackgroundColor3
|
|
310
|
+
setOpen(false)
|
|
311
|
+
crossfade(root, previous, function()
|
|
312
|
+
local handler = runtime.onPicked
|
|
313
|
+
if handler then
|
|
314
|
+
handler(theme.id)
|
|
315
|
+
end
|
|
316
|
+
end)
|
|
317
|
+
markActive()
|
|
318
|
+
end)
|
|
319
|
+
|
|
320
|
+
table.insert(runtime.rows, { frame = row, id = theme.id, marker = marker })
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
--[[
|
|
324
|
+
Builds the tab and the drawer.
|
|
325
|
+
|
|
326
|
+
`onPicked` is called with the new id AFTER `Themes.use` has switched, so the
|
|
327
|
+
caller repaints and persists rather than this module knowing how either is
|
|
328
|
+
done -- `plugin:SetSetting` is not reachable from a ModuleScript anyway.
|
|
329
|
+
]]
|
|
330
|
+
function ThemePicker.mount(root: Frame, onPicked: (string) -> ())
|
|
331
|
+
runtime.root = root
|
|
332
|
+
runtime.onPicked = onPicked
|
|
333
|
+
runtime.rows = {}
|
|
334
|
+
|
|
335
|
+
local palette = Themes.palette()
|
|
336
|
+
|
|
337
|
+
local drawer = Instance.new("Frame")
|
|
338
|
+
drawer.Name = "ThemeDrawer"
|
|
339
|
+
-- Anchored to its own right edge at the tab's height: at width zero it is a
|
|
340
|
+
-- line where the tab is, which is what it grows out of.
|
|
341
|
+
drawer.AnchorPoint = Vector2.new(1, 0.5)
|
|
342
|
+
drawer.Position = UDim2.new(1, -TAB_WIDTH, 0.5, 0)
|
|
343
|
+
drawer.Size = UDim2.fromOffset(0, drawerHeight())
|
|
344
|
+
drawer.BackgroundColor3 = palette.surface
|
|
345
|
+
drawer.BorderSizePixel = 0
|
|
346
|
+
drawer.ClipsDescendants = true
|
|
347
|
+
drawer.ZIndex = Z_DRAWER
|
|
348
|
+
drawer.Parent = root
|
|
349
|
+
runtime.drawer = drawer
|
|
350
|
+
|
|
351
|
+
local drawerCorner = Instance.new("UICorner")
|
|
352
|
+
drawerCorner.CornerRadius = UDim.new(0, 4)
|
|
353
|
+
drawerCorner.Parent = drawer
|
|
354
|
+
|
|
355
|
+
local drawerStroke = Instance.new("UIStroke")
|
|
356
|
+
drawerStroke.Color = palette.rule
|
|
357
|
+
drawerStroke.Transparency = 0.6
|
|
358
|
+
drawerStroke.Parent = drawer
|
|
359
|
+
|
|
360
|
+
local list = Instance.new("Frame")
|
|
361
|
+
list.Name = "Rows"
|
|
362
|
+
list.Position = UDim2.fromOffset(PADDING, PADDING)
|
|
363
|
+
-- Sized to the open width rather than to the parent, so the rows do not
|
|
364
|
+
-- squash and reflow through every frame of the tween.
|
|
365
|
+
list.Size = UDim2.fromOffset(DRAWER_WIDTH - PADDING * 2, drawerHeight() - PADDING * 2)
|
|
366
|
+
list.BackgroundTransparency = 1
|
|
367
|
+
list.ZIndex = Z_LIST
|
|
368
|
+
list.Parent = drawer
|
|
369
|
+
|
|
370
|
+
local layout = Instance.new("UIListLayout")
|
|
371
|
+
layout.FillDirection = Enum.FillDirection.Vertical
|
|
372
|
+
layout.Padding = UDim.new(0, 2)
|
|
373
|
+
layout.SortOrder = Enum.SortOrder.LayoutOrder
|
|
374
|
+
layout.Parent = list
|
|
375
|
+
|
|
376
|
+
for order, theme in Themes.list() do
|
|
377
|
+
buildRow(list, theme, order)
|
|
378
|
+
end
|
|
379
|
+
markActive()
|
|
380
|
+
|
|
381
|
+
local tab = Instance.new("TextButton")
|
|
382
|
+
tab.Name = "ThemeTab"
|
|
383
|
+
tab.Text = ""
|
|
384
|
+
tab.AutoButtonColor = false
|
|
385
|
+
tab.AnchorPoint = Vector2.new(1, 0.5)
|
|
386
|
+
tab.Position = UDim2.new(1, 0, 0.5, 0)
|
|
387
|
+
tab.Size = UDim2.fromOffset(TAB_WIDTH, TAB_HEIGHT)
|
|
388
|
+
tab.BackgroundColor3 = palette.surface
|
|
389
|
+
tab.BorderSizePixel = 0
|
|
390
|
+
tab.ZIndex = Z_TAB
|
|
391
|
+
tab.Parent = root
|
|
392
|
+
runtime.tab = tab
|
|
393
|
+
|
|
394
|
+
local tabCorner = Instance.new("UICorner")
|
|
395
|
+
tabCorner.CornerRadius = UDim.new(0, 3)
|
|
396
|
+
tabCorner.Parent = tab
|
|
397
|
+
|
|
398
|
+
local chevron = Instance.new("TextLabel")
|
|
399
|
+
chevron.Name = "Chevron"
|
|
400
|
+
chevron.AnchorPoint = Vector2.new(0.5, 0.5)
|
|
401
|
+
chevron.Position = UDim2.fromScale(0.5, 0.5)
|
|
402
|
+
chevron.Size = UDim2.fromOffset(TAB_WIDTH, TAB_WIDTH)
|
|
403
|
+
chevron.BackgroundTransparency = 1
|
|
404
|
+
chevron.Font = Enum.Font.Code
|
|
405
|
+
chevron.TextSize = 12
|
|
406
|
+
chevron.TextColor3 = palette.rule
|
|
407
|
+
chevron.Text = "\u{25C0}"
|
|
408
|
+
chevron.ZIndex = Z_TAB + 1
|
|
409
|
+
chevron.Parent = tab
|
|
410
|
+
runtime.chevron = chevron
|
|
411
|
+
|
|
412
|
+
tab.MouseEnter:Connect(function()
|
|
413
|
+
hold()
|
|
414
|
+
setOpen(true)
|
|
415
|
+
end)
|
|
416
|
+
tab.MouseLeave:Connect(scheduleClose)
|
|
417
|
+
drawer.MouseEnter:Connect(hold)
|
|
418
|
+
drawer.MouseLeave:Connect(scheduleClose)
|
|
419
|
+
|
|
420
|
+
-- Clicking works too. Hover is the affordance, but a tab that only responds
|
|
421
|
+
-- to hover is unreachable to anyone driving Studio from the keyboard or a
|
|
422
|
+
-- tablet, and this costs one line.
|
|
423
|
+
tab.MouseButton1Click:Connect(function()
|
|
424
|
+
hold()
|
|
425
|
+
setOpen(not runtime.open)
|
|
426
|
+
end)
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
--[[
|
|
430
|
+
Recolours the tab and drawer chrome after a switch.
|
|
431
|
+
|
|
432
|
+
The ROWS are deliberately left alone: each one wears its own preset's
|
|
433
|
+
palette and always did, which is what makes the drawer a preview rather
|
|
434
|
+
than a list.
|
|
435
|
+
]]
|
|
436
|
+
function ThemePicker.applyTheme()
|
|
437
|
+
local palette = Themes.palette()
|
|
438
|
+
local drawer = runtime.drawer
|
|
439
|
+
local tab = runtime.tab
|
|
440
|
+
local chevron = runtime.chevron
|
|
441
|
+
|
|
442
|
+
if drawer then
|
|
443
|
+
drawer.BackgroundColor3 = palette.surface
|
|
444
|
+
local stroke = drawer:FindFirstChildOfClass("UIStroke")
|
|
445
|
+
if stroke then
|
|
446
|
+
stroke.Color = palette.rule
|
|
447
|
+
end
|
|
448
|
+
end
|
|
449
|
+
if tab then
|
|
450
|
+
tab.BackgroundColor3 = palette.surface
|
|
451
|
+
end
|
|
452
|
+
if chevron then
|
|
453
|
+
chevron.TextColor3 = palette.rule
|
|
454
|
+
end
|
|
455
|
+
markActive()
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
return ThemePicker
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--[[
|
|
3
|
+
Aurora -- load as an amplitude.
|
|
4
|
+
|
|
5
|
+
A curtain of vertical rays whose height follows a travelling wave. The wave
|
|
6
|
+
is always there; what the session changes is how violent it is. Idle, it is
|
|
7
|
+
a shallow ripple you have to look at to notice. Under load it becomes a
|
|
8
|
+
curtain that fills the cell. Nothing here counts or ranks anything, which is
|
|
9
|
+
the point: it answers "how hard is this working" continuously, where Lattice
|
|
10
|
+
answers it in four steps and Orbit in six.
|
|
11
|
+
|
|
12
|
+
Two waves at different rates are summed rather than one, because a single
|
|
13
|
+
sine is a loading bar lying on its side -- the interference is what makes it
|
|
14
|
+
read as something physical rather than as an animation on a loop.
|
|
15
|
+
|
|
16
|
+
Colour runs green at the base to violet at the tips, which is the actual
|
|
17
|
+
emission order of a real aurora and, more usefully here, means the height of
|
|
18
|
+
a ray is legible from its colour alone at four pixels wide.
|
|
19
|
+
]]
|
|
20
|
+
|
|
21
|
+
local Draw = require(script.Parent.Draw)
|
|
22
|
+
local Theme = require(script.Parent.Theme)
|
|
23
|
+
|
|
24
|
+
local RAYS = 15
|
|
25
|
+
|
|
26
|
+
local Aurora = {}
|
|
27
|
+
|
|
28
|
+
Aurora.id = "aurora"
|
|
29
|
+
Aurora.label = "Aurora"
|
|
30
|
+
Aurora.blurb = "Rising curtain"
|
|
31
|
+
|
|
32
|
+
Aurora.palette = {
|
|
33
|
+
background = Color3.fromRGB(9, 14, 28),
|
|
34
|
+
surface = Color3.fromRGB(16, 23, 44),
|
|
35
|
+
rule = Color3.fromRGB(108, 226, 178),
|
|
36
|
+
text = Color3.fromRGB(206, 220, 238),
|
|
37
|
+
dim = Color3.fromRGB(86, 100, 130),
|
|
38
|
+
green = Color3.fromRGB(108, 232, 172),
|
|
39
|
+
red = Color3.fromRGB(246, 126, 146),
|
|
40
|
+
amber = Color3.fromRGB(244, 196, 128),
|
|
41
|
+
cyan = Color3.fromRGB(114, 214, 236),
|
|
42
|
+
violet = Color3.fromRGB(162, 146, 240),
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
local rays: { Frame } = {}
|
|
46
|
+
|
|
47
|
+
function Aurora.mount(cell: Frame)
|
|
48
|
+
rays = {}
|
|
49
|
+
for index = 1, RAYS do
|
|
50
|
+
local ray = Instance.new("Frame")
|
|
51
|
+
ray.Name = string.format("Ray%02d", index)
|
|
52
|
+
-- Anchored at the bottom so height grows upward from a fixed baseline,
|
|
53
|
+
-- which is what makes the curtain rise rather than expand about its
|
|
54
|
+
-- middle.
|
|
55
|
+
ray.AnchorPoint = Vector2.new(0.5, 1)
|
|
56
|
+
ray.BorderSizePixel = 0
|
|
57
|
+
ray.Parent = cell
|
|
58
|
+
local corner = Instance.new("UICorner")
|
|
59
|
+
corner.CornerRadius = UDim.new(0, 1)
|
|
60
|
+
corner.Parent = ray
|
|
61
|
+
rays[index] = ray
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
function Aurora.unmount()
|
|
66
|
+
rays = {}
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
function Aurora.paint(ctx: Theme.Context, _delta: number)
|
|
70
|
+
local width = ctx.size
|
|
71
|
+
local spacing = width / (RAYS + 1)
|
|
72
|
+
|
|
73
|
+
--[[
|
|
74
|
+
The floor is what keeps an idle panel alive.
|
|
75
|
+
|
|
76
|
+
Amplitude driven purely by activity would flatten the curtain to nothing
|
|
77
|
+
between commands, and a flat line is indistinguishable from a plugin
|
|
78
|
+
that has crashed. A tenth of the range is always moving.
|
|
79
|
+
]]
|
|
80
|
+
local amplitude = 0.1 + math.clamp(ctx.activity, 0, 1) * 0.62 + ctx.energy * 0.16
|
|
81
|
+
local pace = if ctx.quiet > 20 then 0.35 else 1
|
|
82
|
+
|
|
83
|
+
for index = 1, RAYS do
|
|
84
|
+
local ray = rays[index]
|
|
85
|
+
if ray == nil then
|
|
86
|
+
continue
|
|
87
|
+
end
|
|
88
|
+
local across = index / RAYS
|
|
89
|
+
|
|
90
|
+
-- Two travelling waves, incommensurate rates. Their beat is slow enough
|
|
91
|
+
-- that the curtain visibly reshapes over several seconds instead of
|
|
92
|
+
-- repeating on a cycle the eye can learn.
|
|
93
|
+
local wave = math.sin(across * 5.2 - ctx.clock * 1.6 * pace)
|
|
94
|
+
+ 0.6 * math.sin(across * 2.7 + ctx.clock * 0.9 * pace)
|
|
95
|
+
local normalised = (wave + 1.6) / 3.2
|
|
96
|
+
|
|
97
|
+
local height = math.max(2, math.floor(width * (0.12 + normalised * amplitude) * ctx.scale))
|
|
98
|
+
local reach = math.clamp(height / width, 0, 1)
|
|
99
|
+
|
|
100
|
+
ray.Size = UDim2.fromOffset(3, height)
|
|
101
|
+
ray.Position = UDim2.fromOffset(spacing * index, width - 2)
|
|
102
|
+
-- Green low, violet high: the ray's own height is readable from its
|
|
103
|
+
-- colour, which a three-pixel column badly needs.
|
|
104
|
+
ray.BackgroundColor3 = ctx.palette.green
|
|
105
|
+
:Lerp(ctx.palette.violet, reach)
|
|
106
|
+
:Lerp(Color3.new(1, 1, 1), ctx.flare * 0.45)
|
|
107
|
+
ray.BackgroundTransparency = math.clamp(0.74 - reach * 0.45 - ctx.flare * 0.3, 0, 1)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
--[[
|
|
112
|
+
The trace as a continuation of the curtain: same rays, same colour rule, but
|
|
113
|
+
their heights are history rather than a wave.
|
|
114
|
+
]]
|
|
115
|
+
function Aurora.paintSlot(slot: Theme.Slot, ctx: Theme.Context)
|
|
116
|
+
local frame = slot.frame
|
|
117
|
+
local recency = 1 - slot.age
|
|
118
|
+
local height = math.max(2, math.floor(2 + slot.weight * 21))
|
|
119
|
+
|
|
120
|
+
frame.AnchorPoint = Vector2.new(0.5, 1)
|
|
121
|
+
frame.Size = UDim2.fromOffset(3, height)
|
|
122
|
+
frame.Position = UDim2.new(frame.Position.X.Scale, frame.Position.X.Offset, 1, 0)
|
|
123
|
+
frame.BackgroundColor3 = if slot.ok
|
|
124
|
+
then ctx.palette.green:Lerp(ctx.palette.violet, slot.weight)
|
|
125
|
+
else ctx.palette.red
|
|
126
|
+
frame.BackgroundTransparency = math.clamp(0.66 - recency * 0.56, 0, 1)
|
|
127
|
+
frame.Rotation = 0
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
function Aurora.swatch(frame: Frame, palette: Theme.Palette)
|
|
131
|
+
for index = 1, 12 do
|
|
132
|
+
local ray = Instance.new("Frame")
|
|
133
|
+
ray.AnchorPoint = Vector2.new(0.5, 1)
|
|
134
|
+
ray.BorderSizePixel = 0
|
|
135
|
+
local across = index / 12
|
|
136
|
+
local wave = math.sin(across * 5.2) + 0.6 * math.sin(across * 2.7)
|
|
137
|
+
local reach = (wave + 1.6) / 3.2
|
|
138
|
+
local height = math.max(2, math.floor(4 + reach * 17))
|
|
139
|
+
ray.Size = UDim2.fromOffset(2, height)
|
|
140
|
+
ray.Position = UDim2.fromOffset(3 + index * 3, 23)
|
|
141
|
+
ray.BackgroundColor3 = palette.green:Lerp(palette.violet, reach)
|
|
142
|
+
ray.BackgroundTransparency = 0.35
|
|
143
|
+
ray.Parent = frame
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
return Aurora
|