@guinetik/gcanvas 1.0.0 → 1.0.1
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/fluid-simple.html +22 -0
- package/demos/fluid.html +37 -0
- package/demos/index.html +2 -0
- package/demos/js/blob.js +18 -5
- package/demos/js/fluid-simple.js +253 -0
- package/demos/js/fluid.js +527 -0
- package/demos/js/tde/accretiondisk.js +64 -11
- package/demos/js/tde/blackholescene.js +2 -2
- package/demos/js/tde/config.js +2 -2
- package/demos/js/tde/index.js +152 -27
- package/demos/js/tde/lensedstarfield.js +32 -25
- package/demos/js/tde/tdestar.js +78 -98
- package/demos/js/tde/tidalstream.js +23 -7
- package/docs/README.md +230 -222
- package/docs/api/FluidSystem.md +173 -0
- package/docs/concepts/architecture-overview.md +204 -204
- package/docs/concepts/rendering-pipeline.md +279 -279
- package/docs/concepts/two-layer-architecture.md +229 -229
- package/docs/fluid-dynamics.md +97 -0
- package/docs/getting-started/first-game.md +354 -354
- package/docs/getting-started/installation.md +175 -157
- package/docs/modules/collision/README.md +2 -2
- package/docs/modules/fluent/README.md +6 -6
- package/docs/modules/game/README.md +303 -303
- package/docs/modules/isometric-camera.md +2 -2
- package/docs/modules/isometric.md +1 -1
- package/docs/modules/painter/README.md +328 -328
- package/docs/modules/particle/README.md +3 -3
- package/docs/modules/shapes/README.md +221 -221
- package/docs/modules/shapes/base/euclidian.md +123 -123
- package/docs/modules/shapes/base/shape.md +262 -262
- package/docs/modules/shapes/base/transformable.md +243 -243
- package/docs/modules/state/README.md +2 -2
- package/docs/modules/util/README.md +1 -1
- package/docs/modules/util/camera3d.md +3 -3
- package/docs/modules/util/scene3d.md +1 -1
- package/package.json +3 -1
- package/readme.md +19 -5
- package/src/collision/collision.js +75 -0
- package/src/game/index.js +2 -1
- package/src/game/pipeline.js +3 -3
- package/src/game/systems/FluidSystem.js +835 -0
- package/src/game/systems/index.js +11 -0
- package/src/game/ui/button.js +39 -18
- package/src/game/ui/cursor.js +14 -0
- package/src/game/ui/fps.js +12 -4
- package/src/game/ui/index.js +2 -0
- package/src/game/ui/stepper.js +549 -0
- package/src/game/ui/theme.js +121 -0
- package/src/game/ui/togglebutton.js +9 -3
- package/src/game/ui/tooltip.js +11 -4
- package/src/math/fluid.js +507 -0
- package/src/math/index.js +2 -0
- package/src/mixins/anchor.js +17 -7
- package/src/motion/tweenetik.js +16 -0
- package/src/shapes/index.js +1 -0
- package/src/util/camera3d.js +218 -12
- package/types/fluent.d.ts +361 -0
- package/types/game.d.ts +303 -0
- package/types/index.d.ts +144 -5
- package/types/math.d.ts +361 -0
- package/types/motion.d.ts +271 -0
- package/types/particle.d.ts +373 -0
- package/types/shapes.d.ts +107 -9
- package/types/util.d.ts +353 -0
- package/types/webgl.d.ts +109 -0
- package/disk_example.png +0 -0
- package/tde.png +0 -0
|
@@ -1,354 +1,354 @@
|
|
|
1
|
-
# First Game
|
|
2
|
-
|
|
3
|
-
> Create an interactive game with the Game layer.
|
|
4
|
-
|
|
5
|
-
## Overview
|
|
6
|
-
|
|
7
|
-
This guide shows how to use the Game layer to create interactive applications. We'll build a simple game with keyboard input, collision detection, and animations.
|
|
8
|
-
|
|
9
|
-
## Basic Game Setup
|
|
10
|
-
|
|
11
|
-
```js
|
|
12
|
-
import { Game, Scene, GameObject, Circle, Group, Rectangle, TextShape } from 'gcanvas';
|
|
13
|
-
|
|
14
|
-
class MyGame extends Game {
|
|
15
|
-
constructor(canvas) {
|
|
16
|
-
super(canvas);
|
|
17
|
-
this.enableFluidSize(); // Canvas fills window
|
|
18
|
-
this.backgroundColor = '#1a1a2e';
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
init() {
|
|
22
|
-
super.init(); // Initialize input systems
|
|
23
|
-
|
|
24
|
-
// Create a scene
|
|
25
|
-
this.scene = new Scene(this);
|
|
26
|
-
this.pipeline.add(this.scene);
|
|
27
|
-
|
|
28
|
-
// Add game objects
|
|
29
|
-
this.scene.add(new Player(this));
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Start the game
|
|
34
|
-
const canvas = document.getElementById('game');
|
|
35
|
-
const game = new MyGame(canvas);
|
|
36
|
-
game.start();
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
## Creating a Player
|
|
40
|
-
|
|
41
|
-
```js
|
|
42
|
-
class Player extends GameObject {
|
|
43
|
-
constructor(game) {
|
|
44
|
-
super(game);
|
|
45
|
-
|
|
46
|
-
// Create the player shape
|
|
47
|
-
this.shape = new Circle(30, {
|
|
48
|
-
color: '#4ecdc4',
|
|
49
|
-
stroke: '#fff',
|
|
50
|
-
lineWidth: 2
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
// Position at center
|
|
54
|
-
this.shape.x = game.width / 2;
|
|
55
|
-
this.shape.y = game.height / 2;
|
|
56
|
-
|
|
57
|
-
// Movement speed (pixels per second)
|
|
58
|
-
this.speed = 300;
|
|
59
|
-
|
|
60
|
-
// Enable mouse/touch input on this shape
|
|
61
|
-
this.enableInteractivity(this.shape);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
update(dt) {
|
|
65
|
-
// Handle keyboard input
|
|
66
|
-
const input = this.game.input;
|
|
67
|
-
|
|
68
|
-
if (input.isKeyDown('ArrowLeft') || input.isKeyDown('KeyA')) {
|
|
69
|
-
this.shape.x -= this.speed * dt;
|
|
70
|
-
}
|
|
71
|
-
if (input.isKeyDown('ArrowRight') || input.isKeyDown('KeyD')) {
|
|
72
|
-
this.shape.x += this.speed * dt;
|
|
73
|
-
}
|
|
74
|
-
if (input.isKeyDown('ArrowUp') || input.isKeyDown('KeyW')) {
|
|
75
|
-
this.shape.y -= this.speed * dt;
|
|
76
|
-
}
|
|
77
|
-
if (input.isKeyDown('ArrowDown') || input.isKeyDown('KeyS')) {
|
|
78
|
-
this.shape.y += this.speed * dt;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Keep player on screen
|
|
82
|
-
this.shape.x = Math.max(30, Math.min(this.game.width - 30, this.shape.x));
|
|
83
|
-
this.shape.y = Math.max(30, Math.min(this.game.height - 30, this.shape.y));
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
render() {
|
|
87
|
-
this.shape.draw();
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Called when player is clicked
|
|
91
|
-
onPointerDown(e) {
|
|
92
|
-
console.log('Player clicked!');
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
## Adding Enemies
|
|
98
|
-
|
|
99
|
-
```js
|
|
100
|
-
class Enemy extends GameObject {
|
|
101
|
-
constructor(game, x, y) {
|
|
102
|
-
super(game);
|
|
103
|
-
|
|
104
|
-
this.shape = new Rectangle({
|
|
105
|
-
x: x,
|
|
106
|
-
y: y,
|
|
107
|
-
width: 40,
|
|
108
|
-
height: 40,
|
|
109
|
-
color: '#ff6b6b'
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
// Random direction
|
|
113
|
-
this.vx = (Math.random() - 0.5) * 200;
|
|
114
|
-
this.vy = (Math.random() - 0.5) * 200;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
update(dt) {
|
|
118
|
-
// Move
|
|
119
|
-
this.shape.x += this.vx * dt;
|
|
120
|
-
this.shape.y += this.vy * dt;
|
|
121
|
-
|
|
122
|
-
// Bounce off walls
|
|
123
|
-
if (this.shape.x < 20 || this.shape.x > this.game.width - 20) {
|
|
124
|
-
this.vx *= -1;
|
|
125
|
-
}
|
|
126
|
-
if (this.shape.y < 20 || this.shape.y > this.game.height - 20) {
|
|
127
|
-
this.vy *= -1;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// Rotate
|
|
131
|
-
this.shape.rotation += dt * 2;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
render() {
|
|
135
|
-
this.shape.draw();
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
## Complete Game Example
|
|
141
|
-
|
|
142
|
-
Here's a complete game from `demos/js/basic.js`:
|
|
143
|
-
|
|
144
|
-
```js
|
|
145
|
-
import {
|
|
146
|
-
Game,
|
|
147
|
-
GameObject,
|
|
148
|
-
Scene,
|
|
149
|
-
Circle,
|
|
150
|
-
Rectangle,
|
|
151
|
-
TextShape,
|
|
152
|
-
Group,
|
|
153
|
-
Motion,
|
|
154
|
-
Easing,
|
|
155
|
-
FPSCounter
|
|
156
|
-
} from 'gcanvas';
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* HelloWorldBox - A simple animated text box
|
|
160
|
-
*/
|
|
161
|
-
class HelloWorldBox extends GameObject {
|
|
162
|
-
constructor(game) {
|
|
163
|
-
super(game);
|
|
164
|
-
|
|
165
|
-
// Create a group to hold shapes
|
|
166
|
-
this.group = new Group({});
|
|
167
|
-
|
|
168
|
-
// Background box
|
|
169
|
-
this.box = new Rectangle({
|
|
170
|
-
width: 200,
|
|
171
|
-
height: 80,
|
|
172
|
-
color: '#333'
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
// Text label
|
|
176
|
-
this.label = new TextShape('Hello World!', {
|
|
177
|
-
font: '18px monospace',
|
|
178
|
-
color: '#0f0',
|
|
179
|
-
align: 'center',
|
|
180
|
-
baseline: 'middle'
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
// Add to group
|
|
184
|
-
this.group.add(this.box);
|
|
185
|
-
this.group.add(this.label);
|
|
186
|
-
|
|
187
|
-
// Animation time
|
|
188
|
-
this.animTime = 0;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
update(dt) {
|
|
192
|
-
this.animTime += dt;
|
|
193
|
-
|
|
194
|
-
// Pulse the text opacity
|
|
195
|
-
const pulse = Motion.pulse(
|
|
196
|
-
0, 1, // min, max opacity
|
|
197
|
-
this.animTime,
|
|
198
|
-
2, // 2 second cycle
|
|
199
|
-
true, // loop
|
|
200
|
-
false, // no yoyo
|
|
201
|
-
Easing.easeInOutSine
|
|
202
|
-
);
|
|
203
|
-
this.label.opacity = pulse.value;
|
|
204
|
-
|
|
205
|
-
// Float the group
|
|
206
|
-
const float = Motion.float(
|
|
207
|
-
{ x: 0, y: 0 },
|
|
208
|
-
this.animTime,
|
|
209
|
-
5, // 5 second cycle
|
|
210
|
-
0.5, // speed
|
|
211
|
-
0.5, // randomness
|
|
212
|
-
50, // radius
|
|
213
|
-
true,
|
|
214
|
-
Easing.easeInOutSine
|
|
215
|
-
);
|
|
216
|
-
this.group.x = float.x;
|
|
217
|
-
this.group.y = float.y;
|
|
218
|
-
|
|
219
|
-
super.update(dt);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
render() {
|
|
223
|
-
this.group.render();
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* DemoGame - Main game class
|
|
229
|
-
*/
|
|
230
|
-
class DemoGame extends Game {
|
|
231
|
-
constructor(canvas) {
|
|
232
|
-
super(canvas);
|
|
233
|
-
this.enableFluidSize();
|
|
234
|
-
this.backgroundColor = 'black';
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
init() {
|
|
238
|
-
super.init();
|
|
239
|
-
|
|
240
|
-
// Create main scene
|
|
241
|
-
this.gameScene = new Scene(this);
|
|
242
|
-
this.gameScene.add(new HelloWorldBox(this));
|
|
243
|
-
this.pipeline.add(this.gameScene);
|
|
244
|
-
|
|
245
|
-
// Add orbiting circle
|
|
246
|
-
this.floatingCircle = new Circle(30, {
|
|
247
|
-
x: this.width / 2 + 200,
|
|
248
|
-
y: this.height / 2,
|
|
249
|
-
color: '#0f0'
|
|
250
|
-
});
|
|
251
|
-
this.floatingCircle.animTime = 0;
|
|
252
|
-
this.pipeline.add(this.floatingCircle);
|
|
253
|
-
|
|
254
|
-
// Add FPS counter
|
|
255
|
-
this.pipeline.add(new FPSCounter(this, {
|
|
256
|
-
anchor: 'bottom-right'
|
|
257
|
-
}));
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
update(dt) {
|
|
261
|
-
super.update(dt);
|
|
262
|
-
|
|
263
|
-
// Center the scene
|
|
264
|
-
this.gameScene.x = this.width / 2;
|
|
265
|
-
this.gameScene.y = this.height / 2;
|
|
266
|
-
|
|
267
|
-
// Orbit the circle
|
|
268
|
-
this.floatingCircle.animTime += dt;
|
|
269
|
-
const orbit = Motion.orbit(
|
|
270
|
-
this.width / 2, this.height / 2,
|
|
271
|
-
200, 200, // radius X, Y
|
|
272
|
-
0, // start angle
|
|
273
|
-
this.floatingCircle.animTime,
|
|
274
|
-
8, // 8 second orbit
|
|
275
|
-
true, // loop
|
|
276
|
-
true // clockwise
|
|
277
|
-
);
|
|
278
|
-
this.floatingCircle.x = orbit.x;
|
|
279
|
-
this.floatingCircle.y = orbit.y;
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// Run the game
|
|
284
|
-
window.addEventListener('load', () => {
|
|
285
|
-
const canvas = document.getElementById('game');
|
|
286
|
-
const game = new DemoGame(canvas);
|
|
287
|
-
game.start();
|
|
288
|
-
});
|
|
289
|
-
```
|
|
290
|
-
|
|
291
|
-
## HTML Template
|
|
292
|
-
|
|
293
|
-
```html
|
|
294
|
-
<!DOCTYPE html>
|
|
295
|
-
<html>
|
|
296
|
-
<head>
|
|
297
|
-
<meta charset="UTF-8">
|
|
298
|
-
<title>My First Game</title>
|
|
299
|
-
<style>
|
|
300
|
-
body { margin: 0; overflow: hidden; }
|
|
301
|
-
canvas { display: block; }
|
|
302
|
-
</style>
|
|
303
|
-
</head>
|
|
304
|
-
<body>
|
|
305
|
-
<canvas id="game"></canvas>
|
|
306
|
-
<script type="module" src="./game.js"></script>
|
|
307
|
-
</body>
|
|
308
|
-
</html>
|
|
309
|
-
```
|
|
310
|
-
|
|
311
|
-
## Key Concepts
|
|
312
|
-
|
|
313
|
-
| Concept | Description |
|
|
314
|
-
|---------|-------------|
|
|
315
|
-
| `Game` | Main class managing loop, canvas, input |
|
|
316
|
-
| `Scene` | Container for GameObjects |
|
|
317
|
-
| `GameObject` | Interactive entity with update/render |
|
|
318
|
-
| `enableInteractivity(shape)` | Enable click/hover on a shape |
|
|
319
|
-
| `dt` | Delta time in seconds |
|
|
320
|
-
| `game.input.isKeyDown(key)` | Check if key is pressed |
|
|
321
|
-
| `Motion.*` | Animation helper functions |
|
|
322
|
-
| `FPSCounter` | Built-in FPS display |
|
|
323
|
-
|
|
324
|
-
## Input Reference
|
|
325
|
-
|
|
326
|
-
```js
|
|
327
|
-
// Keyboard
|
|
328
|
-
game.input.isKeyDown('ArrowUp')
|
|
329
|
-
game.input.isKeyDown('KeyW')
|
|
330
|
-
game.input.isKeyDown('Space')
|
|
331
|
-
|
|
332
|
-
// Mouse position
|
|
333
|
-
game.mouse.x
|
|
334
|
-
game.mouse.y
|
|
335
|
-
|
|
336
|
-
// GameObject events
|
|
337
|
-
onPointerDown(e) { }
|
|
338
|
-
onPointerUp(e) { }
|
|
339
|
-
onPointerMove(e) { }
|
|
340
|
-
onMouseOver() { }
|
|
341
|
-
onMouseOut() { }
|
|
342
|
-
```
|
|
343
|
-
|
|
344
|
-
## Next Steps
|
|
345
|
-
|
|
346
|
-
- [Game Module](../modules/game/README.md) - Full Game API reference
|
|
347
|
-
- [Motion Patterns](../modules/motion/README.md) - Animation functions
|
|
348
|
-
- [Input Handling](../modules/io/README.md) - Mouse, keyboard, touch
|
|
349
|
-
|
|
350
|
-
## Related
|
|
351
|
-
|
|
352
|
-
- [Hello World](./hello-world.md)
|
|
353
|
-
- [Two-Layer Architecture](../concepts/two-layer-architecture.md)
|
|
354
|
-
- [Game Lifecycle](../concepts/lifecycle.md)
|
|
1
|
+
# First Game
|
|
2
|
+
|
|
3
|
+
> Create an interactive game with the Game layer.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This guide shows how to use the Game layer to create interactive applications. We'll build a simple game with keyboard input, collision detection, and animations.
|
|
8
|
+
|
|
9
|
+
## Basic Game Setup
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
import { Game, Scene, GameObject, Circle, Group, Rectangle, TextShape } from '@guinetik/gcanvas';
|
|
13
|
+
|
|
14
|
+
class MyGame extends Game {
|
|
15
|
+
constructor(canvas) {
|
|
16
|
+
super(canvas);
|
|
17
|
+
this.enableFluidSize(); // Canvas fills window
|
|
18
|
+
this.backgroundColor = '#1a1a2e';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
init() {
|
|
22
|
+
super.init(); // Initialize input systems
|
|
23
|
+
|
|
24
|
+
// Create a scene
|
|
25
|
+
this.scene = new Scene(this);
|
|
26
|
+
this.pipeline.add(this.scene);
|
|
27
|
+
|
|
28
|
+
// Add game objects
|
|
29
|
+
this.scene.add(new Player(this));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Start the game
|
|
34
|
+
const canvas = document.getElementById('game');
|
|
35
|
+
const game = new MyGame(canvas);
|
|
36
|
+
game.start();
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Creating a Player
|
|
40
|
+
|
|
41
|
+
```js
|
|
42
|
+
class Player extends GameObject {
|
|
43
|
+
constructor(game) {
|
|
44
|
+
super(game);
|
|
45
|
+
|
|
46
|
+
// Create the player shape
|
|
47
|
+
this.shape = new Circle(30, {
|
|
48
|
+
color: '#4ecdc4',
|
|
49
|
+
stroke: '#fff',
|
|
50
|
+
lineWidth: 2
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Position at center
|
|
54
|
+
this.shape.x = game.width / 2;
|
|
55
|
+
this.shape.y = game.height / 2;
|
|
56
|
+
|
|
57
|
+
// Movement speed (pixels per second)
|
|
58
|
+
this.speed = 300;
|
|
59
|
+
|
|
60
|
+
// Enable mouse/touch input on this shape
|
|
61
|
+
this.enableInteractivity(this.shape);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
update(dt) {
|
|
65
|
+
// Handle keyboard input
|
|
66
|
+
const input = this.game.input;
|
|
67
|
+
|
|
68
|
+
if (input.isKeyDown('ArrowLeft') || input.isKeyDown('KeyA')) {
|
|
69
|
+
this.shape.x -= this.speed * dt;
|
|
70
|
+
}
|
|
71
|
+
if (input.isKeyDown('ArrowRight') || input.isKeyDown('KeyD')) {
|
|
72
|
+
this.shape.x += this.speed * dt;
|
|
73
|
+
}
|
|
74
|
+
if (input.isKeyDown('ArrowUp') || input.isKeyDown('KeyW')) {
|
|
75
|
+
this.shape.y -= this.speed * dt;
|
|
76
|
+
}
|
|
77
|
+
if (input.isKeyDown('ArrowDown') || input.isKeyDown('KeyS')) {
|
|
78
|
+
this.shape.y += this.speed * dt;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Keep player on screen
|
|
82
|
+
this.shape.x = Math.max(30, Math.min(this.game.width - 30, this.shape.x));
|
|
83
|
+
this.shape.y = Math.max(30, Math.min(this.game.height - 30, this.shape.y));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
render() {
|
|
87
|
+
this.shape.draw();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Called when player is clicked
|
|
91
|
+
onPointerDown(e) {
|
|
92
|
+
console.log('Player clicked!');
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Adding Enemies
|
|
98
|
+
|
|
99
|
+
```js
|
|
100
|
+
class Enemy extends GameObject {
|
|
101
|
+
constructor(game, x, y) {
|
|
102
|
+
super(game);
|
|
103
|
+
|
|
104
|
+
this.shape = new Rectangle({
|
|
105
|
+
x: x,
|
|
106
|
+
y: y,
|
|
107
|
+
width: 40,
|
|
108
|
+
height: 40,
|
|
109
|
+
color: '#ff6b6b'
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Random direction
|
|
113
|
+
this.vx = (Math.random() - 0.5) * 200;
|
|
114
|
+
this.vy = (Math.random() - 0.5) * 200;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
update(dt) {
|
|
118
|
+
// Move
|
|
119
|
+
this.shape.x += this.vx * dt;
|
|
120
|
+
this.shape.y += this.vy * dt;
|
|
121
|
+
|
|
122
|
+
// Bounce off walls
|
|
123
|
+
if (this.shape.x < 20 || this.shape.x > this.game.width - 20) {
|
|
124
|
+
this.vx *= -1;
|
|
125
|
+
}
|
|
126
|
+
if (this.shape.y < 20 || this.shape.y > this.game.height - 20) {
|
|
127
|
+
this.vy *= -1;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Rotate
|
|
131
|
+
this.shape.rotation += dt * 2;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
render() {
|
|
135
|
+
this.shape.draw();
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Complete Game Example
|
|
141
|
+
|
|
142
|
+
Here's a complete game from `demos/js/basic.js`:
|
|
143
|
+
|
|
144
|
+
```js
|
|
145
|
+
import {
|
|
146
|
+
Game,
|
|
147
|
+
GameObject,
|
|
148
|
+
Scene,
|
|
149
|
+
Circle,
|
|
150
|
+
Rectangle,
|
|
151
|
+
TextShape,
|
|
152
|
+
Group,
|
|
153
|
+
Motion,
|
|
154
|
+
Easing,
|
|
155
|
+
FPSCounter
|
|
156
|
+
} from '@guinetik/gcanvas';
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* HelloWorldBox - A simple animated text box
|
|
160
|
+
*/
|
|
161
|
+
class HelloWorldBox extends GameObject {
|
|
162
|
+
constructor(game) {
|
|
163
|
+
super(game);
|
|
164
|
+
|
|
165
|
+
// Create a group to hold shapes
|
|
166
|
+
this.group = new Group({});
|
|
167
|
+
|
|
168
|
+
// Background box
|
|
169
|
+
this.box = new Rectangle({
|
|
170
|
+
width: 200,
|
|
171
|
+
height: 80,
|
|
172
|
+
color: '#333'
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// Text label
|
|
176
|
+
this.label = new TextShape('Hello World!', {
|
|
177
|
+
font: '18px monospace',
|
|
178
|
+
color: '#0f0',
|
|
179
|
+
align: 'center',
|
|
180
|
+
baseline: 'middle'
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// Add to group
|
|
184
|
+
this.group.add(this.box);
|
|
185
|
+
this.group.add(this.label);
|
|
186
|
+
|
|
187
|
+
// Animation time
|
|
188
|
+
this.animTime = 0;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
update(dt) {
|
|
192
|
+
this.animTime += dt;
|
|
193
|
+
|
|
194
|
+
// Pulse the text opacity
|
|
195
|
+
const pulse = Motion.pulse(
|
|
196
|
+
0, 1, // min, max opacity
|
|
197
|
+
this.animTime,
|
|
198
|
+
2, // 2 second cycle
|
|
199
|
+
true, // loop
|
|
200
|
+
false, // no yoyo
|
|
201
|
+
Easing.easeInOutSine
|
|
202
|
+
);
|
|
203
|
+
this.label.opacity = pulse.value;
|
|
204
|
+
|
|
205
|
+
// Float the group
|
|
206
|
+
const float = Motion.float(
|
|
207
|
+
{ x: 0, y: 0 },
|
|
208
|
+
this.animTime,
|
|
209
|
+
5, // 5 second cycle
|
|
210
|
+
0.5, // speed
|
|
211
|
+
0.5, // randomness
|
|
212
|
+
50, // radius
|
|
213
|
+
true,
|
|
214
|
+
Easing.easeInOutSine
|
|
215
|
+
);
|
|
216
|
+
this.group.x = float.x;
|
|
217
|
+
this.group.y = float.y;
|
|
218
|
+
|
|
219
|
+
super.update(dt);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
render() {
|
|
223
|
+
this.group.render();
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* DemoGame - Main game class
|
|
229
|
+
*/
|
|
230
|
+
class DemoGame extends Game {
|
|
231
|
+
constructor(canvas) {
|
|
232
|
+
super(canvas);
|
|
233
|
+
this.enableFluidSize();
|
|
234
|
+
this.backgroundColor = 'black';
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
init() {
|
|
238
|
+
super.init();
|
|
239
|
+
|
|
240
|
+
// Create main scene
|
|
241
|
+
this.gameScene = new Scene(this);
|
|
242
|
+
this.gameScene.add(new HelloWorldBox(this));
|
|
243
|
+
this.pipeline.add(this.gameScene);
|
|
244
|
+
|
|
245
|
+
// Add orbiting circle
|
|
246
|
+
this.floatingCircle = new Circle(30, {
|
|
247
|
+
x: this.width / 2 + 200,
|
|
248
|
+
y: this.height / 2,
|
|
249
|
+
color: '#0f0'
|
|
250
|
+
});
|
|
251
|
+
this.floatingCircle.animTime = 0;
|
|
252
|
+
this.pipeline.add(this.floatingCircle);
|
|
253
|
+
|
|
254
|
+
// Add FPS counter
|
|
255
|
+
this.pipeline.add(new FPSCounter(this, {
|
|
256
|
+
anchor: 'bottom-right'
|
|
257
|
+
}));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
update(dt) {
|
|
261
|
+
super.update(dt);
|
|
262
|
+
|
|
263
|
+
// Center the scene
|
|
264
|
+
this.gameScene.x = this.width / 2;
|
|
265
|
+
this.gameScene.y = this.height / 2;
|
|
266
|
+
|
|
267
|
+
// Orbit the circle
|
|
268
|
+
this.floatingCircle.animTime += dt;
|
|
269
|
+
const orbit = Motion.orbit(
|
|
270
|
+
this.width / 2, this.height / 2,
|
|
271
|
+
200, 200, // radius X, Y
|
|
272
|
+
0, // start angle
|
|
273
|
+
this.floatingCircle.animTime,
|
|
274
|
+
8, // 8 second orbit
|
|
275
|
+
true, // loop
|
|
276
|
+
true // clockwise
|
|
277
|
+
);
|
|
278
|
+
this.floatingCircle.x = orbit.x;
|
|
279
|
+
this.floatingCircle.y = orbit.y;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Run the game
|
|
284
|
+
window.addEventListener('load', () => {
|
|
285
|
+
const canvas = document.getElementById('game');
|
|
286
|
+
const game = new DemoGame(canvas);
|
|
287
|
+
game.start();
|
|
288
|
+
});
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
## HTML Template
|
|
292
|
+
|
|
293
|
+
```html
|
|
294
|
+
<!DOCTYPE html>
|
|
295
|
+
<html>
|
|
296
|
+
<head>
|
|
297
|
+
<meta charset="UTF-8">
|
|
298
|
+
<title>My First Game</title>
|
|
299
|
+
<style>
|
|
300
|
+
body { margin: 0; overflow: hidden; }
|
|
301
|
+
canvas { display: block; }
|
|
302
|
+
</style>
|
|
303
|
+
</head>
|
|
304
|
+
<body>
|
|
305
|
+
<canvas id="game"></canvas>
|
|
306
|
+
<script type="module" src="./game.js"></script>
|
|
307
|
+
</body>
|
|
308
|
+
</html>
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
## Key Concepts
|
|
312
|
+
|
|
313
|
+
| Concept | Description |
|
|
314
|
+
|---------|-------------|
|
|
315
|
+
| `Game` | Main class managing loop, canvas, input |
|
|
316
|
+
| `Scene` | Container for GameObjects |
|
|
317
|
+
| `GameObject` | Interactive entity with update/render |
|
|
318
|
+
| `enableInteractivity(shape)` | Enable click/hover on a shape |
|
|
319
|
+
| `dt` | Delta time in seconds |
|
|
320
|
+
| `game.input.isKeyDown(key)` | Check if key is pressed |
|
|
321
|
+
| `Motion.*` | Animation helper functions |
|
|
322
|
+
| `FPSCounter` | Built-in FPS display |
|
|
323
|
+
|
|
324
|
+
## Input Reference
|
|
325
|
+
|
|
326
|
+
```js
|
|
327
|
+
// Keyboard
|
|
328
|
+
game.input.isKeyDown('ArrowUp')
|
|
329
|
+
game.input.isKeyDown('KeyW')
|
|
330
|
+
game.input.isKeyDown('Space')
|
|
331
|
+
|
|
332
|
+
// Mouse position
|
|
333
|
+
game.mouse.x
|
|
334
|
+
game.mouse.y
|
|
335
|
+
|
|
336
|
+
// GameObject events
|
|
337
|
+
onPointerDown(e) { }
|
|
338
|
+
onPointerUp(e) { }
|
|
339
|
+
onPointerMove(e) { }
|
|
340
|
+
onMouseOver() { }
|
|
341
|
+
onMouseOut() { }
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
## Next Steps
|
|
345
|
+
|
|
346
|
+
- [Game Module](../modules/game/README.md) - Full Game API reference
|
|
347
|
+
- [Motion Patterns](../modules/motion/README.md) - Animation functions
|
|
348
|
+
- [Input Handling](../modules/io/README.md) - Mouse, keyboard, touch
|
|
349
|
+
|
|
350
|
+
## Related
|
|
351
|
+
|
|
352
|
+
- [Hello World](./hello-world.md)
|
|
353
|
+
- [Two-Layer Architecture](../concepts/two-layer-architecture.md)
|
|
354
|
+
- [Game Lifecycle](../concepts/lifecycle.md)
|