@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.
- package/demos/coordinates.html +698 -0
- package/demos/cube3d.html +23 -0
- package/demos/demos.css +17 -3
- package/demos/dino.html +42 -0
- package/demos/gameobjects.html +626 -0
- package/demos/index.html +17 -7
- package/demos/js/coordinates.js +840 -0
- package/demos/js/cube3d.js +789 -0
- package/demos/js/dino.js +1420 -0
- package/demos/js/gameobjects.js +176 -0
- package/demos/js/plane3d.js +256 -0
- package/demos/js/platformer.js +1579 -0
- package/demos/js/sphere3d.js +229 -0
- package/demos/js/sprite.js +473 -0
- package/demos/js/tde/accretiondisk.js +3 -3
- package/demos/js/tde/tidalstream.js +2 -2
- package/demos/plane3d.html +24 -0
- package/demos/platformer.html +43 -0
- package/demos/sphere3d.html +24 -0
- package/demos/sprite.html +18 -0
- package/docs/concepts/coordinate-system.md +384 -0
- package/docs/concepts/shapes-vs-gameobjects.md +187 -0
- package/docs/fluid-dynamics.md +99 -97
- package/package.json +1 -1
- package/src/game/game.js +11 -5
- package/src/game/objects/index.js +3 -0
- package/src/game/objects/platformer-scene.js +411 -0
- package/src/game/objects/scene.js +14 -0
- package/src/game/objects/sprite.js +529 -0
- package/src/game/pipeline.js +20 -16
- package/src/game/ui/theme.js +123 -121
- package/src/io/input.js +75 -45
- package/src/io/mouse.js +44 -19
- package/src/io/touch.js +35 -12
- package/src/shapes/cube3d.js +599 -0
- package/src/shapes/index.js +2 -0
- package/src/shapes/plane3d.js +687 -0
- package/src/shapes/sphere3d.js +75 -6
- package/src/util/camera2d.js +315 -0
- package/src/util/index.js +1 -0
- package/src/webgl/shaders/plane-shaders.js +332 -0
- package/src/webgl/shaders/sphere-shaders.js +4 -2
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gameobjects.html - Live demos for Shapes vs GameObjects documentation
|
|
3
|
+
*/
|
|
4
|
+
import {
|
|
5
|
+
Circle,
|
|
6
|
+
Game,
|
|
7
|
+
Group,
|
|
8
|
+
Painter,
|
|
9
|
+
Rectangle,
|
|
10
|
+
Scene,
|
|
11
|
+
ShapeGOFactory,
|
|
12
|
+
Sprite,
|
|
13
|
+
} from "../../src/index.js";
|
|
14
|
+
|
|
15
|
+
// ==================== Demo 1: Shapes Only ====================
|
|
16
|
+
// Even "shapes only" needs Painter initialized, so we use a minimal Game
|
|
17
|
+
class ShapesDemo extends Game {
|
|
18
|
+
constructor(canvas) {
|
|
19
|
+
super(canvas);
|
|
20
|
+
this.backgroundColor = "#050505";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
init() {
|
|
24
|
+
super.init();
|
|
25
|
+
// Shapes are created and rendered in render() below
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
render() {
|
|
29
|
+
super.render(); // Clears canvas
|
|
30
|
+
|
|
31
|
+
const centerY = this.canvas.height / 2;
|
|
32
|
+
|
|
33
|
+
// Create shapes - spread across canvas
|
|
34
|
+
const circle = new Circle(35, { x: this.canvas.width * 0.2, y: centerY, color: "#0f0" });
|
|
35
|
+
const rect = new Rectangle({ x: this.canvas.width * 0.5, y: centerY, width: 80, height: 50, color: "#0ff" });
|
|
36
|
+
|
|
37
|
+
// Group with rotation
|
|
38
|
+
const group = new Group({ x: this.canvas.width * 0.8, y: centerY, rotation: Math.PI / 4 });
|
|
39
|
+
group.add(new Circle(20, { color: "#f0f" }));
|
|
40
|
+
group.add(new Rectangle({ y: 30, width: 40, height: 20, color: "#ff0" }));
|
|
41
|
+
|
|
42
|
+
// Render directly - calling render() on shapes, not using pipeline
|
|
43
|
+
circle.render();
|
|
44
|
+
rect.render();
|
|
45
|
+
group.render();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function initShapesDemo() {
|
|
50
|
+
const canvas = document.getElementById("shapes-canvas");
|
|
51
|
+
if (!canvas) return;
|
|
52
|
+
|
|
53
|
+
const rect = canvas.getBoundingClientRect();
|
|
54
|
+
canvas.width = rect.width;
|
|
55
|
+
canvas.height = rect.height;
|
|
56
|
+
|
|
57
|
+
const demo = new ShapesDemo(canvas);
|
|
58
|
+
demo.start();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ==================== Demo 2: GameObjects with Pipeline ====================
|
|
62
|
+
class GameObjectsDemo extends Game {
|
|
63
|
+
constructor(canvas) {
|
|
64
|
+
super(canvas);
|
|
65
|
+
this.backgroundColor = "#050505";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
init() {
|
|
69
|
+
super.init();
|
|
70
|
+
|
|
71
|
+
const centerX = this.canvas.width / 2;
|
|
72
|
+
const centerY = this.canvas.height / 2;
|
|
73
|
+
|
|
74
|
+
// Create a scene (GameObject container)
|
|
75
|
+
this.scene = new Scene(this, { x: centerX, y: centerY });
|
|
76
|
+
|
|
77
|
+
// Create a sprite with animation (GameObject)
|
|
78
|
+
this.player = new Sprite(this, { frameRate: 8, loop: true, autoPlay: true });
|
|
79
|
+
|
|
80
|
+
// Add frames - pulsing circle animation
|
|
81
|
+
const colors = ["#0f0", "#0ff", "#0f0", "#ff0", "#0f0"];
|
|
82
|
+
const sizes = [20, 25, 30, 25, 20];
|
|
83
|
+
colors.forEach((color, i) => {
|
|
84
|
+
this.player.addFrame(new Circle(sizes[i], { color, stroke: "#fff", lineWidth: 2 }));
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Add to scene
|
|
88
|
+
this.scene.add(this.player);
|
|
89
|
+
|
|
90
|
+
// Add scene to pipeline - now it's managed
|
|
91
|
+
this.pipeline.add(this.scene);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function initGameObjectsDemo() {
|
|
96
|
+
const canvas = document.getElementById("gameobjects-canvas");
|
|
97
|
+
if (!canvas) return;
|
|
98
|
+
|
|
99
|
+
// Set actual canvas size
|
|
100
|
+
const rect = canvas.getBoundingClientRect();
|
|
101
|
+
canvas.width = rect.width;
|
|
102
|
+
canvas.height = rect.height;
|
|
103
|
+
|
|
104
|
+
const demo = new GameObjectsDemo(canvas);
|
|
105
|
+
demo.start();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ==================== Demo 3: Bridging Shape to GameObject ====================
|
|
109
|
+
class BridgingDemo extends Game {
|
|
110
|
+
constructor(canvas) {
|
|
111
|
+
super(canvas);
|
|
112
|
+
this.backgroundColor = "#050505";
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
init() {
|
|
116
|
+
super.init();
|
|
117
|
+
|
|
118
|
+
const centerX = this.canvas.width / 2;
|
|
119
|
+
const centerY = this.canvas.height / 2;
|
|
120
|
+
|
|
121
|
+
// Create a Group of shapes (like a simple avatar)
|
|
122
|
+
const avatar = new Group();
|
|
123
|
+
avatar.add(new Circle(25, { y: -30, color: "#0f0", stroke: "#0a0", lineWidth: 2 })); // head
|
|
124
|
+
avatar.add(new Rectangle({ y: 20, width: 40, height: 50, color: "#0f0", stroke: "#0a0", lineWidth: 2 })); // body
|
|
125
|
+
|
|
126
|
+
// Wrap it as a GameObject so it can join the pipeline
|
|
127
|
+
const avatarGO = ShapeGOFactory.create(this, avatar, {
|
|
128
|
+
x: centerX,
|
|
129
|
+
y: centerY,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Now it's a proper GameObject - add to pipeline
|
|
133
|
+
this.pipeline.add(avatarGO);
|
|
134
|
+
|
|
135
|
+
// Add a second avatar to show multiple instances
|
|
136
|
+
const avatar2 = new Group();
|
|
137
|
+
avatar2.add(new Circle(20, { y: -25, color: "#0ff", stroke: "#0aa", lineWidth: 2 }));
|
|
138
|
+
avatar2.add(new Rectangle({ y: 15, width: 30, height: 40, color: "#0ff", stroke: "#0aa", lineWidth: 2 }));
|
|
139
|
+
|
|
140
|
+
const avatar2GO = ShapeGOFactory.create(this, avatar2, {
|
|
141
|
+
x: centerX + 100,
|
|
142
|
+
y: centerY,
|
|
143
|
+
});
|
|
144
|
+
this.pipeline.add(avatar2GO);
|
|
145
|
+
|
|
146
|
+
// Third one with rotation
|
|
147
|
+
const avatar3 = new Group({ rotation: 0.2 });
|
|
148
|
+
avatar3.add(new Circle(18, { y: -22, color: "#f0f", stroke: "#a0a", lineWidth: 2 }));
|
|
149
|
+
avatar3.add(new Rectangle({ y: 12, width: 25, height: 35, color: "#f0f", stroke: "#a0a", lineWidth: 2 }));
|
|
150
|
+
|
|
151
|
+
const avatar3GO = ShapeGOFactory.create(this, avatar3, {
|
|
152
|
+
x: centerX - 100,
|
|
153
|
+
y: centerY,
|
|
154
|
+
});
|
|
155
|
+
this.pipeline.add(avatar3GO);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function initBridgingDemo() {
|
|
160
|
+
const canvas = document.getElementById("bridging-canvas");
|
|
161
|
+
if (!canvas) return;
|
|
162
|
+
|
|
163
|
+
const rect = canvas.getBoundingClientRect();
|
|
164
|
+
canvas.width = rect.width;
|
|
165
|
+
canvas.height = rect.height;
|
|
166
|
+
|
|
167
|
+
const demo = new BridgingDemo(canvas);
|
|
168
|
+
demo.start();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ==================== Initialize ====================
|
|
172
|
+
window.addEventListener("load", () => {
|
|
173
|
+
initShapesDemo();
|
|
174
|
+
initGameObjectsDemo();
|
|
175
|
+
initBridgingDemo();
|
|
176
|
+
});
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Game,
|
|
3
|
+
Plane3D,
|
|
4
|
+
FPSCounter,
|
|
5
|
+
} from "../../src/index.js";
|
|
6
|
+
import { Camera3D } from "../../src/util/camera3d.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Configuration for the Plane3D showcase demo
|
|
10
|
+
* Sizes are calculated dynamically based on screen size
|
|
11
|
+
*/
|
|
12
|
+
const CONFIG = {
|
|
13
|
+
// Base sizes (will be scaled)
|
|
14
|
+
basePlaneWidth: 0.12, // fraction of min(width, height)
|
|
15
|
+
basePlaneHeight: 0.09, // fraction of min(width, height)
|
|
16
|
+
baseSpacing: 0.18, // fraction of width
|
|
17
|
+
|
|
18
|
+
camera: {
|
|
19
|
+
perspective: 800,
|
|
20
|
+
rotationX: 0.3,
|
|
21
|
+
rotationY: -0.4,
|
|
22
|
+
inertia: true,
|
|
23
|
+
friction: 0.95,
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
selfRotation: {
|
|
27
|
+
speed: 0.8, // radians per second
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
solidPlane: {
|
|
31
|
+
color: "#4A90D9",
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
gradientPlane: {
|
|
35
|
+
color1: [1.0, 0.2, 0.4],
|
|
36
|
+
color2: [0.2, 0.4, 1.0],
|
|
37
|
+
angle: Math.PI / 4,
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
gridPlane: {
|
|
41
|
+
lineColor: [0.2, 0.9, 0.4],
|
|
42
|
+
backgroundColor: [0.1, 0.1, 0.15],
|
|
43
|
+
gridSize: 8.0,
|
|
44
|
+
lineWidth: 0.06,
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* PlaneData - Holds plane and its world position
|
|
50
|
+
*/
|
|
51
|
+
class PlaneData {
|
|
52
|
+
constructor(plane, worldX, worldY, worldZ, label) {
|
|
53
|
+
this.plane = plane;
|
|
54
|
+
this.worldX = worldX;
|
|
55
|
+
this.worldY = worldY;
|
|
56
|
+
this.worldZ = worldZ;
|
|
57
|
+
this.label = label;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Plane3DDemo - Showcases different Plane3D rendering modes
|
|
63
|
+
*/
|
|
64
|
+
class Plane3DDemo extends Game {
|
|
65
|
+
constructor(canvas) {
|
|
66
|
+
super(canvas);
|
|
67
|
+
this.backgroundColor = "#000000";
|
|
68
|
+
this.enableFluidSize();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
init() {
|
|
72
|
+
super.init();
|
|
73
|
+
|
|
74
|
+
// Create camera with mouse controls
|
|
75
|
+
this.camera = new Camera3D({
|
|
76
|
+
perspective: CONFIG.camera.perspective,
|
|
77
|
+
rotationX: CONFIG.camera.rotationX,
|
|
78
|
+
rotationY: CONFIG.camera.rotationY,
|
|
79
|
+
inertia: CONFIG.camera.inertia,
|
|
80
|
+
friction: CONFIG.camera.friction,
|
|
81
|
+
});
|
|
82
|
+
this.camera.enableMouseControl(this.canvas);
|
|
83
|
+
|
|
84
|
+
// Calculate initial sizes based on screen
|
|
85
|
+
this._updateSizes();
|
|
86
|
+
|
|
87
|
+
// Plane 1: Solid Color (Canvas 2D rendering)
|
|
88
|
+
const solidPlane = new Plane3D(
|
|
89
|
+
this.planeWidth,
|
|
90
|
+
this.planeHeight,
|
|
91
|
+
{
|
|
92
|
+
color: CONFIG.solidPlane.color,
|
|
93
|
+
camera: this.camera,
|
|
94
|
+
}
|
|
95
|
+
);
|
|
96
|
+
this.solidData = new PlaneData(
|
|
97
|
+
solidPlane,
|
|
98
|
+
-this.spacing,
|
|
99
|
+
0,
|
|
100
|
+
0,
|
|
101
|
+
"Solid Color"
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
// Plane 2: Gradient (WebGL shader)
|
|
105
|
+
const gradientPlane = new Plane3D(
|
|
106
|
+
this.planeWidth,
|
|
107
|
+
this.planeHeight,
|
|
108
|
+
{
|
|
109
|
+
camera: this.camera,
|
|
110
|
+
useShader: true,
|
|
111
|
+
shaderType: "gradient",
|
|
112
|
+
shaderUniforms: {
|
|
113
|
+
uColor1: CONFIG.gradientPlane.color1,
|
|
114
|
+
uColor2: CONFIG.gradientPlane.color2,
|
|
115
|
+
uAngle: CONFIG.gradientPlane.angle,
|
|
116
|
+
},
|
|
117
|
+
}
|
|
118
|
+
);
|
|
119
|
+
this.gradientData = new PlaneData(gradientPlane, 0, 0, 0, "Gradient");
|
|
120
|
+
|
|
121
|
+
// Plane 3: Grid (WebGL shader)
|
|
122
|
+
const gridPlane = new Plane3D(
|
|
123
|
+
this.planeWidth,
|
|
124
|
+
this.planeHeight,
|
|
125
|
+
{
|
|
126
|
+
camera: this.camera,
|
|
127
|
+
useShader: true,
|
|
128
|
+
shaderType: "grid",
|
|
129
|
+
shaderUniforms: {
|
|
130
|
+
uLineColor: CONFIG.gridPlane.lineColor,
|
|
131
|
+
uBackgroundColor: CONFIG.gridPlane.backgroundColor,
|
|
132
|
+
uGridSize: CONFIG.gridPlane.gridSize,
|
|
133
|
+
uLineWidth: CONFIG.gridPlane.lineWidth,
|
|
134
|
+
},
|
|
135
|
+
}
|
|
136
|
+
);
|
|
137
|
+
this.gridData = new PlaneData(gridPlane, this.spacing, 0, 0, "Grid");
|
|
138
|
+
|
|
139
|
+
// Store all plane data for rendering
|
|
140
|
+
this.planeDataList = [this.solidData, this.gradientData, this.gridData];
|
|
141
|
+
|
|
142
|
+
// Add FPS counter
|
|
143
|
+
this.pipeline.add(
|
|
144
|
+
new FPSCounter(this, {
|
|
145
|
+
anchor: "bottom-right",
|
|
146
|
+
})
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Calculate sizes based on current screen dimensions
|
|
152
|
+
*/
|
|
153
|
+
_updateSizes() {
|
|
154
|
+
const minDim = Math.min(this.width, this.height);
|
|
155
|
+
this.planeWidth = minDim * CONFIG.basePlaneWidth;
|
|
156
|
+
this.planeHeight = minDim * CONFIG.basePlaneHeight;
|
|
157
|
+
this.spacing = this.width * CONFIG.baseSpacing;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Handle window resize
|
|
162
|
+
*/
|
|
163
|
+
onResize() {
|
|
164
|
+
this._updateSizes();
|
|
165
|
+
|
|
166
|
+
// Update plane dimensions
|
|
167
|
+
if (this.planeDataList) {
|
|
168
|
+
for (const data of this.planeDataList) {
|
|
169
|
+
data.plane.planeWidth = this.planeWidth;
|
|
170
|
+
data.plane.planeHeight = this.planeHeight;
|
|
171
|
+
data.plane._generateGeometry();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Update world positions
|
|
175
|
+
this.solidData.worldX = -this.spacing;
|
|
176
|
+
this.gradientData.worldX = 0;
|
|
177
|
+
this.gridData.worldX = this.spacing;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
update(dt) {
|
|
182
|
+
super.update(dt);
|
|
183
|
+
|
|
184
|
+
// Update camera (for inertia)
|
|
185
|
+
this.camera.update(dt);
|
|
186
|
+
|
|
187
|
+
// Update self-rotation for all planes
|
|
188
|
+
for (const data of this.planeDataList) {
|
|
189
|
+
data.plane.selfRotationY += CONFIG.selfRotation.speed * dt;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
render() {
|
|
194
|
+
super.render();
|
|
195
|
+
|
|
196
|
+
const ctx = this.ctx;
|
|
197
|
+
const centerX = this.width / 2;
|
|
198
|
+
const centerY = this.height / 2;
|
|
199
|
+
|
|
200
|
+
// Project and sort by depth (back to front)
|
|
201
|
+
const projected = this.planeDataList.map((data) => {
|
|
202
|
+
const proj = this.camera.project(
|
|
203
|
+
data.worldX,
|
|
204
|
+
data.worldY,
|
|
205
|
+
data.worldZ
|
|
206
|
+
);
|
|
207
|
+
return { data, proj };
|
|
208
|
+
});
|
|
209
|
+
projected.sort((a, b) => b.proj.z - a.proj.z);
|
|
210
|
+
|
|
211
|
+
// Render each plane at its projected position
|
|
212
|
+
for (const { data, proj } of projected) {
|
|
213
|
+
ctx.save();
|
|
214
|
+
// Translate to screen center + projected position
|
|
215
|
+
ctx.translate(centerX + proj.x, centerY + proj.y);
|
|
216
|
+
// Scale by perspective
|
|
217
|
+
ctx.scale(proj.scale, proj.scale);
|
|
218
|
+
// Render plane at origin
|
|
219
|
+
data.plane.draw();
|
|
220
|
+
ctx.restore();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Render labels (always on top)
|
|
224
|
+
this.renderLabels(projected, centerX, centerY);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Render plane labels at projected positions
|
|
229
|
+
*/
|
|
230
|
+
renderLabels(projected, centerX, centerY) {
|
|
231
|
+
const ctx = this.ctx;
|
|
232
|
+
|
|
233
|
+
for (const { data, proj } of projected) {
|
|
234
|
+
// Calculate label position below the plane
|
|
235
|
+
const screenX = centerX + proj.x;
|
|
236
|
+
const screenY =
|
|
237
|
+
centerY +
|
|
238
|
+
proj.y +
|
|
239
|
+
(this.planeHeight / 2) * proj.scale +
|
|
240
|
+
25;
|
|
241
|
+
|
|
242
|
+
ctx.font = "14px monospace";
|
|
243
|
+
ctx.fillStyle = "#ffffff";
|
|
244
|
+
ctx.textAlign = "center";
|
|
245
|
+
ctx.textBaseline = "top";
|
|
246
|
+
ctx.fillText(data.label, screenX, screenY);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Start the demo
|
|
252
|
+
window.addEventListener("load", () => {
|
|
253
|
+
const canvas = document.getElementById("game");
|
|
254
|
+
const demo = new Plane3DDemo(canvas);
|
|
255
|
+
demo.start();
|
|
256
|
+
});
|