jygame 0.1.0
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/LICENSE +674 -0
- package/README.md +62 -0
- package/assets/FontLoader.js +23 -0
- package/assets/ImageLoader.js +37 -0
- package/collision/Collision.js +51 -0
- package/core/Game.js +147 -0
- package/core/Scene.js +17 -0
- package/display/Group.js +107 -0
- package/display/Sprite.js +80 -0
- package/geometry/Rect.js +96 -0
- package/input/Input.js +173 -0
- package/jygame.js +14 -0
- package/math/Vec2.js +90 -0
- package/package.json +41 -0
- package/state/State.js +42 -0
- package/storage/Storage.js +29 -0
- package/time/Clock.js +35 -0
- package/time/Timer.js +54 -0
package/README.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# jygame
|
|
2
|
+
|
|
3
|
+
A lightweight 2D game framework for the browser.
|
|
4
|
+
|
|
5
|
+
```js
|
|
6
|
+
import { Game, Scene, Sprite } from "jygame";
|
|
7
|
+
|
|
8
|
+
class MyScene extends Scene {
|
|
9
|
+
constructor() {
|
|
10
|
+
super();
|
|
11
|
+
this.player = new Sprite(100, 100, 32, 32);
|
|
12
|
+
this.player.style.fill = "#ff6600";
|
|
13
|
+
this.root.appendChild(this.player.rect);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
update(dt) {
|
|
17
|
+
this.player.velocity.x = 0;
|
|
18
|
+
this.player.velocity.y = 0;
|
|
19
|
+
if (Input.isDown("RIGHT")) this.player.velocity.x = 200;
|
|
20
|
+
if (Input.isDown("LEFT")) this.player.velocity.x = -200;
|
|
21
|
+
if (Input.isDown("UP")) this.player.velocity.y = -200;
|
|
22
|
+
if (Input.isDown("DOWN")) this.player.velocity.y = 200;
|
|
23
|
+
this.player.update(dt);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
render(ctx) {
|
|
27
|
+
this.player.render(ctx);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const game = new Game({ parent: document.body, width: 800, height: 600 });
|
|
32
|
+
game.run(new MyScene());
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Install
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
npm install jygame
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## API
|
|
42
|
+
|
|
43
|
+
| Import | Description |
|
|
44
|
+
|---|---|
|
|
45
|
+
| `Game` | Main game loop with fixed timestep, canvas setup, UI layer, and scene management |
|
|
46
|
+
| `Scene` | Lifecycle hooks: `enter`, `exit`, `pause`, `resume`, `update`, `render`, `renderUI` |
|
|
47
|
+
| `Sprite` | Drawable with position, velocity, angle, scale, images, and shape styles |
|
|
48
|
+
| `Group` | Collection of sprites with batch update/render/collision methods |
|
|
49
|
+
| `Vec2` | 2D vector with add, sub, scale, dot, normalize, rotate, lerp |
|
|
50
|
+
| `Rect` | AABB rectangle with collision, containment, overlap, and anchor helpers |
|
|
51
|
+
| `Clock` | Fixed-timestep accumulator for deterministic updates |
|
|
52
|
+
| `Timer` | Countdown timer with optional looping |
|
|
53
|
+
| `Input` | Keyboard (WASD/arrows) and touch (swipe/tap) input handling |
|
|
54
|
+
| `Collision` | AABB, circle, point-rect, rect-circle, and group collision detection |
|
|
55
|
+
| `State` | Observable state container with subscribe/unsubscribe |
|
|
56
|
+
| `Storage` | `localStorage` wrapper with JSON serialization |
|
|
57
|
+
| `ImageLoader` | Image preloading with in-memory cache |
|
|
58
|
+
| `FontLoader` | FontFace loading for custom web fonts |
|
|
59
|
+
|
|
60
|
+
## License
|
|
61
|
+
|
|
62
|
+
GNU General Public License v3.0
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const _loaded = new Set();
|
|
2
|
+
|
|
3
|
+
export const FontLoader = {
|
|
4
|
+
async load(family, path) {
|
|
5
|
+
if (_loaded.has(family)) return;
|
|
6
|
+
|
|
7
|
+
const font = new FontFace(family, `url(${path})`);
|
|
8
|
+
await font.load();
|
|
9
|
+
document.fonts.add(font);
|
|
10
|
+
_loaded.add(family);
|
|
11
|
+
},
|
|
12
|
+
|
|
13
|
+
async loadAll(map) {
|
|
14
|
+
const entries = Object.entries(map);
|
|
15
|
+
await Promise.all(
|
|
16
|
+
entries.map(([family, path]) => this.load(family, path))
|
|
17
|
+
);
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
isLoaded(family) {
|
|
21
|
+
return _loaded.has(family);
|
|
22
|
+
},
|
|
23
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const _cache = new Map();
|
|
2
|
+
|
|
3
|
+
export const ImageLoader = {
|
|
4
|
+
load(path) {
|
|
5
|
+
if (_cache.has(path)) return Promise.resolve(_cache.get(path));
|
|
6
|
+
|
|
7
|
+
return new Promise((resolve, reject) => {
|
|
8
|
+
const img = new Image();
|
|
9
|
+
img.onload = () => {
|
|
10
|
+
_cache.set(path, img);
|
|
11
|
+
resolve(img);
|
|
12
|
+
};
|
|
13
|
+
img.onerror = () => reject(new Error(`Failed to load image: ${path}`));
|
|
14
|
+
img.src = path;
|
|
15
|
+
});
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
loadAll(map) {
|
|
19
|
+
const entries = Object.entries(map);
|
|
20
|
+
return Promise.all(
|
|
21
|
+
entries.map(([key, path]) =>
|
|
22
|
+
this.load(path).then((img) => {
|
|
23
|
+
_cache.set(key, img);
|
|
24
|
+
return [key, img];
|
|
25
|
+
})
|
|
26
|
+
)
|
|
27
|
+
).then((results) => Object.fromEntries(results));
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
get(key) {
|
|
31
|
+
return _cache.get(key) || null;
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
has(key) {
|
|
35
|
+
return _cache.has(key);
|
|
36
|
+
},
|
|
37
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export const Collision = {
|
|
2
|
+
rectRect(a, b) {
|
|
3
|
+
return a.collides(b);
|
|
4
|
+
},
|
|
5
|
+
|
|
6
|
+
circleCircle(a, b) {
|
|
7
|
+
const dx = a.x - b.x;
|
|
8
|
+
const dy = a.y - b.y;
|
|
9
|
+
const r = a.radius + b.radius;
|
|
10
|
+
return dx * dx + dy * dy <= r * r;
|
|
11
|
+
},
|
|
12
|
+
|
|
13
|
+
pointInRect(point, rect) {
|
|
14
|
+
return rect.contains(point);
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
rectCircle(rect, circle) {
|
|
18
|
+
const cx = circle.x;
|
|
19
|
+
const cy = circle.y;
|
|
20
|
+
const r = circle.radius;
|
|
21
|
+
const nearX = Math.max(rect.left, Math.min(cx, rect.right));
|
|
22
|
+
const nearY = Math.max(rect.top, Math.min(cy, rect.bottom));
|
|
23
|
+
const dx = cx - nearX;
|
|
24
|
+
const dy = cy - nearY;
|
|
25
|
+
return dx * dx + dy * dy <= r * r;
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
groupRect(group, rect) {
|
|
29
|
+
const hits = [];
|
|
30
|
+
for (const sprite of group._sprites) {
|
|
31
|
+
if (sprite.visible && Collision.rectRect(sprite.rect, rect)) {
|
|
32
|
+
hits.push(sprite);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return hits;
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
groupGroup(a, b) {
|
|
39
|
+
const pairs = [];
|
|
40
|
+
for (const sa of a._sprites) {
|
|
41
|
+
if (!sa.visible) continue;
|
|
42
|
+
for (const sb of b._sprites) {
|
|
43
|
+
if (!sb.visible) continue;
|
|
44
|
+
if (Collision.rectRect(sa.rect, sb.rect)) {
|
|
45
|
+
pairs.push([sa, sb]);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return pairs;
|
|
50
|
+
},
|
|
51
|
+
};
|
package/core/Game.js
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { Clock } from "../time/Clock.js";
|
|
2
|
+
import { Input } from "../input/Input.js";
|
|
3
|
+
|
|
4
|
+
export class Game {
|
|
5
|
+
constructor({ parent, width, height, fps = 60 }) {
|
|
6
|
+
const container = typeof parent === "string"
|
|
7
|
+
? document.querySelector(parent)
|
|
8
|
+
: parent;
|
|
9
|
+
|
|
10
|
+
this.canvas = document.createElement("canvas");
|
|
11
|
+
this.canvas.width = width;
|
|
12
|
+
this.canvas.height = height;
|
|
13
|
+
this.canvas.style.display = "block";
|
|
14
|
+
container.appendChild(this.canvas);
|
|
15
|
+
|
|
16
|
+
this.domLayer = document.createElement("div");
|
|
17
|
+
this.domLayer.className = "jygame-ui";
|
|
18
|
+
this.domLayer.style.position = "absolute";
|
|
19
|
+
this.domLayer.style.top = "0";
|
|
20
|
+
this.domLayer.style.left = "0";
|
|
21
|
+
this.domLayer.style.width = "100%";
|
|
22
|
+
this.domLayer.style.height = "100%";
|
|
23
|
+
container.appendChild(this.domLayer);
|
|
24
|
+
|
|
25
|
+
if (getComputedStyle(container).position === "static") {
|
|
26
|
+
container.style.position = "relative";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
this.ctx = this.canvas.getContext("2d");
|
|
30
|
+
this.width = width;
|
|
31
|
+
this.height = height;
|
|
32
|
+
this.clock = new Clock(fps);
|
|
33
|
+
this.scene = null;
|
|
34
|
+
this._running = false;
|
|
35
|
+
this._paused = false;
|
|
36
|
+
this._lastTime = 0;
|
|
37
|
+
this._rafId = null;
|
|
38
|
+
this.fps = 60;
|
|
39
|
+
|
|
40
|
+
Input.init();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
get isPaused() {
|
|
44
|
+
return this._paused;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
pause() {
|
|
48
|
+
if (this._paused) return;
|
|
49
|
+
this._paused = true;
|
|
50
|
+
this.scene?.pause?.();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
resume() {
|
|
54
|
+
if (!this._paused) return;
|
|
55
|
+
this._paused = false;
|
|
56
|
+
this.scene?.resume?.();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
togglePause() {
|
|
60
|
+
this._paused ? this.resume() : this.pause();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
run(scene) {
|
|
64
|
+
this.domLayer.append(scene.root);
|
|
65
|
+
scene.dom = scene.root;
|
|
66
|
+
scene.game = this;
|
|
67
|
+
this.scene = scene;
|
|
68
|
+
this.clock.reset();
|
|
69
|
+
scene.enter();
|
|
70
|
+
this._applyUI(scene);
|
|
71
|
+
this._running = true;
|
|
72
|
+
this._lastTime = performance.now();
|
|
73
|
+
this._rafId = requestAnimationFrame((t) => this._loop(t));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
switchScene(scene) {
|
|
77
|
+
this._paused = false;
|
|
78
|
+
this.scene.exit();
|
|
79
|
+
this.scene.root.remove();
|
|
80
|
+
Input.updateFrame();
|
|
81
|
+
this.domLayer.append(scene.root);
|
|
82
|
+
scene.dom = scene.root;
|
|
83
|
+
scene.game = this;
|
|
84
|
+
this.scene = scene;
|
|
85
|
+
this.clock.reset();
|
|
86
|
+
scene.enter();
|
|
87
|
+
this._applyUI(scene);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
refreshUI() {
|
|
91
|
+
this._applyUI(this.scene);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
patchUI(updates) {
|
|
95
|
+
const root = this.scene?.root;
|
|
96
|
+
if (!root) return;
|
|
97
|
+
for (const [id, content] of Object.entries(updates)) {
|
|
98
|
+
const el = root.querySelector("#" + id);
|
|
99
|
+
if (el && el.textContent !== String(content)) {
|
|
100
|
+
el.textContent = content;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
_applyUI(scene) {
|
|
106
|
+
const html = scene.renderUI();
|
|
107
|
+
if (html !== undefined && html !== null) {
|
|
108
|
+
scene.root.innerHTML = html;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
_loop(time) {
|
|
113
|
+
if (!this._running) return;
|
|
114
|
+
|
|
115
|
+
const realDt = (time - this._lastTime) / 1000;
|
|
116
|
+
this._lastTime = time;
|
|
117
|
+
|
|
118
|
+
const ticks = this.clock.tick(realDt);
|
|
119
|
+
|
|
120
|
+
if (ticks > 0) {
|
|
121
|
+
this.scene.update(this.clock.fixedDt);
|
|
122
|
+
Input.clearJustPressed();
|
|
123
|
+
for (let i = 1; i < ticks; i++) {
|
|
124
|
+
if (this._paused) break;
|
|
125
|
+
this.scene.update(this.clock.fixedDt);
|
|
126
|
+
}
|
|
127
|
+
} else {
|
|
128
|
+
this.scene.update(0);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
Input.updateFrame();
|
|
132
|
+
|
|
133
|
+
this.fps += ((1 / Math.max(realDt, 0.001)) - this.fps) * 0.05;
|
|
134
|
+
|
|
135
|
+
this.ctx.clearRect(0, 0, this.width, this.height);
|
|
136
|
+
this.scene.render(this.ctx);
|
|
137
|
+
|
|
138
|
+
this._rafId = requestAnimationFrame((t) => this._loop(t));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
destroy() {
|
|
142
|
+
this._running = false;
|
|
143
|
+
if (this._rafId) cancelAnimationFrame(this._rafId);
|
|
144
|
+
this.scene.exit();
|
|
145
|
+
Input.destroy();
|
|
146
|
+
}
|
|
147
|
+
}
|
package/core/Scene.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export class Scene {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.dom = null;
|
|
4
|
+
this.root = document.createElement("div");
|
|
5
|
+
}
|
|
6
|
+
enter() {}
|
|
7
|
+
exit() {}
|
|
8
|
+
pause() {}
|
|
9
|
+
resume() {}
|
|
10
|
+
update(dt) {}
|
|
11
|
+
render(ctx) {}
|
|
12
|
+
renderUI() {}
|
|
13
|
+
|
|
14
|
+
transitionTo(scene) {
|
|
15
|
+
if (this.game) this.game.switchScene(scene);
|
|
16
|
+
}
|
|
17
|
+
}
|
package/display/Group.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
export class Group {
|
|
2
|
+
constructor() {
|
|
3
|
+
this._sprites = [];
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
add(sprite) {
|
|
7
|
+
if (this._sprites.includes(sprite)) return;
|
|
8
|
+
this._sprites.push(sprite);
|
|
9
|
+
sprite.groups.push(this);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
remove(sprite) {
|
|
13
|
+
const idx = this._sprites.indexOf(sprite);
|
|
14
|
+
if (idx === -1) return;
|
|
15
|
+
this._sprites.splice(idx, 1);
|
|
16
|
+
const gidx = sprite.groups.indexOf(this);
|
|
17
|
+
if (gidx !== -1) sprite.groups.splice(gidx, 1);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
has(sprite) {
|
|
21
|
+
return this._sprites.includes(sprite);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
clear() {
|
|
25
|
+
for (const sprite of this._sprites) {
|
|
26
|
+
const gidx = sprite.groups.indexOf(this);
|
|
27
|
+
if (gidx !== -1) sprite.groups.splice(gidx, 1);
|
|
28
|
+
}
|
|
29
|
+
this._sprites = [];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
get length() {
|
|
33
|
+
return this._sprites.length;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
update(dt) {
|
|
37
|
+
for (const sprite of this._sprites) {
|
|
38
|
+
sprite.update(dt);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
render(ctx) {
|
|
43
|
+
for (const sprite of this._sprites) {
|
|
44
|
+
sprite.render(ctx);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
collideRect(rect) {
|
|
49
|
+
const hits = [];
|
|
50
|
+
for (const sprite of this._sprites) {
|
|
51
|
+
if (!sprite.visible) continue;
|
|
52
|
+
if (sprite.rect.collides(rect)) {
|
|
53
|
+
hits.push(sprite);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return hits;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
collidePoint(point) {
|
|
60
|
+
const hits = [];
|
|
61
|
+
for (const sprite of this._sprites) {
|
|
62
|
+
if (!sprite.visible) continue;
|
|
63
|
+
if (sprite.rect.contains(point)) {
|
|
64
|
+
hits.push(sprite);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return hits;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
collideGroup(other) {
|
|
71
|
+
const pairs = [];
|
|
72
|
+
for (const sa of this._sprites) {
|
|
73
|
+
if (!sa.visible) continue;
|
|
74
|
+
for (const sb of other._sprites) {
|
|
75
|
+
if (!sb.visible) continue;
|
|
76
|
+
if (sa.rect.collides(sb.rect)) {
|
|
77
|
+
pairs.push([sa, sb]);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return pairs;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
collideSprite(sprite) {
|
|
85
|
+
const hits = [];
|
|
86
|
+
if (!sprite.visible) return hits;
|
|
87
|
+
for (const s of this._sprites) {
|
|
88
|
+
if (!s.visible) continue;
|
|
89
|
+
if (s.rect.collides(sprite.rect)) {
|
|
90
|
+
hits.push(s);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return hits;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
forEach(fn) {
|
|
97
|
+
this._sprites.forEach(fn);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
filter(fn) {
|
|
101
|
+
return this._sprites.filter(fn);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
map(fn) {
|
|
105
|
+
return this._sprites.map(fn);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { Rect } from "../geometry/Rect.js";
|
|
2
|
+
import { Vec2 } from "../math/Vec2.js";
|
|
3
|
+
|
|
4
|
+
export class Sprite {
|
|
5
|
+
constructor(x, y, w, h) {
|
|
6
|
+
this.rect = new Rect(x, y, w, h);
|
|
7
|
+
this.position = new Vec2(x, y);
|
|
8
|
+
this.velocity = new Vec2(0, 0);
|
|
9
|
+
this.angle = 0;
|
|
10
|
+
this.scale = new Vec2(1, 1);
|
|
11
|
+
this.visible = true;
|
|
12
|
+
this.groups = [];
|
|
13
|
+
this.image = null;
|
|
14
|
+
this.style = {
|
|
15
|
+
fill: "#ffffff",
|
|
16
|
+
shape: "rect",
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
get x() { return this.rect.x; }
|
|
21
|
+
set x(v) { this.rect.x = v; }
|
|
22
|
+
get y() { return this.rect.y; }
|
|
23
|
+
set y(v) { this.rect.y = v; }
|
|
24
|
+
get width() { return this.rect.w; }
|
|
25
|
+
set width(v) { this.rect.w = v; }
|
|
26
|
+
get height() { return this.rect.h; }
|
|
27
|
+
set height(v) { this.rect.h = v; }
|
|
28
|
+
|
|
29
|
+
update(dt) {
|
|
30
|
+
this.rect.x += this.velocity.x * dt;
|
|
31
|
+
this.rect.y += this.velocity.y * dt;
|
|
32
|
+
this.position.x = this.rect.centerx;
|
|
33
|
+
this.position.y = this.rect.centery;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
render(ctx) {
|
|
37
|
+
if (!this.visible) return;
|
|
38
|
+
|
|
39
|
+
ctx.save();
|
|
40
|
+
ctx.translate(this.rect.centerx, this.rect.centery);
|
|
41
|
+
ctx.rotate(this.angle);
|
|
42
|
+
ctx.scale(this.scale.x, this.scale.y);
|
|
43
|
+
this.draw(ctx);
|
|
44
|
+
ctx.restore();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
draw(ctx) {
|
|
48
|
+
const hw = this.rect.w / 2;
|
|
49
|
+
const hh = this.rect.h / 2;
|
|
50
|
+
|
|
51
|
+
if (this.image) {
|
|
52
|
+
ctx.drawImage(this.image, -hw, -hh, this.rect.w, this.rect.h);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const s = this.style;
|
|
57
|
+
if (!s.fill) return;
|
|
58
|
+
|
|
59
|
+
ctx.fillStyle = s.fill;
|
|
60
|
+
|
|
61
|
+
if (s.shape === "circle") {
|
|
62
|
+
ctx.beginPath();
|
|
63
|
+
ctx.arc(0, 0, Math.min(hw, hh), 0, Math.PI * 2);
|
|
64
|
+
ctx.fill();
|
|
65
|
+
} else if (s.shape === "ellipse") {
|
|
66
|
+
ctx.beginPath();
|
|
67
|
+
ctx.ellipse(0, 0, hw, hh, 0, 0, Math.PI * 2);
|
|
68
|
+
ctx.fill();
|
|
69
|
+
} else {
|
|
70
|
+
ctx.fillRect(-hw, -hh, this.rect.w, this.rect.h);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
kill() {
|
|
75
|
+
for (const group of this.groups) {
|
|
76
|
+
group.remove(this);
|
|
77
|
+
}
|
|
78
|
+
this.groups = [];
|
|
79
|
+
}
|
|
80
|
+
}
|
package/geometry/Rect.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
export class Rect {
|
|
2
|
+
constructor(x = 0, y = 0, w = 0, h = 0) {
|
|
3
|
+
this.x = x;
|
|
4
|
+
this.y = y;
|
|
5
|
+
this.w = w;
|
|
6
|
+
this.h = h;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
get left() { return this.x; }
|
|
10
|
+
set left(v) { this.x = v; }
|
|
11
|
+
get right() { return this.x + this.w; }
|
|
12
|
+
set right(v) { this.x = v - this.w; }
|
|
13
|
+
get top() { return this.y; }
|
|
14
|
+
set top(v) { this.y = v; }
|
|
15
|
+
get bottom() { return this.y + this.h; }
|
|
16
|
+
set bottom(v) { this.y = v - this.h; }
|
|
17
|
+
|
|
18
|
+
get centerx() { return this.x + this.w / 2; }
|
|
19
|
+
set centerx(v) { this.x = v - this.w / 2; }
|
|
20
|
+
get centery() { return this.y + this.h / 2; }
|
|
21
|
+
set centery(v) { this.y = v - this.h / 2; }
|
|
22
|
+
|
|
23
|
+
get topleft() { return { x: this.x, y: this.y }; }
|
|
24
|
+
set topleft(p) { this.x = p.x; this.y = p.y; }
|
|
25
|
+
get topright() { return { x: this.right, y: this.y }; }
|
|
26
|
+
set topright(p) { this.right = p.x; this.y = p.y; }
|
|
27
|
+
get bottomleft() { return { x: this.x, y: this.bottom }; }
|
|
28
|
+
set bottomleft(p) { this.x = p.x; this.bottom = p.y; }
|
|
29
|
+
get bottomright() { return { x: this.right, y: this.bottom }; }
|
|
30
|
+
set bottomright(p){ this.right = p.x; this.bottom = p.y; }
|
|
31
|
+
|
|
32
|
+
get midtop() { return { x: this.centerx, y: this.y }; }
|
|
33
|
+
set midtop(p) { this.centerx = p.x; this.y = p.y; }
|
|
34
|
+
get midleft() { return { x: this.x, y: this.centery }; }
|
|
35
|
+
set midleft(p) { this.x = p.x; this.centery = p.y; }
|
|
36
|
+
get midbottom() { return { x: this.centerx, y: this.bottom }; }
|
|
37
|
+
set midbottom(p){ this.centerx = p.x; this.bottom = p.y; }
|
|
38
|
+
get midright() { return { x: this.right, y: this.centery }; }
|
|
39
|
+
set midright(p) { this.right = p.x; this.centery = p.y; }
|
|
40
|
+
|
|
41
|
+
get center() { return { x: this.centerx, y: this.centery }; }
|
|
42
|
+
set center(p) { this.centerx = p.x; this.centery = p.y; }
|
|
43
|
+
|
|
44
|
+
collides(other) {
|
|
45
|
+
return (
|
|
46
|
+
this.left < other.right &&
|
|
47
|
+
this.right > other.left &&
|
|
48
|
+
this.top < other.bottom &&
|
|
49
|
+
this.bottom > other.top
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
contains(point) {
|
|
54
|
+
return (
|
|
55
|
+
point.x >= this.left &&
|
|
56
|
+
point.x <= this.right &&
|
|
57
|
+
point.y >= this.top &&
|
|
58
|
+
point.y <= this.bottom
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
overlap(other) {
|
|
63
|
+
const l = Math.max(this.left, other.left);
|
|
64
|
+
const r = Math.min(this.right, other.right);
|
|
65
|
+
const t = Math.max(this.top, other.top);
|
|
66
|
+
const b = Math.min(this.bottom, other.bottom);
|
|
67
|
+
if (l >= r || t >= b) return null;
|
|
68
|
+
return new Rect(l, t, r - l, b - t);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
clamp(outer) {
|
|
72
|
+
if (this.left < outer.left) this.left = outer.left;
|
|
73
|
+
if (this.right > outer.right) this.right = outer.right;
|
|
74
|
+
if (this.top < outer.top) this.top = outer.top;
|
|
75
|
+
if (this.bottom > outer.bottom) this.bottom = outer.bottom;
|
|
76
|
+
return this;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
inset(n) {
|
|
80
|
+
this.x += n;
|
|
81
|
+
this.y += n;
|
|
82
|
+
this.w -= n * 2;
|
|
83
|
+
this.h -= n * 2;
|
|
84
|
+
return this;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
move(dx, dy) {
|
|
88
|
+
this.x += dx;
|
|
89
|
+
this.y += dy;
|
|
90
|
+
return this;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
copy() {
|
|
94
|
+
return new Rect(this.x, this.y, this.w, this.h);
|
|
95
|
+
}
|
|
96
|
+
}
|