@jiant/canvable 0.0.4 → 0.0.6
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/README.md +34 -14
- package/dist/main.d.mts +89 -11
- package/dist/main.d.ts +89 -11
- package/dist/main.js +300 -60
- package/dist/main.mjs +296 -60
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -24,35 +24,55 @@ yarn add @jiant/canvable
|
|
|
24
24
|
|
|
25
25
|
### Criando uma Cena
|
|
26
26
|
|
|
27
|
+
### Criando uma Cena
|
|
28
|
+
|
|
27
29
|
A seguir está um exemplo básico de como criar uma cena e adicionar um objeto nela:
|
|
28
30
|
|
|
29
31
|
```typescript
|
|
30
|
-
import {
|
|
31
|
-
import "./style.css";
|
|
32
|
+
import { Scene, Square, Vector2D, getCanvas, KinematicBody } from "@jiant/canvable";
|
|
32
33
|
|
|
33
34
|
// Obtendo o canvas
|
|
34
|
-
const canvas = getCanvas("app")
|
|
35
|
+
const canvas = getCanvas("app");
|
|
35
36
|
const ctx = canvas.getContext("2d")!;
|
|
36
37
|
|
|
37
38
|
// Criando a cena
|
|
38
39
|
const scene = new Scene(ctx);
|
|
39
40
|
|
|
40
|
-
// Criando um
|
|
41
|
-
const
|
|
41
|
+
// Criando um objeto (Quadrado)
|
|
42
|
+
const player = new Square(scene, {
|
|
43
|
+
size: new Vector2D(50, 50),
|
|
44
|
+
startsWithBoundingBox: true, // Adicionar colisão automaticamente
|
|
45
|
+
});
|
|
46
|
+
player.pos = new Vector2D(100, 100);
|
|
47
|
+
player.style.fillStyle = "#00FF00"; // Cor customizada
|
|
42
48
|
|
|
43
|
-
// Adicionando
|
|
44
|
-
scene
|
|
49
|
+
// Adicionando física (Corpo Kinemático)
|
|
50
|
+
const body = new KinematicBody(scene, player, {
|
|
51
|
+
speed: 1000,
|
|
52
|
+
friction: 0.9,
|
|
53
|
+
});
|
|
54
|
+
scene.addObject(body);
|
|
55
|
+
|
|
56
|
+
// Iniciando o loop
|
|
57
|
+
scene.run((deltaTime) => {
|
|
58
|
+
// Movimento simples
|
|
59
|
+
if (scene.inputManager.isPressed("ArrowRight")) {
|
|
60
|
+
body.applyForce(1000 * deltaTime, 0, deltaTime);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
45
63
|
```
|
|
46
64
|
|
|
47
|
-
###
|
|
65
|
+
### Playground Interativo
|
|
48
66
|
|
|
49
|
-
|
|
67
|
+
O pacote inclui um exemplo completo de "Playground de Física" com:
|
|
50
68
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
69
|
+
- Controle de personagem (WASD + Setas)
|
|
70
|
+
- Sprint (Shift)
|
|
71
|
+
- Colisões e Física
|
|
72
|
+
- Câmera que segue o jogador
|
|
73
|
+
- Geração procedural de nível
|
|
74
|
+
|
|
75
|
+
Você pode conferir o código fonte em `example/index.ts` ou rodar localmente com `npm run dev`.
|
|
56
76
|
|
|
57
77
|
---
|
|
58
78
|
|
package/dist/main.d.mts
CHANGED
|
@@ -204,15 +204,18 @@ declare class Scene extends Node {
|
|
|
204
204
|
camera: Camera;
|
|
205
205
|
inputManager: InputManager;
|
|
206
206
|
ctx: CanvasRenderingContext2D;
|
|
207
|
+
private animationFrameId;
|
|
208
|
+
private running;
|
|
207
209
|
private lastTime;
|
|
208
210
|
private totalTime;
|
|
209
211
|
constructor(ctx: CanvasRenderingContext2D, opts?: SceneOptions);
|
|
210
212
|
addObject(object: Node): void;
|
|
211
213
|
setup(): void;
|
|
212
214
|
resizeCanvas(): void;
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
215
|
+
updateFrame(deltaTime: number): void;
|
|
216
|
+
drawFrame(ctx: CanvasRenderingContext2D): void;
|
|
217
|
+
run(callback?: (deltaTime: number, totalTime: number) => void, targetFPS?: number): void;
|
|
218
|
+
stop(): void;
|
|
216
219
|
}
|
|
217
220
|
|
|
218
221
|
interface NodeConfig {
|
|
@@ -225,39 +228,114 @@ declare class Node {
|
|
|
225
228
|
id: string;
|
|
226
229
|
children: Node[];
|
|
227
230
|
scene?: Scene;
|
|
231
|
+
boundingBox?: BoundingBox;
|
|
232
|
+
update?(dt: number): void;
|
|
233
|
+
draw?(ctx: CanvasRenderingContext2D, selected?: boolean): void;
|
|
228
234
|
constructor(scene?: Scene, config?: NodeConfig);
|
|
229
235
|
addObject(object: Node): void;
|
|
230
236
|
removeObject(object: Node): void;
|
|
231
237
|
private generateID;
|
|
232
238
|
}
|
|
233
239
|
|
|
240
|
+
interface CollisionResult {
|
|
241
|
+
isColliding: boolean;
|
|
242
|
+
mtv?: Vector2D;
|
|
243
|
+
}
|
|
234
244
|
declare abstract class BoundingBox extends Node {
|
|
235
|
-
abstract checkCollision(other: BoundingBox):
|
|
245
|
+
abstract checkCollision(other: BoundingBox): CollisionResult;
|
|
236
246
|
abstract isPointInside(_point: Vector2D): boolean;
|
|
237
247
|
}
|
|
238
248
|
|
|
249
|
+
declare class CircleBoundingBox extends BoundingBox {
|
|
250
|
+
radius: number;
|
|
251
|
+
constructor(scene?: Scene, radius?: number);
|
|
252
|
+
checkCollision(other: BoundingBox): CollisionResult;
|
|
253
|
+
isPointInside(_point: Vector2D): boolean;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
declare class SquareBoundingBox extends BoundingBox {
|
|
257
|
+
size: Vector2D;
|
|
258
|
+
constructor(scene?: Scene, size?: Vector2D);
|
|
259
|
+
checkCollision(other: BoundingBox): CollisionResult;
|
|
260
|
+
isPointInside(_point: Vector2D): boolean;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
interface KinematicBodyConfig {
|
|
264
|
+
speed?: number;
|
|
265
|
+
friction?: number;
|
|
266
|
+
maxSpeed?: number;
|
|
267
|
+
}
|
|
268
|
+
declare class KinematicBody extends Node {
|
|
269
|
+
shape: Node;
|
|
270
|
+
velocity: Vector2D;
|
|
271
|
+
speed: number;
|
|
272
|
+
friction: number;
|
|
273
|
+
maxSpeed: number;
|
|
274
|
+
isColliding: boolean;
|
|
275
|
+
constructor(scene: Scene, shape: Node, config?: KinematicBodyConfig);
|
|
276
|
+
applyForce(x: number, y: number, dt: number): void;
|
|
277
|
+
update(dt: number): void;
|
|
278
|
+
private resolveCollisions;
|
|
279
|
+
draw(ctx: CanvasRenderingContext2D, selected?: boolean): void;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
declare class StaticBody extends Node {
|
|
283
|
+
shape: Node;
|
|
284
|
+
constructor(scene: Scene, shape: Node);
|
|
285
|
+
update(_dt: number): void;
|
|
286
|
+
draw(ctx: CanvasRenderingContext2D, selected?: boolean): void;
|
|
287
|
+
}
|
|
288
|
+
|
|
239
289
|
interface CircleConfig {
|
|
240
290
|
vel?: Vector2D;
|
|
241
291
|
size?: number;
|
|
242
292
|
friction?: number;
|
|
243
293
|
startsWithBoundingBox?: boolean;
|
|
294
|
+
style?: {
|
|
295
|
+
fillStyle?: string;
|
|
296
|
+
strokeStyle?: string;
|
|
297
|
+
lineWidth?: number;
|
|
298
|
+
};
|
|
244
299
|
}
|
|
245
300
|
declare class Circle extends Node {
|
|
246
301
|
vel: Vector2D;
|
|
247
302
|
size: number;
|
|
248
303
|
friction: number;
|
|
249
|
-
|
|
304
|
+
style: {
|
|
305
|
+
fillStyle?: string;
|
|
306
|
+
strokeStyle?: string;
|
|
307
|
+
lineWidth?: number;
|
|
308
|
+
};
|
|
250
309
|
constructor(scene?: Scene, config?: CircleConfig);
|
|
310
|
+
update(_deltaTime: number): void;
|
|
251
311
|
draw(ctx: CanvasRenderingContext2D, selected?: boolean): void;
|
|
252
312
|
}
|
|
253
313
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
314
|
+
interface SquareConfig {
|
|
315
|
+
vel?: Vector2D;
|
|
316
|
+
size?: Vector2D;
|
|
317
|
+
friction?: number;
|
|
318
|
+
startsWithBoundingBox?: boolean;
|
|
319
|
+
style?: {
|
|
320
|
+
fillStyle?: string;
|
|
321
|
+
strokeStyle?: string;
|
|
322
|
+
lineWidth?: number;
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
declare class Square extends Node {
|
|
326
|
+
vel: Vector2D;
|
|
327
|
+
size: Vector2D;
|
|
328
|
+
friction: number;
|
|
329
|
+
style: {
|
|
330
|
+
fillStyle?: string;
|
|
331
|
+
strokeStyle?: string;
|
|
332
|
+
lineWidth?: number;
|
|
333
|
+
};
|
|
334
|
+
constructor(scene?: Scene, config?: SquareConfig);
|
|
335
|
+
update(_deltaTime: number): void;
|
|
336
|
+
draw(ctx: CanvasRenderingContext2D, selected?: boolean): void;
|
|
259
337
|
}
|
|
260
338
|
|
|
261
339
|
declare function getCanvas(id: string): HTMLCanvasElement;
|
|
262
340
|
|
|
263
|
-
export { BoundingBox, Camera, Circle, CircleBoundingBox, type CircleConfig, type Drawable, Node, type NodeConfig, Scene, type SceneOptions, TWO_PI, type Updatable, Vector2D, getCanvas };
|
|
341
|
+
export { BoundingBox, Camera, Circle, CircleBoundingBox, type CircleConfig, type CollisionResult, type Drawable, KinematicBody, type KinematicBodyConfig, Node, type NodeConfig, Scene, type SceneOptions, Square, SquareBoundingBox, type SquareConfig, StaticBody, TWO_PI, type Updatable, Vector2D, getCanvas };
|
package/dist/main.d.ts
CHANGED
|
@@ -204,15 +204,18 @@ declare class Scene extends Node {
|
|
|
204
204
|
camera: Camera;
|
|
205
205
|
inputManager: InputManager;
|
|
206
206
|
ctx: CanvasRenderingContext2D;
|
|
207
|
+
private animationFrameId;
|
|
208
|
+
private running;
|
|
207
209
|
private lastTime;
|
|
208
210
|
private totalTime;
|
|
209
211
|
constructor(ctx: CanvasRenderingContext2D, opts?: SceneOptions);
|
|
210
212
|
addObject(object: Node): void;
|
|
211
213
|
setup(): void;
|
|
212
214
|
resizeCanvas(): void;
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
215
|
+
updateFrame(deltaTime: number): void;
|
|
216
|
+
drawFrame(ctx: CanvasRenderingContext2D): void;
|
|
217
|
+
run(callback?: (deltaTime: number, totalTime: number) => void, targetFPS?: number): void;
|
|
218
|
+
stop(): void;
|
|
216
219
|
}
|
|
217
220
|
|
|
218
221
|
interface NodeConfig {
|
|
@@ -225,39 +228,114 @@ declare class Node {
|
|
|
225
228
|
id: string;
|
|
226
229
|
children: Node[];
|
|
227
230
|
scene?: Scene;
|
|
231
|
+
boundingBox?: BoundingBox;
|
|
232
|
+
update?(dt: number): void;
|
|
233
|
+
draw?(ctx: CanvasRenderingContext2D, selected?: boolean): void;
|
|
228
234
|
constructor(scene?: Scene, config?: NodeConfig);
|
|
229
235
|
addObject(object: Node): void;
|
|
230
236
|
removeObject(object: Node): void;
|
|
231
237
|
private generateID;
|
|
232
238
|
}
|
|
233
239
|
|
|
240
|
+
interface CollisionResult {
|
|
241
|
+
isColliding: boolean;
|
|
242
|
+
mtv?: Vector2D;
|
|
243
|
+
}
|
|
234
244
|
declare abstract class BoundingBox extends Node {
|
|
235
|
-
abstract checkCollision(other: BoundingBox):
|
|
245
|
+
abstract checkCollision(other: BoundingBox): CollisionResult;
|
|
236
246
|
abstract isPointInside(_point: Vector2D): boolean;
|
|
237
247
|
}
|
|
238
248
|
|
|
249
|
+
declare class CircleBoundingBox extends BoundingBox {
|
|
250
|
+
radius: number;
|
|
251
|
+
constructor(scene?: Scene, radius?: number);
|
|
252
|
+
checkCollision(other: BoundingBox): CollisionResult;
|
|
253
|
+
isPointInside(_point: Vector2D): boolean;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
declare class SquareBoundingBox extends BoundingBox {
|
|
257
|
+
size: Vector2D;
|
|
258
|
+
constructor(scene?: Scene, size?: Vector2D);
|
|
259
|
+
checkCollision(other: BoundingBox): CollisionResult;
|
|
260
|
+
isPointInside(_point: Vector2D): boolean;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
interface KinematicBodyConfig {
|
|
264
|
+
speed?: number;
|
|
265
|
+
friction?: number;
|
|
266
|
+
maxSpeed?: number;
|
|
267
|
+
}
|
|
268
|
+
declare class KinematicBody extends Node {
|
|
269
|
+
shape: Node;
|
|
270
|
+
velocity: Vector2D;
|
|
271
|
+
speed: number;
|
|
272
|
+
friction: number;
|
|
273
|
+
maxSpeed: number;
|
|
274
|
+
isColliding: boolean;
|
|
275
|
+
constructor(scene: Scene, shape: Node, config?: KinematicBodyConfig);
|
|
276
|
+
applyForce(x: number, y: number, dt: number): void;
|
|
277
|
+
update(dt: number): void;
|
|
278
|
+
private resolveCollisions;
|
|
279
|
+
draw(ctx: CanvasRenderingContext2D, selected?: boolean): void;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
declare class StaticBody extends Node {
|
|
283
|
+
shape: Node;
|
|
284
|
+
constructor(scene: Scene, shape: Node);
|
|
285
|
+
update(_dt: number): void;
|
|
286
|
+
draw(ctx: CanvasRenderingContext2D, selected?: boolean): void;
|
|
287
|
+
}
|
|
288
|
+
|
|
239
289
|
interface CircleConfig {
|
|
240
290
|
vel?: Vector2D;
|
|
241
291
|
size?: number;
|
|
242
292
|
friction?: number;
|
|
243
293
|
startsWithBoundingBox?: boolean;
|
|
294
|
+
style?: {
|
|
295
|
+
fillStyle?: string;
|
|
296
|
+
strokeStyle?: string;
|
|
297
|
+
lineWidth?: number;
|
|
298
|
+
};
|
|
244
299
|
}
|
|
245
300
|
declare class Circle extends Node {
|
|
246
301
|
vel: Vector2D;
|
|
247
302
|
size: number;
|
|
248
303
|
friction: number;
|
|
249
|
-
|
|
304
|
+
style: {
|
|
305
|
+
fillStyle?: string;
|
|
306
|
+
strokeStyle?: string;
|
|
307
|
+
lineWidth?: number;
|
|
308
|
+
};
|
|
250
309
|
constructor(scene?: Scene, config?: CircleConfig);
|
|
310
|
+
update(_deltaTime: number): void;
|
|
251
311
|
draw(ctx: CanvasRenderingContext2D, selected?: boolean): void;
|
|
252
312
|
}
|
|
253
313
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
314
|
+
interface SquareConfig {
|
|
315
|
+
vel?: Vector2D;
|
|
316
|
+
size?: Vector2D;
|
|
317
|
+
friction?: number;
|
|
318
|
+
startsWithBoundingBox?: boolean;
|
|
319
|
+
style?: {
|
|
320
|
+
fillStyle?: string;
|
|
321
|
+
strokeStyle?: string;
|
|
322
|
+
lineWidth?: number;
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
declare class Square extends Node {
|
|
326
|
+
vel: Vector2D;
|
|
327
|
+
size: Vector2D;
|
|
328
|
+
friction: number;
|
|
329
|
+
style: {
|
|
330
|
+
fillStyle?: string;
|
|
331
|
+
strokeStyle?: string;
|
|
332
|
+
lineWidth?: number;
|
|
333
|
+
};
|
|
334
|
+
constructor(scene?: Scene, config?: SquareConfig);
|
|
335
|
+
update(_deltaTime: number): void;
|
|
336
|
+
draw(ctx: CanvasRenderingContext2D, selected?: boolean): void;
|
|
259
337
|
}
|
|
260
338
|
|
|
261
339
|
declare function getCanvas(id: string): HTMLCanvasElement;
|
|
262
340
|
|
|
263
|
-
export { BoundingBox, Camera, Circle, CircleBoundingBox, type CircleConfig, type Drawable, Node, type NodeConfig, Scene, type SceneOptions, TWO_PI, type Updatable, Vector2D, getCanvas };
|
|
341
|
+
export { BoundingBox, Camera, Circle, CircleBoundingBox, type CircleConfig, type CollisionResult, type Drawable, KinematicBody, type KinematicBodyConfig, Node, type NodeConfig, Scene, type SceneOptions, Square, SquareBoundingBox, type SquareConfig, StaticBody, TWO_PI, type Updatable, Vector2D, getCanvas };
|
package/dist/main.js
CHANGED
|
@@ -24,8 +24,12 @@ __export(main_exports, {
|
|
|
24
24
|
Camera: () => Camera,
|
|
25
25
|
Circle: () => Circle,
|
|
26
26
|
CircleBoundingBox: () => CircleBoundingBox,
|
|
27
|
+
KinematicBody: () => KinematicBody,
|
|
27
28
|
Node: () => Node,
|
|
28
|
-
Scene: () =>
|
|
29
|
+
Scene: () => Scene,
|
|
30
|
+
Square: () => Square,
|
|
31
|
+
SquareBoundingBox: () => SquareBoundingBox,
|
|
32
|
+
StaticBody: () => StaticBody,
|
|
29
33
|
TWO_PI: () => TWO_PI,
|
|
30
34
|
Vector2D: () => Vector2D,
|
|
31
35
|
getCanvas: () => getCanvas
|
|
@@ -289,13 +293,14 @@ var Vector2D = class _Vector2D {
|
|
|
289
293
|
}
|
|
290
294
|
};
|
|
291
295
|
|
|
292
|
-
// src/nodes/Node.ts
|
|
296
|
+
// src/nodes/shapes/Node.ts
|
|
293
297
|
var import_uuid = require("uuid");
|
|
294
298
|
var Node = class {
|
|
295
299
|
pos;
|
|
296
300
|
id;
|
|
297
301
|
children = [];
|
|
298
302
|
scene;
|
|
303
|
+
boundingBox;
|
|
299
304
|
constructor(scene, config) {
|
|
300
305
|
this.scene = scene;
|
|
301
306
|
const { id, pos } = config || {};
|
|
@@ -322,25 +327,54 @@ var Node = class {
|
|
|
322
327
|
}
|
|
323
328
|
};
|
|
324
329
|
|
|
325
|
-
// src/nodes/BoundingBox.ts
|
|
330
|
+
// src/nodes/boundingBox/BoundingBox.ts
|
|
326
331
|
var BoundingBox = class extends Node {
|
|
327
332
|
};
|
|
328
333
|
|
|
329
|
-
// src/nodes/
|
|
330
|
-
var
|
|
331
|
-
|
|
332
|
-
constructor(
|
|
333
|
-
super();
|
|
334
|
-
this.
|
|
334
|
+
// src/nodes/boundingBox/SquareBoundingBox.ts
|
|
335
|
+
var SquareBoundingBox = class _SquareBoundingBox extends BoundingBox {
|
|
336
|
+
size;
|
|
337
|
+
constructor(scene, size) {
|
|
338
|
+
super(scene);
|
|
339
|
+
this.size = size || new Vector2D(10, 10);
|
|
335
340
|
}
|
|
336
|
-
|
|
341
|
+
checkCollision(other) {
|
|
342
|
+
if (other instanceof _SquareBoundingBox) {
|
|
343
|
+
const thisPoint = this.scene?.camera.transformCoordinates(new Vector2D(this.pos.x, this.pos.y));
|
|
344
|
+
const otherPoint = this.scene?.camera.transformCoordinates(new Vector2D(other.pos.x, other.pos.y));
|
|
345
|
+
if (!thisPoint || !otherPoint) return { isColliding: false };
|
|
346
|
+
const dx = Math.abs(thisPoint.x - otherPoint.x);
|
|
347
|
+
const dy = Math.abs(thisPoint.y - otherPoint.y);
|
|
348
|
+
const halfWidths = (this.size.x + other.size.x) / 2;
|
|
349
|
+
const halfHeights = (this.size.y + other.size.y) / 2;
|
|
350
|
+
if (dx < halfWidths && dy < halfHeights) {
|
|
351
|
+
const overlapX = halfWidths - dx;
|
|
352
|
+
const overlapY = halfHeights - dy;
|
|
353
|
+
if (overlapX < overlapY) {
|
|
354
|
+
const mtvX = thisPoint.x < otherPoint.x ? -overlapX : overlapX;
|
|
355
|
+
return { isColliding: true, mtv: new Vector2D(mtvX, 0) };
|
|
356
|
+
} else {
|
|
357
|
+
const mtvY = thisPoint.y < otherPoint.y ? -overlapY : overlapY;
|
|
358
|
+
return { isColliding: true, mtv: new Vector2D(0, mtvY) };
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
if (other instanceof CircleBoundingBox) {
|
|
363
|
+
const result = other.checkCollision(this);
|
|
364
|
+
if (result.isColliding && result.mtv) {
|
|
365
|
+
return { isColliding: true, mtv: new Vector2D(-result.mtv.x, -result.mtv.y) };
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
return { isColliding: false };
|
|
337
369
|
}
|
|
338
|
-
|
|
339
|
-
|
|
370
|
+
isPointInside(_point) {
|
|
371
|
+
const x_inside = _point.x >= this.pos.x - this.size.x && _point.x <= this.pos.x + this.size.x;
|
|
372
|
+
const y_inside = _point.y >= this.pos.y - this.size.y && _point.y <= this.pos.y + this.size.y;
|
|
373
|
+
return x_inside && y_inside;
|
|
340
374
|
}
|
|
341
375
|
};
|
|
342
376
|
|
|
343
|
-
// src/nodes/CircleBoundingBox.ts
|
|
377
|
+
// src/nodes/boundingBox/CircleBoundingBox.ts
|
|
344
378
|
var CircleBoundingBox = class _CircleBoundingBox extends BoundingBox {
|
|
345
379
|
radius;
|
|
346
380
|
constructor(scene, radius) {
|
|
@@ -349,15 +383,28 @@ var CircleBoundingBox = class _CircleBoundingBox extends BoundingBox {
|
|
|
349
383
|
}
|
|
350
384
|
checkCollision(other) {
|
|
351
385
|
if (other instanceof _CircleBoundingBox) {
|
|
352
|
-
const
|
|
353
|
-
const
|
|
354
|
-
if (!thisPoint || !otherPoint) return false;
|
|
355
|
-
const dx = thisPoint.x - otherPoint.x;
|
|
356
|
-
const dy = thisPoint.y - otherPoint.y;
|
|
386
|
+
const dx = this.pos.x - other.pos.x;
|
|
387
|
+
const dy = this.pos.y - other.pos.y;
|
|
357
388
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
358
|
-
|
|
389
|
+
if (distance < this.radius + other.radius) {
|
|
390
|
+
const overlap = this.radius + other.radius - distance;
|
|
391
|
+
const mtv = distance === 0 ? new Vector2D(overlap, 0) : new Vector2D(dx / distance * overlap, dy / distance * overlap);
|
|
392
|
+
return { isColliding: true, mtv };
|
|
393
|
+
}
|
|
359
394
|
}
|
|
360
|
-
|
|
395
|
+
if (other instanceof SquareBoundingBox) {
|
|
396
|
+
const closestX = Math.max(other.pos.x - other.size.x / 2, Math.min(this.pos.x, other.pos.x + other.size.x / 2));
|
|
397
|
+
const closestY = Math.max(other.pos.y - other.size.y / 2, Math.min(this.pos.y, other.pos.y + other.size.y / 2));
|
|
398
|
+
const dx = this.pos.x - closestX;
|
|
399
|
+
const dy = this.pos.y - closestY;
|
|
400
|
+
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
401
|
+
if (distance < this.radius) {
|
|
402
|
+
const overlap = this.radius - distance;
|
|
403
|
+
const mtv = distance === 0 ? new Vector2D(overlap, 0) : new Vector2D(dx / distance * overlap, dy / distance * overlap);
|
|
404
|
+
return { isColliding: true, mtv };
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
return { isColliding: false };
|
|
361
408
|
}
|
|
362
409
|
isPointInside(_point) {
|
|
363
410
|
const point = this.scene?.camera.transformCoordinates(_point);
|
|
@@ -369,40 +416,104 @@ var CircleBoundingBox = class _CircleBoundingBox extends BoundingBox {
|
|
|
369
416
|
}
|
|
370
417
|
};
|
|
371
418
|
|
|
372
|
-
// src/nodes/
|
|
373
|
-
var
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
this.
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
419
|
+
// src/nodes/Camera.ts
|
|
420
|
+
var Camera = class extends Node {
|
|
421
|
+
zoom;
|
|
422
|
+
constructor(zoom = 1) {
|
|
423
|
+
super();
|
|
424
|
+
this.zoom = zoom;
|
|
425
|
+
}
|
|
426
|
+
update(deltaTime) {
|
|
427
|
+
}
|
|
428
|
+
transformCoordinates(position) {
|
|
429
|
+
return new Vector2D((position.x - this.pos.x) * this.zoom, (position.y - this.pos.y) * this.zoom);
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
// src/nodes/physics/StaticBody.ts
|
|
434
|
+
var StaticBody = class extends Node {
|
|
435
|
+
shape;
|
|
436
|
+
constructor(scene, shape) {
|
|
437
|
+
super(scene, { pos: shape.pos });
|
|
438
|
+
this.shape = shape;
|
|
439
|
+
this.pos = shape.pos;
|
|
440
|
+
}
|
|
441
|
+
update(_dt) {
|
|
442
|
+
if (this.shape.update && typeof this.shape.update === "function") {
|
|
443
|
+
this.shape.update(_dt);
|
|
386
444
|
}
|
|
387
445
|
}
|
|
388
446
|
draw(ctx, selected) {
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
447
|
+
if (this.shape.draw) this.shape.draw(ctx, selected);
|
|
448
|
+
}
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
// src/nodes/physics/KinematicBody.ts
|
|
452
|
+
var KinematicBody = class _KinematicBody extends Node {
|
|
453
|
+
shape;
|
|
454
|
+
velocity = new Vector2D();
|
|
455
|
+
speed = 1500;
|
|
456
|
+
friction = 0.92;
|
|
457
|
+
maxSpeed = 800;
|
|
458
|
+
isColliding = false;
|
|
459
|
+
constructor(scene, shape, config) {
|
|
460
|
+
super(scene, { pos: shape.pos });
|
|
461
|
+
this.shape = shape;
|
|
462
|
+
if (config?.speed) this.speed = config.speed;
|
|
463
|
+
if (config?.friction) this.friction = config.friction;
|
|
464
|
+
if (config?.maxSpeed) this.maxSpeed = config.maxSpeed;
|
|
465
|
+
}
|
|
466
|
+
applyForce(x, y, dt) {
|
|
467
|
+
const len = Math.sqrt(x * x + y * y);
|
|
468
|
+
if (len > 0) {
|
|
469
|
+
const scale = this.speed * dt / len;
|
|
470
|
+
this.velocity.x += x * scale;
|
|
471
|
+
this.velocity.y += y * scale;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
update(dt) {
|
|
475
|
+
this.velocity.x *= this.friction;
|
|
476
|
+
this.velocity.y *= this.friction;
|
|
477
|
+
const currentSpeed = Math.sqrt(this.velocity.x * this.velocity.x + this.velocity.y * this.velocity.y);
|
|
478
|
+
if (currentSpeed > this.maxSpeed) {
|
|
479
|
+
const scale = this.maxSpeed / currentSpeed;
|
|
480
|
+
this.velocity.x *= scale;
|
|
481
|
+
this.velocity.y *= scale;
|
|
482
|
+
}
|
|
483
|
+
if (Math.abs(this.velocity.x) < 0.1) this.velocity.x = 0;
|
|
484
|
+
if (Math.abs(this.velocity.y) < 0.1) this.velocity.y = 0;
|
|
485
|
+
this.pos.x += this.velocity.x * dt;
|
|
486
|
+
this.pos.y += this.velocity.y * dt;
|
|
487
|
+
this.shape.pos = this.pos;
|
|
488
|
+
if (this.shape.update) this.shape.update(dt);
|
|
489
|
+
this.resolveCollisions();
|
|
490
|
+
}
|
|
491
|
+
resolveCollisions() {
|
|
492
|
+
this.isColliding = false;
|
|
493
|
+
if (!this.shape.boundingBox) return;
|
|
494
|
+
if (!this.scene) return;
|
|
495
|
+
for (const obj of this.scene.children) {
|
|
496
|
+
if (obj === this) continue;
|
|
497
|
+
let otherBBox = null;
|
|
498
|
+
if (obj instanceof StaticBody || obj instanceof _KinematicBody) {
|
|
499
|
+
if (obj.shape.boundingBox) otherBBox = obj.shape.boundingBox;
|
|
500
|
+
} else {
|
|
501
|
+
if (obj.boundingBox) otherBBox = obj.boundingBox;
|
|
502
|
+
}
|
|
503
|
+
if (otherBBox) {
|
|
504
|
+
const result = this.shape.boundingBox.checkCollision(otherBBox);
|
|
505
|
+
if (result.isColliding && result.mtv) {
|
|
506
|
+
this.isColliding = true;
|
|
507
|
+
this.pos.x += result.mtv.x;
|
|
508
|
+
this.pos.y += result.mtv.y;
|
|
509
|
+
this.shape.pos = this.pos;
|
|
510
|
+
if (this.shape.boundingBox) this.shape.boundingBox.pos = this.pos;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
400
513
|
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
ctx.fill();
|
|
405
|
-
ctx.closePath();
|
|
514
|
+
}
|
|
515
|
+
draw(ctx, selected) {
|
|
516
|
+
if (this.shape.draw) this.shape.draw(ctx, selected);
|
|
406
517
|
}
|
|
407
518
|
};
|
|
408
519
|
|
|
@@ -428,14 +539,14 @@ var InputManager = class extends Node {
|
|
|
428
539
|
this.mousePos = new Vector2D(event.clientX, event.clientY);
|
|
429
540
|
}
|
|
430
541
|
handleKeyDown(event) {
|
|
431
|
-
const key = event.
|
|
542
|
+
const key = event.code;
|
|
432
543
|
if (!this.keyState.get(key)) {
|
|
433
544
|
this.keyState.set(key, true);
|
|
434
545
|
this.keyJustPressed.add(key);
|
|
435
546
|
}
|
|
436
547
|
}
|
|
437
548
|
handleKeyUp(event) {
|
|
438
|
-
const key = event.
|
|
549
|
+
const key = event.code;
|
|
439
550
|
this.keyState.set(key, false);
|
|
440
551
|
this.keyJustPressed.delete(key);
|
|
441
552
|
}
|
|
@@ -474,10 +585,12 @@ var InputManager = class extends Node {
|
|
|
474
585
|
};
|
|
475
586
|
|
|
476
587
|
// src/nodes/Scene.ts
|
|
477
|
-
var
|
|
588
|
+
var Scene = class extends Node {
|
|
478
589
|
camera;
|
|
479
590
|
inputManager;
|
|
480
591
|
ctx;
|
|
592
|
+
animationFrameId = null;
|
|
593
|
+
running = false;
|
|
481
594
|
lastTime = 0;
|
|
482
595
|
totalTime = 0;
|
|
483
596
|
constructor(ctx, opts) {
|
|
@@ -502,7 +615,7 @@ var Scene2 = class extends Node {
|
|
|
502
615
|
canvas.width = width;
|
|
503
616
|
canvas.height = height;
|
|
504
617
|
}
|
|
505
|
-
|
|
618
|
+
updateFrame(deltaTime) {
|
|
506
619
|
this.camera.update(deltaTime);
|
|
507
620
|
this.children.forEach((child) => {
|
|
508
621
|
if (isUpdatable(child)) {
|
|
@@ -510,16 +623,20 @@ var Scene2 = class extends Node {
|
|
|
510
623
|
}
|
|
511
624
|
});
|
|
512
625
|
}
|
|
513
|
-
|
|
514
|
-
ctx.fillStyle = ctx.canvas.style.backgroundColor;
|
|
626
|
+
drawFrame(ctx) {
|
|
627
|
+
ctx.fillStyle = ctx.canvas.style.backgroundColor || "black";
|
|
515
628
|
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
|
629
|
+
ctx.save();
|
|
630
|
+
ctx.translate(ctx.canvas.width / 2, ctx.canvas.height / 2);
|
|
516
631
|
this.children.forEach((child) => {
|
|
517
632
|
if (isDrawable(child)) {
|
|
518
633
|
child.draw(ctx);
|
|
519
634
|
}
|
|
520
635
|
});
|
|
636
|
+
ctx.restore();
|
|
521
637
|
}
|
|
522
|
-
|
|
638
|
+
run(callback, targetFPS = 60) {
|
|
639
|
+
this.running = true;
|
|
523
640
|
const frameInterval = 1e3 / targetFPS;
|
|
524
641
|
let lastFrameTime = 0;
|
|
525
642
|
const loop = (currentTime) => {
|
|
@@ -530,8 +647,8 @@ var Scene2 = class extends Node {
|
|
|
530
647
|
this.lastTime = currentTime;
|
|
531
648
|
this.totalTime += deltaTime;
|
|
532
649
|
lastFrameTime = currentTime - elapsed % frameInterval;
|
|
533
|
-
this.
|
|
534
|
-
this.
|
|
650
|
+
this.updateFrame(deltaTime);
|
|
651
|
+
this.drawFrame(this.ctx);
|
|
535
652
|
if (callback) {
|
|
536
653
|
callback(deltaTime, this.totalTime);
|
|
537
654
|
}
|
|
@@ -543,7 +660,14 @@ var Scene2 = class extends Node {
|
|
|
543
660
|
requestAnimationFrame(loop);
|
|
544
661
|
};
|
|
545
662
|
this.lastTime = performance.now();
|
|
546
|
-
requestAnimationFrame(loop);
|
|
663
|
+
this.animationFrameId = requestAnimationFrame(loop);
|
|
664
|
+
}
|
|
665
|
+
stop() {
|
|
666
|
+
if (this.animationFrameId) {
|
|
667
|
+
cancelAnimationFrame(this.animationFrameId);
|
|
668
|
+
this.animationFrameId = null;
|
|
669
|
+
}
|
|
670
|
+
this.running = false;
|
|
547
671
|
}
|
|
548
672
|
};
|
|
549
673
|
function isUpdatable(obj) {
|
|
@@ -553,6 +677,118 @@ function isDrawable(obj) {
|
|
|
553
677
|
return "draw" in obj;
|
|
554
678
|
}
|
|
555
679
|
|
|
680
|
+
// src/nodes/shapes/Circle.ts
|
|
681
|
+
var Circle = class extends Node {
|
|
682
|
+
vel = new Vector2D();
|
|
683
|
+
size = 10;
|
|
684
|
+
friction = 0.1;
|
|
685
|
+
style = {
|
|
686
|
+
fillStyle: "blue"
|
|
687
|
+
};
|
|
688
|
+
constructor(scene, config) {
|
|
689
|
+
super(scene);
|
|
690
|
+
this.vel = config?.vel || new Vector2D();
|
|
691
|
+
this.size = config?.size || 10;
|
|
692
|
+
this.friction = config?.friction || 0.1;
|
|
693
|
+
this.style = { ...this.style, ...config?.style };
|
|
694
|
+
if (config?.startsWithBoundingBox) {
|
|
695
|
+
this.boundingBox = new CircleBoundingBox(scene, this.size);
|
|
696
|
+
this.addObject(this.boundingBox);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
update(_deltaTime) {
|
|
700
|
+
if (this.boundingBox) {
|
|
701
|
+
this.boundingBox.pos = this.pos;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
draw(ctx, selected) {
|
|
705
|
+
const cameraPos = this.scene?.camera.pos || new Vector2D();
|
|
706
|
+
const cameraScale = this.scene?.camera.zoom || 1;
|
|
707
|
+
const relativePos = new Vector2D((this.pos.x - cameraPos.x) * cameraScale, (this.pos.y - cameraPos.y) * cameraScale);
|
|
708
|
+
if (selected) {
|
|
709
|
+
ctx.beginPath();
|
|
710
|
+
ctx.arc(relativePos.x, relativePos.y, this.size + 5, 0, Math.PI * 2);
|
|
711
|
+
ctx.fillStyle = "transparent";
|
|
712
|
+
ctx.lineWidth = 3;
|
|
713
|
+
ctx.strokeStyle = "white";
|
|
714
|
+
ctx.stroke();
|
|
715
|
+
ctx.closePath();
|
|
716
|
+
}
|
|
717
|
+
ctx.beginPath();
|
|
718
|
+
ctx.arc(relativePos.x, relativePos.y, this.size, 0, Math.PI * 2);
|
|
719
|
+
ctx.fillStyle = this.style.fillStyle || "blue";
|
|
720
|
+
ctx.fill();
|
|
721
|
+
if (this.style.strokeStyle) {
|
|
722
|
+
ctx.strokeStyle = this.style.strokeStyle;
|
|
723
|
+
ctx.lineWidth = this.style.lineWidth || 1;
|
|
724
|
+
ctx.stroke();
|
|
725
|
+
}
|
|
726
|
+
ctx.closePath();
|
|
727
|
+
}
|
|
728
|
+
};
|
|
729
|
+
|
|
730
|
+
// src/nodes/shapes/Square.ts
|
|
731
|
+
var Square = class extends Node {
|
|
732
|
+
vel = new Vector2D();
|
|
733
|
+
size;
|
|
734
|
+
friction = 0.1;
|
|
735
|
+
style = {
|
|
736
|
+
fillStyle: "orange"
|
|
737
|
+
};
|
|
738
|
+
constructor(scene, config) {
|
|
739
|
+
super(scene);
|
|
740
|
+
this.vel = config?.vel || new Vector2D();
|
|
741
|
+
this.size = config?.size || new Vector2D(10, 10);
|
|
742
|
+
this.friction = config?.friction || 0.1;
|
|
743
|
+
this.style = { ...this.style, ...config?.style };
|
|
744
|
+
if (config?.startsWithBoundingBox) {
|
|
745
|
+
this.boundingBox = new SquareBoundingBox(scene, this.size);
|
|
746
|
+
this.addObject(this.boundingBox);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
update(_deltaTime) {
|
|
750
|
+
if (this.boundingBox) {
|
|
751
|
+
this.boundingBox.pos = this.pos;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
draw(ctx, selected) {
|
|
755
|
+
const cameraPos = this.scene?.camera.pos || new Vector2D();
|
|
756
|
+
const cameraScale = this.scene?.camera.zoom || 1;
|
|
757
|
+
const relativePos = new Vector2D(
|
|
758
|
+
(this.pos.x - cameraPos.x) * cameraScale,
|
|
759
|
+
(this.pos.y - cameraPos.y) * cameraScale
|
|
760
|
+
);
|
|
761
|
+
const scaledSize = new Vector2D(this.size.x * cameraScale, this.size.y * cameraScale);
|
|
762
|
+
if (selected) {
|
|
763
|
+
ctx.strokeStyle = "white";
|
|
764
|
+
ctx.lineWidth = 3;
|
|
765
|
+
ctx.strokeRect(
|
|
766
|
+
relativePos.x - scaledSize.x / 2 - 5,
|
|
767
|
+
relativePos.y - scaledSize.y / 2 - 5,
|
|
768
|
+
scaledSize.x + 10,
|
|
769
|
+
scaledSize.y + 10
|
|
770
|
+
);
|
|
771
|
+
}
|
|
772
|
+
ctx.fillStyle = this.style.fillStyle || "orange";
|
|
773
|
+
ctx.fillRect(
|
|
774
|
+
relativePos.x - scaledSize.x / 2,
|
|
775
|
+
relativePos.y - scaledSize.y / 2,
|
|
776
|
+
scaledSize.x,
|
|
777
|
+
scaledSize.y
|
|
778
|
+
);
|
|
779
|
+
if (this.style.strokeStyle) {
|
|
780
|
+
ctx.strokeStyle = this.style.strokeStyle;
|
|
781
|
+
ctx.lineWidth = this.style.lineWidth || 1;
|
|
782
|
+
ctx.strokeRect(
|
|
783
|
+
relativePos.x - scaledSize.x / 2,
|
|
784
|
+
relativePos.y - scaledSize.y / 2,
|
|
785
|
+
scaledSize.x,
|
|
786
|
+
scaledSize.y
|
|
787
|
+
);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
};
|
|
791
|
+
|
|
556
792
|
// src/main.ts
|
|
557
793
|
function getCanvas(id) {
|
|
558
794
|
const canvas = document.getElementById(id);
|
|
@@ -567,8 +803,12 @@ function getCanvas(id) {
|
|
|
567
803
|
Camera,
|
|
568
804
|
Circle,
|
|
569
805
|
CircleBoundingBox,
|
|
806
|
+
KinematicBody,
|
|
570
807
|
Node,
|
|
571
808
|
Scene,
|
|
809
|
+
Square,
|
|
810
|
+
SquareBoundingBox,
|
|
811
|
+
StaticBody,
|
|
572
812
|
TWO_PI,
|
|
573
813
|
Vector2D,
|
|
574
814
|
getCanvas
|
package/dist/main.mjs
CHANGED
|
@@ -255,13 +255,14 @@ var Vector2D = class _Vector2D {
|
|
|
255
255
|
}
|
|
256
256
|
};
|
|
257
257
|
|
|
258
|
-
// src/nodes/Node.ts
|
|
258
|
+
// src/nodes/shapes/Node.ts
|
|
259
259
|
import { v4 as uuidv4 } from "uuid";
|
|
260
260
|
var Node = class {
|
|
261
261
|
pos;
|
|
262
262
|
id;
|
|
263
263
|
children = [];
|
|
264
264
|
scene;
|
|
265
|
+
boundingBox;
|
|
265
266
|
constructor(scene, config) {
|
|
266
267
|
this.scene = scene;
|
|
267
268
|
const { id, pos } = config || {};
|
|
@@ -288,25 +289,54 @@ var Node = class {
|
|
|
288
289
|
}
|
|
289
290
|
};
|
|
290
291
|
|
|
291
|
-
// src/nodes/BoundingBox.ts
|
|
292
|
+
// src/nodes/boundingBox/BoundingBox.ts
|
|
292
293
|
var BoundingBox = class extends Node {
|
|
293
294
|
};
|
|
294
295
|
|
|
295
|
-
// src/nodes/
|
|
296
|
-
var
|
|
297
|
-
|
|
298
|
-
constructor(
|
|
299
|
-
super();
|
|
300
|
-
this.
|
|
296
|
+
// src/nodes/boundingBox/SquareBoundingBox.ts
|
|
297
|
+
var SquareBoundingBox = class _SquareBoundingBox extends BoundingBox {
|
|
298
|
+
size;
|
|
299
|
+
constructor(scene, size) {
|
|
300
|
+
super(scene);
|
|
301
|
+
this.size = size || new Vector2D(10, 10);
|
|
301
302
|
}
|
|
302
|
-
|
|
303
|
+
checkCollision(other) {
|
|
304
|
+
if (other instanceof _SquareBoundingBox) {
|
|
305
|
+
const thisPoint = this.scene?.camera.transformCoordinates(new Vector2D(this.pos.x, this.pos.y));
|
|
306
|
+
const otherPoint = this.scene?.camera.transformCoordinates(new Vector2D(other.pos.x, other.pos.y));
|
|
307
|
+
if (!thisPoint || !otherPoint) return { isColliding: false };
|
|
308
|
+
const dx = Math.abs(thisPoint.x - otherPoint.x);
|
|
309
|
+
const dy = Math.abs(thisPoint.y - otherPoint.y);
|
|
310
|
+
const halfWidths = (this.size.x + other.size.x) / 2;
|
|
311
|
+
const halfHeights = (this.size.y + other.size.y) / 2;
|
|
312
|
+
if (dx < halfWidths && dy < halfHeights) {
|
|
313
|
+
const overlapX = halfWidths - dx;
|
|
314
|
+
const overlapY = halfHeights - dy;
|
|
315
|
+
if (overlapX < overlapY) {
|
|
316
|
+
const mtvX = thisPoint.x < otherPoint.x ? -overlapX : overlapX;
|
|
317
|
+
return { isColliding: true, mtv: new Vector2D(mtvX, 0) };
|
|
318
|
+
} else {
|
|
319
|
+
const mtvY = thisPoint.y < otherPoint.y ? -overlapY : overlapY;
|
|
320
|
+
return { isColliding: true, mtv: new Vector2D(0, mtvY) };
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
if (other instanceof CircleBoundingBox) {
|
|
325
|
+
const result = other.checkCollision(this);
|
|
326
|
+
if (result.isColliding && result.mtv) {
|
|
327
|
+
return { isColliding: true, mtv: new Vector2D(-result.mtv.x, -result.mtv.y) };
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return { isColliding: false };
|
|
303
331
|
}
|
|
304
|
-
|
|
305
|
-
|
|
332
|
+
isPointInside(_point) {
|
|
333
|
+
const x_inside = _point.x >= this.pos.x - this.size.x && _point.x <= this.pos.x + this.size.x;
|
|
334
|
+
const y_inside = _point.y >= this.pos.y - this.size.y && _point.y <= this.pos.y + this.size.y;
|
|
335
|
+
return x_inside && y_inside;
|
|
306
336
|
}
|
|
307
337
|
};
|
|
308
338
|
|
|
309
|
-
// src/nodes/CircleBoundingBox.ts
|
|
339
|
+
// src/nodes/boundingBox/CircleBoundingBox.ts
|
|
310
340
|
var CircleBoundingBox = class _CircleBoundingBox extends BoundingBox {
|
|
311
341
|
radius;
|
|
312
342
|
constructor(scene, radius) {
|
|
@@ -315,15 +345,28 @@ var CircleBoundingBox = class _CircleBoundingBox extends BoundingBox {
|
|
|
315
345
|
}
|
|
316
346
|
checkCollision(other) {
|
|
317
347
|
if (other instanceof _CircleBoundingBox) {
|
|
318
|
-
const
|
|
319
|
-
const
|
|
320
|
-
if (!thisPoint || !otherPoint) return false;
|
|
321
|
-
const dx = thisPoint.x - otherPoint.x;
|
|
322
|
-
const dy = thisPoint.y - otherPoint.y;
|
|
348
|
+
const dx = this.pos.x - other.pos.x;
|
|
349
|
+
const dy = this.pos.y - other.pos.y;
|
|
323
350
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
324
|
-
|
|
351
|
+
if (distance < this.radius + other.radius) {
|
|
352
|
+
const overlap = this.radius + other.radius - distance;
|
|
353
|
+
const mtv = distance === 0 ? new Vector2D(overlap, 0) : new Vector2D(dx / distance * overlap, dy / distance * overlap);
|
|
354
|
+
return { isColliding: true, mtv };
|
|
355
|
+
}
|
|
325
356
|
}
|
|
326
|
-
|
|
357
|
+
if (other instanceof SquareBoundingBox) {
|
|
358
|
+
const closestX = Math.max(other.pos.x - other.size.x / 2, Math.min(this.pos.x, other.pos.x + other.size.x / 2));
|
|
359
|
+
const closestY = Math.max(other.pos.y - other.size.y / 2, Math.min(this.pos.y, other.pos.y + other.size.y / 2));
|
|
360
|
+
const dx = this.pos.x - closestX;
|
|
361
|
+
const dy = this.pos.y - closestY;
|
|
362
|
+
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
363
|
+
if (distance < this.radius) {
|
|
364
|
+
const overlap = this.radius - distance;
|
|
365
|
+
const mtv = distance === 0 ? new Vector2D(overlap, 0) : new Vector2D(dx / distance * overlap, dy / distance * overlap);
|
|
366
|
+
return { isColliding: true, mtv };
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return { isColliding: false };
|
|
327
370
|
}
|
|
328
371
|
isPointInside(_point) {
|
|
329
372
|
const point = this.scene?.camera.transformCoordinates(_point);
|
|
@@ -335,40 +378,104 @@ var CircleBoundingBox = class _CircleBoundingBox extends BoundingBox {
|
|
|
335
378
|
}
|
|
336
379
|
};
|
|
337
380
|
|
|
338
|
-
// src/nodes/
|
|
339
|
-
var
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
this.
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
381
|
+
// src/nodes/Camera.ts
|
|
382
|
+
var Camera = class extends Node {
|
|
383
|
+
zoom;
|
|
384
|
+
constructor(zoom = 1) {
|
|
385
|
+
super();
|
|
386
|
+
this.zoom = zoom;
|
|
387
|
+
}
|
|
388
|
+
update(deltaTime) {
|
|
389
|
+
}
|
|
390
|
+
transformCoordinates(position) {
|
|
391
|
+
return new Vector2D((position.x - this.pos.x) * this.zoom, (position.y - this.pos.y) * this.zoom);
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
// src/nodes/physics/StaticBody.ts
|
|
396
|
+
var StaticBody = class extends Node {
|
|
397
|
+
shape;
|
|
398
|
+
constructor(scene, shape) {
|
|
399
|
+
super(scene, { pos: shape.pos });
|
|
400
|
+
this.shape = shape;
|
|
401
|
+
this.pos = shape.pos;
|
|
402
|
+
}
|
|
403
|
+
update(_dt) {
|
|
404
|
+
if (this.shape.update && typeof this.shape.update === "function") {
|
|
405
|
+
this.shape.update(_dt);
|
|
352
406
|
}
|
|
353
407
|
}
|
|
354
408
|
draw(ctx, selected) {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
409
|
+
if (this.shape.draw) this.shape.draw(ctx, selected);
|
|
410
|
+
}
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
// src/nodes/physics/KinematicBody.ts
|
|
414
|
+
var KinematicBody = class _KinematicBody extends Node {
|
|
415
|
+
shape;
|
|
416
|
+
velocity = new Vector2D();
|
|
417
|
+
speed = 1500;
|
|
418
|
+
friction = 0.92;
|
|
419
|
+
maxSpeed = 800;
|
|
420
|
+
isColliding = false;
|
|
421
|
+
constructor(scene, shape, config) {
|
|
422
|
+
super(scene, { pos: shape.pos });
|
|
423
|
+
this.shape = shape;
|
|
424
|
+
if (config?.speed) this.speed = config.speed;
|
|
425
|
+
if (config?.friction) this.friction = config.friction;
|
|
426
|
+
if (config?.maxSpeed) this.maxSpeed = config.maxSpeed;
|
|
427
|
+
}
|
|
428
|
+
applyForce(x, y, dt) {
|
|
429
|
+
const len = Math.sqrt(x * x + y * y);
|
|
430
|
+
if (len > 0) {
|
|
431
|
+
const scale = this.speed * dt / len;
|
|
432
|
+
this.velocity.x += x * scale;
|
|
433
|
+
this.velocity.y += y * scale;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
update(dt) {
|
|
437
|
+
this.velocity.x *= this.friction;
|
|
438
|
+
this.velocity.y *= this.friction;
|
|
439
|
+
const currentSpeed = Math.sqrt(this.velocity.x * this.velocity.x + this.velocity.y * this.velocity.y);
|
|
440
|
+
if (currentSpeed > this.maxSpeed) {
|
|
441
|
+
const scale = this.maxSpeed / currentSpeed;
|
|
442
|
+
this.velocity.x *= scale;
|
|
443
|
+
this.velocity.y *= scale;
|
|
444
|
+
}
|
|
445
|
+
if (Math.abs(this.velocity.x) < 0.1) this.velocity.x = 0;
|
|
446
|
+
if (Math.abs(this.velocity.y) < 0.1) this.velocity.y = 0;
|
|
447
|
+
this.pos.x += this.velocity.x * dt;
|
|
448
|
+
this.pos.y += this.velocity.y * dt;
|
|
449
|
+
this.shape.pos = this.pos;
|
|
450
|
+
if (this.shape.update) this.shape.update(dt);
|
|
451
|
+
this.resolveCollisions();
|
|
452
|
+
}
|
|
453
|
+
resolveCollisions() {
|
|
454
|
+
this.isColliding = false;
|
|
455
|
+
if (!this.shape.boundingBox) return;
|
|
456
|
+
if (!this.scene) return;
|
|
457
|
+
for (const obj of this.scene.children) {
|
|
458
|
+
if (obj === this) continue;
|
|
459
|
+
let otherBBox = null;
|
|
460
|
+
if (obj instanceof StaticBody || obj instanceof _KinematicBody) {
|
|
461
|
+
if (obj.shape.boundingBox) otherBBox = obj.shape.boundingBox;
|
|
462
|
+
} else {
|
|
463
|
+
if (obj.boundingBox) otherBBox = obj.boundingBox;
|
|
464
|
+
}
|
|
465
|
+
if (otherBBox) {
|
|
466
|
+
const result = this.shape.boundingBox.checkCollision(otherBBox);
|
|
467
|
+
if (result.isColliding && result.mtv) {
|
|
468
|
+
this.isColliding = true;
|
|
469
|
+
this.pos.x += result.mtv.x;
|
|
470
|
+
this.pos.y += result.mtv.y;
|
|
471
|
+
this.shape.pos = this.pos;
|
|
472
|
+
if (this.shape.boundingBox) this.shape.boundingBox.pos = this.pos;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
366
475
|
}
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
ctx.fill();
|
|
371
|
-
ctx.closePath();
|
|
476
|
+
}
|
|
477
|
+
draw(ctx, selected) {
|
|
478
|
+
if (this.shape.draw) this.shape.draw(ctx, selected);
|
|
372
479
|
}
|
|
373
480
|
};
|
|
374
481
|
|
|
@@ -394,14 +501,14 @@ var InputManager = class extends Node {
|
|
|
394
501
|
this.mousePos = new Vector2D(event.clientX, event.clientY);
|
|
395
502
|
}
|
|
396
503
|
handleKeyDown(event) {
|
|
397
|
-
const key = event.
|
|
504
|
+
const key = event.code;
|
|
398
505
|
if (!this.keyState.get(key)) {
|
|
399
506
|
this.keyState.set(key, true);
|
|
400
507
|
this.keyJustPressed.add(key);
|
|
401
508
|
}
|
|
402
509
|
}
|
|
403
510
|
handleKeyUp(event) {
|
|
404
|
-
const key = event.
|
|
511
|
+
const key = event.code;
|
|
405
512
|
this.keyState.set(key, false);
|
|
406
513
|
this.keyJustPressed.delete(key);
|
|
407
514
|
}
|
|
@@ -440,10 +547,12 @@ var InputManager = class extends Node {
|
|
|
440
547
|
};
|
|
441
548
|
|
|
442
549
|
// src/nodes/Scene.ts
|
|
443
|
-
var
|
|
550
|
+
var Scene = class extends Node {
|
|
444
551
|
camera;
|
|
445
552
|
inputManager;
|
|
446
553
|
ctx;
|
|
554
|
+
animationFrameId = null;
|
|
555
|
+
running = false;
|
|
447
556
|
lastTime = 0;
|
|
448
557
|
totalTime = 0;
|
|
449
558
|
constructor(ctx, opts) {
|
|
@@ -468,7 +577,7 @@ var Scene2 = class extends Node {
|
|
|
468
577
|
canvas.width = width;
|
|
469
578
|
canvas.height = height;
|
|
470
579
|
}
|
|
471
|
-
|
|
580
|
+
updateFrame(deltaTime) {
|
|
472
581
|
this.camera.update(deltaTime);
|
|
473
582
|
this.children.forEach((child) => {
|
|
474
583
|
if (isUpdatable(child)) {
|
|
@@ -476,16 +585,20 @@ var Scene2 = class extends Node {
|
|
|
476
585
|
}
|
|
477
586
|
});
|
|
478
587
|
}
|
|
479
|
-
|
|
480
|
-
ctx.fillStyle = ctx.canvas.style.backgroundColor;
|
|
588
|
+
drawFrame(ctx) {
|
|
589
|
+
ctx.fillStyle = ctx.canvas.style.backgroundColor || "black";
|
|
481
590
|
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
|
591
|
+
ctx.save();
|
|
592
|
+
ctx.translate(ctx.canvas.width / 2, ctx.canvas.height / 2);
|
|
482
593
|
this.children.forEach((child) => {
|
|
483
594
|
if (isDrawable(child)) {
|
|
484
595
|
child.draw(ctx);
|
|
485
596
|
}
|
|
486
597
|
});
|
|
598
|
+
ctx.restore();
|
|
487
599
|
}
|
|
488
|
-
|
|
600
|
+
run(callback, targetFPS = 60) {
|
|
601
|
+
this.running = true;
|
|
489
602
|
const frameInterval = 1e3 / targetFPS;
|
|
490
603
|
let lastFrameTime = 0;
|
|
491
604
|
const loop = (currentTime) => {
|
|
@@ -496,8 +609,8 @@ var Scene2 = class extends Node {
|
|
|
496
609
|
this.lastTime = currentTime;
|
|
497
610
|
this.totalTime += deltaTime;
|
|
498
611
|
lastFrameTime = currentTime - elapsed % frameInterval;
|
|
499
|
-
this.
|
|
500
|
-
this.
|
|
612
|
+
this.updateFrame(deltaTime);
|
|
613
|
+
this.drawFrame(this.ctx);
|
|
501
614
|
if (callback) {
|
|
502
615
|
callback(deltaTime, this.totalTime);
|
|
503
616
|
}
|
|
@@ -509,7 +622,14 @@ var Scene2 = class extends Node {
|
|
|
509
622
|
requestAnimationFrame(loop);
|
|
510
623
|
};
|
|
511
624
|
this.lastTime = performance.now();
|
|
512
|
-
requestAnimationFrame(loop);
|
|
625
|
+
this.animationFrameId = requestAnimationFrame(loop);
|
|
626
|
+
}
|
|
627
|
+
stop() {
|
|
628
|
+
if (this.animationFrameId) {
|
|
629
|
+
cancelAnimationFrame(this.animationFrameId);
|
|
630
|
+
this.animationFrameId = null;
|
|
631
|
+
}
|
|
632
|
+
this.running = false;
|
|
513
633
|
}
|
|
514
634
|
};
|
|
515
635
|
function isUpdatable(obj) {
|
|
@@ -519,6 +639,118 @@ function isDrawable(obj) {
|
|
|
519
639
|
return "draw" in obj;
|
|
520
640
|
}
|
|
521
641
|
|
|
642
|
+
// src/nodes/shapes/Circle.ts
|
|
643
|
+
var Circle = class extends Node {
|
|
644
|
+
vel = new Vector2D();
|
|
645
|
+
size = 10;
|
|
646
|
+
friction = 0.1;
|
|
647
|
+
style = {
|
|
648
|
+
fillStyle: "blue"
|
|
649
|
+
};
|
|
650
|
+
constructor(scene, config) {
|
|
651
|
+
super(scene);
|
|
652
|
+
this.vel = config?.vel || new Vector2D();
|
|
653
|
+
this.size = config?.size || 10;
|
|
654
|
+
this.friction = config?.friction || 0.1;
|
|
655
|
+
this.style = { ...this.style, ...config?.style };
|
|
656
|
+
if (config?.startsWithBoundingBox) {
|
|
657
|
+
this.boundingBox = new CircleBoundingBox(scene, this.size);
|
|
658
|
+
this.addObject(this.boundingBox);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
update(_deltaTime) {
|
|
662
|
+
if (this.boundingBox) {
|
|
663
|
+
this.boundingBox.pos = this.pos;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
draw(ctx, selected) {
|
|
667
|
+
const cameraPos = this.scene?.camera.pos || new Vector2D();
|
|
668
|
+
const cameraScale = this.scene?.camera.zoom || 1;
|
|
669
|
+
const relativePos = new Vector2D((this.pos.x - cameraPos.x) * cameraScale, (this.pos.y - cameraPos.y) * cameraScale);
|
|
670
|
+
if (selected) {
|
|
671
|
+
ctx.beginPath();
|
|
672
|
+
ctx.arc(relativePos.x, relativePos.y, this.size + 5, 0, Math.PI * 2);
|
|
673
|
+
ctx.fillStyle = "transparent";
|
|
674
|
+
ctx.lineWidth = 3;
|
|
675
|
+
ctx.strokeStyle = "white";
|
|
676
|
+
ctx.stroke();
|
|
677
|
+
ctx.closePath();
|
|
678
|
+
}
|
|
679
|
+
ctx.beginPath();
|
|
680
|
+
ctx.arc(relativePos.x, relativePos.y, this.size, 0, Math.PI * 2);
|
|
681
|
+
ctx.fillStyle = this.style.fillStyle || "blue";
|
|
682
|
+
ctx.fill();
|
|
683
|
+
if (this.style.strokeStyle) {
|
|
684
|
+
ctx.strokeStyle = this.style.strokeStyle;
|
|
685
|
+
ctx.lineWidth = this.style.lineWidth || 1;
|
|
686
|
+
ctx.stroke();
|
|
687
|
+
}
|
|
688
|
+
ctx.closePath();
|
|
689
|
+
}
|
|
690
|
+
};
|
|
691
|
+
|
|
692
|
+
// src/nodes/shapes/Square.ts
|
|
693
|
+
var Square = class extends Node {
|
|
694
|
+
vel = new Vector2D();
|
|
695
|
+
size;
|
|
696
|
+
friction = 0.1;
|
|
697
|
+
style = {
|
|
698
|
+
fillStyle: "orange"
|
|
699
|
+
};
|
|
700
|
+
constructor(scene, config) {
|
|
701
|
+
super(scene);
|
|
702
|
+
this.vel = config?.vel || new Vector2D();
|
|
703
|
+
this.size = config?.size || new Vector2D(10, 10);
|
|
704
|
+
this.friction = config?.friction || 0.1;
|
|
705
|
+
this.style = { ...this.style, ...config?.style };
|
|
706
|
+
if (config?.startsWithBoundingBox) {
|
|
707
|
+
this.boundingBox = new SquareBoundingBox(scene, this.size);
|
|
708
|
+
this.addObject(this.boundingBox);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
update(_deltaTime) {
|
|
712
|
+
if (this.boundingBox) {
|
|
713
|
+
this.boundingBox.pos = this.pos;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
draw(ctx, selected) {
|
|
717
|
+
const cameraPos = this.scene?.camera.pos || new Vector2D();
|
|
718
|
+
const cameraScale = this.scene?.camera.zoom || 1;
|
|
719
|
+
const relativePos = new Vector2D(
|
|
720
|
+
(this.pos.x - cameraPos.x) * cameraScale,
|
|
721
|
+
(this.pos.y - cameraPos.y) * cameraScale
|
|
722
|
+
);
|
|
723
|
+
const scaledSize = new Vector2D(this.size.x * cameraScale, this.size.y * cameraScale);
|
|
724
|
+
if (selected) {
|
|
725
|
+
ctx.strokeStyle = "white";
|
|
726
|
+
ctx.lineWidth = 3;
|
|
727
|
+
ctx.strokeRect(
|
|
728
|
+
relativePos.x - scaledSize.x / 2 - 5,
|
|
729
|
+
relativePos.y - scaledSize.y / 2 - 5,
|
|
730
|
+
scaledSize.x + 10,
|
|
731
|
+
scaledSize.y + 10
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
ctx.fillStyle = this.style.fillStyle || "orange";
|
|
735
|
+
ctx.fillRect(
|
|
736
|
+
relativePos.x - scaledSize.x / 2,
|
|
737
|
+
relativePos.y - scaledSize.y / 2,
|
|
738
|
+
scaledSize.x,
|
|
739
|
+
scaledSize.y
|
|
740
|
+
);
|
|
741
|
+
if (this.style.strokeStyle) {
|
|
742
|
+
ctx.strokeStyle = this.style.strokeStyle;
|
|
743
|
+
ctx.lineWidth = this.style.lineWidth || 1;
|
|
744
|
+
ctx.strokeRect(
|
|
745
|
+
relativePos.x - scaledSize.x / 2,
|
|
746
|
+
relativePos.y - scaledSize.y / 2,
|
|
747
|
+
scaledSize.x,
|
|
748
|
+
scaledSize.y
|
|
749
|
+
);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
};
|
|
753
|
+
|
|
522
754
|
// src/main.ts
|
|
523
755
|
function getCanvas(id) {
|
|
524
756
|
const canvas = document.getElementById(id);
|
|
@@ -532,8 +764,12 @@ export {
|
|
|
532
764
|
Camera,
|
|
533
765
|
Circle,
|
|
534
766
|
CircleBoundingBox,
|
|
767
|
+
KinematicBody,
|
|
535
768
|
Node,
|
|
536
|
-
|
|
769
|
+
Scene,
|
|
770
|
+
Square,
|
|
771
|
+
SquareBoundingBox,
|
|
772
|
+
StaticBody,
|
|
537
773
|
TWO_PI,
|
|
538
774
|
Vector2D,
|
|
539
775
|
getCanvas
|