kippy 0.7.0 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -47
- package/dist/bundle.min.js +1 -1
- package/dist/types/animation.d.ts +4 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
|
+
<img src="./assets/logo.png"/>
|
|
3
|
+
</div>
|
|
2
4
|
|
|
3
5
|
Kippy is a 2D JS game engine written purely for fun and simplicity. It currently utilizes the Canvas 2D context for rendering and aims to have a small set of APIs viable for game dev, but do expect a lot of components to change in the future.
|
|
4
6
|
|
|
@@ -128,6 +130,54 @@ const sprite = new Sprite({
|
|
|
128
130
|
entity.sprite = sprite;
|
|
129
131
|
```
|
|
130
132
|
|
|
133
|
+
### Animation
|
|
134
|
+
|
|
135
|
+
First you create a sprite sheet that contains your frames:
|
|
136
|
+
```js
|
|
137
|
+
import { SpriteSheet } from "kippy";
|
|
138
|
+
|
|
139
|
+
const spriteSheet = new SpriteSheet({
|
|
140
|
+
texture, // Similar to sprite texture
|
|
141
|
+
frameWidth, // Width of each frame, type number
|
|
142
|
+
frameHeight, // Height of each frame, type number
|
|
143
|
+
width, // Width when render, default is frameWidth
|
|
144
|
+
height // Height when render, default is frameHeight
|
|
145
|
+
});
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Then you create an animation object which defines the order of frame to play, fps, and whether to loop or not:
|
|
149
|
+
```js
|
|
150
|
+
import { Animation } from "kippy";
|
|
151
|
+
|
|
152
|
+
const animation = new Animation({
|
|
153
|
+
spriteSheet, // A SpriteSheet instance
|
|
154
|
+
frames, // Order of frames, type number[]
|
|
155
|
+
fps, // Frames per second, type number
|
|
156
|
+
loop // Loop animation endlessly, type boolean
|
|
157
|
+
});
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Then you create an animator, attach it to an entity, and then play an animation:
|
|
161
|
+
```js
|
|
162
|
+
import { Animator } from "kippy";
|
|
163
|
+
|
|
164
|
+
const animator = new Animator({
|
|
165
|
+
animations, // A Record<string, Animation> map that contains all animations
|
|
166
|
+
default // The animation that will be played first, type string
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// Attach it to an entity
|
|
170
|
+
entity.animator = animator
|
|
171
|
+
|
|
172
|
+
// Play an animation
|
|
173
|
+
entity.animator.play("animationName");
|
|
174
|
+
|
|
175
|
+
// You can also call a handler when animation ends
|
|
176
|
+
entity.animator.onEnd = function(name) {
|
|
177
|
+
// Do something
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
131
181
|
### Add controls
|
|
132
182
|
|
|
133
183
|
Game controls like mouse presses, key presses, touch, and cursor tracking (in the game canvas, not the web window) can be done by using the input handler from your `game` instance:
|
|
@@ -318,52 +368,6 @@ camera.zoom // Camera zoom level, default is 1
|
|
|
318
368
|
camera.screenToWorld(input.pointer); // Return new vector
|
|
319
369
|
```
|
|
320
370
|
|
|
321
|
-
### Animation
|
|
322
|
-
|
|
323
|
-
First you create a sprite sheet that contains your frames:
|
|
324
|
-
```js
|
|
325
|
-
import { SpriteSheet } from "kippy";
|
|
326
|
-
|
|
327
|
-
const spriteSheet = new SpriteSheet({
|
|
328
|
-
texture, // Similar to sprite texture
|
|
329
|
-
frameWidth, // Width of each frame, type number
|
|
330
|
-
frameHeight // Height of each frame, type number
|
|
331
|
-
});
|
|
332
|
-
```
|
|
333
|
-
|
|
334
|
-
Then you create an animation object which defines the order of frame to play, fps, and whether to loop or not:
|
|
335
|
-
```js
|
|
336
|
-
import { Animation } from "kippy";
|
|
337
|
-
|
|
338
|
-
const animation = new Animation({
|
|
339
|
-
spriteSheet, // A SpriteSheet instance
|
|
340
|
-
frames, // Order of frames, type number[]
|
|
341
|
-
fps, // Frames per second, type number
|
|
342
|
-
loop // Loop animation endlessly, type boolean
|
|
343
|
-
});
|
|
344
|
-
```
|
|
345
|
-
|
|
346
|
-
Then you create an animator, attach it to an entity, and then play an animation:
|
|
347
|
-
```js
|
|
348
|
-
import { Animator } from "kippy";
|
|
349
|
-
|
|
350
|
-
const animator = new Animator({
|
|
351
|
-
animations, // A Record<string, Animation> map that contains all animations
|
|
352
|
-
default // The animation that will be played first, type string
|
|
353
|
-
});
|
|
354
|
-
|
|
355
|
-
// Attach it to an entity
|
|
356
|
-
entity.animator = animator
|
|
357
|
-
|
|
358
|
-
// Play an animation
|
|
359
|
-
entity.animator.play("animationName");
|
|
360
|
-
|
|
361
|
-
// You can also call a handler when animation ends
|
|
362
|
-
entity.animator.onEnd = function(name) {
|
|
363
|
-
// Do something
|
|
364
|
-
}
|
|
365
|
-
```
|
|
366
|
-
|
|
367
371
|
### Audio
|
|
368
372
|
|
|
369
373
|
To be added, for now use web's built-in `Audio` class.
|
package/dist/bundle.min.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
class t{x;y;static ZERO=new t(0,0);static ONE=new t(1,1);static UP=new t(0,-1);static DOWN=new t(0,1);static LEFT=new t(-1,0);static RIGHT=new t(1,0);constructor(t,e){this.x=t,this.y=e}toString(){return`Vector2(${this.x}, ${this.y})`}add(e){return new t(this.x+e.x,this.y+e.y)}sub(e){return new t(this.x-e.x,this.y-e.y)}mul(e){return new t(this.x*e.x,this.y*e.y)}div(e){return new t(this.x/e.x,this.y/e.y)}neg(){return new t(-this.x,-this.y)}scale(e){return new t(this.x*e,this.y*e)}magnitude(){return Math.sqrt(this.x*this.x+this.y*this.y)}magnitudeSquared(){return this.x*this.x+this.y*this.y}normalize(){const e=this.magnitude();return e>0?new t(this.x/e,this.y/e):new t(0,0)}dot(t){return this.x*t.x+this.y*t.y}cross(t){return this.x*t.y-this.y*t.x}project(t){const e=this.dot(t)/t.magnitudeSquared();return t.scale(e)}min(e){return new t(Math.min(this.x,e.x),Math.min(this.y,e.y))}max(e){return new t(Math.max(this.x,e.x),Math.max(this.y,e.y))}floor(){return new t(Math.floor(this.x),Math.floor(this.y))}ceil(){return new t(Math.ceil(this.x),Math.ceil(this.y))}round(){return new t(Math.round(this.x),Math.round(this.y))}distance(t){return Math.sqrt((this.x-t.x)**2+(this.y-t.y)**2)}distanceSquared(t){return(this.x-t.x)**2+(this.y-t.y)**2}copy(){return new t(this.x,this.y)}lerp(t,e){return this.add(t.sub(this).scale(e))}clamp(t){const e=this.magnitude();return e>t?this.scale(t/e):this.copy()}rotate(e){const i=Math.cos(e),s=Math.sin(e);return new t(this.x*i-this.y*s,this.x*s+this.y*i)}orthogonal(){return new t(-this.y,this.x)}angle(){return Math.atan2(this.y,this.x)}angleTo(t){return Math.atan2(t.y-this.y,t.x-this.x)}reflect(t){const e=this.dot(t);return this.sub(t.scale(2*e))}equals(t){return this.x===t.x&&this.y===t.y}}class e{canvas;keys=new Set;keysPressed=new Set;keysReleased=new Set;pointer=new t(0,0);pointers=new Set;pointersPressed=new Set;pointersReleased=new Set;constructor(t){this.canvas=t.canvas,window.addEventListener("keydown",t=>{this.keys.has(t.key)||this.keysPressed.add(t.key),this.keys.add(t.key)}),window.addEventListener("keyup",t=>{this.keys.delete(t.key),this.keysReleased.add(t.key)}),this.canvas.addEventListener("mousemove",t=>{const e=this.canvas.getBoundingClientRect();this.pointer.x=t.clientX-e.left,this.pointer.y=t.clientY-e.top}),this.canvas.addEventListener("mousedown",t=>{this.pointers.has(t.button)||this.pointersPressed.add(t.button),this.pointers.add(t.button)}),this.canvas.addEventListener("mouseup",t=>{this.pointers.delete(t.button),this.pointersReleased.add(t.button)}),this.canvas.addEventListener("touchmove",t=>{t.preventDefault();const e=this.canvas.getBoundingClientRect(),i=t.touches[0];this.pointer.x=i.clientX-e.left,this.pointer.y=i.clientY-e.top}),this.canvas.addEventListener("touchstart",t=>{t.preventDefault(),this.pointers.has(2)||this.pointersPressed.add(2),this.pointers.add(2)}),this.canvas.addEventListener("touchend",t=>{t.preventDefault(),this.pointers.delete(2),this.pointersReleased.add(2)}),this.canvas.addEventListener("contextmenu",t=>{t.preventDefault()})}update(){this.keysPressed.clear(),this.keysReleased.clear(),this.pointersPressed.clear(),this.pointersReleased.clear()}isKeyDown(t){return this.keys.has(t)}isKeyPressed(t){return this.keysPressed.has(t)}isKeyReleased(t){return this.keysReleased.has(t)}isPointerDown(t=0){return this.pointers.has(t)}isPointerPressed(t=0){return this.pointersPressed.has(t)}isPointerReleased(t=0){return this.pointersReleased.has(t)}}class i{velocity;rotationVelocity;mass;inertia;force;torque;restitution;sleepThreshold;sleepTimeThreshold;isSleeping;sleepTimer;constructor(e={}){this.velocity=e.velocity??new t(0,0),this.rotationVelocity=e.rotationVelocity??0,this.mass=e.mass??1,this.inertia=e.inertia??1,this.force=e.force??new t(0,0),this.torque=e.torque??0,this.restitution=e.restitution??0,this.sleepThreshold=e.sleepThreshold??.1,this.sleepTimeThreshold=e.sleepTimeThreshold??.5,this.isSleeping=e.isSleeping??!1,this.sleepTimer=e.sleepTimer??0}wake(){this.isSleeping=!1,this.sleepTimer=0}}class s{radius;offset;isTrigger;layer;mask;constructor(e){this.radius=e.radius,this.offset=e.offset??new t(0,0),this.isTrigger=e.isTrigger??!1,this.layer=e.layer??1,this.mask=e.mask??4294967295}}class o{width;height;offset;isTrigger;layer;mask;constructor(e){this.width=e.width,this.height=e.height,this.offset=e.offset??new t(0,0),this.isTrigger=e.isTrigger??!1,this.layer=e.layer??1,this.mask=e.mask??4294967295}}class n{cellSize;grid;constructor(t={}){this.cellSize=t.cellSize||100,this.grid=t.grid||new Map}clear(){this.grid.clear()}adaptCellSize(t){const e=[];for(const i of t){const t=this.getEntityBounds(i),s=t.maxX-t.minX,o=t.maxY-t.minY,n=Math.max(s,o);e.push(n)}e.sort((t,e)=>t-e);const i=e[Math.floor(.75*e.length)];this.cellSize=2.5*i}insert(t){if(t.collider){const e=this.getEntityBounds(t),i=Math.floor(e.minX/this.cellSize),s=Math.floor(e.maxX/this.cellSize),o=Math.floor(e.minY/this.cellSize),n=Math.floor(e.maxY/this.cellSize);for(let e=i;e<=s;e++)for(let i=o;i<=n;i++){const s=`${e},${i}`;this.grid.has(s)||this.grid.set(s,new Set),this.grid.get(s).add(t)}}}getNearby(t){const e=Math.floor(t.position.x/this.cellSize),i=Math.floor(t.position.y/this.cellSize),s=new Set;for(let o=-1;o<=1;o++)for(let n=-1;n<=1;n++){const r=`${e+o},${i+n}`,a=this.grid.get(r);if(a)for(const e of a)e!==t&&s.add(e)}return Array.from(s)}getEntityBounds(t){if(t.collider instanceof s){const e=t.collider.radius;return{minX:t.position.x-e,maxX:t.position.x+e,minY:t.position.y-e,maxY:t.position.y+e}}if(t.collider instanceof o){const e=t.collider.width,i=t.collider.height;return{minX:t.position.x-e,maxX:t.position.x+e,minY:t.position.y-i,maxY:t.position.y+i}}throw new Error("Collider type not supported")}}class r{collisionPairs=new Map;spatialGrid=new n;entityCount=0;update(e,s){for(const t of e)if(t.body instanceof i){if((t.body.force.magnitudeSquared()>0||0!==t.body.torque||t.body.velocity.magnitudeSquared()>t.body.sleepThreshold**2||Math.abs(t.body.rotationVelocity)>t.body.sleepThreshold)&&t.body.wake(),t.body.isSleeping)continue;t.body.velocity.x+=t.body.force.x/t.body.mass*s,t.body.velocity.y+=t.body.force.y/t.body.mass*s,t.body.rotationVelocity+=t.body.torque/t.body.inertia*s,t.body.force.x=0,t.body.force.y=0,t.body.torque=0}const o=e.filter(t=>t.collider);this.spatialGrid.clear(),this.entityCount!==o.length&&(this.entityCount=o.length,this.spatialGrid.adaptCellSize(o));for(const t of o)this.spatialGrid.insert(t);const n=new Map,r=[];for(const e of o){const i=this.spatialGrid.getNearby(e);for(const s of i)if(s.collider){if(n.has(e)&&n.get(e)?.has(s)||n.has(s)&&n.get(s)?.has(e))continue;const i=this.checkCollision(e,s);if(i.info){e.body?.isSleeping&&s.body?.isSleeping||(e.body?.wake(),s.body?.wake()),n.has(e)||n.set(e,new Map),n.get(e)?.set(s,i.info);this.collisionPairs.get(e)?.has(s)||this.collisionPairs.get(s)?.has(e)?i.isTrigger?(e.onTriggerStay?.(s),s.onTriggerStay?.(e)):(e.onCollisionStay?.(s,i.info),s.onCollisionStay?.(e,{...i.info,normal:new t(-i.info.normal.x,-i.info.normal.y)})):i.isTrigger?(e.onTriggerEnter?.(s),s.onTriggerEnter?.(e)):(e.onCollisionEnter?.(s,i.info),s.onCollisionEnter?.(e,{...i.info,normal:new t(-i.info.normal.x,-i.info.normal.y)})),i.isTrigger||r.push({entityA:e,entityB:s,info:i.info})}}}for(let t=0;t<6;t++)for(const t of r)this.resolveCollision(t.entityA,t.entityB,t.info,s);for(const[e,i]of this.collisionPairs)for(const[s,o]of i){if(!(n.get(e)?.has(s)||n.get(s)?.has(e))){e.collider?.isTrigger||s.collider?.isTrigger?(e.onTriggerExit?.(s),s.onTriggerExit?.(e)):(e.onCollisionExit?.(s,o),s.onCollisionExit?.(e,{...o,normal:new t(-o.normal.x,-o.normal.y)}))}}this.collisionPairs=n;for(const t of e)if(t.body instanceof i){if(t.body.isSleeping)continue;t.position.x+=t.body.velocity.x*s,t.position.y+=t.body.velocity.y*s,t.rotation+=t.body.rotationVelocity*s;const e=t.body.velocity.magnitude(),i=Math.abs(t.body.rotationVelocity);e<t.body.sleepThreshold&&i<t.body.sleepThreshold?(t.body.sleepTimer+=s,t.body.sleepTimer>=t.body.sleepTimeThreshold&&(t.body.isSleeping=!0,t.body.velocity.x=0,t.body.velocity.y=0,t.body.rotationVelocity=0)):t.body.sleepTimer=0}}checkCollision(e,i){if(e.collider&&i.collider){if(0===(e.collider.mask&i.collider.layer)||0===(i.collider.mask&e.collider.layer))return{isTrigger:!1};const n=e.collider.isTrigger||i.collider.isTrigger,r=e.position.add(e.collider.offset),a=i.position.add(i.collider.offset);if(e.collider instanceof s&&i.collider instanceof s){const s=r.distance(a),o=e.collider.radius+i.collider.radius;if(s>=o)return{isTrigger:!1};const h=o-s,l=a.sub(r);let c=s>0?l.scale(1/s):new t(1,0),d=0;const u=this.collisionPairs.get(e)?.get(i)||this.collisionPairs.get(i)?.get(e);return u&&(d=u.accumulatedNormalImpulse),{isTrigger:n,info:{normal:c,penetration:h,contact:r.add(c.scale(e.collider.radius)),accumulatedNormalImpulse:d}}}if(e.collider instanceof o&&i.collider instanceof o){const s=e.collider.width/2,o=e.collider.height/2,h=i.collider.width/2,l=i.collider.height/2,c=a.sub(r),d=s+h-Math.abs(c.x),u=o+l-Math.abs(c.y);if(d<=0||u<=0)return{isTrigger:!1};let y,m;d<u?(m=d,y=new t(c.x<0?-1:1,0)):(m=u,y=new t(0,c.y<0?-1:1));const p=r.x+Math.sign(c.x)*(s-d/2),f=r.y+Math.sign(c.y)*(o-u/2);let g=0;const x=this.collisionPairs.get(e)?.get(i)||this.collisionPairs.get(i)?.get(e);return x&&(g=x.accumulatedNormalImpulse),{isTrigger:n,info:{normal:y,penetration:m,contact:new t(p,f),accumulatedNormalImpulse:g}}}if(e.collider instanceof s&&i.collider instanceof o||e.collider instanceof o&&i.collider instanceof s){const o=e.collider instanceof s,h=o?r:a,l=o?a:r,c=(o?e:i).collider,d=(o?i:e).collider,u=d.width/2,y=d.height/2,m=h.sub(l);let p,f,g;if(Math.abs(m.x)<u&&Math.abs(m.y)<y){const e=u-Math.abs(m.x),i=y-Math.abs(m.y);if(e<i){const i=m.x<0?-1:1,s=new t(i,0);p=o?s.neg():s,f=c.radius+e,g=new t(l.x+i*u,h.y)}else{const e=m.y<0?-1:1,s=new t(0,e);p=o?s.neg():s,f=c.radius+i,g=new t(h.x,l.y+e*y)}}else{const e=Math.max(-u,Math.min(u,m.x)),i=Math.max(-y,Math.min(y,m.y)),s=l.add(new t(e,i)),n=h.sub(s),r=n.magnitudeSquared();if(r>=c.radius*c.radius)return{isTrigger:!1};const a=Math.sqrt(r),d=a>0?n.scale(1/a):new t(1,0);p=o?d.neg():d,f=c.radius-a,g=s}let x=0;const w=this.collisionPairs.get(e)?.get(i)||this.collisionPairs.get(i)?.get(e);return w&&(x=w.accumulatedNormalImpulse),{isTrigger:n,info:{normal:p,penetration:f,contact:g,accumulatedNormalImpulse:x}}}return{isTrigger:!1}}return{isTrigger:!1}}resolveCollision(t,e,i,s){if(!t.body||!e.body)return;const o=t.body,n=e.body,r=isFinite(o.mass)?1/o.mass:0,a=isFinite(n.mass)?1/n.mass:0,h=r+a;if(0===h)return;const l=n.velocity.sub(o.velocity).dot(i.normal);if(l>0)return;let c=Math.max(o.restitution,n.restitution);Math.abs(l)<.5&&(c=0);const d=(-(1+c)*l+(0===c?.2*Math.max(0,i.penetration-.01)/s:0))/h,u=i.accumulatedNormalImpulse||0;i.accumulatedNormalImpulse=Math.max(0,u+d);const y=i.accumulatedNormalImpulse-u;o.velocity=o.velocity.sub(i.normal.scale(y*r)),n.velocity=n.velocity.add(i.normal.scale(y*a))}}class a{canvas;ctx;scene;lastTime=0;input;physics;paused=!1;constructor(t){this.canvas=t.canvas,this.ctx=this.canvas.getContext("2d"),this.input=t.input??new e({canvas:this.canvas}),this.physics=t.physics??new r}setCanvas(t){this.canvas=t,this.ctx=this.canvas.getContext("2d")}setScene(t){this.scene?.exit(),t.ctx=this.ctx,this.scene=t,this.scene.init()}start(){window.addEventListener("blur",()=>{this.paused=!0}),window.addEventListener("focus",()=>{this.paused=!1,this.lastTime=performance.now()}),requestAnimationFrame(this.loop.bind(this))}loop(t){if(this.paused)return void requestAnimationFrame(this.loop.bind(this));const e=(t-this.lastTime)/1e3;if(this.lastTime=t,!this.scene)throw new Error("Can not run game loop without a scene");this.scene.update(e),this.scene.animationUpdate(e),this.physics.update(this.scene.entities,e),this.ctx.clearRect(0,0,this.ctx.canvas.width,this.ctx.canvas.height),this.scene.render(),this.input.update(),requestAnimationFrame(this.loop.bind(this))}}class h{position;rotation;zoom;scene;constructor(e){this.position=e.position??new t(0,0),this.rotation=e.rotation??0,this.zoom=e.zoom??1,this.scene=e.scene}apply(){const t=this.scene.ctx;if(t){const e=t.canvas.width/2,i=t.canvas.height/2;t.translate(e,i),t.scale(this.zoom,this.zoom),t.rotate(this.rotation),t.translate(-this.position.x-e/this.zoom,-this.position.y-i/this.zoom)}}screenToWorld(e){const i=this.scene.ctx;if(i){const s=i.canvas.width/2,o=i.canvas.height/2;let n=e.x-s,r=e.y-o;const a=Math.cos(-this.rotation),h=Math.sin(-this.rotation),l=n*h+r*a,c=(n*a-r*h)/this.zoom,d=l/this.zoom;return new t(c+this.position.x+s/this.zoom,d+this.position.y+o/this.zoom)}return new t(0,0)}}class l{ctx;camera=new h({scene:this});entities=[];init(){}update(t){}exit(){}addEntity(t){this.entities.push(t)}removeEntity(t){this.entities=this.entities.filter(e=>e!==t)}animationUpdate(t){for(const e of this.entities)e.animator?.update(t)}render(){const t=this.ctx;if(t){t.save(),this.camera.apply();for(const e of this.entities)e.render(t);t.restore()}}}class c{animator;sprite;position;rotation;body;collider;constructor(e={}){this.animator=e.animator,this.sprite=e.sprite,this.position=e.position??new t(0,0),this.rotation=e.rotation??0,this.body=e.body,this.collider=e.collider}onCollisionEnter;onCollisionStay;onCollisionExit;onTriggerEnter;onTriggerStay;onTriggerExit;render(t){if(this.animator){const e=this.animator.currentAnimation.spriteSheet,i=this.animator.currentFrame,s=Math.floor(e.texture.width/e.frameWidth),o=i%s*e.frameWidth,n=Math.floor(i/s)*e.frameHeight;t.save(),t.translate(this.position.x,this.position.y),t.rotate(this.rotation),t.drawImage(e.texture,o,n,e.frameWidth,e.frameHeight,-e.frameWidth/2,-e.frameHeight/2,e.frameWidth,e.frameHeight),t.restore()}else this.sprite&&(t.save(),t.translate(this.position.x,this.position.y),t.rotate(this.rotation),t.drawImage(this.sprite.texture,-this.sprite.width/2,-this.sprite.height/2,this.sprite.width,this.sprite.height),t.restore())}}class d{texture;width;height;constructor(t){this.texture=t.texture,this.width=t.width??this.texture.width,this.height=t.height??this.texture.height}}class u{texture;frameWidth;frameHeight;constructor(t){this.texture=t.texture,this.frameWidth=t.frameWidth,this.frameHeight=t.frameHeight}}class y{spriteSheet;frames;fps;loop;constructor(t){this.spriteSheet=t.spriteSheet,this.frames=t.frames,this.fps=t.fps,this.loop=t.loop??!1}}class m{animations;currentAnimation;currentAnimationName;frameIndex=0;stopped=!1;elapsed=0;onEnd;constructor(t){this.animations=t.animations,this.currentAnimation=this.animations[t.default],this.currentAnimationName=t.default}play(t){this.currentAnimation=this.animations[t],this.currentAnimationName=t,this.frameIndex=0,this.stopped=!1,this.elapsed=0}update(t){if(this.stopped)return;this.elapsed+=t;const e=1/this.currentAnimation.fps;this.elapsed>=e&&(this.elapsed-=e,this.frameIndex++,this.frameIndex>=this.currentAnimation.frames.length&&(this.currentAnimation.loop?this.frameIndex=0:(this.frameIndex=this.currentAnimation.frames.length-1,this.stopped=!0,this.onEnd?.(this.currentAnimationName))))}get currentFrame(){return this.currentAnimation.frames[this.frameIndex]}}export{y as Animation,m as Animator,o as BoxCollider,h as Camera,s as CircleCollider,c as Entity,a as Game,e as Input,r as Physics,i as RigidBody,l as Scene,n as SpatialGrid,d as Sprite,u as SpriteSheet,t as Vector2};
|
|
1
|
+
class t{x;y;static ZERO=new t(0,0);static ONE=new t(1,1);static UP=new t(0,-1);static DOWN=new t(0,1);static LEFT=new t(-1,0);static RIGHT=new t(1,0);constructor(t,e){this.x=t,this.y=e}toString(){return`Vector2(${this.x}, ${this.y})`}add(e){return new t(this.x+e.x,this.y+e.y)}sub(e){return new t(this.x-e.x,this.y-e.y)}mul(e){return new t(this.x*e.x,this.y*e.y)}div(e){return new t(this.x/e.x,this.y/e.y)}neg(){return new t(-this.x,-this.y)}scale(e){return new t(this.x*e,this.y*e)}magnitude(){return Math.sqrt(this.x*this.x+this.y*this.y)}magnitudeSquared(){return this.x*this.x+this.y*this.y}normalize(){const e=this.magnitude();return e>0?new t(this.x/e,this.y/e):new t(0,0)}dot(t){return this.x*t.x+this.y*t.y}cross(t){return this.x*t.y-this.y*t.x}project(t){const e=this.dot(t)/t.magnitudeSquared();return t.scale(e)}min(e){return new t(Math.min(this.x,e.x),Math.min(this.y,e.y))}max(e){return new t(Math.max(this.x,e.x),Math.max(this.y,e.y))}floor(){return new t(Math.floor(this.x),Math.floor(this.y))}ceil(){return new t(Math.ceil(this.x),Math.ceil(this.y))}round(){return new t(Math.round(this.x),Math.round(this.y))}distance(t){return Math.sqrt((this.x-t.x)**2+(this.y-t.y)**2)}distanceSquared(t){return(this.x-t.x)**2+(this.y-t.y)**2}copy(){return new t(this.x,this.y)}lerp(t,e){return this.add(t.sub(this).scale(e))}clamp(t){const e=this.magnitude();return e>t?this.scale(t/e):this.copy()}rotate(e){const i=Math.cos(e),s=Math.sin(e);return new t(this.x*i-this.y*s,this.x*s+this.y*i)}orthogonal(){return new t(-this.y,this.x)}angle(){return Math.atan2(this.y,this.x)}angleTo(t){return Math.atan2(t.y-this.y,t.x-this.x)}reflect(t){const e=this.dot(t);return this.sub(t.scale(2*e))}equals(t){return this.x===t.x&&this.y===t.y}}class e{canvas;keys=new Set;keysPressed=new Set;keysReleased=new Set;pointer=new t(0,0);pointers=new Set;pointersPressed=new Set;pointersReleased=new Set;constructor(t){this.canvas=t.canvas,window.addEventListener("keydown",t=>{this.keys.has(t.key)||this.keysPressed.add(t.key),this.keys.add(t.key)}),window.addEventListener("keyup",t=>{this.keys.delete(t.key),this.keysReleased.add(t.key)}),this.canvas.addEventListener("mousemove",t=>{const e=this.canvas.getBoundingClientRect();this.pointer.x=t.clientX-e.left,this.pointer.y=t.clientY-e.top}),this.canvas.addEventListener("mousedown",t=>{this.pointers.has(t.button)||this.pointersPressed.add(t.button),this.pointers.add(t.button)}),this.canvas.addEventListener("mouseup",t=>{this.pointers.delete(t.button),this.pointersReleased.add(t.button)}),this.canvas.addEventListener("touchmove",t=>{t.preventDefault();const e=this.canvas.getBoundingClientRect(),i=t.touches[0];this.pointer.x=i.clientX-e.left,this.pointer.y=i.clientY-e.top}),this.canvas.addEventListener("touchstart",t=>{t.preventDefault(),this.pointers.has(2)||this.pointersPressed.add(2),this.pointers.add(2)}),this.canvas.addEventListener("touchend",t=>{t.preventDefault(),this.pointers.delete(2),this.pointersReleased.add(2)}),this.canvas.addEventListener("contextmenu",t=>{t.preventDefault()})}update(){this.keysPressed.clear(),this.keysReleased.clear(),this.pointersPressed.clear(),this.pointersReleased.clear()}isKeyDown(t){return this.keys.has(t)}isKeyPressed(t){return this.keysPressed.has(t)}isKeyReleased(t){return this.keysReleased.has(t)}isPointerDown(t=0){return this.pointers.has(t)}isPointerPressed(t=0){return this.pointersPressed.has(t)}isPointerReleased(t=0){return this.pointersReleased.has(t)}}class i{velocity;rotationVelocity;mass;inertia;force;torque;restitution;sleepThreshold;sleepTimeThreshold;isSleeping;sleepTimer;constructor(e={}){this.velocity=e.velocity??new t(0,0),this.rotationVelocity=e.rotationVelocity??0,this.mass=e.mass??1,this.inertia=e.inertia??1,this.force=e.force??new t(0,0),this.torque=e.torque??0,this.restitution=e.restitution??0,this.sleepThreshold=e.sleepThreshold??.1,this.sleepTimeThreshold=e.sleepTimeThreshold??.5,this.isSleeping=e.isSleeping??!1,this.sleepTimer=e.sleepTimer??0}wake(){this.isSleeping=!1,this.sleepTimer=0}}class s{radius;offset;isTrigger;layer;mask;constructor(e){this.radius=e.radius,this.offset=e.offset??new t(0,0),this.isTrigger=e.isTrigger??!1,this.layer=e.layer??1,this.mask=e.mask??4294967295}}class o{width;height;offset;isTrigger;layer;mask;constructor(e){this.width=e.width,this.height=e.height,this.offset=e.offset??new t(0,0),this.isTrigger=e.isTrigger??!1,this.layer=e.layer??1,this.mask=e.mask??4294967295}}class n{cellSize;grid;constructor(t={}){this.cellSize=t.cellSize||100,this.grid=t.grid||new Map}clear(){this.grid.clear()}adaptCellSize(t){const e=[];for(const i of t){const t=this.getEntityBounds(i),s=t.maxX-t.minX,o=t.maxY-t.minY,n=Math.max(s,o);e.push(n)}e.sort((t,e)=>t-e);const i=e[Math.floor(.75*e.length)];this.cellSize=2.5*i}insert(t){if(t.collider){const e=this.getEntityBounds(t),i=Math.floor(e.minX/this.cellSize),s=Math.floor(e.maxX/this.cellSize),o=Math.floor(e.minY/this.cellSize),n=Math.floor(e.maxY/this.cellSize);for(let e=i;e<=s;e++)for(let i=o;i<=n;i++){const s=`${e},${i}`;this.grid.has(s)||this.grid.set(s,new Set),this.grid.get(s).add(t)}}}getNearby(t){const e=this.getEntityBounds(t),i=Math.floor(e.minX/this.cellSize),s=Math.floor(e.maxX/this.cellSize),o=Math.floor(e.minY/this.cellSize),n=Math.floor(e.maxY/this.cellSize),r=new Set;for(let e=i;e<=s;e++)for(let i=o;i<=n;i++){const s=this.grid.get(`${e},${i}`);if(s)for(const e of s)e!==t&&r.add(e)}return Array.from(r)}getEntityBounds(t){if(t.collider instanceof s){const e=t.collider.radius;return{minX:t.position.x-e,maxX:t.position.x+e,minY:t.position.y-e,maxY:t.position.y+e}}if(t.collider instanceof o){const e=t.collider.width/2,i=t.collider.height/2;return{minX:t.position.x-e,maxX:t.position.x+e,minY:t.position.y-i,maxY:t.position.y+i}}throw new Error("Collider type not supported")}}class r{collisionPairs=new Map;spatialGrid=new n;entityCount=0;update(e,s){for(const t of e)if(t.body instanceof i){if((t.body.force.magnitudeSquared()>0||0!==t.body.torque||t.body.velocity.magnitudeSquared()>t.body.sleepThreshold**2||Math.abs(t.body.rotationVelocity)>t.body.sleepThreshold)&&t.body.wake(),t.body.isSleeping)continue;t.body.velocity.x+=t.body.force.x/t.body.mass*s,t.body.velocity.y+=t.body.force.y/t.body.mass*s,t.body.rotationVelocity+=t.body.torque/t.body.inertia*s,t.body.force.x=0,t.body.force.y=0,t.body.torque=0}const o=e.filter(t=>t.collider);this.spatialGrid.clear(),this.entityCount!==o.length&&(this.entityCount=o.length,this.spatialGrid.adaptCellSize(o));for(const t of o)this.spatialGrid.insert(t);const n=new Map,r=[];for(const e of o){const i=this.spatialGrid.getNearby(e);for(const s of i)if(s.collider){if(n.has(e)&&n.get(e)?.has(s)||n.has(s)&&n.get(s)?.has(e))continue;const i=this.checkCollision(e,s);if(i.info){e.body?.isSleeping&&s.body?.isSleeping||(e.body?.wake(),s.body?.wake()),n.has(e)||n.set(e,new Map),n.get(e)?.set(s,i.info);this.collisionPairs.get(e)?.has(s)||this.collisionPairs.get(s)?.has(e)?i.isTrigger?(e.onTriggerStay?.(s),s.onTriggerStay?.(e)):(e.onCollisionStay?.(s,i.info),s.onCollisionStay?.(e,{...i.info,normal:new t(-i.info.normal.x,-i.info.normal.y)})):i.isTrigger?(e.onTriggerEnter?.(s),s.onTriggerEnter?.(e)):(e.onCollisionEnter?.(s,i.info),s.onCollisionEnter?.(e,{...i.info,normal:new t(-i.info.normal.x,-i.info.normal.y)})),i.isTrigger||r.push({entityA:e,entityB:s,info:i.info})}}}for(let t=0;t<6;t++)for(const t of r)this.resolveCollision(t.entityA,t.entityB,t.info,s);for(const[e,i]of this.collisionPairs)for(const[s,o]of i){if(!(n.get(e)?.has(s)||n.get(s)?.has(e))){e.collider?.isTrigger||s.collider?.isTrigger?(e.onTriggerExit?.(s),s.onTriggerExit?.(e)):(e.onCollisionExit?.(s,o),s.onCollisionExit?.(e,{...o,normal:new t(-o.normal.x,-o.normal.y)}))}}this.collisionPairs=n;for(const t of e)if(t.body instanceof i){if(t.body.isSleeping)continue;t.position.x+=t.body.velocity.x*s,t.position.y+=t.body.velocity.y*s,t.rotation+=t.body.rotationVelocity*s;const e=t.body.velocity.magnitude(),i=Math.abs(t.body.rotationVelocity);e<t.body.sleepThreshold&&i<t.body.sleepThreshold?(t.body.sleepTimer+=s,t.body.sleepTimer>=t.body.sleepTimeThreshold&&(t.body.isSleeping=!0,t.body.velocity.x=0,t.body.velocity.y=0,t.body.rotationVelocity=0)):t.body.sleepTimer=0}}checkCollision(e,i){if(e.collider&&i.collider){if(0===(e.collider.mask&i.collider.layer)||0===(i.collider.mask&e.collider.layer))return{isTrigger:!1};const n=e.collider.isTrigger||i.collider.isTrigger,r=e.position.add(e.collider.offset),a=i.position.add(i.collider.offset);if(e.collider instanceof s&&i.collider instanceof s){const s=r.distance(a),o=e.collider.radius+i.collider.radius;if(s>=o)return{isTrigger:!1};const h=o-s,l=a.sub(r);let c=s>0?l.scale(1/s):new t(1,0),d=0;const u=this.collisionPairs.get(e)?.get(i)||this.collisionPairs.get(i)?.get(e);return u&&(d=u.accumulatedNormalImpulse),{isTrigger:n,info:{normal:c,penetration:h,contact:r.add(c.scale(e.collider.radius)),accumulatedNormalImpulse:d}}}if(e.collider instanceof o&&i.collider instanceof o){const s=e.collider.width/2,o=e.collider.height/2,h=i.collider.width/2,l=i.collider.height/2,c=a.sub(r),d=s+h-Math.abs(c.x),u=o+l-Math.abs(c.y);if(d<=0||u<=0)return{isTrigger:!1};let m,y;d<u?(y=d,m=new t(c.x<0?-1:1,0)):(y=u,m=new t(0,c.y<0?-1:1));const p=r.x+Math.sign(c.x)*(s-d/2),f=r.y+Math.sign(c.y)*(o-u/2);let g=0;const x=this.collisionPairs.get(e)?.get(i)||this.collisionPairs.get(i)?.get(e);return x&&(g=x.accumulatedNormalImpulse),{isTrigger:n,info:{normal:m,penetration:y,contact:new t(p,f),accumulatedNormalImpulse:g}}}if(e.collider instanceof s&&i.collider instanceof o||e.collider instanceof o&&i.collider instanceof s){const o=e.collider instanceof s,h=o?r:a,l=o?a:r,c=(o?e:i).collider,d=(o?i:e).collider,u=d.width/2,m=d.height/2,y=h.sub(l);let p,f,g;if(Math.abs(y.x)<u&&Math.abs(y.y)<m){const e=u-Math.abs(y.x),i=m-Math.abs(y.y);if(e<i){const i=y.x<0?-1:1,s=new t(i,0);p=o?s.neg():s,f=c.radius+e,g=new t(l.x+i*u,h.y)}else{const e=y.y<0?-1:1,s=new t(0,e);p=o?s.neg():s,f=c.radius+i,g=new t(h.x,l.y+e*m)}}else{const e=Math.max(-u,Math.min(u,y.x)),i=Math.max(-m,Math.min(m,y.y)),s=l.add(new t(e,i)),n=h.sub(s),r=n.magnitudeSquared();if(r>=c.radius*c.radius)return{isTrigger:!1};const a=Math.sqrt(r),d=a>0?n.scale(1/a):new t(1,0);p=o?d.neg():d,f=c.radius-a,g=s}let x=0;const w=this.collisionPairs.get(e)?.get(i)||this.collisionPairs.get(i)?.get(e);return w&&(x=w.accumulatedNormalImpulse),{isTrigger:n,info:{normal:p,penetration:f,contact:g,accumulatedNormalImpulse:x}}}return{isTrigger:!1}}return{isTrigger:!1}}resolveCollision(t,e,i,s){if(!t.body||!e.body)return;const o=t.body,n=e.body,r=isFinite(o.mass)?1/o.mass:0,a=isFinite(n.mass)?1/n.mass:0,h=r+a;if(0===h)return;const l=n.velocity.sub(o.velocity).dot(i.normal);if(l>0)return;let c=Math.max(o.restitution,n.restitution);Math.abs(l)<.5&&(c=0);const d=(-(1+c)*l+(0===c?.2*Math.max(0,i.penetration-.01)/s:0))/h,u=i.accumulatedNormalImpulse||0;i.accumulatedNormalImpulse=Math.max(0,u+d);const m=i.accumulatedNormalImpulse-u;o.velocity=o.velocity.sub(i.normal.scale(m*r)),n.velocity=n.velocity.add(i.normal.scale(m*a))}}class a{canvas;ctx;scene;lastTime=0;input;physics;paused=!1;constructor(t){this.canvas=t.canvas,this.ctx=this.canvas.getContext("2d"),this.input=t.input??new e({canvas:this.canvas}),this.physics=t.physics??new r}setCanvas(t){this.canvas=t,this.ctx=this.canvas.getContext("2d")}setScene(t){this.scene?.exit(),t.ctx=this.ctx,this.scene=t,this.scene.init()}start(){window.addEventListener("blur",()=>{this.paused=!0}),window.addEventListener("focus",()=>{this.paused=!1,this.lastTime=performance.now()}),requestAnimationFrame(this.loop.bind(this))}loop(t){if(this.paused)return void requestAnimationFrame(this.loop.bind(this));const e=(t-this.lastTime)/1e3;if(this.lastTime=t,!this.scene)throw new Error("Can not run game loop without a scene");this.scene.update(e),this.scene.animationUpdate(e),this.physics.update(this.scene.entities,e),this.ctx.clearRect(0,0,this.ctx.canvas.width,this.ctx.canvas.height),this.scene.render(),this.input.update(),requestAnimationFrame(this.loop.bind(this))}}class h{position;rotation;zoom;scene;constructor(e){this.position=e.position??new t(0,0),this.rotation=e.rotation??0,this.zoom=e.zoom??1,this.scene=e.scene}apply(){const t=this.scene.ctx;if(t){const e=t.canvas.width/2,i=t.canvas.height/2;t.translate(e,i),t.scale(this.zoom,this.zoom),t.rotate(this.rotation),t.translate(-this.position.x-e/this.zoom,-this.position.y-i/this.zoom)}}screenToWorld(e){const i=this.scene.ctx;if(i){const s=i.canvas.width/2,o=i.canvas.height/2;let n=e.x-s,r=e.y-o;const a=Math.cos(-this.rotation),h=Math.sin(-this.rotation),l=n*h+r*a,c=(n*a-r*h)/this.zoom,d=l/this.zoom;return new t(c+this.position.x+s/this.zoom,d+this.position.y+o/this.zoom)}return new t(0,0)}}class l{ctx;camera=new h({scene:this});entities=[];init(){}update(t){}exit(){}addEntity(t){this.entities.push(t)}removeEntity(t){this.entities=this.entities.filter(e=>e!==t)}animationUpdate(t){for(const e of this.entities)e.animator?.update(t)}render(){const t=this.ctx;if(t){t.save(),this.camera.apply();for(const e of this.entities)e.render(t);t.restore()}}}class c{animator;sprite;position;rotation;body;collider;constructor(e={}){this.animator=e.animator,this.sprite=e.sprite,this.position=e.position??new t(0,0),this.rotation=e.rotation??0,this.body=e.body,this.collider=e.collider}onCollisionEnter;onCollisionStay;onCollisionExit;onTriggerEnter;onTriggerStay;onTriggerExit;render(t){if(this.animator){const e=this.animator.currentAnimation.spriteSheet,i=this.animator.currentFrame,s=Math.floor(e.texture.width/e.frameWidth),o=i%s*e.frameWidth,n=Math.floor(i/s)*e.frameHeight,r=e.width??e.frameWidth,a=e.height??e.frameHeight;t.save(),t.translate(this.position.x,this.position.y),t.rotate(this.rotation),t.drawImage(e.texture,o,n,e.frameWidth,e.frameHeight,-r/2,-a/2,r,a),t.restore()}else this.sprite&&(t.save(),t.translate(this.position.x,this.position.y),t.rotate(this.rotation),t.drawImage(this.sprite.texture,-this.sprite.width/2,-this.sprite.height/2,this.sprite.width,this.sprite.height),t.restore())}}class d{texture;width;height;constructor(t){this.texture=t.texture,this.width=t.width??this.texture.width,this.height=t.height??this.texture.height}}class u{texture;frameWidth;frameHeight;width;height;constructor(t){this.texture=t.texture,this.frameWidth=t.frameWidth,this.frameHeight=t.frameHeight,this.width=t.width??t.frameWidth,this.height=t.height??t.frameHeight}}class m{spriteSheet;frames;fps;loop;constructor(t){this.spriteSheet=t.spriteSheet,this.frames=t.frames,this.fps=t.fps,this.loop=t.loop??!1}}class y{animations;currentAnimation;currentAnimationName;frameIndex=0;stopped=!1;elapsed=0;onEnd;constructor(t){this.animations=t.animations,this.currentAnimation=this.animations[t.default],this.currentAnimationName=t.default}play(t){this.currentAnimation=this.animations[t],this.currentAnimationName=t,this.frameIndex=0,this.stopped=!1,this.elapsed=0}update(t){if(this.stopped)return;this.elapsed+=t;const e=1/this.currentAnimation.fps;this.elapsed>=e&&(this.elapsed-=e,this.frameIndex++,this.frameIndex>=this.currentAnimation.frames.length&&(this.currentAnimation.loop?this.frameIndex=0:(this.frameIndex=this.currentAnimation.frames.length-1,this.stopped=!0,this.onEnd?.(this.currentAnimationName))))}get currentFrame(){return this.currentAnimation.frames[this.frameIndex]}}export{m as Animation,y as Animator,o as BoxCollider,h as Camera,s as CircleCollider,c as Entity,a as Game,e as Input,r as Physics,i as RigidBody,l as Scene,n as SpatialGrid,d as Sprite,u as SpriteSheet,t as Vector2};
|
|
@@ -3,11 +3,15 @@ export interface SpriteSheetOptions {
|
|
|
3
3
|
texture: Texture;
|
|
4
4
|
frameWidth: number;
|
|
5
5
|
frameHeight: number;
|
|
6
|
+
width?: number;
|
|
7
|
+
height?: number;
|
|
6
8
|
}
|
|
7
9
|
export declare class SpriteSheet {
|
|
8
10
|
texture: Texture;
|
|
9
11
|
frameWidth: number;
|
|
10
12
|
frameHeight: number;
|
|
13
|
+
width: number;
|
|
14
|
+
height: number;
|
|
11
15
|
constructor(options: SpriteSheetOptions);
|
|
12
16
|
}
|
|
13
17
|
export interface AnimationOptions {
|