@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.
- package/demos/fluid-simple.html +22 -0
- package/demos/fluid.html +37 -0
- package/demos/index.html +2 -0
- package/demos/js/blob.js +18 -5
- package/demos/js/fluid-simple.js +253 -0
- package/demos/js/fluid.js +527 -0
- package/demos/js/tde/accretiondisk.js +64 -11
- package/demos/js/tde/blackholescene.js +2 -2
- package/demos/js/tde/config.js +2 -2
- package/demos/js/tde/index.js +152 -27
- package/demos/js/tde/lensedstarfield.js +32 -25
- package/demos/js/tde/tdestar.js +78 -98
- package/demos/js/tde/tidalstream.js +23 -7
- package/docs/README.md +230 -222
- package/docs/api/FluidSystem.md +173 -0
- package/docs/concepts/architecture-overview.md +204 -204
- package/docs/concepts/rendering-pipeline.md +279 -279
- package/docs/concepts/two-layer-architecture.md +229 -229
- package/docs/fluid-dynamics.md +97 -0
- package/docs/getting-started/first-game.md +354 -354
- package/docs/getting-started/installation.md +175 -157
- package/docs/modules/collision/README.md +2 -2
- package/docs/modules/fluent/README.md +6 -6
- package/docs/modules/game/README.md +303 -303
- package/docs/modules/isometric-camera.md +2 -2
- package/docs/modules/isometric.md +1 -1
- package/docs/modules/painter/README.md +328 -328
- package/docs/modules/particle/README.md +3 -3
- package/docs/modules/shapes/README.md +221 -221
- package/docs/modules/shapes/base/euclidian.md +123 -123
- package/docs/modules/shapes/base/shape.md +262 -262
- package/docs/modules/shapes/base/transformable.md +243 -243
- package/docs/modules/state/README.md +2 -2
- package/docs/modules/util/README.md +1 -1
- package/docs/modules/util/camera3d.md +3 -3
- package/docs/modules/util/scene3d.md +1 -1
- package/package.json +3 -1
- package/readme.md +19 -5
- package/src/collision/collision.js +75 -0
- package/src/game/index.js +2 -1
- package/src/game/pipeline.js +3 -3
- package/src/game/systems/FluidSystem.js +835 -0
- package/src/game/systems/index.js +11 -0
- package/src/game/ui/button.js +39 -18
- package/src/game/ui/cursor.js +14 -0
- package/src/game/ui/fps.js +12 -4
- package/src/game/ui/index.js +2 -0
- package/src/game/ui/stepper.js +549 -0
- package/src/game/ui/theme.js +121 -0
- package/src/game/ui/togglebutton.js +9 -3
- package/src/game/ui/tooltip.js +11 -4
- package/src/math/fluid.js +507 -0
- package/src/math/index.js +2 -0
- package/src/mixins/anchor.js +17 -7
- package/src/motion/tweenetik.js +16 -0
- package/src/shapes/index.js +1 -0
- package/src/util/camera3d.js +218 -12
- package/types/fluent.d.ts +361 -0
- package/types/game.d.ts +303 -0
- package/types/index.d.ts +144 -5
- package/types/math.d.ts +361 -0
- package/types/motion.d.ts +271 -0
- package/types/particle.d.ts +373 -0
- package/types/shapes.d.ts +107 -9
- package/types/util.d.ts +353 -0
- package/types/webgl.d.ts +109 -0
- package/disk_example.png +0 -0
- 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
|
-
|
|
45
|
-
this.
|
|
46
|
-
this.
|
|
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
|
|