@guinetik/gcanvas 1.0.0 → 1.0.1

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 (68) hide show
  1. package/demos/fluid-simple.html +22 -0
  2. package/demos/fluid.html +37 -0
  3. package/demos/index.html +2 -0
  4. package/demos/js/blob.js +18 -5
  5. package/demos/js/fluid-simple.js +253 -0
  6. package/demos/js/fluid.js +527 -0
  7. package/demos/js/tde/accretiondisk.js +64 -11
  8. package/demos/js/tde/blackholescene.js +2 -2
  9. package/demos/js/tde/config.js +2 -2
  10. package/demos/js/tde/index.js +152 -27
  11. package/demos/js/tde/lensedstarfield.js +32 -25
  12. package/demos/js/tde/tdestar.js +78 -98
  13. package/demos/js/tde/tidalstream.js +23 -7
  14. package/docs/README.md +230 -222
  15. package/docs/api/FluidSystem.md +173 -0
  16. package/docs/concepts/architecture-overview.md +204 -204
  17. package/docs/concepts/rendering-pipeline.md +279 -279
  18. package/docs/concepts/two-layer-architecture.md +229 -229
  19. package/docs/fluid-dynamics.md +97 -0
  20. package/docs/getting-started/first-game.md +354 -354
  21. package/docs/getting-started/installation.md +175 -157
  22. package/docs/modules/collision/README.md +2 -2
  23. package/docs/modules/fluent/README.md +6 -6
  24. package/docs/modules/game/README.md +303 -303
  25. package/docs/modules/isometric-camera.md +2 -2
  26. package/docs/modules/isometric.md +1 -1
  27. package/docs/modules/painter/README.md +328 -328
  28. package/docs/modules/particle/README.md +3 -3
  29. package/docs/modules/shapes/README.md +221 -221
  30. package/docs/modules/shapes/base/euclidian.md +123 -123
  31. package/docs/modules/shapes/base/shape.md +262 -262
  32. package/docs/modules/shapes/base/transformable.md +243 -243
  33. package/docs/modules/state/README.md +2 -2
  34. package/docs/modules/util/README.md +1 -1
  35. package/docs/modules/util/camera3d.md +3 -3
  36. package/docs/modules/util/scene3d.md +1 -1
  37. package/package.json +3 -1
  38. package/readme.md +19 -5
  39. package/src/collision/collision.js +75 -0
  40. package/src/game/index.js +2 -1
  41. package/src/game/pipeline.js +3 -3
  42. package/src/game/systems/FluidSystem.js +835 -0
  43. package/src/game/systems/index.js +11 -0
  44. package/src/game/ui/button.js +39 -18
  45. package/src/game/ui/cursor.js +14 -0
  46. package/src/game/ui/fps.js +12 -4
  47. package/src/game/ui/index.js +2 -0
  48. package/src/game/ui/stepper.js +549 -0
  49. package/src/game/ui/theme.js +121 -0
  50. package/src/game/ui/togglebutton.js +9 -3
  51. package/src/game/ui/tooltip.js +11 -4
  52. package/src/math/fluid.js +507 -0
  53. package/src/math/index.js +2 -0
  54. package/src/mixins/anchor.js +17 -7
  55. package/src/motion/tweenetik.js +16 -0
  56. package/src/shapes/index.js +1 -0
  57. package/src/util/camera3d.js +218 -12
  58. package/types/fluent.d.ts +361 -0
  59. package/types/game.d.ts +303 -0
  60. package/types/index.d.ts +144 -5
  61. package/types/math.d.ts +361 -0
  62. package/types/motion.d.ts +271 -0
  63. package/types/particle.d.ts +373 -0
  64. package/types/shapes.d.ts +107 -9
  65. package/types/util.d.ts +353 -0
  66. package/types/webgl.d.ts +109 -0
  67. package/disk_example.png +0 -0
  68. package/tde.png +0 -0
