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 CHANGED
@@ -1,4 +1,6 @@
1
- ## Kippy
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.
@@ -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 {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kippy",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
4
4
  "description": "Kippy 2D web game engine for JS",
5
5
  "keywords": [
6
6
  "kippy",