kippy 0.7.1 → 0.7.3

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
 
@@ -90,13 +92,19 @@ A scene can have multiple entities like players, mobs, obstacles, etc in it. Thi
90
92
  import { Entity } from "kippy";
91
93
 
92
94
  const entity = new Entity({
95
+ // These are all optional, and can also be set later
96
+
97
+ // Graphics
93
98
  animator, // Entity's animator to be rendered, type Animator
94
99
  sprite, // Entity's sprite to be rendered, type Sprite
100
+ // Position
95
101
  position, // Entity's position (centered), type Vector2
96
102
  rotation, // Entity's rotation in radians, type number
103
+ // Physics
97
104
  body, // Entity's physical body, type EntityBody
98
105
  collider, // Entity's collider, type Collider
99
-
106
+ // Effects
107
+ glow
100
108
  });
101
109
 
102
110
  // Add it to a scene
@@ -128,6 +136,54 @@ const sprite = new Sprite({
128
136
  entity.sprite = sprite;
129
137
  ```
130
138
 
139
+ ### Animation
140
+
141
+ First you create a sprite sheet that contains your frames:
142
+ ```js
143
+ import { SpriteSheet } from "kippy";
144
+
145
+ const spriteSheet = new SpriteSheet({
146
+ texture, // Similar to sprite texture
147
+ frameWidth, // Width of each frame, type number
148
+ frameHeight, // Height of each frame, type number
149
+ width, // Width when render, default is frameWidth
150
+ height // Height when render, default is frameHeight
151
+ });
152
+ ```
153
+
154
+ Then you create an animation object which defines the order of frame to play, fps, and whether to loop or not:
155
+ ```js
156
+ import { Animation } from "kippy";
157
+
158
+ const animation = new Animation({
159
+ spriteSheet, // A SpriteSheet instance
160
+ frames, // Order of frames, type number[]
161
+ fps, // Frames per second, type number
162
+ loop // Loop animation endlessly, type boolean
163
+ });
164
+ ```
165
+
166
+ Then you create an animator, attach it to an entity, and then play an animation:
167
+ ```js
168
+ import { Animator } from "kippy";
169
+
170
+ const animator = new Animator({
171
+ animations, // A Record<string, Animation> map that contains all animations
172
+ default // The animation that will be played first, type string
173
+ });
174
+
175
+ // Attach it to an entity
176
+ entity.animator = animator
177
+
178
+ // Play an animation
179
+ entity.animator.play("animationName");
180
+
181
+ // You can also call a handler when animation ends
182
+ entity.animator.onEnd = function(name) {
183
+ // Do something
184
+ }
185
+ ```
186
+
131
187
  ### Add controls
132
188
 
133
189
  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,58 +374,29 @@ camera.zoom // Camera zoom level, default is 1
318
374
  camera.screenToWorld(input.pointer); // Return new vector
319
375
  ```
320
376
 
321
- ### Animation
377
+ ### Audio
322
378
 
323
- First you create a sprite sheet that contains your frames:
324
- ```js
325
- import { SpriteSheet } from "kippy";
379
+ To be added, for now use web's built-in `Audio` class.
326
380
 
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
- width, // Width when render, default is frameWidth
332
- height // Height when render, default is frameHeight
333
- });
334
- ```
381
+ ### Effects
335
382
 
336
- Then you create an animation object which defines the order of frame to play, fps, and whether to loop or not:
337
- ```js
338
- import { Animation } from "kippy";
383
+ Kippy comes with some graphical effects to make things look nicer :)
339
384
 
340
- const animation = new Animation({
341
- spriteSheet, // A SpriteSheet instance
342
- frames, // Order of frames, type number[]
343
- fps, // Frames per second, type number
344
- loop // Loop animation endlessly, type boolean
345
- });
346
- ```
385
+ #### Entity glow
347
386
 
348
- Then you create an animator, attach it to an entity, and then play an animation:
349
387
  ```js
350
- import { Animator } from "kippy";
388
+ entity.glow = {
389
+ color, // Color code as string
390
+ bloom, // Glow bloom/range of spread
391
+ intensity, // Glow intensity/boldness
392
+ }
351
393
 
352
- const animator = new Animator({
353
- animations, // A Record<string, Animation> map that contains all animations
354
- default // The animation that will be played first, type string
394
+ // You can also assign it during entity initialization in case you forgot
395
+ const newEntity = new Entity({
396
+ glow: { /* Your config */ }
355
397
  });
356
-
357
- // Attach it to an entity
358
- entity.animator = animator
359
-
360
- // Play an animation
361
- entity.animator.play("animationName");
362
-
363
- // You can also call a handler when animation ends
364
- entity.animator.onEnd = function(name) {
365
- // Do something
366
- }
367
398
  ```
368
399
 
369
- ### Audio
370
-
371
- To be added, for now use web's built-in `Audio` class.
372
-
373
400
  ### Sleep system
374
401
 
375
402
  When a body's velocity is too low for too long, the body will enter sleep state, which means its position will not be affected by the physics engine until a force is applied, a collision happens, or the velocity is above threshold again, this is to prevent jittering and optimize performance.
@@ -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,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 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;glow;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,this.glow=e.glow}onCollisionEnter;onCollisionStay;onCollisionExit;onTriggerEnter;onTriggerStay;onTriggerExit;drawSelf(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.drawImage(e.texture,o,n,e.frameWidth,e.frameHeight,-r/2,-a/2,r,a)}else this.sprite&&t.drawImage(this.sprite.texture,-this.sprite.width/2,-this.sprite.height/2,this.sprite.width,this.sprite.height)}render(t){if(t.save(),t.translate(this.position.x,this.position.y),t.rotate(this.rotation),this.glow){const e=this.animator?.currentAnimation.spriteSheet,i=e?e.width??e.frameWidth:this.sprite?.width??0,s=e?e.height??e.frameHeight:this.sprite?.height??0,o=2*this.glow.bloom,n=i+o,r=s+o,a=new OffscreenCanvas(n,r),h=a.getContext("2d");h.translate(n/2,r/2),this.drawSelf(h),t.shadowColor=this.glow.color,t.shadowBlur=this.glow.bloom;for(let e=0;e<this.glow.intensity;e++)t.drawImage(a,-n/2,-r/2);t.shadowColor="transparent",t.shadowBlur=0}this.drawSelf(t),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};
@@ -2,6 +2,11 @@ import { Animator } from "./animation.js";
2
2
  import { Collider, CollisionInfo, EntityBody } from "./physics.js";
