q5play 4.1.2 → 4.2.2

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.
Files changed (5) hide show
  1. package/package.json +1 -1
  2. package/py.typed +0 -0
  3. package/q5play.d.ts +1382 -160
  4. package/q5play.js +847 -204
  5. package/q5play.pyi +5007 -0
package/q5play.js CHANGED
@@ -12,21 +12,19 @@
12
12
  * |__/ |__/ \______/
13
13
  *
14
14
  * @package q5play
15
- * @version 4.1
15
+ * @version 4.2
16
16
  * @author quinton-ashley
17
17
  * @website https://q5play.org
18
18
  */
19
19
 
20
- let q5play_version = '4.1';
20
+ let q5play_version = '4.2';
21
21
 
22
- if (typeof globalThis.Q5 == 'undefined') {
22
+ if (typeof globalThis.Q5 == 'undefined' && typeof globalThis.p5 == 'undefined') {
23
23
  console.error('q5play requires q5.js to be loaded first. Visit https://q5js.org to learn more.');
24
- if (typeof globalThis.p5 != 'undefined') {
25
- console.error('p5.js is not compatible with q5play. https://github.com/processing/p5.js/issues/7737');
26
- }
27
24
  }
28
25
 
29
- let box2dPromise;
26
+ let box2dPromise,
27
+ using_p5 = false;
30
28
 
31
29
  // called when a new instance of Q5 is created
32
30
  async function q5playPreSetup(q) {
@@ -150,6 +148,7 @@ async function q5playPreSetup(q) {
150
148
  b2CreateChain,
151
149
  b2Chain_GetSegmentCount,
152
150
  b2Chain_GetSegments,
151
+ b2DestroyChain,
153
152
 
154
153
  /* Body */
155
154
  b2BodyType,
@@ -249,7 +248,7 @@ async function q5playPreSetup(q) {
249
248
  this.context = 'web';
250
249
 
251
250
  this.update = () => q5playUpdate.call($, q);
252
- this.postdraw = () => q5playPostDraw.call($, q);
251
+ this.draw = () => q5playPostDraw.call($, q);
253
252
 
254
253
  if (window.matchMedia) {
255
254
  this.hasMouse = window.matchMedia('(any-hover: none)').matches ? false : true;
@@ -327,7 +326,7 @@ async function q5playPreSetup(q) {
327
326
 
328
327
  async splashScreen() {
329
328
  if (document.getElementById('made-with-q5play')) return;
330
- if (!using_p5v2) $._incrementPreload();
329
+ if (!using_p5) $._incrementPreload();
331
330
  let d = document.createElement('div');
332
331
  d.id = 'made-with-q5play';
333
332
  d.style =
@@ -359,27 +358,30 @@ async function q5playPreSetup(q) {
359
358
  d.style.display = 'none';
360
359
  d.remove();
361
360
  document.getElementById('made-with-q5play')?.remove();
362
- if (!using_p5v2) $._decrementPreload();
361
+ if (!using_p5) $._decrementPreload();
363
362
  }
364
363
  };
365
364
 
366
365
  $.q5play = new $.Q5Play();
367
366
  delete $.Q5Play;
368
367
 
369
- let using_p5v1 = !$._q5 && p5.VERSION[0] == 1;
370
- let using_p5v2 = !$._q5 && p5.VERSION[0] == 2;
368
+ using_p5 = !$._q5;
369
+ if (using_p5 && p5.VERSION[0] != 2) {
370
+ throw new Error(`q5play requires q5.js or p5.js v2. Detected version: ${p5.VERSION}. Please upgrade.`);
371
+ }
371
372
 
372
- // in q5play the default angle mode is degrees
373
373
  const DEGREES = $.DEGREES,
374
374
  DEGTORAD = Math.PI / 180,
375
375
  RADTODEG = 180 / Math.PI;
376
+
377
+ // in q5play the default angle mode is degrees
376
378
  $.angleMode(DEGREES);
377
379
 
378
380
  // in q5play the default color mode is float RGB
379
- $.colorMode($.RGB, 1);
381
+ if (!using_p5) $.colorMode($.RGB, 1);
380
382
 
381
383
  // in q5play the default image mode is center
382
- $.imageMode($.CENTER);
384
+ if (!using_p5) $.imageMode($.CENTER);
383
385
 
384
386
  const ZERO_VEC = new b2Vec2(0, 0),
385
387
  ZERO_ROT = b2MakeRot(0),
@@ -396,7 +398,9 @@ async function q5playPreSetup(q) {
396
398
  const scaleFrom = (x, y) => ({ x: x * meterSize, y: y * meterSize });
397
399
 
398
400
  const linearSlop = 0.005,
399
- angularSlop = 0.000582;
401
+ angularSlop = 0.0333;
402
+
403
+ let visualSlop = 0.5;
400
404
 
401
405
  const isSlop = (val) => Math.abs(val) <= linearSlop;
402
406
  const fixRound = (val) => {
@@ -490,13 +494,13 @@ async function q5playPreSetup(q) {
490
494
  geom = this.geom;
491
495
 
492
496
  if (type == 0) {
493
- let hw = geom._hw * scaleX,
494
- hh = geom._hh * scaleY,
497
+ let hw = geom._hw * Math.abs(scaleX),
498
+ hh = geom._hh * Math.abs(scaleY),
495
499
  rr;
496
500
 
497
501
  if (!geom._rr) geom = b2MakeBox(hw, hh);
498
502
  else {
499
- rr = geom._rr * Math.min(scaleX, scaleY);
503
+ rr = geom._rr * Math.min(Math.abs(scaleX), Math.abs(scaleY));
500
504
  geom = b2MakeRoundedBox(hw, hh, rr);
501
505
  }
502
506
  b2Shape_SetPolygon(id, geom);
@@ -504,11 +508,44 @@ async function q5playPreSetup(q) {
504
508
  geom._hh = hh;
505
509
  geom._rr = rr;
506
510
  this.geom = geom;
511
+ } else if (type == 1) {
512
+ // convex polygon
513
+ geom = b2Shape_GetPolygon(id);
514
+ let verts = [];
515
+ for (let i = 0; i < geom.count; i++) {
516
+ const v = geom.GetVertex(i);
517
+ verts.push({ x: v.x * scaleX, y: v.y * scaleY });
518
+ }
519
+ let hull = b2ComputeHull(verts);
520
+ let rr = geom.radius * Math.min(scaleX, scaleY);
521
+ geom = rr ? b2MakeOffsetRoundedPolygon(hull, ZERO_VEC, ZERO_ROT, rr) : b2MakePolygon(hull, 0);
522
+ b2Shape_SetPolygon(id, geom);
523
+ this.geom = geom;
507
524
  } else if (type == 3) {
508
- geom.radius *= scaleX;
525
+ geom.radius *= Math.abs(scaleX);
509
526
  b2Shape_SetCircle(id, geom);
510
527
  this.geom = geom;
528
+ } else if (type == 4 || type == 7) {
529
+ // capsule (single or capsule chain segment)
530
+ geom = b2Shape_GetCapsule(id);
531
+ geom.center1.x *= scaleX;
532
+ geom.center1.y *= scaleY;
533
+ geom.center2.x *= scaleX;
534
+ geom.center2.y *= scaleY;
535
+ geom.radius *= Math.min(scaleX, scaleY);
536
+ b2Shape_SetCapsule(id, geom);
537
+ this.geom = geom;
538
+ } else if (type == 5) {
539
+ // segment
540
+ geom = b2Shape_GetSegment(id);
541
+ geom.point1.x *= scaleX;
542
+ geom.point1.y *= scaleY;
543
+ geom.point2.x *= scaleX;
544
+ geom.point2.y *= scaleY;
545
+ b2Shape_SetSegment(id, geom);
546
+ this.geom = geom;
511
547
  }
548
+ // type 6 (chain) - handled by sprite's scaleBy, which rescales and sets all segments
512
549
  }
513
550
 
514
551
  _enableContactEvents(val = true) {
@@ -835,7 +872,13 @@ async function q5playPreSetup(q) {
835
872
  if (flipY) ani.scale.y = -ani.scale.y;
836
873
 
837
874
  if (start < 0) start = ani.length + start;
838
- if (start !== undefined) ani._frame = start;
875
+ if (start !== undefined) {
876
+ ani._frame = start;
877
+ } else {
878
+ // reset so recycled animations don't immediately resolve
879
+ ani._frame = 0;
880
+ ani.playing = true;
881
+ }
839
882
 
840
883
  if (end !== undefined) ani.goToFrame(end);
841
884
  else if (ani._frame == ani.lastFrame) resolve();
@@ -952,7 +995,7 @@ async function q5playPreSetup(q) {
952
995
 
953
996
  this._posX = x;
954
997
  this._posY = y;
955
- this._pos = $.createVector.call($);
998
+ this._pos = $.createVector.call($, 0, 0);
956
999
 
957
1000
  let _this = this;
958
1001
  Object.defineProperties(this._pos, {
@@ -974,7 +1017,7 @@ async function q5playPreSetup(q) {
974
1017
  }
975
1018
  });
976
1019
 
977
- this._canvasPos = $.createVector.call($);
1020
+ this._canvasPos = $.createVector.call($, 0, 0);
978
1021
 
979
1022
  Object.defineProperties(this._canvasPos, {
980
1023
  x: {
@@ -994,16 +1037,16 @@ async function q5playPreSetup(q) {
994
1037
  });
995
1038
 
996
1039
  this._direction = 0;
997
- this._velX = 0;
998
- this._velY = 0;
1040
+ this.vx = 0;
1041
+ this.vy = 0;
999
1042
  this._velSynced = true;
1000
- this._vel = $.createVector.call($);
1043
+ this._vel = $.createVector.call($, 0, 0);
1001
1044
  this._vel._useCache = true;
1002
1045
 
1003
1046
  this._syncVel = () => {
1004
1047
  let v = b2Body_GetLinearVelocity(this.bdID);
1005
- this._velX = v.x;
1006
- this._velY = v.y;
1048
+ this.vx = v.x;
1049
+ this.vy = v.y;
1007
1050
  this._velSynced = true;
1008
1051
  };
1009
1052
 
@@ -1013,13 +1056,13 @@ async function q5playPreSetup(q) {
1013
1056
  if (!_this._velSynced && _this._physicsEnabled) {
1014
1057
  _this._syncVel();
1015
1058
  }
1016
- return _this._velX;
1059
+ return _this.vx;
1017
1060
  },
1018
1061
  set(val) {
1019
1062
  if (_this._physicsEnabled) {
1020
1063
  b2Body_SetLinearVelocity(_this.bdID, new b2Vec2(val, this.y));
1021
1064
  }
1022
- _this._velX = val;
1065
+ _this.vx = val;
1023
1066
  this._magCached = this._directionCached = false;
1024
1067
  }
1025
1068
  },
@@ -1028,13 +1071,13 @@ async function q5playPreSetup(q) {
1028
1071
  if (!_this._velSynced && _this._physicsEnabled) {
1029
1072
  _this._syncVel();
1030
1073
  }
1031
- return _this._velY;
1074
+ return _this.vy;
1032
1075
  },
1033
1076
  set(val) {
1034
1077
  if (_this._physicsEnabled) {
1035
1078
  b2Body_SetLinearVelocity(_this.bdID, new b2Vec2(this.x, val));
1036
1079
  }
1037
- _this._velY = val;
1080
+ _this.vy = val;
1038
1081
  this._magCached = this._directionCached = false;
1039
1082
  }
1040
1083
  }
@@ -1153,8 +1196,7 @@ async function q5playPreSetup(q) {
1153
1196
  set(val) {
1154
1197
  if (!val || val == this._x) return;
1155
1198
  if (_this.watch) _this.mod[26] = true;
1156
- let scaleX = Math.abs(val / this._x);
1157
- _this.scaleBy(scaleX, 1);
1199
+ _this.scaleBy(val / this._x, 1);
1158
1200
  this._x = val;
1159
1201
  this._avg = (this._x + this._y) * 0.5;
1160
1202
  _this._shouldScale = this._avg != 1;
@@ -1168,8 +1210,7 @@ async function q5playPreSetup(q) {
1168
1210
  set(val) {
1169
1211
  if (!val || val == this._y) return;
1170
1212
  if (_this.watch) _this.mod[26] = true;
1171
- let scaleY = Math.abs(val / this._y);
1172
- _this.scaleBy(1, scaleY);
1213
+ _this.scaleBy(1, val / this._y);
1173
1214
  this._y = val;
1174
1215
  this._avg = (this._x + this._y) * 0.5;
1175
1216
  _this._shouldScale = this._avg != 1;
@@ -1199,10 +1240,10 @@ async function q5playPreSetup(q) {
1199
1240
  else this.addCollider(...args);
1200
1241
  }
1201
1242
 
1202
- this.prevPos = { x, y };
1243
+ this.prevX = x;
1244
+ this.prevY = y;
1203
1245
  this.prevRotation = 0;
1204
- this._dest = { x, y };
1205
- this._destIdx = 0;
1246
+
1206
1247
  this._debug = false;
1207
1248
 
1208
1249
  if (!group._isAllSpritesGroup) $.allSprites.push(this);
@@ -1512,10 +1553,10 @@ async function q5playPreSetup(q) {
1512
1553
  geom.center2 = vecs[i];
1513
1554
  geom.radius = rr ? rr / meterSize : 0.02;
1514
1555
  id = b2CreateCapsuleShape(bdID, shape.def, geom);
1515
- let shapePart = new Collider(this);
1516
- shape._init(id, 7, geom);
1517
- shapes.push(shapePart);
1518
- shapeDict[id.index1] = shapePart;
1556
+ let sh = new Collider(this);
1557
+ sh._init(id, 7, geom);
1558
+ shapes.push(sh);
1559
+ shapeDict[id.index1] = sh;
1519
1560
  }
1520
1561
  shape = null;
1521
1562
  this.isSuperFast = true;
@@ -1533,12 +1574,17 @@ async function q5playPreSetup(q) {
1533
1574
 
1534
1575
  id = b2CreateChain(bdID, shape.def);
1535
1576
  shape._init(id, 6);
1577
+ shape.friction = 0.5;
1578
+ shape._points = vecs.map((v) => ({ x: v.x, y: v.y }));
1579
+ shape._isLoopChain = vecs.isLoop;
1580
+ this._chain = shape;
1536
1581
 
1537
1582
  let count = b2Chain_GetSegmentCount(id);
1538
1583
  let segments = b2Chain_GetSegments(id, count);
1539
1584
  for (let segID of segments) {
1540
1585
  let sh = new Collider(this);
1541
1586
  sh._init(segID, 6);
1587
+ sh.friction = 0.5;
1542
1588
  shapes.push(sh);
1543
1589
  shapeDict[segID.index1] = sh;
1544
1590
  }
@@ -1891,7 +1937,7 @@ async function q5playPreSetup(q) {
1891
1937
  }
1892
1938
  const speed = this._vel.mag();
1893
1939
  if (speed) {
1894
- this._setVel($.cos(val) * speed, $.sin(val) * speed);
1940
+ this.__setVel($.cos(val) * speed, $.sin(val) * speed);
1895
1941
  this._vel._magCached = false;
1896
1942
  }
1897
1943
  this._vel._direction = val;
@@ -1999,19 +2045,17 @@ async function q5playPreSetup(q) {
1999
2045
  this._opacity = val;
2000
2046
  }
2001
2047
 
2002
- get previousPosition() {
2003
- return this.prevPos;
2048
+ get previousX() {
2049
+ return this.prevX;
2004
2050
  }
2005
- set previousPosition(val) {
2006
- this.prevPos = val;
2051
+
2052
+ get previousY() {
2053
+ return this.prevY;
2007
2054
  }
2008
2055
 
2009
2056
  get previousRotation() {
2010
2057
  return this.prevRotation;
2011
2058
  }
2012
- set previousRotation(val) {
2013
- this.prevRotation = val;
2014
- }
2015
2059
 
2016
2060
  get pixelPerfect() {
2017
2061
  return this._pixelPerfect;
@@ -2035,8 +2079,9 @@ async function q5playPreSetup(q) {
2035
2079
  get rotation() {
2036
2080
  if (!this._physicsEnabled || !usePhysics) return this._rotation || 0;
2037
2081
  let val = b2Body_GetRotation(this.bdID).GetAngle();
2082
+ if ($._angleMode == DEGREES) val = $.degrees(val);
2038
2083
  if (friendlyRounding) val = fixRoundAngular(val);
2039
- return (this._rotation = $._angleMode == DEGREES ? $.degrees(val) : val);
2084
+ return (this._rotation = val);
2040
2085
  }
2041
2086
  set rotation(val) {
2042
2087
  this._rotation = val;
@@ -2089,17 +2134,85 @@ async function q5playPreSetup(q) {
2089
2134
  scaleBy(x, y) {
2090
2135
  if (y === undefined) y = x;
2091
2136
 
2137
+ const ax = Math.abs(x),
2138
+ ay = Math.abs(y);
2139
+
2092
2140
  if (this._shapes) {
2093
2141
  for (let shape of this._shapes) {
2094
2142
  shape.scaleBy(x, y);
2095
2143
  }
2096
2144
  }
2097
2145
 
2098
- this._w *= x;
2099
- this._hw *= x;
2146
+ if (this._hasChain) this._rebuildChain(x, y);
2147
+
2148
+ this._w *= ax;
2149
+ this._hw *= ax;
2100
2150
  if (this._h) {
2101
- this._h *= y;
2102
- this._hh *= y;
2151
+ this._h *= ay;
2152
+ this._hh *= ay;
2153
+ }
2154
+ }
2155
+
2156
+ _rebuildChain(scaleX, scaleY) {
2157
+ const chain = this._chain;
2158
+ const shapes = this._shapes;
2159
+
2160
+ // save properties from existing segments before removing them
2161
+ const firstCollider = this.colliders[0];
2162
+ const savedFriction = firstCollider?._friction ?? 0.5;
2163
+ const savedSurfaceSpeed = firstCollider?._tangentSpeed ?? 0;
2164
+ const savedBounciness = firstCollider?._restitution ?? 0.2;
2165
+ // negate surfaceSpeed when the chain is flipped (odd number of axes negated)
2166
+ const newSurfaceSpeed = scaleX * scaleY < 0 ? -savedSurfaceSpeed : savedSurfaceSpeed;
2167
+
2168
+ // scale the stored points (already in Box2D meter coordinates)
2169
+ for (let p of chain._points) {
2170
+ p.x *= scaleX;
2171
+ p.y *= scaleY;
2172
+ }
2173
+
2174
+ // when an odd number of axes are negated, the winding order reverses,
2175
+ // flipping the collision normal; reverse the point order to compensate
2176
+ if (scaleX * scaleY < 0) chain._points.reverse();
2177
+
2178
+ // clean up old chain segment JS references
2179
+ for (let i = shapes.length - 1; i >= 0; i--) {
2180
+ if (shapes[i].type === 6) {
2181
+ delete shapeDict[shapes[i].id.index1];
2182
+ this.colliders.splice(this.colliders.indexOf(shapes[i]), 1);
2183
+ shapes.splice(i, 1);
2184
+ }
2185
+ }
2186
+
2187
+ // destroy old chain (Box2D frees all segment physics objects)
2188
+ b2DestroyChain(chain.id);
2189
+
2190
+ // recompute packed material data
2191
+ const packedData = ((chain._isFirstShape ? 1 : 0) << 26) | this._uid;
2192
+
2193
+ // create new chain with scaled points
2194
+ const chainDef = new b2DefaultChainDef();
2195
+ chainDef.SetPoints([chain._points[0], ...chain._points, chain._points.at(-1)]);
2196
+ chainDef.isLoop = chain._isLoopChain;
2197
+ chainDef.SetMaterials([{ customColor: packedData }]);
2198
+
2199
+ const newId = b2CreateChain(this.bdID, chainDef);
2200
+ chain._init(newId, 6);
2201
+ chain.friction = savedFriction;
2202
+ chain._tangentSpeed = newSurfaceSpeed;
2203
+
2204
+ // register new chain segments with restored properties
2205
+ const count = b2Chain_GetSegmentCount(newId);
2206
+ const segments = b2Chain_GetSegments(newId, count);
2207
+ for (let segID of segments) {
2208
+ let sh = new Collider(this);
2209
+ sh._init(segID, 6);
2210
+ sh.friction = savedFriction;
2211
+ sh.bounciness = savedBounciness;
2212
+ if (newSurfaceSpeed) sh.surfaceSpeed = newSurfaceSpeed;
2213
+ shapes.push(sh);
2214
+ this.colliders.push(sh);
2215
+ shapeDict[segID.index1] = sh;
2103
2216
  }
2104
2217
  }
2105
2218
 
@@ -2123,10 +2236,7 @@ async function q5playPreSetup(q) {
2123
2236
 
2124
2237
  if (this.watch) this.mod[26] = true;
2125
2238
 
2126
- let scaleX = Math.abs(x / sc._x);
2127
- let scaleY = Math.abs(y / sc._y);
2128
-
2129
- this.scaleBy(scaleX, scaleY);
2239
+ this.scaleBy(x / sc._x, y / sc._y);
2130
2240
 
2131
2241
  sc._x = x;
2132
2242
  sc._y = y;
@@ -2153,14 +2263,14 @@ async function q5playPreSetup(q) {
2153
2263
  return this._vel.mag();
2154
2264
  }
2155
2265
  set speed(val) {
2156
- if (!val) this._setVel(0, 0);
2266
+ if (!val) this.__setVel(0, 0);
2157
2267
  else {
2158
2268
  const mag = this._vel.mag();
2159
2269
  if (mag > 0) {
2160
- this._setVel((this._vel.x / mag) * val, (this._vel.y / mag) * val);
2270
+ this.__setVel((this._vel.x / mag) * val, (this._vel.y / mag) * val);
2161
2271
  } else {
2162
2272
  const dir = this._vel.direction();
2163
- this._setVel($.cos(dir) * val, $.sin(dir) * val);
2273
+ this.__setVel($.cos(dir) * val, $.sin(dir) * val);
2164
2274
  }
2165
2275
  }
2166
2276
  this._vel._mag = val;
@@ -2168,7 +2278,7 @@ async function q5playPreSetup(q) {
2168
2278
  }
2169
2279
 
2170
2280
  setSpeedAndDirection(speed, direction) {
2171
- this._setVel($.cos(direction) * speed, $.sin(direction) * speed);
2281
+ this.__setVel($.cos(direction) * speed, $.sin(direction) * speed);
2172
2282
  this._vel._mag = speed;
2173
2283
  this._vel._direction = direction;
2174
2284
  this._vel._magCached = this._vel._directionCached = true;
@@ -2180,6 +2290,10 @@ async function q5playPreSetup(q) {
2180
2290
  }
2181
2291
  set surfaceSpeed(val) {
2182
2292
  if (this.watch) this.mod[21] = true;
2293
+ if (this._hasCapsuleChain) {
2294
+ return console.error('Can not set surfaceSpeed of a capsule chain.');
2295
+ }
2296
+ if (this._hasChain) this._chain.surfaceSpeed = val;
2183
2297
  for (let collider of this.colliders) {
2184
2298
  collider.surfaceSpeed = val;
2185
2299
  }
@@ -2230,7 +2344,7 @@ async function q5playPreSetup(q) {
2230
2344
  }
2231
2345
 
2232
2346
  get pos() {
2233
- return this._pos;
2347
+ return { x: this._posX, y: this._posY };
2234
2348
  }
2235
2349
  set pos(val) {
2236
2350
  if (val == $.mouse && !$.mouse.isActive) return;
@@ -2409,23 +2523,32 @@ async function q5playPreSetup(q) {
2409
2523
  }
2410
2524
  set vel(val) {
2411
2525
  this._setVel(val[0] ?? val.x, val[1] ?? val.y);
2412
- this._vel._magCached = this._vel._directionCached = false;
2526
+ }
2527
+
2528
+ get velocity() {
2529
+ return this._vel;
2530
+ }
2531
+ set velocity(val) {
2532
+ this.vel = val;
2413
2533
  }
2414
2534
 
2415
2535
  _setVel(x, y) {
2416
2536
  if (this._physicsEnabled) {
2417
2537
  b2Body_SetLinearVelocity(this.bdID, new b2Vec2(x, y));
2418
2538
  }
2419
- this._velX = x;
2420
- this._velY = y;
2539
+ this.vx = x;
2540
+ this.vy = y;
2421
2541
  this._velSynced = true;
2542
+ this._vel._magCached = this._vel._directionCached = false;
2422
2543
  }
2423
2544
 
2424
- get velocity() {
2425
- return this._vel;
2426
- }
2427
- set velocity(val) {
2428
- this.vel = val;
2545
+ __setVel(x, y) {
2546
+ if (this._physicsEnabled) {
2547
+ b2Body_SetLinearVelocity(this.bdID, new b2Vec2(x, y));
2548
+ }
2549
+ this.vx = x;
2550
+ this.vy = y;
2551
+ this._velSynced = true;
2429
2552
  }
2430
2553
 
2431
2554
  get grabbable() {
@@ -2447,6 +2570,9 @@ async function q5playPreSetup(q) {
2447
2570
 
2448
2571
  _update() {
2449
2572
  if (this._customUpdate) this._customUpdate();
2573
+ this.prevX = this._posX;
2574
+ this.prevY = this._posY;
2575
+ this.prevRotation = this.rotation;
2450
2576
  if (this.autoUpdate) this.autoUpdate = null;
2451
2577
  }
2452
2578
 
@@ -2455,8 +2581,8 @@ async function q5playPreSetup(q) {
2455
2581
  if (this._life <= 0) {
2456
2582
  this.delete();
2457
2583
  } else if (!this._physicsEnabled || !usePhysics) {
2458
- this._posX += this._velX * timeScale;
2459
- this._posY += this._velY * timeScale;
2584
+ this._posX += this.vx * timeScale;
2585
+ this._posY += this.vy * timeScale;
2460
2586
  this._rotation += this._rotationSpeed * timeScale;
2461
2587
  }
2462
2588
 
@@ -2468,6 +2594,70 @@ async function q5playPreSetup(q) {
2468
2594
  }
2469
2595
  }
2470
2596
 
2597
+ if (this._destArrivalTime !== undefined && $.world.physicsTime >= this._destArrivalTime) {
2598
+ const x = this._posX,
2599
+ y = this._posY,
2600
+ prevX = this.prevX,
2601
+ prevY = this.prevY,
2602
+ destX = this._destX,
2603
+ destY = this._destY;
2604
+
2605
+ const destReachedX =
2606
+ destX === undefined || (destX >= Math.min(prevX, x) - visualSlop && destX <= Math.max(prevX, x) + visualSlop);
2607
+ const destReachedY =
2608
+ destY === undefined || (destY >= Math.min(prevY, y) - visualSlop && destY <= Math.max(prevY, y) + visualSlop);
2609
+ const destReached = destReachedX && destReachedY;
2610
+
2611
+ if (destReached) {
2612
+ this._setVel(0, 0);
2613
+ this.rotationSpeed = 0;
2614
+ this.pos = [this._destX ?? this._posX, this._destY ?? this._posY];
2615
+ if (this._destRot !== undefined) {
2616
+ this.rotation = this._destRot;
2617
+ }
2618
+ }
2619
+
2620
+ if (this._destResolve) {
2621
+ this._destResolve(destReached);
2622
+ this._destResolve = undefined;
2623
+ }
2624
+
2625
+ this._destX = this._destY = this._destRot = this._destArrivalTime = undefined;
2626
+ }
2627
+
2628
+ if (this._destRotArrivalTime !== undefined && $.world.physicsTime >= this._destRotArrivalTime) {
2629
+ const isDeg = $._angleMode == DEGREES,
2630
+ full = isDeg ? 360 : $.TWO_PI,
2631
+ half = isDeg ? 180 : Math.PI,
2632
+ prevRot = this.prevRotation,
2633
+ currRot = this.rotation,
2634
+ destRot = this._destRot;
2635
+
2636
+ // normalize destRot to [-half, half) to match this.rotation's range
2637
+ let nd = ((destRot % full) + full) % full;
2638
+ if (nd >= half) nd -= full;
2639
+
2640
+ // when the sprite crosses the ±half boundary, prevRot and currRot jump
2641
+ // by ~full°; invert the range check so nd is tested against the arc that
2642
+ // was actually traversed rather than the gap between them
2643
+ const crossed180 = Math.abs(prevRot - currRot) > half;
2644
+ const rotReached = crossed180
2645
+ ? nd >= Math.max(prevRot, currRot) - visualSlop || nd <= Math.min(prevRot, currRot) + visualSlop
2646
+ : nd >= Math.min(prevRot, currRot) - visualSlop && nd <= Math.max(prevRot, currRot) + visualSlop;
2647
+
2648
+ if (rotReached) {
2649
+ this.rotationSpeed = 0;
2650
+ this.rotation = nd;
2651
+ }
2652
+
2653
+ if (this._destRotationResolve) {
2654
+ this._destRotationResolve(rotReached);
2655
+ this._destRotationResolve = undefined;
2656
+ }
2657
+
2658
+ this._destRot = this._destRotArrivalTime = undefined;
2659
+ }
2660
+
2471
2661
  if (!this._physicsEnabled && !this._deleted) return;
2472
2662
 
2473
2663
  this.__step();
@@ -2699,8 +2889,9 @@ async function q5playPreSetup(q) {
2699
2889
  args[2] = args[1];
2700
2890
  args[1] = undefined;
2701
2891
  }
2702
- let o = {};
2703
- o.forceVector = new b2Vec2(args[0], args[1]);
2892
+ const v = this._args2Vec(args[0], args[1]),
2893
+ o = {};
2894
+ o.forceVector = new b2Vec2(v.x, v.y);
2704
2895
  if (args[2] !== undefined) {
2705
2896
  o.poa = this._args2Vec(args[2], args[3]);
2706
2897
  o.poa = scaleTo(o.poa.x, o.poa.y);
@@ -2776,30 +2967,70 @@ async function q5playPreSetup(q) {
2776
2967
  wind.delete();
2777
2968
  }
2778
2969
 
2779
- angleTo(x, y) {
2780
- if (typeof x == 'object') {
2781
- y = x.y;
2782
- x = x.x;
2970
+ move(distance, direction, speed) {
2971
+ if (!distance) return;
2972
+
2973
+ if (typeof direction == 'string') {
2974
+ directionNamed = true;
2975
+ this._heading = direction;
2976
+ direction = this._getDirectionAngle(direction);
2783
2977
  }
2784
- return $.atan2(y - this.y, x - this.x);
2978
+ direction ??= this.direction;
2979
+
2980
+ const x = $.cos(direction) * distance + this.x,
2981
+ y = $.sin(direction) * distance + this.y;
2982
+ return this.moveTo(x, y, speed);
2785
2983
  }
2786
2984
 
2787
- rotationToFace(x, y, facing) {
2788
- if (typeof x == 'object') {
2789
- facing = y;
2790
- y = x.y;
2791
- x = x.x;
2985
+ moveTo(x, y, speed) {
2986
+ if (x === undefined && (y === undefined || y === null)) return;
2987
+
2988
+ if (x !== null && x !== undefined && typeof x != 'number') {
2989
+ let pos = x;
2990
+ if (pos == $.mouse && !$.mouse.isActive) return;
2991
+ speed = y;
2992
+ y = pos.y;
2993
+ x = pos.x;
2792
2994
  }
2793
- // if the sprite is too close to the position, don't rotate
2794
- if (Math.abs(x - this.x) < 0.01 && Math.abs(y - this.y) < 0.01) {
2795
- return 0;
2995
+
2996
+ const moveX = x !== null && x !== undefined;
2997
+ const moveY = y !== null && y !== undefined;
2998
+
2999
+ speed ||= this.speed || 1;
3000
+
3001
+ const dist =
3002
+ moveX && moveY
3003
+ ? Math.hypot(x - this._posX, y - this._posY)
3004
+ : moveX
3005
+ ? Math.abs(x - this._posX)
3006
+ : Math.abs(y - this._posY);
3007
+
3008
+ if (this._destResolve) {
3009
+ this._destResolve(false);
3010
+ this._destResolve = undefined;
2796
3011
  }
2797
- return this.angleTo(x, y) + (facing || 0);
2798
- }
2799
3012
 
2800
- angleToFace(x, y, facing) {
2801
- let ang = this.rotationToFace(x, y, facing);
2802
- return minAngleDist(ang, this.rotation);
3013
+ this._destX = moveX ? x : undefined;
3014
+ this._destY = moveY ? y : undefined;
3015
+ this._destArrivalTime = $.world.physicsTime + dist / (speed * $.world._updateRate);
3016
+
3017
+ if (dist > 0) {
3018
+ if (moveX && moveY) {
3019
+ this.setSpeedAndDirection(speed, $.atan2(y - this.y, x - this.x));
3020
+ } else if (moveX) {
3021
+ this._setVel(x > this._posX ? speed : -speed, this.vy);
3022
+ } else {
3023
+ this._setVel(this.vx, y > this._posY ? speed : -speed);
3024
+ }
3025
+ }
3026
+
3027
+ if ($._py) return new Promise((resolve) => (this._destResolve = resolve));
3028
+
3029
+ return {
3030
+ then: (onFulfilled) => {
3031
+ this._destResolve = onFulfilled;
3032
+ }
3033
+ };
2803
3034
  }
2804
3035
 
2805
3036
  moveTowards(x, y, tracking) {
@@ -2816,21 +3047,120 @@ async function q5playPreSetup(q) {
2816
3047
 
2817
3048
  let velX, velY;
2818
3049
 
2819
- if (x !== null) {
3050
+ if (x !== null && x !== undefined) {
2820
3051
  let diffX = x - this.x;
2821
3052
  if (!isSlop(diffX)) {
2822
3053
  velX = diffX * tracking;
2823
3054
  } else velX = 0;
2824
- } else velX = this._velX;
2825
- if (y !== null) {
3055
+ } else velX = this.vx;
3056
+ if (y !== null && y !== undefined) {
2826
3057
  let diffY = y - this.y;
2827
3058
  if (!isSlop(diffY)) {
2828
3059
  velY = diffY * tracking;
2829
3060
  } else velY = 0;
2830
- } else velY = this._velY;
3061
+ } else velY = this.vy;
2831
3062
 
2832
3063
  this._setVel(velX, velY);
2833
- this._vel._magCached = this._vel._directionCached = false;
3064
+ }
3065
+
3066
+ angleTo(x, y, facing = 0) {
3067
+ if (typeof x == 'object') {
3068
+ facing = y || 0;
3069
+ y = x.y;
3070
+ x = x.x;
3071
+ }
3072
+ // if the sprite is too close to the position, don't rotate
3073
+ if (Math.abs(x - this.x) < 0.01 && Math.abs(y - this.y) < 0.01) {
3074
+ return this.rotation;
3075
+ }
3076
+ return $.atan2(y - this.y, x - this.x) + facing;
3077
+ }
3078
+
3079
+ angleDistTo(x, y, facing = 0) {
3080
+ if (typeof x == 'object') {
3081
+ facing = y || 0;
3082
+ y = x.y;
3083
+ x = x.x;
3084
+ }
3085
+ // if the sprite is too close to the position, don't rotate
3086
+ if (Math.abs(x - this.x) < 0.01 && Math.abs(y - this.y) < 0.01) {
3087
+ return 0;
3088
+ }
3089
+ return minAngleDist($.atan2(y - this.y, x - this.x) + facing, this.rotation);
3090
+ }
3091
+
3092
+ rotateTo(angle, speed) {
3093
+ let args = arguments;
3094
+ let x, y, facing;
3095
+ if (typeof args[0] != 'number') {
3096
+ x = args[0].x;
3097
+ y = args[0].y;
3098
+ speed = args[1];
3099
+ facing = args[2];
3100
+ } else if (arguments.length > 2) {
3101
+ x = args[0];
3102
+ y = args[1];
3103
+ speed = args[2];
3104
+ facing = args[3];
3105
+ }
3106
+
3107
+ if (x !== undefined) angle = this.angleTo(x, y, facing);
3108
+
3109
+ const full = $._angleMode == DEGREES ? 360 : $.TWO_PI;
3110
+ let angleDist = (angle - this.rotation) % full;
3111
+ if (angleDist < 0 && speed > 0) angleDist += full;
3112
+ if (angleDist > 0 && speed < 0) angleDist -= full;
3113
+
3114
+ return this.rotate(angleDist, speed);
3115
+ }
3116
+
3117
+ rotateMinTo(angle, speed, facing) {
3118
+ let args = arguments;
3119
+ let x, y;
3120
+ if (typeof args[0] != 'number') {
3121
+ x = args[0].x;
3122
+ y = args[0].y;
3123
+ speed = args[1];
3124
+ facing = args[2];
3125
+ } else if (args.length > 2) {
3126
+ x = args[0];
3127
+ y = args[1];
3128
+ speed = args[2];
3129
+ facing = args[3];
3130
+ }
3131
+
3132
+ if (x !== undefined) angle = this.angleTo(x, y, facing);
3133
+
3134
+ return this.rotate(minAngleDist(angle, this.rotation), speed);
3135
+ }
3136
+
3137
+ rotate(angleDist, speed) {
3138
+ if (Math.abs(angleDist) <= angularSlop) return;
3139
+
3140
+ speed ||= this.rotationSpeed || 1;
3141
+ speed = Math.abs(speed) * Math.sign(angleDist);
3142
+
3143
+ // cap speed so the sprite doesn't overshoot the destination in one physics step
3144
+ if (Math.abs(speed) > Math.abs(angleDist)) speed = angleDist;
3145
+
3146
+ this._destRotArrivalTime = $.world.physicsTime + Math.abs(angleDist) / (Math.abs(speed) * $.world._updateRate);
3147
+
3148
+ this._destRot = this.rotation + angleDist;
3149
+
3150
+ if (this._destRotationResolve) {
3151
+ this._destRotationResolve(false);
3152
+ this._destRotationResolve = undefined;
3153
+ }
3154
+
3155
+ this.rotationSpeed = speed;
3156
+
3157
+ if ($._py) return new Promise((resolve) => (this._destRotationResolve = resolve));
3158
+
3159
+ return {
3160
+ then: (onFulfilled) => {
3161
+ this._destRotationResolve = onFulfilled;
3162
+ }
3163
+ };
2834
3164
  }
2835
3165
 
2836
3166
  rotateTowards(angle, tracking) {
@@ -2848,18 +3178,31 @@ async function q5playPreSetup(q) {
2848
3178
  facing = args[3];
2849
3179
  }
2850
3180
 
2851
- if (x !== undefined) angle = this.angleToFace(x, y, facing);
3181
+ if (x !== undefined) angle = this.angleDistTo(x, y, facing);
2852
3182
  else angle -= this.rotation;
2853
3183
 
2854
3184
  tracking ??= 0.1;
2855
3185
  this.rotationSpeed = angle * tracking;
2856
3186
  }
2857
3187
 
2858
- _setTargetTransform(x, y, rotation) {
2859
- let t = new b2Transform();
2860
- t.p = scaleTo(x, y);
3188
+ transformTowards(x, y, rotation, tracking = 0.1) {
3189
+ if (x === undefined) return;
3190
+
3191
+ if (typeof x != 'number') {
3192
+ let pos = x;
3193
+ if (pos == $.mouse && !$.mouse.isActive) return;
3194
+ tracking = rotation ?? tracking;
3195
+ rotation = y;
3196
+ y = pos.y;
3197
+ x = pos.x;
3198
+ }
3199
+
3200
+ const t = new b2Transform();
3201
+ t.p = scaleTo(x ?? this._posX, y ?? this._posY);
3202
+ rotation ??= this._rotation;
3203
+ if ($._angleMode == DEGREES) rotation = (rotation % 360) * DEGTORAD;
2861
3204
  t.q = b2MakeRot(rotation);
2862
- b2Body_SetTargetTransform(this.bdID, t, $.world._timeStep);
3205
+ b2Body_SetTargetTransform(this.bdID, t, $.world._timeStep / tracking);
2863
3206
  }
2864
3207
 
2865
3208
  delete() {
@@ -4083,7 +4426,7 @@ async function q5playPreSetup(q) {
4083
4426
  for (let vecProp of vecProps) {
4084
4427
  vecProp = '_' + vecProp;
4085
4428
  if (vecProp != 'vel') this[vecProp] = {};
4086
- else this[vecProp] = $.createVector.call($);
4429
+ else this[vecProp] = $.createVector.call($, 0, 0);
4087
4430
  this[vecProp]._x = undefined;
4088
4431
  this[vecProp]._y = undefined;
4089
4432
  for (let prop of ['x', 'y']) {
@@ -4532,6 +4875,193 @@ async function q5playPreSetup(q) {
4532
4875
  }
4533
4876
  }
4534
4877
 
4878
+ rotateTowards() {
4879
+ for (let s of this) {
4880
+ s.rotateTowards(...arguments);
4881
+ }
4882
+ }
4883
+
4884
+ rotateTo(angle, speed) {
4885
+ const thenables = [];
4886
+ for (let s of this) {
4887
+ thenables.push(s.rotateTo(...arguments));
4888
+ }
4889
+
4890
+ if ($._py) {
4891
+ return new Promise((resolve) => {
4892
+ let pending = thenables.length;
4893
+ if (!pending) return resolve(true);
4894
+ let allReached = true;
4895
+ for (let t of thenables) {
4896
+ t.then((reached) => {
4897
+ if (!reached) allReached = false;
4898
+ if (--pending === 0) resolve(allReached);
4899
+ });
4900
+ }
4901
+ });
4902
+ }
4903
+
4904
+ return {
4905
+ then: (onFulfilled) => {
4906
+ let pending = thenables.length;
4907
+ if (!pending) return onFulfilled(true);
4908
+ let allReached = true;
4909
+ for (let t of thenables) {
4910
+ t.then((reached) => {
4911
+ if (!reached) allReached = false;
4912
+ if (--pending === 0) onFulfilled(allReached);
4913
+ });
4914
+ }
4915
+ }
4916
+ };
4917
+ }
4918
+
4919
+ rotate(angle, speed) {
4920
+ const thenables = [];
4921
+ for (let s of this) {
4922
+ thenables.push(s.rotate(...arguments));
4923
+ }
4924
+
4925
+ if ($._py) {
4926
+ return new Promise((resolve) => {
4927
+ let pending = thenables.length;
4928
+ if (!pending) return resolve(true);
4929
+ let allReached = true;
4930
+ for (let t of thenables) {
4931
+ t.then((reached) => {
4932
+ if (!reached) allReached = false;
4933
+ if (--pending === 0) resolve(allReached);
4934
+ });
4935
+ }
4936
+ });
4937
+ }
4938
+
4939
+ return {
4940
+ then: (onFulfilled) => {
4941
+ let pending = thenables.length;
4942
+ if (!pending) return onFulfilled(true);
4943
+ let allReached = true;
4944
+ for (let t of thenables) {
4945
+ t.then((reached) => {
4946
+ if (!reached) allReached = false;
4947
+ if (--pending === 0) onFulfilled(allReached);
4948
+ });
4949
+ }
4950
+ }
4951
+ };
4952
+ }
4953
+
4954
+ rotateMinTo(angle, speed) {
4955
+ const thenables = [];
4956
+ for (let s of this) {
4957
+ thenables.push(s.rotateMinTo(...arguments));
4958
+ }
4959
+
4960
+ if ($._py) {
4961
+ return new Promise((resolve) => {
4962
+ let pending = thenables.length;
4963
+ if (!pending) return resolve(true);
4964
+ let allReached = true;
4965
+ for (let t of thenables) {
4966
+ t.then((reached) => {
4967
+ if (!reached) allReached = false;
4968
+ if (--pending === 0) resolve(allReached);
4969
+ });
4970
+ }
4971
+ });
4972
+ }
4973
+
4974
+ return {
4975
+ then: (onFulfilled) => {
4976
+ let pending = thenables.length;
4977
+ if (!pending) return onFulfilled(true);
4978
+ let allReached = true;
4979
+ for (let t of thenables) {
4980
+ t.then((reached) => {
4981
+ if (!reached) allReached = false;
4982
+ if (--pending === 0) onFulfilled(allReached);
4983
+ });
4984
+ }
4985
+ }
4986
+ };
4987
+ }
4988
+
4989
+ transformTowards(x, y, rotation, tracking = 0.1) {
4990
+ if (x === undefined) return;
4991
+
4992
+ if (typeof x != 'number') {
4993
+ let pos = x;
4994
+ if (pos == $.mouse && !$.mouse.isActive) return;
4995
+ tracking = rotation ?? tracking;
4996
+ rotation = y;
4997
+ y = pos.y;
4998
+ x = pos.x;
4999
+ }
5000
+
5001
+ this._resetCentroid();
5002
+
5003
+ for (let s of this) {
5004
+ if (s.distCentroid === undefined) {
5005
+ this._resetDistancesFromCentroid();
5006
+ }
5007
+ s.transformTowards(s.distCentroid.x + x, s.distCentroid.y + y, rotation, tracking);
5008
+ }
5009
+ }
5010
+
5011
+ moveTo(x, y, speed) {
5012
+ if (x === undefined && (y === undefined || y === null)) return;
5013
+
5014
+ let nullX = x === null || x === undefined;
5015
+ let nullY = y === null || y === undefined;
5016
+
5017
+ if (!nullX && typeof x != 'number') {
5018
+ let pos = x;
5019
+ if (pos == $.mouse && !$.mouse.isActive) return;
5020
+ speed = y;
5021
+ y = pos.y;
5022
+ x = pos.x;
5023
+ nullX = nullY = false;
5024
+ }
5025
+
5026
+ this._resetCentroid();
5027
+ this._resetDistancesFromCentroid();
5028
+
5029
+ const thenables = [];
5030
+ for (let s of this) {
5031
+ const tx = nullX ? null : s.distCentroid.x + x;
5032
+ const ty = nullY ? null : s.distCentroid.y + y;
5033
+ thenables.push(s.moveTo(tx, ty, speed));
5034
+ }
5035
+
5036
+ if ($._py) {
5037
+ return new Promise((resolve) => {
5038
+ let pending = thenables.length;
5039
+ if (!pending) return resolve(true);
5040
+ let allReached = true;
5041
+ for (let t of thenables) {
5042
+ t.then((reached) => {
5043
+ if (!reached) allReached = false;
5044
+ if (--pending === 0) resolve(allReached);
5045
+ });
5046
+ }
5047
+ });
5048
+ }
5049
+
5050
+ return {
5051
+ then: (onFulfilled) => {
5052
+ let pending = thenables.length;
5053
+ if (!pending) return onFulfilled(true);
5054
+ let allReached = true;
5055
+ for (let t of thenables) {
5056
+ t.then((reached) => {
5057
+ if (!reached) allReached = false;
5058
+ if (--pending === 0) onFulfilled(allReached);
5059
+ });
5060
+ }
5061
+ }
5062
+ };
5063
+ }
5064
+
4535
5065
  toString() {
4536
5066
  return 'g' + this.idNum;
4537
5067
  }
@@ -5040,6 +5570,7 @@ async function q5playPreSetup(q) {
5040
5570
  }
5041
5571
  set meterSize(val) {
5042
5572
  meterSize = val;
5573
+ visualSlop = meterSize / 120;
5043
5574
  }
5044
5575
 
5045
5576
  get profile() {
@@ -5155,6 +5686,8 @@ async function q5playPreSetup(q) {
5155
5686
 
5156
5687
  this.physicsTime += timeStep;
5157
5688
 
5689
+ this._sync();
5690
+
5158
5691
  let sprites = Object.values($.q5play.sprites);
5159
5692
  let groups = Object.values($.q5play.groups);
5160
5693
 
@@ -5171,6 +5704,71 @@ async function q5playPreSetup(q) {
5171
5704
  if (this.autoStep) this.autoStep = null;
5172
5705
  }
5173
5706
 
5707
+ _sync() {
5708
+ jointStack = [];
5709
+ shapeStack = [];
5710
+
5711
+ b2World_Draw(wID, drawCmds.GetDebugDraw());
5712
+
5713
+ let cmdPtr = drawCmds.GetCommandsData(),
5714
+ cmdSize = drawCmds.GetCommandsSize(),
5715
+ cmdStride = drawCmds.GetCommandStride(),
5716
+ offset = cmdPtr,
5717
+ renderJointForces = $.q5play.renderJointForces,
5718
+ s;
5719
+
5720
+ for (let i = 0; i < cmdSize; i++, offset += cmdStride) {
5721
+ // workaround that unpacks data from
5722
+ // the shape material's customColor
5723
+ const customColor = Box2D.HEAPU32[(offset + 4) >> 2],
5724
+ uid = customColor & 0xffffff,
5725
+ isSensor = (customColor >>> 25) & 0x1,
5726
+ isFirstShape = (customColor >>> 26) & 0x1;
5727
+
5728
+ s = $.q5play.sprites[uid];
5729
+
5730
+ let type = Box2D.HEAPU8[offset];
5731
+
5732
+ if (type == 7) {
5733
+ continue;
5734
+ }
5735
+
5736
+ let vertexCount = Box2D.HEAPU16[(offset + 8) >> 1];
5737
+
5738
+ let dataLen = 4;
5739
+ if (type == 1) dataLen = 5 + vertexCount * 2;
5740
+ else if (type == 3 || type == 4) dataLen = 5;
5741
+ let data = new Float32Array(Box2D.HEAPU8.buffer, offset + 12, dataLen);
5742
+
5743
+ if (!s) {
5744
+ if (type == 0 && renderJointForces) jointStack.push(data);
5745
+ continue;
5746
+ }
5747
+
5748
+ // always keep position in sync since it has a low performance cost
5749
+ // unless the shape is a chain
5750
+ if (type < 4 || (type == 4 && !s._hasCapsuleChain)) {
5751
+ s._posX = data[0] * meterSize;
5752
+ s._posY = data[1] * meterSize;
5753
+ }
5754
+
5755
+ s._velSynced = false;
5756
+ s._vel._magCached = false;
5757
+
5758
+ if (!s.visible) {
5759
+ continue;
5760
+ }
5761
+
5762
+ if (s._hasImagery || s._userDefinedDraw) {
5763
+ s._rotation = Math.atan2(data[2], data[3]) * RADTODEG;
5764
+ }
5765
+
5766
+ if (s.debug || (!s._hasImagery && !s._userDefinedDraw)) {
5767
+ shapeStack.push({ type, sprite: s, isSensor, isFirstShape, data, vertexCount });
5768
+ }
5769
+ }
5770
+ }
5771
+
5174
5772
  get realTime() {
5175
5773
  return $.millis() / 1000;
5176
5774
  }
@@ -5278,7 +5876,7 @@ async function q5playPreSetup(q) {
5278
5876
  $.Camera = class {
5279
5877
  constructor() {
5280
5878
  // camera position
5281
- this._pos = $.createVector.call($);
5879
+ this._pos = $.createVector.call($, 0, 0);
5282
5880
 
5283
5881
  // camera translation
5284
5882
  this.__pos = { x: 0, y: 0, rounded: {} };
@@ -5298,7 +5896,7 @@ async function q5playPreSetup(q) {
5298
5896
  }
5299
5897
 
5300
5898
  get pos() {
5301
- return this._pos;
5899
+ return { x: this._pos.x, y: this._pos.y };
5302
5900
  }
5303
5901
  set pos(val) {
5304
5902
  this.x = val[0] ?? val.x;
@@ -6548,10 +7146,18 @@ async function q5playPreSetup(q) {
6548
7146
  };
6549
7147
 
6550
7148
  $.delay = (milliseconds) => {
6551
- if (!milliseconds) return new Promise(requestAnimationFrame);
7149
+ if (!milliseconds) {
7150
+ return new Promise((resolve) => {
7151
+ requestAnimationFrame(() => {
7152
+ if (!$._removed) resolve();
7153
+ });
7154
+ });
7155
+ }
6552
7156
  // else it wraps setTimeout in a Promise
6553
7157
  return new Promise((resolve) => {
6554
- setTimeout(resolve, milliseconds);
7158
+ setTimeout(() => {
7159
+ if (!$._removed) resolve();
7160
+ }, milliseconds);
6555
7161
  });
6556
7162
  };
6557
7163
 
@@ -6601,6 +7207,8 @@ async function q5playPreSetup(q) {
6601
7207
  $.Canvas = $.createCanvas = function (w, h) {
6602
7208
  let args = [...arguments];
6603
7209
 
7210
+ if (using_p5 && !didCreateCanvas && w == 100 && h == 100) return _createCanvas.call($, ...args);
7211
+
6604
7212
  // prevent p5 v1 overriding the user's canvas with a new default canvas
6605
7213
  if (didCreateCanvas && w == 100 && h == 100) return;
6606
7214
 
@@ -6631,7 +7239,7 @@ async function q5playPreSetup(q) {
6631
7239
  let rend = _createCanvas.call($, ...args);
6632
7240
  $.ctx = $.drawingContext;
6633
7241
  let c = rend.canvas || rend;
6634
- window.canvas = c; // for p5 v2
7242
+ if (using_p5) window.canvas = c;
6635
7243
  if (rend.GL) {
6636
7244
  c.renderer = 'webgl';
6637
7245
  $._webgl = true;
@@ -6698,7 +7306,7 @@ async function q5playPreSetup(q) {
6698
7306
  return rend;
6699
7307
  };
6700
7308
 
6701
- $.canvas = $.canvas;
7309
+ $.canvas = $.canvas; // for brython
6702
7310
 
6703
7311
  const _resizeCanvas = $.resizeCanvas;
6704
7312
 
@@ -6826,7 +7434,7 @@ async function q5playPreSetup(q) {
6826
7434
 
6827
7435
  $.allSprites = new $.Group();
6828
7436
  $.world = new $.World();
6829
- $.camera = new $.Camera();
7437
+ $.camera = $._camera = new $.Camera();
6830
7438
 
6831
7439
  $.InputDevice = class {
6832
7440
  constructor() {
@@ -6901,7 +7509,6 @@ async function q5playPreSetup(q) {
6901
7509
 
6902
7510
  this.x = 0;
6903
7511
  this.y = 0;
6904
- this.canvasPos = {};
6905
7512
  this.isOnCanvas = false;
6906
7513
  this.isActive = false;
6907
7514
  this.left = 0;
@@ -6913,7 +7520,7 @@ async function q5playPreSetup(q) {
6913
7520
  let _this = this;
6914
7521
 
6915
7522
  // this.x and this.y store the actual position values of the mouse
6916
- this._pos = $.createVector.call($);
7523
+ this._pos = $.createVector.call($, 0, 0);
6917
7524
 
6918
7525
  Object.defineProperty(this._pos, 'x', {
6919
7526
  get() {
@@ -6954,10 +7561,20 @@ async function q5playPreSetup(q) {
6954
7561
  }
6955
7562
 
6956
7563
  _update() {
6957
- let cam = $.camera;
6958
- let m = this;
6959
- m.x = $.mouseX / cam.zoom + cam.x;
6960
- m.y = $.mouseY / cam.zoom + cam.y;
7564
+ let cam = $.camera,
7565
+ m = this,
7566
+ mx = $.mouseX,
7567
+ my = $.mouseY;
7568
+
7569
+ if (using_p5) {
7570
+ if ($._webgpuFallback) {
7571
+ mx -= $.halfWidth;
7572
+ my -= $.halfHeight;
7573
+ }
7574
+ }
7575
+
7576
+ m.x = mx / cam.zoom + cam.x;
7577
+ m.y = my / cam.zoom + cam.y;
6961
7578
 
6962
7579
  if (m.scroll < 0) m.scroll = 0;
6963
7580
  if (m.scrollDelta.x == 0 && m.scrollDelta.y == 0) {
@@ -6966,7 +7583,7 @@ async function q5playPreSetup(q) {
6966
7583
  }
6967
7584
 
6968
7585
  get pos() {
6969
- return this._pos;
7586
+ return { x: this.x, y: this.y };
6970
7587
  }
6971
7588
  get position() {
6972
7589
  return this._pos;
@@ -7691,7 +8308,7 @@ async function q5playPreSetup(q) {
7691
8308
  this[indexB] = tmp;
7692
8309
  if (indexA == 0 || indexB == 0) {
7693
8310
  $.contro = this[0];
7694
- if (!$._q5 && $._isGlobal) {
8311
+ if (using_p5 && $._isGlobal) {
7695
8312
  window.contro = this[0];
7696
8313
  }
7697
8314
  }
@@ -7778,7 +8395,7 @@ async function q5playPreSetup(q) {
7778
8395
  fpsPos = 0,
7779
8396
  fpsMin = 60,
7780
8397
  fpsMax = 240;
7781
- let statsColor = $.color('lime');
8398
+ let statsColor = $._q5 ? $.color('lime') : 'lime';
7782
8399
 
7783
8400
  $.renderStats = () => {
7784
8401
  let rs = $.q5play._renderStats;
@@ -7879,78 +8496,23 @@ async function q5playPreSetup(q) {
7879
8496
  jointStack = [],
7880
8497
  shapeStack = [];
7881
8498
 
7882
- $._syncWorld = () => {
7883
- jointStack = [];
7884
- shapeStack = [];
7885
-
7886
- b2World_Draw(wID, drawCmds.GetDebugDraw());
7887
-
7888
- let cmdPtr = drawCmds.GetCommandsData(),
7889
- cmdSize = drawCmds.GetCommandsSize(),
7890
- cmdStride = drawCmds.GetCommandStride(),
7891
- offset = cmdPtr,
7892
- renderJointForces = $.q5play.renderJointForces,
7893
- s;
7894
-
7895
- for (let i = 0; i < cmdSize; i++, offset += cmdStride) {
7896
- // workaround that unpacks data from
7897
- // the shape material's customColor
7898
- const customColor = Box2D.HEAPU32[(offset + 4) >> 2],
7899
- uid = customColor & 0xffffff,
7900
- isSensor = (customColor >>> 25) & 0x1,
7901
- isFirstShape = (customColor >>> 26) & 0x1;
7902
-
7903
- s = $.q5play.sprites[uid];
7904
-
7905
- let type = Box2D.HEAPU8[offset];
7906
-
7907
- if (type == 7) {
7908
- continue;
7909
- }
7910
-
7911
- let vertexCount = Box2D.HEAPU16[(offset + 8) >> 1];
7912
-
7913
- let dataLen = 4;
7914
- if (type == 1) dataLen = 5 + vertexCount * 2;
7915
- else if (type == 3 || type == 4) dataLen = 5;
7916
- let data = new Float32Array(Box2D.HEAPU8.buffer, offset + 12, dataLen);
7917
-
7918
- if (!s) {
7919
- if (type == 0 && renderJointForces) jointStack.push(data);
7920
- continue;
7921
- }
7922
-
7923
- // always keep position in sync since it has a low performance cost
7924
- // unless the shape is a chain
7925
- if (type < 4 || (type == 4 && !s._hasCapsuleChain)) {
7926
- s._posX = data[0] * meterSize;
7927
- s._posY = data[1] * meterSize;
7928
- }
7929
-
7930
- s._velSynced = false;
7931
- s._vel._magCached = false;
7932
-
7933
- if (!s.visible) {
7934
- continue;
7935
- }
7936
-
7937
- if (s._hasImagery || s._userDefinedDraw) {
7938
- s._rotation = Math.atan2(data[2], data[3]) * RADTODEG;
7939
- }
7940
-
7941
- if (s.debug || (!s._hasImagery && !s._userDefinedDraw)) {
7942
- shapeStack.push({ type, sprite: s, isSensor, isFirstShape, data, vertexCount });
7943
- }
7944
- }
7945
- };
7946
-
7947
8499
  const colorMax = $._colorFormat,
7948
- debugGreen = $.color(0, colorMax, 0, colorMax * 0.9),
7949
- debugGreenFill = $.color(0, colorMax, 0, colorMax * 0.1),
7950
- debugYellow = $.color(colorMax, colorMax, 0, colorMax * 0.9),
7951
- debugYellowFill = $.color(colorMax, colorMax, 0, colorMax * 0.1);
7952
-
7953
- if ($.canvas.c2d) {
8500
+ debugGreen = $._q5 ? $.color(0, colorMax, 0, colorMax * 0.9) : 'lime',
8501
+ debugGreenFill = $._q5 ? $.color(0, colorMax, 0, colorMax * 0.1) : 'lime',
8502
+ debugYellow = $._q5 ? $.color(colorMax, colorMax, 0, colorMax * 0.9) : 'yellow',
8503
+ debugYellowFill = $._q5 ? $.color(colorMax, colorMax, 0, colorMax * 0.1) : 'yellow';
8504
+
8505
+ if (using_p5) {
8506
+ $._getFillIdx = () => $._renderer.states.fillColor;
8507
+ $._setFillIdx = (v) => $.fill(v);
8508
+ $._getStrokeIdx = () => $._renderer.states.strokeColor;
8509
+ $._setStrokeIdx = (v) => {
8510
+ if ($._renderer.states.strokeSet) $.stroke(v);
8511
+ };
8512
+ $._getStrokeWeight = () => [$._renderer.states.strokeWeight];
8513
+ $._setStrokeWeight = (v) => $.strokeWeight(...v);
8514
+ $._getImageMode = () => $._renderer.states.imageMode;
8515
+ } else if ($.canvas.c2d) {
7954
8516
  // polyfill for q5 WebGPU high efficiency functions
7955
8517
  $._getFillIdx = () => $._fill;
7956
8518
  $._setFillIdx = (v) => ($._fill = v);
@@ -8136,12 +8698,17 @@ async function q5playPreSetup(q) {
8136
8698
  };
8137
8699
 
8138
8700
  // prettier-ignore
8139
- let q5playGlobals = ['q5play','Box2D','DYN','DYNAMIC','STA','STATIC','KIN','KINEMATIC','Sprite','Group','allSprites','Ani','Anis','Visual','Visuals','camera','Joint','GlueJoint','DistanceJoint','WheelJoint','HingeJoint','SliderJoint','GrabberJoint','world','kb','keyboard','mouse','contro','contros','controllers','pointer','pointers','spriteArt','EmojiImage','getFPS','animation','parseTextureAtlas','delay'];
8701
+ let q5playGlobals = ['q5play','Box2D','DYN','DYNAMIC','STA','STATIC','KIN','KINEMATIC','Sprite','Group','allSprites','Ani','Anis','Visual','Visuals','Joint','GlueJoint','DistanceJoint','WheelJoint','HingeJoint','SliderJoint','GrabberJoint','world','kb','keyboard','mouse','contro','contros','controllers','pointer','pointers','spriteArt','EmojiImage','getFPS','animation','parseTextureAtlas','delay'];
8140
8702
 
8141
8703
  // manually propagate q5play stuff to the global window object
8142
8704
  if ($._isGlobal) {
8143
8705
  for (let p of q5playGlobals) {
8144
- window[p] = $[p];
8706
+ Object.defineProperty(window, p, {
8707
+ value: $[p],
8708
+ configurable: true,
8709
+ writable: false,
8710
+ enumerable: true
8711
+ });
8145
8712
  }
8146
8713
  }
8147
8714
 
@@ -8152,10 +8719,31 @@ async function q5playPreSetup(q) {
8152
8719
  function q5playPostSetup() {
8153
8720
  const $ = this;
8154
8721
 
8155
- if ($._isGlobal && window.update) {
8156
- $.update = window.update;
8722
+ if ($._isGlobal && window.update) $.update = window.update;
8723
+
8724
+ if (using_p5) {
8157
8725
  // p5 won't run the draw loop without a draw function defined
8158
- if (!$._q5) window.draw = () => {};
8726
+ window.draw = () => {};
8727
+
8728
+ $.loge = $.log;
8729
+ $.log = console.log;
8730
+ $.camera = $._camera;
8731
+
8732
+ if ($._isGlobal) {
8733
+ Object.defineProperty(window, 'log', {
8734
+ value: console.log
8735
+ });
8736
+ Object.defineProperty(window, 'loge', {
8737
+ value: $.loge
8738
+ });
8739
+ $.camera3D = window.camera;
8740
+ Object.defineProperty(window, 'camera3D', {
8741
+ value: $.camera3D
8742
+ });
8743
+ Object.defineProperty(window, 'camera', {
8744
+ value: $.camera
8745
+ });
8746
+ }
8159
8747
  }
8160
8748
 
8161
8749
  $.update ??= $.clear;
@@ -8167,9 +8755,11 @@ function q5playPostSetup() {
8167
8755
  function q5playUpdate() {
8168
8756
  const $ = this;
8169
8757
 
8170
- if (!$._q5) {
8758
+ if (using_p5) {
8171
8759
  $.q5play._preDrawFrameTime = performance.now();
8760
+ $.resetMatrix();
8172
8761
  }
8762
+
8173
8763
  $.q5play.spritesDrawn = 0;
8174
8764
 
8175
8765
  $.contros._update();
@@ -8242,7 +8832,6 @@ function q5playUpdate() {
8242
8832
 
8243
8833
  if ($.world.autoStep && $.world.timeScale > 0) {
8244
8834
  $.world.physicsUpdate();
8245
- $._syncWorld();
8246
8835
  }
8247
8836
  $.world.autoStep ??= true;
8248
8837
 
@@ -8313,7 +8902,7 @@ function q5playPostDraw() {
8313
8902
  else if ($.kb[k] > 0) $.kb[k]++;
8314
8903
  }
8315
8904
 
8316
- if (!$._q5) {
8905
+ if (using_p5) {
8317
8906
  $.q5play._postDrawFrameTime = performance.now();
8318
8907
  $.q5play._fps = Math.round(1000 / ($.q5play._postDrawFrameTime - $.q5play._preDrawFrameTime)) || 1;
8319
8908
  }
@@ -8321,6 +8910,7 @@ function q5playPostDraw() {
8321
8910
  }
8322
8911
 
8323
8912
  function q5playRemove() {
8913
+ this._removed = true;
8324
8914
  this.world?.delete();
8325
8915
  }
8326
8916
 
@@ -8405,16 +8995,19 @@ addAnis -> es:añadirAnis
8405
8995
  changeAni -> es:cambiarAni
8406
8996
  playAni -> es:reproducirAni
8407
8997
  playAnis -> es:reproducirAnis
8998
+ moveTo -> es:moverA
8408
8999
  moveTowards -> es:moverHacia
9000
+ rotateTo -> es:rotarA
8409
9001
  rotateTowards -> es:rotarHacia
9002
+ transformTowards -> es:transformarHacia
8410
9003
  applyForce -> es:aplicarFuerza
8411
9004
  applyForceScaled -> es:aplicarFuerzaEscalada
8412
9005
  attractTo -> es:atraerA
8413
9006
  repelFrom -> es:repelerDe
8414
9007
  applyTorque -> es:aplicarTorque
8415
- angleTo -> es:ánguloHacia
8416
- rotationToFace -> es:rotaciónParaMirar
8417
- angleToFace -> es:ánguloParaMirar
9008
+ applyWind -> es:aplicarViento
9009
+ angleTo -> es:ánguloA
9010
+ angleDistTo -> es:distÁnguloA
8418
9011
  setSpeedAndDirection -> es:establecerVelocidadYDirección
8419
9012
  scaleBy -> es:escalarPor
8420
9013
  resetMass -> es:reiniciarMasa
@@ -8518,6 +9111,56 @@ pressure -> es:presión
8518
9111
  };
8519
9112
  q5playClassLangs.Group += q5playClassLangs.Sprite;
8520
9113
 
9114
+ if (typeof globalThis.Q5 == 'undefined') {
9115
+ console.warn('p5.js v2 is not fully compatible with q5play. Consider using q5 instead: https://q5js.org');
9116
+
9117
+ p5.addHook = (hook, fn) => {
9118
+ p5.registerAddon((p5, proto, lifecycles) => {
9119
+ lifecycles[hook] = fn;
9120
+ });
9121
+ };
9122
+
9123
+ // p5.js v2 compatibility layer
9124
+ globalThis.Canvas = (...args) => {
9125
+ return new Promise((resolve) => {
9126
+ window.setup = async function () {
9127
+ const $ = p5.instance;
9128
+ $._webgpu = $._webgpuFallback = true;
9129
+
9130
+ $.Canvas(...args);
9131
+
9132
+ // q5play defaults
9133
+ colorMode(RGB, 1);
9134
+ imageMode(CENTER);
9135
+
9136
+ $.halfWidth = width / 2;
9137
+ $.halfHeight = height / 2;
9138
+
9139
+ let _resetMatrix = $.resetMatrix;
9140
+
9141
+ $.resetMatrix = () => {
9142
+ _resetMatrix.call($);
9143
+ $.translate($.halfWidth, $.halfHeight);
9144
+ };
9145
+
9146
+ Object.defineProperty(p5, 'update', {
9147
+ set(fn) {
9148
+ $.update = fn;
9149
+ },
9150
+ get() {
9151
+ return $.update;
9152
+ },
9153
+ configurable: true
9154
+ });
9155
+
9156
+ resolve();
9157
+ };
9158
+ });
9159
+ };
9160
+
9161
+ globalThis.Q5 = globalThis.q5 = p5;
9162
+ }
9163
+
8521
9164
  Q5.addHook('presetup', q5playPreSetup);
8522
9165
  Q5.addHook('postsetup', q5playPostSetup);
8523
9166
  Q5.addHook('predraw', q5playUpdate);