@guinetik/gcanvas 1.0.4 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (193) hide show
  1. package/dist/CNAME +1 -0
  2. package/dist/animations.html +31 -0
  3. package/dist/basic.html +38 -0
  4. package/dist/baskara.html +31 -0
  5. package/dist/bezier.html +35 -0
  6. package/dist/beziersignature.html +29 -0
  7. package/dist/blackhole.html +28 -0
  8. package/dist/blob.html +35 -0
  9. package/dist/coordinates.html +698 -0
  10. package/dist/cube3d.html +23 -0
  11. package/dist/demos.css +303 -0
  12. package/dist/dino.html +42 -0
  13. package/dist/easing.html +28 -0
  14. package/dist/events.html +195 -0
  15. package/dist/fluent.html +647 -0
  16. package/dist/fluid-simple.html +22 -0
  17. package/dist/fluid.html +37 -0
  18. package/dist/fractals.html +36 -0
  19. package/dist/gameobjects.html +626 -0
  20. package/dist/gcanvas.es.js +517 -0
  21. package/dist/gcanvas.es.min.js +1 -1
  22. package/dist/gcanvas.umd.js +1 -1
  23. package/dist/gcanvas.umd.min.js +1 -1
  24. package/dist/genart.html +26 -0
  25. package/dist/gendream.html +26 -0
  26. package/dist/group.html +36 -0
  27. package/dist/home.html +587 -0
  28. package/dist/hyperbolic001.html +23 -0
  29. package/dist/hyperbolic002.html +23 -0
  30. package/dist/hyperbolic003.html +23 -0
  31. package/dist/hyperbolic004.html +23 -0
  32. package/dist/hyperbolic005.html +22 -0
  33. package/dist/index.html +398 -0
  34. package/dist/isometric.html +34 -0
  35. package/dist/js/animations.js +452 -0
  36. package/dist/js/basic.js +204 -0
  37. package/dist/js/baskara.js +751 -0
  38. package/dist/js/bezier.js +692 -0
  39. package/dist/js/beziersignature.js +241 -0
  40. package/dist/js/blackhole/accretiondisk.obj.js +379 -0
  41. package/dist/js/blackhole/blackhole.obj.js +318 -0
  42. package/dist/js/blackhole/index.js +409 -0
  43. package/dist/js/blackhole/particle.js +56 -0
  44. package/dist/js/blackhole/starfield.obj.js +218 -0
  45. package/dist/js/blob.js +2276 -0
  46. package/dist/js/coordinates.js +840 -0
  47. package/dist/js/cube3d.js +789 -0
  48. package/dist/js/dino.js +1420 -0
  49. package/dist/js/easing.js +477 -0
  50. package/dist/js/fluent.js +183 -0
  51. package/dist/js/fluid-simple.js +253 -0
  52. package/dist/js/fluid.js +527 -0
  53. package/dist/js/fractals.js +932 -0
  54. package/dist/js/fractalworker.js +93 -0
  55. package/dist/js/gameobjects.js +176 -0
  56. package/dist/js/genart.js +268 -0
  57. package/dist/js/gendream.js +209 -0
  58. package/dist/js/group.js +140 -0
  59. package/dist/js/hyperbolic001.js +310 -0
  60. package/dist/js/hyperbolic002.js +388 -0
  61. package/dist/js/hyperbolic003.js +319 -0
  62. package/dist/js/hyperbolic004.js +345 -0
  63. package/dist/js/hyperbolic005.js +340 -0
  64. package/dist/js/info-toggle.js +25 -0
  65. package/dist/js/isometric.js +863 -0
  66. package/dist/js/kerr.js +1547 -0
  67. package/dist/js/lavalamp.js +590 -0
  68. package/dist/js/layout.js +354 -0
  69. package/dist/js/mondrian.js +285 -0
  70. package/dist/js/opacity.js +275 -0
  71. package/dist/js/painter.js +484 -0
  72. package/dist/js/particles-showcase.js +514 -0
  73. package/dist/js/particles.js +299 -0
  74. package/dist/js/patterns.js +397 -0
  75. package/dist/js/penrose/artifact.js +69 -0
  76. package/dist/js/penrose/blackhole.js +121 -0
  77. package/dist/js/penrose/constants.js +73 -0
  78. package/dist/js/penrose/game.js +943 -0
  79. package/dist/js/penrose/lore.js +278 -0
  80. package/dist/js/penrose/penrosescene.js +892 -0
  81. package/dist/js/penrose/ship.js +216 -0
  82. package/dist/js/penrose/sounds.js +211 -0
  83. package/dist/js/penrose/voidparticle.js +55 -0
  84. package/dist/js/penrose/voidscene.js +258 -0
  85. package/dist/js/penrose/voidship.js +144 -0
  86. package/dist/js/penrose/wormhole.js +46 -0
  87. package/dist/js/pipeline.js +555 -0
  88. package/dist/js/plane3d.js +256 -0
  89. package/dist/js/platformer.js +1579 -0
  90. package/dist/js/scene.js +304 -0
  91. package/dist/js/scenes.js +320 -0
  92. package/dist/js/schrodinger.js +410 -0
  93. package/dist/js/schwarzschild.js +1015 -0
  94. package/dist/js/shapes.js +628 -0
  95. package/dist/js/space/alien.js +171 -0
  96. package/dist/js/space/boom.js +98 -0
  97. package/dist/js/space/boss.js +353 -0
  98. package/dist/js/space/buff.js +73 -0
  99. package/dist/js/space/bullet.js +102 -0
  100. package/dist/js/space/constants.js +85 -0
  101. package/dist/js/space/game.js +1884 -0
  102. package/dist/js/space/hud.js +112 -0
  103. package/dist/js/space/laserbeam.js +179 -0
  104. package/dist/js/space/lightning.js +277 -0
  105. package/dist/js/space/minion.js +192 -0
  106. package/dist/js/space/missile.js +212 -0
  107. package/dist/js/space/player.js +430 -0
  108. package/dist/js/space/powerup.js +90 -0
  109. package/dist/js/space/starfield.js +58 -0
  110. package/dist/js/space/starpower.js +90 -0
  111. package/dist/js/spacetime.js +559 -0
  112. package/dist/js/sphere3d.js +229 -0
  113. package/dist/js/sprite.js +473 -0
  114. package/dist/js/starfaux/config.js +118 -0
  115. package/dist/js/starfaux/enemy.js +353 -0
  116. package/dist/js/starfaux/hud.js +78 -0
  117. package/dist/js/starfaux/index.js +482 -0
  118. package/dist/js/starfaux/laser.js +182 -0
  119. package/dist/js/starfaux/player.js +468 -0
  120. package/dist/js/starfaux/terrain.js +560 -0
  121. package/dist/js/study001.js +275 -0
  122. package/dist/js/study002.js +366 -0
  123. package/dist/js/study003.js +331 -0
  124. package/dist/js/study004.js +389 -0
  125. package/dist/js/study005.js +209 -0
  126. package/dist/js/study006.js +194 -0
  127. package/dist/js/study007.js +192 -0
  128. package/dist/js/study008.js +413 -0
  129. package/dist/js/svgtween.js +204 -0
  130. package/dist/js/tde/accretiondisk.js +471 -0
  131. package/dist/js/tde/blackhole.js +219 -0
  132. package/dist/js/tde/blackholescene.js +209 -0
  133. package/dist/js/tde/config.js +59 -0
  134. package/dist/js/tde/index.js +820 -0
  135. package/dist/js/tde/jets.js +290 -0
  136. package/dist/js/tde/lensedstarfield.js +154 -0
  137. package/dist/js/tde/tdestar.js +297 -0
  138. package/dist/js/tde/tidalstream.js +372 -0
  139. package/dist/js/tde_old/blackhole.obj.js +354 -0
  140. package/dist/js/tde_old/debris.obj.js +791 -0
  141. package/dist/js/tde_old/flare.obj.js +239 -0
  142. package/dist/js/tde_old/index.js +448 -0
  143. package/dist/js/tde_old/star.obj.js +812 -0
  144. package/dist/js/tetris/config.js +157 -0
  145. package/dist/js/tetris/grid.js +286 -0
  146. package/dist/js/tetris/index.js +1195 -0
  147. package/dist/js/tetris/renderer.js +634 -0
  148. package/dist/js/tetris/tetrominos.js +280 -0
  149. package/dist/js/tiles.js +312 -0
  150. package/dist/js/tweendemo.js +79 -0
  151. package/dist/js/visibility.js +102 -0
  152. package/dist/kerr.html +28 -0
  153. package/dist/lavalamp.html +27 -0
  154. package/dist/layouts.html +37 -0
  155. package/dist/logo.svg +4 -0
  156. package/dist/loop.html +84 -0
  157. package/dist/mondrian.html +32 -0
  158. package/dist/og_image.png +0 -0
  159. package/dist/opacity.html +36 -0
  160. package/dist/painter.html +39 -0
  161. package/dist/particles-showcase.html +28 -0
  162. package/dist/particles.html +24 -0
  163. package/dist/patterns.html +33 -0
  164. package/dist/penrose-game.html +31 -0
  165. package/dist/pipeline.html +737 -0
  166. package/dist/plane3d.html +24 -0
  167. package/dist/platformer.html +43 -0
  168. package/dist/scene.html +33 -0
  169. package/dist/scenes.html +96 -0
  170. package/dist/schrodinger.html +27 -0
  171. package/dist/schwarzschild.html +27 -0
  172. package/dist/shapes.html +16 -0
  173. package/dist/space.html +85 -0
  174. package/dist/spacetime.html +27 -0
  175. package/dist/sphere3d.html +24 -0
  176. package/dist/sprite.html +18 -0
  177. package/dist/starfaux.html +22 -0
  178. package/dist/study001.html +23 -0
  179. package/dist/study002.html +23 -0
  180. package/dist/study003.html +23 -0
  181. package/dist/study004.html +23 -0
  182. package/dist/study005.html +22 -0
  183. package/dist/study006.html +24 -0
  184. package/dist/study007.html +24 -0
  185. package/dist/study008.html +22 -0
  186. package/dist/svgtween.html +29 -0
  187. package/dist/tde.html +28 -0
  188. package/dist/tetris3d.html +25 -0
  189. package/dist/tiles.html +28 -0
  190. package/dist/transforms.html +400 -0
  191. package/dist/tween.html +45 -0
  192. package/dist/visibility.html +33 -0
  193. package/package.json +1 -1
