litecanvas 0.77.0 → 0.78.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/dist/dist.dev.js CHANGED
@@ -30,7 +30,7 @@
30
30
 
31
31
  // src/index.js
32
32
  function litecanvas(settings = {}) {
33
- const root = globalThis, PI = Math.PI, TWO_PI = PI * 2, raf = requestAnimationFrame, _browserEventListeners = [], on = (elem, evt, callback) => {
33
+ const root = globalThis, math = Math, PI = math.PI, TWO_PI = PI * 2, raf = requestAnimationFrame, _browserEventListeners = [], on = (elem, evt, callback) => {
34
34
  elem.addEventListener(evt, callback, false);
35
35
  _browserEventListeners.push(
36
36
  () => elem.removeEventListener(evt, callback, false)
@@ -50,7 +50,7 @@
50
50
  animate: true
51
51
  };
52
52
  settings = Object.assign(defaults, settings);
53
- let _initialized = false, _plugins = [], _canvas = settings.canvas || document.createElement("canvas"), _autoscale = settings.autoscale, _animated = settings.animate, _scale = 1, _ctx, _outline_fix = 0.5, _timeScale = 1, _lastFrameTime, _deltaTime, _accumulated = 0, _rafid, _fontFamily = "sans-serif", _fontSize = 32, _rng_seed = Date.now(), _global = settings.global, _events = {
53
+ let _initialized = false, _plugins = [], _canvas = settings.canvas || document.createElement("canvas"), _animated = settings.animate, _scale = 1, _ctx, _outline_fix = 0.5, _timeScale = 1, _lastFrameTime, _deltaTime = 1 / 60, _accumulated = 0, _rafid, _fontFamily = "sans-serif", _fontSize = 18, _rng_seed = Date.now(), _global = settings.global, _events = {
54
54
  init: null,
55
55
  update: null,
56
56
  draw: null,
@@ -135,6 +135,25 @@
135
135
  DEV: assert(isFinite(rads), "rad2deg: 1st param must be a number");
136
136
  return 180 / PI * rads;
137
137
  },
138
+ /**
139
+ * Calculates the integer closest to a number and optional precision.
140
+ *
141
+ * @param {number} n number to round.
142
+ * @param {number} [precision] number of decimal digits to round to, default is 0.
143
+ * @returns {number} rounded number.
144
+ */
145
+ round: (n, precision = 0) => {
146
+ DEV: assert(isFinite(n), "round: 1st param must be a number");
147
+ DEV: assert(
148
+ null === precision || isFinite(precision) && precision >= 0,
149
+ "round: 2nd param must be a positive number or zero"
150
+ );
151
+ if (!precision) {
152
+ return math.round(n);
153
+ }
154
+ const multiplier = 10 ** precision;
155
+ return math.round(n * multiplier) / multiplier;
156
+ },
138
157
  /**
139
158
  * Constrains a number between `min` and `max`.
140
159
  *
@@ -175,7 +194,7 @@
175
194
  max !== min,
176
195
  "randi: the 2nd param must be not equal to the 3rd param"
177
196
  );
178
- return value - (max - min) * Math.floor((value - min) / (max - min));
197
+ return value - (max - min) * math.floor((value - min) / (max - min));
179
198
  },
180
199
  /**
181
200
  * Re-maps a number from one range to another.
@@ -249,7 +268,7 @@
249
268
  max > min,
250
269
  "randi: the 1st param must be less than the 2nd param"
251
270
  );
252
- return Math.floor(instance.rand(min, max + 1));
271
+ return math.floor(instance.rand(min, max + 1));
253
272
  },
254
273
  /**
255
274
  * If a value is passed, initializes the random number generator with an explicit seed value.
@@ -269,12 +288,12 @@
269
288
  /**
270
289
  * Clear the game screen with an optional color
271
290
  *
272
- * @param {number?} color The background color (index) or null (for transparent)
291
+ * @param {number?} color The background color (index) or null/undefined (for transparent)
273
292
  */
274
293
  cls(color) {
275
294
  DEV: assert(
276
295
  null == color || isFinite(color) && color >= 0,
277
- "cls: 1st param must be a positive number or zero or null"
296
+ "cls: 1st param must be a positive number or zero or undefined"
278
297
  );
279
298
  if (null == color) {
280
299
  _ctx.clearRect(0, 0, _ctx.canvas.width, _ctx.canvas.height);
@@ -797,51 +816,6 @@
797
816
  DEV: assert(isFinite(value), "volume: 1st param must be a number");
798
817
  root.zzfxV = value;
799
818
  },
800
- /** UTILS API */
801
- /**
802
- * Check a collision between two rectangles
803
- *
804
- * @param {number} x1 first rectangle position X
805
- * @param {number} y1 first rectangle position Y
806
- * @param {number} w1 first rectangle width
807
- * @param {number} h1 first rectangle height
808
- * @param {number} x2 second rectangle position X
809
- * @param {number} y2 second rectangle position Y
810
- * @param {number} w2 second rectangle width
811
- * @param {number} h2 second rectangle height
812
- * @returns {boolean}
813
- */
814
- colrect: (x1, y1, w1, h1, x2, y2, w2, h2) => {
815
- DEV: assert(isFinite(x1), "colrect: 1st param must be a number");
816
- DEV: assert(isFinite(y1), "colrect: 2nd param must be a number");
817
- DEV: assert(isFinite(w1), "colrect: 3rd param must be a number");
818
- DEV: assert(isFinite(h1), "colrect: 4th param must be a number");
819
- DEV: assert(isFinite(x2), "colrect: 5th param must be a number");
820
- DEV: assert(isFinite(y2), "colrect: 6th param must be a number");
821
- DEV: assert(isFinite(w2), "colrect: 7th param must be a number");
822
- DEV: assert(isFinite(h2), "colrect: 8th param must be a number");
823
- return x1 < x2 + w2 && x1 + w1 > x2 && y1 < y2 + h2 && y1 + h1 > y2;
824
- },
825
- /**
826
- * Check a collision between two circles
827
- *
828
- * @param {number} x1 first circle position X
829
- * @param {number} y1 first circle position Y
830
- * @param {number} r1 first circle position radius
831
- * @param {number} x2 second circle position X
832
- * @param {number} y2 second circle position Y
833
- * @param {number} r2 second circle position radius
834
- * @returns {boolean}
835
- */
836
- colcirc: (x1, y1, r1, x2, y2, r2) => {
837
- DEV: assert(isFinite(x1), "colcirc: 1st param must be a number");
838
- DEV: assert(isFinite(y1), "colcirc: 2nd param must be a number");
839
- DEV: assert(isFinite(r1), "colcirc: 3rd param must be a number");
840
- DEV: assert(isFinite(x2), "colcirc: 4th param must be a number");
841
- DEV: assert(isFinite(y2), "colcirc: 5th param must be a number");
842
- DEV: assert(isFinite(r2), "colcirc: 6th param must be a number");
843
- return (x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1) <= (r1 + r2) * (r1 + r2);
844
- },
845
819
  /** PLUGINS API */
846
820
  /**
847
821
  * Prepares a plugin to be loaded
@@ -982,22 +956,22 @@
982
956
  * Stops the litecanvas instance and remove all event listeners.
983
957
  */
984
958
  quit() {
959
+ cancelAnimationFrame(_rafid);
985
960
  instance.emit("quit");
961
+ _events = [];
986
962
  for (const removeListener of _browserEventListeners) {
987
963
  removeListener();
988
964
  }
989
- cancelAnimationFrame(_rafid);
990
- _events = false;
991
965
  if (_global) {
992
966
  for (const key in instance) {
993
967
  delete root[key];
994
968
  }
995
- delete root.__litecanvas;
969
+ delete root.ENGINE;
996
970
  }
997
971
  }
998
972
  };
999
- for (const k of "PI,sin,cos,atan2,hypot,tan,abs,ceil,round,floor,trunc,min,max,pow,sqrt,sign,exp".split(",")) {
1000
- instance[k] = Math[k];
973
+ for (const k of "PI,sin,cos,atan2,hypot,tan,abs,ceil,floor,trunc,min,max,pow,sqrt,sign,exp".split(",")) {
974
+ instance[k] = math[k];
1001
975
  }
1002
976
  function init() {
1003
977
  _initialized = true;
@@ -1008,7 +982,7 @@
1008
982
  for (const [callback, config] of _plugins) {
1009
983
  loadPlugin(callback, config);
1010
984
  }
1011
- if (_autoscale) {
985
+ if (settings.autoscale) {
1012
986
  on(root, "resize", onResize);
1013
987
  }
1014
988
  if (settings.tapEvents) {
@@ -1030,7 +1004,7 @@
1030
1004
  const tap = _taps.get(id) || _registerTap(id);
1031
1005
  tap.x = x;
1032
1006
  tap.y = y;
1033
- }, _checkTapped = (tap) => tap && performance.now() - tap.ts <= 200, preventDefault = (ev) => ev.preventDefault();
1007
+ }, _checkTapped = (tap) => tap && performance.now() - tap.ts <= 300, preventDefault = (ev) => ev.preventDefault();
1034
1008
  let _pressingMouse = false;
1035
1009
  on(_canvas, "mousedown", (ev) => {
1036
1010
  if (ev.button === 0) {
@@ -1109,23 +1083,59 @@
1109
1083
  });
1110
1084
  }
1111
1085
  if (settings.keyboardEvents) {
1112
- const _keyDownList = /* @__PURE__ */ new Set();
1113
- const iskeydown = (key) => {
1114
- DEV: assert(
1115
- "string" === typeof key,
1116
- "iskeydown: 1st param must be a string"
1086
+ const toLowerCase = (s) => s.toLowerCase();
1087
+ const _keysDown = /* @__PURE__ */ new Set();
1088
+ const _keysPress = /* @__PURE__ */ new Set();
1089
+ const keyCheck = (keysSet, key) => {
1090
+ return !key ? keysSet.size > 0 : keysSet.has(
1091
+ "space" === toLowerCase(key) ? " " : toLowerCase(key)
1117
1092
  );
1118
- key = key.toLowerCase();
1119
- return "any" === key ? _keyDownList.size > 0 : _keyDownList.has("space" === key ? " " : key);
1120
1093
  };
1121
- instance.setvar("iskeydown", iskeydown);
1122
1094
  on(root, "keydown", (event) => {
1123
- _keyDownList.add(event.key.toLowerCase());
1095
+ if (!_keysDown.has(toLowerCase(event.key))) {
1096
+ _keysDown.add(toLowerCase(event.key));
1097
+ _keysPress.add(toLowerCase(event.key));
1098
+ }
1124
1099
  });
1125
1100
  on(root, "keyup", (event) => {
1126
- _keyDownList.delete(event.key.toLowerCase());
1101
+ _keysDown.delete(toLowerCase(event.key));
1127
1102
  });
1128
- on(root, "blur", () => _keyDownList.clear());
1103
+ on(root, "blur", () => _keysDown.clear());
1104
+ instance.listen("after:draw", () => _keysPress.clear());
1105
+ instance.setvar(
1106
+ "iskeydown",
1107
+ /**
1108
+ * Checks if a which key is pressed (down) on the keyboard.
1109
+ * Note: use `iskeydown()` to check for any key.
1110
+ *
1111
+ * @param {string?} key
1112
+ * @returns {boolean}
1113
+ */
1114
+ (key) => {
1115
+ DEV: assert(
1116
+ null == key || "string" === typeof key,
1117
+ "iskeydown: 1st param must be a string or undefined"
1118
+ );
1119
+ return keyCheck(_keysDown, key);
1120
+ }
1121
+ );
1122
+ instance.setvar(
1123
+ "iskeypressed",
1124
+ /**
1125
+ * Checks if a which key just got pressed on the keyboard.
1126
+ * Note: use `iskeypressed()` to check for any key.
1127
+ *
1128
+ * @param {string?} key
1129
+ * @returns {boolean}
1130
+ */
1131
+ (key) => {
1132
+ DEV: assert(
1133
+ null == key || "string" === typeof key,
1134
+ "iskeypressed: 1st param must be a string or undefined"
1135
+ );
1136
+ return keyCheck(_keysPress, key);
1137
+ }
1138
+ );
1129
1139
  }
1130
1140
  if (settings.pauseOnBlur) {
1131
1141
  on(root, "blur", () => {
@@ -1137,7 +1147,6 @@
1137
1147
  }
1138
1148
  });
1139
1149
  }
1140
- instance.setfps(60);
1141
1150
  instance.emit("init", instance);
1142
1151
  _lastFrameTime = performance.now();
1143
1152
  _rafid = raf(drawFrame);
@@ -1149,7 +1158,7 @@
1149
1158
  let updated = 0, frameTime = (now - _lastFrameTime) / 1e3;
1150
1159
  _lastFrameTime = now;
1151
1160
  if (frameTime > _deltaTime * 30) {
1152
- console.log("skipping too long frame");
1161
+ console.warn("skipping too long frame");
1153
1162
  } else {
1154
1163
  _accumulated += frameTime;
1155
1164
  if (!_animated) {
@@ -1200,12 +1209,12 @@
1200
1209
  }
1201
1210
  function onResize() {
1202
1211
  const styles = _canvas.style;
1203
- if (_autoscale) {
1212
+ if (settings.autoscale) {
1204
1213
  if (!styles.display) {
1205
1214
  styles.display = "block";
1206
1215
  styles.margin = "auto";
1207
1216
  }
1208
- _scale = Math.min(
1217
+ _scale = math.min(
1209
1218
  root.innerWidth / instance.WIDTH,
1210
1219
  root.innerHeight / instance.HEIGHT
1211
1220
  );
@@ -1239,11 +1248,11 @@
1239
1248
  }
1240
1249
  }
1241
1250
  if (_global) {
1242
- if (root.__litecanvas) {
1243
- throw "global litecanvas already instantiated";
1251
+ if (root.ENGINE) {
1252
+ throw "two global litecanvas detected";
1244
1253
  }
1245
1254
  Object.assign(root, instance);
1246
- root.__litecanvas = instance;
1255
+ root.ENGINE = instance;
1247
1256
  }
1248
1257
  setupCanvas();
1249
1258
  if ("loading" === document.readyState) {
package/dist/dist.js CHANGED
@@ -25,7 +25,7 @@
25
25
 
26
26
  // src/index.js
27
27
  function litecanvas(settings = {}) {
28
- const root = globalThis, PI = Math.PI, TWO_PI = PI * 2, raf = requestAnimationFrame, _browserEventListeners = [], on = (elem, evt, callback) => {
28
+ const root = globalThis, math = Math, PI = math.PI, TWO_PI = PI * 2, raf = requestAnimationFrame, _browserEventListeners = [], on = (elem, evt, callback) => {
29
29
  elem.addEventListener(evt, callback, false);
30
30
  _browserEventListeners.push(
31
31
  () => elem.removeEventListener(evt, callback, false)
@@ -45,7 +45,7 @@
45
45
  animate: true
46
46
  };
47
47
  settings = Object.assign(defaults, settings);
48
- let _initialized = false, _plugins = [], _canvas = settings.canvas || document.createElement("canvas"), _autoscale = settings.autoscale, _animated = settings.animate, _scale = 1, _ctx, _outline_fix = 0.5, _timeScale = 1, _lastFrameTime, _deltaTime, _accumulated = 0, _rafid, _fontFamily = "sans-serif", _fontSize = 32, _rng_seed = Date.now(), _global = settings.global, _events = {
48
+ let _initialized = false, _plugins = [], _canvas = settings.canvas || document.createElement("canvas"), _animated = settings.animate, _scale = 1, _ctx, _outline_fix = 0.5, _timeScale = 1, _lastFrameTime, _deltaTime = 1 / 60, _accumulated = 0, _rafid, _fontFamily = "sans-serif", _fontSize = 18, _rng_seed = Date.now(), _global = settings.global, _events = {
49
49
  init: null,
50
50
  update: null,
51
51
  draw: null,
@@ -125,6 +125,20 @@
125
125
  rad2deg: (rads) => {
126
126
  return 180 / PI * rads;
127
127
  },
128
+ /**
129
+ * Calculates the integer closest to a number and optional precision.
130
+ *
131
+ * @param {number} n number to round.
132
+ * @param {number} [precision] number of decimal digits to round to, default is 0.
133
+ * @returns {number} rounded number.
134
+ */
135
+ round: (n, precision = 0) => {
136
+ if (!precision) {
137
+ return math.round(n);
138
+ }
139
+ const multiplier = 10 ** precision;
140
+ return math.round(n * multiplier) / multiplier;
141
+ },
128
142
  /**
129
143
  * Constrains a number between `min` and `max`.
130
144
  *
@@ -147,7 +161,7 @@
147
161
  * @returns {number}
148
162
  */
149
163
  wrap: (value, min, max) => {
150
- return value - (max - min) * Math.floor((value - min) / (max - min));
164
+ return value - (max - min) * math.floor((value - min) / (max - min));
151
165
  },
152
166
  /**
153
167
  * Re-maps a number from one range to another.
@@ -201,7 +215,7 @@
201
215
  * @returns {number} the random number
202
216
  */
203
217
  randi: (min = 0, max = 1) => {
204
- return Math.floor(instance.rand(min, max + 1));
218
+ return math.floor(instance.rand(min, max + 1));
205
219
  },
206
220
  /**
207
221
  * If a value is passed, initializes the random number generator with an explicit seed value.
@@ -217,7 +231,7 @@
217
231
  /**
218
232
  * Clear the game screen with an optional color
219
233
  *
220
- * @param {number?} color The background color (index) or null (for transparent)
234
+ * @param {number?} color The background color (index) or null/undefined (for transparent)
221
235
  */
222
236
  cls(color) {
223
237
  if (null == color) {
@@ -574,37 +588,6 @@
574
588
  volume(value) {
575
589
  root.zzfxV = value;
576
590
  },
577
- /** UTILS API */
578
- /**
579
- * Check a collision between two rectangles
580
- *
581
- * @param {number} x1 first rectangle position X
582
- * @param {number} y1 first rectangle position Y
583
- * @param {number} w1 first rectangle width
584
- * @param {number} h1 first rectangle height
585
- * @param {number} x2 second rectangle position X
586
- * @param {number} y2 second rectangle position Y
587
- * @param {number} w2 second rectangle width
588
- * @param {number} h2 second rectangle height
589
- * @returns {boolean}
590
- */
591
- colrect: (x1, y1, w1, h1, x2, y2, w2, h2) => {
592
- return x1 < x2 + w2 && x1 + w1 > x2 && y1 < y2 + h2 && y1 + h1 > y2;
593
- },
594
- /**
595
- * Check a collision between two circles
596
- *
597
- * @param {number} x1 first circle position X
598
- * @param {number} y1 first circle position Y
599
- * @param {number} r1 first circle position radius
600
- * @param {number} x2 second circle position X
601
- * @param {number} y2 second circle position Y
602
- * @param {number} r2 second circle position radius
603
- * @returns {boolean}
604
- */
605
- colcirc: (x1, y1, r1, x2, y2, r2) => {
606
- return (x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1) <= (r1 + r2) * (r1 + r2);
607
- },
608
591
  /** PLUGINS API */
609
592
  /**
610
593
  * Prepares a plugin to be loaded
@@ -700,22 +683,22 @@
700
683
  * Stops the litecanvas instance and remove all event listeners.
701
684
  */
702
685
  quit() {
686
+ cancelAnimationFrame(_rafid);
703
687
  instance.emit("quit");
688
+ _events = [];
704
689
  for (const removeListener of _browserEventListeners) {
705
690
  removeListener();
706
691
  }
707
- cancelAnimationFrame(_rafid);
708
- _events = false;
709
692
  if (_global) {
710
693
  for (const key in instance) {
711
694
  delete root[key];
712
695
  }
713
- delete root.__litecanvas;
696
+ delete root.ENGINE;
714
697
  }
715
698
  }
716
699
  };
717
- for (const k of "PI,sin,cos,atan2,hypot,tan,abs,ceil,round,floor,trunc,min,max,pow,sqrt,sign,exp".split(",")) {
718
- instance[k] = Math[k];
700
+ for (const k of "PI,sin,cos,atan2,hypot,tan,abs,ceil,floor,trunc,min,max,pow,sqrt,sign,exp".split(",")) {
701
+ instance[k] = math[k];
719
702
  }
720
703
  function init() {
721
704
  _initialized = true;
@@ -726,7 +709,7 @@
726
709
  for (const [callback, config] of _plugins) {
727
710
  loadPlugin(callback, config);
728
711
  }
729
- if (_autoscale) {
712
+ if (settings.autoscale) {
730
713
  on(root, "resize", onResize);
731
714
  }
732
715
  if (settings.tapEvents) {
@@ -748,7 +731,7 @@
748
731
  const tap = _taps.get(id) || _registerTap(id);
749
732
  tap.x = x;
750
733
  tap.y = y;
751
- }, _checkTapped = (tap) => tap && performance.now() - tap.ts <= 200, preventDefault = (ev) => ev.preventDefault();
734
+ }, _checkTapped = (tap) => tap && performance.now() - tap.ts <= 300, preventDefault = (ev) => ev.preventDefault();
752
735
  let _pressingMouse = false;
753
736
  on(_canvas, "mousedown", (ev) => {
754
737
  if (ev.button === 0) {
@@ -827,19 +810,51 @@
827
810
  });
828
811
  }
829
812
  if (settings.keyboardEvents) {
830
- const _keyDownList = /* @__PURE__ */ new Set();
831
- const iskeydown = (key) => {
832
- key = key.toLowerCase();
833
- return "any" === key ? _keyDownList.size > 0 : _keyDownList.has("space" === key ? " " : key);
813
+ const toLowerCase = (s) => s.toLowerCase();
814
+ const _keysDown = /* @__PURE__ */ new Set();
815
+ const _keysPress = /* @__PURE__ */ new Set();
816
+ const keyCheck = (keysSet, key) => {
817
+ return !key ? keysSet.size > 0 : keysSet.has(
818
+ "space" === toLowerCase(key) ? " " : toLowerCase(key)
819
+ );
834
820
  };
835
- instance.setvar("iskeydown", iskeydown);
836
821
  on(root, "keydown", (event) => {
837
- _keyDownList.add(event.key.toLowerCase());
822
+ if (!_keysDown.has(toLowerCase(event.key))) {
823
+ _keysDown.add(toLowerCase(event.key));
824
+ _keysPress.add(toLowerCase(event.key));
825
+ }
838
826
  });
839
827
  on(root, "keyup", (event) => {
840
- _keyDownList.delete(event.key.toLowerCase());
828
+ _keysDown.delete(toLowerCase(event.key));
841
829
  });
842
- on(root, "blur", () => _keyDownList.clear());
830
+ on(root, "blur", () => _keysDown.clear());
831
+ instance.listen("after:draw", () => _keysPress.clear());
832
+ instance.setvar(
833
+ "iskeydown",
834
+ /**
835
+ * Checks if a which key is pressed (down) on the keyboard.
836
+ * Note: use `iskeydown()` to check for any key.
837
+ *
838
+ * @param {string?} key
839
+ * @returns {boolean}
840
+ */
841
+ (key) => {
842
+ return keyCheck(_keysDown, key);
843
+ }
844
+ );
845
+ instance.setvar(
846
+ "iskeypressed",
847
+ /**
848
+ * Checks if a which key just got pressed on the keyboard.
849
+ * Note: use `iskeypressed()` to check for any key.
850
+ *
851
+ * @param {string?} key
852
+ * @returns {boolean}
853
+ */
854
+ (key) => {
855
+ return keyCheck(_keysPress, key);
856
+ }
857
+ );
843
858
  }
844
859
  if (settings.pauseOnBlur) {
845
860
  on(root, "blur", () => {
@@ -851,7 +866,6 @@
851
866
  }
852
867
  });
853
868
  }
854
- instance.setfps(60);
855
869
  instance.emit("init", instance);
856
870
  _lastFrameTime = performance.now();
857
871
  _rafid = raf(drawFrame);
@@ -897,12 +911,12 @@
897
911
  }
898
912
  function onResize() {
899
913
  const styles = _canvas.style;
900
- if (_autoscale) {
914
+ if (settings.autoscale) {
901
915
  if (!styles.display) {
902
916
  styles.display = "block";
903
917
  styles.margin = "auto";
904
918
  }
905
- _scale = Math.min(
919
+ _scale = math.min(
906
920
  root.innerWidth / instance.WIDTH,
907
921
  root.innerHeight / instance.HEIGHT
908
922
  );
@@ -932,11 +946,11 @@
932
946
  }
933
947
  }
934
948
  if (_global) {
935
- if (root.__litecanvas) {
936
- throw "global litecanvas already instantiated";
949
+ if (root.ENGINE) {
950
+ throw "two global litecanvas detected";
937
951
  }
938
952
  Object.assign(root, instance);
939
- root.__litecanvas = instance;
953
+ root.ENGINE = instance;
940
954
  }
941
955
  setupCanvas();
942
956
  if ("loading" === document.readyState) {
package/dist/dist.min.js CHANGED
@@ -1 +1 @@
1
- (()=>{var e=new AudioContext,t=(t=1,a=.05,l=220,n=0,i=0,o=.1,r=0,s=1,c=0,f=0,p=0,d=0,u=0,h=0,g=0,m=0,v=0,E=1,b=0,x=0,T=0)=>{let w=Math,y=2*w.PI,H=c*=500*y/44100/44100,I=l*=(1-a+2*a*w.random(a=[]))*y/44100,S=0,D=0,A=0,k=1,C=0,L=0,X=0,z=T<0?-1:1,P=y*z*T*2/44100,M=w.cos(P),O=w.sin,Y=O(P)/4,F=1+Y,W=-2*M/F,_=(1-Y)/F,R=(1+z*M)/2/F,G=-(z+M)/F,N=0,B=0,U=0,q=0;for(n=44100*n+9,b*=44100,i*=44100,o*=44100,v*=44100,f*=500*y/85766121e6,g*=y/44100,p*=y/44100,d*=44100,u=44100*u|0,t*=.3*(globalThis.zzfxV||1),z=n+b+i+o+v|0;A<z;a[A++]=X*t)++L%(100*m|0)||(X=r?1<r?2<r?3<r?O(S*S):w.max(w.min(w.tan(S),1),-1):1-(2*S/y%2+2)%2:1-4*w.abs(w.round(S/y)-S/y):O(S),X=(u?1-x+x*O(y*A/u):1)*(X<0?-1:1)*w.abs(X)**s*(A<n?A/n:A<n+b?1-(A-n)/b*(1-E):A<n+b+i?E:A<z-v?(z-A-v)/o*E:0),X=v?X/2+(v>A?0:(A<z-v?1:(z-A)/v)*a[A-v|0]/2/t):X,T&&(X=q=R*N+G*(N=B)+R*(B=X)-_*U-W*(U=q))),S+=(P=(l+=c+=f)*w.cos(g*D++))+P*h*O(A**5),k&&++k>d&&(l+=p,I+=p,k=0),!u||++C%u||(l=I,c=H,k=k||1);(t=e.createBuffer(1,z,44100)).getChannelData(0).set(a),(l=e.createBufferSource()).buffer=t,l.connect(e.destination),l.start()},a=["#111","#6a7799","#aec2c2","#FFF1E8","#e83b3b","#fabc20","#155fd9","#3cbcfc","#327345","#63c64d","#6c2c1f","#ac7c00"];globalThis.litecanvas=function(e={}){let l=globalThis,n=Math.PI,i=2*n,o=requestAnimationFrame,r=[],s=(e,t,a)=>{e.addEventListener(t,a,!1),r.push(()=>e.removeEventListener(t,a,!1))};e=Object.assign({width:null,height:null,autoscale:!0,pixelart:!1,antialias:!1,canvas:null,global:!0,loop:null,pauseOnBlur:!0,tapEvents:!0,keyboardEvents:!0,animate:!0},e);let c=!1,f=[],p=e.canvas||document.createElement("canvas"),d=e.autoscale,u=e.animate,h=1,g,m=.5,v=1,E,b,x=0,T,w="sans-serif",y=32,H=Date.now(),I=e.global,S={init:null,update:null,draw:null,resized:null,tap:null,untap:null,tapping:null,tapped:null},D={settings:Object.assign({},e),colors:a},A={WIDTH:e.width,HEIGHT:e.height||e.width,CANVAS:null,ELAPSED:0,CENTERX:0,CENTERY:0,MOUSEX:-1,MOUSEY:-1,DEFAULT_SFX:[.5,,1675,,.06,.2,1,1.8,,,637,.06],TWO_PI:i,HALF_PI:n/2,lerp:(e,t,a)=>a*(t-e)+e,deg2rad:e=>n/180*e,rad2deg:e=>180/n*e,clamp:(e,t,a)=>e<t?t:e>a?a:e,wrap:(e,t,a)=>e-(a-t)*Math.floor((e-t)/(a-t)),map(e,t,a,l,n,i){let o=(e-t)/(a-t)*(n-l)+l;return i?A.clamp(o,l,n):o},norm:(e,t,a)=>A.map(e,t,a,0,1),rand:(e=0,t=1)=>(H=(1664525*H+0x3c6ef35f)%0x100000000)/0x100000000*(t-e)+e,randi:(e=0,t=1)=>Math.floor(A.rand(e,t+1)),seed:e=>null==e?H:H=~~e,cls(e){null==e?g.clearRect(0,0,g.canvas.width,g.canvas.height):A.rectfill(0,0,g.canvas.width,g.canvas.height,e)},rect(e,t,a,l,n,i=null){g.beginPath(),g[i?"roundRect":"rect"](~~e-m,~~t-m,~~a+2*m,~~l+2*m,i),A.stroke(n)},rectfill(e,t,a,l,n,i=null){g.beginPath(),g[i?"roundRect":"rect"](~~e,~~t,~~a,~~l,i),A.fill(n)},circ(e,t,a,l){g.beginPath(),g.arc(~~e,~~t,~~a,0,i),A.stroke(l)},circfill(e,t,a,l){g.beginPath(),g.arc(~~e,~~t,~~a,0,i),A.fill(l)},line(e,t,a,l,n){g.beginPath();let i=.5*(0!==m&&~~e==~~a),o=.5*(0!==m&&~~t==~~l);g.moveTo(~~e+i,~~t+o),g.lineTo(~~a+i,~~l+o),A.stroke(n)},linewidth(e){g.lineWidth=~~e,m=.5*(~~e%2!=0)},linedash(e,t=0){g.setLineDash(e),g.lineDashOffset=t},text(e,t,a,l=3,n="normal"){g.font=`${n} ${y}px ${w}`,g.fillStyle=A.getcolor(l),g.fillText(a,~~e,~~t)},textfont(e){w=e},textsize(e){y=e},textalign(e,t){e&&(g.textAlign=e),t&&(g.textBaseline=t)},image(e,t,a){g.drawImage(a,~~e,~~t)},paint(e,t,a,l={}){let n=l.canvas||new OffscreenCanvas(1,1),i=l.scale||1,o=g;if(n.width=e*i,n.height=t*i,(g=n.getContext("2d")).scale(i,i),a.push){let e=0,t=0;for(let l of(g.imageSmoothingEnabled=!1,a)){for(let a of l)" "!==a&&"."!==a&&A.rectfill(e,t,1,1,parseInt(a,16)),e++;t++,e=0}}else a(g);return g=o,n},ctx:e=>(e&&(g=e),g),push:()=>g.save(),pop:()=>g.restore(),translate:(e,t)=>g.translate(~~e,~~t),scale:(e,t)=>g.scale(e,t||e),rotate:e=>g.rotate(e),alpha(e){g.globalAlpha=A.clamp(e,0,1)},path:e=>new Path2D(e),fill(e,t){g.fillStyle=A.getcolor(e),t?g.fill(t):g.fill()},stroke(e,t){g.strokeStyle=A.getcolor(e),t?g.stroke(t):g.stroke()},clip(e){g.clip(e)},sfx:(e,a=0,n=1)=>!(l.zzfxV<=0)&&(!navigator.userActivation||!!navigator.userActivation.hasBeenActive)&&(e=e||A.DEFAULT_SFX,(0!==a||1!==n)&&((e=e.slice())[0]=n*(e[0]||1),e[10]=~~e[10]+a),t.apply(0,e),e),volume(e){l.zzfxV=e},colrect:(e,t,a,l,n,i,o,r)=>e<n+o&&e+a>n&&t<i+r&&t+l>i,colcirc:(e,t,a,l,n,i)=>(l-e)*(l-e)+(n-t)*(n-t)<=(a+i)*(a+i),use(e,t={}){c?z(e,t):f.push([e,t])},listen:(e,t)=>(S[e]=S[e]||new Set,S[e].add(t),()=>S[e].delete(t)),emit(e,t,a,l,n){c&&(X("before:"+e,t,a,l,n),X(e,t,a,l,n),X("after:"+e,t,a,l,n))},getcolor:e=>a[~~e%a.length],setvar(e,t){A[e]=t,I&&(l[e]=t)},resize(e,t){A.setvar("WIDTH",p.width=e),A.setvar("HEIGHT",p.height=t),A.setvar("CENTERX",A.WIDTH/2),A.setvar("CENTERY",A.HEIGHT/2),L()},timescale(e){v=e},setfps(e){b=1/~~e},quit(){for(let e of(A.emit("quit"),r))e();if(cancelAnimationFrame(T),S=!1,I){for(let e in A)delete l[e];delete l.__litecanvas}}};for(let e of"PI,sin,cos,atan2,hypot,tan,abs,ceil,round,floor,trunc,min,max,pow,sqrt,sign,exp".split(","))A[e]=Math[e];function k(){c=!0;let t=e.loop?e.loop:l;for(let e in S)t[e]&&A.listen(e,t[e]);for(let[e,t]of f)z(e,t);if(d&&s(l,"resize",L),e.tapEvents){let e=(e,t)=>[(e-p.offsetLeft)/h,(t-p.offsetTop)/h],t=new Map,a=(e,a,l)=>{let n={x:a,y:l,startX:a,startY:l,ts:performance.now()};return t.set(e,n),n},n=(e,l,n)=>{let i=t.get(e)||a(e);i.x=l,i.y=n},i=e=>e&&performance.now()-e.ts<=200,o=e=>e.preventDefault(),r=!1;s(p,"mousedown",t=>{if(0===t.button){o(t);let[l,n]=e(t.pageX,t.pageY);A.emit("tap",l,n,0),a(0,l,n),r=!0}}),s(p,"mouseup",a=>{if(0===a.button){o(a);let l=t.get(0),[n,s]=e(a.pageX,a.pageY);i(l)&&A.emit("tapped",l.startX,l.startY,0),A.emit("untap",n,s,0),t.delete(0),r=!1}}),s(p,"mousemove",t=>{o(t);let[a,l]=e(t.pageX,t.pageY);A.setvar("MOUSEX",a),A.setvar("MOUSEY",l),r&&(A.emit("tapping",a,l,0),n(0,a,l))}),s(p,"touchstart",t=>{for(let l of(o(t),t.changedTouches)){let[t,n]=e(l.pageX,l.pageY);A.emit("tap",t,n,l.identifier+1),a(l.identifier+1,t,n)}}),s(p,"touchmove",t=>{for(let a of(o(t),t.changedTouches)){let[t,l]=e(a.pageX,a.pageY);A.emit("tapping",t,l,a.identifier+1),n(a.identifier+1,t,l)}});let c=e=>{o(e);let a=[];if(e.targetTouches.length>0)for(let t of e.targetTouches)a.push(t.identifier+1);for(let[e,l]of t)a.includes(e)||(i(l)&&A.emit("tapped",l.startX,l.startY,e),A.emit("untap",l.x,l.y,e),t.delete(e))};s(p,"touchend",c),s(p,"touchcancel",c),s(l,"blur",()=>{for(let[e,a]of(r=!1,t))A.emit("untap",a.x,a.y,e),t.delete(e)})}if(e.keyboardEvents){let e=new Set;A.setvar("iskeydown",t=>"any"===(t=t.toLowerCase())?e.size>0:e.has("space"===t?" ":t)),s(l,"keydown",t=>{e.add(t.key.toLowerCase())}),s(l,"keyup",t=>{e.delete(t.key.toLowerCase())}),s(l,"blur",()=>e.clear())}e.pauseOnBlur&&(s(l,"blur",()=>{T=cancelAnimationFrame(T)}),s(l,"focus",()=>{T||(T=o(C))})),A.setfps(60),A.emit("init",A),E=performance.now(),T=o(C)}function C(e){u&&(T=o(C));let t=0,a=(e-E)/1e3;if(E=e,a>30*b);else for(x+=a,u||(x=b);x>=b;x-=b)A.emit("update",b*v),A.setvar("ELAPSED",A.ELAPSED+b*v),t++;(t||!u)&&(A.textalign("start","top"),A.emit("draw"))}function L(){let t=p.style;d&&(t.display||(t.display="block",t.margin="auto"),h=Math.min(l.innerWidth/A.WIDTH,l.innerHeight/A.HEIGHT),h=(e.pixelart?~~h:h)||1,t.width=A.WIDTH*h+"px",t.height=A.HEIGHT*h+"px"),(!e.antialias||e.pixelart)&&(g.imageSmoothingEnabled=!1,t.imageRendering="pixelated"),A.emit("resized",h),u||o(C)}function X(e,t,a,l,n){if(S[e])for(let i of S[e])i(t,a,l,n)}function z(e,t){let a=e(A,D,t);for(let e in a)A.setvar(e,a[e])}if(I){if(l.__litecanvas)throw"global litecanvas already instantiated";Object.assign(l,A),l.__litecanvas=A}return p="string"==typeof p?document.querySelector(p):p,A.setvar("CANVAS",p),g=p.getContext("2d"),s(p,"click",()=>l.focus()),p.style="",A.WIDTH||(A.WIDTH=l.innerWidth,A.HEIGHT=l.innerHeight),A.resize(A.WIDTH,A.HEIGHT,!1),p.parentNode||document.body.appendChild(p),"loading"===document.readyState?s(l,"DOMContentLoaded",()=>o(k)):o(k),A}})();
1
+ (()=>{var e=new AudioContext,t=(t=1,a=.05,l=220,n=0,i=0,r=.1,o=0,s=1,c=0,f=0,d=0,u=0,p=0,g=0,h=0,m=0,E=0,v=1,b=0,w=0,x=0)=>{let T=Math,y=2*T.PI,H=c*=500*y/44100/44100,I=l*=(1-a+2*a*T.random(a=[]))*y/44100,S=0,D=0,k=0,A=1,C=0,X=0,z=0,N=x<0?-1:1,P=y*N*x*2/44100,L=T.cos(P),O=T.sin,Y=O(P)/4,F=1+Y,W=-2*L/F,G=(1-Y)/F,M=(1+N*L)/2/F,R=-(N+L)/F,B=0,U=0,q=0,V=0;for(n=44100*n+9,b*=44100,i*=44100,r*=44100,E*=44100,f*=500*y/85766121e6,h*=y/44100,d*=y/44100,u*=44100,p=44100*p|0,t*=.3*(globalThis.zzfxV||1),N=n+b+i+r+E|0;k<N;a[k++]=z*t)++X%(100*m|0)||(z=o?1<o?2<o?3<o?O(S*S):T.max(T.min(T.tan(S),1),-1):1-(2*S/y%2+2)%2:1-4*T.abs(T.round(S/y)-S/y):O(S),z=(p?1-w+w*O(y*k/p):1)*(z<0?-1:1)*T.abs(z)**s*(k<n?k/n:k<n+b?1-(k-n)/b*(1-v):k<n+b+i?v:k<N-E?(N-k-E)/r*v:0),z=E?z/2+(E>k?0:(k<N-E?1:(N-k)/E)*a[k-E|0]/2/t):z,x&&(z=V=M*B+R*(B=U)+M*(U=z)-G*q-W*(q=V))),S+=(P=(l+=c+=f)*T.cos(h*D++))+P*g*O(k**5),A&&++A>u&&(l+=d,I+=d,A=0),!p||++C%p||(l=I,c=H,A=A||1);(t=e.createBuffer(1,N,44100)).getChannelData(0).set(a),(l=e.createBufferSource()).buffer=t,l.connect(e.destination),l.start()},a=["#111","#6a7799","#aec2c2","#FFF1E8","#e83b3b","#fabc20","#155fd9","#3cbcfc","#327345","#63c64d","#6c2c1f","#ac7c00"];globalThis.litecanvas=function(e={}){let l=globalThis,n=Math,i=n.PI,r=2*i,o=requestAnimationFrame,s=[],c=(e,t,a)=>{e.addEventListener(t,a,!1),s.push(()=>e.removeEventListener(t,a,!1))};e=Object.assign({width:null,height:null,autoscale:!0,pixelart:!1,antialias:!1,canvas:null,global:!0,loop:null,pauseOnBlur:!0,tapEvents:!0,keyboardEvents:!0,animate:!0},e);let f=!1,d=[],u=e.canvas||document.createElement("canvas"),p=e.animate,g=1,h,m=.5,E=1,v,b=1/60,w=0,x,T="sans-serif",y=18,H=Date.now(),I=e.global,S={init:null,update:null,draw:null,resized:null,tap:null,untap:null,tapping:null,tapped:null},D={settings:Object.assign({},e),colors:a},k={WIDTH:e.width,HEIGHT:e.height||e.width,CANVAS:null,ELAPSED:0,CENTERX:0,CENTERY:0,MOUSEX:-1,MOUSEY:-1,DEFAULT_SFX:[.5,,1675,,.06,.2,1,1.8,,,637,.06],TWO_PI:r,HALF_PI:i/2,lerp:(e,t,a)=>a*(t-e)+e,deg2rad:e=>i/180*e,rad2deg:e=>180/i*e,round:(e,t=0)=>{if(!t)return n.round(e);let a=10**t;return n.round(e*a)/a},clamp:(e,t,a)=>e<t?t:e>a?a:e,wrap:(e,t,a)=>e-(a-t)*n.floor((e-t)/(a-t)),map(e,t,a,l,n,i){let r=(e-t)/(a-t)*(n-l)+l;return i?k.clamp(r,l,n):r},norm:(e,t,a)=>k.map(e,t,a,0,1),rand:(e=0,t=1)=>(H=(1664525*H+0x3c6ef35f)%0x100000000)/0x100000000*(t-e)+e,randi:(e=0,t=1)=>n.floor(k.rand(e,t+1)),seed:e=>null==e?H:H=~~e,cls(e){null==e?h.clearRect(0,0,h.canvas.width,h.canvas.height):k.rectfill(0,0,h.canvas.width,h.canvas.height,e)},rect(e,t,a,l,n,i=null){h.beginPath(),h[i?"roundRect":"rect"](~~e-m,~~t-m,~~a+2*m,~~l+2*m,i),k.stroke(n)},rectfill(e,t,a,l,n,i=null){h.beginPath(),h[i?"roundRect":"rect"](~~e,~~t,~~a,~~l,i),k.fill(n)},circ(e,t,a,l){h.beginPath(),h.arc(~~e,~~t,~~a,0,r),k.stroke(l)},circfill(e,t,a,l){h.beginPath(),h.arc(~~e,~~t,~~a,0,r),k.fill(l)},line(e,t,a,l,n){h.beginPath();let i=.5*(0!==m&&~~e==~~a),r=.5*(0!==m&&~~t==~~l);h.moveTo(~~e+i,~~t+r),h.lineTo(~~a+i,~~l+r),k.stroke(n)},linewidth(e){h.lineWidth=~~e,m=.5*(~~e%2!=0)},linedash(e,t=0){h.setLineDash(e),h.lineDashOffset=t},text(e,t,a,l=3,n="normal"){h.font=`${n} ${y}px ${T}`,h.fillStyle=k.getcolor(l),h.fillText(a,~~e,~~t)},textfont(e){T=e},textsize(e){y=e},textalign(e,t){e&&(h.textAlign=e),t&&(h.textBaseline=t)},image(e,t,a){h.drawImage(a,~~e,~~t)},paint(e,t,a,l={}){let n=l.canvas||new OffscreenCanvas(1,1),i=l.scale||1,r=h;if(n.width=e*i,n.height=t*i,(h=n.getContext("2d")).scale(i,i),a.push){let e=0,t=0;for(let l of(h.imageSmoothingEnabled=!1,a)){for(let a of l)" "!==a&&"."!==a&&k.rectfill(e,t,1,1,parseInt(a,16)),e++;t++,e=0}}else a(h);return h=r,n},ctx:e=>(e&&(h=e),h),push:()=>h.save(),pop:()=>h.restore(),translate:(e,t)=>h.translate(~~e,~~t),scale:(e,t)=>h.scale(e,t||e),rotate:e=>h.rotate(e),alpha(e){h.globalAlpha=k.clamp(e,0,1)},path:e=>new Path2D(e),fill(e,t){h.fillStyle=k.getcolor(e),t?h.fill(t):h.fill()},stroke(e,t){h.strokeStyle=k.getcolor(e),t?h.stroke(t):h.stroke()},clip(e){h.clip(e)},sfx:(e,a=0,n=1)=>!(l.zzfxV<=0)&&(!navigator.userActivation||!!navigator.userActivation.hasBeenActive)&&(e=e||k.DEFAULT_SFX,(0!==a||1!==n)&&((e=e.slice())[0]=n*(e[0]||1),e[10]=~~e[10]+a),t.apply(0,e),e),volume(e){l.zzfxV=e},use(e,t={}){f?N(e,t):d.push([e,t])},listen:(e,t)=>(S[e]=S[e]||new Set,S[e].add(t),()=>S[e].delete(t)),emit(e,t,a,l,n){f&&(z("before:"+e,t,a,l,n),z(e,t,a,l,n),z("after:"+e,t,a,l,n))},getcolor:e=>a[~~e%a.length],setvar(e,t){k[e]=t,I&&(l[e]=t)},resize(e,t){k.setvar("WIDTH",u.width=e),k.setvar("HEIGHT",u.height=t),k.setvar("CENTERX",k.WIDTH/2),k.setvar("CENTERY",k.HEIGHT/2),X()},timescale(e){E=e},setfps(e){b=1/~~e},quit(){for(let e of(cancelAnimationFrame(x),k.emit("quit"),S=[],s))e();if(I){for(let e in k)delete l[e];delete l.ENGINE}}};for(let e of"PI,sin,cos,atan2,hypot,tan,abs,ceil,floor,trunc,min,max,pow,sqrt,sign,exp".split(","))k[e]=n[e];function A(){f=!0;let t=e.loop?e.loop:l;for(let e in S)t[e]&&k.listen(e,t[e]);for(let[e,t]of d)N(e,t);if(e.autoscale&&c(l,"resize",X),e.tapEvents){let e=(e,t)=>[(e-u.offsetLeft)/g,(t-u.offsetTop)/g],t=new Map,a=(e,a,l)=>{let n={x:a,y:l,startX:a,startY:l,ts:performance.now()};return t.set(e,n),n},n=(e,l,n)=>{let i=t.get(e)||a(e);i.x=l,i.y=n},i=e=>e&&performance.now()-e.ts<=300,r=e=>e.preventDefault(),o=!1;c(u,"mousedown",t=>{if(0===t.button){r(t);let[l,n]=e(t.pageX,t.pageY);k.emit("tap",l,n,0),a(0,l,n),o=!0}}),c(u,"mouseup",a=>{if(0===a.button){r(a);let l=t.get(0),[n,s]=e(a.pageX,a.pageY);i(l)&&k.emit("tapped",l.startX,l.startY,0),k.emit("untap",n,s,0),t.delete(0),o=!1}}),c(u,"mousemove",t=>{r(t);let[a,l]=e(t.pageX,t.pageY);k.setvar("MOUSEX",a),k.setvar("MOUSEY",l),o&&(k.emit("tapping",a,l,0),n(0,a,l))}),c(u,"touchstart",t=>{for(let l of(r(t),t.changedTouches)){let[t,n]=e(l.pageX,l.pageY);k.emit("tap",t,n,l.identifier+1),a(l.identifier+1,t,n)}}),c(u,"touchmove",t=>{for(let a of(r(t),t.changedTouches)){let[t,l]=e(a.pageX,a.pageY);k.emit("tapping",t,l,a.identifier+1),n(a.identifier+1,t,l)}});let s=e=>{r(e);let a=[];if(e.targetTouches.length>0)for(let t of e.targetTouches)a.push(t.identifier+1);for(let[e,l]of t)a.includes(e)||(i(l)&&k.emit("tapped",l.startX,l.startY,e),k.emit("untap",l.x,l.y,e),t.delete(e))};c(u,"touchend",s),c(u,"touchcancel",s),c(l,"blur",()=>{for(let[e,a]of(o=!1,t))k.emit("untap",a.x,a.y,e),t.delete(e)})}if(e.keyboardEvents){let e=e=>e.toLowerCase(),t=new Set,a=new Set,n=(t,a)=>a?t.has("space"===e(a)?" ":e(a)):t.size>0;c(l,"keydown",l=>{t.has(e(l.key))||(t.add(e(l.key)),a.add(e(l.key)))}),c(l,"keyup",a=>{t.delete(e(a.key))}),c(l,"blur",()=>t.clear()),k.listen("after:draw",()=>a.clear()),k.setvar("iskeydown",e=>n(t,e)),k.setvar("iskeypressed",e=>n(a,e))}e.pauseOnBlur&&(c(l,"blur",()=>{x=cancelAnimationFrame(x)}),c(l,"focus",()=>{x||(x=o(C))})),k.emit("init",k),v=performance.now(),x=o(C)}function C(e){p&&(x=o(C));let t=0,a=(e-v)/1e3;if(v=e,a>30*b);else for(w+=a,p||(w=b);w>=b;w-=b)k.emit("update",b*E),k.setvar("ELAPSED",k.ELAPSED+b*E),t++;(t||!p)&&(k.textalign("start","top"),k.emit("draw"))}function X(){let t=u.style;e.autoscale&&(t.display||(t.display="block",t.margin="auto"),g=n.min(l.innerWidth/k.WIDTH,l.innerHeight/k.HEIGHT),g=(e.pixelart?~~g:g)||1,t.width=k.WIDTH*g+"px",t.height=k.HEIGHT*g+"px"),(!e.antialias||e.pixelart)&&(h.imageSmoothingEnabled=!1,t.imageRendering="pixelated"),k.emit("resized",g),p||o(C)}function z(e,t,a,l,n){if(S[e])for(let i of S[e])i(t,a,l,n)}function N(e,t){let a=e(k,D,t);for(let e in a)k.setvar(e,a[e])}if(I){if(l.ENGINE)throw"two global litecanvas detected";Object.assign(l,k),l.ENGINE=k}return u="string"==typeof u?document.querySelector(u):u,k.setvar("CANVAS",u),h=u.getContext("2d"),c(u,"click",()=>l.focus()),u.style="",k.WIDTH||(k.WIDTH=l.innerWidth,k.HEIGHT=l.innerHeight),k.resize(k.WIDTH,k.HEIGHT,!1),u.parentNode||document.body.appendChild(u),"loading"===document.readyState?c(l,"DOMContentLoaded",()=>o(A)):o(A),k}})();
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "litecanvas",
3
- "version": "0.77.0",
3
+ "version": "0.78.0",
4
4
  "description": "Lightweight HTML5 canvas game engine suitable for small projects and creative coding. Inspired by PICO-8 and P5/Processing.",
5
5
  "license": "MIT",
6
6
  "author": "Luiz Bills <luizbills@pm.me>",
7
7
  "contributors": [],
8
8
  "devDependencies": {
9
- "@swc/core": "^1.11.8",
9
+ "@swc/core": "^1.11.9",
10
10
  "ava": "^6.2.0",
11
11
  "esbuild": "^0.25.1",
12
12
  "gzip-size": "^7.0.0",
package/src/index.js CHANGED
@@ -11,7 +11,8 @@ import './types.js'
11
11
  */
12
12
  export default function litecanvas(settings = {}) {
13
13
  const root = globalThis,
14
- PI = Math.PI,
14
+ math = Math,
15
+ PI = math.PI,
15
16
  TWO_PI = PI * 2,
16
17
  raf = requestAnimationFrame,
17
18
  /** @type {Function[]} */
@@ -50,8 +51,6 @@ export default function litecanvas(settings = {}) {
50
51
  /** @type {HTMLCanvasElement|string} _canvas */
51
52
  _canvas = settings.canvas || document.createElement('canvas'),
52
53
  /** @type {boolean} */
53
- _autoscale = settings.autoscale,
54
- /** @type {boolean} */
55
54
  _animated = settings.animate,
56
55
  /** @type {number} */
57
56
  _scale = 1,
@@ -64,7 +63,7 @@ export default function litecanvas(settings = {}) {
64
63
  /** @type {number} */
65
64
  _lastFrameTime,
66
65
  /** @type {number} */
67
- _deltaTime,
66
+ _deltaTime = 1 / 60,
68
67
  /** @type {number} */
69
68
  _accumulated = 0,
70
69
  /** @type {number} */
@@ -72,7 +71,7 @@ export default function litecanvas(settings = {}) {
72
71
  /** @type {string} */
73
72
  _fontFamily = 'sans-serif',
74
73
  /** @type {number} */
75
- _fontSize = 32,
74
+ _fontSize = 18,
76
75
  /** @type {number} */
77
76
  _rng_seed = Date.now(),
78
77
  /** @type {boolean} */
@@ -191,6 +190,26 @@ export default function litecanvas(settings = {}) {
191
190
  return (180 / PI) * rads
192
191
  },
193
192
 
193
+ /**
194
+ * Calculates the integer closest to a number and optional precision.
195
+ *
196
+ * @param {number} n number to round.
197
+ * @param {number} [precision] number of decimal digits to round to, default is 0.
198
+ * @returns {number} rounded number.
199
+ */
200
+ round: (n, precision = 0) => {
201
+ DEV: assert(isFinite(n), 'round: 1st param must be a number')
202
+ DEV: assert(
203
+ null === precision || (isFinite(precision) && precision >= 0),
204
+ 'round: 2nd param must be a positive number or zero'
205
+ )
206
+ if (!precision) {
207
+ return math.round(n)
208
+ }
209
+ const multiplier = 10 ** precision
210
+ return math.round(n * multiplier) / multiplier
211
+ },
212
+
194
213
  /**
195
214
  * Constrains a number between `min` and `max`.
196
215
  *
@@ -234,7 +253,7 @@ export default function litecanvas(settings = {}) {
234
253
  'randi: the 2nd param must be not equal to the 3rd param'
235
254
  )
236
255
 
237
- return value - (max - min) * Math.floor((value - min) / (max - min))
256
+ return value - (max - min) * math.floor((value - min) / (max - min))
238
257
  },
239
258
 
240
259
  /**
@@ -319,7 +338,7 @@ export default function litecanvas(settings = {}) {
319
338
  'randi: the 1st param must be less than the 2nd param'
320
339
  )
321
340
 
322
- return Math.floor(instance.rand(min, max + 1))
341
+ return math.floor(instance.rand(min, max + 1))
323
342
  },
324
343
 
325
344
  /**
@@ -342,12 +361,12 @@ export default function litecanvas(settings = {}) {
342
361
  /**
343
362
  * Clear the game screen with an optional color
344
363
  *
345
- * @param {number?} color The background color (index) or null (for transparent)
364
+ * @param {number?} color The background color (index) or null/undefined (for transparent)
346
365
  */
347
366
  cls(color) {
348
367
  DEV: assert(
349
368
  null == color || (isFinite(color) && color >= 0),
350
- 'cls: 1st param must be a positive number or zero or null'
369
+ 'cls: 1st param must be a positive number or zero or undefined'
351
370
  )
352
371
 
353
372
  if (null == color) {
@@ -955,58 +974,6 @@ export default function litecanvas(settings = {}) {
955
974
  root.zzfxV = value
956
975
  },
957
976
 
958
- /** UTILS API */
959
- /**
960
- * Check a collision between two rectangles
961
- *
962
- * @param {number} x1 first rectangle position X
963
- * @param {number} y1 first rectangle position Y
964
- * @param {number} w1 first rectangle width
965
- * @param {number} h1 first rectangle height
966
- * @param {number} x2 second rectangle position X
967
- * @param {number} y2 second rectangle position Y
968
- * @param {number} w2 second rectangle width
969
- * @param {number} h2 second rectangle height
970
- * @returns {boolean}
971
- */
972
- colrect: (x1, y1, w1, h1, x2, y2, w2, h2) => {
973
- DEV: assert(isFinite(x1), 'colrect: 1st param must be a number')
974
- DEV: assert(isFinite(y1), 'colrect: 2nd param must be a number')
975
- DEV: assert(isFinite(w1), 'colrect: 3rd param must be a number')
976
- DEV: assert(isFinite(h1), 'colrect: 4th param must be a number')
977
- DEV: assert(isFinite(x2), 'colrect: 5th param must be a number')
978
- DEV: assert(isFinite(y2), 'colrect: 6th param must be a number')
979
- DEV: assert(isFinite(w2), 'colrect: 7th param must be a number')
980
- DEV: assert(isFinite(h2), 'colrect: 8th param must be a number')
981
-
982
- return x1 < x2 + w2 && x1 + w1 > x2 && y1 < y2 + h2 && y1 + h1 > y2
983
- },
984
-
985
- /**
986
- * Check a collision between two circles
987
- *
988
- * @param {number} x1 first circle position X
989
- * @param {number} y1 first circle position Y
990
- * @param {number} r1 first circle position radius
991
- * @param {number} x2 second circle position X
992
- * @param {number} y2 second circle position Y
993
- * @param {number} r2 second circle position radius
994
- * @returns {boolean}
995
- */
996
- colcirc: (x1, y1, r1, x2, y2, r2) => {
997
- DEV: assert(isFinite(x1), 'colcirc: 1st param must be a number')
998
- DEV: assert(isFinite(y1), 'colcirc: 2nd param must be a number')
999
- DEV: assert(isFinite(r1), 'colcirc: 3rd param must be a number')
1000
- DEV: assert(isFinite(x2), 'colcirc: 4th param must be a number')
1001
- DEV: assert(isFinite(y2), 'colcirc: 5th param must be a number')
1002
- DEV: assert(isFinite(r2), 'colcirc: 6th param must be a number')
1003
-
1004
- return (
1005
- (x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1) <=
1006
- (r1 + r2) * (r1 + r2)
1007
- )
1008
- },
1009
-
1010
977
  /** PLUGINS API */
1011
978
  /**
1012
979
  * Prepares a plugin to be loaded
@@ -1168,25 +1135,34 @@ export default function litecanvas(settings = {}) {
1168
1135
  * Stops the litecanvas instance and remove all event listeners.
1169
1136
  */
1170
1137
  quit() {
1138
+ // stop the renderer
1139
+ cancelAnimationFrame(_rafid)
1140
+
1141
+ // emit "quit" event to manual clean ups
1171
1142
  instance.emit('quit')
1143
+
1144
+ // clear all engine events
1145
+ _events = []
1146
+
1147
+ // clear all browser events
1172
1148
  for (const removeListener of _browserEventListeners) {
1173
1149
  removeListener()
1174
1150
  }
1175
- cancelAnimationFrame(_rafid)
1176
- _events = false
1151
+
1152
+ // maybe clear global context
1177
1153
  if (_global) {
1178
1154
  for (const key in instance) {
1179
1155
  delete root[key]
1180
1156
  }
1181
- delete root.__litecanvas
1157
+ delete root.ENGINE
1182
1158
  }
1183
1159
  },
1184
1160
  }
1185
1161
 
1186
1162
  // prettier-ignore
1187
- for (const k of 'PI,sin,cos,atan2,hypot,tan,abs,ceil,round,floor,trunc,min,max,pow,sqrt,sign,exp'.split(',')) {
1163
+ for (const k of 'PI,sin,cos,atan2,hypot,tan,abs,ceil,floor,trunc,min,max,pow,sqrt,sign,exp'.split(',')) {
1188
1164
  // import native Math functions
1189
- instance[k] = Math[k]
1165
+ instance[k] = math[k]
1190
1166
  }
1191
1167
 
1192
1168
  function init() {
@@ -1204,7 +1180,7 @@ export default function litecanvas(settings = {}) {
1204
1180
  }
1205
1181
 
1206
1182
  // listen window resize event when "autoscale" is enabled
1207
- if (_autoscale) {
1183
+ if (settings.autoscale) {
1208
1184
  on(root, 'resize', onResize)
1209
1185
  }
1210
1186
 
@@ -1233,7 +1209,7 @@ export default function litecanvas(settings = {}) {
1233
1209
  tap.y = y
1234
1210
  },
1235
1211
  _checkTapped = (tap) =>
1236
- tap && performance.now() - tap.ts <= 200,
1212
+ tap && performance.now() - tap.ts <= 300,
1237
1213
  preventDefault = (ev) => ev.preventDefault()
1238
1214
 
1239
1215
  let _pressingMouse = false
@@ -1330,40 +1306,76 @@ export default function litecanvas(settings = {}) {
1330
1306
  }
1331
1307
 
1332
1308
  if (settings.keyboardEvents) {
1309
+ const toLowerCase = (s) => s.toLowerCase()
1310
+
1333
1311
  /** @type {Set<string>} */
1334
- const _keyDownList = new Set()
1312
+ const _keysDown = new Set()
1313
+
1314
+ /** @type {Set<string>} */
1315
+ const _keysPress = new Set()
1335
1316
 
1336
1317
  /**
1337
- * Checks if a which key is pressed on the keyboard.
1338
- * Note: use `iskeydown("any")` to check for any key pressed.
1339
- *
1340
- * @param {string} key
1318
+ * @param {Set<string>} keysSet
1319
+ * @param {string?} key
1341
1320
  * @returns {boolean}
1342
1321
  */
1343
- const iskeydown = (key) => {
1344
- DEV: assert(
1345
- 'string' === typeof key,
1346
- 'iskeydown: 1st param must be a string'
1347
- )
1348
-
1349
- key = key.toLowerCase()
1350
-
1351
- return 'any' === key
1352
- ? _keyDownList.size > 0
1353
- : _keyDownList.has('space' === key ? ' ' : key)
1322
+ const keyCheck = (keysSet, key) => {
1323
+ return !key
1324
+ ? keysSet.size > 0
1325
+ : keysSet.has(
1326
+ 'space' === toLowerCase(key) ? ' ' : toLowerCase(key)
1327
+ )
1354
1328
  }
1355
1329
 
1356
- instance.setvar('iskeydown', iskeydown)
1357
-
1358
1330
  on(root, 'keydown', (/** @type {KeyboardEvent} */ event) => {
1359
- _keyDownList.add(event.key.toLowerCase())
1331
+ if (!_keysDown.has(toLowerCase(event.key))) {
1332
+ _keysDown.add(toLowerCase(event.key))
1333
+ _keysPress.add(toLowerCase(event.key))
1334
+ }
1360
1335
  })
1361
1336
 
1362
1337
  on(root, 'keyup', (/** @type {KeyboardEvent} */ event) => {
1363
- _keyDownList.delete(event.key.toLowerCase())
1338
+ _keysDown.delete(toLowerCase(event.key))
1364
1339
  })
1365
1340
 
1366
- on(root, 'blur', () => _keyDownList.clear())
1341
+ on(root, 'blur', () => _keysDown.clear())
1342
+ instance.listen('after:draw', () => _keysPress.clear())
1343
+
1344
+ instance.setvar(
1345
+ 'iskeydown',
1346
+ /**
1347
+ * Checks if a which key is pressed (down) on the keyboard.
1348
+ * Note: use `iskeydown()` to check for any key.
1349
+ *
1350
+ * @param {string?} key
1351
+ * @returns {boolean}
1352
+ */
1353
+ (key) => {
1354
+ DEV: assert(
1355
+ null == key || 'string' === typeof key,
1356
+ 'iskeydown: 1st param must be a string or undefined'
1357
+ )
1358
+ return keyCheck(_keysDown, key)
1359
+ }
1360
+ )
1361
+
1362
+ instance.setvar(
1363
+ 'iskeypressed',
1364
+ /**
1365
+ * Checks if a which key just got pressed on the keyboard.
1366
+ * Note: use `iskeypressed()` to check for any key.
1367
+ *
1368
+ * @param {string?} key
1369
+ * @returns {boolean}
1370
+ */
1371
+ (key) => {
1372
+ DEV: assert(
1373
+ null == key || 'string' === typeof key,
1374
+ 'iskeypressed: 1st param must be a string or undefined'
1375
+ )
1376
+ return keyCheck(_keysPress, key)
1377
+ }
1378
+ )
1367
1379
  }
1368
1380
 
1369
1381
  // listen browser focus/blur events and pause the update/draw loop
@@ -1379,8 +1391,6 @@ export default function litecanvas(settings = {}) {
1379
1391
  })
1380
1392
  }
1381
1393
 
1382
- instance.setfps(60)
1383
-
1384
1394
  // start the game loop
1385
1395
  instance.emit('init', instance)
1386
1396
 
@@ -1402,7 +1412,7 @@ export default function litecanvas(settings = {}) {
1402
1412
  _lastFrameTime = now
1403
1413
 
1404
1414
  if (frameTime > _deltaTime * 30) {
1405
- console.log('skipping too long frame')
1415
+ console.warn('skipping too long frame')
1406
1416
  } else {
1407
1417
  _accumulated += frameTime
1408
1418
 
@@ -1472,13 +1482,13 @@ export default function litecanvas(settings = {}) {
1472
1482
  function onResize() {
1473
1483
  const styles = _canvas.style
1474
1484
 
1475
- if (_autoscale) {
1485
+ if (settings.autoscale) {
1476
1486
  if (!styles.display) {
1477
1487
  styles.display = 'block'
1478
1488
  styles.margin = 'auto'
1479
1489
  }
1480
1490
 
1481
- _scale = Math.min(
1491
+ _scale = math.min(
1482
1492
  root.innerWidth / instance.WIDTH,
1483
1493
  root.innerHeight / instance.HEIGHT
1484
1494
  )
@@ -1526,11 +1536,11 @@ export default function litecanvas(settings = {}) {
1526
1536
  }
1527
1537
 
1528
1538
  if (_global) {
1529
- if (root.__litecanvas) {
1530
- throw 'global litecanvas already instantiated'
1539
+ if (root.ENGINE) {
1540
+ throw 'two global litecanvas detected'
1531
1541
  }
1532
1542
  Object.assign(root, instance)
1533
- root.__litecanvas = instance
1543
+ root.ENGINE = instance
1534
1544
  }
1535
1545
 
1536
1546
  setupCanvas()