littlejsengine 1.24.3 → 1.26.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.
Files changed (53) hide show
  1. package/CHANGELOG.md +123 -0
  2. package/EDITOR.md +2 -2
  3. package/FAQ.md +55 -11
  4. package/README.md +6 -2
  5. package/REFERENCE.md +203 -57
  6. package/dist/littlejs.d.ts +933 -313
  7. package/dist/littlejs.esm.js +5143 -2419
  8. package/dist/littlejs.esm.min.js +38 -24
  9. package/dist/littlejs.js +5132 -2416
  10. package/dist/littlejs.min.js +37 -23
  11. package/dist/littlejs.release.js +4905 -2232
  12. package/package.json +4 -4
  13. package/plugins/box2d.js +141 -56
  14. package/plugins/drawUtilities.js +374 -380
  15. package/plugins/gltf.js +383 -60
  16. package/plugins/lightSystem.js +412 -30
  17. package/plugins/math3d.js +1015 -1014
  18. package/plugins/medalSystem.js +19 -7
  19. package/plugins/newgrounds.js +5 -6
  20. package/plugins/parallax.js +1 -0
  21. package/plugins/particleEffects.js +47 -14
  22. package/plugins/pathFinder.js +60 -38
  23. package/plugins/pluginExport.js +11 -0
  24. package/plugins/postProcess.js +200 -67
  25. package/plugins/render3d.js +493 -2391
  26. package/plugins/render3dDebug.js +5 -8
  27. package/plugins/render3dEditor.js +84 -9
  28. package/plugins/render3dExtras.js +122 -33
  29. package/plugins/render3dLevel.js +67 -8
  30. package/plugins/render3dMesh.js +1158 -0
  31. package/plugins/render3dObject.js +1451 -0
  32. package/plugins/render3dVoxels.js +15 -3
  33. package/plugins/textureSheet.js +9 -8
  34. package/plugins/threejs.js +1 -1
  35. package/plugins/tweenSystem.js +117 -87
  36. package/plugins/uiSystem.js +62 -28
  37. package/plugins/wavedash.js +229 -0
  38. package/src/engine.js +192 -40
  39. package/src/engineAudio.js +47 -27
  40. package/src/engineBuild.mjs +6 -1
  41. package/src/engineDebug.js +23 -162
  42. package/src/engineDraw.js +148 -43
  43. package/src/engineEditor.js +153 -40
  44. package/src/engineExport.js +0 -3
  45. package/src/engineInput.js +31 -25
  46. package/src/engineMath.js +38 -20
  47. package/src/engineObject.js +68 -50
  48. package/src/engineParticles.js +66 -18
  49. package/src/engineRelease.js +18 -15
  50. package/src/engineSettings.js +48 -9
  51. package/src/engineTileLayer.js +44 -18
  52. package/src/engineUtilities.js +56 -11
  53. package/src/engineWebGL.js +68 -22
