@guinetik/gcanvas 1.0.1 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/demos/coordinates.html +698 -0
  2. package/demos/cube3d.html +23 -0
  3. package/demos/demos.css +17 -3
  4. package/demos/dino.html +42 -0
  5. package/demos/gameobjects.html +626 -0
  6. package/demos/index.html +17 -7
  7. package/demos/js/coordinates.js +840 -0
  8. package/demos/js/cube3d.js +789 -0
  9. package/demos/js/dino.js +1420 -0
  10. package/demos/js/gameobjects.js +176 -0
  11. package/demos/js/plane3d.js +256 -0
  12. package/demos/js/platformer.js +1579 -0
  13. package/demos/js/sphere3d.js +229 -0
  14. package/demos/js/sprite.js +473 -0
  15. package/demos/js/tde/accretiondisk.js +3 -3
  16. package/demos/js/tde/tidalstream.js +2 -2
  17. package/demos/plane3d.html +24 -0
  18. package/demos/platformer.html +43 -0
  19. package/demos/sphere3d.html +24 -0
  20. package/demos/sprite.html +18 -0
  21. package/docs/concepts/coordinate-system.md +384 -0
  22. package/docs/concepts/shapes-vs-gameobjects.md +187 -0
  23. package/docs/fluid-dynamics.md +99 -97
  24. package/package.json +1 -1
  25. package/src/game/game.js +11 -5
  26. package/src/game/objects/index.js +3 -0
  27. package/src/game/objects/platformer-scene.js +411 -0
  28. package/src/game/objects/scene.js +14 -0
  29. package/src/game/objects/sprite.js +529 -0
  30. package/src/game/pipeline.js +20 -16
  31. package/src/game/ui/theme.js +123 -121
  32. package/src/io/input.js +75 -45
  33. package/src/io/mouse.js +44 -19
  34. package/src/io/touch.js +35 -12
  35. package/src/shapes/cube3d.js +599 -0
  36. package/src/shapes/index.js +2 -0
  37. package/src/shapes/plane3d.js +687 -0
  38. package/src/shapes/sphere3d.js +75 -6
  39. package/src/util/camera2d.js +315 -0
  40. package/src/util/index.js +1 -0
  41. package/src/webgl/shaders/plane-shaders.js +332 -0
  42. package/src/webgl/shaders/sphere-shaders.js +4 -2