@@ -0,0 +1,468 @@
1
+ /**
2
+ * Player - The Arwing-style player ship for StarFaux
3
+ *
4
+ * The ship is viewed from BEHIND (3rd person), positioned in the
5
+ * lower portion of the screen. Player moves left/right/up/down only.
6
+ * Crosshair is locked to ship position - lasers fire straight ahead.
7
+ */
8
+
9
+ import { Keys, Painter } from "/gcanvas.es.min.js";
10
+ import { CONFIG } from "./config.js";
11
+
12
+ export class Player {
13
+ constructor(game, camera) {
14
+ this.game = game;
15
+ this.camera = camera;
16
+
17
+ // Ship position offset from center (keyboard controlled)
18
+ this.offsetX = 0;
19
+ this.offsetY = 0;
20
+
21
+ // Velocity for physics-based movement
22
+ this.velocityX = 0;
23
+ this.velocityY = 0;
24
+
25
+ // Visual banking (rotation based on horizontal movement)
26
+ this.bankAngle = 0;
27
+ this.targetBank = 0;
28
+
29
+ // Pitch angle (tilt up/down based on vertical movement)
30
+ this.pitchAngle = 0;
31
+
32
+ // Shooting
33
+ this.fireTimer = 0;
34
+
35
+ // Health and invincibility
36
+ this.health = CONFIG.player.maxHealth;
37
+ this.isInvincible = false;
38
+ this.invincibilityTimer = 0;
39
+
40
+ // Visual flash when invincible
41
+ this.flashTimer = 0;
42
+ this.visible = true;
43
+
44
+ // Calculate scaled bounds
45
+ this.updateBounds();
46
+ }
47
+
48
+ /**
49
+ * Update movement bounds based on current screen size
50
+ */
51
+ updateBounds() {
52
+ const scale = this.game.scaleFactor || 1;
53
+ this.boundsX = CONFIG.player.bounds.x * scale;
54
+ this.boundsY = CONFIG.player.bounds.y * scale;
55
+ }
56
+
57
+ update(dt) {
58
+ const cfg = CONFIG.player;
59
+ const scale = this.game.scaleFactor || 1;
60
+ const accel = cfg.acceleration * scale;
61
+ const maxSpeed = cfg.moveSpeed * scale;
62
+
63
+ // Track input for banking
64
+ this.targetBank = 0;
65
+ let inputX = 0;
66
+ let inputY = 0;
67
+
68
+ // Handle movement input - builds acceleration, not direct position
69
+ if (Keys.isDown(Keys.LEFT) || Keys.isDown(Keys.A)) {
70
+ inputX = -1;
71
+ this.targetBank = -cfg.bankAngle;
72
+ }
73
+ if (Keys.isDown(Keys.RIGHT) || Keys.isDown(Keys.D)) {
74
+ inputX = 1;
75
+ this.targetBank = cfg.bankAngle;
76
+ }
77
+ if (Keys.isDown(Keys.UP) || Keys.isDown(Keys.W)) {
78
+ inputY = -1; // Up = negative Y
79
+ }
80
+ if (Keys.isDown(Keys.DOWN) || Keys.isDown(Keys.S)) {
81
+ inputY = 1; // Down = positive Y
82
+ }
83
+
84
+ // Apply acceleration based on input
85
+ if (inputX !== 0) {
86
+ this.velocityX += inputX * accel * dt;
87
+ }
88
+ if (inputY !== 0) {
89
+ // Climbing (going up, inputY = -1) is harder than falling
90
+ const resistance = inputY < 0 ? cfg.climbResistance : 1.0;
91
+ this.velocityY += inputY * accel * resistance * dt;
92
+ }
93
+
94
+ // Apply gravity (always pulls down = positive Y)
95
+ this.velocityY += cfg.gravity * scale * dt;
96
+
97
+ // Apply damping (drag) - velocity decays when no input
98
+ if (inputX === 0) {
99
+ this.velocityX *= cfg.damping;
100
+ }
101
+ if (inputY === 0) {
102
+ this.velocityY *= cfg.damping;
103
+ }
104
+
105
+ // Clamp velocity to max speed
106
+ this.velocityX = Math.max(-maxSpeed, Math.min(maxSpeed, this.velocityX));
107
+ this.velocityY = Math.max(-maxSpeed, Math.min(maxSpeed, this.velocityY));
108
+
109
+ // Apply velocity to position
110
+ this.offsetX += this.velocityX * dt;
111
+ this.offsetY += this.velocityY * dt;
112
+
113
+ // Clamp position to bounds (with velocity bounce-back)
114
+ if (this.offsetX < -this.boundsX) {
115
+ this.offsetX = -this.boundsX;
116
+ this.velocityX *= -0.3; // Soft bounce
117
+ } else if (this.offsetX > this.boundsX) {
118
+ this.offsetX = this.boundsX;
119
+ this.velocityX *= -0.3;
120
+ }
121
+ if (this.offsetY < -this.boundsY) {
122
+ this.offsetY = -this.boundsY;
123
+ this.velocityY *= -0.3;
124
+ } else if (this.offsetY > this.boundsY) {
125
+ this.offsetY = this.boundsY;
126
+ this.velocityY *= -0.3;
127
+ }
128
+
129
+ // Smooth banking interpolation (also affected by velocity for more natural feel)
130
+ const velocityBank = (this.velocityX / maxSpeed) * cfg.bankAngle * 0.5;
131
+ this.bankAngle += (this.targetBank + velocityBank - this.bankAngle) * cfg.bankSpeed * dt;
132
+
133
+ // Pitch based on INPUT, not velocity - returns to neutral when keys released
134
+ const maxPitch = 0.35; // Max pitch angle in radians
135
+ const targetPitch = -inputY * maxPitch; // -1 (up key) = positive pitch (nose up), 1 (down key) = negative pitch (nose down)
136
+ this.pitchAngle += (targetPitch - this.pitchAngle) * cfg.bankSpeed * dt;
137
+
138
+ // Handle shooting
139
+ this.fireTimer -= dt;
140
+ if (Keys.isDown(Keys.SPACE) && this.fireTimer <= 0) {
141
+ this.shoot();
142
+ this.fireTimer = cfg.fireRate;
143
+ }
144
+
145
+ // Handle invincibility
146
+ if (this.isInvincible) {
147
+ this.invincibilityTimer -= dt;
148
+ this.flashTimer += dt;
149
+
150
+ // Flash visibility based on config blink rate
151
+ const blinkRate = CONFIG.player.blinkRate || 0.1;
152
+ this.visible = Math.floor(this.flashTimer / blinkRate) % 2 === 0;
153
+
154
+ if (this.invincibilityTimer <= 0) {
155
+ this.isInvincible = false;
156
+ this.visible = true;
157
+ }
158
+ }
159
+ }
160
+
161
+ shoot() {
162
+ // Fire from ship position straight ahead
163
+ const cfg = CONFIG.player;
164
+ const resScale = this.game.scaleFactor || 1;
165
+ const shipY = this.offsetY + cfg.screenY * resScale;
166
+ this.game.fireLaser(this.offsetX, shipY);
167
+ }
168
+
169
+ takeDamage() {
170
+ if (this.isInvincible) return;
171
+
172
+ this.health--;
173
+ this.isInvincible = true;
174
+ this.invincibilityTimer = CONFIG.player.invincibilityTime || 1.5;
175
+ this.flashTimer = 0;
176
+ }
177
+
178
+ reset() {
179
+ this.offsetX = 0;
180
+ this.offsetY = 0;
181
+ this.velocityX = 0;
182
+ this.velocityY = 0;
183
+ this.bankAngle = 0;
184
+ this.pitchAngle = 0;
185
+ this.health = CONFIG.player.maxHealth;
186
+ this.isInvincible = false;
187
+ this.visible = true;
188
+ }
189
+
190
+ render() {
191
+ if (!this.visible) return;
192
+
193
+ const ctx = Painter.ctx;
194
+ const cfg = CONFIG.player;
195
+
196
+ // First render the shadow (on ground plane)
197
+ this.renderShadow(ctx);
198
+
199
+ // Then render the crosshair (ahead of ship, where we're aiming)
200
+ this.renderCrosshair(ctx);
201
+
202
+ // Then render the ship (behind crosshair, lower on screen)
203
+ this.renderShip(ctx);
204
+ }
205
+
206
+ /**
207
+ * Render shadow on the ground plane
208
+ */
209
+ renderShadow(ctx) {
210
+ const camera = this.camera;
211
+ const cfg = CONFIG.player;
212
+ const resScale = this.game.scaleFactor || 1;
213
+
214
+ // Shadow is projected onto ground (y = 0)
215
+ const groundY = 0;
216
+ const shadowZ = cfg.shipZ;
217
+
218
+ // Project shadow position
219
+ const projected = camera.project(this.offsetX, groundY, shadowZ);
220
+
221
+ if (projected.scale <= 0) return;
222
+
223
+ // Shadow size scales with ship height above ground
224
+ // Higher ship = smaller, more diffuse shadow
225
+ const shipY = this.offsetY + cfg.screenY * resScale;
226
+ const heightAboveGround = Math.abs(shipY - groundY);
227
+ const shadowScale = Math.max(0.3, 1 - heightAboveGround / 400);
228
+ const shadowSize = cfg.size * projected.scale * resScale * shadowScale;
229
+
230
+ // Shadow opacity also fades with height
231
+ const shadowAlpha = Math.max(0.1, 0.5 * shadowScale);
232
+
233
+ ctx.save();
234
+ ctx.globalAlpha = shadowAlpha;
235
+ ctx.fillStyle = "#333333"; // Gray shadow for better visibility
236
+
237
+ // Draw ellipse shadow
238
+ ctx.beginPath();
239
+ ctx.ellipse(
240
+ projected.x,
241
+ projected.y,
242
+ shadowSize * 1.5, // Wider
243
+ shadowSize * 0.4, // Flatter (perspective)
244
+ 0, 0, Math.PI * 2
245
+ );
246
+ ctx.fill();
247
+ ctx.restore();
248
+ }
249
+
250
+ /**
251
+ * Render the targeting crosshair (locked to ship position)
252
+ */
253
+ renderCrosshair(ctx) {
254
+ const camera = this.camera;
255
+ const cfg = CONFIG.player;
256
+ const scale = this.game.scaleFactor || 1;
257
+
258
+ // Crosshair is at same X/Y as ship but further into the screen
259
+ const projected = camera.project(this.offsetX, this.offsetY, cfg.crosshairZ);
260
+
261
+ if (projected.scale <= 0) return;
262
+
263
+ // Scale crosshair size with resolution
264
+ const size = 25 * projected.scale * scale;
265
+
266
+ ctx.save();
267
+ ctx.translate(projected.x, projected.y);
268
+
269
+ ctx.strokeStyle = "rgba(255, 50, 50, 0.8)"; // Red crosshair
270
+ ctx.lineWidth = 2 * scale;
271
+
272
+ // Square bracket crosshair like StarFox
273
+ const half = size;
274
+ const corner = size * 0.3;
275
+
276
+ ctx.beginPath();
277
+ // Top-left corner
278
+ ctx.moveTo(-half, -half + corner);
279
+ ctx.lineTo(-half, -half);
280
+ ctx.lineTo(-half + corner, -half);
281
+ // Top-right corner
282
+ ctx.moveTo(half - corner, -half);
283
+ ctx.lineTo(half, -half);
284
+ ctx.lineTo(half, -half + corner);
285
+ // Bottom-right corner
286
+ ctx.moveTo(half, half - corner);
287
+ ctx.lineTo(half, half);
288
+ ctx.lineTo(half - corner, half);
289
+ // Bottom-left corner
290
+ ctx.moveTo(-half + corner, half);
291
+ ctx.lineTo(-half, half);
292
+ ctx.lineTo(-half, half - corner);
293
+ ctx.stroke();
294
+
295
+ ctx.restore();
296
+ }
297
+
298
+ /**
299
+ * Render the ship from behind (3rd person view)
300
+ */
301
+ renderShip(ctx) {
302
+ const camera = this.camera;
303
+ const cfg = CONFIG.player;
304
+ const resScale = this.game.scaleFactor || 1;
305
+
306
+ // Ship is closer to camera than crosshair, and offset down on screen
307
+ const shipY = this.offsetY + cfg.screenY * resScale; // Push ship down visually (scaled)
308
+ const projected = camera.project(this.offsetX, shipY, cfg.shipZ);
309
+
310
+ if (projected.scale <= 0) return;
311
+
312
+ ctx.save();
313
+ ctx.translate(projected.x, projected.y);
314
+
315
+ // Apply bank rotation (left/right tilt)
316
+ ctx.rotate(this.bankAngle);
317
+
318
+ // Apply both perspective scale AND resolution scale
319
+ const baseScale = projected.scale * resScale;
320
+ ctx.scale(baseScale, baseScale);
321
+
322
+ // Apply pitch by skewing the Y axis (simulates 3D tilt toward/away from camera)
323
+ // At neutral (pitchAngle = 0): no transform applied
324
+ // Pitching up (positive): ship leans back, appears compressed vertically
325
+ // Pitching down (negative): ship leans forward
326
+ if (Math.abs(this.pitchAngle) > 0.01) {
327
+ const pitchSkew = Math.sin(this.pitchAngle) * 0.4;
328
+ const pitchScale = 1 - Math.abs(this.pitchAngle) * 0.3; // Compress when pitched
329
+ ctx.transform(1, pitchSkew, 0, pitchScale, 0, 0);
330
+ }
331
+
332
+ // Draw ship from BEHIND (we see the back of it)
333
+ this.drawShipFromBehind(ctx);
334
+
335
+ ctx.restore();
336
+ }
337
+
338
+ /**
339
+ * Draw the ship as seen from behind
340
+ * We see the engines, back of wings, tail
341
+ */
342
+ drawShipFromBehind(ctx) {
343
+ const colors = CONFIG.colors;
344
+ const size = CONFIG.player.size;
345
+
346
+ // Main fuselage (back view - wider at back)
347
+ ctx.fillStyle = colors.player;
348
+ ctx.beginPath();
349
+ ctx.moveTo(0, -size * 0.8); // Top (nose pointing away)
350
+ ctx.lineTo(-size * 0.3, -size * 0.4); // Upper left
351
+ ctx.lineTo(-size * 0.4, size * 0.4); // Lower left
352
+ ctx.lineTo(0, size * 0.2); // Bottom center
353
+ ctx.lineTo(size * 0.4, size * 0.4); // Lower right
354
+ ctx.lineTo(size * 0.3, -size * 0.4); // Upper right
355
+ ctx.closePath();
356
+ ctx.fill();
357
+
358
+ // Left wing (seen from above/behind)
359
+ ctx.fillStyle = colors.playerAccent;
360
+ ctx.beginPath();
361
+ ctx.moveTo(-size * 0.35, -size * 0.2);
362
+ ctx.lineTo(-size * 1.3, size * 0.1);
363
+ ctx.lineTo(-size * 1.1, size * 0.3);
364
+ ctx.lineTo(-size * 0.4, size * 0.2);
365
+ ctx.closePath();
366
+ ctx.fill();
367
+
368
+ // Right wing
369
+ ctx.beginPath();
370
+ ctx.moveTo(size * 0.35, -size * 0.2);
371
+ ctx.lineTo(size * 1.3, size * 0.1);
372
+ ctx.lineTo(size * 1.1, size * 0.3);
373
+ ctx.lineTo(size * 0.4, size * 0.2);
374
+ ctx.closePath();
375
+ ctx.fill();
376
+
377
+ // Engine glows (3 engines like Arwing)
378
+ // Thruster intensity based on vertical movement
379
+ // Going UP (negative velocityY) = MORE thrust (climbing)
380
+ // Going DOWN (positive velocityY) = LESS thrust (falling/gliding)
381
+ const maxSpeed = CONFIG.player.moveSpeed * (this.game.scaleFactor || 1);
382
+ const thrustFactor = 1 - (this.velocityY / maxSpeed); // 0.5 to 1.5 range roughly
383
+ const thrustIntensity = Math.max(0.3, Math.min(2.0, thrustFactor));
384
+
385
+ // Flame length extends when thrusting hard
386
+ const flameLength = size * 0.15 * thrustIntensity;
387
+ const flameFlicker = 1 + Math.sin(Date.now() * 0.02) * 0.15; // Subtle flicker
388
+
389
+ // Color shifts from orange (idle) to bright yellow-white (full thrust)
390
+ const r = Math.min(255, Math.floor(255));
391
+ const g = Math.min(255, Math.floor(100 + thrustIntensity * 80));
392
+ const b = Math.min(255, Math.floor(thrustIntensity * 60));
393
+ const thrustColor = `rgb(${r}, ${g}, ${b})`;
394
+
395
+ ctx.fillStyle = thrustColor;
396
+ ctx.shadowColor = thrustIntensity > 1 ? "#ffff00" : "#ff8800";
397
+ ctx.shadowBlur = 10 + thrustIntensity * 10;
398
+
399
+ // Center engine (main thruster)
400
+ ctx.beginPath();
401
+ ctx.ellipse(0, size * 0.3, size * 0.15, size * 0.1 * flameFlicker, 0, 0, Math.PI * 2);
402
+ ctx.fill();
403
+
404
+ // Center engine flame trail (extends when climbing)
405
+ if (thrustIntensity > 0.8) {
406
+ const trailLength = flameLength * flameFlicker;
407
+ ctx.beginPath();
408
+ ctx.moveTo(-size * 0.1, size * 0.35);
409
+ ctx.lineTo(0, size * 0.35 + trailLength);
410
+ ctx.lineTo(size * 0.1, size * 0.35);
411
+ ctx.closePath();
412
+ ctx.fill();
413
+ }
414
+
415
+ // Left engine
416
+ ctx.beginPath();
417
+ ctx.ellipse(-size * 0.5, size * 0.25, size * 0.1, size * 0.07 * flameFlicker, 0, 0, Math.PI * 2);
418
+ ctx.fill();
419
+
420
+ // Left engine flame trail
421
+ if (thrustIntensity > 0.8) {
422
+ const trailLength = flameLength * 0.7 * flameFlicker;
423
+ ctx.beginPath();
424
+ ctx.moveTo(-size * 0.55, size * 0.28);
425
+ ctx.lineTo(-size * 0.5, size * 0.28 + trailLength);
426
+ ctx.lineTo(-size * 0.45, size * 0.28);
427
+ ctx.closePath();
428
+ ctx.fill();
429
+ }
430
+
431
+ // Right engine
432
+ ctx.beginPath();
433
+ ctx.ellipse(size * 0.5, size * 0.25, size * 0.1, size * 0.07 * flameFlicker, 0, 0, Math.PI * 2);
434
+ ctx.fill();
435
+
436
+ // Right engine flame trail
437
+ if (thrustIntensity > 0.8) {
438
+ const trailLength = flameLength * 0.7 * flameFlicker;
439
+ ctx.beginPath();
440
+ ctx.moveTo(size * 0.45, size * 0.28);
441
+ ctx.lineTo(size * 0.5, size * 0.28 + trailLength);
442
+ ctx.lineTo(size * 0.55, size * 0.28);
443
+ ctx.closePath();
444
+ ctx.fill();
445
+ }
446
+
447
+ ctx.shadowBlur = 0;
448
+
449
+ // Tail fin
450
+ ctx.fillStyle = colors.player;
451
+ ctx.beginPath();
452
+ ctx.moveTo(0, -size * 0.8);
453
+ ctx.lineTo(-size * 0.08, -size * 0.3);
454
+ ctx.lineTo(size * 0.08, -size * 0.3);
455
+ ctx.closePath();
456
+ ctx.fill();
457
+
458
+ // Wing detail lines
459
+ ctx.strokeStyle = colors.player;
460
+ ctx.lineWidth = 2;
461
+ ctx.beginPath();
462
+ ctx.moveTo(-size * 0.5, 0);
463
+ ctx.lineTo(-size * 1.1, size * 0.2);
464
+ ctx.moveTo(size * 0.5, 0);
465
+ ctx.lineTo(size * 1.1, size * 0.2);
466
+ ctx.stroke();
467
+ }
468
+ }