litecanvas 0.77.0 → 0.78.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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,27 @@
135
135
  DEV: assert(isFinite(rads), "rad2deg: 1st param must be a number");
136
136
  return 180 / PI * rads;
137
137
  },
138
+ /**
139
+ * Returns the rounded value of an number to optional precision (number of digits after the decimal point).
140
+ *
141
+ * Note: precision is optional but must be >= 0
142
+ *
143
+ * @param {number} n number to round.
144
+ * @param {number} [precision] number of decimal digits to round to, default is 0.
145
+ * @returns {number} rounded number.
146
+ */
147
+ round: (n, precision = 0) => {
148
+ DEV: assert(isFinite(n), "round: 1st param must be a number");
149
+ DEV: assert(
150
+ null === precision || isFinite(precision) && precision >= 0,
151
+ "round: 2nd param must be a positive number or zero"
152
+ );
153
+ if (!precision) {
154
+ return math.round(n);
155
+ }
156
+ const multiplier = 10 ** precision;
157
+ return math.round(n * multiplier) / multiplier;
158
+ },
138
159
  /**
139
160
  * Constrains a number between `min` and `max`.
140
161
  *
@@ -175,7 +196,7 @@
175
196
  max !== min,
176
197
  "randi: the 2nd param must be not equal to the 3rd param"
177
198
  );
178
- return value - (max - min) * Math.floor((value - min) / (max - min));
199
+ return value - (max - min) * math.floor((value - min) / (max - min));
179
200
  },
180
201
  /**
181
202
  * Re-maps a number from one range to another.
@@ -249,7 +270,7 @@
249
270
  max > min,
250
271
  "randi: the 1st param must be less than the 2nd param"
251
272
  );
252
- return Math.floor(instance.rand(min, max + 1));
273
+ return math.floor(instance.rand(min, max + 1));
253
274
  },
254
275
  /**
255
276
  * If a value is passed, initializes the random number generator with an explicit seed value.
@@ -269,12 +290,12 @@
269
290
  /**
270
291
  * Clear the game screen with an optional color
271
292
  *
272
- * @param {number?} color The background color (index) or null (for transparent)
293
+ * @param {number?} color The background color (index) or null/undefined (for transparent)
273
294
  */
274
295
  cls(color) {
275
296
  DEV: assert(
276
297
  null == color || isFinite(color) && color >= 0,
277
- "cls: 1st param must be a positive number or zero or null"
298
+ "cls: 1st param must be a positive number or zero or undefined"
278
299
  );
279
300
  if (null == color) {
280
301
  _ctx.clearRect(0, 0, _ctx.canvas.width, _ctx.canvas.height);
@@ -797,51 +818,6 @@
797
818
  DEV: assert(isFinite(value), "volume: 1st param must be a number");
798
819
  root.zzfxV = value;
799
820
  },
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
821
  /** PLUGINS API */
846
822
  /**
847
823
  * Prepares a plugin to be loaded
@@ -982,22 +958,22 @@
982
958
  * Stops the litecanvas instance and remove all event listeners.
983
959
  */
984
960
  quit() {
961
+ cancelAnimationFrame(_rafid);
985
962
  instance.emit("quit");
963
+ _events = [];
986
964
  for (const removeListener of _browserEventListeners) {
987
965
  removeListener();
988
966
  }
989
- cancelAnimationFrame(_rafid);
990
- _events = false;
991
967
  if (_global) {
992
968
  for (const key in instance) {
993
969
  delete root[key];
994
970
  }
995
- delete root.__litecanvas;
971
+ delete root.ENGINE;
996
972
  }
997
973
  }
998
974
  };
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];
975
+ for (const k of "PI,sin,cos,atan2,hypot,tan,abs,ceil,floor,trunc,min,max,pow,sqrt,sign,exp".split(",")) {
976
+ instance[k] = math[k];
1001
977
  }