@@ -0,0 +1,549 @@
1
+ /***************************************************************
2
+ * Stepper.js
3
+ *
4
+ * A numeric stepper UI component with increment/decrement buttons.
5
+ * Displays a value with [-] and [+] buttons on either side.
6
+ *
7
+ * Theme: Terminal × Vercel aesthetic
8
+ * - Dark transparent backgrounds
9
+ * - Neon green accents (#0f0)
10
+ * - Inverted colors on hover
11
+ ***************************************************************/
12
+
13
+ import { Group, Rectangle, TextShape } from "../../shapes";
14
+ import { GameObject } from "../objects/go";
15
+ import { UI_THEME } from "./theme.js";
16
+
17
+ /**
18
+ * Stepper - A numeric input component with increment/decrement buttons.
19
+ *
20
+ * The Stepper displays:
21
+ * - A decrement button [-]
22
+ * - A value display showing the current number
23
+ * - An increment button [+]
24
+ *
25
+ * Supports min/max bounds, custom step size, and onChange callbacks.
26
+ *
27
+ * Example usage:
28
+ * ```js
29
+ * const stepper = new Stepper(game, {
30
+ * x: 200,
31
+ * y: 100,
32
+ * value: 5,
33
+ * min: 0,
34
+ * max: 10,
35
+ * step: 1,
36
+ * onChange: (value) => console.log("New value:", value)
37
+ * });
38
+ * game.pipeline.add(stepper);
39
+ * ```
40
+ *
41
+ * @extends GameObject
42
+ */
43
+ export class Stepper extends GameObject {
44
+ /**
45
+ * Create a Stepper instance.
46
+ * @param {Game} game - The main game instance.
47
+ * @param {object} [options={}] - Configuration for the Stepper.
48
+ * @param {number} [options.x=0] - X-position of the Stepper (center).
49
+ * @param {number} [options.y=0] - Y-position of the Stepper (center).
50
+ * @param {number} [options.value=0] - Initial value.
51
+ * @param {number} [options.min=-Infinity] - Minimum allowed value.
52
+ * @param {number} [options.max=Infinity] - Maximum allowed value.
53
+ * @param {number} [options.step=1] - Amount to increment/decrement per click.
54
+ * @param {number} [options.buttonSize=32] - Size of the +/- buttons.
55
+ * @param {number} [options.valueWidth=60] - Width of the value display area.
56
+ * @param {number} [options.height=32] - Height of the stepper.
57
+ * @param {number} [options.gap=4] - Gap between elements.
58
+ * @param {string} [options.font="14px monospace"] - Font for text.
59
+ * @param {Function} [options.onChange=null] - Callback when value changes.
60
+ * @param {Function} [options.formatValue=null] - Custom formatter for display value.
61
+ * @param {string} [options.label=""] - Optional label text above the stepper.
62
+ */
63
+ constructor(game, options = {}) {
64
+ super(game, options);
65
+
66
+ const {
67
+ x = 0,
68
+ y = 0,
69
+ value = 0,
70
+ min = -Infinity,
71
+ max = Infinity,
72
+ step = 1,
73
+ buttonSize = 32,
74
+ valueWidth = 60,
75
+ height = 32,
76
+ gap = 4,
77
+ font = UI_THEME.fonts.medium,
78
+ onChange = null,
79
+ formatValue = null,
80
+ label = "",
81
+ } = options;
82
+
83
+ // Position and sizing
84
+ this.x = x;
85
+ this.y = y;
86
+ this.buttonSize = buttonSize;
87
+ this.valueWidth = valueWidth;
88
+ this.stepperHeight = height;
89
+ this.gap = gap;
90
+ this.font = font;
91
+
92
+ // Value state
93
+ this._value = this.clamp(value, min, max);
94
+ this.min = min;
95
+ this.max = max;
96
+ this.step = step;
97
+
98
+ // Callbacks
99
+ this.onChange = onChange;
100
+ this.formatValue = formatValue || ((v) => String(v));
101
+ this.labelText = label;
102
+
103
+ // Calculate total width
104
+ this.width = buttonSize + gap + valueWidth + gap + buttonSize;
105
+ this.height = label ? height + 20 : height;
106
+
107
+ // Initialize components
108
+ this.initComponents();
109
+ this.initEvents();
110
+ }
111
+
112
+ /**
113
+ * Clamp a value between min and max
114
+ * @param {number} val - Value to clamp
115
+ * @param {number} min - Minimum bound
116
+ * @param {number} max - Maximum bound
117
+ * @returns {number} Clamped value
118
+ * @private
119
+ */
120
+ clamp(val, min, max) {
121
+ return Math.max(min, Math.min(max, val));
122
+ }
123
+
124
+ /**
125
+ * Initialize all visual components
126
+ * @private
127
+ */
128
+ initComponents() {
129
+ this.group = new Group();
130
+
131
+ // Calculate positions (centered layout)
132
+ const totalWidth = this.width;
133
+ const halfWidth = totalWidth / 2;
134
+ const btnHalf = this.buttonSize / 2;
135
+ const valHalf = this.valueWidth / 2;
136
+
137
+ // Positions from center
138
+ const decrementX = -halfWidth + btnHalf;
139
+ const valueX = 0;
140
+ const incrementX = halfWidth - btnHalf;
141
+
142
+ // Calculate vertical positioning
143
+ // When label exists, layout is: [label] [gap] [controls]
144
+ // Everything should fit within this.height bounds
145
+ const labelHeight = 12; // Approximate height of label text
146
+ const labelGap = 4;
147
+
148
+ // Controls Y offset: positive when label exists (pushes controls down)
149
+ const controlsY = this.labelText
150
+ ? (labelHeight + labelGap) / 2 // Controls shift down to make room for label
151
+ : 0;
152
+
153
+ // Label Y offset: negative (above center)
154
+ const labelY = this.labelText
155
+ ? -(this.stepperHeight / 2 + labelGap) // Label sits above controls
156
+ : 0;
157
+
158
+ // Create optional label
159
+ if (this.labelText) {
160
+ this.label = new TextShape(this.labelText, {
161
+ font: UI_THEME.fonts.small,
162
+ color: UI_THEME.colors.dimText,
163
+ align: "center",
164
+ baseline: "middle",
165
+ });
166
+ this.label.y = labelY;
167
+ this.group.add(this.label);
168
+ }
169
+
170
+ // Create decrement button background
171
+ this.decrementBg = new Rectangle({
172
+ width: this.buttonSize,
173
+ height: this.stepperHeight,
174
+ color: UI_THEME.button.default.bg,
175
+ stroke: UI_THEME.button.default.stroke,
176
+ lineWidth: 1,
177
+ });
178
+ this.decrementBg.x = decrementX;
179
+ this.decrementBg.y = controlsY;
180
+
181
+ // Create decrement button text
182
+ this.decrementText = new TextShape("−", {
183
+ font: this.font,
184
+ color: UI_THEME.button.default.text,
185
+ align: "center",
186
+ baseline: "middle",
187
+ });
188
+ this.decrementText.x = decrementX;
189
+ this.decrementText.y = controlsY;
190
+
191
+ // Create value display background
192
+ this.valueBg = new Rectangle({
193
+ width: this.valueWidth,
194
+ height: this.stepperHeight,
195
+ color: UI_THEME.colors.darkerBg,
196
+ stroke: UI_THEME.colors.subtleBorder,
197
+ lineWidth: 1,
198
+ });
199
+ this.valueBg.x = valueX;
200
+ this.valueBg.y = controlsY;
201
+
202
+ // Create value display text
203
+ this.valueText = new TextShape(this.formatValue(this._value), {
204
+ font: this.font,
205
+ color: UI_THEME.colors.neonGreen,
206
+ align: "center",
207
+ baseline: "middle",
208
+ });
209
+ this.valueText.x = valueX;
210
+ this.valueText.y = controlsY;
211
+
212
+ // Create increment button background
213
+ this.incrementBg = new Rectangle({
214
+ width: this.buttonSize,
215
+ height: this.stepperHeight,
216
+ color: UI_THEME.button.default.bg,
217
+ stroke: UI_THEME.button.default.stroke,
218
+ lineWidth: 1,
219
+ });
220
+ this.incrementBg.x = incrementX;
221
+ this.incrementBg.y = controlsY;
222
+
223
+ // Create increment button text
224
+ this.incrementText = new TextShape("+", {
225
+ font: this.font,
226
+ color: UI_THEME.button.default.text,
227
+ align: "center",
228
+ baseline: "middle",
229
+ });
230
+ this.incrementText.x = incrementX;
231
+ this.incrementText.y = controlsY;
232
+
233
+ // Add all to group (order matters for rendering)
234
+ this.group.add(this.decrementBg);
235
+ this.group.add(this.decrementText);
236
+ this.group.add(this.valueBg);
237
+ this.group.add(this.valueText);
238
+ this.group.add(this.incrementBg);
239
+ this.group.add(this.incrementText);
240
+
241
+ // Store button bounds for hit testing
242
+ this._decrementBounds = {
243
+ x: decrementX - this.buttonSize / 2,
244
+ y: controlsY - this.stepperHeight / 2,
245
+ width: this.buttonSize,
246
+ height: this.stepperHeight,
247
+ };
248
+
249
+ this._incrementBounds = {
250
+ x: incrementX - this.buttonSize / 2,
251
+ y: controlsY - this.stepperHeight / 2,
252
+ width: this.buttonSize,
253
+ height: this.stepperHeight,
254
+ };
255
+
256
+ // Track hover states
257
+ this._decrementHover = false;
258
+ this._incrementHover = false;
259
+ this._decrementPressed = false;
260
+ this._incrementPressed = false;
261
+ }
262
+
263
+ /**
264
+ * Initialize event handlers
265
+ * @private
266
+ */
267
+ initEvents() {
268
+ this.interactive = true;
269
+ this._isMouseOver = false;
270
+
271
+ // Use mouseover/mouseout to know when we're over the stepper
272
+ this.on("mouseover", () => {
273
+ this._isMouseOver = true;
274
+ });
275
+
276
+ this.on("mouseout", () => {
277
+ this._isMouseOver = false;
278
+ this.handleMouseOut();
279
+ });
280
+
281
+ // Use global inputmove for continuous position tracking (like Tooltip)
282
+ this.game.events.on("inputmove", (e) => {
283
+ if (this._isMouseOver) {
284
+ this.handleMouseMove(e);
285
+ }
286
+ });
287
+
288
+ this.on("inputdown", (e) => this.handleInputDown(e));
289
+ this.on("inputup", () => this.handleInputUp());
290
+ }
291
+
292
+ /**
293
+ * Convert screen coordinates to local space, accounting for parent hierarchy.
294
+ * Uses the same transform chain logic as GameObject._hitTest.
295
+ * @param {number} screenX - Screen X coordinate
296
+ * @param {number} screenY - Screen Y coordinate
297
+ * @returns {{x: number, y: number}} Local coordinates
298
+ * @private
299
+ */
300
+ screenToLocal(screenX, screenY) {
301
+ let localX = screenX;
302
+ let localY = screenY;
303
+
304
+ // Build transform chain (from root to this object)
305
+ const transformChain = [];
306
+ let current = this;
307
+ while (current) {
308
+ transformChain.unshift(current);
309
+ current = current.parent;
310
+ }
311
+
312
+ // Apply inverse transforms in sequence (from root to object)
313
+ for (const obj of transformChain) {
314
+ // Translation: subtract object position
315
+ localX -= obj.x || 0;
316
+ localY -= obj.y || 0;
317
+
318
+ // Rotation: apply inverse rotation if needed
319
+ if (obj.rotation) {
320
+ const cos = Math.cos(-obj.rotation);
321
+ const sin = Math.sin(-obj.rotation);
322
+ const tempX = localX;
323
+ localX = tempX * cos - localY * sin;
324
+ localY = tempX * sin + localY * cos;
325
+ }
326
+
327
+ // Scale: apply inverse scale if needed
328
+ if (obj.scaleX !== undefined && obj.scaleX !== 0) {
329
+ localX /= obj.scaleX;
330
+ }
331
+ if (obj.scaleY !== undefined && obj.scaleY !== 0) {
332
+ localY /= obj.scaleY;
333
+ }
334
+ }
335
+
336
+ return { x: localX, y: localY };
337
+ }
338
+
339
+ /**
340
+ * Check if a local point is within button bounds
341
+ * @param {number} localX - Local X coordinate
342
+ * @param {number} localY - Local Y coordinate
343
+ * @param {object} bounds - Button bounds object
344
+ * @returns {boolean} True if point is within bounds
345
+ * @private
346
+ */
347
+ isPointInBounds(localX, localY, bounds) {
348
+ return (
349
+ localX >= bounds.x &&
350
+ localX <= bounds.x + bounds.width &&
351
+ localY >= bounds.y &&
352
+ localY <= bounds.y + bounds.height
353
+ );
354
+ }
355
+
356
+ /**
357
+ * Handle mouse move for hover states
358
+ * @param {object} e - Event object with x, y coordinates
359
+ * @private
360
+ */
361
+ handleMouseMove(e) {
362
+ // Convert screen coordinates to local space (accounts for parent hierarchy)
363
+ const local = this.screenToLocal(e.x, e.y);
364
+
365
+ const wasDecHover = this._decrementHover;
366
+ const wasIncHover = this._incrementHover;
367
+
368
+ this._decrementHover = this.isPointInBounds(local.x, local.y, this._decrementBounds);
369
+ this._incrementHover = this.isPointInBounds(local.x, local.y, this._incrementBounds);
370
+
371
+ // Update visuals if hover state changed
372
+ if (wasDecHover !== this._decrementHover || wasIncHover !== this._incrementHover) {
373
+ this.updateButtonStates();
374
+ }
375
+
376
+ // Update cursor
377
+ if (this._decrementHover || this._incrementHover) {
378
+ this.game.canvas.style.cursor = "pointer";
379
+ } else {
380
+ this.game.canvas.style.cursor = "default";
381
+ }
382
+ }
383
+
384
+ /**
385
+ * Handle mouse out
386
+ * @private
387
+ */
388
+ handleMouseOut() {
389
+ this._decrementHover = false;
390
+ this._incrementHover = false;
391
+ this._decrementPressed = false;
392
+ this._incrementPressed = false;
393
+ this.updateButtonStates();
394
+ this.game.canvas.style.cursor = "default";
395
+ }
396
+
397
+ /**
398
+ * Handle input down (click/tap)
399
+ * @param {object} e - Event object
400
+ * @private
401
+ */
402
+ handleInputDown(e) {
403
+ // Convert screen coordinates to local space (accounts for parent hierarchy)
404
+ const local = this.screenToLocal(e.x, e.y);
405
+
406
+ if (this.isPointInBounds(local.x, local.y, this._decrementBounds)) {
407
+ this._decrementPressed = true;
408
+ this.decrement();
409
+ } else if (this.isPointInBounds(local.x, local.y, this._incrementBounds)) {
410
+ this._incrementPressed = true;
411
+ this.increment();
412
+ }
413
+
414
+ this.updateButtonStates();
415
+ }
416
+
417
+ /**
418
+ * Handle input up
419
+ * @private
420
+ */
421
+ handleInputUp() {
422
+ this._decrementPressed = false;
423
+ this._incrementPressed = false;
424
+ this.updateButtonStates();
425
+ }
426
+
427
+ /**
428
+ * Update button visual states based on hover/pressed
429
+ * @private
430
+ */
431
+ updateButtonStates() {
432
+ // Decrement button
433
+ if (this._decrementPressed) {
434
+ this.decrementBg.color = UI_THEME.button.pressed.bg;
435
+ this.decrementBg.stroke = UI_THEME.button.pressed.stroke;
436
+ this.decrementText.color = UI_THEME.button.pressed.text;
437
+ } else if (this._decrementHover) {
438
+ this.decrementBg.color = UI_THEME.button.hover.bg;
439
+ this.decrementBg.stroke = UI_THEME.button.hover.stroke;
440
+ this.decrementText.color = UI_THEME.button.hover.text;
441
+ } else {
442
+ this.decrementBg.color = UI_THEME.button.default.bg;
443
+ this.decrementBg.stroke = UI_THEME.button.default.stroke;
444
+ this.decrementText.color = UI_THEME.button.default.text;
445
+ }
446
+
447
+ // Check if at min - dim the decrement button
448
+ if (this._value <= this.min) {
449
+ this.decrementBg.stroke = UI_THEME.colors.subtleBorder;
450
+ this.decrementText.color = UI_THEME.colors.dimText;
451
+ }
452
+
453
+ // Increment button
454
+ if (this._incrementPressed) {
455
+ this.incrementBg.color = UI_THEME.button.pressed.bg;
456
+ this.incrementBg.stroke = UI_THEME.button.pressed.stroke;
457
+ this.incrementText.color = UI_THEME.button.pressed.text;
458
+ } else if (this._incrementHover) {
459
+ this.incrementBg.color = UI_THEME.button.hover.bg;
460
+ this.incrementBg.stroke = UI_THEME.button.hover.stroke;
461
+ this.incrementText.color = UI_THEME.button.hover.text;
462
+ } else {
463
+ this.incrementBg.color = UI_THEME.button.default.bg;
464
+ this.incrementBg.stroke = UI_THEME.button.default.stroke;
465
+ this.incrementText.color = UI_THEME.button.default.text;
466
+ }
467
+
468
+ // Check if at max - dim the increment button
469
+ if (this._value >= this.max) {
470
+ this.incrementBg.stroke = UI_THEME.colors.subtleBorder;
471
+ this.incrementText.color = UI_THEME.colors.dimText;
472
+ }
473
+ }
474
+
475
+ /**
476
+ * Increment the value by step amount
477
+ */
478
+ increment() {
479
+ this.value = this._value + this.step;
480
+ }
481
+
482
+ /**
483
+ * Decrement the value by step amount
484
+ */
485
+ decrement() {
486
+ this.value = this._value - this.step;
487
+ }
488
+
489
+ /**
490
+ * Get the current value
491
+ * @returns {number} Current value
492
+ */
493
+ get value() {
494
+ return this._value;
495
+ }
496
+
497
+ /**
498
+ * Set the value (clamped to min/max)
499
+ * @param {number} newValue - New value to set
500
+ */
501
+ set value(newValue) {
502
+ const clamped = this.clamp(newValue, this.min, this.max);
503
+
504
+ if (clamped !== this._value) {
505
+ this._value = clamped;
506
+ this.valueText.text = this.formatValue(this._value);
507
+ this.updateButtonStates();
508
+
509
+ if (typeof this.onChange === "function") {
510
+ this.onChange(this._value);
511
+ }
512
+ }
513
+ }
514
+
515
+ /**
516
+ * Set new min/max bounds
517
+ * @param {number} min - New minimum
518
+ * @param {number} max - New maximum
519
+ */
520
+ setBounds(min, max) {
521
+ this.min = min;
522
+ this.max = max;
523
+ // Re-clamp current value
524
+ this.value = this._value;
525
+ }
526
+
527
+ /**
528
+ * Get the bounding box of this Stepper for hit testing.
529
+ * Required for the event system to work properly.
530
+ * @returns {{x: number, y: number, width: number, height: number}} Bounds object
531
+ */
532
+ getBounds() {
533
+ return {
534
+ x: this.x,
535
+ y: this.y,
536
+ width: this.width,
537
+ height: this.height,
538
+ };
539
+ }
540
+
541
+ /**
542
+ * Render the Stepper
543
+ */
544
+ draw() {
545
+ super.draw();
546
+ this.group.render();
547
+ }
548
+ }
549
+
@@ -0,0 +1,121 @@
1
+ /***************************************************************
2
+ * theme.js
3
+ *
4
+ * Terminal × Vercel design system for GCanvas UI components.
5
+ *
6
+ * Aesthetic principles:
7
+ * - Dark translucent backgrounds (depth, layering)
8
+ * - Neon green (#0f0) as primary accent
9
+ * - Monospace typography throughout
10
+ * - Inverted colors on hover for clear feedback
11
+ * - Minimal, clean lines inspired by terminal UIs
12
+ ***************************************************************/
13
+
14
+ /**
15
+ * Core theme colors and values for UI components.
16
+ * Import this for consistent styling across custom UI elements.
17
+ *
18
+ * @example
19
+ * ```js
20
+ * import { UI_THEME } from "gcanvas";
21
+ *
22
+ * const myButton = new Button(game, {
23
+ * colorDefaultBg: UI_THEME.colors.darkBg,
24
+ * colorDefaultStroke: UI_THEME.colors.neonGreen,
25
+ * colorDefaultText: UI_THEME.colors.neonGreen,
26
+ * });
27
+ * ```
28
+ *
29
+ * @constant {Object}
30
+ */
31
+ export const UI_THEME = {
32
+ /**
33
+ * Color palette
34
+ */
35
+ colors: {
36
+ // Primary accent - terminal green
37
+ neonGreen: "#0f0",
38
+ terminalGreen: "#16F529",
39
+
40
+ // Secondary accent - cyan
41
+ cyanAccent: "#0ff",
42
+
43
+ // Backgrounds
44
+ darkBg: "rgba(0, 0, 0, 0.85)",
45
+ darkerBg: "rgba(0, 0, 0, 0.92)",
46
+ hoverBg: "#0f0",
47
+ pressedBg: "#0c0",
48
+ activeBg: "rgba(0, 255, 0, 0.15)",
49
+
50
+ // Text
51
+ lightText: "#0f0",
52
+ darkText: "#000",
53
+ dimText: "rgba(0, 255, 0, 0.7)",
54
+
55
+ // Borders
56
+ subtleBorder: "rgba(0, 255, 0, 0.4)",
57
+ activeBorder: "#0f0",
58
+ glowBorder: "rgba(0, 255, 0, 0.5)",
59
+ },
60
+
61
+ /**
62
+ * Typography
63
+ */
64
+ fonts: {
65
+ primary: "monospace",
66
+ small: "11px monospace",
67
+ medium: "14px monospace",
68
+ large: "18px monospace",
69
+ heading: "bold 24px monospace",
70
+ },
71
+
72
+ /**
73
+ * Spacing values
74
+ */
75
+ spacing: {
76
+ xs: 4,
77
+ sm: 8,
78
+ md: 12,
79
+ lg: 16,
80
+ xl: 24,
81
+ },
82
+
83
+ /**
84
+ * Pre-configured button color schemes
85
+ */
86
+ button: {
87
+ default: {
88
+ bg: "rgba(0, 0, 0, 0.85)",
89
+ stroke: "rgba(0, 255, 0, 0.4)",
90
+ text: "#0f0",
91
+ },
92
+ hover: {
93
+ bg: "#0f0",
94
+ stroke: "#0f0",
95
+ text: "#000",
96
+ },
97
+ pressed: {
98
+ bg: "#0c0",
99
+ stroke: "#0f0",
100
+ text: "#000",
101
+ },
102
+ active: {
103
+ bg: "rgba(0, 255, 0, 0.15)",
104
+ stroke: "#0f0",
105
+ text: "#0f0",
106
+ },
107
+ },
108
+
109
+ /**
110
+ * Pre-configured tooltip styles
111
+ */
112
+ tooltip: {
113
+ bg: "rgba(0, 0, 0, 0.92)",
114
+ border: "rgba(0, 255, 0, 0.5)",
115
+ text: "#0f0",
116
+ },
117
+ };
118
+
119
+ export default UI_THEME;
120
+
121
+
@@ -1,7 +1,12 @@
1
1
  import { Button } from "./button.js";
