littlejsengine 1.18.2 → 1.18.4
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/AI.md +1 -0
- package/LICENSE +5 -27
- package/dist/littlejs.d.ts +201 -3
- package/dist/littlejs.esm.js +713 -18
- package/dist/littlejs.esm.min.js +1 -1
- package/dist/littlejs.js +701 -18
- package/dist/littlejs.min.js +1 -1
- package/dist/littlejs.release.js +701 -18
- package/examples/index.html +62 -800
- package/examples/shorts/base.html +1 -1
- package/examples/shorts/textureWrapped.js +12 -0
- package/examples/shorts/tween.js +24 -0
- package/examples/shorts.js +743 -0
- package/examples/starter/index.html +2 -2
- package/examples/tweenSystem/game.js +171 -0
- package/examples/tweenSystem/index.html +10 -0
- package/examples/tweenSystem/tiles.png +0 -0
- package/package.json +1 -1
- package/plugins/box2d.js +4 -2
- package/plugins/pluginExport.js +7 -0
- package/plugins/tween.js +509 -0
- package/plugins/uiSystem.js +1 -1
- package/reference.md +1 -0
- package/src/engine.js +9 -6
- package/src/engineBuild.mjs +1 -0
- package/src/engineDraw.js +133 -2
- package/src/engineExport.js +5 -0
- package/src/engineInput.js +26 -0
- package/src/engineSettings.js +17 -0
- package/src/engineWebGL.js +3 -7
- package/test/smoke.test.mjs +16 -0
- package/test/tween.test.mjs +576 -0
- package/.claude/settings.local.json +0 -7
package/src/engineDraw.js
CHANGED
|
@@ -72,6 +72,12 @@ let textureInfos = [];
|
|
|
72
72
|
* @memberof Draw */
|
|
73
73
|
let drawCount;
|
|
74
74
|
|
|
75
|
+
// internal predicates for tint short-circuiting in canvas2D draw paths
|
|
76
|
+
// isWhite ignores alpha because alpha is applied via globalAlpha, not multiply
|
|
77
|
+
// isBlack includes alpha so additive colors that only contribute alpha are not skipped
|
|
78
|
+
/** @param {Color} c */ function isWhite(c) { return c.r >= 1 && c.g >= 1 && c.b >= 1; }
|
|
79
|
+
/** @param {Color} c */ function isBlack(c) { return c.r <= 0 && c.g <= 0 && c.b <= 0 && c.a <= 0; }
|
|
80
|
+
|
|
75
81
|
///////////////////////////////////////////////////////////////////////////////
|
|
76
82
|
|
|
77
83
|
/**
|
|
@@ -401,6 +407,96 @@ function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=BLACK, angle=0,
|
|
|
401
407
|
}
|
|
402
408
|
}
|
|
403
409
|
|
|
410
|
+
/** Draw a texture tiled (wrapped) across a rectangle in world space.
|
|
411
|
+
* Useful for backgrounds, repeating patterns, and seamless fills.
|
|
412
|
+
* The whole texture is tiled — sub-region (TileInfo) wrapping is not supported.
|
|
413
|
+
* @param {Vector2} pos - Center of the rect in world space
|
|
414
|
+
* @param {Vector2} size - Size of the rect in world space
|
|
415
|
+
* @param {Vector2} wrapCount - How many times the texture repeats (x, y)
|
|
416
|
+
* @param {TextureInfo|number} [texture=0] - TextureInfo or texture index into textureInfos
|
|
417
|
+
* @param {Color} [color=WHITE] - Color to modulate with
|
|
418
|
+
* @param {number} [angle=0] - Angle to rotate by
|
|
419
|
+
* @param {Color} [additiveColor] - Additive color to be applied if any
|
|
420
|
+
* @param {boolean} [useWebGL=glEnable] - Use accelerated WebGL rendering?
|
|
421
|
+
* @param {boolean} [screenSpace=false] - Are pos and size in screen space?
|
|
422
|
+
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context] - Canvas 2D context to draw to
|
|
423
|
+
* @memberof Draw */
|
|
424
|
+
function drawTextureWrapped(pos, size, wrapCount, texture=0, color=WHITE,
|
|
425
|
+
angle=0, additiveColor, useWebGL=glEnable, screenSpace=false, context)
|
|
426
|
+
{
|
|
427
|
+
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
428
|
+
ASSERT(isVector2(size), 'size must be a vec2');
|
|
429
|
+
ASSERT(isVector2(wrapCount), 'wrapCount must be a vec2');
|
|
430
|
+
ASSERT(isColor(color), 'color is invalid');
|
|
431
|
+
ASSERT(isNumber(angle), 'angle must be a number');
|
|
432
|
+
ASSERT(!additiveColor || isColor(additiveColor), 'additiveColor must be a color');
|
|
433
|
+
ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
|
|
434
|
+
ASSERT(!(texture instanceof TileInfo),
|
|
435
|
+
'pass a TextureInfo or texture index, not a TileInfo — use tileInfo.textureInfo');
|
|
436
|
+
|
|
437
|
+
// short-circuit before texture lookup — textureInfos[0] is undefined in headless mode
|
|
438
|
+
if (headlessMode) return;
|
|
439
|
+
|
|
440
|
+
// resolve texture argument: TextureInfo or index
|
|
441
|
+
const textureInfo = typeof texture === 'number' ? textureInfos[texture] : texture;
|
|
442
|
+
ASSERT(textureInfo instanceof TextureInfo, 'texture not loaded');
|
|
443
|
+
ASSERT(textureInfo.size.x > 0, 'texture not loaded');
|
|
444
|
+
|
|
445
|
+
if (useWebGL && glEnable)
|
|
446
|
+
{
|
|
447
|
+
ASSERT(!!glContext, 'WebGL is not enabled!');
|
|
448
|
+
if (screenSpace)
|
|
449
|
+
[pos, size, angle] = screenToWorldTransform(pos, size, angle);
|
|
450
|
+
glSetTexture(textureInfo.glTexture);
|
|
451
|
+
glDraw(pos.x, pos.y, size.x, size.y, angle,
|
|
452
|
+
0, 0, wrapCount.x, wrapCount.y,
|
|
453
|
+
color.rgbaInt(), additiveColor && additiveColor.rgbaInt());
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Canvas2D path — increment drawCount here (WebGL batch counts via glBatchCount)
|
|
458
|
+
++drawCount;
|
|
459
|
+
|
|
460
|
+
if (!screenSpace)
|
|
461
|
+
{
|
|
462
|
+
pos = worldToScreen(pos);
|
|
463
|
+
size = size.scale(cameraScale);
|
|
464
|
+
angle -= cameraAngle;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// pick image source: raw, or tinted bake. Match drawImageColor's
|
|
468
|
+
// "no tint needed" predicate so behavior stays consistent.
|
|
469
|
+
const noTint = !canvasColorTiles ||
|
|
470
|
+
(additiveColor
|
|
471
|
+
? isWhite(color.add(additiveColor)) && additiveColor.a <= 0
|
|
472
|
+
: isWhite(color));
|
|
473
|
+
// alpha is baked into pixels by bakeTintedImage's additive branch;
|
|
474
|
+
// in that case globalAlpha must NOT also apply color.a
|
|
475
|
+
const alphaBaked = !noTint && additiveColor && !isBlack(additiveColor);
|
|
476
|
+
const source = noTint
|
|
477
|
+
? textureInfo.image
|
|
478
|
+
: bakeTintedImage(textureInfo.image, color, additiveColor);
|
|
479
|
+
|
|
480
|
+
context = context || drawContext;
|
|
481
|
+
context.save();
|
|
482
|
+
context.translate(pos.x + .5, pos.y + .5);
|
|
483
|
+
context.rotate(angle);
|
|
484
|
+
context.globalAlpha = alphaBaked ? 1 : color.a;
|
|
485
|
+
|
|
486
|
+
const pattern = context.createPattern(source, 'repeat');
|
|
487
|
+
// map pattern-source pixels into user space so the rect contains
|
|
488
|
+
// wrapCount.x × wrapCount.y repeats
|
|
489
|
+
const m = new DOMMatrix()
|
|
490
|
+
.translate(-size.x/2, -size.y/2)
|
|
491
|
+
.scale(size.x / (wrapCount.x * source.width),
|
|
492
|
+
size.y / (wrapCount.y * source.height));
|
|
493
|
+
pattern.setTransform(m);
|
|
494
|
+
context.fillStyle = pattern;
|
|
495
|
+
context.fillRect(-size.x/2, -size.y/2, size.x, size.y);
|
|
496
|
+
context.globalAlpha = 1;
|
|
497
|
+
context.restore();
|
|
498
|
+
}
|
|
499
|
+
|
|
404
500
|
/** Draw connected lines between a series of points
|
|
405
501
|
* @param {Array<Vector2>} points
|
|
406
502
|
* @param {number} [width]
|
|
@@ -924,6 +1020,43 @@ function combineCanvases()
|
|
|
924
1020
|
mainContext.drawImage(workCanvas, 0, 0);
|
|
925
1021
|
}
|
|
926
1022
|
|
|
1023
|
+
// Internal: bake a color/additive-color tint into workReadCanvas at the
|
|
1024
|
+
// image's native resolution. Returns the work canvas, suitable for
|
|
1025
|
+
// passing to context.createPattern. Used by drawTextureWrapped's
|
|
1026
|
+
// Canvas2D path. Caller is responsible for short-circuiting when no
|
|
1027
|
+
// tint is needed (i.e. color is white and additiveColor is black/none).
|
|
1028
|
+
function bakeTintedImage(image, color, additiveColor)
|
|
1029
|
+
{
|
|
1030
|
+
const w = image.width|0, h = image.height|0;
|
|
1031
|
+
workReadCanvas.width = w;
|
|
1032
|
+
workReadCanvas.height = h;
|
|
1033
|
+
workReadContext.drawImage(image, 0, 0);
|
|
1034
|
+
|
|
1035
|
+
const imageData = workReadContext.getImageData(0, 0, w, h);
|
|
1036
|
+
const data = imageData.data;
|
|
1037
|
+
if (additiveColor && !isBlack(additiveColor))
|
|
1038
|
+
{
|
|
1039
|
+
// multiply + additive (slower)
|
|
1040
|
+
const colorMultiply = [color.r, color.g, color.b, color.a];
|
|
1041
|
+
const colorAdd = [additiveColor.r * 255, additiveColor.g * 255,
|
|
1042
|
+
additiveColor.b * 255, additiveColor.a * 255];
|
|
1043
|
+
for (let i = 0; i < data.length; ++i)
|
|
1044
|
+
data[i] = data[i] * colorMultiply[i&3] + colorAdd[i&3] |0;
|
|
1045
|
+
}
|
|
1046
|
+
else
|
|
1047
|
+
{
|
|
1048
|
+
// RGB only, faster — alpha left intact for the caller
|
|
1049
|
+
for (let i = 0; i < data.length; i+=4)
|
|
1050
|
+
{
|
|
1051
|
+
data[i ] *= color.r;
|
|
1052
|
+
data[i+1] *= color.g;
|
|
1053
|
+
data[i+2] *= color.b;
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
workReadContext.putImageData(imageData, 0, 0);
|
|
1057
|
+
return workReadCanvas;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
927
1060
|
/** Helper function to draw an image with color and additive color applied
|
|
928
1061
|
* This is slower then normal drawImage when color is applied
|
|
929
1062
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} context
|
|
@@ -942,8 +1075,6 @@ function combineCanvases()
|
|
|
942
1075
|
* @memberof Draw */
|
|
943
1076
|
function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight, color, additiveColor, bleed=0)
|
|
944
1077
|
{
|
|
945
|
-
function isWhite(c) { return c.r >= 1 && c.g >= 1 && c.b >= 1; }
|
|
946
|
-
function isBlack(c) { return c.r <= 0 && c.g <= 0 && c.b <= 0 && c.a <= 0; }
|
|
947
1078
|
const sx2 = bleed;
|
|
948
1079
|
const sy2 = bleed;
|
|
949
1080
|
sWidth = max(1,sWidth|0);
|
package/src/engineExport.js
CHANGED
|
@@ -51,6 +51,7 @@ export
|
|
|
51
51
|
cameraPos,
|
|
52
52
|
cameraAngle,
|
|
53
53
|
cameraScale,
|
|
54
|
+
timeScale,
|
|
54
55
|
canvasColorTiles,
|
|
55
56
|
canvasClearColor,
|
|
56
57
|
canvasMaxSize,
|
|
@@ -98,6 +99,7 @@ export
|
|
|
98
99
|
setCameraPos,
|
|
99
100
|
setCameraAngle,
|
|
100
101
|
setCameraScale,
|
|
102
|
+
setTimeScale,
|
|
101
103
|
setCanvasColorTiles,
|
|
102
104
|
setCanvasClearColor,
|
|
103
105
|
setCanvasMaxSize,
|
|
@@ -246,6 +248,7 @@ export
|
|
|
246
248
|
drawTile,
|
|
247
249
|
drawRect,
|
|
248
250
|
drawRectGradient,
|
|
251
|
+
drawTextureWrapped,
|
|
249
252
|
drawLineList,
|
|
250
253
|
drawLine,
|
|
251
254
|
drawPoly,
|
|
@@ -312,6 +315,8 @@ export
|
|
|
312
315
|
gamepadStick,
|
|
313
316
|
gamepadDpad,
|
|
314
317
|
gamepadConnected,
|
|
318
|
+
gamepadVibrate,
|
|
319
|
+
gamepadVibrateStop,
|
|
315
320
|
vibrate,
|
|
316
321
|
vibrateStop,
|
|
317
322
|
pointerLockRequest,
|
package/src/engineInput.js
CHANGED
|
@@ -260,6 +260,32 @@ function gamepadStickCount(gamepad=gamepadPrimary)
|
|
|
260
260
|
return gamepadStickData[gamepad]?.length ?? 0;
|
|
261
261
|
}
|
|
262
262
|
|
|
263
|
+
/** Pulse a gamepad's vibration hardware using the dual-rumble effect if it exists
|
|
264
|
+
* Strong magnitude is usually the left side motor, weak magnitude is usually the right side motor
|
|
265
|
+
* @param {number} [gamepad] - gamepad index
|
|
266
|
+
* @param {number} [duration] - effect duration in ms
|
|
267
|
+
* @param {number} [strongMagnitude] - strong (left) motor intensity, 0 to 1
|
|
268
|
+
* @param {number} [weakMagnitude] - weak (right) motor intensity, 0 to 1
|
|
269
|
+
* @param {number} [startDelay] - delay in ms before the effect starts
|
|
270
|
+
* @memberof Input */
|
|
271
|
+
function gamepadVibrate(gamepad=gamepadPrimary, duration=200, strongMagnitude=1, weakMagnitude=1, startDelay=0)
|
|
272
|
+
{
|
|
273
|
+
ASSERT(isNumber(gamepad), 'gamepad must be a number');
|
|
274
|
+
if (!vibrateEnable || headlessMode) return;
|
|
275
|
+
const pad = navigator?.getGamepads?.()[gamepad];
|
|
276
|
+
pad?.vibrationActuator?.playEffect?.('dual-rumble', {duration, strongMagnitude, weakMagnitude, startDelay});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** Stop vibration on a gamepad
|
|
280
|
+
* @memberof Input */
|
|
281
|
+
function gamepadVibrateStop(gamepad=gamepadPrimary)
|
|
282
|
+
{
|
|
283
|
+
ASSERT(isNumber(gamepad), 'gamepad must be a number');
|
|
284
|
+
if (!vibrateEnable || headlessMode) return;
|
|
285
|
+
const pad = navigator?.getGamepads?.()[gamepad];
|
|
286
|
+
pad?.vibrationActuator?.reset?.();
|
|
287
|
+
}
|
|
288
|
+
|
|
263
289
|
///////////////////////////////////////////////////////////////////////////////
|
|
264
290
|
|
|
265
291
|
/** Pulse the vibration hardware if it exists
|
package/src/engineSettings.js
CHANGED
|
@@ -27,6 +27,18 @@ let cameraAngle = 0;
|
|
|
27
27
|
* @memberof Settings */
|
|
28
28
|
let cameraScale = 32;
|
|
29
29
|
|
|
30
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
31
|
+
// Time settings
|
|
32
|
+
|
|
33
|
+
/** Scale applied to engine time, can be used for slow motion or fast forward
|
|
34
|
+
* - 1 is normal speed, 2 is double speed, 0.5 is half speed
|
|
35
|
+
* - 0 freezes the simulation without setting the paused flag
|
|
36
|
+
* - Should be >= 0; stacks multiplicatively with the debug +/- shortcut
|
|
37
|
+
* @type {number}
|
|
38
|
+
* @default
|
|
39
|
+
* @memberof Settings */
|
|
40
|
+
let timeScale = 1;
|
|
41
|
+
|
|
30
42
|
///////////////////////////////////////////////////////////////////////////////
|
|
31
43
|
// Display settings
|
|
32
44
|
|
|
@@ -355,6 +367,11 @@ function setCameraAngle(angle) { cameraAngle = angle; }
|
|
|
355
367
|
* @memberof Settings */
|
|
356
368
|
function setCameraScale(scale) { cameraScale = scale; }
|
|
357
369
|
|
|
370
|
+
/** Set scale applied to engine time
|
|
371
|
+
* @param {number} scale
|
|
372
|
+
* @memberof Settings */
|
|
373
|
+
function setTimeScale(scale) { timeScale = scale; }
|
|
374
|
+
|
|
358
375
|
/** Set if tiles should be colorized when using canvas2d
|
|
359
376
|
* This can be slower but results should look nearly identical to WebGL rendering
|
|
360
377
|
* It can be enabled/disabled at any time
|
package/src/engineWebGL.js
CHANGED
|
@@ -304,9 +304,8 @@ function glClearCanvas()
|
|
|
304
304
|
/** Set the WebGL texture, called automatically if using multiple textures
|
|
305
305
|
* - This may also flush the gl buffer resulting in more draw calls and worse performance
|
|
306
306
|
* @param {WebGLTexture} texture
|
|
307
|
-
* @param {boolean} [wrap] - Should the texture wrap or clamp
|
|
308
307
|
* @memberof WebGL */
|
|
309
|
-
function glSetTexture(texture
|
|
308
|
+
function glSetTexture(texture)
|
|
310
309
|
{
|
|
311
310
|
// must flush cache with the old texture to set a new one
|
|
312
311
|
if (!glContext || texture === glActiveTexture) return;
|
|
@@ -314,11 +313,6 @@ function glSetTexture(texture, wrap=false)
|
|
|
314
313
|
glFlush();
|
|
315
314
|
glActiveTexture = texture;
|
|
316
315
|
glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
|
|
317
|
-
|
|
318
|
-
// set wrap mode
|
|
319
|
-
const wrapMode = wrap ? glContext.REPEAT : glContext.CLAMP_TO_EDGE;
|
|
320
|
-
glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_WRAP_S, wrapMode);
|
|
321
|
-
glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_WRAP_T, wrapMode);
|
|
322
316
|
}
|
|
323
317
|
|
|
324
318
|
/** Compile WebGL shader of the given type, will throw errors if in debug mode
|
|
@@ -393,6 +387,8 @@ function glCreateTexture(image)
|
|
|
393
387
|
const minFilter = mipMap ? glContext.LINEAR_MIPMAP_LINEAR : magFilter;
|
|
394
388
|
glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_MAG_FILTER, magFilter);
|
|
395
389
|
glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_MIN_FILTER, minFilter);
|
|
390
|
+
glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_WRAP_S, glContext.REPEAT);
|
|
391
|
+
glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_WRAP_T, glContext.REPEAT);
|
|
396
392
|
if (mipMap)
|
|
397
393
|
glContext.generateMipmap(glContext.TEXTURE_2D);
|
|
398
394
|
|
package/test/smoke.test.mjs
CHANGED
|
@@ -256,3 +256,19 @@ test('TileLayer extends CanvasLayer and stubs render methods in headless', () =>
|
|
|
256
256
|
assert.doesNotThrow(() => tl.render());
|
|
257
257
|
assert.doesNotThrow(() => tl.redraw());
|
|
258
258
|
});
|
|
259
|
+
|
|
260
|
+
test('drawTextureWrapped is exported', async () =>
|
|
261
|
+
{
|
|
262
|
+
const mod = await import('../dist/littlejs.esm.js');
|
|
263
|
+
assert.equal(typeof mod.drawTextureWrapped, 'function');
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test('drawTextureWrapped is callable in headless mode', async () =>
|
|
267
|
+
{
|
|
268
|
+
const mod = await import('../dist/littlejs.esm.js');
|
|
269
|
+
const { drawTextureWrapped, vec2 } = mod;
|
|
270
|
+
// headlessMode short-circuits before texture lookup, so this just
|
|
271
|
+
// exercises argument-type ASSERTs (which are stripped in release)
|
|
272
|
+
assert.doesNotThrow(() =>
|
|
273
|
+
drawTextureWrapped(vec2(), vec2(1, 1), vec2(2, 2)));
|
|
274
|
+
});
|