@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.
- package/dist/CNAME +1 -0
- package/dist/animations.html +31 -0
- package/dist/basic.html +38 -0
- package/dist/baskara.html +31 -0
- package/dist/bezier.html +35 -0
- package/dist/beziersignature.html +29 -0
- package/dist/blackhole.html +28 -0
- package/dist/blob.html +35 -0
- package/dist/coordinates.html +698 -0
- package/dist/cube3d.html +23 -0
- package/dist/demos.css +303 -0
- package/dist/dino.html +42 -0
- package/dist/easing.html +28 -0
- package/dist/events.html +195 -0
- package/dist/fluent.html +647 -0
- package/dist/fluid-simple.html +22 -0
- package/dist/fluid.html +37 -0
- package/dist/fractals.html +36 -0
- package/dist/gameobjects.html +626 -0
- package/dist/gcanvas.es.js +517 -0
- package/dist/gcanvas.es.min.js +1 -1
- package/dist/gcanvas.umd.js +1 -1
- package/dist/gcanvas.umd.min.js +1 -1
- package/dist/genart.html +26 -0
- package/dist/gendream.html +26 -0
- package/dist/group.html +36 -0
- package/dist/home.html +587 -0
- package/dist/hyperbolic001.html +23 -0
- package/dist/hyperbolic002.html +23 -0
- package/dist/hyperbolic003.html +23 -0
- package/dist/hyperbolic004.html +23 -0
- package/dist/hyperbolic005.html +22 -0
- package/dist/index.html +398 -0
- package/dist/isometric.html +34 -0
- package/dist/js/animations.js +452 -0
- package/dist/js/basic.js +204 -0
- package/dist/js/baskara.js +751 -0
- package/dist/js/bezier.js +692 -0
- package/dist/js/beziersignature.js +241 -0
- package/dist/js/blackhole/accretiondisk.obj.js +379 -0
- package/dist/js/blackhole/blackhole.obj.js +318 -0
- package/dist/js/blackhole/index.js +409 -0
- package/dist/js/blackhole/particle.js +56 -0
- package/dist/js/blackhole/starfield.obj.js +218 -0
- package/dist/js/blob.js +2276 -0
- package/dist/js/coordinates.js +840 -0
- package/dist/js/cube3d.js +789 -0
- package/dist/js/dino.js +1420 -0
- package/dist/js/easing.js +477 -0
- package/dist/js/fluent.js +183 -0
- package/dist/js/fluid-simple.js +253 -0
- package/dist/js/fluid.js +527 -0
- package/dist/js/fractals.js +932 -0
- package/dist/js/fractalworker.js +93 -0
- package/dist/js/gameobjects.js +176 -0
- package/dist/js/genart.js +268 -0
- package/dist/js/gendream.js +209 -0
- package/dist/js/group.js +140 -0
- package/dist/js/hyperbolic001.js +310 -0
- package/dist/js/hyperbolic002.js +388 -0
- package/dist/js/hyperbolic003.js +319 -0
- package/dist/js/hyperbolic004.js +345 -0
- package/dist/js/hyperbolic005.js +340 -0
- package/dist/js/info-toggle.js +25 -0
- package/dist/js/isometric.js +863 -0
- package/dist/js/kerr.js +1547 -0
- package/dist/js/lavalamp.js +590 -0
- package/dist/js/layout.js +354 -0
- package/dist/js/mondrian.js +285 -0
- package/dist/js/opacity.js +275 -0
- package/dist/js/painter.js +484 -0
- package/dist/js/particles-showcase.js +514 -0
- package/dist/js/particles.js +299 -0
- package/dist/js/patterns.js +397 -0
- package/dist/js/penrose/artifact.js +69 -0
- package/dist/js/penrose/blackhole.js +121 -0
- package/dist/js/penrose/constants.js +73 -0
- package/dist/js/penrose/game.js +943 -0
- package/dist/js/penrose/lore.js +278 -0
- package/dist/js/penrose/penrosescene.js +892 -0
- package/dist/js/penrose/ship.js +216 -0
- package/dist/js/penrose/sounds.js +211 -0
- package/dist/js/penrose/voidparticle.js +55 -0
- package/dist/js/penrose/voidscene.js +258 -0
- package/dist/js/penrose/voidship.js +144 -0
- package/dist/js/penrose/wormhole.js +46 -0
- package/dist/js/pipeline.js +555 -0
- package/dist/js/plane3d.js +256 -0
- package/dist/js/platformer.js +1579 -0
- package/dist/js/scene.js +304 -0
- package/dist/js/scenes.js +320 -0
- package/dist/js/schrodinger.js +410 -0
- package/dist/js/schwarzschild.js +1015 -0
- package/dist/js/shapes.js +628 -0
- package/dist/js/space/alien.js +171 -0
- package/dist/js/space/boom.js +98 -0
- package/dist/js/space/boss.js +353 -0
- package/dist/js/space/buff.js +73 -0
- package/dist/js/space/bullet.js +102 -0
- package/dist/js/space/constants.js +85 -0
- package/dist/js/space/game.js +1884 -0
- package/dist/js/space/hud.js +112 -0
- package/dist/js/space/laserbeam.js +179 -0
- package/dist/js/space/lightning.js +277 -0
- package/dist/js/space/minion.js +192 -0
- package/dist/js/space/missile.js +212 -0
- package/dist/js/space/player.js +430 -0
- package/dist/js/space/powerup.js +90 -0
- package/dist/js/space/starfield.js +58 -0
- package/dist/js/space/starpower.js +90 -0
- package/dist/js/spacetime.js +559 -0
- package/dist/js/sphere3d.js +229 -0
- package/dist/js/sprite.js +473 -0
- package/dist/js/starfaux/config.js +118 -0
- package/dist/js/starfaux/enemy.js +353 -0
- package/dist/js/starfaux/hud.js +78 -0
- package/dist/js/starfaux/index.js +482 -0
- package/dist/js/starfaux/laser.js +182 -0
- package/dist/js/starfaux/player.js +468 -0
- package/dist/js/starfaux/terrain.js +560 -0
- package/dist/js/study001.js +275 -0
- package/dist/js/study002.js +366 -0
- package/dist/js/study003.js +331 -0
- package/dist/js/study004.js +389 -0
- package/dist/js/study005.js +209 -0
- package/dist/js/study006.js +194 -0
- package/dist/js/study007.js +192 -0
- package/dist/js/study008.js +413 -0
- package/dist/js/svgtween.js +204 -0
- package/dist/js/tde/accretiondisk.js +471 -0
- package/dist/js/tde/blackhole.js +219 -0
- package/dist/js/tde/blackholescene.js +209 -0
- package/dist/js/tde/config.js +59 -0
- package/dist/js/tde/index.js +820 -0
- package/dist/js/tde/jets.js +290 -0
- package/dist/js/tde/lensedstarfield.js +154 -0
- package/dist/js/tde/tdestar.js +297 -0
- package/dist/js/tde/tidalstream.js +372 -0
- package/dist/js/tde_old/blackhole.obj.js +354 -0
- package/dist/js/tde_old/debris.obj.js +791 -0
- package/dist/js/tde_old/flare.obj.js +239 -0
- package/dist/js/tde_old/index.js +448 -0
- package/dist/js/tde_old/star.obj.js +812 -0
- package/dist/js/tetris/config.js +157 -0
- package/dist/js/tetris/grid.js +286 -0
- package/dist/js/tetris/index.js +1195 -0
- package/dist/js/tetris/renderer.js +634 -0
- package/dist/js/tetris/tetrominos.js +280 -0
- package/dist/js/tiles.js +312 -0
- package/dist/js/tweendemo.js +79 -0
- package/dist/js/visibility.js +102 -0
- package/dist/kerr.html +28 -0
- package/dist/lavalamp.html +27 -0
- package/dist/layouts.html +37 -0
- package/dist/logo.svg +4 -0
- package/dist/loop.html +84 -0
- package/dist/mondrian.html +32 -0
- package/dist/og_image.png +0 -0
- package/dist/opacity.html +36 -0
- package/dist/painter.html +39 -0
- package/dist/particles-showcase.html +28 -0
- package/dist/particles.html +24 -0
- package/dist/patterns.html +33 -0
- package/dist/penrose-game.html +31 -0
- package/dist/pipeline.html +737 -0
- package/dist/plane3d.html +24 -0
- package/dist/platformer.html +43 -0
- package/dist/scene.html +33 -0
- package/dist/scenes.html +96 -0
- package/dist/schrodinger.html +27 -0
- package/dist/schwarzschild.html +27 -0
- package/dist/shapes.html +16 -0
- package/dist/space.html +85 -0
- package/dist/spacetime.html +27 -0
- package/dist/sphere3d.html +24 -0
- package/dist/sprite.html +18 -0
- package/dist/starfaux.html +22 -0
- package/dist/study001.html +23 -0
- package/dist/study002.html +23 -0
- package/dist/study003.html +23 -0
- package/dist/study004.html +23 -0
- package/dist/study005.html +22 -0
- package/dist/study006.html +24 -0
- package/dist/study007.html +24 -0
- package/dist/study008.html +22 -0
- package/dist/svgtween.html +29 -0
- package/dist/tde.html +28 -0
- package/dist/tetris3d.html +25 -0
- package/dist/tiles.html +28 -0
- package/dist/transforms.html +400 -0
- package/dist/tween.html +45 -0
- package/dist/visibility.html +33 -0
- package/package.json +1 -1
|
@@ -0,0 +1,560 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terrain - Wireframe ground grid with Perlin noise deformation for StarFaux
|
|
3
|
+
*
|
|
4
|
+
* Uses Perlin noise to create smooth, procedural terrain that the
|
|
5
|
+
* grid deforms to - just like classic StarFox SNES/N64!
|
|
6
|
+
*
|
|
7
|
+
* Pattern borrowed from spacetime.js: pre-compute grid vertices,
|
|
8
|
+
* update heights per frame, project all then draw.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { Painter, Noise } from "/gcanvas.es.min.js";
|
|
12
|
+
import { CONFIG } from "./config.js";
|
|
13
|
+
|
|
14
|
+
export class Terrain {
|
|
15
|
+
constructor(game, camera) {
|
|
16
|
+
this.game = game;
|
|
17
|
+
this.camera = camera;
|
|
18
|
+
this.config = CONFIG.terrain;
|
|
19
|
+
|
|
20
|
+
// Seed the noise for consistent terrain
|
|
21
|
+
Noise.seed(42);
|
|
22
|
+
|
|
23
|
+
// Grid dimensions from config
|
|
24
|
+
this.numRows = this.config.totalRows || 80;
|
|
25
|
+
this.numCols = this.config.numCols || 60;
|
|
26
|
+
|
|
27
|
+
// Calculate dynamic width based on screen dimensions
|
|
28
|
+
this.updateDimensions();
|
|
29
|
+
|
|
30
|
+
// Pre-compute flat grid structure (will be updated each frame)
|
|
31
|
+
this.gridVertices = [];
|
|
32
|
+
|
|
33
|
+
// Track the nearest visible row for ground fill
|
|
34
|
+
this.nearestVisibleRow = null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Update terrain dimensions based on current screen size
|
|
39
|
+
* Called on init and resize
|
|
40
|
+
*/
|
|
41
|
+
updateDimensions() {
|
|
42
|
+
// Width scales with screen width to always reach edges
|
|
43
|
+
const multiplier = this.config.widthMultiplier || 6;
|
|
44
|
+
this.terrainWidth = this.game.width * multiplier;
|
|
45
|
+
|
|
46
|
+
// Column spacing adapts to maintain grid density
|
|
47
|
+
this.colSpacing = this.terrainWidth / this.numCols;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
update(dt) {
|
|
51
|
+
// Rebuild grid vertices each frame based on current scroll position
|
|
52
|
+
this.buildGridVertices();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Build grid of vertices at current scroll position with terrain heights
|
|
57
|
+
*/
|
|
58
|
+
buildGridVertices() {
|
|
59
|
+
const cfg = this.config;
|
|
60
|
+
// Use dynamic width calculated in updateDimensions
|
|
61
|
+
const halfWidth = this.terrainWidth / 2;
|
|
62
|
+
const groundY = cfg.yPosition;
|
|
63
|
+
const colSpacing = this.colSpacing;
|
|
64
|
+
|
|
65
|
+
// Use config values for grid density
|
|
66
|
+
const nearSpacing = cfg.nearSpacing || 25;
|
|
67
|
+
const totalRows = this.numRows;
|
|
68
|
+
const startZ = cfg.nearPlaneZ || 50;
|
|
69
|
+
|
|
70
|
+
this.gridVertices = [];
|
|
71
|
+
this.nearestVisibleRow = null;
|
|
72
|
+
|
|
73
|
+
// Calculate scroll offset based on our actual spacing
|
|
74
|
+
const scrollOffset = this.game.distance % nearSpacing;
|
|
75
|
+
|
|
76
|
+
for (let row = 0; row < totalRows; row++) {
|
|
77
|
+
const rowVertices = [];
|
|
78
|
+
|
|
79
|
+
// Start grid at bottom of screen, scrolling smoothly
|
|
80
|
+
const lineZ = startZ + row * nearSpacing - scrollOffset;
|
|
81
|
+
|
|
82
|
+
// Skip if behind camera
|
|
83
|
+
if (lineZ <= 0) {
|
|
84
|
+
this.gridVertices.push(null);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// World Z for this row
|
|
89
|
+
const worldZ = this.game.distance + lineZ;
|
|
90
|
+
|
|
91
|
+
for (let col = 0; col <= this.numCols; col++) {
|
|
92
|
+
const worldX = -halfWidth + col * colSpacing;
|
|
93
|
+
|
|
94
|
+
// Get terrain height at this point using Perlin noise
|
|
95
|
+
const terrainHeight = this.getHeightAt(worldX, worldZ);
|
|
96
|
+
|
|
97
|
+
// Y position: ground minus terrain height (terrain rises UP = negative Y)
|
|
98
|
+
const y = groundY - terrainHeight;
|
|
99
|
+
|
|
100
|
+
rowVertices.push({
|
|
101
|
+
worldX,
|
|
102
|
+
worldZ,
|
|
103
|
+
localZ: lineZ, // Z relative to camera for projection
|
|
104
|
+
y,
|
|
105
|
+
height: terrainHeight,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
this.gridVertices.push(rowVertices);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Get terrain height at a world position
|
|
115
|
+
* Canyon-style: dramatic mountains on edges, mostly flat middle with occasional features
|
|
116
|
+
*/
|
|
117
|
+
getHeightAt(worldX, worldZ) {
|
|
118
|
+
const cfg = this.config.features;
|
|
119
|
+
const maxH = cfg.maxHeight;
|
|
120
|
+
|
|
121
|
+
// How far from center (0 = center, 1 = edge)
|
|
122
|
+
const halfWidth = this.terrainWidth / 2;
|
|
123
|
+
const edgeFactor = Math.abs(worldX) / halfWidth;
|
|
124
|
+
|
|
125
|
+
// DRAMATIC canyon walls - steep mountains on left/right margins
|
|
126
|
+
// Use steeper curve (power of 3) and higher multiplier for more pronounced edges
|
|
127
|
+
const edgeHeight = Math.pow(edgeFactor, 3) * maxH * 1.8;
|
|
128
|
+
|
|
129
|
+
// Add dramatic noise variation to canyon walls - jagged peaks
|
|
130
|
+
const wallNoise = Noise.perlin2(worldX * 0.004, worldZ * 0.004);
|
|
131
|
+
const wallVariation = wallNoise * maxH * 0.6 * edgeFactor;
|
|
132
|
+
|
|
133
|
+
// Secondary ridge detail on edges
|
|
134
|
+
const ridgeNoise = Noise.perlin2(worldX * 0.012, worldZ * 0.008);
|
|
135
|
+
const ridges = Math.max(0, ridgeNoise) * maxH * 0.3 * Math.pow(edgeFactor, 2);
|
|
136
|
+
|
|
137
|
+
// TALL obstacle mountains in the middle - these are the main hazards to dodge
|
|
138
|
+
const obstaclePeakNoise = Noise.perlin2(worldX * 0.002 + 200, worldZ * 0.0015);
|
|
139
|
+
let obstaclePeak = 0;
|
|
140
|
+
if (obstaclePeakNoise > 0.4 && edgeFactor < 0.5) {
|
|
141
|
+
// Tall, sharp mountains that require dodging
|
|
142
|
+
const sharpness = Noise.perlin2(worldX * 0.02, worldZ * 0.02);
|
|
143
|
+
obstaclePeak = (obstaclePeakNoise - 0.4) * 8 * maxH * Math.max(0.5, sharpness + 0.5);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Additional scattered tall spires in the middle
|
|
147
|
+
const spireNoise = Noise.perlin2(worldX * 0.004 + 300, worldZ * 0.003 + 100);
|
|
148
|
+
let spire = 0;
|
|
149
|
+
if (spireNoise > 0.5 && edgeFactor < 0.4) {
|
|
150
|
+
spire = (spireNoise - 0.5) * 6 * maxH;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Depressions/valleys in the middle (negative contribution to make dips)
|
|
154
|
+
const valleyNoise = Noise.perlin2(worldX * 0.002 + 50, worldZ * 0.002 + 50);
|
|
155
|
+
let valley = 0;
|
|
156
|
+
if (valleyNoise < -0.3 && edgeFactor < 0.4) {
|
|
157
|
+
// Create subtle dips/depressions
|
|
158
|
+
valley = (valleyNoise + 0.3) * maxH * 0.2; // Negative value = depression
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Rolling hills in middle area (gentle undulation)
|
|
162
|
+
const hillNoise = Noise.perlin2(worldX * 0.003, worldZ * 0.003);
|
|
163
|
+
const middleHills = Math.max(0, hillNoise * 0.5 + 0.1) * maxH * 0.2 * (1 - edgeFactor);
|
|
164
|
+
|
|
165
|
+
// Combine all layers
|
|
166
|
+
let height = edgeHeight + wallVariation + ridges + obstaclePeak + spire + valley + middleHills;
|
|
167
|
+
|
|
168
|
+
// Ensure non-negative
|
|
169
|
+
height = Math.max(0, height);
|
|
170
|
+
|
|
171
|
+
return height;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Check if player collides with terrain
|
|
176
|
+
*/
|
|
177
|
+
checkPlayerCollision(playerX, playerY) {
|
|
178
|
+
const playerWorldZ = this.game.distance + CONFIG.player.shipZ;
|
|
179
|
+
const terrainHeight = this.getHeightAt(playerX, playerWorldZ);
|
|
180
|
+
|
|
181
|
+
// Only check collision for reasonably tall terrain
|
|
182
|
+
const collisionThreshold = this.config.features.collisionHeight || 100;
|
|
183
|
+
if (terrainHeight < collisionThreshold) {
|
|
184
|
+
return false; // Terrain too short to collide
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// groundY is 0, terrainHeight is positive (e.g. 200)
|
|
188
|
+
// surfaceY = 0 - 200 = -200 (negative = higher on screen)
|
|
189
|
+
// playerY is from offsetY + screenY, can be negative (high) or positive (low)
|
|
190
|
+
// Player collides if their Y is GREATER than surfaceY (lower on screen = into mountain)
|
|
191
|
+
const groundY = this.config.yPosition;
|
|
192
|
+
const surfaceY = groundY - terrainHeight;
|
|
193
|
+
|
|
194
|
+
// Small buffer for ship size
|
|
195
|
+
const collisionBuffer = 30;
|
|
196
|
+
return playerY > surfaceY + collisionBuffer;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
render(parallaxOffsetY = 0) {
|
|
200
|
+
const ctx = Painter.ctx;
|
|
201
|
+
|
|
202
|
+
// Store parallax for use in sky rendering
|
|
203
|
+
this.parallaxOffsetY = parallaxOffsetY;
|
|
204
|
+
|
|
205
|
+
// Render sky gradient first (behind everything)
|
|
206
|
+
this.renderSky(ctx);
|
|
207
|
+
|
|
208
|
+
// Fill the gap between horizon and terrain with dark color
|
|
209
|
+
this.renderHorizonFill(ctx);
|
|
210
|
+
|
|
211
|
+
// Build projection data
|
|
212
|
+
const projected = this.buildProjectedGrid();
|
|
213
|
+
|
|
214
|
+
// Render the grid lines - NO ground fill, just clean lines like Joy Division
|
|
215
|
+
this.renderGridLines(ctx, projected);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Fill the area below the horizon to eliminate the gap
|
|
220
|
+
*/
|
|
221
|
+
renderHorizonFill(ctx) {
|
|
222
|
+
const w = this.game.width;
|
|
223
|
+
const h = this.game.height;
|
|
224
|
+
const groundY = this.config.yPosition;
|
|
225
|
+
|
|
226
|
+
// Horizon stays FIXED - no parallax
|
|
227
|
+
const horizonPoint = this.camera.project(0, groundY, 10000);
|
|
228
|
+
const horizonY = horizonPoint.y;
|
|
229
|
+
|
|
230
|
+
// Fill from horizon down (but not all the way - leave room for HUD)
|
|
231
|
+
// The fill height should just cover the gap, terrain lines will render on top
|
|
232
|
+
const fillHeight = h * 0.6; // Fill about 60% down from horizon
|
|
233
|
+
|
|
234
|
+
const gradient = ctx.createLinearGradient(0, horizonY, 0, horizonY + fillHeight);
|
|
235
|
+
gradient.addColorStop(0, "#001a00"); // Very dark green at horizon
|
|
236
|
+
gradient.addColorStop(0.5, "#000800"); // Darker
|
|
237
|
+
gradient.addColorStop(1, "#000000"); // Black
|
|
238
|
+
|
|
239
|
+
ctx.fillStyle = gradient;
|
|
240
|
+
ctx.fillRect(-w, horizonY, w * 2, fillHeight);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Build projected grid data for rendering
|
|
245
|
+
*/
|
|
246
|
+
buildProjectedGrid() {
|
|
247
|
+
const camera = this.camera;
|
|
248
|
+
const projected = [];
|
|
249
|
+
|
|
250
|
+
// Track nearest visible row for ground fill
|
|
251
|
+
this.nearestVisibleRow = null;
|
|
252
|
+
|
|
253
|
+
for (let row = 0; row < this.gridVertices.length; row++) {
|
|
254
|
+
const rowVerts = this.gridVertices[row];
|
|
255
|
+
if (!rowVerts) {
|
|
256
|
+
projected.push(null);
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const rowProjected = [];
|
|
261
|
+
let hasVisiblePoints = false;
|
|
262
|
+
|
|
263
|
+
for (const vertex of rowVerts) {
|
|
264
|
+
const p = camera.project(vertex.worldX, vertex.y, vertex.localZ);
|
|
265
|
+
|
|
266
|
+
if (p.scale > 0.008) {
|
|
267
|
+
rowProjected.push({
|
|
268
|
+
x: p.x,
|
|
269
|
+
y: p.y,
|
|
270
|
+
scale: p.scale,
|
|
271
|
+
height: vertex.height,
|
|
272
|
+
});
|
|
273
|
+
hasVisiblePoints = true;
|
|
274
|
+
} else {
|
|
275
|
+
rowProjected.push(null);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
projected.push(rowProjected);
|
|
280
|
+
|
|
281
|
+
// Track the nearest (first) visible row for ground fill
|
|
282
|
+
if (hasVisiblePoints && !this.nearestVisibleRow) {
|
|
283
|
+
this.nearestVisibleRow = rowProjected;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return projected;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Draw grid lines from projected data - Joy Division style (horizontal only)
|
|
292
|
+
* Like the Unknown Pleasures album cover - stacked waveform lines
|
|
293
|
+
* Mountains above horizon render as black silhouettes for contrast
|
|
294
|
+
*/
|
|
295
|
+
renderGridLines(ctx, projected) {
|
|
296
|
+
const scale = this.game.scaleFactor || 1;
|
|
297
|
+
const groundY = this.config.yPosition;
|
|
298
|
+
|
|
299
|
+
// Horizon stays FIXED
|
|
300
|
+
const horizonPoint = this.camera.project(0, groundY, 10000);
|
|
301
|
+
const horizonY = horizonPoint.y;
|
|
302
|
+
|
|
303
|
+
// Joy Division style horizontal waveform lines
|
|
304
|
+
for (let row = 0; row < projected.length; row++) {
|
|
305
|
+
const rowPoints = projected[row];
|
|
306
|
+
if (!rowPoints) continue;
|
|
307
|
+
|
|
308
|
+
// Each line is one continuous stroke - consistent color
|
|
309
|
+
ctx.strokeStyle = "#00ff00";
|
|
310
|
+
ctx.lineWidth = 1.5 * scale;
|
|
311
|
+
ctx.beginPath();
|
|
312
|
+
let started = false;
|
|
313
|
+
|
|
314
|
+
for (let col = 0; col < rowPoints.length; col++) {
|
|
315
|
+
const point = rowPoints[col];
|
|
316
|
+
if (!point) {
|
|
317
|
+
if (started) ctx.stroke();
|
|
318
|
+
ctx.beginPath();
|
|
319
|
+
started = false;
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (!started) {
|
|
324
|
+
ctx.moveTo(point.x, point.y);
|
|
325
|
+
started = true;
|
|
326
|
+
} else {
|
|
327
|
+
ctx.lineTo(point.x, point.y);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
if (started) ctx.stroke();
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Render black silhouettes for mountain peaks that extend above the horizon
|
|
336
|
+
* This creates contrast against the bright sun/sky
|
|
337
|
+
*/
|
|
338
|
+
renderSilhouettes(ctx, projected, horizonY, scale) {
|
|
339
|
+
ctx.fillStyle = "#000000";
|
|
340
|
+
|
|
341
|
+
// Process each row - fill area from line to horizon if above horizon
|
|
342
|
+
for (let row = projected.length - 1; row >= 0; row--) {
|
|
343
|
+
const rowPoints = projected[row];
|
|
344
|
+
if (!rowPoints) continue;
|
|
345
|
+
|
|
346
|
+
// Check if any points in this row are above horizon
|
|
347
|
+
let hasAboveHorizon = false;
|
|
348
|
+
for (const point of rowPoints) {
|
|
349
|
+
if (point && point.y < horizonY) {
|
|
350
|
+
hasAboveHorizon = true;
|
|
351
|
+
break;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (!hasAboveHorizon) continue;
|
|
356
|
+
|
|
357
|
+
// Draw filled polygon from the line down to horizon
|
|
358
|
+
ctx.beginPath();
|
|
359
|
+
let started = false;
|
|
360
|
+
let firstX = 0;
|
|
361
|
+
|
|
362
|
+
for (let col = 0; col < rowPoints.length; col++) {
|
|
363
|
+
const point = rowPoints[col];
|
|
364
|
+
if (!point) continue;
|
|
365
|
+
|
|
366
|
+
// Only include points above or near horizon
|
|
367
|
+
const y = Math.min(point.y, horizonY);
|
|
368
|
+
|
|
369
|
+
if (!started) {
|
|
370
|
+
firstX = point.x;
|
|
371
|
+
ctx.moveTo(point.x, y);
|
|
372
|
+
started = true;
|
|
373
|
+
} else {
|
|
374
|
+
ctx.lineTo(point.x, y);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (started) {
|
|
379
|
+
// Close the shape by going down to horizon and back
|
|
380
|
+
const lastPoint = rowPoints[rowPoints.length - 1] || rowPoints.find(p => p);
|
|
381
|
+
if (lastPoint) {
|
|
382
|
+
ctx.lineTo(lastPoint.x, horizonY + 2); // Go to horizon
|
|
383
|
+
ctx.lineTo(firstX, horizonY + 2); // Go back along horizon
|
|
384
|
+
ctx.closePath();
|
|
385
|
+
ctx.fill();
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Get color based on terrain height - green with brighter peaks
|
|
393
|
+
*/
|
|
394
|
+
getHeightColor(height) {
|
|
395
|
+
// Green terrain - brighter for peaks like classic StarFox
|
|
396
|
+
if (height < 5) {
|
|
397
|
+
return "#005500"; // Flat ground - dark green
|
|
398
|
+
} else if (height < 40) {
|
|
399
|
+
return "#007700"; // Low hills - medium green
|
|
400
|
+
} else if (height < 80) {
|
|
401
|
+
return "#00aa00"; // Medium hills - brighter green
|
|
402
|
+
} else {
|
|
403
|
+
return "#00ff00"; // Peaks - bright green
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
renderSky(ctx) {
|
|
408
|
+
const camera = this.camera;
|
|
409
|
+
const groundY = this.config.yPosition;
|
|
410
|
+
const scale = this.game.scaleFactor || 1;
|
|
411
|
+
const w = this.game.width;
|
|
412
|
+
const h = this.game.height;
|
|
413
|
+
|
|
414
|
+
// Horizon stays FIXED - no parallax on sky/horizon
|
|
415
|
+
const horizonPoint = camera.project(0, groundY, 10000);
|
|
416
|
+
const horizonY = horizonPoint.y;
|
|
417
|
+
|
|
418
|
+
// === DARK PURPLE/MAGENTA SKY - contrasts with green terrain ===
|
|
419
|
+
|
|
420
|
+
// Dark purple gradient sky - complementary to green
|
|
421
|
+
const skyGradient = ctx.createLinearGradient(0, -h / 2, 0, horizonY);
|
|
422
|
+
skyGradient.addColorStop(0, "#050008"); // Near black with purple tint
|
|
423
|
+
skyGradient.addColorStop(0.3, "#0a0012"); // Very dark purple
|
|
424
|
+
skyGradient.addColorStop(0.5, "#120020"); // Dark purple
|
|
425
|
+
skyGradient.addColorStop(0.7, "#1a0030"); // Medium purple
|
|
426
|
+
skyGradient.addColorStop(0.85, "#220040"); // Lighter purple near horizon
|
|
427
|
+
skyGradient.addColorStop(1, "#2a0050"); // Purple at horizon
|
|
428
|
+
ctx.fillStyle = skyGradient;
|
|
429
|
+
ctx.fillRect(-w, -h / 2, w * 2, horizonY + h / 2);
|
|
430
|
+
|
|
431
|
+
// === TERMINAL SUN ===
|
|
432
|
+
const sunRadius = 120 * scale;
|
|
433
|
+
const sunY = horizonY - sunRadius * 0.4; // Sun sitting on horizon
|
|
434
|
+
|
|
435
|
+
// Sun glow (outer) - green/cyan glow against purple sky
|
|
436
|
+
const glowGradient = ctx.createRadialGradient(0, sunY, sunRadius * 0.5, 0, sunY, sunRadius * 2.5);
|
|
437
|
+
glowGradient.addColorStop(0, "rgba(100, 255, 150, 0.4)");
|
|
438
|
+
glowGradient.addColorStop(0.4, "rgba(0, 255, 100, 0.2)");
|
|
439
|
+
glowGradient.addColorStop(0.7, "rgba(0, 200, 100, 0.1)");
|
|
440
|
+
glowGradient.addColorStop(1, "rgba(50, 0, 80, 0)");
|
|
441
|
+
ctx.fillStyle = glowGradient;
|
|
442
|
+
ctx.fillRect(-w, horizonY - sunRadius * 3, w * 2, sunRadius * 3);
|
|
443
|
+
|
|
444
|
+
// Sun body - gradient from white/cyan to green
|
|
445
|
+
const sunGradient = ctx.createLinearGradient(0, sunY - sunRadius, 0, sunY + sunRadius);
|
|
446
|
+
sunGradient.addColorStop(0, "#eeffff"); // White/cyan top
|
|
447
|
+
sunGradient.addColorStop(0.25, "#88ffcc"); // Cyan-green
|
|
448
|
+
sunGradient.addColorStop(0.5, "#00ff88"); // Bright green-cyan
|
|
449
|
+
sunGradient.addColorStop(0.75, "#00cc66"); // Medium green
|
|
450
|
+
sunGradient.addColorStop(1, "#006633"); // Dark green bottom
|
|
451
|
+
|
|
452
|
+
ctx.save();
|
|
453
|
+
// Clip sun to above horizon (sun setting)
|
|
454
|
+
ctx.beginPath();
|
|
455
|
+
ctx.rect(-w, -h, w * 2, horizonY + h);
|
|
456
|
+
ctx.clip();
|
|
457
|
+
|
|
458
|
+
ctx.fillStyle = sunGradient;
|
|
459
|
+
ctx.beginPath();
|
|
460
|
+
ctx.arc(0, sunY, sunRadius, 0, Math.PI * 2);
|
|
461
|
+
ctx.fill();
|
|
462
|
+
|
|
463
|
+
// No scanlines - clean sun
|
|
464
|
+
ctx.restore();
|
|
465
|
+
|
|
466
|
+
// === CLOUDS ===
|
|
467
|
+
this.renderSynthwaveClouds(ctx, horizonY, scale);
|
|
468
|
+
|
|
469
|
+
// === HORIZON LINE ===
|
|
470
|
+
ctx.strokeStyle = "#00ff88"; // Cyan-green horizon to match sun
|
|
471
|
+
ctx.lineWidth = 2 * scale;
|
|
472
|
+
ctx.shadowColor = "#00ff88";
|
|
473
|
+
ctx.shadowBlur = 15 * scale;
|
|
474
|
+
ctx.beginPath();
|
|
475
|
+
ctx.moveTo(-w, horizonY);
|
|
476
|
+
ctx.lineTo(w, horizonY);
|
|
477
|
+
ctx.stroke();
|
|
478
|
+
ctx.shadowBlur = 0;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Render synthwave-style geometric clouds
|
|
483
|
+
*/
|
|
484
|
+
renderSynthwaveClouds(ctx, horizonY, scale) {
|
|
485
|
+
const w = this.game.width;
|
|
486
|
+
const h = this.game.height;
|
|
487
|
+
|
|
488
|
+
// Scroll clouds slowly based on game distance
|
|
489
|
+
const scrollOffset = (this.game.distance * 0.02) % (w * 2);
|
|
490
|
+
|
|
491
|
+
ctx.save();
|
|
492
|
+
ctx.globalAlpha = 0.25;
|
|
493
|
+
|
|
494
|
+
// Layer 1: Far clouds (slower, smaller) - dark purple
|
|
495
|
+
const farOffset = scrollOffset * 0.3;
|
|
496
|
+
this.drawCloudLayer(ctx, horizonY - 180 * scale, 40 * scale, farOffset, 5, "#442266", scale);
|
|
497
|
+
|
|
498
|
+
// Layer 2: Mid clouds - purple/magenta
|
|
499
|
+
const midOffset = scrollOffset * 0.5;
|
|
500
|
+
this.drawCloudLayer(ctx, horizonY - 120 * scale, 30 * scale, midOffset, 4, "#663388", scale);
|
|
501
|
+
|
|
502
|
+
// Layer 3: Near clouds (faster, larger) - cyan-green glow
|
|
503
|
+
const nearOffset = scrollOffset * 0.8;
|
|
504
|
+
this.drawCloudLayer(ctx, horizonY - 60 * scale, 20 * scale, nearOffset, 3, "#00cc88", scale);
|
|
505
|
+
|
|
506
|
+
ctx.restore();
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Draw a single layer of geometric clouds
|
|
511
|
+
*/
|
|
512
|
+
drawCloudLayer(ctx, y, height, offset, count, color, scale) {
|
|
513
|
+
const w = this.game.width;
|
|
514
|
+
ctx.strokeStyle = color;
|
|
515
|
+
ctx.lineWidth = 1.5 * scale;
|
|
516
|
+
|
|
517
|
+
for (let i = 0; i < count; i++) {
|
|
518
|
+
// Position clouds across the sky with offset for scrolling
|
|
519
|
+
const baseX = (i / count) * w * 2 - w + offset;
|
|
520
|
+
const x = ((baseX + w * 2) % (w * 2)) - w; // Wrap around
|
|
521
|
+
|
|
522
|
+
// Draw geometric cloud shape (horizontal lines stacked)
|
|
523
|
+
const cloudWidth = (80 + Math.sin(i * 1.5) * 40) * scale;
|
|
524
|
+
const lineCount = 3 + (i % 2);
|
|
525
|
+
|
|
526
|
+
for (let j = 0; j < lineCount; j++) {
|
|
527
|
+
const lineY = y + j * (height / lineCount);
|
|
528
|
+
const lineWidth = cloudWidth * (1 - j * 0.15); // Taper toward bottom
|
|
529
|
+
ctx.beginPath();
|
|
530
|
+
ctx.moveTo(x - lineWidth / 2, lineY);
|
|
531
|
+
ctx.lineTo(x + lineWidth / 2, lineY);
|
|
532
|
+
ctx.stroke();
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Render ground fill - dark gradient at bottom ~15% of screen
|
|
539
|
+
*/
|
|
540
|
+
renderGroundFill(ctx) {
|
|
541
|
+
const w = this.game.width;
|
|
542
|
+
const halfH = this.game.height / 2;
|
|
543
|
+
|
|
544
|
+
// Fill bottom 15% with dark gradient
|
|
545
|
+
const fillHeight = halfH * 0.3; // 15% of screen height
|
|
546
|
+
const fillStart = halfH - fillHeight;
|
|
547
|
+
|
|
548
|
+
const gradient = ctx.createLinearGradient(0, fillStart, 0, halfH);
|
|
549
|
+
gradient.addColorStop(0, "rgba(0, 15, 0, 0)"); // Transparent at top
|
|
550
|
+
gradient.addColorStop(0.3, "rgba(0, 10, 0, 0.7)");
|
|
551
|
+
gradient.addColorStop(1, "rgba(0, 5, 5, 1)"); // Solid dark at bottom
|
|
552
|
+
|
|
553
|
+
ctx.fillStyle = gradient;
|
|
554
|
+
ctx.fillRect(-w, fillStart, w * 2, fillHeight + 50);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
reset() {
|
|
558
|
+
// Nothing to reset - terrain is procedural
|
|
559
|
+
}
|
|
560
|
+
}
|