1002
978
  function init() {
1003
979
  _initialized = true;
@@ -1008,7 +984,7 @@
1008
984
  for (const [callback, config] of _plugins) {
1009
985
  loadPlugin(callback, config);
1010
986
  }
1011
- if (_autoscale) {
987
+ if (settings.autoscale) {
1012
988
  on(root, "resize", onResize);
1013
989
  }
1014
990
  if (settings.tapEvents) {
@@ -1030,7 +1006,7 @@
1030
1006
  const tap = _taps.get(id) || _registerTap(id);
1031
1007
  tap.x = x;
1032
1008
  tap.y = y;
1033
- }, _checkTapped = (tap) => tap && performance.now() - tap.ts <= 200, preventDefault = (ev) => ev.preventDefault();
1009
+ }, _checkTapped = (tap) => tap && performance.now() - tap.ts <= 300, preventDefault = (ev) => ev.preventDefault();
1034
1010
  let _pressingMouse = false;
1035
1011
  on(_canvas, "mousedown", (ev) => {
1036
1012
  if (ev.button === 0) {
@@ -1109,23 +1085,59 @@
1109
1085
  });
1110
1086
  }
1111
1087
  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"
1088
+ const toLowerCase = (s) => s.toLowerCase();
1089
+ const _keysDown = /* @__PURE__ */ new Set();
1090
+ const _keysPress = /* @__PURE__ */ new Set();
1091
+ const keyCheck = (keysSet, key) => {
1092
+ return !key ? keysSet.size > 0 : keysSet.has(
1093
+ "space" === toLowerCase(key) ? " " : toLowerCase(key)
1117
1094
  );
1118
- key = key.toLowerCase();
1119
- return "any" === key ? _keyDownList.size > 0 : _keyDownList.has("space" === key ? " " : key);
1120
1095
  };
1121
- instance.setvar("iskeydown", iskeydown);
1122
1096
  on(root, "keydown", (event) => {
1123
- _keyDownList.add(event.key.toLowerCase());
1097
+ if (!_keysDown.has(toLowerCase(event.key))) {
1098
+ _keysDown.add(toLowerCase(event.key));
1099
+ _keysPress.add(toLowerCase(event.key));
1100
+ }
1124
1101
  });
1125
1102
  on(root, "keyup", (event) => {
1126
- _keyDownList.delete(event.key.toLowerCase());
1103
+ _keysDown.delete(toLowerCase(event.key));
1127
1104
  });
1128
- on(root, "blur", () => _keyDownList.clear());
1105
+ on(root, "blur", () => _keysDown.clear());
1106
+ instance.listen("after:draw", () => _keysPress.clear());
1107
+ instance.setvar(
1108
+ "iskeydown",
1109
+ /**
1110
+ * Checks if a which key is pressed (down) on the keyboard.
1111
+ * Note: use `iskeydown()` to check for any key.
1112
+ *
1113
+ * @param {string?} key
1114
+ * @returns {boolean}
1115
+ */
1116
+ (key) => {
1117
+ DEV: assert(
1118
+ null == key || "string" === typeof key,
1119
+ "iskeydown: 1st param must be a string or undefined"
1120
+ );
1121
+ return keyCheck(_keysDown, key);
1122
+ }
1123
+ );
1124
+ instance.setvar(
1125
+ "iskeypressed",
1126
+ /**
1127
+ * Checks if a which key just got pressed on the keyboard.
1128
+ * Note: use `iskeypressed()` to check for any key.
1129
+ *
1130
+ * @param {string?} key
1131
+ * @returns {boolean}
1132
+ */
1133
+ (key) => {
1134
+ DEV: assert(
1135
+ null == key || "string" === typeof key,
1136
+ "iskeypressed: 1st param must be a string or undefined"
1137
+ );
1138
+ return keyCheck(_keysPress, key);
1139
+ }
1140
+ );
1129
1141
  }
1130
1142
  if (settings.pauseOnBlur) {
1131
1143
  on(root, "blur", () => {
@@ -1137,7 +1149,6 @@
1137
1149
  }
1138
1150
  });
1139
1151
  }
1140
- instance.setfps(60);
1141
1152
  instance.emit("init", instance);
1142
1153
  _lastFrameTime = performance.now();
1143
1154
  _rafid = raf(drawFrame);
@@ -1149,7 +1160,7 @@
1149
1160
  let updated = 0, frameTime = (now - _lastFrameTime) / 1e3;