@@ -1,97 +1,99 @@
1
- # Fluid & Gas Dynamics (Math-Only)
2
-
3
- Pure math helpers for SPH-style liquids and lightweight gas simulation. The math stays in `src/math/fluid.js`; consumers (games/demos) are responsible for applying forces to their particles.
4
-
5
- ## What’s Included
6
- - SPH density, pressure, and viscosity kernels (`computeDensities`, `computePressures`, `computeFluidForces`).
7
- - Simplified gas mixing with diffusion/pressure/turbulence (`computeGasForces`).
8
- - Thermal buoyancy coupling (`computeThermalBuoyancy`) that pairs with `src/math/heat.js`.
9
- - Force blending and pure Euler integration (`blendForces`, `integrateEuler`).
10
- - Config factory with no magic numbers (`getDefaultFluidConfig`).
11
-
12
- ## Config
13
- Defined in `src/math/fluid.js` and merged via `mergeConfig` internally.
14
-
15
- ```js
16
- const CONFIG = {
17
- kernel: { smoothingRadius: 28 },
18
- fluid: {
19
- restDensity: 1.1,
20
- particleMass: 1,
21
- pressureStiffness: 1800,
22
- viscosity: 0.18,
23
- surfaceTension: 0,
24
- maxForce: 6000,
25
- },
26
- gas: {
27
- interactionRadius: 34,
28
- pressure: 12,
29
- diffusion: 0.08,
30
- drag: 0.04,
31
- buoyancy: 260,
32
- neutralTemperature: 0.5,
33
- turbulence: 16,
34
- },
35
- external: { gravity: { x: 0, y: 820 } },
36
- };
37
- ```
38
-
39
- Override by passing `{ fluid: { … }, gas: { … }, kernel: { … } }` to any helper.
40
-
41
- ## Particle Shape
42
- Any object with `{ x, y, vx, vy, size?, mass?, custom? }`. Mass is resolved from:
43
- `custom.mass` → `mass` → `size` → `config.fluid.particleMass`.
44
-
45
- ## Core API (pure)
46
- - `computeDensities(particles, cfg?)` → `Float32Array densities`.
47
- - `computePressures(densities, cfg?)` → `Float32Array pressures`.
48
- - `computeFluidForces(particles, cfg?)` → `{ forces, densities, pressures }`.
49
- - `computeGasForces(particles, cfg?)` → `{ forces }`.
50
- - `computeThermalBuoyancy(particles, cfg?)` → `forces[]` using `temperature` or `custom.temperature`.
51
- - `blendForces(a, b, t)` → lerped forces.
52
- - `integrateEuler(particles, forces, dt, cfg?)` → new `{ x, y, vx, vy }[]` (no mutation).
53
-
54
- ## Applying in a Game (pattern)
55
- 1) Build forces:
56
-
57
- ```js
58
- import { computeFluidForces, computeGasForces, blendForces, computeThermalBuoyancy } from "../../src/math/fluid.js";
59
-
60
- const liquid = computeFluidForces(particles, { kernel: { smoothingRadius: 26 } });
61
- const gas = computeGasForces(particles, { gas: { interactionRadius: 40 } });
62
- const buoyancy = computeThermalBuoyancy(particles);
63
-
64
- // Combine as you see fit (example: mix liquid & gas modes, then add buoyancy)
65
- const mixed = blendForces(liquid.forces, gas.forces, modeT); // modeT: 0..1
66
- for (let i = 0; i < particles.length; i++) {
67
- mixed[i].x += buoyancy[i].x;
68
- mixed[i].y += buoyancy[i].y;
69
- }
70
- ```
71
-
72
- 2) Apply to your particles (consumer-controlled):
73
-
74
- ```js
75
- // Mutate in your game loop; math module stays pure.
76
- for (let i = 0; i < particles.length; i++) {
77
- const p = particles[i];
78
- const f = mixed[i];
79
- const mass = p.custom.mass ?? 1;
80
- p.vx += (f.x / mass) * dt;
81
- p.vy += (f.y / mass) * dt;
82
- }
83
- ```
84
-
85
- 3) Let your normal updaters move/render (e.g., `ParticleSystem` velocity updater).
86
-
87
- ## Heat Coupling
88
- Assign `p.temperature` or `p.custom.temperature` each frame (e.g., via `heat.zoneTemperature`). Pass the same particles to `computeThermalBuoyancy` and add the resulting forces.
89
-
90
- ## Notes & Tips
91
- - Keep `smoothingRadius` proportional to particle spacing (roughly 2–3× average spacing).
92
- - Clamp velocities in the consumer if you target 10k–20k particles to keep the frame budget.
93
- - For gases, favor lower `pressure` and higher `diffusion` to avoid jitter.
94
- - The math never allocates inside the hot path besides output arrays; reuse them between frames if you need fewer allocations (pass your own particles array).
95
-
96
-
97
-
1
+ # Fluid & Gas Dynamics (Math-Only)
2
+
3
+ Pure math helpers for SPH-style liquids and lightweight gas simulation. The math stays in `src/math/fluid.js`; consumers (games/demos) are responsible for applying forces to their particles.
4
+
5
+ ## What’s Included
6
+ - SPH density, pressure, and viscosity kernels (`computeDensities`, `computePressures`, `computeFluidForces`).
7
+ - Simplified gas mixing with diffusion/pressure/turbulence (`computeGasForces`).
8
+ - Thermal buoyancy coupling (`computeThermalBuoyancy`) that pairs with `src/math/heat.js`.
9
+ - Force blending and pure Euler integration (`blendForces`, `integrateEuler`).
10
+ - Config factory with no magic numbers (`getDefaultFluidConfig`).
11
+
12
+ ## Config
13
+ Defined in `src/math/fluid.js` and merged via `mergeConfig` internally.
14
+
15
+ ```js
16
+ const CONFIG = {
17
+ kernel: { smoothingRadius: 28 },
18
+ fluid: {
19
+ restDensity: 1.1,
20
+ particleMass: 1,
21
+ pressureStiffness: 1800,
22
+ viscosity: 0.18,
23
+ surfaceTension: 0,
24
+ maxForce: 6000,
25
+ },
26
+ gas: {
27
+ interactionRadius: 34,
28
+ pressure: 12,
29
+ diffusion: 0.08,
30
+ drag: 0.04,
31
+ buoyancy: 260,
32
+ neutralTemperature: 0.5,
33
+ turbulence: 16,
34
+ },
35
+ external: { gravity: { x: 0, y: 820 } },
36
+ };
37
+ ```
38
+
39
+ Override by passing `{ fluid: { … }, gas: { … }, kernel: { … } }` to any helper.
40
+
41
+ ## Particle Shape
42
+ Any object with `{ x, y, vx, vy, size?, mass?, custom? }`. Mass is resolved from:
43
+ `custom.mass` → `mass` → `size` → `config.fluid.particleMass`.
44
+
45
+ ## Core API (pure)
46
+ - `computeDensities(particles, cfg?)` → `Float32Array densities`.
47
+ - `computePressures(densities, cfg?)` → `Float32Array pressures`.
48
+ - `computeFluidForces(particles, cfg?)` → `{ forces, densities, pressures }`.
49
+ - `computeGasForces(particles, cfg?)` → `{ forces }`.
50
+ - `computeThermalBuoyancy(particles, cfg?)` → `forces[]` using `temperature` or `custom.temperature`.
51
+ - `blendForces(a, b, t)` → lerped forces.
52
+ - `integrateEuler(particles, forces, dt, cfg?)` → new `{ x, y, vx, vy }[]` (no mutation).
53
+
54
+ ## Applying in a Game (pattern)
55
+ 1) Build forces:
56
+
57
+ ```js
58
+ import { computeFluidForces, computeGasForces, blendForces, computeThermalBuoyancy } from "../../src/math/fluid.js";
59
+
60
+ const liquid = computeFluidForces(particles, { kernel: { smoothingRadius: 26 } });
61
+ const gas = computeGasForces(particles, { gas: { interactionRadius: 40 } });
62
+ const buoyancy = computeThermalBuoyancy(particles);
63
+
64
+ // Combine as you see fit (example: mix liquid & gas modes, then add buoyancy)
65
+ const mixed = blendForces(liquid.forces, gas.forces, modeT); // modeT: 0..1
66
+ for (let i = 0; i < particles.length; i++) {
67
+ mixed[i].x += buoyancy[i].x;
68
+ mixed[i].y += buoyancy[i].y;
69
+ }
70
+ ```
71
+
72
+ 2) Apply to your particles (consumer-controlled):
73
+
74
+ ```js
75
+ // Mutate in your game loop; math module stays pure.
76
+ for (let i = 0; i < particles.length; i++) {
77
+ const p = particles[i];
78
+ const f = mixed[i];
79
+ const mass = p.custom.mass ?? 1;
80
+ p.vx += (f.x / mass) * dt;
81
+ p.vy += (f.y / mass) * dt;
82
+ }
83
+ ```
84
+
85
+ 3) Let your normal updaters move/render (e.g., `ParticleSystem` velocity updater).
86
+
87
+ ## Heat Coupling
88
+ Assign `p.temperature` or `p.custom.temperature` each frame (e.g., via `heat.zoneTemperature`). Pass the same particles to `computeThermalBuoyancy` and add the resulting forces.
89
+
90
+ ## Notes & Tips
91
+ - Keep `smoothingRadius` proportional to particle spacing (roughly 2–3× average spacing).
92
+ - Clamp velocities in the consumer if you target 10k–20k particles to keep the frame budget.
93
+ - For gases, favor lower `pressure` and higher `diffusion` to avoid jitter.
94
+ - The math never allocates inside the hot path besides output arrays; reuse them between frames if you need fewer allocations (pass your own particles array).
95
+
96
+
97
+
98
+
99
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guinetik/gcanvas",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "Canvas Utilities and 2d Primitives",
5
5
  "main": "index.js",