3
3
  import { Sprite } from "./sprite.js";
4
4
  import { Vector2 } from "./vector.js";
5
+ export interface GlowEffect {
6
+ color: string;
7
+ bloom: number;
8
+ intensity: number;
9
+ }
5
10
  export interface EntityOptions {
6
11
  animator?: Animator;
7
12
  sprite?: Sprite;
@@ -9,6 +14,7 @@ export interface EntityOptions {
9
14
  rotation?: number;
10
15
  body?: EntityBody;
11
16
  collider?: Collider;
17
+ glow?: GlowEffect;
12
18
  }
13
19
  export declare class Entity {
14
20
  animator?: Animator;
@@ -17,6 +23,7 @@ export declare class Entity {
17
23
  rotation: number;
18
24
  body?: EntityBody;
19
25
  collider?: Collider;
26
+ glow?: GlowEffect;
20
27
  constructor(options?: EntityOptions);
21
28
  onCollisionEnter?: (other: Entity, info: CollisionInfo) => void;
22
29
  onCollisionStay?: (other: Entity, info: CollisionInfo) => void;
@@ -24,5 +31,6 @@ export declare class Entity {
24
31
  onTriggerEnter?: (other: Entity) => void;
25
32
  onTriggerStay?: (other: Entity) => void;
26
33
  onTriggerExit?: (other: Entity) => void;
34
+ drawSelf(ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D): void;
27
35
  render(ctx: CanvasRenderingContext2D): void;
28
36
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kippy",
3
- "version": "0.7.1",
3
+ "version": "0.7.3",
4
4
  "description": "Kippy 2D web game engine for JS",
5
5
  "keywords": [
6
6
  "kippy",