1150
1161
  _lastFrameTime = now;
1151
1162
  if (frameTime > _deltaTime * 30) {
1152
- console.log("skipping too long frame");
1163
+ console.warn("skipping too long frame");
1153
1164
  } else {
1154
1165
  _accumulated += frameTime;
1155
1166
  if (!_animated) {
@@ -1200,12 +1211,12 @@
1200
1211
  }
1201
1212
  function onResize() {
1202
1213
  const styles = _canvas.style;
1203
- if (_autoscale) {
1214
+ if (settings.autoscale) {
1204
1215
  if (!styles.display) {
1205
1216
  styles.display = "block";
1206
1217
  styles.margin = "auto";
1207
1218
  }
1208
- _scale = Math.min(
1219
+ _scale = math.min(
1209
1220
  root.innerWidth / instance.WIDTH,
1210
1221
  root.innerHeight / instance.HEIGHT
1211
1222
  );
@@ -1239,11 +1250,11 @@
1239
1250
  }
1240
1251
  }
1241
1252
  if (_global) {
1242
- if (root.__litecanvas) {
1243
- throw "global litecanvas already instantiated";
1253
+ if (root.ENGINE) {
1254
+ throw "two global litecanvas detected";
1244
1255
  }
1245
1256
  Object.assign(root, instance);
1246
- root.__litecanvas = instance;
1257
+ root.ENGINE = instance;
1247
1258
  }
1248
1259
  setupCanvas();
1249
1260
  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,22 @@
125
125
  rad2deg: (rads) => {
126
126
  return 180 / PI * rads;
127
127
  },
128
+ /**
129
+ * Returns the rounded value of an number to optional precision (number of digits after the decimal point).
130
+ *
131
+ * Note: precision is optional but must be >= 0
132
+ *
133
+ * @param {number} n number to round.
134
+ * @param {number} [precision] number of decimal digits to round to, default is 0.
135
+ * @returns {number} rounded number.
136
+ */
137
+ round: (n, precision = 0) => {
138
+ if (!precision) {
139
+ return math.round(n);
140
+ }
141
+ const multiplier = 10 ** precision;
142
+ return math.round(n * multiplier) / multiplier;
143
+ },
128
144
  /**
129
145
  * Constrains a number between `min` and `max`.
130
146
  *
@@ -147,7 +163,7 @@
147
163
  * @returns {number}
148
164
  */
149
165
  wrap: (value, min, max) => {
150
- return value - (max - min) * Math.floor((value - min) / (max - min));
166
+ return value - (max - min) * math.floor((value - min) / (max - min));
151
167
  },
152
168
  /**
153
169
  * Re-maps a number from one range to another.
@@ -201,7 +217,7 @@
201
217
  * @returns {number} the random number
202
218
  */
203
219
  randi: (min = 0, max = 1) => {
204
- return Math.floor(instance.rand(min, max + 1));
220
+ return math.floor(instance.rand(min, max + 1));
205
221
  },
206
222
  /**
207
223
  * If a value is passed, initializes the random number generator with an explicit seed value.
@@ -217,7 +233,7 @@
217
233
  /**
218
234
  * Clear the game screen with an optional color
219
235
  *
220
- * @param {number?} color The background color (index) or null (for transparent)
236
+ * @param {number?} color The background color (index) or null/undefined (for transparent)
221
237
  */
222
238
  cls(color) {
223
239
  if (null == color) {
@@ -574,37 +590,6 @@
574
590
  volume(value) {
575
591
  root.zzfxV = value;
576
592
  },
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
593
  /** PLUGINS API */
609
594
  /**
610
595
  * Prepares a plugin to be loaded
@@ -700,22 +685,22 @@
700
685
  * Stops the litecanvas instance and remove all event listeners.
701
686
  */
702
687
  quit() {
688
+ cancelAnimationFrame(_rafid);
703
689
  instance.emit("quit");
690
+ _events = [];
704
691
  for (const removeListener of _browserEventListeners) {
705
692
  removeListener();
706
693
  }
707
- cancelAnimationFrame(_rafid);
708
- _events = false;
709
694
  if (_global) {
710
695
  for (const key in instance) {
711
696
  delete root[key];
712
697
  }
713
- delete root.__litecanvas;
698
+ delete root.ENGINE;
714
699
  }
715
700
  }
716
701
  };
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];
702
+ for (const k of "PI,sin,cos,atan2,hypot,tan,abs,ceil,floor,trunc,min,max,pow,sqrt,sign,exp".split(",")) {
703
+ instance[k] = math[k];
719
704
  }
720
705
  function init() {
721
706
  _initialized = true;
@@ -726,7 +711,7 @@
726
711
  for (const [callback, config] of _plugins) {
727
712
  loadPlugin(callback, config);
728
713
  }
729
- if (_autoscale) {
714
+ if (settings.autoscale) {
730
715
  on(root, "resize", onResize);
731
716
  }
732
717
  if (settings.tapEvents) {
@@ -748,7 +733,7 @@
748
733
  const tap = _taps.get(id) || _registerTap(id);
749
734
  tap.x = x;
750
735
  tap.y = y;
751
- }, _checkTapped = (tap) => tap && performance.now() - tap.ts <= 200, preventDefault = (ev) => ev.preventDefault();
736
+ }, _checkTapped = (tap) => tap && performance.now() - tap.ts <= 300, preventDefault = (ev) => ev.preventDefault();
752
737
  let _pressingMouse = false;
753
738
  on(_canvas, "mousedown", (ev) => {
754
739
  if (ev.button === 0) {
@@ -827,19 +812,51 @@
827
812
  });
828
813
  }
829
814
  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);
815
+ const toLowerCase = (s) => s.toLowerCase();
816
+ const _keysDown = /* @__PURE__ */ new Set();
817
+ const _keysPress = /* @__PURE__ */ new Set();
818
+ const keyCheck = (keysSet, key) => {
819
+ return !key ? keysSet.size > 0 : keysSet.has(
820
+ "space" === toLowerCase(key) ? " " : toLowerCase(key)
821
+ );
834
822
  };
835
- instance.setvar("iskeydown", iskeydown);
836
823
  on(root, "keydown", (event) => {
837
- _keyDownList.add(event.key.toLowerCase());
824
+ if (!_keysDown.has(toLowerCase(event.key))) {
825
+ _keysDown.add(toLowerCase(event.key));
826
+ _keysPress.add(toLowerCase(event.key));
827
+ }
838
828
  });
839
829
  on(root, "keyup", (event) => {
840
- _keyDownList.delete(event.key.toLowerCase());
830
+ _keysDown.delete(toLowerCase(event.key));
841
831
  });
842
- on(root, "blur", () => _keyDownList.clear());
832
+ on(root, "blur", () => _keysDown.clear());
833
+ instance.listen("after:draw", () => _keysPress.clear());
834
+ instance.setvar(
835
+ "iskeydown",
836
+ /**
837
+ * Checks if a which key is pressed (down) on the keyboard.
838
+ * Note: use `iskeydown()` to check for any key.
839
+ *
840
+ * @param {string?} key
841
+ * @returns {boolean}
842
+ */
843
+ (key) => {
844
+ return keyCheck(_keysDown, key);
845
+ }
846
+ );
847
+ instance.setvar(
848
+ "iskeypressed",
849
+ /**
850
+ * Checks if a which key just got pressed on the keyboard.
851
+ * Note: use `iskeypressed()` to check for any key.
852
+ *
853
+ * @param {string?} key
854
+ * @returns {boolean}
855
+ */
856
+ (key) => {
857
+ return keyCheck(_keysPress, key);
858
+ }
859
+ );
843
860
  }
844
861
  if (settings.pauseOnBlur) {
845
862
  on(root, "blur", () => {
@@ -851,7 +868,6 @@
851
868
  }
852
869
  });
853
870
  }
854
- instance.setfps(60);
855
871
  instance.emit("init", instance);
856
872
  _lastFrameTime = performance.now();
857
873
  _rafid = raf(drawFrame);
@@ -897,12 +913,12 @@
897
913
  }
898
914
  function onResize() {
899
915
  const styles = _canvas.style;
900
- if (_autoscale) {
916
+ if (settings.autoscale) {
901
917
  if (!styles.display) {
902
918
  styles.display = "block";
903
919
  styles.margin = "auto";
904
920
  }
905
- _scale = Math.min(
921
+ _scale = math.min(
906
922
  root.innerWidth / instance.WIDTH,
907
923
  root.innerHeight / instance.HEIGHT
908
924
  );
@@ -932,11 +948,11 @@
932
948
  }
933
949
  }
934
950
  if (_global) {
935
- if (root.__litecanvas) {
936
- throw "global litecanvas already instantiated";
951
+ if (root.ENGINE) {
952
+ throw "two global litecanvas detected";
937
953
  }
938
954
  Object.assign(root, instance);
939
- root.__litecanvas = instance;
955
+ root.ENGINE = instance;
940
956
  }
941
957
  setupCanvas();
942
958
  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.1",
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,28 @@ export default function litecanvas(settings = {}) {
191
190
  return (180 / PI) * rads
192
191
  },
193
192
 
193
+ /**
194
+ * Returns the rounded value of an number to optional precision (number of digits after the decimal point).
195
+ *
196
+ * Note: precision is optional but must be >= 0
197
+ *
198
+ * @param {number} n number to round.
199
+ * @param {number} [precision] number of decimal digits to round to, default is 0.
200
+ * @returns {number} rounded number.
201
+ */
202
+ round: (n, precision = 0) => {
203
+ DEV: assert(isFinite(n), 'round: 1st param must be a number')
204
+ DEV: assert(
205
+ null === precision || (isFinite(precision) && precision >= 0),
206
+ 'round: 2nd param must be a positive number or zero'
207
+ )
208
+ if (!precision) {
209
+ return math.round(n)
210
+ }
211
+ const multiplier = 10 ** precision
212
+ return math.round(n * multiplier) / multiplier
213
+ },
214
+
194
215
  /**
195
216
  * Constrains a number between `min` and `max`.
196
217
  *
@@ -234,7 +255,7 @@ export default function litecanvas(settings = {}) {
234
255
  'randi: the 2nd param must be not equal to the 3rd param'
235
256
  )
236
257
 
237
- return value - (max - min) * Math.floor((value - min) / (max - min))
258
+ return value - (max - min) * math.floor((value - min) / (max - min))
238
259
  },
239
260
 
240
261
  /**
@@ -319,7 +340,7 @@ export default function litecanvas(settings = {}) {
319
340
  'randi: the 1st param must be less than the 2nd param'
320
341
  )
321
342
 
322
- return Math.floor(instance.rand(min, max + 1))
343
+ return math.floor(instance.rand(min, max + 1))
323
344
  },
324
345
 
325
346
  /**
@@ -342,12 +363,12 @@ export default function litecanvas(settings = {}) {
342
363
  /**
343
364
  * Clear the game screen with an optional color
344
365
  *
345
- * @param {number?} color The background color (index) or null (for transparent)
366
+ * @param {number?} color The background color (index) or null/undefined (for transparent)
346
367
  */
347
368
  cls(color) {
348
369
  DEV: assert(
349
370
  null == color || (isFinite(color) && color >= 0),
350
- 'cls: 1st param must be a positive number or zero or null'
371
+ 'cls: 1st param must be a positive number or zero or undefined'
351
372
  )
352
373
 
353
374
  if (null == color) {
@@ -955,58 +976,6 @@ export default function litecanvas(settings = {}) {
955
976
  root.zzfxV = value
956
977
  },
957
978
 
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
979
  /** PLUGINS API */
1011
980
  /**
1012
981
  * Prepares a plugin to be loaded
@@ -1168,25 +1137,34 @@ export default function litecanvas(settings = {}) {
1168
1137
  * Stops the litecanvas instance and remove all event listeners.
1169
1138
  */
1170
1139
  quit() {
1140
+ // stop the renderer
1141
+ cancelAnimationFrame(_rafid)
1142
+
1143
+ // emit "quit" event to manual clean ups
1171
1144
  instance.emit('quit')
1145
+
1146
+ // clear all engine events
1147
+ _events = []
1148
+
1149
+ // clear all browser events
1172
1150
  for (const removeListener of _browserEventListeners) {
1173
1151
  removeListener()
1174
1152
  }
1175
- cancelAnimationFrame(_rafid)
1176
- _events = false
1153
+
1154
+ // maybe clear global context
1177
1155
  if (_global) {
1178
1156
  for (const key in instance) {
1179
1157
  delete root[key]
1180
1158
  }
1181
- delete root.__litecanvas
1159
+ delete root.ENGINE
1182
1160
  }
1183
1161
  },
1184
1162
  }
1185
1163
 
1186
1164
  // 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(',')) {
1165
+ for (const k of 'PI,sin,cos,atan2,hypot,tan,abs,ceil,floor,trunc,min,max,pow,sqrt,sign,exp'.split(',')) {
1188
1166
  // import native Math functions
1189
- instance[k] = Math[k]
1167
+ instance[k] = math[k]
1190
1168
  }
1191
1169
 
1192
1170
  function init() {
@@ -1204,7 +1182,7 @@ export default function litecanvas(settings = {}) {
1204
1182
  }
1205
1183
 
1206
1184
  // listen window resize event when "autoscale" is enabled
1207
- if (_autoscale) {
1185
+ if (settings.autoscale) {
1208
1186
  on(root, 'resize', onResize)
1209
1187
  }
1210
1188
 
@@ -1233,7 +1211,7 @@ export default function litecanvas(settings = {}) {
1233
1211
  tap.y = y
1234
1212
  },
1235
1213
  _checkTapped = (tap) =>
1236
- tap && performance.now() - tap.ts <= 200,
1214
+ tap && performance.now() - tap.ts <= 300,
1237
1215
  preventDefault = (ev) => ev.preventDefault()
1238
1216
 
1239
1217
  let _pressingMouse = false
@@ -1330,40 +1308,76 @@ export default function litecanvas(settings = {}) {
1330
1308
  }
1331
1309
 
1332
1310
  if (settings.keyboardEvents) {
1311
+ const toLowerCase = (s) => s.toLowerCase()
1312
+
1333
1313
  /** @type {Set<string>} */
1334
- const _keyDownList = new Set()
1314
+ const _keysDown = new Set()
1315
+
1316
+ /** @type {Set<string>} */
1317
+ const _keysPress = new Set()
1335
1318
 
1336
1319
  /**
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
1320
+ * @param {Set<string>} keysSet
1321
+ * @param {string?} key
1341
1322
  * @returns {boolean}
1342
1323
  */
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)
1324
+ const keyCheck = (keysSet, key) => {
1325
+ return !key
1326
+ ? keysSet.size > 0
1327
+ : keysSet.has(
1328
+ 'space' === toLowerCase(key) ? ' ' : toLowerCase(key)
1329
+ )
1354
1330
  }
1355
1331
 
1356
- instance.setvar('iskeydown', iskeydown)
1357
-
1358
1332
  on(root, 'keydown', (/** @type {KeyboardEvent} */ event) => {
1359
- _keyDownList.add(event.key.toLowerCase())
1333
+ if (!_keysDown.has(toLowerCase(event.key))) {
1334
+ _keysDown.add(toLowerCase(event.key))
1335
+ _keysPress.add(toLowerCase(event.key))
1336
+ }
1360
1337
  })
1361
1338
 
1362
1339
  on(root, 'keyup', (/** @type {KeyboardEvent} */ event) => {
1363
- _keyDownList.delete(event.key.toLowerCase())
1340
+ _keysDown.delete(toLowerCase(event.key))
1364
1341
  })
1365
1342
 
1366
- on(root, 'blur', () => _keyDownList.clear())
1343
+ on(root, 'blur', () => _keysDown.clear())
1344
+ instance.listen('after:draw', () => _keysPress.clear())
1345
+
1346
+ instance.setvar(
1347
+ 'iskeydown',
1348
+ /**
1349
+ * Checks if a which key is pressed (down) on the keyboard.
1350
+ * Note: use `iskeydown()` to check for any key.
1351
+ *
1352
+ * @param {string?} key
1353
+ * @returns {boolean}
1354
+ */
1355
+ (key) => {
1356
+ DEV: assert(
1357
+ null == key || 'string' === typeof key,
1358
+ 'iskeydown: 1st param must be a string or undefined'
1359
+ )
1360
+ return keyCheck(_keysDown, key)
1361
+ }
1362
+ )
1363
+
1364
+ instance.setvar(
1365
+ 'iskeypressed',
1366
+ /**
1367
+ * Checks if a which key just got pressed on the keyboard.
1368
+ * Note: use `iskeypressed()` to check for any key.
1369
+ *
1370
+ * @param {string?} key
1371
+ * @returns {boolean}
1372
+ */
1373
+ (key) => {
1374
+ DEV: assert(
1375
+ null == key || 'string' === typeof key,
1376
+ 'iskeypressed: 1st param must be a string or undefined'
1377
+ )
1378
+ return keyCheck(_keysPress, key)
1379
+ }
1380
+ )
1367
1381
  }
1368
1382
 
1369
1383
  // listen browser focus/blur events and pause the update/draw loop
@@ -1379,8 +1393,6 @@ export default function litecanvas(settings = {}) {
1379
1393
  })
1380
1394
  }
1381
1395
 
1382
- instance.setfps(60)
1383
-
1384
1396
  // start the game loop
1385
1397
  instance.emit('init', instance)
1386
1398
 
@@ -1402,7 +1414,7 @@ export default function litecanvas(settings = {}) {
1402
1414
  _lastFrameTime = now
1403
1415
 
1404
1416
  if (frameTime > _deltaTime * 30) {
1405
- console.log('skipping too long frame')
1417
+ console.warn('skipping too long frame')
1406
1418
  } else {
1407
1419
  _accumulated += frameTime
1408
1420
 
@@ -1472,13 +1484,13 @@ export default function litecanvas(settings = {}) {
1472
1484
  function onResize() {
1473
1485
  const styles = _canvas.style
1474
1486
 
1475
- if (_autoscale) {
1487
+ if (settings.autoscale) {
1476
1488
  if (!styles.display) {
1477
1489
  styles.display = 'block'
1478
1490
  styles.margin = 'auto'
1479
1491
  }
1480
1492
 
1481
- _scale = Math.min(
1493
+ _scale = math.min(
1482
1494
  root.innerWidth / instance.WIDTH,
1483
1495
  root.innerHeight / instance.HEIGHT
1484
1496
  )
@@ -1526,11 +1538,11 @@ export default function litecanvas(settings = {}) {
1526
1538
  }
1527
1539
 
1528
1540
  if (_global) {
1529
- if (root.__litecanvas) {
1530
- throw 'global litecanvas already instantiated'
1541
+ if (root.ENGINE) {
1542
+ throw 'two global litecanvas detected'
1531
1543
  }
1532
1544
  Object.assign(root, instance)
1533
- root.__litecanvas = instance
1545
+ root.ENGINE = instance
1534
1546
  }
1535
1547
 
1536
1548
  setupCanvas()
package/types/index.d.ts CHANGED
@@ -72,6 +72,14 @@ declare global {
72
72
  * @returns the value in degrees
73
73
  */
74
74
  function rad2deg(rads: number): number
75
+ /**
76
+ * Returns the rounded value of an number to optional precision (number of digits after the decimal point).
77
+ *
78
+ * @param n number to round.
79
+ * @param [precision] number of decimal digits to round to, default is 0.
80
+ * @returns the rounded number.
81
+ */
82
+ function round(n: number, precision?: number): number
75
83
  /**
76
84
  * Constrains a number between `min` and `max`.
77
85
  *
package/types/types.d.ts CHANGED
@@ -61,6 +61,14 @@ type LitecanvasInstance = {
61
61
  * @returns the value in degrees
62
62
  */
63
63
  rad2deg(rads: number): number
64
+ /**
65
+ * Returns the rounded value of an number to optional precision (number of digits after the decimal point).
66
+ *
67
+ * @param n number to round.
68
+ * @param [precision] number of decimal digits to round to, default is 0.
69
+ * @returns the rounded number.
70
+ */
71
+ round(n: number, precision?: number): number
64
72
  /**
65
73
  * Constrains a number between `min` and `max`.
66
74
  *