6
6
  "types": "types/index.d.ts",
package/src/game/game.js CHANGED
@@ -211,12 +211,18 @@ export class Game {
211
211
  /**
212
212
  * Enables automatic resizing of the canvas to either the window or a given container.
213
213
  * @param {HTMLElement} [container=window] - Element to observe for resizing. Defaults to window.
214
+ * @param {Object} [padding={}] - Optional padding to subtract from the canvas size.
215
+ * @param {number} [padding.top=0] - Top padding.
216
+ * @param {number} [padding.right=0] - Right padding.
217
+ * @param {number} [padding.bottom=0] - Bottom padding.
218
+ * @param {number} [padding.left=0] - Left padding.
214
219
  */
215
- enableFluidSize(container = window) {
220
+ enableFluidSize(container = window, padding = {}) {
221
+ const { top = 0, right = 0, bottom = 0, left = 0 } = padding;
216
222
  if (container === window) {
217
223
  const resizeCanvas = () => {
218
- this.canvas.width = window.innerWidth;
219
- this.canvas.height = window.innerHeight;
224
+ this.canvas.width = window.innerWidth - left - right;
225
+ this.canvas.height = window.innerHeight - top - bottom;
220
226
  if (
221
227
  this.#prevWidth !== this.canvas.width ||
222
228
  this.#prevHeight !== this.canvas.height
@@ -240,8 +246,8 @@ export class Game {
240
246
  }
241
247
  const resizeCanvas = () => {
242
248
  const rect = container.getBoundingClientRect();
243
- this.canvas.width = rect.width;
244
- this.canvas.height = rect.height;
249
+ this.canvas.width = rect.width - left - right;
250
+ this.canvas.height = rect.height - top - bottom;
245
251
  };
246
252
  const observer = new ResizeObserver(() => {
247
253
  resizeCanvas();
@@ -5,6 +5,7 @@
5
5
  * This module exports a collection of pre-built GameObject classes that provide
6
6
  * specific functionality commonly needed in games:
7
7
  * - {@link Scene}: A container for organizing hierarchies of GameObjects
8
+ * - {@link Sprite}: A MovieClip-style GameObject with frame-by-frame timeline animation
8
9
  * - {@link Text}: A GameObject for rendering text with various styling options
9
10
  *
10
11
  * All classes in this module extend the core {@link GameObject} class and inherit
@@ -48,7 +49,9 @@ export { GameObjectShapeWrapper, ShapeGOFactory } from "./wrapper.js";
48
49
  export { Scene } from "./scene.js";
49
50
  export { Scene3D } from "./scene3d.js";
50
51
  export { IsometricScene } from "./isometric-scene.js";
52
+ export { PlatformerScene } from "./platformer-scene.js";
51
53
  export * from "./layoutscene.js";
52
54
  // Specialized GameObjects
55
+ export { Sprite } from "./sprite.js";
53
56
  export { Text } from "./text.js";
54
57
  export { ImageGo } from "./imagego.js";
@@ -0,0 +1,411 @@
1
+ import { Scene } from "./scene.js";
2
+ import { Camera2D } from "../../util/camera2d.js";
3
+ import { Painter } from "../../painter/painter.js";
4
+ import { Keys } from "../../io/keys.js";
5
+
6
+ /**
7
+ * PlatformerScene - A scene optimized for side-scrolling platformer games
8
+ *
9
+ * Provides:
10
+ * - Parallax layer support with configurable scroll speeds
11
+ * - Automatic gravity and input handling for the player
12
+ * - Camera integration with smooth following
13
+ * - Viewport clipping
14
+ * - Override hooks for customization
15
+ *
16
+ * @example
17
+ * // Basic usage
18
+ * const level = new PlatformerScene(game, {
19
+ * player: playerGameObject,
20
+ * gravity: 1200,
21
+ * groundY: 500,
22
+ * });
23
+ *
24
+ * // Add parallax layers
25
+ * level.addLayer(background, { speed: 0.3 }); // slow parallax
26
+ * level.addLayer(platforms, { speed: 1.0 }); // normal
27
+ * level.addLayer(foreground, { speed: 1.5 }); // fast parallax
28
+ *
29
+ * // Add player (not as layer, moves with camera)
30
+ * level.add(player);
31
+ *
32
+ * this.pipeline.add(level);
33
+ *
34
+ * @example
35
+ * // Custom camera behavior (endless runner)
36
+ * class EndlessRunnerScene extends PlatformerScene {
37
+ * updateCamera(dt) {
38
+ * // No camera following - fixed viewport
39
+ * }
40
+ *
41
+ * getCameraOffset() {
42
+ * return { x: 0, y: 0 };
43
+ * }
44
+ * }
45
+ *
46
+ * @extends Scene
47
+ */
48
+ export class PlatformerScene extends Scene {
49
+ /**
50
+ * @param {Game} game - Game instance
51
+ * @param {Object} options - Configuration
52
+ * @param {GameObject} [options.player=null] - Player GameObject with x, y; will add vx, vy, _grounded
53
+ * @param {Camera2D} [options.camera] - Optional Camera2D (created automatically if player provided)
54
+ * @param {number} [options.viewportWidth] - Viewport width (defaults to game width)
55
+ * @param {number} [options.viewportHeight] - Viewport height (defaults to game height)
56
+ * @param {number} [options.gravity=1200] - Gravity acceleration (pixels/second^2)
57
+ * @param {number} [options.groundY=null] - Default ground Y level (null = no default ground)
58
+ * @param {number} [options.moveSpeed=300] - Horizontal movement speed (pixels/second)
59
+ * @param {number} [options.jumpVelocity=-500] - Initial jump velocity (negative = up)
60
+ * @param {boolean} [options.autoInput=true] - Auto-apply WASD/Arrow input
61
+ * @param {boolean} [options.autoGravity=true] - Auto-apply gravity to player
62
+ */
63
+ constructor(game, options = {}) {
64
+ super(game, options);
65
+
66
+ /** @type {GameObject|null} The player GameObject */
67
+ this.player = options.player ?? null;
68
+
69
+ /** @type {Array<{gameObject: GameObject, speed: number, offsetX: number, offsetY: number}>} */
70
+ this._layers = [];
71
+
72
+ // Physics configuration
73
+ /** @type {number} Gravity acceleration in pixels/second^2 */
74
+ this.gravity = options.gravity ?? 1200;
75
+
76
+ /** @type {number|null} Default ground Y level (null = no ground) */
77
+ this.groundY = options.groundY ?? null;
78
+
79
+ /** @type {number} Horizontal movement speed in pixels/second */
80
+ this.moveSpeed = options.moveSpeed ?? 300;
81
+
82
+ /** @type {number} Initial jump velocity (negative = up) */
83
+ this.jumpVelocity = options.jumpVelocity ?? -500;
84
+
85
+ // Feature toggles
86
+ /** @type {boolean} Whether to auto-apply WASD/Arrow input */
87
+ this.autoInput = options.autoInput ?? true;
88
+
89
+ /** @type {boolean} Whether to auto-apply gravity to player */
90
+ this.autoGravity = options.autoGravity ?? true;
91
+
92
+ // Viewport dimensions
93
+ /** @type {number|null} Viewport width (null = use game width) */
94
+ this._viewportWidth = options.viewportWidth ?? null;
95
+
96
+ /** @type {number|null} Viewport height (null = use game height) */
97
+ this._viewportHeight = options.viewportHeight ?? null;
98
+
99
+ // Initialize player physics properties if not present
100
+ if (this.player) {
101
+ if (this.player.vx === undefined) this.player.vx = 0;
102
+ if (this.player.vy === undefined) this.player.vy = 0;
103
+ if (this.player._grounded === undefined) this.player._grounded = true;
104
+ }
105
+
106
+ // Camera - create default if player provided and no camera given
107
+ if (options.camera) {
108
+ this.camera = options.camera;
109
+ } else if (this.player) {
110
+ this.camera = new Camera2D({
111
+ target: this.player,
112
+ viewportWidth: this._viewportWidth ?? game.width,
113
+ viewportHeight: this._viewportHeight ?? game.height,
114
+ lerp: 0.1,
115
+ });
116
+ } else {
117
+ this.camera = null;
118
+ }
119
+ }
120
+
121
+ // ==================== Layer API ====================
122
+
123
+ /**
124
+ * Add a parallax layer to the scene
125
+ * @param {GameObject} gameObject - The layer content
126
+ * @param {Object} options - Layer options
127
+ * @param {number} [options.speed=1] - Scroll speed multiplier (0=fixed, 0.5=slow, 1=normal, 1.5=fast)
128
+ * @param {number} [options.offsetX=0] - Fixed X offset
129
+ * @param {number} [options.offsetY=0] - Fixed Y offset
130
+ * @returns {GameObject} The added game object
131
+ */
132
+ addLayer(gameObject, options = {}) {
133
+ const layer = {
134
+ gameObject,
135
+ speed: options.speed ?? 1,
136
+ offsetX: options.offsetX ?? 0,
137
+ offsetY: options.offsetY ?? 0,
138
+ };
139
+
140
+ this._layers.push(layer);
141
+
142
+ // Also add to scene's collection for update calls
143
+ this.add(gameObject);
144
+
145
+ return gameObject;
146
+ }
147
+
148
+ /**
149
+ * Remove a layer from the scene
150
+ * @param {GameObject} gameObject - The layer to remove
151
+ * @returns {boolean} True if layer was found and removed
152
+ */
153
+ removeLayer(gameObject) {
154
+ const index = this._layers.findIndex((l) => l.gameObject === gameObject);
155
+ if (index !== -1) {
156
+ this._layers.splice(index, 1);
157
+ this.remove(gameObject);
158
+ return true;
159
+ }
160
+ return false;
161
+ }
162
+
163
+ /**
164
+ * Get all layers
165
+ * @returns {Array<{gameObject: GameObject, speed: number, offsetX: number, offsetY: number}>}
166
+ */
167
+ getLayers() {
168
+ return [...this._layers];
169
+ }
170
+
171
+ /**
172
+ * Check if a game object is a layer
173
+ * @param {GameObject} gameObject
174
+ * @returns {boolean}
175
+ */
176
+ isLayer(gameObject) {
177
+ return this._layers.some((l) => l.gameObject === gameObject);
178
+ }
179
+
180
+ // ==================== Override Hooks ====================
181
+
182
+ /**
183
+ * Apply gravity to the player
184
+ * Override this method to customize gravity behavior
185
+ * @param {GameObject} player - The player object
186
+ * @param {number} dt - Delta time in seconds
187
+ */
188
+ applyGravity(player, dt) {
189
+ player.vy = (player.vy || 0) + this.gravity * dt;
190
+ }
191
+
192
+ /**
193
+ * Apply input to the player
194
+ * Override this method to customize input handling
195
+ * @param {GameObject} player - The player object
196
+ * @param {number} dt - Delta time in seconds
197
+ */
198
+ applyInput(player, dt) {
199
+ // Horizontal movement
200
+ let moveX = 0;
201
+ if (Keys.isDown(Keys.LEFT) || Keys.isDown(Keys.A)) {
202
+ moveX = -1;
203
+ } else if (Keys.isDown(Keys.RIGHT) || Keys.isDown(Keys.D)) {
204
+ moveX = 1;
205
+ }
206
+ player.vx = moveX * this.moveSpeed;
207
+
208
+ // Jump
209
+ const jumpPressed =
210
+ Keys.isDown(Keys.SPACE) || Keys.isDown(Keys.W) || Keys.isDown(Keys.UP);
211
+ if (jumpPressed && this.isPlayerGrounded()) {
212
+ player.vy = this.jumpVelocity;
213
+ player._grounded = false;
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Update the camera
219
+ * Override this method to customize camera behavior
220
+ * @param {number} dt - Delta time in seconds
221
+ */
222
+ updateCamera(dt) {
223
+ if (this.camera) {
224
+ this.camera.update(dt);
225
+ }
226
+ }
227
+
228
+ /**
229
+ * Get the current camera offset for rendering
230
+ * Override this method to customize scroll behavior
231
+ * @returns {{x: number, y: number}}
232
+ */
233
+ getCameraOffset() {
234
+ if (this.camera) {
235
+ return this.camera.getOffset();
236
+ }
237
+ return { x: 0, y: 0 };
238
+ }
239
+
240
+ /**
241
+ * Check if player is on the ground
242
+ * Override for custom ground detection (platforms, etc.)
243
+ * @returns {boolean}
244
+ */
245
+ isPlayerGrounded() {
246
+ return this.player?._grounded === true;
247
+ }
248
+
249
+ /**
250
+ * Apply velocity to player position
251
+ * Override for custom movement physics
252
+ * @param {GameObject} player - The player object
253
+ * @param {number} dt - Delta time in seconds
254
+ */
255
+ applyVelocity(player, dt) {
256
+ player.x += (player.vx || 0) * dt;
257
+ player.y += (player.vy || 0) * dt;
258
+ }
259
+
260
+ /**
261
+ * Handle ground collision for player
262
+ * Override for custom ground collision behavior
263
+ * @param {GameObject} player - The player object
264
+ */
265
+ handleGroundCollision(player) {
266
+ if (this.groundY !== null && player.y >= this.groundY) {
267
+ player.y = this.groundY;
268
+ player.vy = 0;
269
+ player._grounded = true;
270
+ }
271
+ }
272
+
273
+ // ==================== Update ====================
274
+
275
+ /**
276
+ * Update method - applies physics, input, and camera following
277
+ * @param {number} dt - Delta time in seconds
278
+ */
279
+ update(dt) {
280
+ if (this.player) {
281
+ // Apply gravity (if enabled)
282
+ if (this.autoGravity) {
283
+ this.applyGravity(this.player, dt);
284
+ }
285
+
286
+ // Apply input (if enabled)
287
+ if (this.autoInput) {
288
+ this.applyInput(this.player, dt);
289
+ }
290
+
291
+ // Apply velocity to position
292
+ this.applyVelocity(this.player, dt);
293
+
294
+ // Handle ground collision
295
+ this.handleGroundCollision(this.player);
296
+ }
297
+
298
+ // Update camera
299
+ this.updateCamera(dt);
300
+
301
+ // Update all children (layers and non-layer children)
302
+ super.update(dt);
303
+ }
304
+
305
+ // ==================== Rendering ====================
306
+
307
+ /**
308
+ * Draw with viewport clipping and parallax
309
+ */
310
+ draw() {
311
+ // Apply scene transforms (rotation, scale, etc.)
312
+ this.applyTransforms();
313
+
314
+ // Draw debug bounds if enabled
315
+ this.drawDebug();
316
+
317
+ // Get viewport dimensions
318
+ const viewportW = this._viewportWidth ?? this.game.width;
319
+ const viewportH = this._viewportHeight ?? this.game.height;
320
+
321
+ // Get camera offset
322
+ const offset = this.getCameraOffset();
323
+
324
+ Painter.save();
325
+
326
+ // Clip to viewport
327
+ // Account for scene position - clip in world coordinates
328
+ Painter.ctx.beginPath();
329
+ Painter.ctx.rect(-this.x, -this.y, viewportW, viewportH);
330
+ Painter.ctx.clip();
331
+ Painter.ctx.beginPath();
332
+
333
+ // Translate to world origin so children render at their world positions
334
+ Painter.ctx.translate(-this.x, -this.y);
335
+
336
+ // Render layers with parallax
337
+ for (const layer of this._layers) {
338
+ if (!layer.gameObject.visible) continue;
339
+
340
+ Painter.save();
341
+
342
+ // Apply parallax offset based on layer speed
343
+ const parallaxX = offset.x * layer.speed + (layer.offsetX || 0);
344
+ const parallaxY = offset.y * layer.speed + (layer.offsetY || 0);
345
+
346
+ Painter.ctx.translate(-parallaxX, -parallaxY);
347
+
348
+ // Render the layer
349
+ layer.gameObject.render();
350
+
351
+ Painter.restore();
352
+ }
353
+
354
+ // Render non-layer children (like player, enemies) with full camera offset
355
+ for (const child of this._collection.getSortedChildren()) {
356
+ if (!child.visible) continue;
357
+ // Skip if this child is a layer (already rendered above)
358
+ if (this._layers.some((l) => l.gameObject === child)) continue;
359
+
360
+ Painter.save();
361
+ // Apply full camera offset
362
+ Painter.ctx.translate(-offset.x, -offset.y);
363
+ child.render();
364
+ Painter.restore();
365
+ }
366
+
367
+ Painter.restore();
368
+ }
369
+
370
+ // ==================== Utilities ====================
371
+
372
+ /**
373
+ * Shake the camera
374
+ * @param {number} intensity - Shake amount in pixels
375
+ * @param {number} duration - Shake duration in seconds
376
+ * @returns {PlatformerScene} this for chaining
377
+ */
378
+ shakeCamera(intensity, duration) {
379
+ if (this.camera) {
380
+ this.camera.shake(intensity, duration);
381
+ }
382
+ return this;
383
+ }
384
+
385
+ /**
386
+ * Set viewport dimensions
387
+ * @param {number} width - Viewport width
388
+ * @param {number} height - Viewport height
389
+ * @returns {PlatformerScene} this for chaining
390
+ */
391
+ setViewport(width, height) {
392
+ this._viewportWidth = width;
393
+ this._viewportHeight = height;
394
+ if (this.camera) {
395
+ this.camera.viewportWidth = width;
396
+ this.camera.viewportHeight = height;
397
+ }
398
+ return this;
399
+ }
400
+
401
+ /**
402
+ * Get viewport dimensions
403
+ * @returns {{width: number, height: number}}
404
+ */
405
+ getViewport() {
406
+ return {
407
+ width: this._viewportWidth ?? this.game.width,
408
+ height: this._viewportHeight ?? this.game.height,
409
+ };
410
+ }
411
+ }
@@ -147,6 +147,20 @@ export class Scene extends GameObject {
147
147
  };
148
148
  }
149
149
 
150
+ /**
151
+ * Returns the scene's bounding box.
152
+ * Required for hit testing when the scene is interactive.
153
+ * @returns {{x: number, y: number, width: number, height: number}}
154
+ */
155
+ getBounds() {
156
+ return {
157
+ x: this.x,
158
+ y: this.y,
159
+ width: this._width || 0,
160
+ height: this._height || 0,
161
+ };
162
+ }
163
+
150
164
  bringToFront(go) {
151
165
  return this._collection.bringToFront(go);
152
166
  }