@el4cteo/rbx-studio-mcp 0.3.1 → 0.3.6
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/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/input.js +38 -5
- package/dist/tools/input.js.map +1 -1
- package/dist/tools/instances.js +49 -2
- package/dist/tools/instances.js.map +1 -1
- package/dist/tools/perf.js +9 -8
- package/dist/tools/perf.js.map +1 -1
- package/dist/tools/scripts.js +10 -1
- package/dist/tools/scripts.js.map +1 -1
- package/package.json +1 -1
- package/plugin/src/Config.luau +1 -1
- package/plugin/src/Console.luau +301 -117
- package/plugin/src/Mirror.luau +126 -0
- package/plugin/src/Serialize.luau +43 -3
- package/plugin/src/ThemePicker.luau +458 -0
- package/plugin/src/Themes/Aurora.luau +161 -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 +266 -0
- package/plugin/src/Themes/init.luau +116 -0
- package/plugin/src/Undo.luau +29 -0
- package/plugin/src/Visuals.luau +838 -907
- package/plugin/src/handlers/Device.luau +21 -1
- package/plugin/src/handlers/Discover.luau +75 -1
- package/plugin/src/handlers/Exec.luau +17 -5
- package/plugin/src/handlers/Input.luau +583 -493
- package/plugin/src/handlers/Instances.luau +35 -3
- package/plugin/src/init.server.luau +209 -11
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--[[
|
|
3
|
+
Orbit -- the only preset that reports something the others cannot.
|
|
4
|
+
|
|
5
|
+
A body at the centre with satellites running elliptical paths around it. The
|
|
6
|
+
number of satellites is the session's activity, so where Lattice climbs a
|
|
7
|
+
ladder of shapes to say "busy", this says it in a way you can count: one
|
|
8
|
+
moon is a quiet session, five is a burst.
|
|
9
|
+
|
|
10
|
+
The ellipses are a projected circle rather than a drawn oval, so the whole
|
|
11
|
+
system tilts as the angle turns and a satellite genuinely passes behind the
|
|
12
|
+
body -- it dims and shrinks on the far side because it is further away. That
|
|
13
|
+
is what stops it reading as a loading spinner with extra dots.
|
|
14
|
+
|
|
15
|
+
Colour is warm here on purpose. Every other dark preset in the set leans
|
|
16
|
+
blue or violet; a gold system against navy is the one that looks like an
|
|
17
|
+
instrument rather than a screensaver.
|
|
18
|
+
]]
|
|
19
|
+
|
|
20
|
+
local Draw = require(script.Parent.Draw)
|
|
21
|
+
local Theme = require(script.Parent.Theme)
|
|
22
|
+
|
|
23
|
+
-- Satellites are pooled at the maximum and hidden when the session is calm, so
|
|
24
|
+
-- activity changes what is visible rather than what exists.
|
|
25
|
+
local MOONS = 6
|
|
26
|
+
local PATH_DOTS = 28
|
|
27
|
+
local FOCAL = 3.2
|
|
28
|
+
|
|
29
|
+
--[[
|
|
30
|
+
How far the orbits reach, as a multiple of the body's radius.
|
|
31
|
+
|
|
32
|
+
Chosen so the outermost satellite sits about 18px from a centre with 20px of
|
|
33
|
+
room: filling the cell, with just enough margin that it never touches the
|
|
34
|
+
clip. Two earlier values were both wrong for the same underlying reason --
|
|
35
|
+
2.4 threw the sixth moon 26px out when busy, and 1.5 fixed the clipping by
|
|
36
|
+
making the whole system too small to read.
|
|
37
|
+
]]
|
|
38
|
+
local ORBIT_SCALE = 1.95
|
|
39
|
+
|
|
40
|
+
--[[
|
|
41
|
+
How much of the scale spring the ORBITS take. Almost none.
|
|
42
|
+
|
|
43
|
+
The spring swings 0.82 to 1.20, and letting the satellites carry that meant
|
|
44
|
+
the system had to rest small enough that its overshoot still fit -- so it
|
|
45
|
+
was sized by its worst moment rather than by the one you spend all your time
|
|
46
|
+
looking at. The body and the halo carry the spring instead: they sit at the
|
|
47
|
+
centre with room to grow, and a struck object whose middle flexes reads the
|
|
48
|
+
same as one that flexes all over.
|
|
49
|
+
]]
|
|
50
|
+
local ORBIT_BREATH = 0.08
|
|
51
|
+
|
|
52
|
+
local Orbit = {}
|
|
53
|
+
|
|
54
|
+
Orbit.id = "orbit"
|
|
55
|
+
Orbit.label = "Orbit"
|
|
56
|
+
Orbit.blurb = "Body and satellites"
|
|
57
|
+
|
|
58
|
+
Orbit.palette = {
|
|
59
|
+
background = Color3.fromRGB(10, 13, 22),
|
|
60
|
+
surface = Color3.fromRGB(17, 22, 36),
|
|
61
|
+
rule = Color3.fromRGB(240, 186, 108),
|
|
62
|
+
text = Color3.fromRGB(226, 222, 210),
|
|
63
|
+
dim = Color3.fromRGB(96, 100, 116),
|
|
64
|
+
green = Color3.fromRGB(134, 214, 158),
|
|
65
|
+
red = Color3.fromRGB(240, 130, 116),
|
|
66
|
+
amber = Color3.fromRGB(246, 190, 106),
|
|
67
|
+
cyan = Color3.fromRGB(132, 196, 226),
|
|
68
|
+
violet = Color3.fromRGB(196, 164, 226),
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
local body: Frame? = nil
|
|
72
|
+
local halo: Frame? = nil
|
|
73
|
+
local moons: { Frame } = {}
|
|
74
|
+
local path: { Frame } = {}
|
|
75
|
+
|
|
76
|
+
--[[
|
|
77
|
+
Orbital parameters for satellite `index`.
|
|
78
|
+
|
|
79
|
+
Fixed per index so a moon keeps its own path for the life of the session --
|
|
80
|
+
re-rolling them would make the system reshuffle every frame. Radius rises
|
|
81
|
+
with the index so the moons never share an orbit, and the inclination is
|
|
82
|
+
what makes the paths cross rather than sit in one flat ring.
|
|
83
|
+
]]
|
|
84
|
+
local function orbitOf(index: number): (number, number, number)
|
|
85
|
+
-- Tightened from `0.42 + index * 0.1`, which put the sixth moon at 1.02 and
|
|
86
|
+
-- 26px from a centre with 20px of room -- so the satellite that only appears
|
|
87
|
+
-- when the session is busiest was the one that flew off the edge.
|
|
88
|
+
local radius = 0.34 + index * 0.055
|
|
89
|
+
local speed = 1.5 / (radius * 2.4)
|
|
90
|
+
local inclination = (Draw.hash(index, 5) - 0.5) * 1.6
|
|
91
|
+
return radius, speed, inclination
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
function Orbit.mount(cell: Frame)
|
|
95
|
+
path = Draw.pool(cell, PATH_DOTS, "Path", true)
|
|
96
|
+
|
|
97
|
+
local disc = Instance.new("Frame")
|
|
98
|
+
disc.Name = "Halo"
|
|
99
|
+
disc.AnchorPoint = Vector2.new(0.5, 0.5)
|
|
100
|
+
disc.BorderSizePixel = 0
|
|
101
|
+
disc.Parent = cell
|
|
102
|
+
local haloCorner = Instance.new("UICorner")
|
|
103
|
+
haloCorner.CornerRadius = UDim.new(1, 0)
|
|
104
|
+
haloCorner.Parent = disc
|
|
105
|
+
halo = disc
|
|
106
|
+
|
|
107
|
+
local core = Instance.new("Frame")
|
|
108
|
+
core.Name = "Body"
|
|
109
|
+
core.AnchorPoint = Vector2.new(0.5, 0.5)
|
|
110
|
+
core.BorderSizePixel = 0
|
|
111
|
+
core.Parent = cell
|
|
112
|
+
local coreCorner = Instance.new("UICorner")
|
|
113
|
+
coreCorner.CornerRadius = UDim.new(1, 0)
|
|
114
|
+
coreCorner.Parent = core
|
|
115
|
+
body = core
|
|
116
|
+
|
|
117
|
+
-- Last, so a near satellite passes in front of the body it orbits.
|
|
118
|
+
moons = Draw.pool(cell, MOONS, "Moon", true)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
function Orbit.unmount()
|
|
122
|
+
body = nil
|
|
123
|
+
halo = nil
|
|
124
|
+
moons = {}
|
|
125
|
+
path = {}
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
function Orbit.paint(ctx: Theme.Context, _delta: number)
|
|
129
|
+
local breath = (1 - ORBIT_BREATH) + ctx.scale * ORBIT_BREATH
|
|
130
|
+
local radius = ctx.size * 0.34 * breath
|
|
131
|
+
local core = body
|
|
132
|
+
local glow = halo
|
|
133
|
+
|
|
134
|
+
if core ~= nil then
|
|
135
|
+
--[[
|
|
136
|
+
Amber, not the status tint.
|
|
137
|
+
|
|
138
|
+
Every other preset paints its cell in `ctx.tint` so the panel reads as
|
|
139
|
+
disconnected at a glance. Orbit cannot: the tint is green while
|
|
140
|
+
connected, so the body came out green while the drawer previewed it in
|
|
141
|
+
gold, and the preview is a promise. The HALO carries the status colour
|
|
142
|
+
instead -- same information, on the part of the scene that is supposed
|
|
143
|
+
to change.
|
|
144
|
+
]]
|
|
145
|
+
local size = math.max(5, math.floor(7.5 * ctx.scale + ctx.flare * 3))
|
|
146
|
+
Draw.dot(
|
|
147
|
+
core,
|
|
148
|
+
ctx.centre,
|
|
149
|
+
size,
|
|
150
|
+
ctx.palette.amber:Lerp(Color3.new(1, 1, 1), 0.3 + ctx.flare * 0.5),
|
|
151
|
+
0
|
|
152
|
+
)
|
|
153
|
+
end
|
|
154
|
+
if glow ~= nil then
|
|
155
|
+
-- A halo that grows with energy rather than a body that does: the body
|
|
156
|
+
-- staying put is what gives the satellites something fixed to orbit.
|
|
157
|
+
local size = math.max(10, math.floor(13 * ctx.scale + ctx.energy * 8 + ctx.flare * 6))
|
|
158
|
+
Draw.dot(glow, ctx.centre, size, ctx.tint, math.clamp(0.9 - ctx.flare * 0.35, 0, 1))
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
--[[
|
|
162
|
+
The innermost path, drawn as points so it fades with depth.
|
|
163
|
+
|
|
164
|
+
Only one path is drawn even though there are six orbits. Six rings turns
|
|
165
|
+
the cell into a scribble at forty pixels across; one ring establishes
|
|
166
|
+
that these things are orbiting and the rest is left to the motion.
|
|
167
|
+
]]
|
|
168
|
+
local pathRadius, _, pathTilt = orbitOf(1)
|
|
169
|
+
for index = 1, PATH_DOTS do
|
|
170
|
+
local frame = path[index]
|
|
171
|
+
local turn = (index / PATH_DOTS) * math.pi * 2
|
|
172
|
+
local local3 = Vector3.new(
|
|
173
|
+
math.cos(turn) * pathRadius,
|
|
174
|
+
math.sin(turn) * pathRadius * math.sin(pathTilt),
|
|
175
|
+
math.sin(turn) * pathRadius * math.cos(pathTilt)
|
|
176
|
+
)
|
|
177
|
+
local point, depth = Draw.project(
|
|
178
|
+
Draw.rotate(local3, ctx.angle * 0.5),
|
|
179
|
+
FOCAL,
|
|
180
|
+
radius * ORBIT_SCALE,
|
|
181
|
+
ctx.centre
|
|
182
|
+
)
|
|
183
|
+
local near = math.clamp((depth + 1) / 2, 0, 1)
|
|
184
|
+
Draw.dot(frame, point, 1, ctx.palette.dim, math.clamp(0.78 - near * 0.3, 0, 1))
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
--[[
|
|
188
|
+
How many satellites are up. Activity runs 0..1 over about ten seconds,
|
|
189
|
+
so this climbs through the pool as a burst lands and drops back as it
|
|
190
|
+
unwinds -- which is the readout the theme exists for.
|
|
191
|
+
]]
|
|
192
|
+
local visible = 1 + math.floor(math.clamp(ctx.activity, 0, 1) * (MOONS - 1) + 0.5)
|
|
193
|
+
|
|
194
|
+
for index = 1, MOONS do
|
|
195
|
+
local frame = moons[index]
|
|
196
|
+
if frame == nil then
|
|
197
|
+
continue
|
|
198
|
+
end
|
|
199
|
+
if index > visible then
|
|
200
|
+
frame.Visible = false
|
|
201
|
+
continue
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
local orbitRadius, speed, tilt = orbitOf(index)
|
|
205
|
+
-- Phase offset per moon so they do not launch from the same point and
|
|
206
|
+
-- travel as a clump.
|
|
207
|
+
local turn = ctx.clock * speed + Draw.hash(index, 6) * math.pi * 2
|
|
208
|
+
local local3 = Vector3.new(
|
|
209
|
+
math.cos(turn) * orbitRadius,
|
|
210
|
+
math.sin(turn) * orbitRadius * math.sin(tilt),
|
|
211
|
+
math.sin(turn) * orbitRadius * math.cos(tilt)
|
|
212
|
+
)
|
|
213
|
+
local point, depth = Draw.project(
|
|
214
|
+
Draw.rotate(local3, ctx.angle * 0.5),
|
|
215
|
+
FOCAL,
|
|
216
|
+
radius * ORBIT_SCALE,
|
|
217
|
+
ctx.centre
|
|
218
|
+
)
|
|
219
|
+
local near = math.clamp((depth + 1) / 2, 0, 1)
|
|
220
|
+
Draw.dot(
|
|
221
|
+
frame,
|
|
222
|
+
point,
|
|
223
|
+
math.max(2, math.floor(2.5 + near * 2.5 + ctx.flare * 1.5)),
|
|
224
|
+
ctx.palette.amber:Lerp(Color3.new(1, 1, 1), near * 0.4 + ctx.flare * 0.3),
|
|
225
|
+
math.clamp(0.35 - near * 0.32 - ctx.flare * 0.3, 0, 1)
|
|
226
|
+
)
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
--[[
|
|
231
|
+
History as a run of moons at their apoapsis: a dot per call, high when slow.
|
|
232
|
+
|
|
233
|
+
Bars would be the obvious choice and are wrong here -- a rectangle sitting
|
|
234
|
+
on a baseline has nothing to do with the rest of the scene, and the trace
|
|
235
|
+
and the cell would read as two unrelated instruments sharing a strip.
|
|
236
|
+
]]
|
|
237
|
+
function Orbit.paintSlot(slot: Theme.Slot, ctx: Theme.Context)
|
|
238
|
+
local frame = slot.frame
|
|
239
|
+
local recency = 1 - slot.age
|
|
240
|
+
|
|
241
|
+
frame.AnchorPoint = Vector2.new(0.5, 0.5)
|
|
242
|
+
local size = math.max(2, math.floor(2 + slot.weight * 3 + recency))
|
|
243
|
+
frame.Size = UDim2.fromOffset(size, size)
|
|
244
|
+
frame.Position = UDim2.new(frame.Position.X.Scale, frame.Position.X.Offset, 0.88 - slot.weight * 0.74, 0)
|
|
245
|
+
frame.BackgroundColor3 = if slot.ok then ctx.palette.amber else ctx.palette.red
|
|
246
|
+
frame.BackgroundTransparency = math.clamp((if slot.ok then 0.7 else 0.35) - recency * 0.55, 0, 1)
|
|
247
|
+
frame.Rotation = 0
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
function Orbit.swatch(frame: Frame, palette: Theme.Palette)
|
|
251
|
+
local centre = Vector2.new(20, 12)
|
|
252
|
+
local ring = Draw.pool(frame, 20, "Path", true)
|
|
253
|
+
for index = 1, 20 do
|
|
254
|
+
local turn = (index / 20) * math.pi * 2
|
|
255
|
+
Draw.dot(
|
|
256
|
+
ring[index],
|
|
257
|
+
centre + Vector2.new(math.cos(turn) * 13, math.sin(turn) * 5.5),
|
|
258
|
+
1,
|
|
259
|
+
palette.dim,
|
|
260
|
+
0.6
|
|
261
|
+
)
|
|
262
|
+
end
|
|
263
|
+
local pool = Draw.pool(frame, 2, "Body", true)
|
|
264
|
+
Draw.dot(pool[1], centre, 7, palette.amber:Lerp(Color3.new(1, 1, 1), 0.3), 0)
|
|
265
|
+
Draw.dot(pool[2], centre + Vector2.new(13, -1), 3, palette.amber, 0.1)
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
return Orbit
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--[[
|
|
3
|
+
Phosphor -- a CRT oscilloscope.
|
|
4
|
+
|
|
5
|
+
A Lissajous figure traced by a moving beam, with the phosphor decaying
|
|
6
|
+
behind it. The head is white-hot, the tail fades to green over about forty
|
|
7
|
+
samples, and the whole cell sits under scanlines.
|
|
8
|
+
|
|
9
|
+
It deliberately does NOT reuse the wireframe. Drawing Lattice's solid in
|
|
10
|
+
green with a trail would be the obvious CRT gag and would leave two of the
|
|
11
|
+
eight presets doing the same thing in different colours. A beam tracing a
|
|
12
|
+
curve is what a scope actually does, and it gives the theme its own answer
|
|
13
|
+
to "is this alive": the figure's shape changes with the session, because the
|
|
14
|
+
frequency ratio is driven by activity. Idle it draws a lazy ellipse; busy it
|
|
15
|
+
knots into a rose.
|
|
16
|
+
|
|
17
|
+
The sample buffer is a ring rather than a growing list. The head advances,
|
|
18
|
+
the sample under it is overwritten, and age is computed from the distance
|
|
19
|
+
back to the head -- so nothing is allocated or shifted per frame.
|
|
20
|
+
]]
|
|
21
|
+
|
|
22
|
+
local Draw = require(script.Parent.Draw)
|
|
23
|
+
local Theme = require(script.Parent.Theme)
|
|
24
|
+
|
|
25
|
+
local SAMPLES = 46
|
|
26
|
+
-- Samples per second the beam advances. Fast enough that the figure is a curve
|
|
27
|
+
-- rather than a dotted line, slow enough that the decay is visible.
|
|
28
|
+
local RATE = 60
|
|
29
|
+
|
|
30
|
+
local Phosphor = {}
|
|
31
|
+
|
|
32
|
+
Phosphor.id = "phosphor"
|
|
33
|
+
Phosphor.label = "Phosphor"
|
|
34
|
+
Phosphor.blurb = "CRT scope beam"
|
|
35
|
+
|
|
36
|
+
Phosphor.palette = {
|
|
37
|
+
background = Color3.fromRGB(4, 8, 5),
|
|
38
|
+
surface = Color3.fromRGB(9, 16, 11),
|
|
39
|
+
rule = Color3.fromRGB(86, 232, 122),
|
|
40
|
+
text = Color3.fromRGB(178, 236, 190),
|
|
41
|
+
dim = Color3.fromRGB(70, 112, 82),
|
|
42
|
+
green = Color3.fromRGB(86, 240, 128),
|
|
43
|
+
red = Color3.fromRGB(248, 118, 96),
|
|
44
|
+
amber = Color3.fromRGB(240, 208, 106),
|
|
45
|
+
cyan = Color3.fromRGB(122, 234, 208),
|
|
46
|
+
violet = Color3.fromRGB(150, 226, 150),
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
local dots: { Frame } = {}
|
|
50
|
+
local scanlines: { Frame } = {}
|
|
51
|
+
local samples: { Vector2 } = {}
|
|
52
|
+
local head = 0
|
|
53
|
+
local accumulator = 0
|
|
54
|
+
|
|
55
|
+
function Phosphor.mount(cell: Frame)
|
|
56
|
+
dots = Draw.pool(cell, SAMPLES, "Sample", true)
|
|
57
|
+
|
|
58
|
+
scanlines = {}
|
|
59
|
+
samples = {}
|
|
60
|
+
head = 0
|
|
61
|
+
accumulator = 0
|
|
62
|
+
|
|
63
|
+
--[[
|
|
64
|
+
Scanlines last, so they lie OVER the beam.
|
|
65
|
+
|
|
66
|
+
Underneath they were invisible -- the beam is the brightest thing in the
|
|
67
|
+
cell and simply covered them. A CRT's lines cross what it is drawing;
|
|
68
|
+
that is what makes it read as a screen rather than as a dark background
|
|
69
|
+
with stripes on it.
|
|
70
|
+
]]
|
|
71
|
+
for index = 1, 10 do
|
|
72
|
+
local line = Instance.new("Frame")
|
|
73
|
+
line.Name = string.format("Scan%02d", index)
|
|
74
|
+
line.BorderSizePixel = 0
|
|
75
|
+
line.BackgroundColor3 = Color3.new(0, 0, 0)
|
|
76
|
+
line.BackgroundTransparency = 0.78
|
|
77
|
+
line.Size = UDim2.new(1, 0, 0, 1)
|
|
78
|
+
line.Position = UDim2.fromOffset(0, index * 4)
|
|
79
|
+
line.ZIndex = 4
|
|
80
|
+
line.Parent = cell
|
|
81
|
+
scanlines[index] = line
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
function Phosphor.unmount()
|
|
86
|
+
dots = {}
|
|
87
|
+
scanlines = {}
|
|
88
|
+
samples = {}
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
function Phosphor.paint(ctx: Theme.Context, delta: number)
|
|
92
|
+
local radius = ctx.size * 0.33 * ctx.scale
|
|
93
|
+
|
|
94
|
+
--[[
|
|
95
|
+
The figure's ratio, from activity.
|
|
96
|
+
|
|
97
|
+
A Lissajous curve closes when its two frequencies are a simple ratio, so
|
|
98
|
+
holding X at 2 and sliding Y between 3 and roughly 5 walks it from an
|
|
99
|
+
open ellipse through a figure-eight into a knot. The session's load
|
|
100
|
+
therefore changes the SHAPE, not just the speed -- which is the same
|
|
101
|
+
trick Lattice plays with its solids, done continuously.
|
|
102
|
+
]]
|
|
103
|
+
local ratio = 3 + math.clamp(ctx.activity, 0, 1) * 2.1
|
|
104
|
+
|
|
105
|
+
-- Fixed-step sampling. Advancing one sample per frame would make the trail's
|
|
106
|
+
-- length depend on Studio's frame rate, so the beam would visibly speed up
|
|
107
|
+
-- whenever the editor was idle.
|
|
108
|
+
accumulator += math.min(delta, 1 / 15)
|
|
109
|
+
local advance = math.floor(accumulator * RATE)
|
|
110
|
+
if advance > 0 then
|
|
111
|
+
accumulator -= advance / RATE
|
|
112
|
+
for _ = 1, math.min(advance, SAMPLES) do
|
|
113
|
+
head += 1
|
|
114
|
+
local t = head / RATE
|
|
115
|
+
samples[(head % SAMPLES) + 1] = Vector2.new(
|
|
116
|
+
math.sin(t * 2) * radius,
|
|
117
|
+
math.sin(t * ratio + ctx.clock * 0.15) * radius
|
|
118
|
+
)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
for index = 1, SAMPLES do
|
|
123
|
+
local frame = dots[index]
|
|
124
|
+
local point = samples[index]
|
|
125
|
+
if frame == nil then
|
|
126
|
+
continue
|
|
127
|
+
end
|
|
128
|
+
if point == nil then
|
|
129
|
+
frame.Visible = false
|
|
130
|
+
continue
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
--[[
|
|
134
|
+
How far back from the head this sample is, 0 (newest) to 1 (about to
|
|
135
|
+
be overwritten). The modulo is what lets the ring buffer stand in for
|
|
136
|
+
an ordered trail without moving anything.
|
|
137
|
+
]]
|
|
138
|
+
local back = (head - (index - 1)) % SAMPLES
|
|
139
|
+
local age = back / SAMPLES
|
|
140
|
+
|
|
141
|
+
-- Squared, so the head is a bright point with a long dim tail rather
|
|
142
|
+
-- than an evenly grey worm. Phosphor decays fast at first.
|
|
143
|
+
local glow = (1 - age) * (1 - age)
|
|
144
|
+
|
|
145
|
+
Draw.dot(
|
|
146
|
+
frame,
|
|
147
|
+
ctx.centre + point,
|
|
148
|
+
math.max(1, math.floor(1 + glow * 2 + ctx.flare * 1.5)),
|
|
149
|
+
ctx.palette.green:Lerp(Color3.new(1, 1, 1), glow * 0.85),
|
|
150
|
+
math.clamp(0.94 - glow * 0.9 - ctx.flare * 0.2, 0, 1)
|
|
151
|
+
)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
--[[
|
|
156
|
+
History as scope columns: hard-edged, one pixel wide, no rounding. The trace
|
|
157
|
+
is the one part of a scope's screen that is supposed to look like a chart.
|
|
158
|
+
]]
|
|
159
|
+
function Phosphor.paintSlot(slot: Theme.Slot, ctx: Theme.Context)
|
|
160
|
+
local frame = slot.frame
|
|
161
|
+
local recency = 1 - slot.age
|
|
162
|
+
local height = math.max(1, math.floor(1 + slot.weight * 22))
|
|
163
|
+
|
|
164
|
+
frame.AnchorPoint = Vector2.new(0.5, 1)
|
|
165
|
+
frame.Size = UDim2.fromOffset(2, height)
|
|
166
|
+
frame.Position = UDim2.new(frame.Position.X.Scale, frame.Position.X.Offset, 1, 0)
|
|
167
|
+
frame.BackgroundColor3 = if slot.ok
|
|
168
|
+
then ctx.palette.green:Lerp(Color3.new(1, 1, 1), recency * 0.4)
|
|
169
|
+
else ctx.palette.red
|
|
170
|
+
frame.BackgroundTransparency = math.clamp(0.7 - recency * 0.65, 0, 1)
|
|
171
|
+
frame.Rotation = 0
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
function Phosphor.swatch(frame: Frame, palette: Theme.Palette)
|
|
175
|
+
local centre = Vector2.new(20, 12)
|
|
176
|
+
local pool = Draw.pool(frame, 30, "Sample", true)
|
|
177
|
+
for index = 1, 30 do
|
|
178
|
+
local t = index / 6
|
|
179
|
+
local glow = index / 30
|
|
180
|
+
Draw.dot(
|
|
181
|
+
pool[index],
|
|
182
|
+
centre + Vector2.new(math.sin(t * 2) * 15, math.sin(t * 3) * 9),
|
|
183
|
+
1 + math.floor(glow * 2),
|
|
184
|
+
palette.green:Lerp(Color3.new(1, 1, 1), glow * 0.8),
|
|
185
|
+
0.85 - glow * 0.8
|
|
186
|
+
)
|
|
187
|
+
end
|
|
188
|
+
for index = 1, 6 do
|
|
189
|
+
local line = Instance.new("Frame")
|
|
190
|
+
line.BorderSizePixel = 0
|
|
191
|
+
line.BackgroundColor3 = Color3.new(0, 0, 0)
|
|
192
|
+
line.BackgroundTransparency = 0.75
|
|
193
|
+
line.Size = UDim2.new(1, 0, 0, 1)
|
|
194
|
+
line.Position = UDim2.fromOffset(0, index * 4)
|
|
195
|
+
line.ZIndex = 4
|
|
196
|
+
line.Parent = frame
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
return Phosphor
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--[[
|
|
3
|
+
The contract every preset satisfies.
|
|
4
|
+
|
|
5
|
+
A theme owns two surfaces and nothing else. `cell` is the square at the left
|
|
6
|
+
of the activity band -- the slot the wireframe prism occupies under the
|
|
7
|
+
default theme, and which any other theme is free to fill with something that
|
|
8
|
+
is not a prism at all. `slots` are the forty most recent calls, drawn as bars
|
|
9
|
+
under the default theme and as anything else under the rest.
|
|
10
|
+
|
|
11
|
+
What a theme deliberately does NOT own is the simulation. Energy, activity,
|
|
12
|
+
flare, shake, the scale spring and the quiet timer are computed once in
|
|
13
|
+
Visuals and handed to whichever preset is active. That is the whole reason
|
|
14
|
+
eight presets do not mean eight animation systems: they are eight painters
|
|
15
|
+
over one set of physics, so a preset is a few hundred lines of drawing and
|
|
16
|
+
holds no state that could drift out of step with the session.
|
|
17
|
+
]]
|
|
18
|
+
|
|
19
|
+
export type Palette = {
|
|
20
|
+
background: Color3,
|
|
21
|
+
surface: Color3,
|
|
22
|
+
rule: Color3,
|
|
23
|
+
text: Color3,
|
|
24
|
+
dim: Color3,
|
|
25
|
+
green: Color3,
|
|
26
|
+
red: Color3,
|
|
27
|
+
amber: Color3,
|
|
28
|
+
cyan: Color3,
|
|
29
|
+
violet: Color3,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
--[[
|
|
33
|
+
The session, as numbers a painter can use.
|
|
34
|
+
|
|
35
|
+
Everything here decays on its own, so a painter never has to schedule
|
|
36
|
+
anything: read the value, draw that frame, and the motion follows.
|
|
37
|
+
|
|
38
|
+
`energy` rises with each command and falls over about a second -- use it for
|
|
39
|
+
anything that should react. `activity` is the slower average, over roughly
|
|
40
|
+
ten seconds, and is what should drive how BUSY a scene looks rather than how
|
|
41
|
+
startled. `flare` spikes to 1 the instant a reply lands and is gone in a
|
|
42
|
+
third of a second, which is what makes a completed call read as an event.
|
|
43
|
+
`shake` is the same but only for failures. `quiet` is seconds since anything
|
|
44
|
+
ran; past `QUIET_AFTER` the session has stopped rather than paused, and a
|
|
45
|
+
scene that keeps working at full pace after that is claiming an activity
|
|
46
|
+
which is not happening.
|
|
47
|
+
]]
|
|
48
|
+
export type Context = {
|
|
49
|
+
palette: Palette,
|
|
50
|
+
-- Connection state blended toward the kind of work last done.
|
|
51
|
+
tint: Color3,
|
|
52
|
+
energy: number,
|
|
53
|
+
activity: number,
|
|
54
|
+
flare: number,
|
|
55
|
+
shake: number,
|
|
56
|
+
-- The scale spring: 1 at rest, overshoots past 1 on a reply, 0.82 when idle.
|
|
57
|
+
scale: number,
|
|
58
|
+
quiet: number,
|
|
59
|
+
-- Accumulated rotation on three axes. Meaningful to anything that turns;
|
|
60
|
+
-- ignorable by anything that does not.
|
|
61
|
+
angle: Vector3,
|
|
62
|
+
-- `os.clock()`, sampled once per frame so every painter agrees on the time.
|
|
63
|
+
clock: number,
|
|
64
|
+
-- The cell is square; this is its side in pixels.
|
|
65
|
+
size: number,
|
|
66
|
+
centre: Vector2,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
--[[
|
|
70
|
+
One finished call, as history rather than as an event.
|
|
71
|
+
|
|
72
|
+
`age` is 0 for the newest and rises toward 1 for the oldest still on screen,
|
|
73
|
+
which is what lets a preset fade its history out without tracking when
|
|
74
|
+
anything happened. `frame` is pooled and reused as bars shift left, so a
|
|
75
|
+
painter must set every property it cares about rather than assuming the
|
|
76
|
+
frame arrives clean.
|
|
77
|
+
]]
|
|
78
|
+
export type Slot = {
|
|
79
|
+
frame: Frame,
|
|
80
|
+
milliseconds: number,
|
|
81
|
+
ok: boolean,
|
|
82
|
+
title: string,
|
|
83
|
+
age: number,
|
|
84
|
+
-- Duration against the "slow" reference, clamped to 1. Height, for a bar.
|
|
85
|
+
weight: number,
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export type Theme = {
|
|
89
|
+
id: string,
|
|
90
|
+
label: string,
|
|
91
|
+
-- One line, shown under the label in the picker.
|
|
92
|
+
blurb: string,
|
|
93
|
+
palette: Palette,
|
|
94
|
+
--[[
|
|
95
|
+
Builds whatever the cell needs, once. Pooled Instances belong here:
|
|
96
|
+
allocating inside `paint` is how a decorative strip becomes a performance
|
|
97
|
+
complaint.
|
|
98
|
+
]]
|
|
99
|
+
mount: (cell: Frame) -> (),
|
|
100
|
+
paint: (ctx: Context, delta: number) -> (),
|
|
101
|
+
--[[
|
|
102
|
+
Destroys what `mount` built. Called on every theme switch, so it must
|
|
103
|
+
leave the cell empty enough for the next preset to start clean.
|
|
104
|
+
]]
|
|
105
|
+
unmount: () -> (),
|
|
106
|
+
--[[
|
|
107
|
+
Draws one call of history. Optional: a preset that wants ordinary bars
|
|
108
|
+
omits it and Visuals draws them.
|
|
109
|
+
]]
|
|
110
|
+
paintSlot: ((slot: Slot, ctx: Context) -> ())?,
|
|
111
|
+
--[[
|
|
112
|
+
A still miniature for the picker, drawn into a 40x24 frame.
|
|
113
|
+
|
|
114
|
+
Static on purpose. Eight live renderers running behind a hover menu is
|
|
115
|
+
the one place the "only one preset runs at a time" argument breaks, and
|
|
116
|
+
it breaks in a drawer the user looks at for two seconds.
|
|
117
|
+
]]
|
|
118
|
+
swatch: (frame: Frame, palette: Palette) -> (),
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return {}
|