action-engine-js 1.0.0
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/LICENSE +45 -0
- package/README.md +348 -0
- package/actionengine/3rdparty/goblin/goblin.js +9609 -0
- package/actionengine/3rdparty/goblin/goblin.min.js +5 -0
- package/actionengine/camera/actioncamera.js +90 -0
- package/actionengine/camera/cameracollisionhandler.js +69 -0
- package/actionengine/character/actioncharacter.js +360 -0
- package/actionengine/character/actioncharacter3D.js +61 -0
- package/actionengine/core/app.js +430 -0
- package/actionengine/debug/basedebugpanel.js +858 -0
- package/actionengine/display/canvasmanager.js +75 -0
- package/actionengine/display/gl/programmanager.js +570 -0
- package/actionengine/display/gl/shaders/lineshader.js +118 -0
- package/actionengine/display/gl/shaders/objectshader.js +1756 -0
- package/actionengine/display/gl/shaders/particleshader.js +43 -0
- package/actionengine/display/gl/shaders/shadowshader.js +319 -0
- package/actionengine/display/gl/shaders/spriteshader.js +100 -0
- package/actionengine/display/gl/shaders/watershader.js +67 -0
- package/actionengine/display/graphics/actionmodel3D.js +191 -0
- package/actionengine/display/graphics/actionsprite3D.js +230 -0
- package/actionengine/display/graphics/lighting/actiondirectionalshadowlight.js +864 -0
- package/actionengine/display/graphics/lighting/actionlight.js +211 -0
- package/actionengine/display/graphics/lighting/actionomnidirectionalshadowlight.js +862 -0
- package/actionengine/display/graphics/lighting/lightingconstants.js +263 -0
- package/actionengine/display/graphics/lighting/lightmanager.js +789 -0
- package/actionengine/display/graphics/renderableobject.js +44 -0
- package/actionengine/display/graphics/renderers/actionrenderer2D.js +341 -0
- package/actionengine/display/graphics/renderers/actionrenderer3D/actionrenderer3D.js +655 -0
- package/actionengine/display/graphics/renderers/actionrenderer3D/canvasmanager3D.js +82 -0
- package/actionengine/display/graphics/renderers/actionrenderer3D/debugrenderer3D.js +493 -0
- package/actionengine/display/graphics/renderers/actionrenderer3D/objectrenderer3D.js +790 -0
- package/actionengine/display/graphics/renderers/actionrenderer3D/spriteRenderer3D.js +266 -0
- package/actionengine/display/graphics/renderers/actionrenderer3D/sunrenderer3D.js +140 -0
- package/actionengine/display/graphics/renderers/actionrenderer3D/waterrenderer3D.js +173 -0
- package/actionengine/display/graphics/renderers/actionrenderer3D/weatherrenderer3D.js +87 -0
- package/actionengine/display/graphics/texture/proceduraltexture.js +192 -0
- package/actionengine/display/graphics/texture/texturemanager.js +242 -0
- package/actionengine/display/graphics/texture/textureregistry.js +177 -0
- package/actionengine/input/actionscrollablearea.js +1405 -0
- package/actionengine/input/inputhandler.js +1647 -0
- package/actionengine/math/geometry/geometrybuilder.js +161 -0
- package/actionengine/math/geometry/glbexporter.js +364 -0
- package/actionengine/math/geometry/glbloader.js +722 -0
- package/actionengine/math/geometry/modelcodegenerator.js +97 -0
- package/actionengine/math/geometry/triangle.js +33 -0
- package/actionengine/math/geometry/triangleutils.js +34 -0
- package/actionengine/math/mathutils.js +25 -0
- package/actionengine/math/matrix4.js +785 -0
- package/actionengine/math/physics/actionphysics.js +108 -0
- package/actionengine/math/physics/actionphysicsobject3D.js +164 -0
- package/actionengine/math/physics/actionphysicsworld3D.js +238 -0
- package/actionengine/math/physics/actionraycast.js +129 -0
- package/actionengine/math/physics/shapes/actionphysicsbox3D.js +158 -0
- package/actionengine/math/physics/shapes/actionphysicscapsule3D.js +200 -0
- package/actionengine/math/physics/shapes/actionphysicscompoundshape3D.js +147 -0
- package/actionengine/math/physics/shapes/actionphysicscone3D.js +126 -0
- package/actionengine/math/physics/shapes/actionphysicsconvexshape3D.js +72 -0
- package/actionengine/math/physics/shapes/actionphysicscylinder3D.js +117 -0
- package/actionengine/math/physics/shapes/actionphysicsmesh3D.js +74 -0
- package/actionengine/math/physics/shapes/actionphysicsplane3D.js +100 -0
- package/actionengine/math/physics/shapes/actionphysicssphere3D.js +95 -0
- package/actionengine/math/quaternion.js +61 -0
- package/actionengine/math/vector2.js +277 -0
- package/actionengine/math/vector3.js +318 -0
- package/actionengine/math/viewfrustum.js +136 -0
- package/actionengine/network/ACTIONNETREADME.md +810 -0
- package/actionengine/network/client/ActionNetManager.js +802 -0
- package/actionengine/network/client/ActionNetManagerGUI.js +1709 -0
- package/actionengine/network/client/ActionNetManagerP2P.js +1537 -0
- package/actionengine/network/client/SyncSystem.js +422 -0
- package/actionengine/network/p2p/ActionNetPeer.js +142 -0
- package/actionengine/network/p2p/ActionNetTrackerClient.js +623 -0
- package/actionengine/network/p2p/DataConnection.js +282 -0
- package/actionengine/network/p2p/README.md +510 -0
- package/actionengine/network/p2p/example.html +502 -0
- package/actionengine/network/server/ActionNetServer.js +577 -0
- package/actionengine/network/server/ActionNetServerSSL.js +579 -0
- package/actionengine/network/server/ActionNetServerUtils.js +458 -0
- package/actionengine/network/server/SERVERREADME.md +314 -0
- package/actionengine/network/server/package-lock.json +35 -0
- package/actionengine/network/server/package.json +13 -0
- package/actionengine/network/server/start.bat +27 -0
- package/actionengine/network/server/start.sh +25 -0
- package/actionengine/network/server/startwss.bat +27 -0
- package/actionengine/sound/audiomanager.js +1589 -0
- package/actionengine/sound/soundfont/ACTIONSOUNDFONT_README.md +205 -0
- package/actionengine/sound/soundfont/actionparser.js +718 -0
- package/actionengine/sound/soundfont/actionreverb.js +252 -0
- package/actionengine/sound/soundfont/actionsoundfont.js +543 -0
- package/actionengine/sound/soundfont/sf2playerlicence.txt +29 -0
- package/actionengine/sound/soundfont/soundfont.js +2 -0
- package/dist/action-engine.min.js +328 -0
- package/package.json +35 -0
|
@@ -0,0 +1,1405 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ActionScrollableArea - A comprehensive, reusable scrollable area component for ActionEngine
|
|
3
|
+
*
|
|
4
|
+
* This class provides a complete scrollbar implementation that handles all common scrolling interactions
|
|
5
|
+
* including drag scrolling, click-to-jump, mouse wheel scrolling, and keyboard navigation. It's designed
|
|
6
|
+
* to be easily integrated into any UI that needs to display a scrollable list of items.
|
|
7
|
+
*
|
|
8
|
+
* KEY FEATURES:
|
|
9
|
+
* - Self-Contained: Draws both scrollbar and content area background automatically
|
|
10
|
+
* - Background Management: Handles content area background, borders, and styling independently
|
|
11
|
+
* - Drag & Drop: Click and drag the scrollbar thumb with threshold-based drag detection
|
|
12
|
+
* - Click-to-Jump: Click anywhere on the scrollbar track to instantly jump to that position
|
|
13
|
+
* - Mouse Wheel: Seamless scroll wheel support with proper delta handling
|
|
14
|
+
* - Keyboard Scrolling: Arrow key navigation when hovering over the list area
|
|
15
|
+
* - Proportional Display: Scrollbar thumb size and position accurately represent content ratio
|
|
16
|
+
* - Visual Feedback: Hover states, drag indicators, and smooth interactions
|
|
17
|
+
* - Customizable Background: Configurable content area background with borders and rounded corners
|
|
18
|
+
* - Easy Configuration: Simple configuration object for different use cases
|
|
19
|
+
*
|
|
20
|
+
* USAGE EXAMPLE:
|
|
21
|
+
* ```javascript
|
|
22
|
+
* // Basic setup - component handles everything automatically
|
|
23
|
+
* this.inventoryScroller = new ActionScrollableArea({
|
|
24
|
+
* listAreaX: 400, listAreaY: 100, listAreaWidth: 450, listAreaHeight: 400,
|
|
25
|
+
* itemHeight: 60, padding: 10, scrollBarX: 860, scrollBarY: 90,
|
|
26
|
+
* scrollBarTrackHeight: 380, scrollBarThumbStartY: 120
|
|
27
|
+
* }, game.input, game.guiCtx);
|
|
28
|
+
*
|
|
29
|
+
* // In your update loop
|
|
30
|
+
* this.inventoryScroller.update(this.game.gameState.inventory.length, deltaTime);
|
|
31
|
+
*
|
|
32
|
+
* // Enhanced draw method with automatic clipping and item rendering
|
|
33
|
+
* this.inventoryScroller.draw(items, (item, index, y) => {
|
|
34
|
+
* this.drawInventoryItem(item, y);
|
|
35
|
+
* });
|
|
36
|
+
* ```
|
|
37
|
+
*
|
|
38
|
+
* CONFIGURATION OPTIONS:
|
|
39
|
+
* - listAreaX, listAreaY, listAreaWidth, listAreaHeight: Define the scrollable content area
|
|
40
|
+
* - itemHeight: Pixel height of each item in your list
|
|
41
|
+
* - padding: Pixel gap between items in the list (default: 8)
|
|
42
|
+
* - scrollBarX, scrollBarY: Position of the scrollbar relative to content
|
|
43
|
+
* - scrollBarTrackHeight: Total height available for scrollbar track
|
|
44
|
+
* - scrollBarThumbStartY: Where the scrollbar thumb should start (usually = scrollBarY + 10)
|
|
45
|
+
*
|
|
46
|
+
* ADDITIONAL CONFIGURATION OPTIONS:
|
|
47
|
+
* - enableClipping: Enable clipping support (default: false)
|
|
48
|
+
* - clipBounds: {x, y, width, height} for clipping visible area
|
|
49
|
+
* - colors: Override default scrollbar colors (see colors config structure below)
|
|
50
|
+
* - backgroundColor: Background fill color for content area (default: "rgba(40, 40, 40, 0.8)")
|
|
51
|
+
* - borderColor: Border color for content area (default: "rgba(255, 255, 255, 0.3)")
|
|
52
|
+
* - borderWidth: Border thickness for content area (default: 2)
|
|
53
|
+
* - cornerRadius: Corner radius for rounded background (default: 0)
|
|
54
|
+
* - drawBackground: Whether to draw background automatically (default: true)
|
|
55
|
+
* - onRegisterInput: Custom input registration callback
|
|
56
|
+
* - onRegisterItemInput: Custom item input registration callback
|
|
57
|
+
* - generateItemId: Function to generate item IDs (item, index) => string
|
|
58
|
+
*
|
|
59
|
+
* IMPORTANT NOTES:
|
|
60
|
+
* - The component automatically registers its scrollbar elements with the input system
|
|
61
|
+
* - Mouse wheel events are captured automatically (no additional setup needed)
|
|
62
|
+
* - The scrollable area handles its own cleanup when destroy() is called
|
|
63
|
+
* - All positions are automatically calculated proportionally for smooth scrolling
|
|
64
|
+
* - Visual feedback (hover states, drag indicators) is handled internally
|
|
65
|
+
*/
|
|
66
|
+
|
|
67
|
+
class ActionScrollableArea {
|
|
68
|
+
/**
|
|
69
|
+
* Creates a new scrollable area component with comprehensive scrolling functionality
|
|
70
|
+
*
|
|
71
|
+
* This constructor initializes all the necessary properties for scrollbar functionality,
|
|
72
|
+
* registers input handlers for user interactions, and sets up mouse wheel support.
|
|
73
|
+
* The component is immediately ready to use after construction.
|
|
74
|
+
*
|
|
75
|
+
* @param {Object} config - Configuration object defining the scrollable area's dimensions and behavior
|
|
76
|
+
* @param {number} config.listAreaX - X coordinate of the top-left corner of the scrollable content area
|
|
77
|
+
* @param {number} config.listAreaY - Y coordinate of the top-left corner of the scrollable content area
|
|
78
|
+
* @param {number} config.listAreaWidth - Width in pixels of the scrollable content area
|
|
79
|
+
* @param {number} config.listAreaHeight - Height in pixels of the scrollable content area
|
|
80
|
+
* @param {number} config.itemHeight - Height in pixels of each individual item in the list
|
|
81
|
+
* @param {number} [config.padding] - Pixel gap between items in the list (default: 8)
|
|
82
|
+
* @param {number} config.scrollBarX - X coordinate where the scrollbar should be positioned
|
|
83
|
+
* @param {number} config.scrollBarY - Y coordinate where the scrollbar track starts
|
|
84
|
+
* @param {number} config.scrollBarTrackHeight - Total height available for the scrollbar track
|
|
85
|
+
* @param {number} [config.scrollBarThumbStartY] - Y coordinate where scrollbar thumb should start (optional, calculated if not provided)
|
|
86
|
+
* @param {string} [config.backgroundColor] - Background fill color for the content area (default: "rgba(40, 40, 40, 0.8)")
|
|
87
|
+
* @param {string} [config.borderColor] - Border color for the content area (default: "rgba(255, 255, 255, 0.3)")
|
|
88
|
+
* @param {number} [config.borderWidth] - Border thickness for the content area (default: 2)
|
|
89
|
+
* @param {number} [config.cornerRadius] - Corner radius for rounded background corners (default: 0)
|
|
90
|
+
* @param {boolean} [config.drawBackground] - Whether to draw the background automatically (default: true)
|
|
91
|
+
*
|
|
92
|
+
* @param {Object} input - Reference to the ActionEngine input system for handling user interactions
|
|
93
|
+
* @param {CanvasRenderingContext2D} ctx - Canvas 2D rendering context for drawing the scrollbar components
|
|
94
|
+
*
|
|
95
|
+
* @example
|
|
96
|
+
* // Basic setup - component handles background automatically
|
|
97
|
+
* const inventoryScroller = new ActionScrollableArea({
|
|
98
|
+
* listAreaX: 50, listAreaY: 100, listAreaWidth: 300, listAreaHeight: 400,
|
|
99
|
+
* itemHeight: 60, scrollBarX: 360, scrollBarY: 100, scrollBarTrackHeight: 400,
|
|
100
|
+
*
|
|
101
|
+
* // Optional: customize background (defaults are sensible)
|
|
102
|
+
* backgroundColor: "rgba(40, 40, 40, 0.8)",
|
|
103
|
+
* borderColor: "rgba(255, 255, 255, 0.3)"
|
|
104
|
+
* }, game.input, game.guiCtx);
|
|
105
|
+
*
|
|
106
|
+
* // Advanced setup with custom styling and background
|
|
107
|
+
* const customScroller = new ActionScrollableArea({
|
|
108
|
+
* listAreaX: 400, listAreaY: 100, listAreaWidth: 450, listAreaHeight: 400,
|
|
109
|
+
* itemHeight: 120, scrollBarX: 860, scrollBarY: 90,
|
|
110
|
+
* scrollBarTrackHeight: 380, scrollBarThumbStartY: 120,
|
|
111
|
+
*
|
|
112
|
+
* // Enable clipping for precise input bounds
|
|
113
|
+
* enableClipping: true,
|
|
114
|
+
* clipBounds: { x: 400, y: 100, width: 450, height: 400 },
|
|
115
|
+
*
|
|
116
|
+
* // Custom scrollbar colors for theme consistency
|
|
117
|
+
* colors: {
|
|
118
|
+
* track: { normal: "rgba(255, 255, 255, 0.1)", hover: "rgba(255, 255, 255, 0.2)" },
|
|
119
|
+
* thumb: { normal: "rgba(255, 100, 100, 0.3)", hover: "rgba(255, 100, 100, 0.6)", drag: "rgba(255, 100, 100, 0.8)" },
|
|
120
|
+
* button: { normal: "rgba(255, 100, 100, 0.1)", hover: "rgba(255, 100, 100, 0.3)" },
|
|
121
|
+
* buttonText: { normal: "rgba(255, 100, 100, 0.8)", hover: "#ff6464" },
|
|
122
|
+
* thumbBorder: { normal: "rgba(255, 100, 100, 0.5)", drag: "#ff6464" }
|
|
123
|
+
* },
|
|
124
|
+
*
|
|
125
|
+
* // Self-contained background styling - no manual drawing needed!
|
|
126
|
+
* drawBackground: true, // Enable automatic background
|
|
127
|
+
* backgroundColor: "rgba(20, 20, 30, 0.9)", // Dark blue background
|
|
128
|
+
* borderColor: "rgba(255, 100, 100, 0.6)", // Red border with 60% opacity
|
|
129
|
+
* borderWidth: 3, // Thicker border
|
|
130
|
+
* cornerRadius: 8 // Rounded corners
|
|
131
|
+
* }, game.input, game.guiCtx);
|
|
132
|
+
*
|
|
133
|
+
* // Chat log scroller with different ID format
|
|
134
|
+
* const chatScroller = new ActionScrollableArea({
|
|
135
|
+
* listAreaX: 50, listAreaY: 100, listAreaWidth: 300, listAreaHeight: 400,
|
|
136
|
+
* itemHeight: 40, scrollBarX: 360, scrollBarY: 100, scrollBarTrackHeight: 400,
|
|
137
|
+
*
|
|
138
|
+
* // Generate chat-specific IDs
|
|
139
|
+
* generateItemId: (message, index) => `chat_msg_${message.timestamp}_${index}`,
|
|
140
|
+
*
|
|
141
|
+
* // Enable clipping for chat area
|
|
142
|
+
* enableClipping: true,
|
|
143
|
+
* clipBounds: { x: 50, y: 100, width: 300, height: 400 }
|
|
144
|
+
* }, game.input, game.guiCtx);
|
|
145
|
+
*/
|
|
146
|
+
constructor(config, input, ctx) {
|
|
147
|
+
this.input = input;
|
|
148
|
+
this.ctx = ctx;
|
|
149
|
+
|
|
150
|
+
// Scroll state
|
|
151
|
+
this.scrollOffset = 0;
|
|
152
|
+
this.maxScrollOffset = 0;
|
|
153
|
+
this.isDragging = false;
|
|
154
|
+
this.dragStartY = 0;
|
|
155
|
+
this.lastScrollOffset = 0;
|
|
156
|
+
this.dragThreshold = 5;
|
|
157
|
+
this.hasMovedBeyondThreshold = false;
|
|
158
|
+
|
|
159
|
+
// Track scroll and content changes to prevent unnecessary input re-registration
|
|
160
|
+
this.lastScrollOffsetForInput = -1; // Initialize to -1 so first update always triggers
|
|
161
|
+
this.lastItemCountForInput = -1; // Track item count changes for new insertions/deletions
|
|
162
|
+
this.registeredItems = new Set(); // Track which items are currently registered
|
|
163
|
+
|
|
164
|
+
// List area dimensions
|
|
165
|
+
this.listArea = {
|
|
166
|
+
x: config.listAreaX || 400,
|
|
167
|
+
y: config.listAreaY || 100,
|
|
168
|
+
width: config.listAreaWidth || 450,
|
|
169
|
+
height: config.listAreaHeight || 400,
|
|
170
|
+
itemHeight: config.itemHeight || 60,
|
|
171
|
+
padding: config.padding !== undefined ? config.padding : 8,
|
|
172
|
+
scrollBarWidth: 20
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
// Scrollbar positioning
|
|
176
|
+
this.scrollArea = {
|
|
177
|
+
x: config.scrollBarX || 860,
|
|
178
|
+
y: config.scrollBarY || 90,
|
|
179
|
+
trackHeight: config.scrollBarTrackHeight || 380,
|
|
180
|
+
thumbStartY: config.scrollBarThumbStartY || 120
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
// Configurable colors with sensible defaults
|
|
184
|
+
// COLOR CONFIGURATION STRUCTURE:
|
|
185
|
+
// - track: Scrollbar track background {normal, hover}
|
|
186
|
+
// - thumb: Scrollbar thumb {normal, hover, drag}
|
|
187
|
+
// - button: Up/Down button backgrounds {normal, hover}
|
|
188
|
+
// - buttonText: Button text colors {normal, hover}
|
|
189
|
+
// - thumbBorder: Thumb border colors {normal, drag}
|
|
190
|
+
this.colors = {
|
|
191
|
+
track: {
|
|
192
|
+
normal: "rgba(0, 0, 0, 0.2)",
|
|
193
|
+
hover: "rgba(0, 0, 0, 0.3)"
|
|
194
|
+
},
|
|
195
|
+
thumb: {
|
|
196
|
+
normal: "rgba(52, 152, 219, 0.3)",
|
|
197
|
+
hover: "rgba(52, 152, 219, 0.6)",
|
|
198
|
+
drag: "rgba(52, 152, 219, 0.8)"
|
|
199
|
+
},
|
|
200
|
+
button: {
|
|
201
|
+
normal: "rgba(52, 152, 219, 0.1)",
|
|
202
|
+
hover: "rgba(52, 152, 219, 0.3)"
|
|
203
|
+
},
|
|
204
|
+
buttonText: {
|
|
205
|
+
normal: "rgba(52, 152, 219, 0.8)",
|
|
206
|
+
hover: "#3498DB"
|
|
207
|
+
},
|
|
208
|
+
thumbBorder: {
|
|
209
|
+
normal: "rgba(52, 152, 219, 0.5)",
|
|
210
|
+
drag: "#3498DB"
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
// Override with custom colors if provided
|
|
215
|
+
if (config.colors) {
|
|
216
|
+
Object.assign(this.colors, config.colors);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Clipping support (fundamental feature)
|
|
220
|
+
this.clipBounds = config.clipBounds || null; // {x, y, width, height} for clipping
|
|
221
|
+
this.enableClipping = config.enableClipping || false;
|
|
222
|
+
|
|
223
|
+
// Custom input registration callback
|
|
224
|
+
this.onRegisterInput =
|
|
225
|
+
config.onRegisterInput ||
|
|
226
|
+
((id, bounds, layer = "gui") => {
|
|
227
|
+
if (bounds && bounds.width > 0 && bounds.height > 0) {
|
|
228
|
+
this.input.registerElement(id, { bounds: () => bounds }, layer);
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// Generate item ID for input registration (configurable)
|
|
233
|
+
this.generateItemId = config.generateItemId || ((item, index) => `item_${index}`);
|
|
234
|
+
|
|
235
|
+
// Custom item input registration callback (stable registration)
|
|
236
|
+
this.onRegisterItemInput =
|
|
237
|
+
config.onRegisterItemInput ||
|
|
238
|
+
((itemId, index, bounds) => {
|
|
239
|
+
if (bounds && bounds.width > 0 && bounds.height > 0) {
|
|
240
|
+
// Always register visible items - the registration system handles duplicates
|
|
241
|
+
this.onRegisterInput(itemId, bounds);
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// Background configuration
|
|
246
|
+
this.drawBackground = config.drawBackground !== false; // Default to true
|
|
247
|
+
this.backgroundConfig = {
|
|
248
|
+
fillColor: config.backgroundColor || "rgba(40, 40, 40, 0.8)",
|
|
249
|
+
borderColor: config.borderColor || "rgba(255, 255, 255, 0.3)",
|
|
250
|
+
borderWidth: config.borderWidth || 2,
|
|
251
|
+
cornerRadius: config.cornerRadius || 0 // For rounded corners
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const scrollButtonWidth = 20;
|
|
255
|
+
const scrollButtonHeight = 20;
|
|
256
|
+
|
|
257
|
+
// Scroll buttons (using configurable colors)
|
|
258
|
+
this.scrollUpButton = {
|
|
259
|
+
width: scrollButtonWidth,
|
|
260
|
+
height: scrollButtonHeight,
|
|
261
|
+
x: this.scrollArea.x,
|
|
262
|
+
y: this.scrollArea.y - scrollButtonHeight,
|
|
263
|
+
text: "▲",
|
|
264
|
+
color: this.colors.button.normal,
|
|
265
|
+
hovered: false
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
this.scrollDownButton = {
|
|
269
|
+
x: this.scrollArea.x,
|
|
270
|
+
y: this.scrollArea.y + this.scrollArea.trackHeight,
|
|
271
|
+
width: scrollButtonWidth,
|
|
272
|
+
height: scrollButtonHeight,
|
|
273
|
+
text: "▼",
|
|
274
|
+
color: this.colors.button.normal,
|
|
275
|
+
hovered: false
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
this.scrollThumb = {
|
|
279
|
+
x: this.scrollArea.x,
|
|
280
|
+
y: this.scrollArea.thumbStartY,
|
|
281
|
+
width: 20,
|
|
282
|
+
height: 60,
|
|
283
|
+
color: this.colors.thumb.normal,
|
|
284
|
+
hovered: false
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
// Element IDs for input registration
|
|
288
|
+
this.elementIds = {
|
|
289
|
+
scrollUp: this.generateElementId(),
|
|
290
|
+
scrollDown: this.generateElementId(),
|
|
291
|
+
scrollbarTrack: this.generateElementId()
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
this.setupInput();
|
|
295
|
+
this.setupMouseWheel();
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Generates a unique element ID for input system registration
|
|
300
|
+
*
|
|
301
|
+
* Creates a random string identifier to avoid conflicts when multiple scrollable
|
|
302
|
+
* areas are used in the same application. Uses base-36 encoding for compactness.
|
|
303
|
+
*
|
|
304
|
+
* @returns {string} A unique identifier string for input element registration
|
|
305
|
+
*
|
|
306
|
+
* @private
|
|
307
|
+
* @example
|
|
308
|
+
* // Returns something like: "scrollable_a4f2k1"
|
|
309
|
+
* const elementId = this.generateElementId();
|
|
310
|
+
*/
|
|
311
|
+
generateElementId() {
|
|
312
|
+
return `scrollable_${Math.random().toString(36).substr(2, 9)}`;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Sets up input system registration for scrollbar interactive elements
|
|
317
|
+
*
|
|
318
|
+
* Registers three interactive elements with the input system:
|
|
319
|
+
* - scrollbarTrack: The background area of the scrollbar (for click-to-jump)
|
|
320
|
+
* - scrollUp: The up arrow button for incremental scrolling
|
|
321
|
+
* - scrollDown: The down arrow button for incremental scrolling
|
|
322
|
+
*
|
|
323
|
+
* Each element is registered with dynamic bounds calculation that updates
|
|
324
|
+
* in real-time to match the current scrollbar position and dimensions.
|
|
325
|
+
*
|
|
326
|
+
* @private
|
|
327
|
+
* @example
|
|
328
|
+
* // Automatically called during construction
|
|
329
|
+
* this.setupInput(); // Registers "scrollUp", "scrollDown", and "scrollbarTrack"
|
|
330
|
+
*/
|
|
331
|
+
setupInput() {
|
|
332
|
+
// Register scrollbar track for click-to-jump and dragging
|
|
333
|
+
this.input.registerElement(
|
|
334
|
+
this.elementIds.scrollbarTrack,
|
|
335
|
+
{
|
|
336
|
+
bounds: () => ({
|
|
337
|
+
x: this.scrollArea.x,
|
|
338
|
+
y: this.scrollArea.thumbStartY,
|
|
339
|
+
width: 20,
|
|
340
|
+
height: this.scrollArea.trackHeight
|
|
341
|
+
})
|
|
342
|
+
},
|
|
343
|
+
"gui"
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
// Register scroll up button
|
|
347
|
+
this.input.registerElement(
|
|
348
|
+
this.elementIds.scrollUp,
|
|
349
|
+
{
|
|
350
|
+
bounds: () => ({
|
|
351
|
+
x: this.scrollUpButton.x,
|
|
352
|
+
y: this.scrollUpButton.y,
|
|
353
|
+
width: this.scrollUpButton.width,
|
|
354
|
+
height: this.scrollUpButton.height
|
|
355
|
+
})
|
|
356
|
+
},
|
|
357
|
+
"gui"
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
// Register scroll down button
|
|
361
|
+
this.input.registerElement(
|
|
362
|
+
this.elementIds.scrollDown,
|
|
363
|
+
{
|
|
364
|
+
bounds: () => ({
|
|
365
|
+
x: this.scrollDownButton.x,
|
|
366
|
+
y: this.scrollDownButton.y,
|
|
367
|
+
width: this.scrollDownButton.width,
|
|
368
|
+
height: this.scrollDownButton.height
|
|
369
|
+
})
|
|
370
|
+
},
|
|
371
|
+
"gui"
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Sets up automatic mouse wheel event handling for seamless scrolling
|
|
377
|
+
*
|
|
378
|
+
* This method adds wheel event listeners to both the game canvas and window
|
|
379
|
+
* as a fallback. The event listeners are added with a small delay to ensure
|
|
380
|
+
* the canvas element exists in the DOM before attempting to attach listeners.
|
|
381
|
+
*
|
|
382
|
+
* Features:
|
|
383
|
+
* - Prevents default browser scroll behavior when over canvas
|
|
384
|
+
* - Handles both positive and negative delta values
|
|
385
|
+
* - Provides fallback to window if canvas is not available
|
|
386
|
+
* - Automatically removes events when component is destroyed
|
|
387
|
+
*
|
|
388
|
+
* @private
|
|
389
|
+
* @example
|
|
390
|
+
* // Automatically called during construction with 100ms delay
|
|
391
|
+
* setTimeout(() => {
|
|
392
|
+
* canvas.addEventListener("wheel", (e) => {
|
|
393
|
+
* this.handleMouseWheel(e.deltaY);
|
|
394
|
+
* });
|
|
395
|
+
* }, 100);
|
|
396
|
+
*/
|
|
397
|
+
setupMouseWheel() {
|
|
398
|
+
setTimeout(() => {
|
|
399
|
+
const canvas = document.querySelector("#gameCanvas");
|
|
400
|
+
if (canvas) {
|
|
401
|
+
// Explicitly mark as non-passive since we call preventDefault()
|
|
402
|
+
canvas.addEventListener(
|
|
403
|
+
"wheel",
|
|
404
|
+
(e) => {
|
|
405
|
+
e.preventDefault();
|
|
406
|
+
this.handleMouseWheel(e.deltaY);
|
|
407
|
+
},
|
|
408
|
+
{ passive: false }
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
// Fallback to window: does not call preventDefault, so can be passive
|
|
412
|
+
window.addEventListener(
|
|
413
|
+
"wheel",
|
|
414
|
+
(e) => {
|
|
415
|
+
this.handleMouseWheel(e.deltaY);
|
|
416
|
+
},
|
|
417
|
+
{ passive: true }
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
}, 100);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Updates the scrollable area state and handles all scrolling interactions
|
|
425
|
+
*
|
|
426
|
+
* This is the main update method that should be called every frame in your game loop.
|
|
427
|
+
* It performs several critical functions:
|
|
428
|
+
* - Updates maximum scroll range based on item count
|
|
429
|
+
* - Recalculates scrollbar thumb position and size
|
|
430
|
+
* - Processes user input (clicks, drags, keyboard)
|
|
431
|
+
* - Handles boundary constraints
|
|
432
|
+
*
|
|
433
|
+
* Call this method before drawing to ensure scrollbar state is current.
|
|
434
|
+
*
|
|
435
|
+
* @param {number} totalItemCount - Total number of items in the scrollable list
|
|
436
|
+
* @param {number} deltaTime - Time elapsed since last frame in seconds (for smooth keyboard scrolling)
|
|
437
|
+
*
|
|
438
|
+
* @example
|
|
439
|
+
* // In your game update loop
|
|
440
|
+
* update(deltaTime) {
|
|
441
|
+
* // Update scrollable area before processing input
|
|
442
|
+
* this.inventoryScroller.update(this.gameState.inventory.length, deltaTime);
|
|
443
|
+
* }
|
|
444
|
+
*/
|
|
445
|
+
update(totalItemCount, deltaTime) {
|
|
446
|
+
this.updateMaxScroll(totalItemCount);
|
|
447
|
+
this.updateScrollbarThumb();
|
|
448
|
+
this.handleInput(deltaTime);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Updates the maximum scroll offset based on total item count and visible area
|
|
453
|
+
*
|
|
454
|
+
* Calculates how far the user can scroll by comparing the total content height
|
|
455
|
+
* (itemCount × itemHeight) with the visible area height. The difference represents
|
|
456
|
+
* the maximum scroll distance needed to see all content.
|
|
457
|
+
*
|
|
458
|
+
* Formula: maxScroll = (totalItems × itemHeight) - visibleAreaHeight
|
|
459
|
+
* Example: 20 items × 60px = 1200px total height, 400px visible area = 800px max scroll
|
|
460
|
+
*
|
|
461
|
+
* @param {number} totalItemCount - Total number of items in the list
|
|
462
|
+
*
|
|
463
|
+
* @private
|
|
464
|
+
* @example
|
|
465
|
+
* // 100 items × 50px each = 5000px total height
|
|
466
|
+
* // 300px visible area = 4700px maximum scroll
|
|
467
|
+
* this.updateMaxScroll(100);
|
|
468
|
+
* console.log(this.maxScrollOffset); // 4700
|
|
469
|
+
*/
|
|
470
|
+
updateMaxScroll(totalItemCount) {
|
|
471
|
+
const totalContentHeight = totalItemCount * (this.listArea.itemHeight + this.listArea.padding);
|
|
472
|
+
const visibleHeight = this.listArea.height;
|
|
473
|
+
this.maxScrollOffset = Math.max(0, totalContentHeight - visibleHeight);
|
|
474
|
+
this.scrollOffset = Math.max(0, Math.min(this.maxScrollOffset, this.scrollOffset));
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Updates the scrollbar thumb position and size based on current scroll state
|
|
479
|
+
*
|
|
480
|
+
* This method implements proportional scrollbar behavior where:
|
|
481
|
+
* - Thumb height represents the ratio of visible content to total content
|
|
482
|
+
* - Thumb position represents the current scroll progress through the content
|
|
483
|
+
* - Thumb moves smoothly as user scrolls through the content
|
|
484
|
+
*
|
|
485
|
+
* The scrollbar provides visual feedback about:
|
|
486
|
+
* 1. How much content exists (thumb height = visible/total ratio)
|
|
487
|
+
* 2. Where you are in the content (thumb position = scroll progress)
|
|
488
|
+
* 3. Content boundaries (thumb stops at track limits)
|
|
489
|
+
*
|
|
490
|
+
* @private
|
|
491
|
+
* @example
|
|
492
|
+
* // Content: 1000px total, 400px visible = 40% thumb height
|
|
493
|
+
* // Scrolled 250px = 25% progress = thumb at 25% down the track
|
|
494
|
+
* this.scrollOffset = 250;
|
|
495
|
+
* this.updateScrollbarThumb();
|
|
496
|
+
* console.log(this.scrollThumb.height); // ~40% of track height
|
|
497
|
+
* console.log(this.scrollThumb.y); // 25% down from track start
|
|
498
|
+
*/
|
|
499
|
+
updateScrollbarThumb() {
|
|
500
|
+
const trackHeight = this.scrollArea.trackHeight;
|
|
501
|
+
|
|
502
|
+
// Calculate total content height (all items that exist)
|
|
503
|
+
const totalItemCount =
|
|
504
|
+
this.maxScrollOffset / (this.listArea.itemHeight + this.listArea.padding) +
|
|
505
|
+
this.listArea.height / (this.listArea.itemHeight + this.listArea.padding);
|
|
506
|
+
const totalContentHeight = totalItemCount * (this.listArea.itemHeight + this.listArea.padding);
|
|
507
|
+
|
|
508
|
+
// Thumb height represents the ratio of visible content to total content
|
|
509
|
+
const thumbHeightRatio = this.listArea.height / totalContentHeight;
|
|
510
|
+
this.scrollThumb.height = Math.max(30, trackHeight * thumbHeightRatio);
|
|
511
|
+
|
|
512
|
+
// Calculate thumb position based on current scroll progress
|
|
513
|
+
const currentScrollProgress = this.maxScrollOffset > 0 ? this.scrollOffset / this.maxScrollOffset : 0;
|
|
514
|
+
const availableTrackSpace = trackHeight - this.scrollThumb.height;
|
|
515
|
+
this.scrollThumb.y = this.scrollArea.thumbStartY + availableTrackSpace * currentScrollProgress;
|
|
516
|
+
|
|
517
|
+
// Ensure thumb stays within track bounds
|
|
518
|
+
const maxThumbY = this.scrollArea.thumbStartY + trackHeight - this.scrollThumb.height;
|
|
519
|
+
this.scrollThumb.y = Math.max(this.scrollArea.thumbStartY, Math.min(maxThumbY, this.scrollThumb.y));
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Handles all user input events for the scrollable area
|
|
524
|
+
*
|
|
525
|
+
* This is the central input handling method that processes:
|
|
526
|
+
* - Scroll up/down button clicks (incremental scrolling)
|
|
527
|
+
* - Scrollbar track clicks (click-to-jump functionality)
|
|
528
|
+
* - Thumb dragging (smooth continuous scrolling)
|
|
529
|
+
* - Keyboard navigation when hovering over content area
|
|
530
|
+
*
|
|
531
|
+
* The method updates hover states for visual feedback and coordinates
|
|
532
|
+
* all scrolling interactions into smooth, responsive movement.
|
|
533
|
+
*
|
|
534
|
+
* @param {number} deltaTime - Time elapsed since last frame for smooth keyboard scrolling
|
|
535
|
+
*
|
|
536
|
+
* @private
|
|
537
|
+
* @example
|
|
538
|
+
* // Called automatically by update() - no manual invocation needed
|
|
539
|
+
* handleInput(deltaTime) {
|
|
540
|
+
* // Handles button clicks, dragging, and keyboard input
|
|
541
|
+
* // Updates this.scrollOffset based on user interactions
|
|
542
|
+
* }
|
|
543
|
+
*/
|
|
544
|
+
handleInput(deltaTime) {
|
|
545
|
+
const pointer = this.input.getPointerPosition();
|
|
546
|
+
|
|
547
|
+
// Handle scroll up/down buttons
|
|
548
|
+
if (this.input.isElementJustPressed(this.elementIds.scrollUp, "gui")) {
|
|
549
|
+
this.scrollUp();
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
if (this.input.isElementJustPressed(this.elementIds.scrollDown, "gui")) {
|
|
553
|
+
this.scrollDown();
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Update hover states
|
|
557
|
+
this.scrollUpButton.hovered = this.input.isElementHovered(this.elementIds.scrollUp, "gui");
|
|
558
|
+
this.scrollDownButton.hovered = this.input.isElementHovered(this.elementIds.scrollDown, "gui");
|
|
559
|
+
|
|
560
|
+
// Handle dragging
|
|
561
|
+
this.handleDragging(pointer);
|
|
562
|
+
|
|
563
|
+
// Handle track clicks for click-to-jump functionality
|
|
564
|
+
if (this.input.isElementJustPressed(this.elementIds.scrollbarTrack, "gui")) {
|
|
565
|
+
const trackY = pointer.y - this.scrollArea.thumbStartY;
|
|
566
|
+
const scrollableHeight = this.scrollArea.trackHeight;
|
|
567
|
+
const jumpPercent = Math.max(0, Math.min(1, trackY / scrollableHeight));
|
|
568
|
+
|
|
569
|
+
this.scrollOffset = jumpPercent * this.maxScrollOffset;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Handle keyboard scrolling when hovering over list area
|
|
573
|
+
this.handleKeyboardScrolling(deltaTime);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Handles mouse wheel scroll events for smooth scrolling
|
|
578
|
+
*
|
|
579
|
+
* Processes wheel delta values to determine scroll direction and amount.
|
|
580
|
+
* Positive delta values scroll down (show content below), negative values
|
|
581
|
+
* scroll up (show content above). The scroll amount is fixed at 60 pixels
|
|
582
|
+
* per wheel "notch" for consistent behavior across different mice.
|
|
583
|
+
*
|
|
584
|
+
* @param {number} deltaY - Wheel delta value (positive = scroll down, negative = scroll up)
|
|
585
|
+
*
|
|
586
|
+
* @private
|
|
587
|
+
* @example
|
|
588
|
+
* // Mouse wheel scrolled down (deltaY = 120)
|
|
589
|
+
* this.handleMouseWheel(120);
|
|
590
|
+
* this.scrollOffset += 60; // Scroll down by one "notch"
|
|
591
|
+
*
|
|
592
|
+
* // Mouse wheel scrolled up (deltaY = -120)
|
|
593
|
+
* this.handleMouseWheel(-120);
|
|
594
|
+
* this.scrollOffset -= 60; // Scroll up by one "notch"
|
|
595
|
+
*/
|
|
596
|
+
handleMouseWheel(deltaY) {
|
|
597
|
+
const scrollAmount = deltaY > 0 ? 20 : -20; // ← Changed from 60 to 20
|
|
598
|
+
this.scrollOffset = Math.max(0, Math.min(this.maxScrollOffset, this.scrollOffset + scrollAmount));
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Scrolls content up by exactly one item height
|
|
603
|
+
*
|
|
604
|
+
* Provides incremental scrolling up, moving the view to show content
|
|
605
|
+
* that was previously above the visible area. The scroll amount equals
|
|
606
|
+
* the itemHeight specified in configuration.
|
|
607
|
+
*
|
|
608
|
+
* @example
|
|
609
|
+
* // If itemHeight = 60, scrolls up by exactly 60 pixels
|
|
610
|
+
* this.scrollUp();
|
|
611
|
+
* // Content moves up, showing items that were above the visible area
|
|
612
|
+
*/
|
|
613
|
+
scrollUp() {
|
|
614
|
+
const scrollAmount = 50; // ← Changed from itemHeight (120) to 50
|
|
615
|
+
this.scrollOffset = Math.max(0, this.scrollOffset - scrollAmount);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Scrolls content down by exactly one item height
|
|
620
|
+
*
|
|
621
|
+
* Provides incremental scrolling down, moving the view to show content
|
|
622
|
+
* that was previously below the visible area. The scroll amount equals
|
|
623
|
+
* the itemHeight specified in configuration.
|
|
624
|
+
*
|
|
625
|
+
* @example
|
|
626
|
+
* // If itemHeight = 60, scrolls down by exactly 60 pixels
|
|
627
|
+
* this.scrollDown();
|
|
628
|
+
* // Content moves down, showing items that were below the visible area
|
|
629
|
+
*/
|
|
630
|
+
scrollDown() {
|
|
631
|
+
const scrollAmount = 50; // ← Changed from itemHeight (120) to 50
|
|
632
|
+
this.scrollOffset = Math.min(this.maxScrollOffset, this.scrollOffset + scrollAmount);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Handles scrollbar thumb dragging with threshold-based drag detection
|
|
637
|
+
*
|
|
638
|
+
* This method implements sophisticated drag behavior:
|
|
639
|
+
* 1. Detects when user clicks directly on the scrollbar thumb
|
|
640
|
+
* 2. Waits for movement beyond threshold before considering it a drag
|
|
641
|
+
* 3. Calculates scroll position based on thumb position in real-time
|
|
642
|
+
* 4. Provides smooth, continuous scrolling during drag operation
|
|
643
|
+
*
|
|
644
|
+
* The drag threshold prevents accidental drags from single clicks and
|
|
645
|
+
* ensures intentional scrolling behavior.
|
|
646
|
+
*
|
|
647
|
+
* @param {Object} pointer - Current pointer position from input system
|
|
648
|
+
* @param {number} pointer.x - X coordinate of mouse/touch
|
|
649
|
+
* @param {number} pointer.y - Y coordinate of mouse/touch
|
|
650
|
+
*
|
|
651
|
+
* @private
|
|
652
|
+
* @example
|
|
653
|
+
* // User clicks on thumb at position 200, drags to 250
|
|
654
|
+
* handleDragging({x: 100, y: 250});
|
|
655
|
+
* // Calculates: deltaY = 50, converts to scroll percentage
|
|
656
|
+
* // Updates this.scrollOffset proportionally
|
|
657
|
+
*/
|
|
658
|
+
handleDragging(pointer) {
|
|
659
|
+
// Check if clicking directly on thumb
|
|
660
|
+
const isOverThumb =
|
|
661
|
+
pointer.x >= this.scrollThumb.x &&
|
|
662
|
+
pointer.x <= this.scrollThumb.x + this.scrollThumb.width &&
|
|
663
|
+
pointer.y >= this.scrollThumb.y &&
|
|
664
|
+
pointer.y <= this.scrollThumb.y + this.scrollThumb.height;
|
|
665
|
+
|
|
666
|
+
if (this.input.isPointerJustDown() && isOverThumb) {
|
|
667
|
+
this.isDragging = true;
|
|
668
|
+
this.dragStartY = pointer.y;
|
|
669
|
+
this.lastScrollOffset = this.scrollOffset;
|
|
670
|
+
this.hasMovedBeyondThreshold = false;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// Handle dragging
|
|
674
|
+
if (this.isDragging && this.input.isPointerDown()) {
|
|
675
|
+
const deltaY = pointer.y - this.dragStartY;
|
|
676
|
+
|
|
677
|
+
if (Math.abs(deltaY) > this.dragThreshold) {
|
|
678
|
+
this.hasMovedBeyondThreshold = true;
|
|
679
|
+
|
|
680
|
+
// Calculate scroll based on mouse position relative to track
|
|
681
|
+
const trackY = pointer.y - this.scrollArea.thumbStartY;
|
|
682
|
+
const availableTrackHeight = this.scrollArea.trackHeight - this.scrollThumb.height;
|
|
683
|
+
const scrollPercent = Math.max(0, Math.min(1, trackY / Math.max(1, availableTrackHeight)));
|
|
684
|
+
|
|
685
|
+
this.scrollOffset = scrollPercent * this.maxScrollOffset;
|
|
686
|
+
this.scrollOffset = Math.max(0, Math.min(this.maxScrollOffset, this.scrollOffset));
|
|
687
|
+
}
|
|
688
|
+
} else {
|
|
689
|
+
this.isDragging = false;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* Handles keyboard scrolling when pointer is hovering over the list area
|
|
695
|
+
*
|
|
696
|
+
* Provides keyboard navigation for users who prefer keyboard input or
|
|
697
|
+
* accessibility. Only activates when the pointer is within the list area
|
|
698
|
+
* bounds to avoid conflicts with other keyboard handlers.
|
|
699
|
+
*
|
|
700
|
+
* Features:
|
|
701
|
+
* - DirUp key scrolls up smoothly based on deltaTime
|
|
702
|
+
* - DirDown key scrolls down smoothly based on deltaTime
|
|
703
|
+
* - Respects scroll boundaries (won't scroll past limits)
|
|
704
|
+
* - Smooth scrolling speed (300 pixels per second)
|
|
705
|
+
*
|
|
706
|
+
* @param {number} deltaTime - Time elapsed since last frame for consistent scroll speed
|
|
707
|
+
*
|
|
708
|
+
* @private
|
|
709
|
+
* @example
|
|
710
|
+
* // Pointer at (450, 200) - within list area (400-850, 100-500)
|
|
711
|
+
* // User presses DirUp key
|
|
712
|
+
* handleKeyboardScrolling(0.016); // ~60fps
|
|
713
|
+
* this.scrollOffset -= 300 * 0.016; // Scroll up ~4.8 pixels
|
|
714
|
+
*/
|
|
715
|
+
handleKeyboardScrolling(deltaTime) {
|
|
716
|
+
const pointer = this.input.getPointerPosition();
|
|
717
|
+
const isOverListArea =
|
|
718
|
+
pointer.x >= this.listArea.x &&
|
|
719
|
+
pointer.x <= this.listArea.x + this.listArea.width - this.listArea.scrollBarWidth - 10 &&
|
|
720
|
+
pointer.y >= this.listArea.y &&
|
|
721
|
+
pointer.y <= this.listArea.y + this.listArea.height;
|
|
722
|
+
|
|
723
|
+
if (isOverListArea) {
|
|
724
|
+
if (this.input.isKeyPressed("DirUp")) {
|
|
725
|
+
this.scrollOffset = Math.max(0, this.scrollOffset - 300 * deltaTime);
|
|
726
|
+
}
|
|
727
|
+
if (this.input.isKeyPressed("DirDown")) {
|
|
728
|
+
this.scrollOffset = Math.min(this.maxScrollOffset, this.scrollOffset + 300 * deltaTime);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* Handles click-to-jump functionality on the scrollbar track
|
|
735
|
+
*
|
|
736
|
+
* When user clicks on the scrollbar track (not on the thumb), the content
|
|
737
|
+
* immediately jumps to show the portion of content at that relative position.
|
|
738
|
+
* This provides quick navigation to different parts of long content.
|
|
739
|
+
*
|
|
740
|
+
* Algorithm:
|
|
741
|
+
* 1. Calculate click position relative to track start
|
|
742
|
+
* 2. Convert to percentage of total track height
|
|
743
|
+
* 3. Apply percentage to total scrollable content height
|
|
744
|
+
* 4. Jump directly to that scroll position
|
|
745
|
+
*
|
|
746
|
+
* @param {Object} pointer - Current pointer position from input system
|
|
747
|
+
* @param {number} pointer.y - Y coordinate of the click
|
|
748
|
+
*
|
|
749
|
+
* @example
|
|
750
|
+
* // User clicks 50% down the scrollbar track
|
|
751
|
+
* handleTrackClick({x: 100, y: 200});
|
|
752
|
+
* // If trackY = 100px from start, scrollableHeight = 300px
|
|
753
|
+
* // jumpPercent = 100/300 = 0.33
|
|
754
|
+
* // If maxScroll = 600px, new scrollOffset = 0.33 * 600 = 200px
|
|
755
|
+
*/
|
|
756
|
+
handleTrackClick(pointer) {
|
|
757
|
+
if (this.input.isElementJustPressed(this.elementIds.scrollbarTrack, "gui")) {
|
|
758
|
+
const trackY = pointer.y - this.scrollArea.thumbStartY;
|
|
759
|
+
const scrollableHeight = this.scrollArea.trackHeight - 30;
|
|
760
|
+
const jumpPercent = Math.max(0, Math.min(1, trackY / scrollableHeight));
|
|
761
|
+
|
|
762
|
+
this.scrollOffset = jumpPercent * this.maxScrollOffset;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
/**
|
|
767
|
+
* Checks if an item at the given index is visible in the current scroll view
|
|
768
|
+
*
|
|
769
|
+
* Determines whether an item should be drawn by checking if its calculated
|
|
770
|
+
* position falls within the visible area boundaries. This prevents drawing
|
|
771
|
+
* items that are scrolled out of view, improving performance with large lists.
|
|
772
|
+
*
|
|
773
|
+
* The check uses the same positioning calculation as getItemDrawY() to ensure
|
|
774
|
+
* consistency between visibility testing and actual drawing positions.
|
|
775
|
+
*
|
|
776
|
+
* @param {number} index - Zero-based index of the item to check
|
|
777
|
+
* @param {Object} customClipBounds - Optional custom clip bounds to override default
|
|
778
|
+
* @returns {boolean} true if item is visible, false if scrolled out of view
|
|
779
|
+
*
|
|
780
|
+
* @example
|
|
781
|
+
* // Basic usage (no clipping)
|
|
782
|
+
* const visible = isItemVisible(5); // true
|
|
783
|
+
*
|
|
784
|
+
* // With custom clipping
|
|
785
|
+
* const visible = isItemVisible(5, {x: 400, y: 100, width: 450, height: 400});
|
|
786
|
+
*/
|
|
787
|
+
isItemVisible(index, customClipBounds = null) {
|
|
788
|
+
if (this.enableClipping) {
|
|
789
|
+
const itemBounds = this.getItemBounds(index);
|
|
790
|
+
const clipBounds = customClipBounds || this.clipBounds;
|
|
791
|
+
return this.calculateIntersection(itemBounds, clipBounds).area > 0;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// Fallback to original logic when clipping is disabled
|
|
795
|
+
const itemY = this.listArea.y + 10 + index * this.listArea.itemHeight - this.scrollOffset;
|
|
796
|
+
const itemBottom = itemY + this.listArea.itemHeight;
|
|
797
|
+
const visibleTop = this.listArea.y;
|
|
798
|
+
const visibleBottom = this.listArea.y + this.listArea.height;
|
|
799
|
+
|
|
800
|
+
// Item is visible if any part of it intersects with the visible area
|
|
801
|
+
return !(itemBottom <= visibleTop || itemY >= visibleBottom);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
/**
|
|
805
|
+
* Gets the correct Y position for drawing an item at the given index
|
|
806
|
+
*
|
|
807
|
+
* Calculates the exact screen position where an item should be drawn,
|
|
808
|
+
* taking into account the current scroll offset. This ensures items
|
|
809
|
+
* move smoothly as the user scrolls through the content.
|
|
810
|
+
*
|
|
811
|
+
* Formula: itemY = listArea.y + padding + (index × itemHeight) - scrollOffset
|
|
812
|
+
*
|
|
813
|
+
* This method is crucial for synchronizing item drawing positions with
|
|
814
|
+
* input handling and scrollbar visual feedback.
|
|
815
|
+
*
|
|
816
|
+
* @param {number} index - Zero-based index of the item
|
|
817
|
+
* @returns {number} Y coordinate where the item should be drawn
|
|
818
|
+
*
|
|
819
|
+
* @example
|
|
820
|
+
* // List configuration: listArea.y = 100, itemHeight = 60
|
|
821
|
+
* // Item at index 3, scrolled down by 120 pixels
|
|
822
|
+
* const y = getItemDrawY(3);
|
|
823
|
+
* // y = 100 + 10 + (3 * 60) - 120 = 100 + 10 + 180 - 120 = 170
|
|
824
|
+
* // Item appears at screen coordinate 170
|
|
825
|
+
*/
|
|
826
|
+
getItemDrawY(index) {
|
|
827
|
+
return (
|
|
828
|
+
this.listArea.y +
|
|
829
|
+
this.listArea.padding +
|
|
830
|
+
index * (this.listArea.itemHeight + this.listArea.padding) -
|
|
831
|
+
this.scrollOffset
|
|
832
|
+
);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
/**
|
|
836
|
+
* Gets the full bounds of an item (before clipping)
|
|
837
|
+
*
|
|
838
|
+
* @param {number} index - Zero-based index of the item
|
|
839
|
+
* @returns {Object} Item bounds {x, y, width, height}
|
|
840
|
+
*/
|
|
841
|
+
getItemBounds(index) {
|
|
842
|
+
return {
|
|
843
|
+
x: this.listArea.x + 10,
|
|
844
|
+
y: this.getItemDrawY(index),
|
|
845
|
+
width: this.listArea.width - 20,
|
|
846
|
+
height: this.listArea.itemHeight
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
/**
|
|
851
|
+
* Gets the clipped bounds of an item within the clip region
|
|
852
|
+
* Only returns bounds if the item intersects with the visible area
|
|
853
|
+
*
|
|
854
|
+
* @param {number} index - Zero-based index of the item
|
|
855
|
+
* @param {Object} customClipBounds - Optional custom clip bounds
|
|
856
|
+
* @returns {Object|null} Clipped bounds {x, y, width, height} or null if not visible
|
|
857
|
+
*/
|
|
858
|
+
getClippedItemBounds(index, customClipBounds = null) {
|
|
859
|
+
if (!this.enableClipping) {
|
|
860
|
+
return this.getItemBounds(index); // No clipping, return full bounds
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
const itemBounds = this.getItemBounds(index);
|
|
864
|
+
const clipBounds = customClipBounds || this.clipBounds;
|
|
865
|
+
|
|
866
|
+
if (!clipBounds) {
|
|
867
|
+
return itemBounds; // No clip bounds defined, return full bounds
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
const intersection = this.calculateIntersection(itemBounds, clipBounds);
|
|
871
|
+
return intersection.area > 0 ? intersection.bounds : null;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
/**
|
|
875
|
+
* Calculates rectangle intersection between two bounds
|
|
876
|
+
*
|
|
877
|
+
* @param {Object} bounds1 - First bounds {x, y, width, height}
|
|
878
|
+
* @param {Object} bounds2 - Second bounds {x, y, width, height}
|
|
879
|
+
* @returns {Object} Intersection result {bounds, area}
|
|
880
|
+
*/
|
|
881
|
+
calculateIntersection(bounds1, bounds2) {
|
|
882
|
+
const x1 = Math.max(bounds1.x, bounds2.x);
|
|
883
|
+
const y1 = Math.max(bounds1.y, bounds2.y);
|
|
884
|
+
const x2 = Math.min(bounds1.x + bounds1.width, bounds2.x + bounds2.width);
|
|
885
|
+
const y2 = Math.min(bounds1.y + bounds1.height, bounds2.y + bounds2.height);
|
|
886
|
+
|
|
887
|
+
const width = Math.max(0, x2 - x1);
|
|
888
|
+
const height = Math.max(0, y2 - y1);
|
|
889
|
+
const area = width * height;
|
|
890
|
+
|
|
891
|
+
return {
|
|
892
|
+
bounds: width > 0 && height > 0 ? { x: x1, y: y1, width, height } : null,
|
|
893
|
+
area: area
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
/**
|
|
898
|
+
* Registers input for a specific item with proper clipping
|
|
899
|
+
* Intelligently handles registration based on visibility and current state
|
|
900
|
+
*
|
|
901
|
+
* @param {Object} item - The item object
|
|
902
|
+
* @param {number} index - Index of the item in the list
|
|
903
|
+
* @param {string} layer - Input layer (default: 'gui')
|
|
904
|
+
*/
|
|
905
|
+
registerItemInput(item, index, layer = "gui") {
|
|
906
|
+
const itemId = this.generateItemId(item, index);
|
|
907
|
+
const isCurrentlyRegistered = this.registeredItems.has(itemId);
|
|
908
|
+
const clippedBounds = this.getClippedItemBounds(index);
|
|
909
|
+
|
|
910
|
+
if (clippedBounds && clippedBounds.width > 0 && clippedBounds.height > 0) {
|
|
911
|
+
// Item is visible - register if not already registered
|
|
912
|
+
if (!isCurrentlyRegistered) {
|
|
913
|
+
this.onRegisterItemInput(itemId, index, clippedBounds, layer);
|
|
914
|
+
this.registeredItems.add(itemId);
|
|
915
|
+
}
|
|
916
|
+
} else {
|
|
917
|
+
// Item is not visible - remove registration if it exists
|
|
918
|
+
if (isCurrentlyRegistered) {
|
|
919
|
+
this.input.removeElement(itemId, layer);
|
|
920
|
+
this.registeredItems.delete(itemId);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
/**
|
|
926
|
+
* Updates input registrations for all visible items
|
|
927
|
+
* Intelligently handles registration based on visibility and state changes
|
|
928
|
+
*
|
|
929
|
+
* @param {Array} items - Array of items to register input for
|
|
930
|
+
* @param {string} layer - Input layer (default: 'gui')
|
|
931
|
+
*/
|
|
932
|
+
updateItemInputs(items, layer = "gui") {
|
|
933
|
+
const currentItemCount = items.length;
|
|
934
|
+
|
|
935
|
+
// Only update if scroll position OR item count changed
|
|
936
|
+
if (this.scrollOffset === this.lastScrollOffsetForInput && currentItemCount === this.lastItemCountForInput) {
|
|
937
|
+
return; // No changes, skip update
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
// For item count changes, we need to be more selective
|
|
941
|
+
// Only clear items that are no longer valid or visible
|
|
942
|
+
if (currentItemCount !== this.lastItemCountForInput) {
|
|
943
|
+
this.cleanupInvalidRegistrations(items, layer);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// Update registrations for all items (this will add new ones and remove invisible ones)
|
|
947
|
+
items.forEach((item, index) => {
|
|
948
|
+
this.registerItemInput(item, index, layer);
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
// Update tracking
|
|
952
|
+
this.lastScrollOffsetForInput = this.scrollOffset;
|
|
953
|
+
this.lastItemCountForInput = currentItemCount;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
/**
|
|
957
|
+
* Cleans up registrations for items that are no longer valid
|
|
958
|
+
* More intelligent than clearing everything - only removes truly invalid items
|
|
959
|
+
*
|
|
960
|
+
* @param {Array} items - Current array of valid items
|
|
961
|
+
* @param {string} layer - Input layer
|
|
962
|
+
*/
|
|
963
|
+
cleanupInvalidRegistrations(items, layer = "gui") {
|
|
964
|
+
// Create a set of current valid item IDs
|
|
965
|
+
const validItemIds = new Set();
|
|
966
|
+
items.forEach((item, index) => {
|
|
967
|
+
validItemIds.add(this.generateItemId(item, index));
|
|
968
|
+
});
|
|
969
|
+
|
|
970
|
+
// Remove registrations for items that are no longer in the valid set
|
|
971
|
+
const itemsToRemove = [];
|
|
972
|
+
this.registeredItems.forEach((itemId) => {
|
|
973
|
+
if (!validItemIds.has(itemId)) {
|
|
974
|
+
itemsToRemove.push(itemId);
|
|
975
|
+
}
|
|
976
|
+
});
|
|
977
|
+
|
|
978
|
+
// Remove the invalid items
|
|
979
|
+
itemsToRemove.forEach((itemId) => {
|
|
980
|
+
this.input.removeElement(itemId, layer);
|
|
981
|
+
this.registeredItems.delete(itemId);
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
/**
|
|
986
|
+
* Forces an input update for all items (useful when items are added/removed)
|
|
987
|
+
* Call this when the item array changes outside of normal scroll updates
|
|
988
|
+
*
|
|
989
|
+
* @param {Array} items - Array of items to register input for
|
|
990
|
+
* @param {string} layer - Input layer (default: 'gui')
|
|
991
|
+
*/
|
|
992
|
+
refreshItems(items, layer = "gui") {
|
|
993
|
+
// Clear ALL existing item registrations from input system first
|
|
994
|
+
this.clearAllItemInputs(layer);
|
|
995
|
+
|
|
996
|
+
// Reset tracking to force update
|
|
997
|
+
this.lastScrollOffsetForInput = -1;
|
|
998
|
+
this.lastItemCountForInput = -1;
|
|
999
|
+
this.registeredItems.clear();
|
|
1000
|
+
|
|
1001
|
+
// Update with new items
|
|
1002
|
+
this.updateItemInputs(items, layer);
|
|
1003
|
+
|
|
1004
|
+
// Re-detect hover state after refresh
|
|
1005
|
+
this.reDetectHoverAfterRefresh(items, layer);
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
/**
|
|
1009
|
+
* Re-detects hover state immediately after refreshing items
|
|
1010
|
+
* Checks current mouse position and sets hover for any item under cursor
|
|
1011
|
+
*
|
|
1012
|
+
* @param {Array} items - The current items array
|
|
1013
|
+
* @param {string} layer - Input layer (default: 'gui')
|
|
1014
|
+
*/
|
|
1015
|
+
reDetectHoverAfterRefresh(items, layer = "gui") {
|
|
1016
|
+
const pointer = this.input.getPointerPosition();
|
|
1017
|
+
|
|
1018
|
+
// Check if mouse is over the list area
|
|
1019
|
+
const isOverListArea =
|
|
1020
|
+
pointer.x >= this.listArea.x &&
|
|
1021
|
+
pointer.x <= this.listArea.x + this.listArea.width &&
|
|
1022
|
+
pointer.y >= this.listArea.y &&
|
|
1023
|
+
pointer.y <= this.listArea.y + this.listArea.height;
|
|
1024
|
+
|
|
1025
|
+
if (isOverListArea) {
|
|
1026
|
+
// Find which item (if any) is under the mouse
|
|
1027
|
+
for (let i = 0; i < items.length; i++) {
|
|
1028
|
+
const itemBounds = this.getItemBounds(i);
|
|
1029
|
+
|
|
1030
|
+
if (
|
|
1031
|
+
pointer.x >= itemBounds.x &&
|
|
1032
|
+
pointer.x <= itemBounds.x + itemBounds.width &&
|
|
1033
|
+
pointer.y >= itemBounds.y &&
|
|
1034
|
+
pointer.y <= itemBounds.y + itemBounds.height
|
|
1035
|
+
) {
|
|
1036
|
+
// Found the item under mouse - set it as hovered
|
|
1037
|
+
const itemId = this.generateItemId(items[i], i);
|
|
1038
|
+
const element = this.input.rawState.elements[layer].get(itemId);
|
|
1039
|
+
if (element) {
|
|
1040
|
+
element.isHovered = true;
|
|
1041
|
+
element.hoverTimestamp = performance.now();
|
|
1042
|
+
}
|
|
1043
|
+
return; // Stop after finding the first (topmost) item
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
/**
|
|
1050
|
+
* Clears all item input registrations from the input system
|
|
1051
|
+
* This prevents multiple items from being selected when scrolling
|
|
1052
|
+
*
|
|
1053
|
+
* @param {string} layer - Input layer to clear (default: 'gui')
|
|
1054
|
+
*/
|
|
1055
|
+
clearAllItemInputs(layer = "gui") {
|
|
1056
|
+
// Remove all currently registered items from input system
|
|
1057
|
+
this.registeredItems.forEach((itemId) => {
|
|
1058
|
+
this.input.removeElement(itemId, layer);
|
|
1059
|
+
});
|
|
1060
|
+
this.registeredItems.clear();
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
/**
|
|
1064
|
+
* Draws the scrollable content area background fill only
|
|
1065
|
+
*
|
|
1066
|
+
* Renders the background fill for the scrollable content area.
|
|
1067
|
+
* Border is drawn separately to allow layering over items.
|
|
1068
|
+
*
|
|
1069
|
+
* @private
|
|
1070
|
+
*/
|
|
1071
|
+
drawScrollableBackgroundFill() {
|
|
1072
|
+
if (!this.drawBackground) return;
|
|
1073
|
+
|
|
1074
|
+
const { x, y, width, height } = this.listArea;
|
|
1075
|
+
|
|
1076
|
+
// Draw background fill
|
|
1077
|
+
if (this.backgroundConfig.fillColor) {
|
|
1078
|
+
this.ctx.fillStyle = this.backgroundConfig.fillColor;
|
|
1079
|
+
if (this.backgroundConfig.cornerRadius > 0) {
|
|
1080
|
+
this.drawRoundedRect(x, y, width, height, this.backgroundConfig.cornerRadius);
|
|
1081
|
+
} else {
|
|
1082
|
+
this.ctx.fillRect(x, y, width, height);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
/**
|
|
1088
|
+
* Draws the scrollable content area border only
|
|
1089
|
+
*
|
|
1090
|
+
* Renders the border for the scrollable content area.
|
|
1091
|
+
* Called after items are drawn to ensure border appears on top.
|
|
1092
|
+
*
|
|
1093
|
+
* @private
|
|
1094
|
+
*/
|
|
1095
|
+
drawScrollableBorder() {
|
|
1096
|
+
if (!this.drawBackground) return;
|
|
1097
|
+
|
|
1098
|
+
const { x, y, width, height } = this.listArea;
|
|
1099
|
+
|
|
1100
|
+
// Draw border
|
|
1101
|
+
if (this.backgroundConfig.borderColor && this.backgroundConfig.borderWidth > 0) {
|
|
1102
|
+
this.ctx.strokeStyle = this.backgroundConfig.borderColor;
|
|
1103
|
+
this.ctx.lineWidth = this.backgroundConfig.borderWidth;
|
|
1104
|
+
if (this.backgroundConfig.cornerRadius > 0) {
|
|
1105
|
+
this.drawRoundedRectStroke(x, y, width, height, this.backgroundConfig.cornerRadius);
|
|
1106
|
+
} else {
|
|
1107
|
+
this.ctx.strokeRect(x, y, width, height);
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
/**
|
|
1113
|
+
* Draws a rounded rectangle filled shape
|
|
1114
|
+
* @private
|
|
1115
|
+
*/
|
|
1116
|
+
drawRoundedRect(x, y, width, height, radius) {
|
|
1117
|
+
this.ctx.beginPath();
|
|
1118
|
+
this.ctx.moveTo(x + radius, y);
|
|
1119
|
+
this.ctx.lineTo(x + width - radius, y);
|
|
1120
|
+
this.ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
|
|
1121
|
+
this.ctx.lineTo(x + width, y + height - radius);
|
|
1122
|
+
this.ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
|
|
1123
|
+
this.ctx.lineTo(x + radius, y + height);
|
|
1124
|
+
this.ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
|
|
1125
|
+
this.ctx.lineTo(x, y + radius);
|
|
1126
|
+
this.ctx.quadraticCurveTo(x, y, x + radius, y);
|
|
1127
|
+
this.ctx.closePath();
|
|
1128
|
+
this.ctx.fill();
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
/**
|
|
1132
|
+
* Draws a rounded rectangle outline
|
|
1133
|
+
* @private
|
|
1134
|
+
*/
|
|
1135
|
+
drawRoundedRectStroke(x, y, width, height, radius) {
|
|
1136
|
+
this.ctx.beginPath();
|
|
1137
|
+
this.ctx.moveTo(x + radius, y);
|
|
1138
|
+
this.ctx.lineTo(x + width - radius, y);
|
|
1139
|
+
this.ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
|
|
1140
|
+
this.ctx.lineTo(x + width, y + height - radius);
|
|
1141
|
+
this.ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
|
|
1142
|
+
this.ctx.lineTo(x + radius, y + height);
|
|
1143
|
+
this.ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
|
|
1144
|
+
this.ctx.lineTo(x, y + radius);
|
|
1145
|
+
this.ctx.quadraticCurveTo(x, y, x + radius, y);
|
|
1146
|
+
this.ctx.closePath();
|
|
1147
|
+
this.ctx.stroke();
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
/**
|
|
1151
|
+
* Draws only the scrollbar elements (without background)
|
|
1152
|
+
*
|
|
1153
|
+
* Renders only the scrollbar UI components:
|
|
1154
|
+
* - Scrollbar track (background area)
|
|
1155
|
+
* - Scrollbar thumb (draggable handle)
|
|
1156
|
+
* - Up/Down arrow buttons
|
|
1157
|
+
*
|
|
1158
|
+
* Only draws the scrollbar if there's content to scroll (maxScrollOffset > 0).
|
|
1159
|
+
*
|
|
1160
|
+
* @private
|
|
1161
|
+
*/
|
|
1162
|
+
drawScrollbarElements() {
|
|
1163
|
+
// Only show scrollbar if there's actually content to scroll
|
|
1164
|
+
if (this.maxScrollOffset > 0) {
|
|
1165
|
+
this.drawScrollbarTrack();
|
|
1166
|
+
this.drawScrollButtons();
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
/**
|
|
1171
|
+
* Draws the complete scrollbar interface with automatic clipping
|
|
1172
|
+
*
|
|
1173
|
+
* Enhanced drawing method that handles clipping automatically when enabled.
|
|
1174
|
+
* Provide a renderItem function to draw items without manual clipping management.
|
|
1175
|
+
*
|
|
1176
|
+
* @param {Array} items - Array of items to draw
|
|
1177
|
+
* @param {Function} renderItem - Function to render each item: (item, index, y) => void
|
|
1178
|
+
* @param {Object} options - Additional options
|
|
1179
|
+
* @param {Function} options.renderHeader - Optional header rendering function
|
|
1180
|
+
*
|
|
1181
|
+
* @example
|
|
1182
|
+
* this.roomScroller = new ActionScrollableArea({
|
|
1183
|
+
* enableClipping: true,
|
|
1184
|
+
* clipBounds: { x: 250, y: 330, width: 300, height: 240 }
|
|
1185
|
+
* });
|
|
1186
|
+
*
|
|
1187
|
+
* this.roomScroller.draw(rooms, (room, index, y) => {
|
|
1188
|
+
* this.guiCtx.fillStyle = isHovered ? '#0099dd' : '#007acc';
|
|
1189
|
+
* this.guiCtx.fillRect(250, y, 300, 25);
|
|
1190
|
+
* this.guiCtx.fillText(room.name, 400, y + 17);
|
|
1191
|
+
* }, {
|
|
1192
|
+
* renderHeader: () => {
|
|
1193
|
+
* this.guiCtx.fillText('Available Rooms:', 400, 320);
|
|
1194
|
+
* }
|
|
1195
|
+
* });
|
|
1196
|
+
*/
|
|
1197
|
+
draw(items, renderItem, options = {}) {
|
|
1198
|
+
// Draw scrollbar elements first (never clipped)
|
|
1199
|
+
this.drawScrollbarElements();
|
|
1200
|
+
|
|
1201
|
+
// Apply clipping if enabled
|
|
1202
|
+
if (this.enableClipping && this.clipBounds) {
|
|
1203
|
+
this.ctx.save();
|
|
1204
|
+
this.ctx.beginPath();
|
|
1205
|
+
this.ctx.rect(this.clipBounds.x, this.clipBounds.y, this.clipBounds.width, this.clipBounds.height);
|
|
1206
|
+
this.ctx.clip();
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
// Draw background (fill only, no border)
|
|
1210
|
+
this.drawScrollableBackgroundFill();
|
|
1211
|
+
|
|
1212
|
+
// Draw header if provided
|
|
1213
|
+
if (options.renderHeader) {
|
|
1214
|
+
options.renderHeader();
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
// Draw all visible items with automatic clipping
|
|
1218
|
+
if (items && renderItem) {
|
|
1219
|
+
items.forEach((item, index) => {
|
|
1220
|
+
if (this.isItemVisible(index)) {
|
|
1221
|
+
const y = this.getItemDrawY(index);
|
|
1222
|
+
renderItem(item, index, y);
|
|
1223
|
+
}
|
|
1224
|
+
});
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
// Restore clipping context
|
|
1228
|
+
if (this.enableClipping && this.clipBounds) {
|
|
1229
|
+
this.ctx.restore();
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
// Draw border on top (after clipping restored)
|
|
1233
|
+
this.drawScrollableBorder();
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
/**
|
|
1237
|
+
* Call this after drawing items to restore clipping context
|
|
1238
|
+
* Only needed if enableClipping is true and using legacy draw() method
|
|
1239
|
+
*/
|
|
1240
|
+
endClipping() {
|
|
1241
|
+
if (this.enableClipping && this.clipBounds) {
|
|
1242
|
+
this.ctx.restore();
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
/**
|
|
1247
|
+
* Draws the scrollbar track and thumb with visual feedback
|
|
1248
|
+
*
|
|
1249
|
+
* Renders the scrollbar track (background) and thumb (draggable handle)
|
|
1250
|
+
* with sophisticated visual states:
|
|
1251
|
+
* - Track changes opacity when hovered for user feedback
|
|
1252
|
+
* - Thumb changes color and opacity based on interaction state
|
|
1253
|
+
* - Thumb shows different appearance when being dragged vs hovered vs idle
|
|
1254
|
+
* - All visual states provide immediate feedback about scrollbar state
|
|
1255
|
+
*
|
|
1256
|
+
* Color scheme:
|
|
1257
|
+
* - Track: Semi-transparent dark background
|
|
1258
|
+
* - Thumb: Blue with alpha transparency
|
|
1259
|
+
* - Hover: Increased brightness and opacity
|
|
1260
|
+
* - Drag: Maximum brightness and opacity with border highlight
|
|
1261
|
+
*
|
|
1262
|
+
* @private
|
|
1263
|
+
* @example
|
|
1264
|
+
* // Idle state: thumb is subtle blue
|
|
1265
|
+
* this.isDragging = false;
|
|
1266
|
+
* drawScrollbarTrack(); // Thumb: rgba(52, 152, 219, 0.3)
|
|
1267
|
+
*
|
|
1268
|
+
* // Hover state: thumb brightens
|
|
1269
|
+
* this.scrollThumb.hovered = true;
|
|
1270
|
+
* drawScrollbarTrack(); // Thumb: rgba(52, 152, 219, 0.6)
|
|
1271
|
+
*
|
|
1272
|
+
* // Drag state: thumb is most prominent
|
|
1273
|
+
* this.isDragging = true;
|
|
1274
|
+
* drawScrollbarTrack(); // Thumb: rgba(52, 152, 219, 0.8) with blue border
|
|
1275
|
+
*/
|
|
1276
|
+
drawScrollbarTrack() {
|
|
1277
|
+
const scrollbarX = this.scrollArea.x;
|
|
1278
|
+
const scrollbarWidth = 20;
|
|
1279
|
+
const scrollAreaHeight = this.scrollArea.trackHeight;
|
|
1280
|
+
|
|
1281
|
+
// Check if mouse is over scrollbar track for visual feedback
|
|
1282
|
+
const mousePos = this.input.getPointerPosition();
|
|
1283
|
+
const isOverTrack =
|
|
1284
|
+
mousePos.x >= scrollbarX &&
|
|
1285
|
+
mousePos.x <= scrollbarX + scrollbarWidth &&
|
|
1286
|
+
mousePos.y >= this.scrollArea.thumbStartY &&
|
|
1287
|
+
mousePos.y <= this.scrollArea.y + this.scrollArea.trackHeight;
|
|
1288
|
+
|
|
1289
|
+
// Dark track background (using configurable colors)
|
|
1290
|
+
this.ctx.fillStyle = isOverTrack ? this.colors.track.hover : this.colors.track.normal;
|
|
1291
|
+
this.ctx.fillRect(scrollbarX, this.scrollArea.y, scrollbarWidth, scrollAreaHeight);
|
|
1292
|
+
|
|
1293
|
+
// Draw scrollbar thumb (using configurable colors)
|
|
1294
|
+
const thumbColor = this.scrollThumb.hovered
|
|
1295
|
+
? this.colors.thumb.hover
|
|
1296
|
+
: this.isDragging
|
|
1297
|
+
? this.colors.thumb.drag
|
|
1298
|
+
: this.colors.thumb.normal;
|
|
1299
|
+
|
|
1300
|
+
this.ctx.fillStyle = thumbColor;
|
|
1301
|
+
this.ctx.fillRect(this.scrollThumb.x, this.scrollThumb.y, this.scrollThumb.width, this.scrollThumb.height);
|
|
1302
|
+
|
|
1303
|
+
// Draw thumb border (using configurable colors)
|
|
1304
|
+
this.ctx.strokeStyle = this.isDragging ? this.colors.thumbBorder.drag : this.colors.thumbBorder.normal;
|
|
1305
|
+
this.ctx.lineWidth = 2;
|
|
1306
|
+
this.ctx.strokeRect(this.scrollThumb.x, this.scrollThumb.y, this.scrollThumb.width, this.scrollThumb.height);
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
/**
|
|
1310
|
+
* Draws both the up and down scroll buttons
|
|
1311
|
+
*
|
|
1312
|
+
* Convenience method that renders both arrow buttons at the top and
|
|
1313
|
+
* bottom of the scrollbar track. Each button provides incremental
|
|
1314
|
+
* scrolling when clicked.
|
|
1315
|
+
*
|
|
1316
|
+
* @private
|
|
1317
|
+
* @example
|
|
1318
|
+
* // Draws up button (▲) at top of track
|
|
1319
|
+
* // Draws down button (▼) at bottom of track
|
|
1320
|
+
* drawScrollButtons();
|
|
1321
|
+
*/
|
|
1322
|
+
drawScrollButtons() {
|
|
1323
|
+
this.drawScrollButton(this.scrollUpButton);
|
|
1324
|
+
this.drawScrollButton(this.scrollDownButton);
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
/**
|
|
1328
|
+
* Draws a single scroll button with hover effects and proper styling
|
|
1329
|
+
*
|
|
1330
|
+
* Renders an individual scroll button (up or down arrow) with:
|
|
1331
|
+
* - Semi-transparent background that brightens on hover
|
|
1332
|
+
* - Border that highlights when hovered
|
|
1333
|
+
* - Centered text (▲ or ▼) that changes color on interaction
|
|
1334
|
+
* - Consistent visual feedback matching the scrollbar theme
|
|
1335
|
+
*
|
|
1336
|
+
* The button integrates with the input system for click detection
|
|
1337
|
+
* and hover state management.
|
|
1338
|
+
*
|
|
1339
|
+
* @param {Object} button - Button configuration object
|
|
1340
|
+
* @param {number} button.x - X coordinate of the button
|
|
1341
|
+
* @param {number} button.y - Y coordinate of the button
|
|
1342
|
+
* @param {number} button.width - Width of the button
|
|
1343
|
+
* @param {number} button.height - Height of the button
|
|
1344
|
+
* @param {string} button.text - Button text (▲ or ▼)
|
|
1345
|
+
* @param {boolean} button.hovered - Current hover state
|
|
1346
|
+
*
|
|
1347
|
+
* @private
|
|
1348
|
+
* @example
|
|
1349
|
+
* // Draw up button with hover effect
|
|
1350
|
+
* const upButton = {
|
|
1351
|
+
* x: 860, y: 90, width: 20, height: 20,
|
|
1352
|
+
* text: "▲", hovered: true
|
|
1353
|
+
* };
|
|
1354
|
+
* drawScrollButton(upButton);
|
|
1355
|
+
* // Renders bright button with "#3498DB" text color
|
|
1356
|
+
*/
|
|
1357
|
+
drawScrollButton(button) {
|
|
1358
|
+
this.ctx.save();
|
|
1359
|
+
|
|
1360
|
+
const cornerX = button.x;
|
|
1361
|
+
const cornerY = button.y;
|
|
1362
|
+
|
|
1363
|
+
// Button background (using configurable colors)
|
|
1364
|
+
this.ctx.fillStyle = button.hovered ? this.colors.button.hover : this.colors.button.normal;
|
|
1365
|
+
this.ctx.fillRect(cornerX, cornerY, button.width, button.height);
|
|
1366
|
+
|
|
1367
|
+
// Button border (using configurable colors)
|
|
1368
|
+
this.ctx.strokeStyle = button.hovered ? this.colors.buttonText.hover : this.colors.buttonText.normal;
|
|
1369
|
+
this.ctx.lineWidth = 1;
|
|
1370
|
+
this.ctx.strokeRect(cornerX, cornerY, button.width, button.height);
|
|
1371
|
+
|
|
1372
|
+
// Button text (using configurable colors)
|
|
1373
|
+
this.ctx.fillStyle = button.hovered ? this.colors.buttonText.hover : this.colors.buttonText.normal;
|
|
1374
|
+
this.ctx.font = "bold 14px Arial";
|
|
1375
|
+
this.ctx.textAlign = "center";
|
|
1376
|
+
this.ctx.textBaseline = "middle";
|
|
1377
|
+
this.ctx.fillText(button.text, button.x + button.width / 2, button.y + button.height / 2 + 1);
|
|
1378
|
+
|
|
1379
|
+
this.ctx.restore();
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
/**
|
|
1383
|
+
* Destroys the scrollable area and cleans up all resources
|
|
1384
|
+
*
|
|
1385
|
+
* Properly cleans up the component by:
|
|
1386
|
+
* 1. Removing all input element registrations from the input system
|
|
1387
|
+
* 2. Removing mouse wheel event listeners from canvas and window
|
|
1388
|
+
* 3. Preventing memory leaks and input conflicts
|
|
1389
|
+
*
|
|
1390
|
+
* Always call this method when removing a scrollable area from your game
|
|
1391
|
+
* to ensure proper cleanup and prevent lingering event handlers.
|
|
1392
|
+
*
|
|
1393
|
+
* @example
|
|
1394
|
+
* // When removing a UI screen or component
|
|
1395
|
+
* destroy() {
|
|
1396
|
+
* this.inventoryScroller.destroy();
|
|
1397
|
+
* this.inventoryScroller = null;
|
|
1398
|
+
* }
|
|
1399
|
+
*/
|
|
1400
|
+
destroy() {
|
|
1401
|
+
this.input.removeElement(this.elementIds.scrollUp, "gui");
|
|
1402
|
+
this.input.removeElement(this.elementIds.scrollDown, "gui");
|
|
1403
|
+
this.input.removeElement(this.elementIds.scrollbarTrack, "gui");
|
|
1404
|
+
}
|
|
1405
|
+
}
|