@blorkfield/overlay-core 0.8.11 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -22,26 +22,82 @@ pnpm add @blorkfield/overlay-core
22
22
 
23
23
  ### Tag Based Behavior
24
24
 
25
- Objects don't have fixed types. Instead, their behavior is determined by string tags. Import the tag constants to avoid magic strings:
25
+ Tags are the source of truth for object behavior. Adding a tag activates the associated effect; removing it deactivates it. There are no separate type systems — the tag array on each object drives everything.
26
+
27
+ Import the tag constants to avoid magic strings:
26
28
 
27
29
  ```typescript
28
- import { TAGS, TAG_FALLING, TAG_GRABABLE, TAG_FOLLOW_WINDOW } from '@blorkfield/overlay-core';
30
+ import { TAGS, TAG_STATIC, TAG_GRABABLE, TAG_FOLLOW_WINDOW } from '@blorkfield/overlay-core';
29
31
 
30
32
  // Use individual constants
31
- scene.spawnObject({ tags: [TAG_FALLING, TAG_GRABABLE], ... });
33
+ scene.spawnObject({ tags: [TAG_GRABABLE], ... }); // dynamic by default
34
+ scene.spawnObject({ tags: [TAG_STATIC, TAG_GRABABLE], ... }); // static obstacle
32
35
 
33
36
  // Or destructure from TAGS object
34
- const { FALLING, GRABABLE } = TAGS;
35
- scene.spawnObject({ tags: [FALLING, GRABABLE], ... });
37
+ const { STATIC, GRABABLE } = TAGS;
38
+ scene.spawnObject({ tags: [STATIC], ... });
36
39
  ```
37
40
 
38
41
  | Constant | Value | Behavior |
39
42
  |----------|-------|----------|
40
- | `TAG_FALLING` / `TAGS.FALLING` | `'falling'` | Object is dynamic and affected by gravity |
41
- | `TAG_FOLLOW_WINDOW` / `TAGS.FOLLOW_WINDOW` | `'follow_window'` | Object follows mouse position when grounded |
43
+ | `TAG_STATIC` / `TAGS.STATIC` | `'static'` | Object is a static obstacle, not affected by gravity. Without this tag, objects are dynamic by default. |
44
+ | `TAG_FOLLOW_WINDOW` / `TAGS.FOLLOW_WINDOW` | `'follow_window'` | Object walks toward a target when grounded (default: mouse) |
42
45
  | `TAG_GRABABLE` / `TAGS.GRABABLE` | `'grabable'` | Object can be grabbed and moved with mouse |