@@ -1,381 +1,375 @@
1
- /**
2
- * LittleJS Drawing Utilities Plugin
3
- * - Extra drawing functions for LittleJS
4
- * - Nine slice and three slice drawing, and TileSlice to keep one as a style, like a UI skin
5
- * @namespace DrawUtilities
6
- */
7
-
8
- 'use strict';
9
-
10
- ///////////////////////////////////////////////////////////////////////////////
11
-
12
- /** Draw a scalable nine-slice UI element in screen space, drawNineSlice with screenSpace set
13
- * - Draws with the 2D context by default, on top of what WebGL drew, like drawTextScreen
14
- * - With no angle its pieces land on whole pixels and meet exactly, so pixel art lines up and extraSpace is not used
15
- * @param {Vector2} pos - Screen space position
16
- * @param {Vector2} size - Screen space size
17
- * @param {TileInfo} startTile - Top-left tile of the 3x3 block to sample (see drawNineSlice)
18
- * @param {Color|number} [color=WHITE] - Color to modulate with; a number here is the borderSize of the order before 1.20, deprecated
19
- * @param {number} [borderSize] - Rendered thickness of the border sections
20
- * @param {Color} [additiveColor] - Additive color
21
- * @param {number} [extraSpace] - Extra spacing adjustment
22
- * @param {number} [angle] - Angle to rotate by
23
- * @param {boolean} [useWebGL] - Use WebGL for rendering
24
- * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context] - Canvas context to use
25
- * @memberof DrawUtilities */
26
- function drawNineSliceScreen(pos, size, startTile, color=WHITE, borderSize=32, additiveColor, extraSpace=2, angle=0, useWebGL=false, context)
27
- {
28
- if (isNumber(color)) // deprecated since 1.20, the order before it: borderSize, extraSpace and angle after startTile
29
- {
30
- [borderSize, extraSpace, angle] = [/** @type {number} */ (color), arguments[4] ?? 2, arguments[5] ?? 0];
31
- color = WHITE, additiveColor = undefined;
32
- }
33
- drawNineSlice(pos, size, startTile, /** @type {Color} */ (color), borderSize, additiveColor, extraSpace, angle, useWebGL, true, context);
34
- }
35
-
36
- /** Draw a scalable nine-slice UI element in world space
37
- * Color and additive color apply in WebGL and Canvas2D (with canvasColorTiles)
38
- * The nine-slice samples a 3x3 block of tiles from the tilesheet, it does not
39
- * subdivide a single tile. Pass the top-left tile of that block as startTile;
40
- * the other 8 tiles (edges, corners, and center) are taken automatically from
41
- * the 3x3 grid of tiles extending right and down from it. borderSize only sets
42
- * the rendered thickness of the edges and corners, not how the texture is cut.
43
- * @param {Vector2} pos - World space position
44
- * @param {Vector2} size - World space size
45
- * @param {TileInfo} startTile - Top-left tile of the 3x3 block to sample the nine-slice from
46
- * @param {Color} [color] - Color to modulate with
47
- * @param {number} [borderSize] - Rendered thickness of the border sections, 1 in world space and 32 pixels in
48
- * screen space by default
49
- * @param {Color} [additiveColor] - Additive color
50
- * @param {number} [extraSpace] - Extra spacing adjustment, .05 in world space and 2 pixels in screen space by
51
- * default
52
- * @param {number} [angle] - Angle to rotate by
53
- * @param {boolean} [useWebGL=glEnable] - Use WebGL for rendering
54
- * @param {boolean} [screenSpace=drawScreenSpace] - Use screen space coordinates
55
- * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context] - Canvas context to use
56
- * @memberof DrawUtilities */
57
- function drawNineSlice(pos, size, startTile, color, borderSize, additiveColor, extraSpace, angle=0, useWebGL=glEnable, screenSpace=drawScreenSpace, context)
58
- {
59
- // the border and spacing defaults are in the units of the space it draws in
60
- borderSize ??= screenSpace ? 32 : 1;
61
- extraSpace ??= screenSpace ? 2 : .05;
62
- if (!size.x || !size.y) return; // nothing to draw, the spacing must not make a sliver of it
63
- if (color && color.a < 1)
64
- extraSpace = 0; // see through, the overlap that hides a seam would draw twice and show brighter
65
- // setup nine slice tiles - startTile is the top-left of a 3x3 tile block,
66
- // so the center tile is one tile down and right from it, stepping over
67
- // the padding around each tile the way tile() lays out the grid
68
- const step = startTile.size.add(vec2(startTile.padding*2));
69
- if (screenSpace && !angle)
70
- {
71
- // on whole pixels, the pieces in the screen's rows and columns of the block
72
- drawSliceSnapped(pos, size, borderSize, (col, row)=>
73
- [startTile.offset(step.multiply(vec2(col, row))), 0], color, additiveColor, useWebGL, context);
74
- return;
75
- }
76
- borderSize = min(borderSize, abs(size.x)/2, abs(size.y)/2); // a box too small for two borders splits between them
77
- const centerTile = startTile.offset(step);
78
- const centerSize = size.add(vec2(extraSpace-borderSize*2));
79
- const cornerSize = vec2(borderSize);
80
- const cornerOffset = size.scale(.5).subtract(cornerSize.scale(.5));
81
- const flip = screenSpace ? -1 : 1;
82
- const rotateAngle = screenSpace ? -angle : angle;
83
-
84
- // center
85
- drawTile(pos, centerSize, centerTile, color, angle, false, additiveColor, useWebGL, screenSpace, context);
86
- for (let i=4; i--;)
87
- {
88
- // sides
89
- const horizontal = i%2;
90
- const sidePos = cornerOffset.multiply(vec2(horizontal?i===1?1:-1:0, horizontal?0:i?-1:1));
91
- const sideSize = vec2(horizontal ? borderSize : centerSize.x, horizontal ? centerSize.y : borderSize);
92
- const sideTile = centerTile.offset(step.multiply(vec2(i===1?1:i===3?-1:0,i===0?-flip:i===2?flip:0)))
93
- drawTile(pos.add(sidePos.rotate(rotateAngle)), sideSize, sideTile, color, angle, false, additiveColor, useWebGL, screenSpace, context);
94
- }
95
- for (let i=4; i--;)
96
- {
97
- // corners
98
- const flipX = i>1;
99
- const flipY = i && i<3;
100
- const cornerPos = cornerOffset.multiply(vec2(flipX?-1:1, flipY?-1:1));
101
- const cornerTile = centerTile.offset(step.multiply(vec2(flipX?-1:1,flipY?flip:-flip)));
102
- drawTile(pos.add(cornerPos.rotate(rotateAngle)), cornerSize, cornerTile, color, angle, false, additiveColor, useWebGL, screenSpace, context);
103
- }
104
- }
105
-
106
- /** Draw a scalable three-slice UI element in screen space, drawThreeSlice with screenSpace set
107
- * - Draws with the 2D context by default, on top of what WebGL drew, like drawTextScreen
108
- * - With no angle its pieces land on whole pixels and meet exactly, so pixel art lines up and extraSpace is not used
109
- * @param {Vector2} pos - Screen space position
110
- * @param {Vector2} size - Screen space size
111
- * @param {TileInfo} startTile - First of 3 consecutive tiles: corner, side, center (see drawThreeSlice)
112
- * @param {Color|number} [color=WHITE] - Color to modulate with; a number here is the borderSize of the order before 1.20, deprecated
113
- * @param {number} [borderSize] - Rendered thickness of the border sections
114
- * @param {Color} [additiveColor] - Additive color
115
- * @param {number} [extraSpace] - Extra spacing adjustment
116
- * @param {number} [angle] - Angle to rotate by
117
- * @param {boolean} [useWebGL] - Use WebGL for rendering
118
- * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context] - Canvas context to use
119
- * @memberof DrawUtilities */
120
- function drawThreeSliceScreen(pos, size, startTile, color=WHITE, borderSize=32, additiveColor, extraSpace=2, angle=0, useWebGL=false, context)
121
- {
122
- if (isNumber(color)) // deprecated since 1.20, the order before it: borderSize, extraSpace and angle after startTile
123
- {
124
- [borderSize, extraSpace, angle] = [/** @type {number} */ (color), arguments[4] ?? 2, arguments[5] ?? 0];
125
- color = WHITE, additiveColor = undefined;
126
- }
127
- drawThreeSlice(pos, size, startTile, /** @type {Color} */ (color), borderSize, additiveColor, extraSpace, angle, useWebGL, true, context);
128
- }
129
-
130
- /** Draw a scalable three-slice UI element in world space
131
- * Color and additive color apply in WebGL and Canvas2D (with canvasColorTiles)
132
- * The three-slice samples 3 consecutive tiles from the tilesheet, it does not
133
- * subdivide a single tile. Pass the first tile as startTile; the three tiles
134
- * are used in order as corner, side, and center, then rotated and mirrored to
135
- * build all four edges and corners. borderSize only sets the rendered thickness.
136
- * @param {Vector2} pos - World space position
137
- * @param {Vector2} size - World space size
138
- * @param {TileInfo} startTile - First of 3 consecutive tiles (corner, side, center) for the three-slice
139
- * @param {Color} [color] - Color to modulate with
140
- * @param {number} [borderSize] - Rendered thickness of the border sections, 1 in world space and 32 pixels in
141
- * screen space by default
142
- * @param {Color} [additiveColor] - Additive color
143
- * @param {number} [extraSpace] - Extra spacing adjustment, .05 in world space and 2 pixels in screen space by
144
- * default
145
- * @param {number} [angle] - Angle to rotate by
146
- * @param {boolean} [useWebGL=glEnable] - Use WebGL for rendering
147
- * @param {boolean} [screenSpace=drawScreenSpace] - Use screen space coordinates
148
- * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context] - Canvas context to use
149
- * @memberof DrawUtilities */
150
- function drawThreeSlice(pos, size, startTile, color, borderSize, additiveColor, extraSpace, angle=0, useWebGL=glEnable, screenSpace=drawScreenSpace, context)
151
- {
152
- // the border and spacing defaults are in the units of the space it draws in
153
- borderSize ??= screenSpace ? 32 : 1;
154
- extraSpace ??= screenSpace ? 2 : .05;
155
- if (!size.x || !size.y) return; // nothing to draw, the spacing must not make a sliver of it
156
- if (color && color.a < 1)
157
- extraSpace = 0; // see through, the overlap that hides a seam would draw twice and show brighter
158
- // setup three slice tiles - 3 tiles in a row starting at startTile
159
- const cornerTile = startTile.frame(0);
160
- const sideTile = startTile.frame(1);
161
- const centerTile = startTile.frame(2);
162
- if (screenSpace && !angle)
163
- {
164
- // on whole pixels, each corner and side the top left one's tile turned a quarter more going clockwise
165
- const turns = [[0,0,1],[3,-1,1],[3,2,2]]; // by column in each row, -1 for the center
166
- drawSliceSnapped(pos, size, borderSize, (col, row)=>
167
- {
168
- const turn = turns[row][col], corner = col !== 1 && row !== 1;
169
- return turn < 0 ? [centerTile, 0] : [corner ? cornerTile : sideTile, turn*PI/2];
170
- }, color, additiveColor, useWebGL, context);
171
- return;
172
- }
173
- borderSize = min(borderSize, abs(size.x)/2, abs(size.y)/2); // a box too small for two borders splits between them
174
- const centerSize = size.add(vec2(extraSpace-borderSize*2));
175
- const cornerSize = vec2(borderSize);
176
- const cornerOffset = size.scale(.5).subtract(cornerSize.scale(.5));
177
- const flip = screenSpace ? -1 : 1;
178
- const rotateAngle = screenSpace ? -angle : angle;
179
-
180
- // center
181
- drawTile(pos, centerSize, centerTile, color, angle, false, additiveColor, useWebGL, screenSpace, context);
182
- for (let i=4; i--;)
183
- {
184
- // sides
185
- const a = angle + i*PI/2;
186
- const horizontal = i%2;
187
- const sidePos = cornerOffset.multiply(vec2(horizontal?i===1?1:-1:0, horizontal?0:i?-flip:flip));
188
- const sideSize = vec2(horizontal ? centerSize.y : centerSize.x, borderSize);
189
- drawTile(pos.add(sidePos.rotate(rotateAngle)), sideSize, sideTile, color, a, false, additiveColor, useWebGL, screenSpace, context);
190
- }
191
- for (let i=4; i--;)
192
- {
193
- // corners
194
- const a = angle + i*PI/2;
195
- const flipX = !i || i>2;
196
- const flipY = i>1;
197
- const cornerPos = cornerOffset.multiply(vec2(flipX?-1:1, flipY?-flip:flip));
198
- drawTile(pos.add(cornerPos.rotate(rotateAngle)), cornerSize, cornerTile, color, a, false, additiveColor, useWebGL, screenSpace, context);
199
- }
200
- }
201
-
202
- ///////////////////////////////////////////////////////////////////////////////
203
-
204
- /**
205
- * A tile drawn as a box of any size, kept as a style to draw with, like a UI skin
206
- * - 9 slices is a nine-slice from the 3x3 block of tiles at tileInfo, see drawNineSlice
207
- * - 3 slices is a three-slice from the 3 tiles in a row at tileInfo, see drawThreeSlice
208
- * - 1 slice is the whole tile stretched over the box, a plain image
209
- * - The UI system draws a widget's background with one, see uiSystem.defaultSlice
210
- * @memberof DrawUtilities
211
- * @example
212
- * const panel = new TileSlice(tile(0, 16), 9, .5); // a border of half a world unit
213
- * panel.draw(vec2(0, 5), vec2(10, 4));
214
- * uiSystem.defaultSlice = new TileSlice(tile(0, 16), 9, 16); // UI sizes are pixels, every widget made after this
215
- */
216
- class TileSlice
217
- {
218
- /** Create a tile slice style
219
- * @param {TileInfo} tileInfo - The tile, or the first of the tiles, to draw with
220
- * @param {number} [slices] - 9 for a nine-slice, 3 for a three-slice, 1 for the whole tile
221
- * @param {number} [borderSize] - Drawn thickness of the edges and corners, undefined for the draw's own default
222
- * @param {number} [extraSpace] - Extra spacing adjustment of the slices, undefined for the draw's own default */
223
- constructor(tileInfo, slices=9, borderSize, extraSpace)
224
- {
225
- ASSERT(tileInfo instanceof TileInfo, 'tileInfo must be a TileInfo');
226
- ASSERT(slices === 9 || slices === 3 || slices === 1, 'slices must be 9, 3 or 1');
227
- ASSERT(borderSize === undefined || isNumber(borderSize), 'borderSize must be a number');
228
-
229
- /** @property {TileInfo} - The tile, or the first of the tiles, to draw with */
230
- this.tileInfo = tileInfo;
231
- /** @property {number} - 9 for a nine-slice, 3 for a three-slice, 1 for the whole tile */
232
- this.slices = slices;
233
- /** @property {number|undefined} - Drawn thickness of the edges and corners, undefined for the draw's default
234
- * @type {number|undefined} */
235
- this.borderSize = borderSize;
236
- /** @property {number|undefined} - Extra spacing adjustment of the slices, undefined for the draw's default
237
- * @type {number|undefined} */
238
- this.extraSpace = extraSpace;
239
- }
240
-
241
- /** Draw it as a box in world space, or in screen space
242
- * @param {Vector2} pos - Center position
243
- * @param {Vector2} size - Size of the box
244
- * @param {Color} [color] - Color to modulate with
245
- * @param {Color} [additiveColor] - Additive color
246
- * @param {number} [angle] - Angle to rotate by
247
- * @param {boolean} [useWebGL=glEnable] - Use WebGL for rendering
248
- * @param {boolean} [screenSpace=drawScreenSpace] - Are pos and size in screen space?
249
- * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context] - Canvas context to use */
250
- draw(pos, size, color=WHITE, additiveColor, angle=0, useWebGL=glEnable, screenSpace=drawScreenSpace, context)
251
- {
252
- if (screenSpace) // with the screen space defaults for the border and spacing
253
- return this.drawScreen(pos, size, color, additiveColor, angle, useWebGL, context);
254
- if (this.slices === 9)
255
- drawNineSlice(pos, size, this.tileInfo, color, this.borderSize, additiveColor, this.extraSpace, angle, useWebGL, screenSpace, context);
256
- else if (this.slices === 3)
257
- drawThreeSlice(pos, size, this.tileInfo, color, this.borderSize, additiveColor, this.extraSpace, angle, useWebGL, screenSpace, context);
258
- else
259
- drawTile(pos, size, this.tileInfo, color, angle, false, additiveColor, useWebGL, screenSpace, context);
260
- }
261
-
262
- /** Draw it as a box in screen space, with the 2D context by default, on top of what WebGL drew, like drawTextScreen
263
- * @param {Vector2} pos - Screen space center position
264
- * @param {Vector2} size - Screen space size
265
- * @param {Color} [color] - Color to modulate with
266
- * @param {Color} [additiveColor] - Additive color
267
- * @param {number} [angle] - Angle to rotate by
268
- * @param {boolean} [useWebGL] - Use WebGL for rendering
269
- * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context] - Canvas context to use */
270
- drawScreen(pos, size, color=WHITE, additiveColor, angle=0, useWebGL=false, context)
271
- {
272
- if (this.slices === 9)
273
- drawNineSliceScreen(pos, size, this.tileInfo, color, this.borderSize, additiveColor, this.extraSpace, angle, useWebGL, context);
274
- else if (this.slices === 3)
275
- drawThreeSliceScreen(pos, size, this.tileInfo, color, this.borderSize, additiveColor, this.extraSpace, angle, useWebGL, context);
276
- else
277
- drawTile(pos, size, this.tileInfo, color, angle, false, additiveColor, useWebGL, true, context);
278
- }
279
- }
280
-
281
- // draw a slice box in screen space with no angle on whole pixels: the edges and the border are rounded, and every
282
- // piece is cut from them, so the pieces meet exactly and their texels line up where they do, with no overlap to hide
283
- // a seam; pieceTile(col, row) gives the tile and angle for the piece in that column and row of the box, top left 0
284
- function drawSliceSnapped(pos, size, borderSize, pieceTile, color, additiveColor, useWebGL, context)
285
- {
286
- // the pixels it lands on are the device's: a 2D context's own scale and offset, like the UI under nativeHeight
287
- // centered on an odd sized canvas, or the pixel ratio; the draws land half a pixel past, see shift below
288
- const canvas2D = !useWebGL || !glEnable || !!context;
289
- const transform = canvas2D && (context || drawContext)?.getTransform?.();
290
- const ratio = getCanvasPixelRatio();
291
- const [scaleX, scaleY, offsetX, offsetY] = transform ?
292
- [transform.a || 1, transform.d || 1, transform.e || 0, transform.f || 0] : [ratio, ratio, 0, 0];
293
- const edges = (center, length, scale, offset)=>
294
- {
295
- // snapped in device order, which a mirrored context reverses, then put back in the box's own order
296
- const border = max(1, round(borderSize * abs(scale)));
297
- const a = round((center - length/2) * scale + offset), b = a + round(length * scale);
298
- const start = min(a, b), end = max(a, b);
299
- const inner = min(start + border, floor((start + end)/2)); // a box too small for two borders splits
300
- const device = [start, inner, max(end - border, inner), end];
301
- return (scale < 0 ? device.reverse() : device).map(edge=> (edge - offset) / scale);
302
- };
303
- const xs = edges(pos.x, size.x, scaleX, offsetX), ys = edges(pos.y, size.y, scaleY, offsetY);
304
- const shift = .5; // a screen space draw lands half a pixel past its position, in Canvas2D and WebGL alike
305
- for (let row = 3; row--;)
306
- for (let col = 3; col--;)
307
- {
308
- const w = xs[col+1] - xs[col], h = ys[row+1] - ys[row];
309
- if (w <= 0 || h <= 0) continue;
310
- const [tileInfo, angle] = pieceTile(col, row);
311
- const turned = round(angle / (PI/2)) % 2; // a quarter turn swaps the sides it is drawn with
312
- const piecePos = vec2(xs[col] + w/2 - shift, ys[row] + h/2 - shift);
313
- drawTile(piecePos, turned ? vec2(h, w) : vec2(w, h), tileInfo, color, angle, false, additiveColor, useWebGL, true, context);
314
- }
315
- }
316
-
317
- /** Draw a crescent / moon-phase shape built from a polygon
318
- * Routes through drawPoly, so it supports WebGL, screen space, color, and outlines
319
- * - At angle 0 the lit side faces up while waxing (percent below .5) and down while waning, and the lit width
320
- * grows evenly with the phase, not as the real moon's does
321
- * @param {Vector2} pos - Center position
322
- * @param {number} [size] - Diameter
323
- * @param {number} [percent] - Moon phase over a full cycle (0=new, .25=first quarter, .5=full, .75=last quarter), wraps
324
- * @param {Color} [color] - Fill color
325
- * @param {number} [angle] - Angle to rotate by
326
- * @param {boolean} [invert] - Draw the unlit part of the disk instead of the lit part
327
- * @param {number} [lineWidth] - Outline width, 0 for no outline
328
- * @param {Color} [lineColor] - Outline color
329
- * @param {boolean} [useWebGL=glEnable] - Use WebGL for rendering
330
- * @param {boolean} [screenSpace=drawScreenSpace] - Use screen space coordinates
331
- * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context] - Canvas context to use
332
- * @memberof DrawUtilities */
333
- function drawCrescent(pos, size=1, percent=0, color=WHITE, angle=0, invert=false, lineWidth=0, lineColor=BLACK, useWebGL=glEnable, screenSpace=drawScreenSpace, context)
334
- {
335
- // build local-space points and let drawPoly apply pos/angle so screen space works
336
- const points = getCrescentPoints(vec2(), size, percent, 0, invert);
337
- drawPoly(points, color, lineWidth, lineColor, pos, angle, useWebGL, screenSpace, context);
338
- }
339
-
340
- /** Get the list of points that make up a crescent / moon-phase shape
341
- * Returns world-space points with pos and angle baked in, ready for drawPoly or other use
342
- * @param {Vector2} pos - Center position
343
- * @param {number} [size] - Diameter
344
- * @param {number} [percent] - Moon phase over a full cycle (0=new, .25=first quarter, .5=full, .75=last quarter), wraps
345
- * @param {number} [angle] - Angle to rotate by
346
- * @param {boolean} [invert] - The unlit part of the disk instead of the lit part
347
- * @param {number} [sides=glCircleSides] - Number of sides for a full circle (halved per arc)
348
- * @return {Array<Vector2>} - List of points making up the crescent
349
- * @memberof DrawUtilities */
350
- function getCrescentPoints(pos, size=1, percent=0, angle=0, invert=false, sides=glCircleSides)
351
- {
352
- ASSERT(isVector2(pos), 'pos must be a vec2');
353
- ASSERT(isNumber(size) && isNumber(percent), 'size and percent must be numbers');
354
-
355
- // map phase to a signed terminator curve: -1 new, 0 half, 1 full
356
- let p = mod(percent*4, 4); // quarter phase 0..4
357
- if (p >= 2) // second half of cycle flips orientation
358
- angle += PI;
359
- p = p <= 2 ? p-1 : 3-p;
360
- if (invert) // the unlit part: the other side of the same curve
361
- {
362
- p = -p;
363
- angle += PI;
364
- }
365
-
366
- // build the crescent: outer semicircle, then inner half-ellipse traced back
367
- const points = [];
368
- const segs = max(3, sides>>1);
369
- const radius = size/2;
370
- for (let i=0; i<=segs; i++)
371
- {
372
- const t = i/segs*PI;
373
- points.push(vec2(radius*cos(t), radius*sin(t)).rotate(angle).add(pos));
374
- }
375
- for (let i=segs; i>=0; i--)
376
- {
377
- const t = i/segs*PI;
378
- points.push(vec2(radius*cos(t), -radius*p*sin(t)).rotate(angle).add(pos));
379
- }
380
- return points;
1
+ /**
2
+ * LittleJS Drawing Utilities Plugin
3
+ * - Extra drawing functions for LittleJS
4
+ * - Nine slice and three slice drawing, and TileSlice to keep one as a style, like a UI skin
5
+ * @namespace DrawUtilities
6
+ */
7
+
8
+ 'use strict';
9
+
10
+ ///////////////////////////////////////////////////////////////////////////////
11
+
12
+ /** Draw a scalable nine-slice UI element in screen space, drawNineSlice with screenSpace set
13
+ * - Draws with the 2D context by default, on top of what WebGL drew, like drawTextScreen
14
+ * - With no angle its pieces land on whole pixels and meet exactly, so pixel art lines up and extraSpace is not used
15
+ * @param {Vector2} pos - Screen space position
16
+ * @param {Vector2} size - Screen space size
17
+ * @param {TileInfo} startTile - Top-left tile of the 3x3 block to sample (see drawNineSlice)
18
+ * @param {Color} [color=WHITE] - Color to modulate with
19
+ * @param {number} [borderSize] - Rendered thickness of the border sections
20
+ * @param {Color} [additiveColor] - Additive color
21
+ * @param {number} [extraSpace] - Extra spacing adjustment
22
+ * @param {number} [angle] - Angle to rotate by
23
+ * @param {boolean} [useWebGL] - Use WebGL for rendering
24
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context] - Canvas context to use
25
+ * @memberof DrawUtilities */
26
+ function drawNineSliceScreen(pos, size, startTile, color=WHITE, borderSize=32, additiveColor, extraSpace=2, angle=0, useWebGL=false, context)
27
+ {
28
+ // the order before 1.20 had borderSize, extraSpace and angle after startTile
29
+ ASSERT(isColor(color), 'drawNineSliceScreen takes a color after startTile, then borderSize', color);
30
+ drawNineSlice(pos, size, startTile, color, borderSize, additiveColor, extraSpace, angle, useWebGL, true, context);
31
+ }
32
+
33
+ /** Draw a scalable nine-slice UI element in world space
34
+ * Color and additive color apply in WebGL and Canvas2D (with canvasColorTiles)
35
+ * The nine-slice samples a 3x3 block of tiles from the tilesheet, it does not
36
+ * subdivide a single tile. Pass the top-left tile of that block as startTile;
37
+ * the other 8 tiles (edges, corners, and center) are taken automatically from
38
+ * the 3x3 grid of tiles extending right and down from it. borderSize only sets
39
+ * the rendered thickness of the edges and corners, not how the texture is cut.
40
+ * @param {Vector2} pos - World space position
41
+ * @param {Vector2} size - World space size
42
+ * @param {TileInfo} startTile - Top-left tile of the 3x3 block to sample the nine-slice from
43
+ * @param {Color} [color] - Color to modulate with
44
+ * @param {number} [borderSize] - Rendered thickness of the border sections, 1 in world space and 32 pixels in
45
+ * screen space by default
46
+ * @param {Color} [additiveColor] - Additive color
47
+ * @param {number} [extraSpace] - Extra spacing adjustment, .05 in world space and 2 pixels in screen space by
48
+ * default
49
+ * @param {number} [angle] - Angle to rotate by
50
+ * @param {boolean} [useWebGL=glEnable] - Use WebGL for rendering
51
+ * @param {boolean} [screenSpace=drawScreenSpace] - Use screen space coordinates
52
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context] - Canvas context to use
53
+ * @memberof DrawUtilities */
54
+ function drawNineSlice(pos, size, startTile, color, borderSize, additiveColor, extraSpace, angle=0, useWebGL=glEnable, screenSpace=drawScreenSpace, context)
55
+ {
56
+ // the border and spacing defaults are in the units of the space it draws in
57
+ borderSize ??= screenSpace ? 32 : 1;
58
+ extraSpace ??= screenSpace ? 2 : .05;
59
+ if (!size.x || !size.y) return; // nothing to draw, the spacing must not make a sliver of it
60
+ if (color && color.a < 1)
61
+ extraSpace = 0; // see through, the overlap that hides a seam would draw twice and show brighter
62
+ // setup nine slice tiles - startTile is the top-left of a 3x3 tile block,
63
+ // so the center tile is one tile down and right from it, stepping over
64
+ // the padding around each tile the way tile() lays out the grid
65
+ const step = startTile.size.add(vec2(startTile.padding*2));
66
+ if (screenSpace && !angle)
67
+ {
68
+ // on whole pixels, the pieces in the screen's rows and columns of the block
69
+ drawSliceSnapped(pos, size, borderSize, (col, row)=>
70
+ [startTile.offset(step.multiply(vec2(col, row))), 0], color, additiveColor, useWebGL, context);
71
+ return;
72
+ }
73
+ borderSize = min(borderSize, abs(size.x)/2, abs(size.y)/2); // a box too small for two borders splits between them
74
+ const centerTile = startTile.offset(step);
75
+ const centerSize = size.add(vec2(extraSpace-borderSize*2));
76
+ const cornerSize = vec2(borderSize);
77
+ const cornerOffset = size.scale(.5).subtract(cornerSize.scale(.5));
78
+ const flip = screenSpace ? -1 : 1;
79
+ const rotateAngle = screenSpace ? -angle : angle;
80
+
81
+ // center
82
+ drawTile(pos, centerSize, centerTile, color, angle, false, additiveColor, useWebGL, screenSpace, context);
83
+ for (let i=4; i--;)
84
+ {
85
+ // sides
86
+ const horizontal = i%2;
87
+ const sidePos = cornerOffset.multiply(vec2(horizontal?i===1?1:-1:0, horizontal?0:i?-1:1));
88
+ const sideSize = vec2(horizontal ? borderSize : centerSize.x, horizontal ? centerSize.y : borderSize);
89
+ const sideTile = centerTile.offset(step.multiply(vec2(i===1?1:i===3?-1:0,i===0?-flip:i===2?flip:0)))
90
+ drawTile(pos.add(sidePos.rotate(rotateAngle)), sideSize, sideTile, color, angle, false, additiveColor, useWebGL, screenSpace, context);
91
+ }
92
+ for (let i=4; i--;)
93
+ {
94
+ // corners
95
+ const flipX = i>1;
96
+ const flipY = i && i<3;
97
+ const cornerPos = cornerOffset.multiply(vec2(flipX?-1:1, flipY?-1:1));
98
+ const cornerTile = centerTile.offset(step.multiply(vec2(flipX?-1:1,flipY?flip:-flip)));
99
+ drawTile(pos.add(cornerPos.rotate(rotateAngle)), cornerSize, cornerTile, color, angle, false, additiveColor, useWebGL, screenSpace, context);
100
+ }
101
+ }
102
+
103
+ /** Draw a scalable three-slice UI element in screen space, drawThreeSlice with screenSpace set
104
+ * - Draws with the 2D context by default, on top of what WebGL drew, like drawTextScreen
105
+ * - With no angle its pieces land on whole pixels and meet exactly, so pixel art lines up and extraSpace is not used
106
+ * @param {Vector2} pos - Screen space position
107
+ * @param {Vector2} size - Screen space size
108
+ * @param {TileInfo} startTile - First of 3 consecutive tiles: corner, side, center (see drawThreeSlice)
109
+ * @param {Color} [color=WHITE] - Color to modulate with
110
+ * @param {number} [borderSize] - Rendered thickness of the border sections
111
+ * @param {Color} [additiveColor] - Additive color
112
+ * @param {number} [extraSpace] - Extra spacing adjustment
113
+ * @param {number} [angle] - Angle to rotate by
114
+ * @param {boolean} [useWebGL] - Use WebGL for rendering
115
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context] - Canvas context to use
116
+ * @memberof DrawUtilities */
117
+ function drawThreeSliceScreen(pos, size, startTile, color=WHITE, borderSize=32, additiveColor, extraSpace=2, angle=0, useWebGL=false, context)
118
+ {
119
+ // the order before 1.20 had borderSize, extraSpace and angle after startTile
120
+ ASSERT(isColor(color), 'drawThreeSliceScreen takes a color after startTile, then borderSize', color);
121
+ drawThreeSlice(pos, size, startTile, color, borderSize, additiveColor, extraSpace, angle, useWebGL, true, context);
122
+ }
123
+
124
+ /** Draw a scalable three-slice UI element in world space
125
+ * Color and additive color apply in WebGL and Canvas2D (with canvasColorTiles)
126
+ * The three-slice samples 3 consecutive tiles from the tilesheet, it does not
127
+ * subdivide a single tile. Pass the first tile as startTile; the three tiles
128
+ * are used in order as corner, side, and center, then rotated and mirrored to
129
+ * build all four edges and corners. borderSize only sets the rendered thickness.
130
+ * @param {Vector2} pos - World space position
131
+ * @param {Vector2} size - World space size
132
+ * @param {TileInfo} startTile - First of 3 consecutive tiles (corner, side, center) for the three-slice
133
+ * @param {Color} [color] - Color to modulate with
134
+ * @param {number} [borderSize] - Rendered thickness of the border sections, 1 in world space and 32 pixels in
135
+ * screen space by default
136
+ * @param {Color} [additiveColor] - Additive color
137
+ * @param {number} [extraSpace] - Extra spacing adjustment, .05 in world space and 2 pixels in screen space by
138
+ * default
139
+ * @param {number} [angle] - Angle to rotate by
140
+ * @param {boolean} [useWebGL=glEnable] - Use WebGL for rendering
141
+ * @param {boolean} [screenSpace=drawScreenSpace] - Use screen space coordinates
142
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context] - Canvas context to use
143
+ * @memberof DrawUtilities */
144
+ function drawThreeSlice(pos, size, startTile, color, borderSize, additiveColor, extraSpace, angle=0, useWebGL=glEnable, screenSpace=drawScreenSpace, context)
145
+ {
146
+ // the border and spacing defaults are in the units of the space it draws in
147
+ borderSize ??= screenSpace ? 32 : 1;
148
+ extraSpace ??= screenSpace ? 2 : .05;
149
+ if (!size.x || !size.y) return; // nothing to draw, the spacing must not make a sliver of it
150
+ if (color && color.a < 1)
151
+ extraSpace = 0; // see through, the overlap that hides a seam would draw twice and show brighter
152
+ // setup three slice tiles - 3 tiles in a row starting at startTile
153
+ const cornerTile = startTile.frame(0);
154
+ const sideTile = startTile.frame(1);
155
+ const centerTile = startTile.frame(2);
156
+ if (screenSpace && !angle)
157
+ {
158
+ // on whole pixels, each corner and side the top left one's tile turned a quarter more going clockwise
159
+ const turns = [[0,0,1],[3,-1,1],[3,2,2]]; // by column in each row, -1 for the center
160
+ drawSliceSnapped(pos, size, borderSize, (col, row)=>
161
+ {
162
+ const turn = turns[row][col], corner = col !== 1 && row !== 1;
163
+ return turn < 0 ? [centerTile, 0] : [corner ? cornerTile : sideTile, turn*PI/2];
164
+ }, color, additiveColor, useWebGL, context);
165
+ return;
166
+ }
167
+ borderSize = min(borderSize, abs(size.x)/2, abs(size.y)/2); // a box too small for two borders splits between them
168
+ const centerSize = size.add(vec2(extraSpace-borderSize*2));
169
+ const cornerSize = vec2(borderSize);
170
+ const cornerOffset = size.scale(.5).subtract(cornerSize.scale(.5));
171
+ const flip = screenSpace ? -1 : 1;
172
+ const rotateAngle = screenSpace ? -angle : angle;
173
+
174
+ // center
175
+ drawTile(pos, centerSize, centerTile, color, angle, false, additiveColor, useWebGL, screenSpace, context);
176
+ for (let i=4; i--;)
177
+ {
178
+ // sides
179
+ const a = angle + i*PI/2;
180
+ const horizontal = i%2;
181
+ const sidePos = cornerOffset.multiply(vec2(horizontal?i===1?1:-1:0, horizontal?0:i?-flip:flip));
182
+ const sideSize = vec2(horizontal ? centerSize.y : centerSize.x, borderSize);
183
+ drawTile(pos.add(sidePos.rotate(rotateAngle)), sideSize, sideTile, color, a, false, additiveColor, useWebGL, screenSpace, context);
184
+ }
185
+ for (let i=4; i--;)
186
+ {
187
+ // corners
188
+ const a = angle + i*PI/2;
189
+ const flipX = !i || i>2;
190
+ const flipY = i>1;
191
+ const cornerPos = cornerOffset.multiply(vec2(flipX?-1:1, flipY?-flip:flip));
192
+ drawTile(pos.add(cornerPos.rotate(rotateAngle)), cornerSize, cornerTile, color, a, false, additiveColor, useWebGL, screenSpace, context);
193
+ }
194
+ }
195
+
196
+ ///////////////////////////////////////////////////////////////////////////////
197
+
198
+ /**
199
+ * A tile drawn as a box of any size, kept as a style to draw with, like a UI skin
200
+ * - 9 slices is a nine-slice from the 3x3 block of tiles at tileInfo, see drawNineSlice
201
+ * - 3 slices is a three-slice from the 3 tiles in a row at tileInfo, see drawThreeSlice
202
+ * - 1 slice is the whole tile stretched over the box, a plain image
203
+ * - The UI system draws a widget's background with one, see uiSystem.defaultSlice
204
+ * @memberof DrawUtilities
205
+ * @example
206
+ * const panel = new TileSlice(tile(0, 16), 9, .5); // a border of half a world unit
207
+ * panel.draw(vec2(0, 5), vec2(10, 4));
208
+ * uiSystem.defaultSlice = new TileSlice(tile(0, 16), 9, 16); // UI sizes are pixels, every widget made after this
209
+ */
210
+ class TileSlice
211
+ {
212
+ /** Create a tile slice style
213
+ * @param {TileInfo} tileInfo - The tile, or the first of the tiles, to draw with
214
+ * @param {number} [slices] - 9 for a nine-slice, 3 for a three-slice, 1 for the whole tile
215
+ * @param {number} [borderSize] - Drawn thickness of the edges and corners, undefined for the draw's own default
216
+ * @param {number} [extraSpace] - Extra spacing adjustment of the slices, undefined for the draw's own default */
217
+ constructor(tileInfo, slices=9, borderSize, extraSpace)
218
+ {
219
+ ASSERT(tileInfo instanceof TileInfo, 'tileInfo must be a TileInfo');
220
+ ASSERT(slices === 9 || slices === 3 || slices === 1, 'slices must be 9, 3 or 1');
221
+ ASSERT(borderSize === undefined || isNumber(borderSize), 'borderSize must be a number');
222
+
223
+ /** @property {TileInfo} - The tile, or the first of the tiles, to draw with */
224
+ this.tileInfo = tileInfo;
225
+ /** @property {number} - 9 for a nine-slice, 3 for a three-slice, 1 for the whole tile */
226
+ this.slices = slices;
227
+ /** @property {number|undefined} - Drawn thickness of the edges and corners, undefined for the draw's default
228
+ * @type {number|undefined} */
229
+ this.borderSize = borderSize;
230
+ /** @property {number|undefined} - Extra spacing adjustment of the slices, undefined for the draw's default
231
+ * @type {number|undefined} */
232
+ this.extraSpace = extraSpace;
233
+ }
234
+
235
+ /** Draw it as a box in world space, or in screen space
236
+ * @param {Vector2} pos - Center position
237
+ * @param {Vector2} size - Size of the box
238
+ * @param {Color} [color] - Color to modulate with
239
+ * @param {Color} [additiveColor] - Additive color
240
+ * @param {number} [angle] - Angle to rotate by
241
+ * @param {boolean} [useWebGL=glEnable] - Use WebGL for rendering
242
+ * @param {boolean} [screenSpace=drawScreenSpace] - Are pos and size in screen space?
243
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context] - Canvas context to use */
244
+ draw(pos, size, color=WHITE, additiveColor, angle=0, useWebGL=glEnable, screenSpace=drawScreenSpace, context)
245
+ {
246
+ if (screenSpace) // with the screen space defaults for the border and spacing
247
+ return this.drawScreen(pos, size, color, additiveColor, angle, useWebGL, context);
248
+ if (this.slices === 9)
249
+ drawNineSlice(pos, size, this.tileInfo, color, this.borderSize, additiveColor, this.extraSpace, angle, useWebGL, screenSpace, context);
250
+ else if (this.slices === 3)
251
+ drawThreeSlice(pos, size, this.tileInfo, color, this.borderSize, additiveColor, this.extraSpace, angle, useWebGL, screenSpace, context);
252
+ else
253
+ drawTile(pos, size, this.tileInfo, color, angle, false, additiveColor, useWebGL, screenSpace, context);
254
+ }
255
+
256
+ /** Draw it as a box in screen space, with the 2D context by default, on top of what WebGL drew, like drawTextScreen
257
+ * @param {Vector2} pos - Screen space center position
258
+ * @param {Vector2} size - Screen space size
259
+ * @param {Color} [color] - Color to modulate with
260
+ * @param {Color} [additiveColor] - Additive color
261
+ * @param {number} [angle] - Angle to rotate by
262
+ * @param {boolean} [useWebGL] - Use WebGL for rendering
263
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context] - Canvas context to use */
264
+ drawScreen(pos, size, color=WHITE, additiveColor, angle=0, useWebGL=false, context)
265
+ {
266
+ if (this.slices === 9)
267
+ drawNineSliceScreen(pos, size, this.tileInfo, color, this.borderSize, additiveColor, this.extraSpace, angle, useWebGL, context);
268
+ else if (this.slices === 3)
269
+ drawThreeSliceScreen(pos, size, this.tileInfo, color, this.borderSize, additiveColor, this.extraSpace, angle, useWebGL, context);
270
+ else
271
+ drawTile(pos, size, this.tileInfo, color, angle, false, additiveColor, useWebGL, true, context);
272
+ }
273
+ }
274
+
275
+ // draw a slice box in screen space with no angle on whole pixels: the edges and the border are rounded, and every
276
+ // piece is cut from them, so the pieces meet exactly and their texels line up where they do, with no overlap to hide
277
+ // a seam; pieceTile(col, row) gives the tile and angle for the piece in that column and row of the box, top left 0
278
+ function drawSliceSnapped(pos, size, borderSize, pieceTile, color, additiveColor, useWebGL, context)
279
+ {
280
+ // the pixels it lands on are the device's: a 2D context's own scale and offset, like the UI under nativeHeight
281
+ // centered on an odd sized canvas, or the pixel ratio; the draws land half a pixel past, see shift below
282
+ const canvas2D = !useWebGL || !glEnable || !!context;
283
+ const transform = canvas2D && (context || drawContext)?.getTransform?.();
284
+ const ratio = getCanvasPixelRatio();
285
+ const [scaleX, scaleY, offsetX, offsetY] = transform ?
286
+ [transform.a || 1, transform.d || 1, transform.e || 0, transform.f || 0] : [ratio, ratio, 0, 0];
287
+ const edges = (center, length, scale, offset)=>
288
+ {
289
+ // snapped in device order, which a mirrored context reverses, then put back in the box's own order
290
+ const border = max(1, round(borderSize * abs(scale)));
291
+ const a = round((center - length/2) * scale + offset), b = a + round(length * scale);
292
+ const start = min(a, b), end = max(a, b);
293
+ const inner = min(start + border, floor((start + end)/2)); // a box too small for two borders splits
294
+ const device = [start, inner, max(end - border, inner), end];
295
+ return (scale < 0 ? device.reverse() : device).map(edge=> (edge - offset) / scale);
296
+ };
297
+ const xs = edges(pos.x, size.x, scaleX, offsetX), ys = edges(pos.y, size.y, scaleY, offsetY);
298
+ const shift = .5; // a screen space draw lands half a pixel past its position, in Canvas2D and WebGL alike
299
+ for (let row = 3; row--;)
300
+ for (let col = 3; col--;)
301
+ {
302
+ const w = xs[col+1] - xs[col], h = ys[row+1] - ys[row];
303
+ if (w <= 0 || h <= 0) continue;
304
+ const [tileInfo, angle] = pieceTile(col, row);
305
+ const turned = round(angle / (PI/2)) % 2; // a quarter turn swaps the sides it is drawn with
306
+ const piecePos = vec2(xs[col] + w/2 - shift, ys[row] + h/2 - shift);
307
+ drawTile(piecePos, turned ? vec2(h, w) : vec2(w, h), tileInfo, color, angle, false, additiveColor, useWebGL, true, context);
308
+ }
309
+ }
310
+
311
+ /** Draw a crescent / moon-phase shape built from a polygon
312
+ * Routes through drawPoly, so it supports WebGL, screen space, color, and outlines
313
+ * - At angle 0 the lit side faces up while waxing (percent below .5) and down while waning, and the lit width
314
+ * grows evenly with the phase, not as the real moon's does
315
+ * @param {Vector2} pos - Center position
316
+ * @param {number} [size] - Diameter
317
+ * @param {number} [percent] - Moon phase over a full cycle (0=new, .25=first quarter, .5=full, .75=last quarter), wraps
318
+ * @param {Color} [color] - Fill color
319
+ * @param {number} [angle] - Angle to rotate by
320
+ * @param {boolean} [invert] - Draw the unlit part of the disk instead of the lit part
321
+ * @param {number} [lineWidth] - Outline width, 0 for no outline
322
+ * @param {Color} [lineColor] - Outline color
323
+ * @param {boolean} [useWebGL=glEnable] - Use WebGL for rendering
324
+ * @param {boolean} [screenSpace=drawScreenSpace] - Use screen space coordinates
325
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context] - Canvas context to use
326
+ * @memberof DrawUtilities */
327
+ function drawCrescent(pos, size=1, percent=0, color=WHITE, angle=0, invert=false, lineWidth=0, lineColor=BLACK, useWebGL=glEnable, screenSpace=drawScreenSpace, context)
328
+ {
329
+ // build local-space points and let drawPoly apply pos/angle so screen space works
330
+ const points = getCrescentPoints(vec2(), size, percent, 0, invert);
331
+ drawPoly(points, color, lineWidth, lineColor, pos, angle, useWebGL, screenSpace, context);
332
+ }
333
+
334
+ /** Get the list of points that make up a crescent / moon-phase shape
335
+ * Returns world-space points with pos and angle baked in, ready for drawPoly or other use
336
+ * @param {Vector2} pos - Center position
337
+ * @param {number} [size] - Diameter
338
+ * @param {number} [percent] - Moon phase over a full cycle (0=new, .25=first quarter, .5=full, .75=last quarter), wraps
339
+ * @param {number} [angle] - Angle to rotate by
340
+ * @param {boolean} [invert] - The unlit part of the disk instead of the lit part
341
+ * @param {number} [sides=glCircleSides] - Number of sides for a full circle (halved per arc)
342
+ * @return {Array<Vector2>} - List of points making up the crescent
343
+ * @memberof DrawUtilities */
344
+ function getCrescentPoints(pos, size=1, percent=0, angle=0, invert=false, sides=glCircleSides)
345
+ {
346
+ ASSERT(isVector2(pos), 'pos must be a vec2');
347
+ ASSERT(isNumber(size) && isNumber(percent), 'size and percent must be numbers');
348
+
349
+ // map phase to a signed terminator curve: -1 new, 0 half, 1 full
350
+ let p = mod(percent*4, 4); // quarter phase 0..4
351
+ if (p >= 2) // second half of cycle flips orientation
352
+ angle += PI;
353
+ p = p <= 2 ? p-1 : 3-p;
354
+ if (invert) // the unlit part: the other side of the same curve
355
+ {
356
+ p = -p;
357
+ angle += PI;
358
+ }
359
+
360
+ // build the crescent: outer semicircle, then inner half-ellipse traced back
361
+ const points = [];
362
+ const segs = max(3, sides>>1);
363
+ const radius = size/2;
364
+ for (let i=0; i<=segs; i++)
365
+ {
366
+ const t = i/segs*PI;
367
+ points.push(vec2(radius*cos(t), radius*sin(t)).rotate(angle).add(pos));
368
+ }
369
+ for (let i=segs; i>=0; i--)
370
+ {
371
+ const t = i/segs*PI;
372
+ points.push(vec2(radius*cos(t), -radius*p*sin(t)).rotate(angle).add(pos));
373
+ }
374
+ return points;
381
375
  }