@el4cteo/rbx-studio-mcp 0.3.1 → 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/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 +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 +257 -116
- 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/init.server.luau +386 -354
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--[[
|
|
3
|
+
Nebula -- the pink one, and the only preset with no straight lines.
|
|
4
|
+
|
|
5
|
+
Soft blobs advected by a slow flow field, magenta through cyan. Every other
|
|
6
|
+
theme in the set draws edges, points or bars; this one draws nothing with a
|
|
7
|
+
corner in it, which is what makes it read as a different instrument rather
|
|
8
|
+
than as the same instrument recoloured.
|
|
9
|
+
|
|
10
|
+
The flow is a pair of sines sampled at each blob's position -- cheap, and
|
|
11
|
+
continuous, so neighbouring blobs drift in similar directions and the field
|
|
12
|
+
holds together instead of scattering. Blobs that leave the cell wrap back to
|
|
13
|
+
the far side rather than being respawned, so the field never thins out.
|
|
14
|
+
|
|
15
|
+
A reply blooms rather than flashes: the blobs swell and brighten over the
|
|
16
|
+
third of a second `flare` lasts. On a strip this small a hard flash reads as
|
|
17
|
+
a rendering fault; a bloom reads as something arriving.
|
|
18
|
+
]]
|
|
19
|
+
|
|
20
|
+
local Draw = require(script.Parent.Draw)
|
|
21
|
+
local Theme = require(script.Parent.Theme)
|
|
22
|
+
|
|
23
|
+
local BLOBS = 20
|
|
24
|
+
|
|
25
|
+
local Nebula = {}
|
|
26
|
+
|
|
27
|
+
Nebula.id = "nebula"
|
|
28
|
+
Nebula.label = "Nebula"
|
|
29
|
+
Nebula.blurb = "Drifting dust"
|
|
30
|
+
|
|
31
|
+
Nebula.palette = {
|
|
32
|
+
background = Color3.fromRGB(19, 11, 24),
|
|
33
|
+
surface = Color3.fromRGB(30, 18, 38),
|
|
34
|
+
rule = Color3.fromRGB(236, 122, 196),
|
|
35
|
+
text = Color3.fromRGB(238, 214, 236),
|
|
36
|
+
dim = Color3.fromRGB(122, 96, 128),
|
|
37
|
+
green = Color3.fromRGB(142, 230, 196),
|
|
38
|
+
red = Color3.fromRGB(255, 122, 148),
|
|
39
|
+
amber = Color3.fromRGB(250, 190, 140),
|
|
40
|
+
cyan = Color3.fromRGB(134, 226, 240),
|
|
41
|
+
violet = Color3.fromRGB(226, 140, 236),
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
type Blob = {
|
|
45
|
+
frame: Frame,
|
|
46
|
+
position: Vector2,
|
|
47
|
+
-- Fixed per blob: size, how much of the magenta-to-cyan range it sits at,
|
|
48
|
+
-- and its own drift phase.
|
|
49
|
+
scale: number,
|
|
50
|
+
hue: number,
|
|
51
|
+
phase: number,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
local blobs: { Blob } = {}
|
|
55
|
+
local size = 40
|
|
56
|
+
|
|
57
|
+
--[[
|
|
58
|
+
The flow field. Two sines crossed, which is enough structure to look like a
|
|
59
|
+
current and cheap enough to sample forty times a frame.
|
|
60
|
+
|
|
61
|
+
Time enters slowly. A field that changes as fast as the things moving
|
|
62
|
+
through it reads as noise; at this rate the current itself visibly turns
|
|
63
|
+
over about a minute, under blobs that take a few seconds to cross.
|
|
64
|
+
]]
|
|
65
|
+
local function flow(at: Vector2, clock: number): Vector2
|
|
66
|
+
local x = math.sin(at.Y * 0.16 + clock * 0.21)
|
|
67
|
+
local y = math.cos(at.X * 0.14 - clock * 0.17)
|
|
68
|
+
return Vector2.new(x, y)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
function Nebula.mount(cell: Frame)
|
|
72
|
+
size = cell.AbsoluteSize.X
|
|
73
|
+
if size < 8 then
|
|
74
|
+
size = 40
|
|
75
|
+
end
|
|
76
|
+
local frames = Draw.pool(cell, BLOBS, "Blob", true)
|
|
77
|
+
blobs = {}
|
|
78
|
+
for index = 1, BLOBS do
|
|
79
|
+
blobs[index] = {
|
|
80
|
+
frame = frames[index],
|
|
81
|
+
position = Vector2.new(Draw.hash(index, 21) * size, Draw.hash(index, 22) * size),
|
|
82
|
+
scale = 0.4 + Draw.hash(index, 23) * 1.6,
|
|
83
|
+
hue = Draw.hash(index, 24),
|
|
84
|
+
phase = Draw.hash(index, 25) * math.pi * 2,
|
|
85
|
+
}
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
function Nebula.unmount()
|
|
90
|
+
blobs = {}
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
function Nebula.paint(ctx: Theme.Context, delta: number)
|
|
94
|
+
-- Clamped because Studio hands out long frames while it compiles, and an
|
|
95
|
+
-- unclamped step teleports the whole field across the cell in one of them.
|
|
96
|
+
local step = math.min(delta, 1 / 30)
|
|
97
|
+
local drift = 6 + ctx.energy * 22
|
|
98
|
+
|
|
99
|
+
for index = 1, #blobs do
|
|
100
|
+
local blob = blobs[index]
|
|
101
|
+
blob.position += flow(blob.position, ctx.clock + blob.phase) * drift * step
|
|
102
|
+
|
|
103
|
+
-- Wrapped rather than respawned. A blob that reappears at a random point
|
|
104
|
+
-- pops; one that comes back around the far edge is simply still moving.
|
|
105
|
+
local margin = 6
|
|
106
|
+
if blob.position.X < -margin then
|
|
107
|
+
blob.position = Vector2.new(size + margin, blob.position.Y)
|
|
108
|
+
elseif blob.position.X > size + margin then
|
|
109
|
+
blob.position = Vector2.new(-margin, blob.position.Y)
|
|
110
|
+
end
|
|
111
|
+
if blob.position.Y < -margin then
|
|
112
|
+
blob.position = Vector2.new(blob.position.X, size + margin)
|
|
113
|
+
elseif blob.position.Y > size + margin then
|
|
114
|
+
blob.position = Vector2.new(blob.position.X, -margin)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
--[[
|
|
118
|
+
Breathing on the blob's own phase, so the cloud is never uniform.
|
|
119
|
+
The bloom on top of it is shared, which is what makes a reply read as
|
|
120
|
+
one event happening to the whole field rather than as twenty
|
|
121
|
+
independent flickers.
|
|
122
|
+
]]
|
|
123
|
+
local breathe = 0.85 + 0.15 * math.sin(ctx.clock * 0.8 + blob.phase)
|
|
124
|
+
local diameter = math.max(
|
|
125
|
+
2,
|
|
126
|
+
math.floor(blob.scale * 5 * breathe * ctx.scale + ctx.flare * 4)
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
local colour = ctx.palette.violet:Lerp(ctx.palette.cyan, blob.hue)
|
|
130
|
+
Draw.dot(
|
|
131
|
+
blob.frame,
|
|
132
|
+
blob.position,
|
|
133
|
+
diameter,
|
|
134
|
+
colour:Lerp(Color3.new(1, 1, 1), ctx.flare * 0.5),
|
|
135
|
+
--[[
|
|
136
|
+
Deliberately very transparent. Dust is only dust when the blobs
|
|
137
|
+
overlap into something denser than any one of them, and at any
|
|
138
|
+
less than this each blob reads as a discrete circle.
|
|
139
|
+
]]
|
|
140
|
+
math.clamp(0.82 - ctx.energy * 0.12 - ctx.flare * 0.25, 0, 1)
|
|
141
|
+
)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
--[[
|
|
146
|
+
History as columns of dust lit from below rather than as bars.
|
|
147
|
+
|
|
148
|
+
Same data, same heights, but drawn with a rounded cap and no hard baseline,
|
|
149
|
+
so the trace belongs to the same picture as the cell.
|
|
150
|
+
]]
|
|
151
|
+
function Nebula.paintSlot(slot: Theme.Slot, ctx: Theme.Context)
|
|
152
|
+
local frame = slot.frame
|
|
153
|
+
local recency = 1 - slot.age
|
|
154
|
+
local height = math.max(2, math.floor(2 + slot.weight * 20))
|
|
155
|
+
|
|
156
|
+
frame.AnchorPoint = Vector2.new(0.5, 1)
|
|
157
|
+
frame.Size = UDim2.fromOffset(3, height)
|
|
158
|
+
frame.Position = UDim2.new(frame.Position.X.Scale, frame.Position.X.Offset, 1, 0)
|
|
159
|
+
frame.BackgroundColor3 = if slot.ok
|
|
160
|
+
then ctx.palette.violet:Lerp(ctx.palette.cyan, slot.weight)
|
|
161
|
+
else ctx.palette.red
|
|
162
|
+
frame.BackgroundTransparency = math.clamp(0.72 - recency * 0.6, 0, 1)
|
|
163
|
+
frame.Rotation = 0
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
function Nebula.swatch(frame: Frame, palette: Theme.Palette)
|
|
167
|
+
local pool = Draw.pool(frame, 12, "Blob", true)
|
|
168
|
+
for index = 1, 12 do
|
|
169
|
+
local x = 4 + Draw.hash(index, 31) * 32
|
|
170
|
+
local y = 3 + Draw.hash(index, 32) * 18
|
|
171
|
+
local hue = Draw.hash(index, 33)
|
|
172
|
+
Draw.dot(
|
|
173
|
+
pool[index],
|
|
174
|
+
Vector2.new(x, y),
|
|
175
|
+
4 + math.floor(Draw.hash(index, 34) * 6),
|
|
176
|
+
palette.violet:Lerp(palette.cyan, hue),
|
|
177
|
+
0.7
|
|
178
|
+
)
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
return Nebula
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--[[
|
|
3
|
+
Observatory -- AMOLED, and a session you can read as a sky.
|
|
4
|
+
|
|
5
|
+
The background is true #000000. On an OLED display those pixels are off, so
|
|
6
|
+
the panel has no edges at all and the console appears to float in the Studio
|
|
7
|
+
dock rather than sit in a box. That only works if the black is actually
|
|
8
|
+
black: a near-black at RGB(11, 12, 20) lights every pixel and the illusion
|
|
9
|
+
collapses, which is why this theme does not simply darken the default one.
|
|
10
|
+
|
|
11
|
+
The cell holds a star sphere instead of a prism -- points on a shell, turned
|
|
12
|
+
slowly and projected, twinkling on their own phases. It reads as depth
|
|
13
|
+
without a single straight line, which is the whole point of the contrast
|
|
14
|
+
with Lattice.
|
|
15
|
+
|
|
16
|
+
The trace is the reason the theme exists. Each finished call is a star whose
|
|
17
|
+
height is how slow it was and whose brightness fades as it ages, so forty
|
|
18
|
+
calls of history read as a constellation rather than as a bar chart. A
|
|
19
|
+
failure is a red giant: bigger, redder, and slower to fade, so the one thing
|
|
20
|
+
you actually want to find in a session's history is the one thing the eye
|
|
21
|
+
lands on first.
|
|
22
|
+
]]
|
|
23
|
+
|
|
24
|
+
local Draw = require(script.Parent.Draw)
|
|
25
|
+
local Theme = require(script.Parent.Theme)
|
|
26
|
+
|
|
27
|
+
-- Points on the shell. Enough to read as a field, few enough that every one of
|
|
28
|
+
-- them is a Frame the paint loop touches every frame.
|
|
29
|
+
local STARS = 26
|
|
30
|
+
local FOCAL = 3.2
|
|
31
|
+
local SHELL = 0.36
|
|
32
|
+
|
|
33
|
+
local Observatory = {}
|
|
34
|
+
|
|
35
|
+
Observatory.id = "observatory"
|
|
36
|
+
Observatory.label = "Observatory"
|
|
37
|
+
Observatory.blurb = "True black. Stars"
|
|
38
|
+
|
|
39
|
+
Observatory.palette = {
|
|
40
|
+
-- Genuinely off, not merely dark. Every other colour here is chosen to hold
|
|
41
|
+
-- contrast against zero rather than against a lifted black.
|
|
42
|
+
background = Color3.fromRGB(0, 0, 0),
|
|
43
|
+
surface = Color3.fromRGB(8, 9, 12),
|
|
44
|
+
rule = Color3.fromRGB(148, 178, 214),
|
|
45
|
+
text = Color3.fromRGB(214, 224, 238),
|
|
46
|
+
dim = Color3.fromRGB(78, 88, 106),
|
|
47
|
+
green = Color3.fromRGB(126, 226, 176),
|
|
48
|
+
red = Color3.fromRGB(248, 122, 122),
|
|
49
|
+
amber = Color3.fromRGB(240, 196, 118),
|
|
50
|
+
cyan = Color3.fromRGB(150, 214, 240),
|
|
51
|
+
violet = Color3.fromRGB(176, 172, 236),
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
local stars: { Frame } = {}
|
|
55
|
+
local meridian: { Frame } = {}
|
|
56
|
+
|
|
57
|
+
--[[
|
|
58
|
+
A fixed point on the shell for star `index`.
|
|
59
|
+
|
|
60
|
+
Derived from the index rather than stored, and derived the same way every
|
|
61
|
+
frame -- a field that re-randomised itself per frame would boil rather than
|
|
62
|
+
drift.
|
|
63
|
+
|
|
64
|
+
Evenly spread rather than randomly placed. A hashed longitude and latitude
|
|
65
|
+
is the obvious way to do this and it produced a visibly lopsided sky: the
|
|
66
|
+
field bunched into one corner of the cell with an empty half beside it,
|
|
67
|
+
which reads as a rendering fault rather than as stars. Twinkle supplies the
|
|
68
|
+
irregularity instead, and it does it without leaving holes.
|
|
69
|
+
]]
|
|
70
|
+
local function starAt(index: number): Vector3
|
|
71
|
+
return Draw.sphere(index, STARS)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
function Observatory.mount(cell: Frame)
|
|
75
|
+
-- The meridian sits under the stars so a bright point is never cut by the
|
|
76
|
+
-- line it happens to cross.
|
|
77
|
+
meridian = Draw.pool(cell, 24, "Meridian", true)
|
|
78
|
+
stars = Draw.pool(cell, STARS, "Star", true)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
function Observatory.unmount()
|
|
82
|
+
stars = {}
|
|
83
|
+
meridian = {}
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
function Observatory.paint(ctx: Theme.Context, _delta: number)
|
|
87
|
+
local radius = ctx.size * SHELL * ctx.scale
|
|
88
|
+
-- A third of Lattice's rate. Stars that turn at a wireframe's pace read as
|
|
89
|
+
-- a spinning object; at this speed they read as a sky the panel is looking
|
|
90
|
+
-- out at, which is the difference the theme is trading on.
|
|
91
|
+
local angle = ctx.angle * 0.34
|
|
92
|
+
|
|
93
|
+
for index = 1, STARS do
|
|
94
|
+
local frame = stars[index]
|
|
95
|
+
if frame == nil then
|
|
96
|
+
continue
|
|
97
|
+
end
|
|
98
|
+
local point, depth = Draw.project(
|
|
99
|
+
Draw.rotate(starAt(index), angle),
|
|
100
|
+
FOCAL,
|
|
101
|
+
radius,
|
|
102
|
+
ctx.centre
|
|
103
|
+
)
|
|
104
|
+
local near = math.clamp((depth + 1) / 2, 0, 1)
|
|
105
|
+
|
|
106
|
+
--[[
|
|
107
|
+
Each star on its own phase and rate, so the field never pulses as one.
|
|
108
|
+
|
|
109
|
+
A shared clock would make the whole sphere breathe together, which
|
|
110
|
+
reads as the panel flickering rather than as stars twinkling. The
|
|
111
|
+
rates are deliberately irrational relative to each other.
|
|
112
|
+
]]
|
|
113
|
+
local twinkle = 0.5 + 0.5 * math.sin(ctx.clock * (0.7 + Draw.hash(index, 3) * 1.9)
|
|
114
|
+
+ Draw.hash(index, 4) * 9)
|
|
115
|
+
|
|
116
|
+
local brightness = near * 0.7 + twinkle * 0.3
|
|
117
|
+
Draw.dot(
|
|
118
|
+
frame,
|
|
119
|
+
point,
|
|
120
|
+
math.max(1, math.floor(1 + near * 2 + ctx.flare * 2)),
|
|
121
|
+
ctx.tint:Lerp(Color3.new(1, 1, 1), 0.3 + brightness * 0.6),
|
|
122
|
+
math.clamp(0.9 - brightness * 0.8 - ctx.flare * 0.4, 0, 1)
|
|
123
|
+
)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
--[[
|
|
127
|
+
A great circle through the sphere: the horizon an observatory is aimed
|
|
128
|
+
along. Drawn as points rather than segments so it fades in and out with
|
|
129
|
+
depth like the stars do, instead of reading as a hard hoop drawn on top
|
|
130
|
+
of them.
|
|
131
|
+
]]
|
|
132
|
+
-- 1.05 rather than the 1.18 this started at: at full overshoot the wider
|
|
133
|
+
-- ring reached 21.5px from a centre with 20px of room, so the horizon was
|
|
134
|
+
-- sliced off by the cell's own clipping exactly when the session was busiest.
|
|
135
|
+
for index = 1, #meridian do
|
|
136
|
+
local frame = meridian[index]
|
|
137
|
+
local turn = (index / #meridian) * math.pi * 2
|
|
138
|
+
local point, depth = Draw.project(
|
|
139
|
+
Draw.rotate(Vector3.new(math.cos(turn), 0, math.sin(turn)), angle),
|
|
140
|
+
FOCAL,
|
|
141
|
+
radius * 1.05,
|
|
142
|
+
ctx.centre
|
|
143
|
+
)
|
|
144
|
+
local near = math.clamp((depth + 1) / 2, 0, 1)
|
|
145
|
+
Draw.dot(frame, point, 1, ctx.tint, math.clamp(0.94 - near * 0.35, 0, 1))
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
--[[
|
|
150
|
+
One call as a star rather than as a bar.
|
|
151
|
+
|
|
152
|
+
Height is duration, so a slow call sits high and the eye reads the skyline
|
|
153
|
+
as a latency profile without being told to. Brightness is recency, so the
|
|
154
|
+
newest calls are the ones that stand out and old history recedes on its own
|
|
155
|
+
rather than needing to be scrolled away.
|
|
156
|
+
]]
|
|
157
|
+
function Observatory.paintSlot(slot: Theme.Slot, ctx: Theme.Context)
|
|
158
|
+
local frame = slot.frame
|
|
159
|
+
local recency = 1 - slot.age
|
|
160
|
+
|
|
161
|
+
-- Failures ignore the height rule. A red giant is large because it failed,
|
|
162
|
+
-- not because it was slow, and conflating the two would hide a fast error
|
|
163
|
+
-- among fast successes at the bottom of the trace.
|
|
164
|
+
local size = if slot.ok
|
|
165
|
+
then math.max(2, math.floor(1.5 + slot.weight * 2.5 + recency * 1.5))
|
|
166
|
+
else math.max(4, math.floor(4 + recency * 2))
|
|
167
|
+
|
|
168
|
+
frame.AnchorPoint = Vector2.new(0.5, 0.5)
|
|
169
|
+
frame.Size = UDim2.fromOffset(size, size)
|
|
170
|
+
-- 0.12 keeps the newest star clear of the trace's top edge, and the 0.82
|
|
171
|
+
-- span leaves the baseline visible under the lowest one.
|
|
172
|
+
frame.Position = UDim2.new(frame.Position.X.Scale, frame.Position.X.Offset, 0.9 - slot.weight * 0.78, 0)
|
|
173
|
+
frame.BackgroundColor3 = if slot.ok
|
|
174
|
+
then ctx.palette.cyan:Lerp(Color3.new(1, 1, 1), 0.25 + recency * 0.4)
|
|
175
|
+
else ctx.palette.red
|
|
176
|
+
frame.BackgroundTransparency = if slot.ok
|
|
177
|
+
then math.clamp(0.72 - recency * 0.62, 0, 1)
|
|
178
|
+
-- Slower to fade than a success, so the failure in a session's history
|
|
179
|
+
-- is still legible forty calls later.
|
|
180
|
+
else math.clamp(0.45 - recency * 0.4, 0, 1)
|
|
181
|
+
frame.Rotation = 0
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
function Observatory.swatch(frame: Frame, palette: Theme.Palette)
|
|
185
|
+
local pool = Draw.pool(frame, 14, "Star", true)
|
|
186
|
+
for index = 1, 14 do
|
|
187
|
+
local x = 3 + Draw.hash(index, 11) * 34
|
|
188
|
+
local y = 3 + Draw.hash(index, 12) * 18
|
|
189
|
+
local bright = Draw.hash(index, 13)
|
|
190
|
+
Draw.dot(
|
|
191
|
+
pool[index],
|
|
192
|
+
Vector2.new(x, y),
|
|
193
|
+
if bright > 0.85 then 3 else 1 + math.floor(bright * 2),
|
|
194
|
+
palette.cyan:Lerp(Color3.new(1, 1, 1), bright * 0.7),
|
|
195
|
+
0.85 - bright * 0.7
|
|
196
|
+
)
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
return Observatory
|
|
@@ -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
|