littlejsengine 1.15.2 → 1.15.3

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.
@@ -216,8 +216,9 @@ declare module "littlejsengine" {
216
216
  * @param {number} [time]
217
217
  * @param {number} [angle]
218
218
  * @param {boolean} [fill]
219
+ * @param {boolean} [screenSpace]
219
220
  * @memberof Debug */
220
- export function debugRect(pos: Vector2, size?: Vector2, color?: Color | string, time?: number, angle?: number, fill?: boolean): void;
221
+ export function debugRect(pos: Vector2, size?: Vector2, color?: Color | string, time?: number, angle?: number, fill?: boolean, screenSpace?: boolean): void;
221
222
  /** Draw a debug poly in world space
222
223
  * @param {Vector2} pos
223
224
  * @param {Array<Vector2>} points
@@ -225,39 +226,45 @@ declare module "littlejsengine" {
225
226
  * @param {number} [time]
226
227
  * @param {number} [angle]
227
228
  * @param {boolean} [fill]
229
+ * @param {boolean} [screenSpace]
228
230
  * @memberof Debug */
229
- export function debugPoly(pos: Vector2, points: Array<Vector2>, color?: Color | string, time?: number, angle?: number, fill?: boolean): void;
231
+ export function debugPoly(pos: Vector2, points: Array<Vector2>, color?: Color | string, time?: number, angle?: number, fill?: boolean, screenSpace?: boolean): void;
230
232
  /** Draw a debug circle in world space
231
233
  * @param {Vector2} pos
232
234
  * @param {number} [size] - diameter
233
235
  * @param {Color|string} [color]
234
236
  * @param {number} [time]
235
237
  * @param {boolean} [fill]
238
+ * @param {boolean} [screenSpace]
236
239
  * @memberof Debug */
237
- export function debugCircle(pos: Vector2, size?: number, color?: Color | string, time?: number, fill?: boolean): void;
240
+ export function debugCircle(pos: Vector2, size?: number, color?: Color | string, time?: number, fill?: boolean, screenSpace?: boolean): void;
238
241
  /** Draw a debug point in world space
239
242
  * @param {Vector2} pos
240
243
  * @param {Color|string} [color]
241
244
  * @param {number} [time]
242
245
  * @param {number} [angle]
246
+ * @param {boolean} [screenSpace]
243
247
  * @memberof Debug */
244
- export function debugPoint(pos: Vector2, color?: Color | string, time?: number, angle?: number): void;
248
+ export function debugPoint(pos: Vector2, color?: Color | string, time?: number, angle?: number, screenSpace?: boolean): void;
245
249
  /** Draw a debug line in world space
246
250
  * @param {Vector2} posA
247
251
  * @param {Vector2} posB
248
252
  * @param {Color|string} [color]
249
253
  * @param {number} [width]
250
254
  * @param {number} [time]
255
+ * @param {boolean} [screenSpace]
251
256
  * @memberof Debug */
252
- export function debugLine(posA: Vector2, posB: Vector2, color?: Color | string, width?: number, time?: number): void;
257
+ export function debugLine(posA: Vector2, posB: Vector2, color?: Color | string, width?: number, time?: number, screenSpace?: boolean): void;
253
258
  /** Draw a debug combined axis aligned bounding box in world space
254
259
  * @param {Vector2} posA
255
260
  * @param {Vector2} sizeA
256
261
  * @param {Vector2} posB
257
262
  * @param {Vector2} sizeB
258
263
  * @param {Color|string} [color]
264
+ * @param {number} [time]
265
+ * @param {boolean} [screenSpace]
259
266
  * @memberof Debug */
260
- export function debugOverlap(posA: Vector2, sizeA: Vector2, posB: Vector2, sizeB: Vector2, color?: Color | string): void;
267
+ export function debugOverlap(posA: Vector2, sizeA: Vector2, posB: Vector2, sizeB: Vector2, color?: Color | string, time?: number, screenSpace?: boolean): void;
261
268
  /** Draw a debug axis aligned bounding box in world space
262
269
  * @param {string} text
263
270
  * @param {Vector2} pos
@@ -266,8 +273,9 @@ declare module "littlejsengine" {
266
273
  * @param {number} [time]
267
274
  * @param {number} [angle]
268
275
  * @param {string} [font]
276
+ * @param {boolean} [screenSpace]
269
277
  * @memberof Debug */
270
- export function debugText(text: string, pos: Vector2, size?: number, color?: Color | string, time?: number, angle?: number, font?: string): void;
278
+ export function debugText(text: string, pos: Vector2, size?: number, color?: Color | string, time?: number, angle?: number, font?: string, screenSpace?: boolean): void;
271
279
  /** Clear all debug primitives in the list
272
280
  * @memberof Debug */
273
281
  export function debugClear(): void;
@@ -3146,6 +3154,12 @@ declare module "littlejsengine" {
3146
3154
  defaultSoundRelease: any;
3147
3155
  /** @property {Sound} - Default sound when interactive UI element is clicked */
3148
3156
  defaultSoundClick: any;
3157
+ /** @property {Color} - Color for shadow */
3158
+ defaultShadowColor: Color;
3159
+ /** @property {number} - Size of shadow blur */
3160
+ defaultShadowBlur: number;
3161
+ /** @property {Vector2} - Offset of shadow blur */
3162
+ defaultShadowOffset: Vector2;
3149
3163
  /** @property {Array<UIObject>} - List of all UI elements */
3150
3164
  uiObjects: any[];
3151
3165
  /** @property {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} - Context to render UI elements to */
@@ -3165,8 +3179,11 @@ declare module "littlejsengine" {
3165
3179
  * @param {number} [lineWidth]
3166
3180
  * @param {Color} [lineColor]
3167
3181
  * @param {number} [cornerRadius]
3168
- * @param {Color} [gradientColor] */
3169
- drawRect(pos: Vector2, size: Vector2, color?: Color, lineWidth?: number, lineColor?: Color, cornerRadius?: number, gradientColor?: Color): void;
3182
+ * @param {Color} [gradientColor]
3183
+ * @param {Color} [shadowColor]
3184
+ * @param {number} [shadowBlur]
3185
+ * @param {Color} [shadowOffset] */
3186
+ drawRect(pos: Vector2, size: Vector2, color?: Color, lineWidth?: number, lineColor?: Color, cornerRadius?: number, gradientColor?: Color, shadowColor?: Color, shadowBlur?: number, shadowOffset?: Color): void;
3170
3187
  /** Draw a line to the UI context
3171
3188
  * @param {Vector2} posA
3172
3189
  * @param {Vector2} posB
@@ -3283,6 +3300,12 @@ declare module "littlejsengine" {
3283
3300
  dragActivate: boolean;
3284
3301
  /** @property {boolean} - True if this can be a hover object */
3285
3302
  canBeHover: boolean;
3303
+ /** @property {Color} - Color for shadow, undefined if no shadow */
3304
+ shadowColor: Color;
3305
+ /** @property {number} - Size of shadow blur */
3306
+ shadowBlur: number;
3307
+ /** @property {Vector2} - Offset of shadow blur */
3308
+ shadowOffset: Vector2;
3286
3309
  /** @property {Vector2} - How much to offset the text shadow or undefined */
3287
3310
  textShadow: any;
3288
3311
  /** Add a child UIObject to this object
@@ -33,7 +33,7 @@ const engineName = 'LittleJS';
33
33
  * @type {string}
34
34
  * @default
35
35
  * @memberof Engine */
36
- const engineVersion = '1.15.2';
36
+ const engineVersion = '1.15.3';
37
37
 
38
38
  /** Frames per second to update
39
39
  * @type {number}
@@ -790,8 +790,9 @@ function LOG(...output) { console.log(...output); }
790
790
  * @param {number} [time]
791
791
  * @param {number} [angle]
792
792
  * @param {boolean} [fill]
793
+ * @param {boolean} [screenSpace]
793
794
  * @memberof Debug */
794
- function debugRect(pos, size=vec2(), color=WHITE, time=0, angle=0, fill=false)
795
+ function debugRect(pos, size=vec2(), color=WHITE, time=0, angle=0, fill=false, screenSpace=false)
795
796
  {
796
797
  ASSERT(isVector2(pos), 'pos must be a vec2');
797
798
  ASSERT(isVector2(size), 'size must be a vec2');
@@ -806,7 +807,7 @@ function debugRect(pos, size=vec2(), color=WHITE, time=0, angle=0, fill=false)
806
807
  pos = pos.copy();
807
808
  size = size.copy();
808
809
  const timer = new Timer(time);
809
- debugPrimitives.push({pos:pos.copy(), size:size.copy(), color, timer, angle, fill});
810
+ debugPrimitives.push({pos:pos.copy(), size:size.copy(), color, timer, angle, fill, screenSpace});
810
811
  }
811
812
 
812
813
  /** Draw a debug poly in world space
@@ -816,8 +817,9 @@ function debugRect(pos, size=vec2(), color=WHITE, time=0, angle=0, fill=false)
816
817
  * @param {number} [time]
817
818
  * @param {number} [angle]
818
819
  * @param {boolean} [fill]
820
+ * @param {boolean} [screenSpace]
819
821
  * @memberof Debug */
820
- function debugPoly(pos, points, color=WHITE, time=0, angle=0, fill=false)
822
+ function debugPoly(pos, points, color=WHITE, time=0, angle=0, fill=false, screenSpace=false)
821
823
  {
822
824
  ASSERT(isVector2(pos), 'pos must be a vec2');
823
825
  ASSERT(isArray(points), 'points must be an array');
@@ -830,7 +832,7 @@ function debugPoly(pos, points, color=WHITE, time=0, angle=0, fill=false)
830
832
  pos = pos.copy();
831
833
  points = points.map(p=>p.copy());
832
834
  const timer = new Timer(time);
833
- debugPrimitives.push({pos, points, color, timer, angle, fill});
835
+ debugPrimitives.push({pos, points, color, timer, angle, fill, screenSpace});
834
836
  }
835
837
 
836
838
  /** Draw a debug circle in world space
@@ -839,8 +841,9 @@ function debugPoly(pos, points, color=WHITE, time=0, angle=0, fill=false)
839
841
  * @param {Color|string} [color]
840
842
  * @param {number} [time]
841
843
  * @param {boolean} [fill]
844
+ * @param {boolean} [screenSpace]
842
845
  * @memberof Debug */
843
- function debugCircle(pos, size=0, color=WHITE, time=0, fill=false)
846
+ function debugCircle(pos, size=0, color=WHITE, time=0, fill=false, screenSpace=false)
844
847
  {
845
848
  ASSERT(isVector2(pos), 'pos must be a vec2');
846
849
  ASSERT(isNumber(size), 'size must be a number');
@@ -851,7 +854,7 @@ function debugCircle(pos, size=0, color=WHITE, time=0, fill=false)
851
854
  color = color.toString();
852
855
  pos = pos.copy();
853
856
  const timer = new Timer(time);
854
- debugPrimitives.push({pos, size, color, timer, angle:0, fill});
857
+ debugPrimitives.push({pos, size, color, timer, angle:0, fill, screenSpace});
855
858
  }
856
859
 
857
860
  /** Draw a debug point in world space
@@ -859,9 +862,10 @@ function debugCircle(pos, size=0, color=WHITE, time=0, fill=false)
859
862
  * @param {Color|string} [color]
860
863
  * @param {number} [time]
861
864
  * @param {number} [angle]
865
+ * @param {boolean} [screenSpace]
862
866
  * @memberof Debug */
863
- function debugPoint(pos, color, time, angle)
864
- { debugRect(pos, undefined, color, time, angle); }
867
+ function debugPoint(pos, color, time, angle, screenSpace=false)
868
+ { debugRect(pos, undefined, color, time, angle, false, screenSpace); }
865
869
 
866
870
  /** Draw a debug line in world space
867
871
  * @param {Vector2} posA
@@ -869,8 +873,9 @@ function debugPoint(pos, color, time, angle)
869
873
  * @param {Color|string} [color]
870
874
  * @param {number} [width]
871
875
  * @param {number} [time]
876
+ * @param {boolean} [screenSpace]
872
877
  * @memberof Debug */
873
- function debugLine(posA, posB, color, width=.1, time)
878
+ function debugLine(posA, posB, color, width=.1, time, screenSpace=false)
874
879
  {
875
880
  ASSERT(isVector2(posA), 'posA must be a vec2');
876
881
  ASSERT(isVector2(posB), 'posB must be sa vec2');
@@ -878,7 +883,7 @@ function debugLine(posA, posB, color, width=.1, time)
878
883
 
879
884
  const halfDelta = vec2((posB.x - posA.x)/2, (posB.y - posA.y)/2);
880
885
  const size = vec2(width, halfDelta.length()*2);
881
- debugRect(posA.add(halfDelta), size, color, time, halfDelta.angle(), true);
886
+ debugRect(posA.add(halfDelta), size, color, time, halfDelta.angle(), true, screenSpace);
882
887
  }
883
888
 
884
889
  /** Draw a debug combined axis aligned bounding box in world space
@@ -887,8 +892,10 @@ function debugLine(posA, posB, color, width=.1, time)
887
892
  * @param {Vector2} posB
888
893
  * @param {Vector2} sizeB
889
894
  * @param {Color|string} [color]
895
+ * @param {number} [time]
896
+ * @param {boolean} [screenSpace]
890
897
  * @memberof Debug */
891
- function debugOverlap(posA, sizeA, posB, sizeB, color)
898
+ function debugOverlap(posA, sizeA, posB, sizeB, color, time, screenSpace=false)
892
899
  {
893
900
  ASSERT(isVector2(posA), 'posA must be a vec2');
894
901
  ASSERT(isVector2(posB), 'posB must be a vec2');
@@ -903,7 +910,7 @@ function debugOverlap(posA, sizeA, posB, sizeB, color)
903
910
  max(posA.x + sizeA.x/2, posB.x + sizeB.x/2),
904
911
  max(posA.y + sizeA.y/2, posB.y + sizeB.y/2)
905
912
  );
906
- debugRect(minPos.lerp(maxPos,.5), maxPos.subtract(minPos), color);
913
+ debugRect(minPos.lerp(maxPos,.5), maxPos.subtract(minPos), color, time, 0, false, screenSpace);
907
914
  }
908
915
 
909
916
  /** Draw a debug axis aligned bounding box in world space
@@ -914,8 +921,9 @@ function debugOverlap(posA, sizeA, posB, sizeB, color)
914
921
  * @param {number} [time]
915
922
  * @param {number} [angle]
916
923
  * @param {string} [font]
924
+ * @param {boolean} [screenSpace]
917
925
  * @memberof Debug */
918
- function debugText(text, pos, size=1, color=WHITE, time=0, angle=0, font='monospace')
926
+ function debugText(text, pos, size=1, color=WHITE, time=0, angle=0, font='monospace', screenSpace=false)
919
927
  {
920
928
  ASSERT(isString(text), 'text must be a string');
921
929
  ASSERT(isVector2(pos), 'pos must be a vec2');
@@ -929,7 +937,7 @@ function debugText(text, pos, size=1, color=WHITE, time=0, angle=0, font='monosp
929
937
  color = color.toString();
930
938
  pos = pos.copy();
931
939
  const timer = new Timer(time);
932
- debugPrimitives.push({text, pos, size, color, timer, angle, font});
940
+ debugPrimitives.push({text, pos, size, color, timer, angle, font, screenSpace});
933
941
  }
934
942
 
935
943
  /** Clear all debug primitives in the list
@@ -1139,21 +1147,26 @@ function debugRender()
1139
1147
  {
1140
1148
  // draw debug primitives
1141
1149
  overlayContext.lineWidth = 2;
1142
- const pointSize = debugPointSize * cameraScale;
1143
1150
  debugPrimitives.forEach(p=>
1144
1151
  {
1145
1152
  overlayContext.save();
1146
1153
 
1147
1154
  // create canvas transform from world space to screen space
1148
- const pos = worldToScreen(p.pos);
1155
+ // without scaling because we want consistent pixel sizes
1156
+ let pos = p.pos, scale = 1, angle = p.angle;
1157
+ if (!p.screenSpace)
1158
+ {
1159
+ pos = worldToScreen(p.pos);
1160
+ scale = cameraScale;
1161
+ angle -= cameraAngle;
1162
+ }
1149
1163
  overlayContext.translate(pos.x|0, pos.y|0);
1150
- overlayContext.rotate(p.angle);
1164
+ overlayContext.rotate(angle);
1151
1165
  overlayContext.scale(1, p.text ? 1 : -1);
1152
1166
  overlayContext.fillStyle = overlayContext.strokeStyle = p.color;
1153
-
1154
1167
  if (p.text !== undefined)
1155
1168
  {
1156
- overlayContext.font = p.size*cameraScale + 'px '+ p.font;
1169
+ overlayContext.font = p.size*scale + 'px '+ p.font;
1157
1170
  overlayContext.textAlign = 'center';
1158
1171
  overlayContext.textBaseline = 'middle';
1159
1172
  overlayContext.fillText(p.text, 0, 0);
@@ -1164,7 +1177,7 @@ function debugRender()
1164
1177
  overlayContext.beginPath();
1165
1178
  for (const point of p.points)
1166
1179
  {
1167
- const p2 = point.scale(cameraScale).floor();
1180
+ const p2 = point.scale(scale).floor();
1168
1181
  overlayContext.lineTo(p2.x, p2.y);
1169
1182
  }
1170
1183
  overlayContext.closePath();
@@ -1174,13 +1187,14 @@ function debugRender()
1174
1187
  else if (p.size === 0 || (p.size.x === 0 && p.size.y === 0))
1175
1188
  {
1176
1189
  // point
1190
+ const pointSize = debugPointSize * scale;
1177
1191
  overlayContext.fillRect(-pointSize/2, -1, pointSize, 3);
1178
1192
  overlayContext.fillRect(-1, -pointSize/2, 3, pointSize);
1179
1193
  }
1180
1194
  else if (p.size.x !== undefined)
1181
1195
  {
1182
1196
  // rect
1183
- const s = p.size.scale(cameraScale).floor();
1197
+ const s = p.size.scale(scale).floor();
1184
1198
  const w = s.x, h = s.y;
1185
1199
  p.fill && overlayContext.fillRect(-w/2|0, -h/2|0, w, h);
1186
1200
  overlayContext.strokeRect(-w/2|0, -h/2|0, w, h);
@@ -1189,7 +1203,7 @@ function debugRender()
1189
1203
  {
1190
1204
  // circle
1191
1205
  overlayContext.beginPath();
1192
- overlayContext.arc(0, 0, p.size*cameraScale/2, 0, 9);
1206
+ overlayContext.arc(0, 0, p.size*scale/2, 0, 9);
1193
1207
  p.fill && overlayContext.fill();
1194
1208
  overlayContext.stroke();
1195
1209
  }
@@ -3644,7 +3658,9 @@ class EngineObject
3644
3658
  {
3645
3659
  ASSERT(child.parent === this && this.children.includes(child));
3646
3660
  ASSERT(child instanceof EngineObject, 'child must be an EngineObject');
3647
- this.children.splice(this.children.indexOf(child), 1);
3661
+ const index = this.children.indexOf(child);
3662
+ ASSERT(index >= 0, 'child not found in children array');
3663
+ index >= 0 && this.children.splice(index, 1);
3648
3664
  child.parent = 0;
3649
3665
  }
3650
3666
 
@@ -3963,15 +3979,11 @@ function drawTile(pos, size=new Vector2(1), tileInfo, color=WHITE,
3963
3979
 
3964
3980
  const textureInfo = tileInfo && tileInfo.textureInfo;
3965
3981
  const bleedScale = tileInfo ? tileInfo.bleedScale : 0;
3966
- if (useWebGL)
3982
+ if (useWebGL && glEnable)
3967
3983
  {
3968
3984
  ASSERT(!!glContext, 'WebGL is not enabled!');
3969
3985
  if (screenSpace)
3970
- {
3971
- // convert to world space
3972
- pos = screenToWorld(pos);
3973
- size = size.scale(1/cameraScale);
3974
- }
3986
+ [pos, size, angle] = screenToWorldTransform(pos, size, angle);
3975
3987
  if (textureInfo)
3976
3988
  {
3977
3989
  // calculate uvs and render
@@ -4059,7 +4071,8 @@ function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=BLACK, angle=0,
4059
4071
  ASSERT(isColor(colorTop) && isColor(colorBottom), 'color is invalid');
4060
4072
  ASSERT(isNumber(angle), 'angle must be a number');
4061
4073
  ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
4062
- if (useWebGL)
4074
+
4075
+ if (useWebGL && glEnable)
4063
4076
  {
4064
4077
  ASSERT(!!glContext, 'WebGL is not enabled!');
4065
4078
  if (screenSpace)
@@ -4067,6 +4080,7 @@ function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=BLACK, angle=0,
4067
4080
  // convert to world space
4068
4081
  pos = screenToWorld(pos);
4069
4082
  size = size.scale(1/cameraScale);
4083
+ angle += cameraAngle;
4070
4084
  }
4071
4085
  // build 4 corner points for the rectangle
4072
4086
  const points = [], colors = [];
@@ -4122,17 +4136,14 @@ function drawLineList(points, width=.1, color, wrap=false, pos=vec2(), angle=0,
4122
4136
  ASSERT(isVector2(pos), 'pos must be a vec2');
4123
4137
  ASSERT(isNumber(angle), 'angle must be a number');
4124
4138
  ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
4125
- if (useWebGL)
4139
+
4140
+ if (useWebGL && glEnable)
4126
4141
  {
4127
4142
  ASSERT(!!glContext, 'WebGL is not enabled!');
4128
- let scale = 1;
4143
+ let size = vec2(1);
4129
4144
  if (screenSpace)
4130
- {
4131
- // convert to world space
4132
- pos = screenToWorld(pos);
4133
- scale = 1/cameraScale;
4134
- }
4135
- glDrawOutlineTransform(points, color.rgbaInt(), width, pos.x, pos.y, scale, scale, angle, wrap);
4145
+ [pos, size, angle] = screenToWorldTransform(pos, size, angle);
4146
+ glDrawOutlineTransform(points, color.rgbaInt(), width, pos.x, pos.y, size.x, size.y, angle, wrap);
4136
4147
  }
4137
4148
  else
4138
4149
  {
@@ -4228,19 +4239,15 @@ function drawPoly(points, color=WHITE, lineWidth=0, lineColor=BLACK, pos=vec2(),
4228
4239
  ASSERT(isNumber(angle), 'angle must be a number');
4229
4240
  ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
4230
4241
 
4231
- if (useWebGL)
4242
+ if (useWebGL && glEnable)
4232
4243
  {
4233
4244
  ASSERT(!!glContext, 'WebGL is not enabled!');
4234
- let scale = 1;
4245
+ let size = vec2(1);
4235
4246
  if (screenSpace)
4236
- {
4237
- // convert to world space
4238
- pos = screenToWorld(pos);
4239
- scale = 1/cameraScale;
4240
- }
4241
- glDrawPointsTransform(points, color.rgbaInt(), pos.x, pos.y, scale, scale, angle);
4247
+ [pos, size, angle] = screenToWorldTransform(pos, size, angle);
4248
+ glDrawPointsTransform(points, color.rgbaInt(), pos.x, pos.y, size.x, size.y, angle);
4242
4249
  if (lineWidth > 0)
4243
- glDrawOutlineTransform(points, lineColor.rgbaInt(), lineWidth, pos.x, pos.y, scale, scale, angle);
4250
+ glDrawOutlineTransform(points, lineColor.rgbaInt(), lineWidth, pos.x, pos.y, size.x, size.y, angle);
4244
4251
  }
4245
4252
  else
4246
4253
  {
@@ -4283,7 +4290,7 @@ function drawEllipse(pos, size=vec2(1), color=WHITE, angle=0, lineWidth=0, lineC
4283
4290
  ASSERT(lineWidth >= 0 && lineWidth < size.x && lineWidth < size.y, 'invalid lineWidth');
4284
4291
  ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
4285
4292
 
4286
- if (useWebGL)
4293
+ if (useWebGL && glEnable)
4287
4294
  {
4288
4295
  // draw as a regular polygon
4289
4296
  const sides = glCircleSides;
@@ -4346,14 +4353,10 @@ function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpac
4346
4353
  ASSERT(typeof drawFunction === 'function', 'drawFunction must be a function');
4347
4354
 
4348
4355
  if (!screenSpace)
4349
- {
4350
- // transform from world space to screen space
4351
- pos = worldToScreen(pos);
4352
- size = size.scale(cameraScale);
4353
- }
4356
+ [pos, size, angle] = worldToScreenTransform(pos, size, angle);
4354
4357
  context.save();
4355
4358
  context.translate(pos.x+.5, pos.y+.5);
4356
- context.rotate(angle-cameraAngle);
4359
+ context.rotate(angle);
4357
4360
  context.scale(mirror ? -size.x : size.x, -size.y);
4358
4361
  drawFunction(context);
4359
4362
  context.restore();
@@ -4379,7 +4382,14 @@ function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpac
4379
4382
  * @memberof Draw */
4380
4383
  function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, fontStyle, maxWidth, angle=0, context=drawContext)
4381
4384
  {
4382
- drawTextScreen(text, worldToScreen(pos), size*cameraScale, color, lineWidth*cameraScale, lineColor, textAlign, font, fontStyle, maxWidth, angle, context);
4385
+ // convert to screen space
4386
+ pos = worldToScreen(pos);
4387
+ size *= cameraScale;
4388
+ lineWidth *= cameraScale;
4389
+ angle -= cameraAngle;
4390
+ angle *= -1;
4391
+
4392
+ drawTextScreen(text, pos, size, color, lineWidth, lineColor, textAlign, font, fontStyle, maxWidth, angle, context);
4383
4393
  }
4384
4394
 
4385
4395
  /** Draw text on overlay canvas in world space
@@ -4538,6 +4548,36 @@ function worldToScreenDelta(worldDelta)
4538
4548
  return new Vector2(x * cameraScale, y * -cameraScale);
4539
4549
  }
4540
4550
 
4551
+ /** Convert screen space transform to world space
4552
+ * @param {Vector2} screenPos
4553
+ * @param {Vector2} screenSize
4554
+ * @param {number} [screenAngle]
4555
+ * @return {[Vector2, Vector2, number]} - [pos, size, angle]
4556
+ * @memberof Draw */
4557
+ function screenToWorldTransform(screenPos, screenSize, screenAngle=0)
4558
+ {
4559
+ return [
4560
+ screenToWorld(screenPos),
4561
+ screenSize.scale(1/cameraScale),
4562
+ screenAngle + cameraAngle
4563
+ ];
4564
+ }
4565
+
4566
+ /** Convert world space transform to screen space
4567
+ * @param {Vector2} worldPos
4568
+ * @param {Vector2} worldSize
4569
+ * @param {number} [worldAngle]
4570
+ * @return {[Vector2, Vector2, number]} - [pos, size, angle]
4571
+ * @memberof Draw */
4572
+ function worldToScreenTransform(worldPos, worldSize, worldAngle=0)
4573
+ {
4574
+ return [
4575
+ worldToScreen(worldPos),
4576
+ worldSize.scale(cameraScale),
4577
+ worldAngle - cameraAngle
4578
+ ];
4579
+ }
4580
+
4541
4581
  /** Get the camera's visible area in world space
4542
4582
  * @return {Vector2}
4543
4583
  * @memberof Draw */
@@ -4945,7 +4985,7 @@ function inputUpdate()
4945
4985
  if (headlessMode) return;
4946
4986
 
4947
4987
  // clear input when lost focus (prevent stuck keys)
4948
- if(!(touchInputEnable && isTouchDevice) && !document.hasFocus())
4988
+ if (!(touchInputEnable && isTouchDevice) && !document.hasFocus())
4949
4989
  inputClear();
4950
4990
 
4951
4991
  // update mouse world space position and delta
@@ -5503,7 +5543,6 @@ class Sound
5503
5543
  ASSERT(isNumber(pitch), 'pitch must be a number');
5504
5544
  ASSERT(isNumber(randomnessScale), 'randomnessScale must be a number');
5505
5545
 
5506
-
5507
5546
  if (!soundEnable || headlessMode) return;
5508
5547
  if (!this.sampleChannels) return;
5509
5548
 
@@ -6636,7 +6675,7 @@ class TileCollisionLayer extends TileLayer
6636
6675
  // remove from collision layers array and destroy
6637
6676
  const index = tileCollisionLayers.indexOf(this);
6638
6677
  ASSERT(index >= 0, 'tile collision layer not found in array');
6639
- tileCollisionLayers.splice(index, 1);
6678
+ index >= 0 && tileCollisionLayers.splice(index, 1);
6640
6679
  super.destroy();
6641
6680
  }
6642
6681
 
@@ -8682,6 +8721,7 @@ class UISystemPlugin
8682
8721
  ASSERT(!uiSystem, 'UI system already initialized');
8683
8722
  uiSystem = this;
8684
8723
 
8724
+ // default settings
8685
8725
  /** @property {Color} - Default fill color for UI elements */
8686
8726
  this.defaultColor = WHITE;
8687
8727
  /** @property {Color} - Default outline color for UI elements */
@@ -8710,6 +8750,13 @@ class UISystemPlugin
8710
8750
  this.defaultSoundRelease = undefined;
8711
8751
  /** @property {Sound} - Default sound when interactive UI element is clicked */
8712
8752
  this.defaultSoundClick = undefined;
8753
+ /** @property {Color} - Color for shadow */
8754
+ this.defaultShadowColor = CLEAR_BLACK;
8755
+ /** @property {number} - Size of shadow blur */
8756
+ this.defaultShadowBlur = 5;
8757
+ /** @property {Vector2} - Offset of shadow blur */
8758
+ this.defaultShadowOffset = vec2(5);
8759
+ // system state
8713
8760
  /** @property {Array<UIObject>} - List of all UI elements */
8714
8761
  this.uiObjects = [];
8715
8762
  /** @property {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} - Context to render UI elements to */
@@ -8800,8 +8847,11 @@ class UISystemPlugin
8800
8847
  * @param {number} [lineWidth]
8801
8848
  * @param {Color} [lineColor]
8802
8849
  * @param {number} [cornerRadius]
8803
- * @param {Color} [gradientColor] */
8804
- drawRect(pos, size, color=WHITE, lineWidth=0, lineColor=BLACK, cornerRadius=0, gradientColor)
8850
+ * @param {Color} [gradientColor]
8851
+ * @param {Color} [shadowColor]
8852
+ * @param {number} [shadowBlur]
8853
+ * @param {Color} [shadowOffset] */
8854
+ drawRect(pos, size, color=WHITE, lineWidth=0, lineColor=BLACK, cornerRadius=0, gradientColor, shadowColor=BLACK, shadowBlur=0, shadowOffset=vec2())
8805
8855
  {
8806
8856
  ASSERT(isVector2(pos), 'pos must be a vec2');
8807
8857
  ASSERT(isVector2(size), 'size must be a vec2');
@@ -8823,12 +8873,22 @@ class UISystemPlugin
8823
8873
  }
8824
8874
  else
8825
8875
  context.fillStyle = color.toString();
8876
+ if (shadowBlur || shadowOffset.x || shadowOffset.y)
8877
+ if (shadowColor.a > 0)
8878
+ {
8879
+ // setup shadow
8880
+ context.shadowColor = shadowColor.toString();
8881
+ context.shadowBlur = shadowBlur;
8882
+ context.shadowOffsetX = shadowOffset.x;
8883
+ context.shadowOffsetY = shadowOffset.y;
8884
+ }
8826
8885
  context.beginPath();
8827
8886
  if (cornerRadius && context['roundRect'])
8828
8887
  context['roundRect'](pos.x-size.x/2, pos.y-size.y/2, size.x, size.y, cornerRadius);
8829
8888
  else
8830
8889
  context.rect(pos.x-size.x/2, pos.y-size.y/2, size.x, size.y);
8831
8890
  context.fill();
8891
+ context.shadowColor = '#0000'
8832
8892
  if (lineWidth)
8833
8893
  {
8834
8894
  context.strokeStyle = lineColor.toString();
@@ -9017,6 +9077,12 @@ class UIObject
9017
9077
  this.dragActivate = false;
9018
9078
  /** @property {boolean} - True if this can be a hover object */
9019
9079
  this.canBeHover = true;
9080
+ /** @property {Color} - Color for shadow, undefined if no shadow */
9081
+ this.shadowColor = uiSystem.defaultShadowColor?.copy();
9082
+ /** @property {number} - Size of shadow blur */
9083
+ this.shadowBlur = uiSystem.defaultShadowBlur;
9084
+ /** @property {Vector2} - Offset of shadow blur */
9085
+ this.shadowOffset = uiSystem.defaultShadowOffset.copy();
9020
9086
  uiSystem.uiObjects.push(this);
9021
9087
 
9022
9088
  /** @property {Vector2} - How much to offset the text shadow or undefined */
@@ -9134,7 +9200,7 @@ class UIObject
9134
9200
 
9135
9201
  const lineColor = this.interactive && this.isActiveObject() && !this.disabled ? this.color : this.lineColor;
9136
9202
  const color = this.disabled ? this.disabledColor : this.interactive ? this.isActiveObject() ? this.activeColor || this.color : this.isHoverObject() ? this.hoverColor : this.color : this.color;
9137
- uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, lineColor, this.cornerRadius, this.gradientColor);
9203
+ uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, lineColor, this.cornerRadius, this.gradientColor, this.shadowColor, this.shadowBlur, this.shadowOffset);
9138
9204
  }
9139
9205
 
9140
9206
  /** Special update when object is not visible */