litecanvas 0.98.4 → 0.100.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/README.md CHANGED
@@ -30,22 +30,24 @@ Litecanvas is a lightweight HTML5 canvas 2D engine suitable for small web games,
30
30
 
31
31
  ## Getting Started
32
32
 
33
- ### Installation
33
+ You can get started using our [online playground](https://litecanvas.github.io) without installing nothing.
34
34
 
35
- You can get started using our [online playground](https://litecanvas.github.io).
35
+ If you want to test locally, just use one of the installation options.
36
36
 
37
- Or installing our package via NPM:
37
+ ### HTML/CDN
38
38
 
39
- ```sh
40
- npm i litecanvas
41
- ```
42
-
43
- Or just create a HTML file and add a `<script>` tag with our CDN link:
39
+ Create a HTML file and add a `<script>` tag with our CDN link:
44
40
 
45
41
  ```html
46
42
  <script src="https://unpkg.com/litecanvas"></script>
47
43
  ```
48
44
 
45
+ ### Template
46
+
47
+ For those who are familiar with Node/NPM, we have a [basic template](https://github.com/litecanvas/template).
48
+
49
+ ## API
50
+
49
51
  ### Basic game structure
50
52
 
51
53
  ```js
@@ -104,7 +106,7 @@ Each time a Litecanvas' function ask for a color, you should use an of theses co
104
106
 
105
107
  ```js
106
108
  // example: draw a white rectangle
107
- color = 3
109
+ let color = 3
108
110
  rectfill(0, 0, 32, 32, color)
109
111
  ```
110
112
 
@@ -117,11 +119,13 @@ function draw() {
117
119
  // clear and fill the game screen with color #0 (black)
118
120
  cls(0)
119
121
 
120
- // print a red "Hello" text at x=0, y=0
121
- text(0, 0, 'Hello', 4)
122
+ // print a red text at x=0, y=0
123
+ text(0, 0, 'Hello!!!', 4)
122
124
  }
123
125
  ```
124
126
 
127
+ [Live Demo](https://litecanvas.js.org?c=eJwtTDEOwjAQ2%2FMKVwxNpErNwNqdbxzXK404EpQGWoT4O6HC8mBbtjUUYYpPWqwzZnpELiFFjJlW6%2FA2qOh7sAplUBwxBVWUWXChm2DhLBKxhjKDk6aMg4c9K%2FHV7VvWxfr6%2FP%2B55xALCFlGFNmqLNgG3%2BE1%2BL3zC231le1JVFPTNG2HozOfL6wIMU0%3D)
128
+
125
129
  ### Drawing shapes
126
130
 
127
131
  You can use the following functions to draw shapes:
@@ -139,18 +143,20 @@ litecanvas()
139
143
  function draw() {
140
144
  cls(0)
141
145
 
142
- // draw a color filled rectangle at x=0 and y=0
146
+ // draw a color filled rectangle at x=10 and y=20
143
147
  // with width=32 and height=32
144
148
  // and color=3 (white)
145
- rectfill(0, 0, 32, 32, 3)
149
+ rectfill(10, 20, 32, 32, 3)
146
150
 
147
- // draw a circle outline at x=64 and y=32
148
- // with radius=40
151
+ // draw a circle outline at x=64 and y=96
152
+ // with radius=50
149
153
  // and color=5 (yellow)
150
- circ(64, 32, 40, 5)
154
+ circ(64, 96, 50, 5)
151
155
  }
152
156
  ```
153
157
 
158
+ [Live Demo](https://litecanvas.js.org?c=eJxljk0KwyAQhfc5xSwNBGJ%2BFLrwMKImCoOCMU1D6d2rabIoGZhBfeN7H7pklPRPuZC6qqbVq%2BSCBx3lRmp4V5BL4UJoVsu5bQ8NJKiAIcLkEI2GaFSSfkYDMsFLdBSk17CLnl6%2FNpdsHjpZMfSHao2bbcq3a6U8Hq5iALLZTFYfUjEvOaSjDfS5h%2F7sO5SLKkOENaHzJwwfT5gH%2F4OJUrt1EYze8xmQ3SCG7QdQXAkfG3jwBlgGYHX1%2BQLFaFeI)
159
+
154
160
  ### Drawing sprites
155
161
 
156
162
  ```js
@@ -162,7 +168,7 @@ litecanvas({
162
168
  // each visible char is a pixel
163
169
  // numbers are colors
164
170
  // dots are transparent pixels
165
- const smile = `
171
+ let smile8x8 = `
166
172
  .555555.
167
173
  55555555
168
174
  55055055
@@ -176,26 +182,26 @@ function draw() {
176
182
  cls(0)
177
183
 
178
184
  spr(
179
- 0, 0, // position X Y
180
- 8, 8, // sprite Width and Height
181
- smile // the pixels
185
+ 0, 0, // position X Y
186
+ 8, 8, // the sprite Width and Height
187
+ smile8x8 // the sprite Pixels
182
188
  )
183
189
  }
184
190
  ```
185
191
 
192
+ [Live Demo](https://litecanvas.js.org?c=eJxtUMtqwzAQvOsr5mhDSNxCIBR67x%2B0vUWR1UigSEa7zoOQf%2B9KdkwPHfawuzOzSBM8W6PjWVNzV8DF9%2Bze8PK6U49Wqc0GtzRCBDDZaragIYuDRMgOxNnHIxWZ1cbh7MkfgoVxOsMTNAZ%2FtaHwcTwdbJZVFjqFlKurTzytOOtIg3SRJw%2BpYBl08nLuHXt5GrDeVqzrsJ0xD12t%2F4a%2Fsq603SITzMzz9F6pnzEa9imiz%2FrStLhXgQnUdJJI6SWDpjYF3aqU%2FGVI5KvvC98Lu1uVEnbKDZ8lX%2BjY48P6o%2BNFOP1UhOzsM4Gyb9XjF0Vyb7o%3D)
193
+
186
194
  ### Creating and drawing images
187
195
 
188
196
  ```js
189
197
  litecanvas()
190
198
 
191
- // lets create the flag of the Japan
192
- const japan = paint(
193
- 48, 32, // image width and height
199
+ // lets create flag of Japan
200
+ let japanFlag = paint(
201
+ 48, 32, // the image width and height
194
202
  function () {
195
- // the result of this drawings
196
- // goes to an offscreen canvas
197
- rectfill(0, 0, 48, 32, 3)
198
- circfill(24, 16, 8, 4)
203
+ cls(3) // white background
204
+ circfill(24, 16, 8, 4) // red circle
199
205
  }, {
200
206
  // you can scale your image
201
207
  // by default, scale=1
@@ -205,25 +211,32 @@ const japan = paint(
205
211
 
206
212
  function draw() {
207
213
  cls(0)
208
- // now the japan variable holds a image
209
- image(W/2 - japan.width/2, H/2 - japan.height/2, japan)
214
+
215
+ // draw the japanFlag image
216
+ image(
217
+ W/2 - japanFlag.width/2, // game screen center X
218
+ H/2 - japanFlag.height/2, // game screen center Y
219
+ japanFlag // the image
220
+ )
210
221
  }
211
222
  ```
212
223
 
213
- It's very useful when you need to draw something the same way every time. This way, you create an image of that drawing, working as a kind of cache.
224
+ [Live Demo](https://litecanvas.js.org?c=eJxtUctOwzAQvPsr5phIgdA0QgipV4T4AeC4dTaJi%2BtUjkNUof47toOcqmJPXs%2FMPma1cizJfNOY5UKUJTS7EdIyOUarqcPQ4o1OZIRHcAivl%2FC9w4mUcZmAj%2FqpwLYq4PWuZ6gjdYxZNa4HmQY9q653kdlORjo1GGQ5fuJPCKnHbJsH%2Bdz7gbAn%2BdXZYTLNSlFWtkrrrKoLbB4L%2BJZ1lFhuIqo5ki%2FFVWEPn4cJfkOMkjSHzC7zXXP2ZzTc0qRdsfB2mwTH%2FBn1Ulx4l9IOjaU57RF2ePDoX8mARTNWy9a28ZWlFu9lhbuVeB%2BdK72foVBHR%2FZDWGYDycaxxUdSvt4oF6fL5RT%2FKD%2BTch3rNq6PGOm5uPwCs6aXUg%3D%3D)
214
225
 
215
- You can also use the image() function to draw PNG/JPG images, but you'll need to load them first:
226
+ > Note: It's very useful when you need to draw something the same way every time. This way, you create an image of that drawing, working as a kind of cache.
227
+
228
+ You can also draw PNG/JPG image files, but you'll need to load them first:
216
229
 
217
230
  ```js
218
231
  litecanvas()
219
232
 
220
- let PngImage
233
+ let myImage
221
234
 
222
235
  function init() {
223
236
  // load a image from its URL
224
- const img = new Image()
237
+ let img = new Image()
225
238
  img.onload = () => {
226
- PngImage = img
239
+ myImage = img
227
240
  }
228
241
  img.src = 'https://litecanvas.js.org/icons/icon-128.png'
229
242
  }
@@ -231,17 +244,19 @@ function init() {
231
244
  function draw() {
232
245
  cls(0)
233
246
 
234
- if (!PngImage) {
247
+ if (!myImage) {
235
248
  // if not loaded, show this message
236
249
  text(10, 10, 'Loading image...')
237
250
  } else {
238
251
  // when loaded, draw the image file
239
- image(0, 0, PngImage)
252
+ image(0, 0, myImage)
240
253
  }
241
254
  }
242
255
  ```
243
256
 
244
- To help you load multiple assets (images, fonts, music, etc.), you can I recommend you the [Asset Loader Plugin](https://github.com/litecanvas/plugin-asset-loader).
257
+ [Live Demo](https://litecanvas.js.org?c=eJxVkMFOwzAMhu95CnNqKo1044SQyh1pJyQeIGrTNih1ptpQEOq742RtGVGSg%2F37s38Hz66x%2BGlJl0oFxzB%2Bv4y2d0p1H9iwjwgePesSfhTIqSoI0bZgwScZdFMcwTPB2%2Bs5CxLDjz3UgG6GzBJ0ykjURMzVNQiwfl6Z6axtJSOyHF32IpoaiRcD84WeqirsM5t3MnHqK99EpPzfnx4ezQX7Qi03DtrJzruDJpA%2BitlM70Dfra3Lm2nEpaQwcjbr2gPQEGfgwROMjigtaNOy%2B2J9Oh4gveIseo%2F9dTvGmOLqfQEXyP3vMA8Od34aUfhuW6sPfx1ySAtd7jbsuqLlFwWNf7A%3D)
258
+
259
+ If you need to load multiple assets (images, fonts, music, etc.), I recommend you the [Asset Loader Plugin](https://github.com/litecanvas/plugin-asset-loader).
245
260
 
246
261
  ### Keyboard
247
262
 
@@ -258,9 +273,7 @@ function update() {
258
273
  }
259
274
 
260
275
  // Returns the last key pressed in your keyboard.
261
- const key = lastkey()
262
-
263
- console.log(key)
276
+ let key = lastkey()
264
277
  }
265
278
  ```
266
279
 
@@ -324,7 +337,7 @@ You can find a complete list of everything litecanvas has to offer on our [cheat
324
337
 
325
338
  Try some demos in the playground:
326
339
 
327
- - [Bouncing Ball](https://litecanvas.js.org?c=eJxtkkFugzAQRfecYpaGOMFJW6lVQhddcQPWlm0iSy4gY0hRkrt3ADc4SRdI9nzm%2FzcMRjlo6lY7XVeQQa8E2TJG8YlpBHg3tdBuGM9Hy3t%2FtFzqrsX3X9%2BjyGinBK963pI4isquEpOZrrQjMZwDFx9QJDuap7t4v5h6hdEcq9fApWskd4pINzv9oW5%2BYJXdfPGWgHShPtzpw6wHKLOBT1%2F6g45AXtrTFL5qZFNQlyUoeVQtVnUJZCFb%2Ba%2FzCQVcLgHy2gsHYPM0dzhJBuvt%2Fp8q23y8TeVg%2BAyE4d9NkEr9UigUPifGpusjHc4FN778CWOYMJ4TxwXl3neyDXckLT%2F5TQvTEjYtVmgrSm1MiLjYUQ%2F78rDu8SfAUYbZzSrX2QrOYwUjfwF7%2FdPj)
340
+ - [Bouncing Ball](https://litecanvas.js.org?c=eJxtksFygyAQhu8%2BxR7VmGjSdqZNag89%2BQaeGcAMM1QdRBsm8d27Co3Y9MAM7M%2F%2B%2By2L5BraphNaNDXkMHAa7rMswRUlAeBZNlRoM%2B3PigxuqwgTfYf3n1%2BDQArNKakH0oVREFR9TWczUQsdRnD1XDDhejmW8SExxyI9jKfF1UrZJGB49Hz6lhHNQ6at1y%2Fs7gKb%2FO6MpxiY9nWz0o3VPRhr4Mov%2BV6GJy%2FpaQqfDbJxaKoKODvzDqOignAh27j3%2BYASbjcPeeuEd8hsNyucOIft%2FvRPNNu9vcxhr%2FkcqCRfrVc1cWNJoHR1Ikwa%2F9JhX3DnKx4wzIzxWHGaUOF8Z1t%2FRkyRbzdrKrswi6YuqFC0ElL6iItd4mCfovW4px%2BIrRjrprjuVY1fAyNY8gdFptV%2F)
328
341
  - [Scroller](https://litecanvas.js.org?c=eJxVUM1SgzAQvvMU68FpAhFDLVpH%2BxbOcOj0ECGUzATSIYsyOn13NwXRHpJNvv1%2BkrUGdam6D%2BUZj6J66Eo0rgPTGQTG4TsCaLX36qhhB6tC29K1GtCBXYQ3KyKN1C6onpSl034jcvEonsRWPItMiiw7ROd%2F9sOpUqiBVThFePMV%2FIv7OSu1ujtiQ52G4FxeAu528CAlxFDhHNiqkY0C1pJfuVe9%2Bvx9fGk9ozZA7XpgViMYEsoXKq9wnUZYkkwqgMBEYr4lTKZZbPiC0kLdh%2FRZ7Yd3jz0zArI%2FVumsCySax762zvUM4%2FyWbnMYP1yoqEcMn2dh4wvExsTEAYpluhXQJGuZeNORx4bHjZjfIKaUIDvTAH4AEIGEgw%3D%3D)
329
342
  - [3D projection](https://litecanvas.js.org?c=eJyNVcuS2jAQvPsrJofUykE8wykJySlVyRckuy4fZGwWgZEpWQSWFP%2BeGUlgizVLqgyyprul1mgkl9IUc6H%2BiJrFUbTYqbmRlQKppGEx%2FI0AanksYAa%2F4Cv8gG%2F4G8IUPmEfW4S3uloVTjSDBAMAyZjDCJ%2BUuy6%2Bj8MuRWw3pREqqUzdqPs0I4d2k3IYDiE5cHjhcEwdr4vWRjqA%2Fj2kRQgHuwm8jvc7gbTJVZHTYikg1HNJyR1Fp1byd9tcmILlxm2AI%2FWQNRiNIwwsKg2sLAxIkn7G5otP4qAs1LNZYqjXc2KAeaVqgzhl2LImG2G0PLDBYOBUiUxjS6UxdWWEc4i0za5kNoC2fjNrJOY0luPf5D5euJ5yh%2F%2FUzb%2FkCx02oqbiAvopyGGuxd4X8Lys2SimxG139ZIRt5Sq2MvcLNmEukYLVZeUc6rqCbdFbpGuVE%2Bv06vQDZMcmIQejGN4D1PeeI9DGlIQZS1u7EK3BBxkB%2BFkD84WV9Ne9VkkOGTc14QzSitmfrtFmozoOTMoMA4C2TUjuzA%2BhjP6PaEJfbZtuYl5VVLBCdT4omwwXe0tdg1kXpR1iDIvys4AInIBzE30bjZzjBjMElvA7YXvWleaPfzEy62UObiqL%2BoHWwpuVF3U56PYtdPWabDbKHC1mLhL6KJaOdUKVXYZ%2BNqo3Mmqdxt71F3kolw75Zrmc8p1WwlW16NUyjRZp%2FAB87NOk1XqCSffOmsYx%2FFQEp2xEy1XF2anFXGC3WvOtha5xEPg5vXsm7f5vKovAns1qla3TQyRUDby12KXn8e3%2FFwNE85y45tz7TE084aTp%2F93cisP95Pw6qPYdhLc2f4LGJpJDngukxf6O6Yk%2FgdWEko3)
330
343
  - [Rendering Benchmark](https://litecanvas.js.org?c=eJylVVtP2zAUfs%2BvsLpJSSCkSS9QurYTQkx7AAkB0x6qariJ21q4See40Az633dsp8G9UNDmB1Kf853j71yJ0iQTKEoTwVOWoS6K02g%2BJYnwI06wIBeMyJtjx%2FTRdq0V0KdJQvj3u6tLMLm3OqDtWQhOJwN9Mu6dTdN5ItqdanHXSowmnIy6la9YqbthEFR68KdTxW8gmhLR3IcAc%2Bnk4W1ETSFqexANhWjsQbQUorUHER5rIsd7MHVNpV5w6VRV3mT2UCZyRrqVWZpRQdOkjThhWNBHUil8RTh5xBmicbcSVXqdqr6vnNxbZeGGaZz7eDYjSeys6uVaG3r1nB%2BlLOVQQftTGIb2TsgQRw9jDuzj8xI8Go1sy2JEoDlnIEnIE%2Fpxc%2BmwNMKSvOspypmABgL1s7rJE6mmQIdg5mcE82hyjTmeZv6YQIvpJNkuenlBUFWvNHuisZi0QVZrvAonhI4n4KzRKpBLoEQF0Xlx9KOFpWLiq4vGroy1Qt%2B0Rpu3IcrItpaQNw3JZpyqYGaYwjgoaFjz1r79kpvtw2nAkV%2Fb25BjvEMeBFIeBFvymvRT25CfwKnDacJZkx8fn9TDsN4cDj8mf8OPfPcUzua7kudpq3W6zdP3pXxXvNKPIR%2Foz2tTZBFmpI3qqypCzkfzJJKNhGhCheMW4G%2FXt7%2BuLu4ubvyYZjOGc0fwOQF42Wx%2BRv%2FIIpk102XXoGoVQcMJGjGC1H6DN3TV1RqcFbqs6OkzzuER7Uy1rmuAYyqHoR966CgcKPkIxsORU0FBEXyBTwcZxiA4PHTXpkH6WQCYY5hVFx2YjYqOjJg2bPJtG93D%2B4z68cJDcT6QrEuVPMoTxFGX3iCsvhRQJ%2FBQ6A68f4EOyl9lSvtUPvyMgEMONBQVKLYuuVFBswblb8vIO6PJg%2Fl%2F6vec8PyWMBKJlDv3%2FfWN%2B%2FnZqMCyMrjXJaQjqBQ4Mssh78XSG8Ha%2FKkTCvtumLLYLpgujeaMOX4qmxNa65zBSkNiQoolokmzzAnc7fbDCZ3u67%2BNbPxHg3Ezk1AGq9Rzf4GOoJP8eGHI8kKWvwJluiS4gwLT%2F8oHkNmQxQt00IXBKMVLRFhGCkf5bkf5Lkf5pqNtVofm8PfMGdpN9t0h%2B1gMO5%2FVY7g7tvcH9QMxT%2FGYyKg96dRb23Ru2aN%2FASaYns8%3D)
package/dist/dist.dev.js CHANGED
@@ -32,7 +32,7 @@
32
32
  };
33
33
 
34
34
  // src/version.js
35
- var version = "0.98.4";
35
+ var version = "0.100.0";
36
36
 
37
37
  // src/index.js
38
38
  function litecanvas(settings = {}) {
@@ -50,7 +50,7 @@
50
50
  keyboardEvents: true
51
51
  };
52
52
  settings = Object.assign(defaults, settings);
53
- let _initialized = false, _canvas, _scale = 1, _ctx, _outline_fix = 0.5, _timeScale = 1, _lastFrameTime, _fpsInterval = 1e3 / 60, _accumulated, _rafid, _fontFamily = "sans-serif", _fontSize = 20, _rngSeed = Date.now(), _colorPalette = defaultPalette, _colorPaletteState = [], _defaultSound = [0.5, 0, 1750, , , 0.3, 1, , , , 600, 0.1], _coreEvents = "init,update,draw,tap,untap,tapping,tapped,resized", _mathFunctions = "PI,sin,cos,atan2,hypot,tan,abs,ceil,floor,trunc,min,max,pow,sqrt,sign,exp", _eventListeners = {};
53
+ let _initialized = false, _paused = true, _canvas, _scale = 1, _ctx, _outline_fix = 0.5, _timeScale = 1, _lastFrameTime, _fpsInterval = 1e3 / 60, _accumulated, _rafid, _fontFamily = "sans-serif", _fontSize = 20, _fontLineHeight = 1.2, _rngSeed = Date.now(), _colorPalette = defaultPalette, _colorPaletteState = [], _defaultSound = [0.5, 0, 1750, , , 0.3, 1, , , , 600, 0.1], _coreEvents = "init,update,draw,tap,untap,tapping,tapped,resized", _mathFunctions = "PI,sin,cos,atan2,hypot,tan,abs,ceil,floor,trunc,min,max,pow,sqrt,sign,exp", _eventListeners = {};
54
54
  const instance = {
55
55
  /** @type {number} */
56
56
  W: 0,
@@ -545,7 +545,7 @@
545
545
  },
546
546
  /** TEXT RENDERING API */
547
547
  /**
548
- * Draw text
548
+ * Draw text. You can use `\n` to break lines.
549
549
  *
550
550
  * @param {number} x
551
551
  * @param {number} y
@@ -566,7 +566,20 @@
566
566
  );
567
567
  _ctx.font = `${fontStyle} ${_fontSize}px ${_fontFamily}`;
568
568
  _ctx.fillStyle = getColor(color);
569
- _ctx.fillText(message, ~~x, ~~y);
569
+ const messages = ("" + message).split("\n");
570
+ for (let i = 0; i < messages.length; i++) {
571
+ _ctx.fillText(messages[i], ~~x, ~~y + _fontSize * _fontLineHeight * i);
572
+ }
573
+ },
574
+ /**
575
+ * Sets the height ratio of the text lines based on current text size.
576
+ *
577
+ * Default = `1.2`
578
+ *
579
+ * @param value
580
+ */
581
+ textgap(value) {
582
+ _fontLineHeight = value;
570
583
  },
571
584
  /**
572
585
  * Set the font family
@@ -995,7 +1008,7 @@
995
1008
  _fpsInterval = 1e3 / ~~value;
996
1009
  },
997
1010
  /**
998
- * Returns information about that engine instance.
1011
+ * Returns information about the engine instance.
999
1012
  *
1000
1013
  * @param {number|string} index
1001
1014
  * @returns {any}
@@ -1028,43 +1041,34 @@
1028
1041
  _rngSeed,
1029
1042
  // 10
1030
1043
  _fontSize,
1031
- // 11
1032
- _fontFamily
1044
+ // 11
1045
+ _fontFamily,
1046
+ // 12
1047
+ _colorPaletteState,
1048
+ // 13
1049
+ _fontLineHeight
1033
1050
  ];
1034
1051
  const data = { index, value: internals[index] };
1035
1052
  instance.emit("stat", data);
1036
1053
  return data.value;
1037
1054
  },
1038
- /**
1039
- * Stops the litecanvas instance and remove all event listeners.
1040
- */
1041
- quit() {
1042
- instance.pause();
1043
- instance.emit("quit");
1044
- _eventListeners = {};
1045
- for (const removeListener of _browserEventListeners) {
1046
- removeListener();
1047
- }
1048
- if (settings.global) {
1049
- for (const key in instance) {
1050
- delete root[key];
1051
- }
1052
- delete root.ENGINE;
1053
- }
1054
- _initialized = false;
1055
- },
1056
1055
  /**
1057
1056
  * Pauses the engine loop (update & draw).
1058
1057
  */
1059
1058
  pause() {
1059
+ _paused = true;
1060
1060
  cancelAnimationFrame(_rafid);
1061
- _rafid = 0;
1062
1061
  },
1063
1062
  /**
1064
1063
  * Resumes (if paused) the engine loop.
1065
1064
  */
1066
1065
  resume() {
1067
- if (_initialized && !_rafid) {
1066
+ DEV: assert(
1067
+ _initialized,
1068
+ '[litecanvas] resume() cannot be called before the "init" event and neither after the quit() function'
1069
+ );
1070
+ if (_initialized && _paused) {
1071
+ _paused = false;
1068
1072
  _accumulated = _fpsInterval;
1069
1073
  _lastFrameTime = Date.now();
1070
1074
  _rafid = raf(drawFrame);
@@ -1076,7 +1080,26 @@
1076
1080
  * @returns {boolean}
1077
1081
  */
1078
1082
  paused() {
1079
- return !_rafid;
1083
+ return _paused;
1084
+ },
1085
+ /**
1086
+ * Shutdown the litecanvas instance and remove all event listeners.
1087
+ */
1088
+ quit() {
1089
+ instance.emit("quit");
1090
+ instance.pause();
1091
+ _initialized = false;
1092
+ _eventListeners = {};
1093
+ for (const removeListener of _browserEventListeners) {
1094
+ removeListener();
1095
+ }
1096
+ if (settings.global) {
1097
+ for (const key in instance) {
1098
+ delete root[key];
1099
+ }
1100
+ delete root.ENGINE;
1101
+ }
1102
+ DEV: console.warn("[litecanvas] quit() terminated a Litecanvas instance.");
1080
1103
  }
1081
1104
  };
1082
1105
  for (const k of _mathFunctions.split(",")) {
@@ -1428,7 +1451,7 @@
1428
1451
  if ("loading" === document.readyState) {
1429
1452
  on(root, "DOMContentLoaded", () => raf(init));
1430
1453
  } else {
1431
- raf(init);
1454
+ _rafid = raf(init);
1432
1455
  }
1433
1456
  return instance;
1434
1457
  }
package/dist/dist.js CHANGED
@@ -42,7 +42,7 @@
42
42
  keyboardEvents: true
43
43
  };
44
44
  settings = Object.assign(defaults, settings);
45
- let _initialized = false, _canvas, _scale = 1, _ctx, _outline_fix = 0.5, _timeScale = 1, _lastFrameTime, _fpsInterval = 1e3 / 60, _accumulated, _rafid, _fontFamily = "sans-serif", _fontSize = 20, _rngSeed = Date.now(), _colorPalette = defaultPalette, _colorPaletteState = [], _defaultSound = [0.5, 0, 1750, , , 0.3, 1, , , , 600, 0.1], _coreEvents = "init,update,draw,tap,untap,tapping,tapped,resized", _mathFunctions = "PI,sin,cos,atan2,hypot,tan,abs,ceil,floor,trunc,min,max,pow,sqrt,sign,exp", _eventListeners = {};
45
+ let _initialized = false, _paused = true, _canvas, _scale = 1, _ctx, _outline_fix = 0.5, _timeScale = 1, _lastFrameTime, _fpsInterval = 1e3 / 60, _accumulated, _rafid, _fontFamily = "sans-serif", _fontSize = 20, _fontLineHeight = 1.2, _rngSeed = Date.now(), _colorPalette = defaultPalette, _colorPaletteState = [], _defaultSound = [0.5, 0, 1750, , , 0.3, 1, , , , 600, 0.1], _coreEvents = "init,update,draw,tap,untap,tapping,tapped,resized", _mathFunctions = "PI,sin,cos,atan2,hypot,tan,abs,ceil,floor,trunc,min,max,pow,sqrt,sign,exp", _eventListeners = {};
46
46
  const instance = {
47
47
  /** @type {number} */
48
48
  W: 0,
@@ -363,7 +363,7 @@
363
363
  },
364
364
  /** TEXT RENDERING API */
365
365
  /**
366
- * Draw text
366
+ * Draw text. You can use `\n` to break lines.
367
367
  *
368
368
  * @param {number} x
369
369
  * @param {number} y
@@ -374,7 +374,20 @@
374
374
  text(x, y, message, color = 3, fontStyle = "normal") {
375
375
  _ctx.font = `${fontStyle} ${_fontSize}px ${_fontFamily}`;
376
376
  _ctx.fillStyle = getColor(color);
377
- _ctx.fillText(message, ~~x, ~~y);
377
+ const messages = ("" + message).split("\n");
378
+ for (let i = 0; i < messages.length; i++) {
379
+ _ctx.fillText(messages[i], ~~x, ~~y + _fontSize * _fontLineHeight * i);
380
+ }
381
+ },
382
+ /**
383
+ * Sets the height ratio of the text lines based on current text size.
384
+ *
385
+ * Default = `1.2`
386
+ *
387
+ * @param value
388
+ */
389
+ textgap(value) {
390
+ _fontLineHeight = value;
378
391
  },
379
392
  /**
380
393
  * Set the font family
@@ -687,7 +700,7 @@
687
700
  _fpsInterval = 1e3 / ~~value;
688
701
  },
689
702
  /**
690
- * Returns information about that engine instance.
703
+ * Returns information about the engine instance.
691
704
  *
692
705
  * @param {number|string} index
693
706
  * @returns {any}
@@ -716,43 +729,30 @@
716
729
  _rngSeed,
717
730
  // 10
718
731
  _fontSize,
719
- // 11
720
- _fontFamily
732
+ // 11
733
+ _fontFamily,
734
+ // 12
735
+ _colorPaletteState,
736
+ // 13
737
+ _fontLineHeight
721
738
  ];
722
739
  const data = { index, value: internals[index] };
723
740
  instance.emit("stat", data);
724
741
  return data.value;
725
742
  },
726
- /**
727
- * Stops the litecanvas instance and remove all event listeners.
728
- */
729
- quit() {
730
- instance.pause();
731
- instance.emit("quit");
732
- _eventListeners = {};
733
- for (const removeListener of _browserEventListeners) {
734
- removeListener();
735
- }
736
- if (settings.global) {
737
- for (const key in instance) {
738
- delete root[key];
739
- }
740
- delete root.ENGINE;
741
- }
742
- _initialized = false;
743
- },
744
743
  /**
745
744
  * Pauses the engine loop (update & draw).
746
745
  */
747
746
  pause() {
747
+ _paused = true;
748
748
  cancelAnimationFrame(_rafid);
749
- _rafid = 0;
750
749
  },
751
750
  /**
752
751
  * Resumes (if paused) the engine loop.
753
752
  */
754
753
  resume() {
755
- if (_initialized && !_rafid) {
754
+ if (_initialized && _paused) {
755
+ _paused = false;
756
756
  _accumulated = _fpsInterval;
757
757
  _lastFrameTime = Date.now();
758
758
  _rafid = raf(drawFrame);
@@ -764,7 +764,25 @@
764
764
  * @returns {boolean}
765
765
  */
766
766
  paused() {
767
- return !_rafid;
767
+ return _paused;
768
+ },
769
+ /**
770
+ * Shutdown the litecanvas instance and remove all event listeners.
771
+ */
772
+ quit() {
773
+ instance.emit("quit");
774
+ instance.pause();
775
+ _initialized = false;
776
+ _eventListeners = {};
777
+ for (const removeListener of _browserEventListeners) {
778
+ removeListener();
779
+ }
780
+ if (settings.global) {
781
+ for (const key in instance) {
782
+ delete root[key];
783
+ }
784
+ delete root.ENGINE;
785
+ }
768
786
  }
769
787
  };
770
788
  for (const k of _mathFunctions.split(",")) {
@@ -1076,7 +1094,7 @@
1076
1094
  if ("loading" === document.readyState) {
1077
1095
  on(root, "DOMContentLoaded", () => raf(init));
1078
1096
  } else {
1079
- raf(init);
1097
+ _rafid = raf(init);
1080
1098
  }
1081
1099
  return instance;
1082
1100
  }
package/dist/dist.min.js CHANGED
@@ -1 +1 @@
1
- (()=>{var e=["#111","#6a7799","#aec2c2","#FFF1E8","#e83b3b","#fabc20","#155fd9","#3cbcfc","#327345","#63c64d","#6c2c1f","#ac7c00"];globalThis.litecanvas=function(t={}){let a=window,l=Math,n=2*l.PI,i=requestAnimationFrame,o=[],r=(e,t,a)=>{e.addEventListener(t,a,!1),o.push(()=>e.removeEventListener(t,a,!1))},s=e=>e.toLowerCase(),c=e=>e.preventDefault(),f=e=>e.beginPath(),d=(e=>{let t=new AudioContext;return e.zzfxV=1,(a=1,l=.05,n=220,i=0,o=0,r=.1,s=0,c=1,f=0,d=0,u=0,p=0,h=0,m=0,g=0,v=0,w=0,x=1,y=0,b=0,k=0)=>{let E=Math,z=2*E.PI,T=f*=500*z/44100/44100,I=n*=(1-l+2*l*E.random(l=[]))*z/44100,D=0,S=0,A=0,M=1,C=0,L=0,N=0,P=k<0?-1:1,F=z*P*k*2/44100,q=E.cos(F),B=E.sin,H=B(F)/4,O=1+H,V=-2*q/O,W=(1-H)/O,R=(1+P*q)/2/O,G=-(P+q)/O,X=0,Y=0,$=0,j=0;for(i=44100*i+9,y*=44100,o*=44100,r*=44100,w*=44100,d*=500*z/85766121e6,g*=z/44100,u*=z/44100,p*=44100,h=44100*h|0,a*=.3*e.zzfxV,P=i+y+o+r+w|0;A<P;l[A++]=N*a)++L%(100*v|0)||(N=s?1<s?2<s?3<s?B(D*D):E.max(E.min(E.tan(D),1),-1):1-(2*D/z%2+2)%2:1-4*E.abs(E.round(D/z)-D/z):B(D),N=(h?1-b+b*B(z*A/h):1)*(N<0?-1:1)*E.abs(N)**c*(A<i?A/i:A<i+y?1-(A-i)/y*(1-x):A<i+y+o?x:A<P-w?(P-A-w)/r*x:0),N=w?N/2+(w>A?0:(A<P-w?1:(P-A)/w)*l[A-w|0]/2/a):N,k&&(N=j=R*X+G*(X=Y)+R*(Y=N)-W*$-V*($=j))),D+=(F=(n+=f+=d)*E.cos(g*S++))+F*m*B(A**5),M&&++M>p&&(n+=u,I+=u,M=0),!h||++C%h||(n=I,f=T,M=M||1);(a=t.createBuffer(1,P,44100)).getChannelData(0).set(l),(n=t.createBufferSource()).buffer=a,n.connect(t.destination),n.start()}})(a);t=Object.assign({width:null,height:null,autoscale:!0,canvas:null,global:!0,loop:null,tapEvents:!0,keyboardEvents:!0},t);let u=!1,p,h=1,m,g=.5,v=1,w,x=1e3/60,y,b,k="sans-serif",E=20,z=Date.now(),T=e,I=[],D=[.5,0,1750,,,.3,1,,,,600,.1],S={},A={W:0,H:0,T:0,MX:-1,MY:-1,TWO_PI:n,HALF_PI:n/4,lerp:(e,t,a)=>a*(t-e)+e,deg2rad:e=>l.PI/180*e,rad2deg:e=>180/l.PI*e,round:(e,t=0)=>{if(!t)return l.round(e);let a=10**t;return l.round(e*a)/a},clamp:(e,t,a)=>e<t?t:e>a?a:e,wrap:(e,t,a)=>e-(a-t)*l.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),wave:(e,t,a,l=Math.sin)=>e+(l(a)+1)/2*(t-e),rand:(e=0,t=1)=>(z=(1664525*z+0x3c6ef35f)%0x100000000)/0x100000000*(t-e)+e,randi:(e=0,t=1)=>l.floor(A.rand(e,t+1)),rseed(e){z=~~e},cls(e){null==e?m.clearRect(0,0,m.canvas.width,m.canvas.height):A.rectfill(0,0,m.canvas.width,m.canvas.height,e)},rect(e,t,a,l,n,i){f(m),m[i?"roundRect":"rect"](~~e-g,~~t-g,~~a+2*g,~~l+2*g,i),A.stroke(n)},rectfill(e,t,a,l,n,i){f(m),m[i?"roundRect":"rect"](~~e,~~t,~~a,~~l,i),A.fill(n)},circ(e,t,a,l){f(m),m.arc(~~e,~~t,~~a,0,n),A.stroke(l)},circfill(e,t,a,l){f(m),m.arc(~~e,~~t,~~a,0,n),A.fill(l)},oval(e,t,a,l,i){f(m),m.ellipse(~~e,~~t,~~a,~~l,0,0,n),A.stroke(i)},ovalfill(e,t,a,l,i){f(m),m.ellipse(~~e,~~t,~~a,~~l,0,0,n),A.fill(i)},line(e,t,a,l,n){f(m);let i=.5*(0!==g&&~~e==~~a),o=.5*(0!==g&&~~t==~~l);m.moveTo(~~e+i,~~t+o),m.lineTo(~~a+i,~~l+o),A.stroke(n)},linewidth(e){m.lineWidth=~~e,g=.5*(0!=~~e%2)},linedash(e,t=0){m.setLineDash(e),m.lineDashOffset=t},text(e,t,a,l=3,n="normal"){m.font=`${n} ${E}px ${k}`,m.fillStyle=P(l),m.fillText(a,~~e,~~t)},textfont(e){k=e},textsize(e){E=e},textalign(e,t){e&&(m.textAlign=e),t&&(m.textBaseline=t)},image(e,t,a){m.drawImage(a,~~e,~~t)},spr(e,t,a,l,n){let i=n.replace(/\s/g,"");for(let n=0;n<a;n++)for(let o=0;o<l;o++){let l=i[a*o+n]||".";"."!==l&&A.rectfill(e+n,t+o,1,1,parseInt(l,36)||0)}},paint(e,t,a,l={}){let n=l.canvas||new OffscreenCanvas(1,1),i=l.scale||1,o=m;return n.width=e*i,n.height=t*i,(m=n.getContext("2d")).scale(i,i),a(m),m=o,n.transferToImageBitmap()},ctx:e=>(e&&(m=e),m),push(){m.save()},pop(){m.restore()},translate(e,t){m.translate(~~e,~~t)},scale(e,t){m.scale(e,t||e)},rotate(e){m.rotate(e)},alpha(e){m.globalAlpha=A.clamp(e,0,1)},fill(e){m.fillStyle=P(e),m.fill()},stroke(e){m.strokeStyle=P(e),m.stroke()},clip(e){f(m),e(m),m.clip()},sfx:(e,t=0,l=1)=>!!a.zzfxV&&(!navigator.userActivation||!!navigator.userActivation.hasBeenActive)&&(e=e||D,(0!==t||1!==l)&&((e=e.slice())[0]=l*(e[0]||1),e[10]=~~e[10]+t),d.apply(0,e),e),volume(e){a.zzfxV=e},canvas:()=>p,use(e,t={}){var a=e,l=t;let n=a(A,l);for(let e in n)A.def(e,n[e])},listen:(e,t)=>(S[e=s(e)]=S[e]||new Set,S[e].add(t),()=>S&&S[e].delete(t)),emit(e,t,a,l,n){u&&(N("before:"+(e=s(e)),t,a,l,n),N(e,t,a,l,n),N("after:"+e,t,a,l,n))},pal(t=e){T=t,I=[]},palc(e,t){null==e?I=[]:I[e]=t},def(e,l){A[e]=l,t.global&&(a[e]=l)},timescale(e){v=e},framerate(e){x=1e3/~~e},stat(e){let l={index:e,value:[t,u,x/1e3,h,S,T,D,v,a.zzfxV,z,E,k][e]};return A.emit("stat",l),l.value},quit(){for(let e of(A.pause(),A.emit("quit"),S={},o))e();if(t.global){for(let e in A)delete a[e];delete a.ENGINE}u=!1},pause(){cancelAnimationFrame(b),b=0},resume(){u&&!b&&(y=x,w=Date.now(),b=i(C))},paused:()=>!b};for(let e of"PI,sin,cos,atan2,hypot,tan,abs,ceil,floor,trunc,min,max,pow,sqrt,sign,exp".split(","))A[e]=l[e];function M(){let e=t.loop?t.loop:a;for(let t of"init,update,draw,tap,untap,tapping,tapped,resized".split(","))e[t]&&A.listen(t,e[t]);if(t.autoscale&&r(a,"resize",L),t.tapEvents){let e=e=>[(e.pageX-p.offsetLeft)/h,(e.pageY-p.offsetTop)/h],t=new Map,l=(e,a,l)=>{let n={x:a,y:l,xi:a,yi:l,t:Date.now()};return t.set(e,n),n},n=(e,a,n)=>{let i=t.get(e)||l(e);i.x=a,i.y=n},i=e=>e&&Date.now()-e.t<=300,o=!1;r(p,"mousedown",t=>{if(0===t.button){c(t);let[a,n]=e(t);A.emit("tap",a,n,0),l(0,a,n),o=!0}}),r(p,"mouseup",a=>{if(0===a.button){c(a);let l=t.get(0),[n,r]=e(a);i(l)&&A.emit("tapped",l.xi,l.yi,0),A.emit("untap",n,r,0),t.delete(0),o=!1}}),r(a,"mousemove",t=>{c(t);let[a,l]=e(t);A.def("MX",a),A.def("MY",l),o&&(A.emit("tapping",a,l,0),n(0,a,l))}),r(p,"touchstart",t=>{for(let a of(c(t),t.changedTouches)){let[t,n]=e(a);A.emit("tap",t,n,a.identifier+1),l(a.identifier+1,t,n)}}),r(p,"touchmove",t=>{for(let a of(c(t),t.changedTouches)){let[t,l]=e(a);A.emit("tapping",t,l,a.identifier+1),n(a.identifier+1,t,l)}});let s=e=>{c(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.xi,l.yi,e),A.emit("untap",l.x,l.y,e),t.delete(e))};r(p,"touchend",s),r(p,"touchcancel",s),r(a,"blur",()=>{for(let[e,a]of(o=!1,t))A.emit("untap",a.x,a.y,e),t.delete(e)})}if(t.keyboardEvents){let e=new Set,t=new Set,l=(e,t="")=>(t=s(t))?e.has("space"===t?" ":t):e.size>0,n="";r(a,"keydown",a=>{let l=s(a.key);e.has(l)||(e.add(l),t.add(l),n=" "===l?"space":l)}),r(a,"keyup",t=>{e.delete(s(t.key))}),r(a,"blur",()=>e.clear()),A.listen("after:update",()=>t.clear()),A.def("iskeydown",t=>l(e,t)),A.def("iskeypressed",e=>l(t,e)),A.def("lastkey",()=>n)}u=!0,A.emit("init",A),A.resume()}function C(){b=i(C);let e=Date.now(),t=0,a=e-w;for(w=e,y+=a<100?a:x;y>=x;){t++,y-=x;let e=x/1e3*v;A.emit("update",e,t),A.def("T",A.T+e)}t&&(A.emit("draw",m),t>1&&(y=0))}function L(){let e=t.width>0?t.width:innerWidth,a=t.width>0?t.height||t.width:innerHeight;if(A.def("W",e),A.def("H",a),p.width=e,p.height=a,t.autoscale){let n=+t.autoscale;p.style.display||(p.style.display="block",p.style.margin="auto"),h=l.min(innerWidth/e,innerHeight/a),h=n>1&&h>n?n:h,p.style.width=e*h+"px",p.style.height=a*h+"px"}m.imageSmoothingEnabled=!1,A.textalign("start","top"),A.emit("resized",h)}function N(e,t,a,l,n){if(S[e])for(let i of S[e])i(t,a,l,n)}function P(e){return T[~~(I[e]??e)%T.length]}if(t.global){if(a.ENGINE)throw Error("only one global litecanvas is allowed");Object.assign(a,A),a.ENGINE=A}return m=(p=(p="string"==typeof t.canvas?document.querySelector(t.canvas):t.canvas)||document.createElement("canvas")).getContext("2d"),r(p,"click",()=>focus()),L(),p.parentNode||document.body.appendChild(p),p.style.imageRendering="pixelated",p.oncontextmenu=()=>!1,"loading"===document.readyState?r(a,"DOMContentLoaded",()=>i(M)):i(M),A}})();
1
+ (()=>{var e=["#111","#6a7799","#aec2c2","#FFF1E8","#e83b3b","#fabc20","#155fd9","#3cbcfc","#327345","#63c64d","#6c2c1f","#ac7c00"];globalThis.litecanvas=function(t={}){let a,l=window,n=Math,i=2*n.PI,o=requestAnimationFrame,r=[],s=(e,t,a)=>{e.addEventListener(t,a,!1),r.push(()=>e.removeEventListener(t,a,!1))},f=(a=new AudioContext,l.zzfxV=1,(e=1,t=.05,n=220,i=0,o=0,r=.1,s=0,f=1,c=0,d=0,u=0,p=0,h=0,g=0,m=0,v=0,w=0,x=1,b=0,y=0,k=0)=>{let E=Math,z=2*E.PI,D=c*=500*z/44100/44100,P=n*=(1-t+2*t*E.random(t=[]))*z/44100,T=0,C=0,I=0,L=1,S=0,A=0,M=0,N=k<0?-1:1,F=z*N*k*2/44100,q=E.cos(F),B=E.sin,H=B(F)/4,O=1+H,V=-2*q/O,W=(1-H)/O,R=(1+N*q)/2/O,G=-(N+q)/O,X=0,Y=0,$=0,j=0;for(i=44100*i+9,b*=44100,o*=44100,r*=44100,w*=44100,d*=500*z/85766121e6,m*=z/44100,u*=z/44100,p*=44100,h=44100*h|0,e*=.3*l.zzfxV,N=i+b+o+r+w|0;I<N;t[I++]=M*e)++A%(100*v|0)||(M=s?1<s?2<s?3<s?B(T*T):E.max(E.min(E.tan(T),1),-1):1-(2*T/z%2+2)%2:1-4*E.abs(E.round(T/z)-T/z):B(T),M=(h?1-y+y*B(z*I/h):1)*(M<0?-1:1)*E.abs(M)**f*(I<i?I/i:I<i+b?1-(I-i)/b*(1-x):I<i+b+o?x:I<N-w?(N-I-w)/r*x:0),M=w?M/2+(w>I?0:(I<N-w?1:(N-I)/w)*t[I-w|0]/2/e):M,k&&(M=j=R*X+G*(X=Y)+R*(Y=M)-W*$-V*($=j))),T+=(F=(n+=c+=d)*E.cos(m*C++))+F*g*B(I**5),L&&++L>p&&(n+=u,P+=u,L=0),!h||++S%h||(n=P,c=D,L=L||1);(e=a.createBuffer(1,N,44100)).getChannelData(0).set(t),(n=a.createBufferSource()).buffer=e,n.connect(a.destination),n.start()});t=Object.assign({width:null,height:null,autoscale:!0,canvas:null,global:!0,loop:null,tapEvents:!0,keyboardEvents:!0},t);let c=!1,d=!0,u,p=1,h,g=.5,m=1,v,w=1e3/60,x,b,y="sans-serif",k=20,E=1.2,z=Date.now(),D=e,P=[],T=[.5,0,1750,,,.3,1,,,,600,.1],C={},I={W:0,H:0,T:0,MX:-1,MY:-1,TWO_PI:i,HALF_PI:i/4,lerp:(e,t,a)=>a*(t-e)+e,deg2rad:e=>n.PI/180*e,rad2deg:e=>180/n.PI*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 o=(e-t)/(a-t)*(n-l)+l;return i?I.clamp(o,l,n):o},norm:(e,t,a)=>I.map(e,t,a,0,1),wave:(e,t,a,l=Math.sin)=>e+(l(a)+1)/2*(t-e),rand:(e=0,t=1)=>(z=(1664525*z+0x3c6ef35f)%0x100000000)/0x100000000*(t-e)+e,randi:(e=0,t=1)=>n.floor(I.rand(e,t+1)),rseed(e){z=~~e},cls(e){null==e?h.clearRect(0,0,h.canvas.width,h.canvas.height):I.rectfill(0,0,h.canvas.width,h.canvas.height,e)},rect(e,t,a,l,n,i){h.beginPath(),h[i?"roundRect":"rect"](~~e-g,~~t-g,~~a+2*g,~~l+2*g,i),I.stroke(n)},rectfill(e,t,a,l,n,i){h.beginPath(),h[i?"roundRect":"rect"](~~e,~~t,~~a,~~l,i),I.fill(n)},circ(e,t,a,l){h.beginPath(),h.arc(~~e,~~t,~~a,0,i),I.stroke(l)},circfill(e,t,a,l){h.beginPath(),h.arc(~~e,~~t,~~a,0,i),I.fill(l)},oval(e,t,a,l,n){h.beginPath(),h.ellipse(~~e,~~t,~~a,~~l,0,0,i),I.stroke(n)},ovalfill(e,t,a,l,n){h.beginPath(),h.ellipse(~~e,~~t,~~a,~~l,0,0,i),I.fill(n)},line(e,t,a,l,n){h.beginPath();let i=.5*(0!==g&&~~e==~~a),o=.5*(0!==g&&~~t==~~l);h.moveTo(~~e+i,~~t+o),h.lineTo(~~a+i,~~l+o),I.stroke(n)},linewidth(e){h.lineWidth=~~e,g=.5*(0!=~~e%2)},linedash(e,t=0){h.setLineDash(e),h.lineDashOffset=t},text(e,t,a,l=3,n="normal"){h.font=`${n} ${k}px ${y}`,h.fillStyle=N(l);let i=(""+a).split("\n");for(let a=0;a<i.length;a++)h.fillText(i[a],~~e,~~t+k*E*a)},textgap(e){E=e},textfont(e){y=e},textsize(e){k=e},textalign(e,t){e&&(h.textAlign=e),t&&(h.textBaseline=t)},image(e,t,a){h.drawImage(a,~~e,~~t)},spr(e,t,a,l,n){let i=n.replace(/\s/g,"");for(let n=0;n<a;n++)for(let o=0;o<l;o++){let l=i[a*o+n]||".";"."!==l&&I.rectfill(e+n,t+o,1,1,parseInt(l,36)||0)}},paint(e,t,a,l={}){let n=l.canvas||new OffscreenCanvas(1,1),i=l.scale||1,o=h;return n.width=e*i,n.height=t*i,(h=n.getContext("2d")).scale(i,i),a(h),h=o,n.transferToImageBitmap()},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=I.clamp(e,0,1)},fill(e){h.fillStyle=N(e),h.fill()},stroke(e){h.strokeStyle=N(e),h.stroke()},clip(e){h.beginPath(),e(h),h.clip()},sfx:(e,t=0,a=1)=>!!l.zzfxV&&(!navigator.userActivation||!!navigator.userActivation.hasBeenActive)&&(e=e||T,(0!==t||1!==a)&&((e=e.slice())[0]=a*(e[0]||1),e[10]=~~e[10]+t),f.apply(0,e),e),volume(e){l.zzfxV=e},canvas:()=>u,use(e,t={}){var a=e,l=t;let n=a(I,l);for(let e in n)I.def(e,n[e])},listen:(e,t)=>(C[e=e.toLowerCase()]=C[e]||new Set,C[e].add(t),()=>C&&C[e].delete(t)),emit(e,t,a,l,n){c&&(M("before:"+(e=e.toLowerCase()),t,a,l,n),M(e,t,a,l,n),M("after:"+e,t,a,l,n))},pal(t=e){D=t,P=[]},palc(e,t){null==e?P=[]:P[e]=t},def(e,a){I[e]=a,t.global&&(l[e]=a)},timescale(e){m=e},framerate(e){w=1e3/~~e},stat(e){let a={index:e,value:[t,c,w/1e3,p,C,D,T,m,l.zzfxV,z,k,y,P,E][e]};return I.emit("stat",a),a.value},pause(){d=!0,cancelAnimationFrame(b)},resume(){c&&d&&(d=!1,x=w,v=Date.now(),b=o(S))},paused:()=>d,quit(){for(let e of(I.emit("quit"),I.pause(),c=!1,C={},r))e();if(t.global){for(let e in I)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(","))I[e]=n[e];function L(){let e=t.loop?t.loop:l;for(let t of"init,update,draw,tap,untap,tapping,tapped,resized".split(","))e[t]&&I.listen(t,e[t]);if(t.autoscale&&s(l,"resize",A),t.tapEvents){let e=e=>[(e.pageX-u.offsetLeft)/p,(e.pageY-u.offsetTop)/p],t=new Map,a=(e,a,l)=>{let n={x:a,y:l,xi:a,yi:l,t:Date.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&&Date.now()-e.t<=300,o=!1;s(u,"mousedown",t=>{if(0===t.button){t.preventDefault();let[l,n]=e(t);I.emit("tap",l,n,0),a(0,l,n),o=!0}}),s(u,"mouseup",a=>{if(0===a.button){a.preventDefault();let l=t.get(0),[n,r]=e(a);i(l)&&I.emit("tapped",l.xi,l.yi,0),I.emit("untap",n,r,0),t.delete(0),o=!1}}),s(l,"mousemove",t=>{t.preventDefault();let[a,l]=e(t);I.def("MX",a),I.def("MY",l),o&&(I.emit("tapping",a,l,0),n(0,a,l))}),s(u,"touchstart",t=>{for(let l of(t.preventDefault(),t.changedTouches)){let[t,n]=e(l);I.emit("tap",t,n,l.identifier+1),a(l.identifier+1,t,n)}}),s(u,"touchmove",t=>{for(let a of(t.preventDefault(),t.changedTouches)){let[t,l]=e(a);I.emit("tapping",t,l,a.identifier+1),n(a.identifier+1,t,l)}});let r=e=>{e.preventDefault();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)&&I.emit("tapped",l.xi,l.yi,e),I.emit("untap",l.x,l.y,e),t.delete(e))};s(u,"touchend",r),s(u,"touchcancel",r),s(l,"blur",()=>{for(let[e,a]of(o=!1,t))I.emit("untap",a.x,a.y,e),t.delete(e)})}if(t.keyboardEvents){let e=new Set,t=new Set,a=(e,t="")=>(t=t.toLowerCase())?e.has("space"===t?" ":t):e.size>0,n="";s(l,"keydown",a=>{let l=a.key.toLowerCase();e.has(l)||(e.add(l),t.add(l),n=" "===l?"space":l)}),s(l,"keyup",t=>{e.delete(t.key.toLowerCase())}),s(l,"blur",()=>e.clear()),I.listen("after:update",()=>t.clear()),I.def("iskeydown",t=>a(e,t)),I.def("iskeypressed",e=>a(t,e)),I.def("lastkey",()=>n)}c=!0,I.emit("init",I),I.resume()}function S(){b=o(S);let e=Date.now(),t=0,a=e-v;for(v=e,x+=a<100?a:w;x>=w;){t++,x-=w;let e=w/1e3*m;I.emit("update",e,t),I.def("T",I.T+e)}t&&(I.emit("draw",h),t>1&&(x=0))}function A(){let e=t.width>0?t.width:innerWidth,a=t.width>0?t.height||t.width:innerHeight;if(I.def("W",e),I.def("H",a),u.width=e,u.height=a,t.autoscale){let l=+t.autoscale;u.style.display||(u.style.display="block",u.style.margin="auto"),p=n.min(innerWidth/e,innerHeight/a),p=l>1&&p>l?l:p,u.style.width=e*p+"px",u.style.height=a*p+"px"}h.imageSmoothingEnabled=!1,I.textalign("start","top"),I.emit("resized",p)}function M(e,t,a,l,n){if(C[e])for(let i of C[e])i(t,a,l,n)}function N(e){return D[~~(P[e]??e)%D.length]}if(t.global){if(l.ENGINE)throw Error("only one global litecanvas is allowed");Object.assign(l,I),l.ENGINE=I}return h=(u=(u="string"==typeof t.canvas?document.querySelector(t.canvas):t.canvas)||document.createElement("canvas")).getContext("2d"),s(u,"click",()=>focus()),A(),u.parentNode||document.body.appendChild(u),u.style.imageRendering="pixelated",u.oncontextmenu=()=>!1,"loading"===document.readyState?s(l,"DOMContentLoaded",()=>o(L)):b=o(L),I}})();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "litecanvas",
3
- "version": "0.98.4",
3
+ "version": "0.100.0",
4
4
  "description": "Lightweight HTML5 canvas 2D game engine suitable for small projects and creative coding. Inspired by PICO-8 and p5.js/Processing.",
5
5
  "license": "MIT",
6
6
  "author": "Luiz Bills <luizbills@pm.me>",
@@ -31,15 +31,13 @@
31
31
  "creative coding"
32
32
  ],
33
33
  "devDependencies": {
34
- "@litecanvas/jsdom-extras": "^2.0.1",
34
+ "@happy-dom/global-registrator": "^18.0.1",
35
35
  "@size-limit/preset-small-lib": "^11.2.0",
36
- "@swc/core": "^1.13.3",
37
- "@types/jsdom": "^21.1.7",
36
+ "@swc/core": "^1.13.19",
38
37
  "ava": "^6.4.1",
39
- "esbuild": "^0.25.8",
38
+ "esbuild": "^0.25.10",
40
39
  "genversion": "^3.2.0",
41
40
  "gzip-size": "^7.0.0",
42
- "jsdom": "^26.1.0",
43
41
  "prettier": "^3.6.2",
44
42
  "sinon": "^21.0.0",
45
43
  "size-limit": "^11.2.0",
@@ -51,7 +49,7 @@
51
49
  "scripts": {
52
50
  "prepare": "npm run build",
53
51
  "prepublishOnly": "npm test",
54
- "test": "ava --timeout=1s --fast-fail --tap | tap-min",
52
+ "test": "ava --tap | tap-min",
55
53
  "test:watch": "ava --watch",
56
54
  "dev": "esbuild src/web.js --bundle --watch --outfile=samples/dist.js --servedir=samples",
57
55
  "build": "npm run genversion && node script/build.js && size-limit",
@@ -66,8 +64,14 @@
66
64
  ],
67
65
  "ava": {
68
66
  "files": [
69
- "tests/**/*.js"
70
- ]
67
+ "tests/**/*.js",
68
+ "!tests/_preload/**/*.js"
69
+ ],
70
+ "require": [
71
+ "./tests/_preload/happy-dom.js"
72
+ ],
73
+ "failFast": true,
74
+ "timeout": "10s"
71
75
  },
72
76
  "size-limit": [
73
77
  {
package/src/index.js CHANGED
@@ -48,6 +48,8 @@ export default function litecanvas(settings = {}) {
48
48
 
49
49
  let /** @type {boolean} */
50
50
  _initialized = false,
51
+ /** @type {boolean} */
52
+ _paused = true,
51
53
  /** @type {HTMLCanvasElement} _canvas */
52
54
  _canvas,
53
55
  /** @type {number} */
@@ -71,6 +73,8 @@ export default function litecanvas(settings = {}) {
71
73
  /** @type {number} */
72
74
  _fontSize = 20,
73
75
  /** @type {number} */
76
+ _fontLineHeight = 1.2,
77
+ /** @type {number} */
74
78
  _rngSeed = Date.now(),
75
79
  /** @type {string[]} */
76
80
  _colorPalette = defaultPalette,
@@ -637,7 +641,7 @@ export default function litecanvas(settings = {}) {
637
641
 
638
642
  /** TEXT RENDERING API */
639
643
  /**
640
- * Draw text
644
+ * Draw text. You can use `\n` to break lines.
641
645
  *
642
646
  * @param {number} x
643
647
  * @param {number} y
@@ -659,7 +663,22 @@ export default function litecanvas(settings = {}) {
659
663
 
660
664
  _ctx.font = `${fontStyle} ${_fontSize}px ${_fontFamily}`
661
665
  _ctx.fillStyle = getColor(color)
662
- _ctx.fillText(message, ~~x, ~~y)
666
+
667
+ const messages = ('' + message).split('\n')
668
+ for (let i = 0; i < messages.length; i++) {
669
+ _ctx.fillText(messages[i], ~~x, ~~y + _fontSize * _fontLineHeight * i)
670
+ }
671
+ },
672
+
673
+ /**
674
+ * Sets the height ratio of the text lines based on current text size.
675
+ *
676
+ * Default = `1.2`
677
+ *
678
+ * @param value
679
+ */
680
+ textgap(value) {
681
+ _fontLineHeight = value
663
682
  },
664
683
 
665
684
  /**
@@ -1056,6 +1075,7 @@ export default function litecanvas(settings = {}) {
1056
1075
  'string' === typeof eventName,
1057
1076
  '[litecanvas] emit() 1st param must be a string'
1058
1077
  )
1078
+
1059
1079
  if (_initialized) {
1060
1080
  eventName = lowerCase(eventName)
1061
1081
 
@@ -1156,7 +1176,7 @@ export default function litecanvas(settings = {}) {
1156
1176
  },
1157
1177
 
1158
1178
  /**
1159
- * Returns information about that engine instance.
1179
+ * Returns information about the engine instance.
1160
1180
  *
1161
1181
  * @param {number|string} index
1162
1182
  * @returns {any}
@@ -1190,8 +1210,12 @@ export default function litecanvas(settings = {}) {
1190
1210
  _rngSeed,
1191
1211
  // 10
1192
1212
  _fontSize,
1193
- // 11
1213
+ // 11
1194
1214
  _fontFamily,
1215
+ // 12
1216
+ _colorPaletteState,
1217
+ // 13
1218
+ _fontLineHeight,
1195
1219
  ]
1196
1220
 
1197
1221
  const data = { index, value: internals[index] }
@@ -1202,49 +1226,24 @@ export default function litecanvas(settings = {}) {
1202
1226
  return data.value
1203
1227
  },
1204
1228
 
1205
- /**
1206
- * Stops the litecanvas instance and remove all event listeners.
1207
- */
1208
- quit() {
1209
- // stop the game loop (update & draw)
1210
- instance.pause()
1211
-
1212
- // emit "quit" event to manual clean ups
1213
- instance.emit('quit')
1214
-
1215
- // clear all engine event listeners
1216
- _eventListeners = {}
1217
-
1218
- // clear all browser event listeners
1219
- for (const removeListener of _browserEventListeners) {
1220
- removeListener()
1221
- }
1222
-
1223
- // maybe clear global context
1224
- if (settings.global) {
1225
- for (const key in instance) {
1226
- delete root[key]
1227
- }
1228
- delete root.ENGINE
1229
- }
1230
-
1231
- // unset that flag
1232
- _initialized = false
1233
- },
1234
-
1235
1229
  /**
1236
1230
  * Pauses the engine loop (update & draw).
1237
1231
  */
1238
1232
  pause() {
1233
+ _paused = true
1239
1234
  cancelAnimationFrame(_rafid)
1240
- _rafid = 0
1241
1235
  },
1242
1236
 
1243
1237
  /**
1244
1238
  * Resumes (if paused) the engine loop.
1245
1239
  */
1246
1240
  resume() {
1247
- if (_initialized && !_rafid) {
1241
+ DEV: assert(
1242
+ _initialized,
1243
+ '[litecanvas] resume() cannot be called before the "init" event and neither after the quit() function'
1244
+ )
1245
+ if (_initialized && _paused) {
1246
+ _paused = false
1248
1247
  _accumulated = _fpsInterval
1249
1248
  _lastFrameTime = Date.now()
1250
1249
  _rafid = raf(drawFrame)
@@ -1257,7 +1256,39 @@ export default function litecanvas(settings = {}) {
1257
1256
  * @returns {boolean}
1258
1257
  */
1259
1258
  paused() {
1260
- return !_rafid
1259
+ return _paused
1260
+ },
1261
+
1262
+ /**
1263
+ * Shutdown the litecanvas instance and remove all event listeners.
1264
+ */
1265
+ quit() {
1266
+ // emit "quit" event to manual clean ups
1267
+ instance.emit('quit')
1268
+
1269
+ // stop the game loop (update & draw)
1270
+ instance.pause()
1271
+
1272
+ // deinitialize the engine
1273
+ _initialized = false
1274
+
1275
+ // clear all engine event listeners
1276
+ _eventListeners = {}
1277
+
1278
+ // clear all browser event listeners
1279
+ for (const removeListener of _browserEventListeners) {
1280
+ removeListener()
1281
+ }
1282
+
1283
+ // maybe clear global context
1284
+ if (settings.global) {
1285
+ for (const key in instance) {
1286
+ delete root[key]
1287
+ }
1288
+ delete root.ENGINE
1289
+ }
1290
+
1291
+ DEV: console.warn('[litecanvas] quit() terminated a Litecanvas instance.')
1261
1292
  },
1262
1293
  }
1263
1294
 
@@ -1713,7 +1744,7 @@ export default function litecanvas(settings = {}) {
1713
1744
  if ('loading' === document.readyState) {
1714
1745
  on(root, 'DOMContentLoaded', () => raf(init))
1715
1746
  } else {
1716
- raf(init)
1747
+ _rafid = raf(init)
1717
1748
  }
1718
1749
 
1719
1750
  return instance
package/src/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // Generated by genversion.
2
- export const version = '0.98.4'
2
+ export const version = '0.100.0'
package/types/global.d.ts CHANGED
@@ -325,7 +325,7 @@ declare global {
325
325
 
326
326
  /** TEXT RENDERING API */
327
327
  /**
328
- * Draw text
328
+ * Draw text. You can use `\n` to break lines.
329
329
  *
330
330
  * @param x
331
331
  * @param y
@@ -355,6 +355,14 @@ declare global {
355
355
  * @see https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/textAlign
356
356
  */
357
357
  function textalign(align: CanvasTextAlign, baseline: CanvasTextBaseline): void
358
+ /**
359
+ * Sets the height ratio of the text lines based on current text size.
360
+ *
361
+ * Default = `1.2`
362
+ *
363
+ * @param value
364
+ */
365
+ function textgap(value: number): void
358
366
 
359
367
  /** BASIC GRAPHICS API */
360
368
  /**
@@ -583,9 +591,9 @@ declare global {
583
591
  */
584
592
  function framerate(fps: number): void
585
593
  /**
586
- * Returns information about that engine instance.
594
+ * Returns information about the engine instance.
587
595
  *
588
- * - n = 0: the settings passed to that instance
596
+ * - n = 0: the settings passed to this instance
589
597
  * - n = 1: returns true if the "init" event has already been emitted
590
598
  * - n = 2: the current delta time (dt)
591
599
  * - n = 3: the current canvas element scale (not the context 2D scale)
@@ -597,15 +605,13 @@ declare global {
597
605
  * - n = 9: the current RNG state
598
606
  * - n = 10: the current font size
599
607
  * - n = 11: the current font family
608
+ * - n = 12: the current state of the color palette
609
+ * - n = 13: the current font gap
600
610
  * - n = *any other value*: probably returns undefined
601
611
  *
602
- * @param n
612
+ * @param index
603
613
  */
604
- function stat(n: number): any
605
- /**
606
- * Shutdown the litecanvas instance and remove all event listeners.
607
- */
608
- function quit(): void
614
+ function stat(index: number | string): any
609
615
  /**
610
616
  * Pauses the engine loop (update & draw).
611
617
  */
@@ -618,4 +624,8 @@ declare global {
618
624
  * Returns `true` if the engine loop is paused.
619
625
  */
620
626
  function paused(): boolean
627
+ /**
628
+ * Shutdown the litecanvas instance and remove all event listeners.
629
+ */
630
+ function quit(): void
621
631
  }
package/types/types.d.ts CHANGED
@@ -319,7 +319,7 @@ type LitecanvasInstance = {
319
319
 
320
320
  /** TEXT RENDERING API */
321
321
  /**
322
- * Draw text
322
+ * Draw text. You can use `\n` to break lines.
323
323
  *
324
324
  * @param x
325
325
  * @param y
@@ -349,6 +349,14 @@ type LitecanvasInstance = {
349
349
  * @see https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/textAlign
350
350
  */
351
351
  textalign(align: CanvasTextAlign, baseline: CanvasTextBaseline): void
352
+ /**
353
+ * Sets the height ratio of the text lines based on current text size.
354
+ *
355
+ * Default = `1.2`
356
+ *
357
+ * @param value
358
+ */
359
+ textgap(value: number): void
352
360
 
353
361
  /** BASIC GRAPHICS API */
354
362
  /**
@@ -573,9 +581,9 @@ type LitecanvasInstance = {
573
581
  */
574
582
  framerate(fps: number): void
575
583
  /**
576
- * Returns information about that engine instance.
584
+ * Returns information about the engine instance.
577
585
  *
578
- * - n = 0: the settings passed to that instance
586
+ * - n = 0: the settings passed to this instance
579
587
  * - n = 1: returns true if the "init" event has already been emitted
580
588
  * - n = 2: the current delta time (dt)
581
589
  * - n = 3: the current canvas element scale (not the context 2D scale)
@@ -587,15 +595,13 @@ type LitecanvasInstance = {
587
595
  * - n = 9: the current RNG state
588
596
  * - n = 10: the current font size
589
597
  * - n = 11: the current font family
598
+ * - n = 12: the current state of the color palette
599
+ * - n = 13: the current font gap
590
600
  * - n = *any other value*: probably returns undefined
591
601
  *
592
- * @param n
602
+ * @param index
593
603
  */
594
- stat(n: number): any
595
- /**
596
- * Stops the litecanvas instance and remove all event listeners.
597
- */
598
- quit(): void
604
+ stat(index: number | string): any
599
605
  /**
600
606
  * Pauses the engine loop (update & draw).
601
607
  */
@@ -608,6 +614,10 @@ type LitecanvasInstance = {
608
614
  * Returns `true` if the engine loop is paused.
609
615
  */
610
616
  paused(): boolean
617
+ /**
618
+ * Shutdown the litecanvas instance and remove all event listeners.
619
+ */
620
+ quit(): void
611
621
  }
612
622
 
613
623
  type LitecanvasOptions = {