46
+ | `TAG_GRAVITY_OVERRIDE` / `TAGS.GRAVITY_OVERRIDE` | `'gravity_override'` | Object uses its own gravity vector instead of scene gravity |
47
+ | `TAG_SPEED_OVERRIDE` / `TAGS.SPEED_OVERRIDE` | `'speed_override'` | Multiplies movement speed for `follow_window` and future movement behaviors. Negative = run away from target. |
48
+ | `TAG_MASS_OVERRIDE` / `TAGS.MASS_OVERRIDE` | `'mass_override'` | Overrides the physics mass. Higher mass resists follow forces more; lower mass allows the follow force to overcome gravity. |
49
+
50
+ Tags can be added and removed at runtime to change behavior dynamically:
51
+
52
+ ```typescript
53
+ scene.addTag(id, 'static'); // freeze an object in place
54
+ scene.removeTag(id, 'static'); // release it to fall
55
+ scene.addTag(id, 'grabable'); // make it grabbable
56
+ scene.removeTag(id, 'grabable'); // prevent grabbing
57
+ ```
58
+
59
+ The `gravity_override` tag carries a value (a Vector2). Use `setObjectGravityOverride` to set the value and activate the tag, or pass it via `gravityOverride` in spawn config. Removing the tag restores scene gravity.
60
+
61
+ The `speed_override` tag carries a numeric multiplier. Use `setObjectSpeedOverride` to set the value and activate the tag, or pass it via `speedOverride` in spawn config. Removing the tag restores default speed.
62
+
63
+ The `mass_override` tag carries a numeric mass value. Use `setObjectMassOverride` to set the value and activate the tag, or pass it via `massOverride` in spawn config. Removing the tag restores the natural density-based mass.
64
+
65
+ ### Entity Tags
66
+
67
+ Every spawned object is automatically assigned a permanent, human-readable **entity tag** that serves as its stable identity. The format is derived from the object's shape or image:
68
+
69
+ | Object type | Entity tag format | Example |
70
+ |-------------|-------------------|---------|
71
+ | Circle | `circle-<4hex>` | `circle-a3f2` |
72
+ | Rectangle | `rect-<4hex>` | `rect-b1c4` |
73
+ | Image-based | `<filename>-<4hex>` | `cat-e9d2` |
74
+ | Text letter | `letter-<char>-<4hex>` | `letter-h-7a3f` |
75
+ | DOM element | `dom-<4hex>` | `dom-5c21` |
76
+
77
+ Entity tags appear in `getAllTags()` alongside all other tags, making them usable as follow targets or for runtime queries. They **cannot be removed** — calling `removeTag(id, entityTag)` will log a warning and return without effect. This ensures every object always has a stable, identifiable tag.
78
+
79
+ ### `follow_window` Tag Target
80
+
81
+ The `follow_window` tag walks an object toward a target when grounded. The default target is `'mouse'`. You can change it at any time:
82
+
83
+ ```typescript
84
+ // Follow the mouse (default)
85
+ scene.addTag(id, 'follow_window');
43
86
 
44
- Without the `falling` tag, objects are static and won't move.
87
+ // Follow a named target (e.g., another entity's tag)
88
+ scene.setFollowWindowTarget(id, 'circle-a3f2');
89
+
90
+ // Follow any entity that has a given tag
91
+ scene.setFollowWindowTarget(id, 'letter-h-7a3f');
92
+
93
+ // Follow a string/word group tag
94
+ scene.setFollowWindowTarget(id, 'title-text');
95
+
96
+ // Stop following by removing the tag
97
+ scene.removeTag(id, 'follow_window');
98
+ ```
99
+
100
+ Target resolution order: named follow targets (e.g., `'mouse'`) → object ID → first object with a matching tag.
45
101
 
46
102
  ### Pressure System
47
103
 
