@blorkfield/overlay-core 0.9.0 → 0.10.1

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,27 +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 |
43
- | `TAG_GRAVITY_OVERRIDE` / `TAGS.GRAVITY_OVERRIDE` | `'gravity_override'` | Object uses its own gravity (set via `gravityOverride` in config) |
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');
44
86
 
45
- 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.
46
101
 
47
102
  ### Pressure System
48
103
 
@@ -83,7 +138,7 @@ const { canvas, bounds } = OverlayScene.createContainer(container, {
83
138
  // Create scene
84
139
  const scene = new OverlayScene(canvas, {
85
140
  bounds,
86
- gravity: { x: 0, y: 1 },
141
+ gravity: { x: 0, y: -1 },
87
142
  wrapHorizontal: true,
88
143
  background: 'transparent'
89
144
  });
@@ -98,22 +153,22 @@ All objects are created through `spawnObject()` (or `spawnObjectAsync()` for ima
98
153
  ### Basic Shapes
99
154
 
100
155
  ```typescript
101
- // Circle (dynamic, falls with gravity)
156
+ // Circle (dynamic by default — falls with gravity)
102
157
  scene.spawnObject({
103
158
  x: 100,
104
159
  y: 50,
105
160
  radius: 20,
106
161
  fillStyle: '#ff0000',
107
- tags: ['falling']
108
162
  });
109
163
 
110
- // Rectangle (static, doesn't move)
164
+ // Rectangle (static obstacle — won't move)
111
165
  scene.spawnObject({
112
166
  x: 200,
113
167
  y: 300,
114
168
  width: 100,
115
169
  height: 20,
116
- fillStyle: '#0000ff'
170
+ fillStyle: '#0000ff',
171
+ tags: ['static']
117
172
  });
118
173
 
119
174
  // Polygon shapes
@@ -122,7 +177,6 @@ scene.spawnObject({
122
177
  y: 100,
123
178
  radius: 25,
124
179
  fillStyle: '#00ff00',
125
- tags: ['falling'],
126
180
  shape: { type: 'hexagon' }
127
181
  });
128
182
  ```
@@ -137,7 +191,7 @@ const id = await scene.spawnObjectAsync({
137
191
  y: 100,
138
192
  imageUrl: '/images/coin.png',
139
193
  size: 50,
140
- tags: ['falling', 'grabable']
194
+ tags: ['grabable'] // dynamic by default
141
195
  });
142
196
  ```
143
197
 
@@ -242,14 +296,16 @@ const result = await scene.addTTFTextObstacles({
242
296
  });
243
297
  ```
244
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
+
245
301
  ### Managing Text Obstacles
246
302
 
247
303
  ```typescript
248
- // Spawn text that immediately falls (already has 'falling' tag)
304
+ // Spawn text that immediately falls (isStatic: false)
249
305
  const result = await scene.spawnFallingTextObstacles(config);
250
306
  const result = await scene.spawnFallingTTFTextObstacles(config);
251
307
 
252
- // Release text obstacles (add 'falling' tag)
308
+ // Release static text obstacles (removes 'static' tag so they fall)
253
309
  scene.releaseTextObstacles(wordTag);
254
310
 
255
311
  // Release letters one by one with delay
@@ -280,7 +336,7 @@ scene.setEffect({
280
336
  objectConfig: {
281
337
  radius: 15,
282
338
  fillStyle: '#4a90d9',
283
- tags: ['falling']
339
+ // dynamic by default — no tags needed
284
340
  },
285
341
  probability: 1,
286
342
  minScale: 0.8,
@@ -325,6 +381,26 @@ scene.setEffect({
325
381
  });
326
382
  ```
327
383
 
384
+ ### Managing Effects
385
+
386
+ ```typescript
387
+ // Pause/resume an effect without removing it
388
+ scene.setEffectEnabled('my-rain', false); // pause
389
+ scene.setEffectEnabled('my-rain', true); // resume
390
+
391
+ // Check if an effect is enabled
392
+ scene.isEffectEnabled('my-rain'); // → true/false
393
+
394
+ // Get effect config
395
+ const effect = scene.getEffect('my-rain'); // → EffectConfig | undefined
396
+
397
+ // List all effect IDs
398
+ const ids = scene.getEffectIds(); // → string[]
399
+
400
+ // Remove an effect entirely
401
+ scene.removeEffect('my-rain');
402
+ ```
403
+
328
404
  ## Managing Objects
329
405
 
330
406
  ```typescript
@@ -342,14 +418,24 @@ scene.removeAllObjects();
342
418
  scene.removeAll(); // Alias for removeAllObjects
343
419
  scene.removeAllByTag('tag'); // Alias for removeObjectsByTag
344
420
 
345
- // Add or remove tags
346
- scene.addTag(id, 'falling');
347
- scene.addFallingTag(id); // Convenience for adding 'falling' tag
348
- scene.removeTag(id, 'grabable');
421
+ // Add or remove tags — tags drive behavior, changes take effect immediately
422
+ scene.addTag(id, 'grabable');
423
+ scene.removeTag(id, 'static'); // releases a static object to fall
424
+ scene.addTag(id, 'static'); // freezes a dynamic object in place
425
+ scene.addFallingTag(id); // convenience: removes 'static', adds 'grabable'
426
+ scene.setFollowWindowTarget(id, 'mouse'); // change what a follow_window object walks toward
427
+ scene.setObjectSpeedOverride(id, 2); // double movement speed (negative = run away)
428
+ scene.setObjectSpeedOverride(id, null); // remove speed override
429
+ scene.setObjectMassOverride(id, 50); // heavy object resists follow force
430
+ scene.setObjectMassOverride(id, null); // restore natural mass
431
+ scene.setPosition(id, { x: 400, y: 300 }); // teleport object
432
+ scene.setVelocity(id, { x: 10, y: 0 }); // launch object rightward
433
+ scene.setObjectAngularVelocity(id, Math.PI); // set spin (rad/s)
434
+ scene.setObjectScale(id, 2, 2); // scale x and y independently
349
435
 
350
436
  // Get object info
351
437
  const ids = scene.getObjectIds();
352
- const tagged = scene.getObjectIdsByTag('falling');
438
+ const tagged = scene.getObjectIdsByTag('static');
353
439
  const allTags = scene.getAllTags();
354
440
 
355
441
  // Get full object state
@@ -362,13 +448,25 @@ const objs = scene.getObjectsByTag('tag'); // Returns ObjectState[]
362
448
  ```typescript
363
449
  // Apply force to objects
364
450
  scene.applyForce(id, { x: 0.01, y: -0.02 });
365
- scene.applyForceToTag('falling', { x: 0.005, y: 0 });
451
+ scene.applyForceToTag('grabable', { x: 0.005, y: 0 });
366
452
 
367
- // Set velocity directly
368
- scene.setVelocity(id, { x: 5, y: -10 });
453
+ // Set velocity directly (physical convention: positive y = upward, negative y = downward)
454
+ scene.setVelocity(id, { x: 5, y: 10 }); // moving right and upward
455
+ scene.setVelocity(id, { x: 0, y: -10 }); // moving downward
456
+ scene.setVelocity(id, { x: 0, y: 0 }); // stop all movement
369
457
 
370
- // Set position directly
458
+ // Set position directly (screen pixels, y=0 at top)
371
459
  scene.setPosition(id, { x: 100, y: 200 });
460
+
461
+ // Set angular velocity (spin) in radians/second
462
+ // Positive = counter-clockwise, negative = clockwise
463
+ scene.setObjectAngularVelocity(id, Math.PI); // half-rotation per second CCW
464
+ scene.setObjectAngularVelocity(id, 0); // stop spinning
465
+
466
+ // Scale an object (updates both physics shape and sprite rendering)
467
+ scene.setObjectScale(id, 2, 2); // double size uniformly
468
+ scene.setObjectScale(id, 2, 0.5); // stretch wide, squash tall
469
+ scene.setObjectScale(id, 1, 1); // restore original size
372
470
  ```
373
471
 
374
472
  ## Mouse Position and Grab API
@@ -427,7 +525,7 @@ const currentGrab = scene.getGrabbedObject(); // Returns ID or null
427
525
  ```typescript
428
526
  const scene = new OverlayScene(canvas, {
429
527
  bounds: { top: 0, bottom: 600, left: 0, right: 800 },
430
- gravity: { x: 0, y: 1 },
528
+ gravity: { x: 0, y: -1 },
431
529
  wrapHorizontal: true,
432
530
  debug: false,
433
531
  background: '#16213e',
@@ -508,7 +606,7 @@ Called every frame with all dynamic object states:
508
606
 
509
607
  ```typescript
510
608
  scene.onUpdate((data) => {
511
- // data.objects contains all dynamic (falling) objects
609
+ // data.objects contains all dynamic objects (those without the 'static' tag)
512
610
  for (const obj of data.objects) {
513
611
  console.log(obj.id, obj.x, obj.y, obj.angle, obj.tags);
514
612
  }
@@ -654,7 +752,7 @@ scene.destroy(); // Clean up resources
654
752
 
655
753
  ### Per-Object Gravity Override
656
754
 
657
- Individual dynamic objects can have their own gravity vector, independent of the scene gravity. This is done via the `gravityOverride` field in `ObjectConfig`, which automatically adds the `gravity_override` tag to the object.
755
+ 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.
658
756
 
659
757
  ```typescript
660
758
  // Spawn a floaty object that drifts upward
@@ -662,7 +760,7 @@ scene.spawnObject({
662
760
  x: 200, y: 300,
663
761
  radius: 20,
664
762
  fillStyle: '#4a90d9',
665
- tags: ['falling', 'grabable'],
763
+ tags: ['grabable'],
666
764
  gravityOverride: { x: 0, y: -0.3 } // floats upward
667
765
  });
668
766
 
@@ -671,23 +769,93 @@ scene.spawnObject({
671
769
  x: 400, y: 200,
672
770
  radius: 15,
673
771
  fillStyle: '#e94560',
674
- tags: ['falling'],
675
772
  gravityOverride: { x: 0, y: 0 }
676
773
  });
677
774
 
678
775
  // Change or clear a gravity override at runtime
679
776
  scene.setObjectGravityOverride(id, { x: 0.5, y: 0 }); // drift sideways
680
777
  scene.setObjectGravityOverride(id, null); // restore scene gravity
778
+
779
+ // removeTag works too — setObjectGravityOverride(id, null) calls it internally
780
+ scene.removeTag(id, 'gravity_override'); // restores scene gravity
781
+ ```
782
+
783
+ 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.
784
+
785
+ | Tag | Behavior |
786
+ |-----|----------|
787
+ | `static` | Static obstacle, not affected by gravity. Absent by default — objects are dynamic unless tagged static. |
788
+ | `grabable` | Can be grabbed and dragged with the mouse |
789
+ | `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. |
790
+ | `gravity_override` | Uses its own gravity vector instead of scene gravity (value set via `setObjectGravityOverride`) |
791
+ | `speed_override` | Multiplies movement speed for `follow_window` and future movement behaviors (value set via `setObjectSpeedOverride`). Negative = runs away from target. Default multiplier: 1 |
792
+ | `mass_override` | Overrides physics mass (value set via `setObjectMassOverride`). Higher mass resists follow forces; lower mass may allow entity to overcome gravity. |
793
+
794
+ ### Speed Override
795
+
796
+ 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.
797
+
798
+ ```typescript
799
+ // Spawn a fast follower
800
+ scene.spawnObject({
801
+ tags: ['follow_window'],
802
+ speedOverride: 3, // 3× normal speed — automatically adds 'speed_override' tag
803
+ // ...
804
+ });
805
+
806
+ // Spawn a coward that runs away from the mouse
807
+ scene.spawnObject({
808
+ tags: ['follow_window'],
809
+ speedOverride: -2, // flees at 2× speed
810
+ // ...
811
+ });
812
+
813
+ // Change speed at runtime
814
+ scene.setObjectSpeedOverride(id, 5); // very fast follower
815
+ scene.setObjectSpeedOverride(id, -1); // now runs away at normal speed
816
+ scene.setObjectSpeedOverride(id, null); // remove override, restore default speed
817
+ ```
818
+
819
+ ### Mass Override
820
+
821
+ 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.
822
+
823
+ ```typescript
824
+ // Light object — follow force can overcome normal gravity, moves freely in all directions
825
+ scene.spawnObject({
826
+ tags: ['follow_window'],
827
+ massOverride: 1,
828
+ // ...
829
+ });
830
+
831
+ // Very heavy object — barely moves toward target under normal gravity
832
+ scene.spawnObject({
833
+ tags: ['follow_window'],
834
+ massOverride: 100,
835
+ // ...
836
+ });
837
+
838
+ // Change or clear mass at runtime
839
+ scene.setObjectMassOverride(id, 50); // now heavy
840
+ scene.setObjectMassOverride(id, null); // restore natural mass
681
841
  ```
682
842
 
683
- Tags are either boolean (presence = true) or carry a value. `gravity_override` is a value tag — its Vector2 value is set via `gravityOverride` in the config. Boolean tags (`falling`, `grabable`, `follow_window`) need no value.
843
+ ### Scale
844
+
845
+ `setObjectScale(id, x, y)` resizes an object at runtime. Both the physics collision shape and sprite rendering are updated together. Scale is absolute — calling `setObjectScale(id, 2, 2)` always doubles the original size regardless of current scale. Scaling also changes body mass proportionally (area scales by x×y); use `setObjectMassOverride` after if you need a fixed mass.
846
+
847
+ ```typescript
848
+ // Uniform scale
849
+ scene.setObjectScale(id, 2, 2); // double size
850
+ scene.setObjectScale(id, 0.5, 0.5); // half size
851
+
852
+ // Non-uniform scale (independent x and y)
853
+ scene.setObjectScale(id, 3, 1); // stretch wide, keep height
854
+ scene.setObjectScale(id, 1, 0.5); // squash vertically
684
855
 
685
- | Tag | Type | Behavior |
686
- |-----|------|----------|
687
- | `falling` | boolean | Dynamic body affected by gravity |
688
- | `grabable` | boolean | Can be grabbed with mouse |
689
- | `follow_window` | boolean | Walks toward mouse when grounded |
690
- | `gravity_override` | Vector2 | Uses own gravity instead of scene gravity |
856
+ // Restore original size
857
+ scene.setObjectScale(id, 1, 1);
858
+ ```
691
859
 
692
860
  ## Examples
693
861
 
@@ -732,6 +900,11 @@ import type {
732
900
  TTFTextObstacleConfig,
733
901
  TextAlign,
734
902
  TextBounds,
903
+ LetterDebugInfo,
904
+
905
+ // DOM obstacle types
906
+ DOMObstacleConfig,
907
+ DOMObstacleResult,
735
908
 
736
909
  // Effect types
737
910
  EffectConfig,
@@ -774,5 +947,5 @@ import type {
774
947
  } from '@blorkfield/overlay-core';
775
948
 
776
949
  // Tag constants (values, not types)
777
- import { TAGS, TAG_FALLING, TAG_GRABABLE, TAG_FOLLOW_WINDOW, TAG_GRAVITY_OVERRIDE } from '@blorkfield/overlay-core';
950
+ import { TAGS, TAG_STATIC, TAG_GRABABLE, TAG_FOLLOW_WINDOW, TAG_GRAVITY_OVERRIDE, TAG_SPEED_OVERRIDE, TAG_MASS_OVERRIDE } from '@blorkfield/overlay-core';
778
951
  ```