@jiant/canvable 0.0.3 → 0.0.5

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/dist/main.mjs CHANGED
@@ -255,13 +255,14 @@ var Vector2D = class _Vector2D {
255
255
  }
256
256
  };
257
257
 
258
- // src/nodes/Node.ts
258
+ // src/nodes/shapes/Node.ts
259
259
  import { v4 as uuidv4 } from "uuid";
260
260
  var Node = class {
261
261
  pos;
262
262
  id;
263
263
  children = [];
264
264
  scene;
265
+ boundingBox;
265
266
  constructor(scene, config) {
266
267
  this.scene = scene;
267
268
  const { id, pos } = config || {};
@@ -288,25 +289,54 @@ var Node = class {
288
289
  }
289
290
  };
290
291
 
291
- // src/nodes/BoundingBox.ts
292
+ // src/nodes/boundingBox/BoundingBox.ts
292
293
  var BoundingBox = class extends Node {
293
294
  };
294
295
 
295
- // src/nodes/Camera.ts
296
- var Camera = class extends Node {
297
- zoom;
298
- constructor(zoom = 1) {
299
- super();
300
- this.zoom = zoom;
296
+ // src/nodes/boundingBox/SquareBoundingBox.ts
297
+ var SquareBoundingBox = class _SquareBoundingBox extends BoundingBox {
298
+ size;
299
+ constructor(scene, size) {
300
+ super(scene);
301
+ this.size = size || new Vector2D(10, 10);
301
302
  }
302
- update(deltaTime) {
303
+ checkCollision(other) {
304
+ if (other instanceof _SquareBoundingBox) {
305
+ const thisPoint = this.scene?.camera.transformCoordinates(new Vector2D(this.pos.x, this.pos.y));
306
+ const otherPoint = this.scene?.camera.transformCoordinates(new Vector2D(other.pos.x, other.pos.y));
307
+ if (!thisPoint || !otherPoint) return { isColliding: false };
308
+ const dx = Math.abs(thisPoint.x - otherPoint.x);
309
+ const dy = Math.abs(thisPoint.y - otherPoint.y);
310
+ const halfWidths = (this.size.x + other.size.x) / 2;
311
+ const halfHeights = (this.size.y + other.size.y) / 2;
312
+ if (dx < halfWidths && dy < halfHeights) {
313
+ const overlapX = halfWidths - dx;
314
+ const overlapY = halfHeights - dy;
315
+ if (overlapX < overlapY) {
316
+ const mtvX = thisPoint.x < otherPoint.x ? -overlapX : overlapX;
317
+ return { isColliding: true, mtv: new Vector2D(mtvX, 0) };
318
+ } else {
319
+ const mtvY = thisPoint.y < otherPoint.y ? -overlapY : overlapY;
320
+ return { isColliding: true, mtv: new Vector2D(0, mtvY) };
321
+ }
322
+ }
323
+ }
324
+ if (other instanceof CircleBoundingBox) {
325
+ const result = other.checkCollision(this);
326
+ if (result.isColliding && result.mtv) {
327
+ return { isColliding: true, mtv: new Vector2D(-result.mtv.x, -result.mtv.y) };
328
+ }
329
+ }
330
+ return { isColliding: false };
303
331
  }
304
- transformCoordinates(position) {
305
- return new Vector2D((position.x - this.pos.x) * this.zoom, (position.y - this.pos.y) * this.zoom);
332
+ isPointInside(_point) {
333
+ const x_inside = _point.x >= this.pos.x - this.size.x && _point.x <= this.pos.x + this.size.x;
334
+ const y_inside = _point.y >= this.pos.y - this.size.y && _point.y <= this.pos.y + this.size.y;
335
+ return x_inside && y_inside;
306
336
  }
307
337
  };
308
338
 
309
- // src/nodes/CircleBoundingBox.ts
339
+ // src/nodes/boundingBox/CircleBoundingBox.ts
310
340
  var CircleBoundingBox = class _CircleBoundingBox extends BoundingBox {
311
341
  radius;
312
342
  constructor(scene, radius) {
@@ -315,15 +345,28 @@ var CircleBoundingBox = class _CircleBoundingBox extends BoundingBox {
315
345
  }
316
346
  checkCollision(other) {
317
347
  if (other instanceof _CircleBoundingBox) {
318
- const thisPoint = this.scene?.camera.transformCoordinates(new Vector2D(this.pos.x, this.pos.y));
319
- const otherPoint = this.scene?.camera.transformCoordinates(new Vector2D(other.pos.x, other.pos.y));
320
- if (!thisPoint || !otherPoint) return false;
321
- const dx = thisPoint.x - otherPoint.x;
322
- const dy = thisPoint.y - otherPoint.y;
348
+ const dx = this.pos.x - other.pos.x;
349
+ const dy = this.pos.y - other.pos.y;
323
350
  const distance = Math.sqrt(dx * dx + dy * dy);
324
- return distance < this.radius + other.radius;
351
+ if (distance < this.radius + other.radius) {
352
+ const overlap = this.radius + other.radius - distance;
353
+ const mtv = distance === 0 ? new Vector2D(overlap, 0) : new Vector2D(dx / distance * overlap, dy / distance * overlap);
354
+ return { isColliding: true, mtv };
355
+ }
325
356
  }
326
- return false;
357
+ if (other instanceof SquareBoundingBox) {
358
+ const closestX = Math.max(other.pos.x - other.size.x / 2, Math.min(this.pos.x, other.pos.x + other.size.x / 2));
359
+ const closestY = Math.max(other.pos.y - other.size.y / 2, Math.min(this.pos.y, other.pos.y + other.size.y / 2));
360
+ const dx = this.pos.x - closestX;
361
+ const dy = this.pos.y - closestY;
362
+ const distance = Math.sqrt(dx * dx + dy * dy);
363
+ if (distance < this.radius) {
364
+ const overlap = this.radius - distance;
365
+ const mtv = distance === 0 ? new Vector2D(overlap, 0) : new Vector2D(dx / distance * overlap, dy / distance * overlap);
366
+ return { isColliding: true, mtv };
367
+ }
368
+ }
369
+ return { isColliding: false };
327
370
  }
328
371
  isPointInside(_point) {
329
372
  const point = this.scene?.camera.transformCoordinates(_point);
@@ -335,40 +378,104 @@ var CircleBoundingBox = class _CircleBoundingBox extends BoundingBox {
335
378
  }
336
379
  };
337
380
 
338
- // src/nodes/Circle.ts
339
- var Circle = class extends Node {
340
- vel = new Vector2D();
341
- size = 10;
342
- friction = 0.1;
343
- boundingBox = new CircleBoundingBox(this.scene, this.size);
344
- constructor(scene, config) {
345
- super(scene);
346
- this.vel = config?.vel || new Vector2D();
347
- this.size = config?.size || 10;
348
- this.friction = config?.friction || 0.1;
349
- if (config?.startsWithBoundingBox) {
350
- this.boundingBox = new CircleBoundingBox(scene, this.size);
351
- this.addObject(this.boundingBox);
381
+ // src/nodes/Camera.ts
382
+ var Camera = class extends Node {
383
+ zoom;
384
+ constructor(zoom = 1) {
385
+ super();
386
+ this.zoom = zoom;
387
+ }
388
+ update(deltaTime) {
389
+ }
390
+ transformCoordinates(position) {
391
+ return new Vector2D((position.x - this.pos.x) * this.zoom, (position.y - this.pos.y) * this.zoom);
392
+ }
393
+ };
394
+
395
+ // src/nodes/physics/StaticBody.ts
396
+ var StaticBody = class extends Node {
397
+ shape;
398
+ constructor(scene, shape) {
399
+ super(scene, { pos: shape.pos });
400
+ this.shape = shape;
401
+ this.pos = shape.pos;
402
+ }
403
+ update(_dt) {
404
+ if (this.shape.update && typeof this.shape.update === "function") {
405
+ this.shape.update(_dt);
352
406
  }
353
407
  }
354
408
  draw(ctx, selected) {
355
- const cameraPos = this.scene?.camera.pos || new Vector2D();
356
- const cameraScale = this.scene?.camera.zoom || 1;
357
- const relativePos = new Vector2D((this.pos.x - cameraPos.x) * cameraScale, (this.pos.y - cameraPos.y) * cameraScale);
358
- if (selected) {
359
- ctx.beginPath();
360
- ctx.arc(relativePos.x, relativePos.y, this.size + 5, 0, Math.PI * 2);
361
- ctx.fillStyle = "transparent";
362
- ctx.lineWidth = 3;
363
- ctx.strokeStyle = "white";
364
- ctx.stroke();
365
- ctx.closePath();
409
+ if (this.shape.draw) this.shape.draw(ctx, selected);
410
+ }
411
+ };
412
+
413
+ // src/nodes/physics/KinematicBody.ts
414
+ var KinematicBody = class _KinematicBody extends Node {
415
+ shape;
416
+ velocity = new Vector2D();
417
+ speed = 1500;
418
+ friction = 0.92;
419
+ maxSpeed = 800;
420
+ isColliding = false;
421
+ constructor(scene, shape, config) {
422
+ super(scene, { pos: shape.pos });
423
+ this.shape = shape;
424
+ if (config?.speed) this.speed = config.speed;
425
+ if (config?.friction) this.friction = config.friction;
426
+ if (config?.maxSpeed) this.maxSpeed = config.maxSpeed;
427
+ }
428
+ applyForce(x, y, dt) {
429
+ const len = Math.sqrt(x * x + y * y);
430
+ if (len > 0) {
431
+ const scale = this.speed * dt / len;
432
+ this.velocity.x += x * scale;
433
+ this.velocity.y += y * scale;
434
+ }
435
+ }
436
+ update(dt) {
437
+ this.velocity.x *= this.friction;
438
+ this.velocity.y *= this.friction;
439
+ const currentSpeed = Math.sqrt(this.velocity.x * this.velocity.x + this.velocity.y * this.velocity.y);
440
+ if (currentSpeed > this.maxSpeed) {
441
+ const scale = this.maxSpeed / currentSpeed;
442
+ this.velocity.x *= scale;
443
+ this.velocity.y *= scale;
444
+ }
445
+ if (Math.abs(this.velocity.x) < 0.1) this.velocity.x = 0;
446
+ if (Math.abs(this.velocity.y) < 0.1) this.velocity.y = 0;
447
+ this.pos.x += this.velocity.x * dt;
448
+ this.pos.y += this.velocity.y * dt;
449
+ this.shape.pos = this.pos;
450
+ if (this.shape.update) this.shape.update(dt);
451
+ this.resolveCollisions();
452
+ }
453
+ resolveCollisions() {
454
+ this.isColliding = false;
455
+ if (!this.shape.boundingBox) return;
456
+ if (!this.scene) return;
457
+ for (const obj of this.scene.children) {
458
+ if (obj === this) continue;
459
+ let otherBBox = null;
460
+ if (obj instanceof StaticBody || obj instanceof _KinematicBody) {
461
+ if (obj.shape.boundingBox) otherBBox = obj.shape.boundingBox;
462
+ } else {
463
+ if (obj.boundingBox) otherBBox = obj.boundingBox;
464
+ }
465
+ if (otherBBox) {
466
+ const result = this.shape.boundingBox.checkCollision(otherBBox);
467
+ if (result.isColliding && result.mtv) {
468
+ this.isColliding = true;
469
+ this.pos.x += result.mtv.x;
470
+ this.pos.y += result.mtv.y;
471
+ this.shape.pos = this.pos;
472
+ if (this.shape.boundingBox) this.shape.boundingBox.pos = this.pos;
473
+ }
474
+ }
366
475
  }
367
- ctx.beginPath();
368
- ctx.arc(relativePos.x, relativePos.y, this.size, 0, Math.PI * 2);
369
- ctx.fillStyle = "blue";
370
- ctx.fill();
371
- ctx.closePath();
476
+ }
477
+ draw(ctx, selected) {
478
+ if (this.shape.draw) this.shape.draw(ctx, selected);
372
479
  }
373
480
  };
374
481
 
@@ -394,14 +501,14 @@ var InputManager = class extends Node {
394
501
  this.mousePos = new Vector2D(event.clientX, event.clientY);
395
502
  }
396
503
  handleKeyDown(event) {
397
- const key = event.key;
504
+ const key = event.code;
398
505
  if (!this.keyState.get(key)) {
399
506
  this.keyState.set(key, true);
400
507
  this.keyJustPressed.add(key);
401
508
  }
402
509
  }
403
510
  handleKeyUp(event) {
404
- const key = event.key;
511
+ const key = event.code;
405
512
  this.keyState.set(key, false);
406
513
  this.keyJustPressed.delete(key);
407
514
  }
@@ -440,10 +547,12 @@ var InputManager = class extends Node {
440
547
  };
441
548
 
442
549
  // src/nodes/Scene.ts
443
- var Scene2 = class extends Node {
550
+ var Scene = class extends Node {
444
551
  camera;
445
552
  inputManager;
446
553
  ctx;
554
+ animationFrameId = null;
555
+ running = false;
447
556
  lastTime = 0;
448
557
  totalTime = 0;
449
558
  constructor(ctx, opts) {
@@ -468,7 +577,7 @@ var Scene2 = class extends Node {
468
577
  canvas.width = width;
469
578
  canvas.height = height;
470
579
  }
471
- update(deltaTime) {
580
+ updateFrame(deltaTime) {
472
581
  this.camera.update(deltaTime);
473
582
  this.children.forEach((child) => {
474
583
  if (isUpdatable(child)) {
@@ -476,16 +585,20 @@ var Scene2 = class extends Node {
476
585
  }
477
586
  });
478
587
  }
479
- draw(ctx) {
480
- ctx.fillStyle = ctx.canvas.style.backgroundColor;
588
+ drawFrame(ctx) {
589
+ ctx.fillStyle = ctx.canvas.style.backgroundColor || "black";
481
590
  ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
591
+ ctx.save();
592
+ ctx.translate(ctx.canvas.width / 2, ctx.canvas.height / 2);
482
593
  this.children.forEach((child) => {
483
594
  if (isDrawable(child)) {
484
595
  child.draw(ctx);
485
596
  }
486
597
  });
598
+ ctx.restore();
487
599
  }
488
- gameLoop(callback, targetFPS = 60) {
600
+ run(callback, targetFPS = 60) {
601
+ this.running = true;
489
602
  const frameInterval = 1e3 / targetFPS;
490
603
  let lastFrameTime = 0;
491
604
  const loop = (currentTime) => {
@@ -496,8 +609,8 @@ var Scene2 = class extends Node {
496
609
  this.lastTime = currentTime;
497
610
  this.totalTime += deltaTime;
498
611
  lastFrameTime = currentTime - elapsed % frameInterval;
499
- this.update(deltaTime);
500
- this.draw(this.ctx);
612
+ this.updateFrame(deltaTime);
613
+ this.drawFrame(this.ctx);
501
614
  if (callback) {
502
615
  callback(deltaTime, this.totalTime);
503
616
  }
@@ -509,7 +622,14 @@ var Scene2 = class extends Node {
509
622
  requestAnimationFrame(loop);
510
623
  };
511
624
  this.lastTime = performance.now();
512
- requestAnimationFrame(loop);
625
+ this.animationFrameId = requestAnimationFrame(loop);
626
+ }
627
+ stop() {
628
+ if (this.animationFrameId) {
629
+ cancelAnimationFrame(this.animationFrameId);
630
+ this.animationFrameId = null;
631
+ }
632
+ this.running = false;
513
633
  }
514
634
  };
515
635
  function isUpdatable(obj) {
@@ -519,6 +639,118 @@ function isDrawable(obj) {
519
639
  return "draw" in obj;
520
640
  }
521
641
 
642
+ // src/nodes/shapes/Circle.ts
643
+ var Circle = class extends Node {
644
+ vel = new Vector2D();
645
+ size = 10;
646
+ friction = 0.1;
647
+ style = {
648
+ fillStyle: "blue"
649
+ };
650
+ constructor(scene, config) {
651
+ super(scene);
652
+ this.vel = config?.vel || new Vector2D();
653
+ this.size = config?.size || 10;
654
+ this.friction = config?.friction || 0.1;
655
+ this.style = { ...this.style, ...config?.style };
656
+ if (config?.startsWithBoundingBox) {
657
+ this.boundingBox = new CircleBoundingBox(scene, this.size);
658
+ this.addObject(this.boundingBox);
659
+ }
660
+ }
661
+ update(_deltaTime) {
662
+ if (this.boundingBox) {
663
+ this.boundingBox.pos = this.pos;
664
+ }
665
+ }
666
+ draw(ctx, selected) {
667
+ const cameraPos = this.scene?.camera.pos || new Vector2D();
668
+ const cameraScale = this.scene?.camera.zoom || 1;
669
+ const relativePos = new Vector2D((this.pos.x - cameraPos.x) * cameraScale, (this.pos.y - cameraPos.y) * cameraScale);
670
+ if (selected) {
671
+ ctx.beginPath();
672
+ ctx.arc(relativePos.x, relativePos.y, this.size + 5, 0, Math.PI * 2);
673
+ ctx.fillStyle = "transparent";
674
+ ctx.lineWidth = 3;
675
+ ctx.strokeStyle = "white";
676
+ ctx.stroke();
677
+ ctx.closePath();
678
+ }
679
+ ctx.beginPath();
680
+ ctx.arc(relativePos.x, relativePos.y, this.size, 0, Math.PI * 2);
681
+ ctx.fillStyle = this.style.fillStyle || "blue";
682
+ ctx.fill();
683
+ if (this.style.strokeStyle) {
684
+ ctx.strokeStyle = this.style.strokeStyle;
685
+ ctx.lineWidth = this.style.lineWidth || 1;
686
+ ctx.stroke();
687
+ }
688
+ ctx.closePath();
689
+ }
690
+ };
691
+
692
+ // src/nodes/shapes/Square.ts
693
+ var Square = class extends Node {
694
+ vel = new Vector2D();
695
+ size;
696
+ friction = 0.1;
697
+ style = {
698
+ fillStyle: "orange"
699
+ };
700
+ constructor(scene, config) {
701
+ super(scene);
702
+ this.vel = config?.vel || new Vector2D();
703
+ this.size = config?.size || new Vector2D(10, 10);
704
+ this.friction = config?.friction || 0.1;
705
+ this.style = { ...this.style, ...config?.style };
706
+ if (config?.startsWithBoundingBox) {
707
+ this.boundingBox = new SquareBoundingBox(scene, this.size);
708
+ this.addObject(this.boundingBox);
709
+ }
710
+ }
711
+ update(_deltaTime) {
712
+ if (this.boundingBox) {
713
+ this.boundingBox.pos = this.pos;
714
+ }
715
+ }
716
+ draw(ctx, selected) {
717
+ const cameraPos = this.scene?.camera.pos || new Vector2D();
718
+ const cameraScale = this.scene?.camera.zoom || 1;
719
+ const relativePos = new Vector2D(
720
+ (this.pos.x - cameraPos.x) * cameraScale,
721
+ (this.pos.y - cameraPos.y) * cameraScale
722
+ );
723
+ const scaledSize = new Vector2D(this.size.x * cameraScale, this.size.y * cameraScale);
724
+ if (selected) {
725
+ ctx.strokeStyle = "white";
726
+ ctx.lineWidth = 3;
727
+ ctx.strokeRect(
728
+ relativePos.x - scaledSize.x / 2 - 5,
729
+ relativePos.y - scaledSize.y / 2 - 5,
730
+ scaledSize.x + 10,
731
+ scaledSize.y + 10
732
+ );
733
+ }
734
+ ctx.fillStyle = this.style.fillStyle || "orange";
735
+ ctx.fillRect(
736
+ relativePos.x - scaledSize.x / 2,
737
+ relativePos.y - scaledSize.y / 2,
738
+ scaledSize.x,
739
+ scaledSize.y
740
+ );
741
+ if (this.style.strokeStyle) {
742
+ ctx.strokeStyle = this.style.strokeStyle;
743
+ ctx.lineWidth = this.style.lineWidth || 1;
744
+ ctx.strokeRect(
745
+ relativePos.x - scaledSize.x / 2,
746
+ relativePos.y - scaledSize.y / 2,
747
+ scaledSize.x,
748
+ scaledSize.y
749
+ );
750
+ }
751
+ }
752
+ };
753
+
522
754
  // src/main.ts
523
755
  function getCanvas(id) {
524
756
  const canvas = document.getElementById(id);
@@ -532,8 +764,12 @@ export {
532
764
  Camera,
533
765
  Circle,
534
766
  CircleBoundingBox,
767
+ KinematicBody,
535
768
  Node,
536
- Scene2 as Scene,
769
+ Scene,
770
+ Square,
771
+ SquareBoundingBox,
772
+ StaticBody,
537
773
  TWO_PI,
538
774
  Vector2D,
539
775
  getCanvas
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jiant/canvable",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "description": "A tool for building canvas apps",
5
5
  "main": "./dist/main.js",
6
6
  "module": "./dist/main.mjs",
@@ -12,14 +12,17 @@
12
12
  ],
13
13
  "scripts": {
14
14
  "build": "tsup",
15
- "dev": "tsup --watch"
15
+ "watch": "tsup --watch",
16
+ "dev": "vite"
16
17
  },
17
18
  "dependencies": {
18
19
  "uuid": "^11.0.3"
19
20
  },
20
21
  "devDependencies": {
22
+ "@types/node": "^25.2.0",
23
+ "tsup": "^8.3.5",
21
24
  "typescript": "^5.7.2",
22
- "tsup": "^8.3.5"
25
+ "vite": "^7.3.1"
23
26
  },
24
27
  "keywords": [
25
28
  "canvas",
@@ -34,4 +37,4 @@
34
37
  "publishConfig": {
35
38
  "access": "public"
36
39
  }
37
- }
40
+ }