2
+ import { UI_THEME } from "./theme.js";
2
3
 
3
4
  /**
4
5
  * ToggleButton - A variant of Button with a persistent "toggled" (active) state.
6
+ *
7
+ * Theme: Terminal × Vercel aesthetic
8
+ * - When toggled ON: glowing green border with subtle green tint
9
+ * - When toggled OFF: inherits default button styling
5
10
  *
6
11
  * Usage:
7
12
  * const myToggle = new ToggleButton(game, {
@@ -41,9 +46,10 @@ export class ToggleButton extends Button {
41
46
  this.refreshToggleVisual();
42
47
  },
43
48
  });
44
- this.colorActiveBg = options.colorActiveBg || "#444";
45
- this.colorActiveStroke = options.colorActiveStroke || "#0f0";
46
- this.colorActiveText = options.colorActiveText || "#0f0";
49
+ // Terminal × Vercel theme for toggled state
50
+ this.colorActiveBg = options.colorActiveBg || UI_THEME.button.active.bg;
51
+ this.colorActiveStroke = options.colorActiveStroke || UI_THEME.button.active.stroke;
52
+ this.colorActiveText = options.colorActiveText || UI_THEME.button.active.text;
47
53
  // Track toggled state. Default is false unless 'startToggled' is set
48
54
  this.toggled = !!options.startToggled;
49
55