squishjs 1.3.8 → 1.3.9

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.
@@ -0,0 +1,8 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Read(//Users/josephgarcia/homegames/homegames-client/**)",
5
+ "Read(//Users/josephgarcia/homegames/homegames-web/**)"
6
+ ]
7
+ }
8
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "squishjs",
3
- "version": "1.3.8",
3
+ "version": "1.3.9",
4
4
  "description": "squish & unsquish stuff",
5
5
  "scripts": {
6
6
  "test": "node testRunner.js"
package/src/Squisher.js CHANGED
@@ -23,6 +23,13 @@ class Squisher {
23
23
  this.playerFrames = {};
24
24
 
25
25
  this.listeners = new Set();
26
+ // Coalescing: a single tick often mutates many nodes, each firing
27
+ // onStateChange. Instead of re-squishing the whole tree + broadcasting
28
+ // per mutation, we mark dirty and flush once at the end of the event
29
+ // loop turn. Consumers that need the current state synchronously (e.g.
30
+ // sending an initial frame to a player who just joined) call flush().
31
+ this._dirty = false;
32
+ this._flushScheduled = false;
26
33
  this.scale = scale || {x: 1, y: 1};
27
34
  this.state = this.squish(this.game.getLayers());
28
35
 
@@ -281,10 +288,25 @@ class Squisher {
281
288
  }
282
289
 
283
290
  handleStateChange(node, layerName) {
284
- if (this.listeners.size > 0) {
285
- this.state = this.squish(this.game.getLayers());
286
- this.broadcast();
287
- }
291
+ if (this.listeners.size === 0) return;
292
+ this._dirty = true;
293
+ if (this._flushScheduled) return;
294
+ this._flushScheduled = true;
295
+ const schedule = (typeof setImmediate === 'function')
296
+ ? setImmediate
297
+ : (fn) => setTimeout(fn, 0);
298
+ schedule(() => this.flush());
299
+ }
300
+
301
+ // Squish + broadcast any pending state changes synchronously. Safe to call
302
+ // when nothing is pending (no-op). Coalesces a burst of mutations into a
303
+ // single squish + single broadcast carrying the latest state.
304
+ flush() {
305
+ this._flushScheduled = false;
306
+ if (!this._dirty) return;
307
+ this._dirty = false;
308
+ this.state = this.squish(this.game.getLayers());
309
+ this.broadcast();
288
310
  }
289
311
 
290
312
  broadcast() {
@@ -6,7 +6,7 @@ const squishAsset = {
6
6
  type: ASSET_SUBTYPE,
7
7
  squish: (a, scale) => {
8
8
  const assetKey = Object.keys(a)[0];
9
- const squishedAssets = new Array(10 + assetKey.length);
9
+ const squishedAssets = new Array(18 + assetKey.length);
10
10
 
11
11
  const asset = a[assetKey];
12
12
 
@@ -18,6 +18,15 @@ const squishAsset = {
18
18
 
19
19
  const startTimeSecond = asset.startTime || 0;
20
20
 
21
+ // Crop values are percentages (0-100) of the source image, expressed as
22
+ // the amount removed from each edge. They sample a sub-region of the
23
+ // source and are independent of layer scale (which affects pos/size, i.e.
24
+ // where the cropped region lands on screen). Default to 0 (no crop).
25
+ const cropLeft = asset.cropLeft || 0;
26
+ const cropTop = asset.cropTop || 0;
27
+ const cropRight = asset.cropRight || 0;
28
+ const cropBottom = asset.cropBottom || 0;
29
+
21
30
  squishedAssets[0] = Math.floor(posX);
22
31
  squishedAssets[1] = getFractional(posX);
23
32
 
@@ -33,8 +42,20 @@ const squishAsset = {
33
42
  squishedAssets[8] = Math.floor(startTimeSecond);
34
43
  squishedAssets[9] = getFractional(startTimeSecond);
35
44
 
45
+ squishedAssets[10] = Math.floor(cropLeft);
46
+ squishedAssets[11] = getFractional(cropLeft);
47
+
48
+ squishedAssets[12] = Math.floor(cropTop);
49
+ squishedAssets[13] = getFractional(cropTop);
50
+
51
+ squishedAssets[14] = Math.floor(cropRight);
52
+ squishedAssets[15] = getFractional(cropRight);
53
+
54
+ squishedAssets[16] = Math.floor(cropBottom);
55
+ squishedAssets[17] = getFractional(cropBottom);
56
+
36
57
  for (let i = 0; i < assetKey.length; i++) {
37
- squishedAssets[10 + i] = assetKey.codePointAt(i);
58
+ squishedAssets[18 + i] = assetKey.codePointAt(i);
38
59
  }
39
60
 
40
61
  return squishedAssets;
@@ -48,7 +69,12 @@ const squishAsset = {
48
69
 
49
70
  const startTime = squished[8] + squished[9] / 100;
50
71
 
51
- const assetKey = String.fromCodePoint.apply(null, squished.slice(10));
72
+ const cropLeft = squished[10] + squished[11] / 100;
73
+ const cropTop = squished[12] + squished[13] / 100;
74
+ const cropRight = squished[14] + squished[15] / 100;
75
+ const cropBottom = squished[16] + squished[17] / 100;
76
+
77
+ const assetKey = String.fromCodePoint.apply(null, squished.slice(18));
52
78
 
53
79
  return {
54
80
  [assetKey]: {
@@ -60,7 +86,11 @@ const squishAsset = {
60
86
  x: assetSizeX,
61
87
  y: assetSizeY
62
88
  },
63
- startTime
89
+ startTime,
90
+ cropLeft,
91
+ cropTop,
92
+ cropRight,
93
+ cropBottom
64
94
  }
65
95
  }
66
96
  }
@@ -69,4 +99,4 @@ const squishAsset = {
69
99
  module.exports = {
70
100
  ASSET_SUBTYPE,
71
101
  squishAsset
72
- };
102
+ };
@@ -56,6 +56,8 @@ test("test layer with child added", () => {
56
56
 
57
57
  // adding a child should trigger an update in the squisher and result in a new state
58
58
  layer.addChild(child);
59
+ // state changes are coalesced; flush to materialize them synchronously
60
+ squisher.flush();
59
61
 
60
62
  const expectedLayerValue = [squish(layer), squish(child)].flat();
61
63
  const stateWithChild = Array.from(squisher.state);
@@ -5,48 +5,49 @@ const { unsquish } = require('../src/squish');
5
5
 
6
6
  const { FakeGame, verifyArrayEquality, rectNode, textNode, polygonNode, circleNode, lineNode } = require('./utils');
7
7
 
8
- test("squisher listener", () => {
8
+ test("squisher listener coalesces state changes", () => {
9
9
 
10
10
  const root = lineNode({coords: [[20, 20], [80, 80]], color: COLORS.ORANGE});
11
-
12
- const game = new FakeGame([
11
+
12
+ const game = new FakeGame([
13
13
  {
14
14
  root,
15
15
  scale: {
16
16
  x: 1,
17
17
  y: 1
18
18
  }
19
- }
19
+ }
20
20
  ]);
21
21
 
22
22
  const squisher = new Squisher({ game });
23
23
 
24
24
  let timesInvoked = 0;
25
+ let lastState = null;
25
26
 
26
27
  squisher.addListener((newSquishedState) => {
27
- if (timesInvoked === 0) {
28
- assert(newSquishedState.length == 1);
29
- const updatedRoot = unsquish(newSquishedState[0]);
30
- verifyArrayEquality(updatedRoot.node.color, COLORS.BLACK);
31
- } else if (timesInvoked === 1) {
32
- assert(newSquishedState.length == 1);
33
- const updatedRoot = unsquish(newSquishedState[0]);
34
- verifyArrayEquality(updatedRoot.node.color, COLORS.GREEN);
35
- }
36
28
  timesInvoked++;
29
+ lastState = newSquishedState;
37
30
  });
38
31
 
39
32
  assert(timesInvoked === 0);
40
33
 
34
+ // A burst of mutations in the same turn is deferred — nothing broadcast yet.
41
35
  root.node.color = COLORS.BLACK;
42
36
  root.node.onStateChange();
43
-
44
- assert(timesInvoked === 1);
45
-
46
37
  root.node.color = COLORS.GREEN;
47
38
  root.node.onStateChange();
48
39
 
49
- assert(timesInvoked === 2);
40
+ assert(timesInvoked === 0);
41
+
42
+ // flush() coalesces the burst into a single broadcast with the final state.
43
+ squisher.flush();
44
+ assert(timesInvoked === 1);
45
+ assert(lastState.length == 1);
46
+ verifyArrayEquality(unsquish(lastState[0]).node.color, COLORS.GREEN);
47
+
48
+ // flush() with nothing pending is a no-op.
49
+ squisher.flush();
50
+ assert(timesInvoked === 1);
50
51
  });
51
52
 
52
53
 
package/test/main.test.js CHANGED
@@ -435,6 +435,7 @@ test("Simple shape with updates", () => {
435
435
  base.update({
436
436
  fill: COLORS.BLUE
437
437
  });
438
+ squisher.flush();
438
439
 
439
440
  const state2 = Array.from(squisher.state);
440
441
  verifyArrayEquality(unsquish(state2[0]).node.fill, COLORS.BLUE);
@@ -442,6 +443,7 @@ test("Simple shape with updates", () => {
442
443
  base.update({
443
444
  fill: COLORS.GREEN
444
445
  });
446
+ squisher.flush();
445
447
 
446
448
  const state3 = Array.from(squisher.state);
447
449
  verifyArrayEquality(unsquish(state3[0]).node.fill, COLORS.GREEN);