@@ -82,7 +138,7 @@ const { canvas, bounds } = OverlayScene.createContainer(container, {
82
138
  // Create scene
83
139
  const scene = new OverlayScene(canvas, {
84
140
  bounds,
85
- gravity: 1,
141
+ gravity: { x: 0, y: -1 },
86
142
  wrapHorizontal: true,
87
143
  background: 'transparent'
88
144
  });
@@ -97,22 +153,22 @@ All objects are created through `spawnObject()` (or `spawnObjectAsync()` for ima
97
153
  ### Basic Shapes
98
154
 
99
155
  ```typescript
100
- // Circle (dynamic, falls with gravity)
156
+ // Circle (dynamic by default — falls with gravity)
101
157
  scene.spawnObject({
102
158
  x: 100,
103
159
  y: 50,
104
160
  radius: 20,
105
161
  fillStyle: '#ff0000',
106
- tags: ['falling']
107
162
  });
108
163
 
109
- // Rectangle (static, doesn't move)
164
+ // Rectangle (static obstacle — won't move)
110
165
  scene.spawnObject({
111
166
  x: 200,
112
167
  y: 300,
113
168
  width: 100,
114
169
  height: 20,
115
- fillStyle: '#0000ff'
170
+ fillStyle: '#0000ff',
171
+ tags: ['static']
116
172
  });
117
173
 
118
174
  // Polygon shapes
@@ -121,7 +177,6 @@ scene.spawnObject({
121
177
  y: 100,
122
178
  radius: 25,
123
179
  fillStyle: '#00ff00',
124
- tags: ['falling'],
125
180
  shape: { type: 'hexagon' }
126
181
  });
127
182
  ```
@@ -136,7 +191,7 @@ const id = await scene.spawnObjectAsync({
136
191
  y: 100,
137
192
  imageUrl: '/images/coin.png',
138
193
  size: 50,
139
- tags: ['falling', 'grabable']
194
+ tags: ['grabable'] // dynamic by default
140
195
  });
141
196
  ```
142
197
 
@@ -241,14 +296,16 @@ const result = await scene.addTTFTextObstacles({
241
296
  });
242
297
  ```
243
298
 
299
+ Text obstacles default to `isStatic: true` — they are static obstacles that things fall onto. Pass `isStatic: false` to spawn falling text instead.
300
+
244
301
  ### Managing Text Obstacles
245
302
 
246
303
  ```typescript
247
- // Spawn text that immediately falls (already has 'falling' tag)
304
+ // Spawn text that immediately falls (isStatic: false)
248
305
  const result = await scene.spawnFallingTextObstacles(config);
249
306
  const result = await scene.spawnFallingTTFTextObstacles(config);
250
307
 
251
- // Release text obstacles (add 'falling' tag)
308
+ // Release static text obstacles (removes 'static' tag so they fall)
252
309
  scene.releaseTextObstacles(wordTag);
253
310
 
254
311
  // Release letters one by one with delay
@@ -279,7 +336,7 @@ scene.setEffect({
279
336
  objectConfig: {
280
337
  radius: 15,
281
338
  fillStyle: '#4a90d9',
282
- tags: ['falling']
339
+ // dynamic by default — no tags needed
283
340
  },
284
341
  probability: 1,
285
342
  minScale: 0.8,
@@ -341,14 +398,24 @@ scene.removeAllObjects();
341
398
  scene.removeAll(); // Alias for removeAllObjects
342
399
  scene.removeAllByTag('tag'); // Alias for removeObjectsByTag
343
400
 
344
- // Add or remove tags
345
- scene.addTag(id, 'falling');
346
- scene.addFallingTag(id); // Convenience for adding 'falling' tag
347
- scene.removeTag(id, 'grabable');
401
+ // Add or remove tags — tags drive behavior, changes take effect immediately
402
+ scene.addTag(id, 'grabable');
403
+ scene.removeTag(id, 'static'); // releases a static object to fall
404
+ scene.addTag(id, 'static'); // freezes a dynamic object in place
405
+ scene.addFallingTag(id); // convenience: removes 'static', adds 'grabable'
406
+ scene.setFollowWindowTarget(id, 'mouse'); // change what a follow_window object walks toward
407
+ scene.setObjectSpeedOverride(id, 2); // double movement speed (negative = run away)
408
+ scene.setObjectSpeedOverride(id, null); // remove speed override
409
+ scene.setObjectMassOverride(id, 50); // heavy object resists follow force
410
+ scene.setObjectMassOverride(id, null); // restore natural mass
411
+ scene.setPosition(id, { x: 400, y: 300 }); // teleport object
412
+ scene.setVelocity(id, { x: 10, y: 0 }); // launch object rightward
413
+ scene.setObjectAngularVelocity(id, Math.PI); // set spin (rad/s)
414
+ scene.setObjectScale(id, 2, 2); // scale x and y independently
348
415
 
349
416
  // Get object info
350
417
  const ids = scene.getObjectIds();
351
- const tagged = scene.getObjectIdsByTag('falling');
418
+ const tagged = scene.getObjectIdsByTag('static');
352
419
  const allTags = scene.getAllTags();
353
420
 
354
421
  // Get full object state
@@ -361,13 +428,25 @@ const objs = scene.getObjectsByTag('tag'); // Returns ObjectState[]
361
428
  ```typescript
362
429
  // Apply force to objects
363
430
  scene.applyForce(id, { x: 0.01, y: -0.02 });
364
- scene.applyForceToTag('falling', { x: 0.005, y: 0 });
431
+ scene.applyForceToTag('grabable', { x: 0.005, y: 0 });
365
432
 
366
- // Set velocity directly
367
- scene.setVelocity(id, { x: 5, y: -10 });
433
+ // Set velocity directly (physical convention: positive y = upward, negative y = downward)
434
+ scene.setVelocity(id, { x: 5, y: 10 }); // moving right and upward
435
+ scene.setVelocity(id, { x: 0, y: -10 }); // moving downward
436
+ scene.setVelocity(id, { x: 0, y: 0 }); // stop all movement
368
437
 
369
- // Set position directly
438
+ // Set position directly (screen pixels, y=0 at top)
370
439
  scene.setPosition(id, { x: 100, y: 200 });
440
+
441
+ // Set angular velocity (spin) in radians/second
442
+ // Positive = counter-clockwise, negative = clockwise
443
+ scene.setObjectAngularVelocity(id, Math.PI); // half-rotation per second CCW
444
+ scene.setObjectAngularVelocity(id, 0); // stop spinning
445
+
446
+ // Scale an object (updates both physics shape and sprite rendering)
447
+ scene.setObjectScale(id, 2, 2); // double size uniformly
448
+ scene.setObjectScale(id, 2, 0.5); // stretch wide, squash tall
449
+ scene.setObjectScale(id, 1, 1); // restore original size
371
450
  ```
372
451
 
373
452
  ## Mouse Position and Grab API
@@ -426,7 +505,7 @@ const currentGrab = scene.getGrabbedObject(); // Returns ID or null
426
505
  ```typescript
427
506
  const scene = new OverlayScene(canvas, {
428
507
  bounds: { top: 0, bottom: 600, left: 0, right: 800 },
429
- gravity: 1,
508
+ gravity: { x: 0, y: -1 },
430
509
  wrapHorizontal: true,
431
510
  debug: false,
432
511
  background: '#16213e',
@@ -443,7 +522,7 @@ const scene = new OverlayScene(canvas, {
443
522
 
444
523
  | Option | Default | Description |
445
524
  |--------|---------|-------------|
446
- | `gravity` | 1 | Gravity strength |
525
+ | `gravity` | `{ x: 0, y: 1 }` | Gravity vector. Both axes support negative values |
447
526
  | `wrapHorizontal` | true | Objects wrap around screen edges |
448
527
  | `debug` | false | Show collision wireframes |
449
528
  | `background` | transparent | Canvas background color |
@@ -507,7 +586,7 @@ Called every frame with all dynamic object states:
507
586
 
508
587
  ```typescript
509
588
  scene.onUpdate((data) => {
510
- // data.objects contains all dynamic (falling) objects
589
+ // data.objects contains all dynamic objects (those without the 'static' tag)
511
590
  for (const obj of data.objects) {
512
591
  console.log(obj.id, obj.x, obj.y, obj.angle, obj.tags);
513
592
  }
@@ -641,11 +720,104 @@ setLogLevel('debug'); // Options: debug, info, warn, error
641
720
  ## Lifecycle
642
721
 
643
722
  ```typescript
644
- scene.start(); // Start simulation
645
- scene.stop(); // Pause simulation
646
- scene.resize(w, h); // Resize canvas and bounds
647
- scene.setDebug(true); // Toggle wireframe mode
648
- scene.destroy(); // Clean up resources
723
+ scene.start(); // Start simulation
724
+ scene.stop(); // Pause simulation
725
+ scene.resize(w, h); // Resize canvas and bounds
726
+ scene.setDebug(true); // Toggle wireframe mode
727
+ scene.setGravity({ x: 0, y: -1 }); // Set gravity (negative y = upward)
728
+ scene.setGravity({ x: 0, y: 0 }); // Zero gravity
729
+ scene.setGravity({ x: 1, y: 0 }); // Sideways gravity
730
+ scene.destroy(); // Clean up resources
731
+ ```
732
+
733
+ ### Per-Object Gravity Override
734
+
735
+ Dynamic objects can have their own gravity vector instead of the scene gravity. Set it via `gravityOverride` in spawn config, or change it at runtime with `setObjectGravityOverride`. This automatically adds or removes the `gravity_override` tag.
736
+
737
+ ```typescript
738
+ // Spawn a floaty object that drifts upward
739
+ scene.spawnObject({
740
+ x: 200, y: 300,
741
+ radius: 20,
742
+ fillStyle: '#4a90d9',
743
+ tags: ['grabable'],
744
+ gravityOverride: { x: 0, y: -0.3 } // floats upward
745
+ });
746
+
747
+ // Zero gravity — hovers in place
748
+ scene.spawnObject({
749
+ x: 400, y: 200,
750
+ radius: 15,
751
+ fillStyle: '#e94560',
752
+ gravityOverride: { x: 0, y: 0 }
753
+ });
754
+
755
+ // Change or clear a gravity override at runtime
756
+ scene.setObjectGravityOverride(id, { x: 0.5, y: 0 }); // drift sideways
757
+ scene.setObjectGravityOverride(id, null); // restore scene gravity
758
+
759
+ // removeTag works too — setObjectGravityOverride(id, null) calls it internally
760
+ scene.removeTag(id, 'gravity_override'); // restores scene gravity
761
+ ```
762
+
763
+ Tags are the source of truth for all behavior. Boolean tags (presence = active, absence = inactive). `gravity_override` additionally carries a Vector2 value managed via `setObjectGravityOverride` or `gravityOverride` in spawn config.
764
+
765
+ | Tag | Behavior |
766
+ |-----|----------|
767
+ | `static` | Static obstacle, not affected by gravity. Absent by default — objects are dynamic unless tagged static. |
768
+ | `grabable` | Can be grabbed and dragged with the mouse |
769
+ | `follow_window` | Always applies a directional force toward its target (in all axes). Gravity determines whether the entity can actually reach targets above/below it. |
770
+ | `gravity_override` | Uses its own gravity vector instead of scene gravity (value set via `setObjectGravityOverride`) |
771
+ | `speed_override` | Multiplies movement speed for `follow_window` and future movement behaviors (value set via `setObjectSpeedOverride`). Negative = runs away from target. Default multiplier: 1 |
772
+ | `mass_override` | Overrides physics mass (value set via `setObjectMassOverride`). Higher mass resists follow forces; lower mass may allow entity to overcome gravity. |
773
+
774
+ ### Speed Override
775
+
776
+ Objects with `follow_window` move toward their target at default speed. `speed_override` multiplies this force. Negative values reverse the direction, causing the object to run away.
777
+
778
+ ```typescript
779
+ // Spawn a fast follower
780
+ scene.spawnObject({
781
+ tags: ['follow_window'],
782
+ speedOverride: 3, // 3× normal speed — automatically adds 'speed_override' tag
783
+ // ...
784
+ });
785
+
786
+ // Spawn a coward that runs away from the mouse
787
+ scene.spawnObject({
788
+ tags: ['follow_window'],
789
+ speedOverride: -2, // flees at 2× speed
790
+ // ...
791
+ });
792
+
793
+ // Change speed at runtime
794
+ scene.setObjectSpeedOverride(id, 5); // very fast follower
795
+ scene.setObjectSpeedOverride(id, -1); // now runs away at normal speed
796
+ scene.setObjectSpeedOverride(id, null); // remove override, restore default speed
797
+ ```
798
+
799
+ ### Mass Override
800
+
801
+ Entities have a default density of `0.005`, which gives typical sizes a mass of ~6–20 units — enough that normal gravity prevents the follow force from lifting them vertically. `mass_override` lets you change this at spawn or runtime.
802
+
803
+ ```typescript
804
+ // Light object — follow force can overcome normal gravity, moves freely in all directions
805
+ scene.spawnObject({
806
+ tags: ['follow_window'],
807
+ massOverride: 1,
808
+ // ...
809
+ });
810
+
811
+ // Very heavy object — barely moves toward target under normal gravity
812
+ scene.spawnObject({
813
+ tags: ['follow_window'],
814
+ massOverride: 100,
815
+ // ...
816
+ });
817
+
818
+ // Change or clear mass at runtime
819
+ scene.setObjectMassOverride(id, 50); // now heavy
820
+ scene.setObjectMassOverride(id, null); // restore natural mass
649
821
  ```
650
822
 
651
823
  ## Examples
@@ -673,6 +845,7 @@ import type {
673
845
  // Scene configuration
674
846
  OverlaySceneConfig,
675
847
  Bounds,
848
+ Vector2,
676
849
  ContainerOptions,
677
850
  FloorConfig,
678
851
 
@@ -732,5 +905,5 @@ import type {
732
905
  } from '@blorkfield/overlay-core';
733
906
 
734
907
  // Tag constants (values, not types)
735
- import { TAGS, TAG_FALLING, TAG_GRABABLE, TAG_FOLLOW_WINDOW } from '@blorkfield/overlay-core';
908
+ import { TAGS, TAG_FALLING, TAG_GRABABLE, TAG_FOLLOW_WINDOW, TAG_GRAVITY_OVERRIDE } from '@blorkfield/overlay-core';
736
909
  ```