littlejsengine 1.18.2 → 1.18.7
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/COPYRIGHT.txt +38 -0
- package/FAQ.md +633 -0
- package/LICENSE +5 -27
- package/README.md +19 -1
- package/dist/littlejs.d.ts +550 -57
- package/dist/littlejs.esm.js +2208 -492
- package/dist/littlejs.esm.min.js +1 -1
- package/dist/littlejs.js +2165 -480
- package/dist/littlejs.min.js +1 -1
- package/dist/littlejs.release.js +2166 -481
- package/package.json +8 -1
- package/plugins/box2d.js +36 -9
- package/{src/engineMedals.js → plugins/medalSystem.js} +248 -196
- package/plugins/pathFinder.js +758 -0
- package/plugins/pluginExport.js +26 -0
- package/plugins/tween.js +509 -0
- package/plugins/tweenSystem.js +509 -0
- package/plugins/uiSystem.js +151 -12
- package/src/engine.js +9 -145
- package/src/engineAudio.js +8 -5
- package/src/engineBuild.mjs +4 -1
- package/src/engineDebug.js +1 -1
- package/src/engineDraw.js +154 -5
- package/src/engineExport.js +17 -12
- package/src/engineInput.js +27 -1
- package/src/engineLogo.js +146 -0
- package/src/engineMath.js +10 -3
- package/src/engineObject.js +11 -5
- package/src/engineParticles.js +6 -3
- package/src/engineRelease.js +1 -1
- package/src/engineSettings.js +17 -47
- package/src/engineUtilities.js +7 -2
- package/src/engineWebGL.js +31 -8
- package/src/jsconfig.json +3 -1
- package/.claude/settings.local.json +0 -7
- package/.github/workflows/test.yml +0 -17
- package/AI.md +0 -172
- package/CLAUDE.md +0 -1
- package/examples/box2d/game.js +0 -192
- package/examples/box2d/gameObjects.js +0 -564
- package/examples/box2d/index.html +0 -9
- package/examples/box2d/scenes.js +0 -194
- package/examples/box2d/tiles.png +0 -0
- package/examples/breakout/game.js +0 -175
- package/examples/breakout/gameObjects.js +0 -147
- package/examples/breakout/index.html +0 -8
- package/examples/breakout/tiles.png +0 -0
- package/examples/breakoutTutorial/README.md +0 -521
- package/examples/breakoutTutorial/game.js +0 -191
- package/examples/breakoutTutorial/images/1.png +0 -0
- package/examples/breakoutTutorial/images/10.png +0 -0
- package/examples/breakoutTutorial/images/11.png +0 -0
- package/examples/breakoutTutorial/images/2.png +0 -0
- package/examples/breakoutTutorial/images/3.png +0 -0
- package/examples/breakoutTutorial/images/4.png +0 -0
- package/examples/breakoutTutorial/images/5.png +0 -0
- package/examples/breakoutTutorial/images/6.png +0 -0
- package/examples/breakoutTutorial/images/7.png +0 -0
- package/examples/breakoutTutorial/images/8.png +0 -0
- package/examples/breakoutTutorial/images/9.png +0 -0
- package/examples/breakoutTutorial/index.html +0 -8
- package/examples/electron/build.mjs +0 -127
- package/examples/electron/electron.js +0 -35
- package/examples/electron/game.js +0 -54
- package/examples/electron/index.html +0 -13
- package/examples/electron/package.json +0 -5
- package/examples/electron/tiles.png +0 -0
- package/examples/empty/game.js +0 -51
- package/examples/empty/index.html +0 -5
- package/examples/empty/tiles.png +0 -0
- package/examples/favicon.png +0 -0
- package/examples/games.jpg +0 -0
- package/examples/htmlMenu/game.js +0 -82
- package/examples/htmlMenu/index.html +0 -45
- package/examples/htmlMenu/tiles.png +0 -0
- package/examples/index.html +0 -800
- package/examples/logo.png +0 -0
- package/examples/logo2.png +0 -0
- package/examples/module/build.mjs +0 -124
- package/examples/module/game.js +0 -132
- package/examples/module/index.html +0 -10
- package/examples/module/tiles.png +0 -0
- package/examples/particles/index.html +0 -426
- package/examples/particles/tiles.png +0 -0
- package/examples/platformer/data/gameLevelData.tmx +0 -143
- package/examples/platformer/data/gameLevelData.tsx +0 -4
- package/examples/platformer/game.js +0 -147
- package/examples/platformer/gameCharacter.js +0 -309
- package/examples/platformer/gameEffects.js +0 -278
- package/examples/platformer/gameLevel.js +0 -205
- package/examples/platformer/gameLevelData.json +0 -171
- package/examples/platformer/gameObjects.js +0 -378
- package/examples/platformer/gamePlayer.js +0 -35
- package/examples/platformer/index.html +0 -9
- package/examples/platformer/tiles.png +0 -0
- package/examples/platformer/tilesLevel.png +0 -0
- package/examples/puzzle/game.js +0 -331
- package/examples/puzzle/index.html +0 -8
- package/examples/puzzle/tiles.png +0 -0
- package/examples/screenshot.jpg +0 -0
- package/examples/shorts/animation.js +0 -18
- package/examples/shorts/base.html +0 -54
- package/examples/shorts/blending.js +0 -14
- package/examples/shorts/box2d.js +0 -51
- package/examples/shorts/box2dCar.js +0 -59
- package/examples/shorts/box2dPool.js +0 -107
- package/examples/shorts/box2dTileLayer.js +0 -48
- package/examples/shorts/cameraDrag.js +0 -22
- package/examples/shorts/clock.js +0 -21
- package/examples/shorts/colors.js +0 -25
- package/examples/shorts/debugDraw.js +0 -37
- package/examples/shorts/empty.js +0 -30
- package/examples/shorts/flappyGame.js +0 -55
- package/examples/shorts/fontImage.js +0 -17
- package/examples/shorts/fps.js +0 -90
- package/examples/shorts/helloWorld.js +0 -11
- package/examples/shorts/hillGlideGame.js +0 -63
- package/examples/shorts/input.js +0 -64
- package/examples/shorts/landerGame.js +0 -57
- package/examples/shorts/maze.js +0 -48
- package/examples/shorts/medals.js +0 -51
- package/examples/shorts/music.js +0 -78
- package/examples/shorts/musicPlayer.js +0 -137
- package/examples/shorts/nineSlice.js +0 -40
- package/examples/shorts/parallax.js +0 -72
- package/examples/shorts/particles.js +0 -28
- package/examples/shorts/piano.js +0 -44
- package/examples/shorts/platformer.js +0 -45
- package/examples/shorts/pongGame.js +0 -41
- package/examples/shorts/postProcess.js +0 -62
- package/examples/shorts/sequencer.js +0 -124
- package/examples/shorts/shader.js +0 -29
- package/examples/shorts/shapes.js +0 -21
- package/examples/shorts/slidingPuzzle.js +0 -52
- package/examples/shorts/song.mp3 +0 -0
- package/examples/shorts/sound.js +0 -36
- package/examples/shorts/spaceGame.js +0 -58
- package/examples/shorts/spriteAtlas.js +0 -31
- package/examples/shorts/starfield.js +0 -15
- package/examples/shorts/texture.js +0 -16
- package/examples/shorts/tileLayer.js +0 -48
- package/examples/shorts/tileRaycast.js +0 -39
- package/examples/shorts/tiles.png +0 -0
- package/examples/shorts/tiltedView.js +0 -63
- package/examples/shorts/timers.js +0 -53
- package/examples/shorts/topDown.js +0 -38
- package/examples/shorts/uiSystem.js +0 -61
- package/examples/shorts/video.webm +0 -0
- package/examples/shorts/videoPlayer.js +0 -34
- package/examples/starter/build.bat +0 -7
- package/examples/starter/build.mjs +0 -126
- package/examples/starter/game.js +0 -137
- package/examples/starter/index.html +0 -35
- package/examples/starter/tiles.png +0 -0
- package/examples/stress/index.html +0 -173
- package/examples/style.css +0 -150
- package/examples/typescript/build.mjs +0 -60
- package/examples/typescript/game.js +0 -100
- package/examples/typescript/game.ts +0 -132
- package/examples/typescript/index.html +0 -10
- package/examples/typescript/tiles.png +0 -0
- package/examples/typescript/tsconfig.json +0 -17
- package/examples/uiSystem/game.js +0 -139
- package/examples/uiSystem/index.html +0 -10
- package/examples/uiSystem/tiles.png +0 -0
- package/jsconfig.json +0 -12
- package/plugins/desktop.ini +0 -2
- package/reference.md +0 -447
- package/test/math.test.mjs +0 -774
- package/test/setup.mjs +0 -22
- package/test/smoke.test.mjs +0 -258
- package/test/util.test.mjs +0 -80
package/plugins/uiSystem.js
CHANGED
|
@@ -118,16 +118,13 @@ class UISystemPlugin
|
|
|
118
118
|
this.lastHoverObject = undefined;
|
|
119
119
|
/** @property {UIObject} - Current confirm menu being shown */
|
|
120
120
|
this.confirmDialog = undefined;
|
|
121
|
-
/** @
|
|
122
|
-
this.
|
|
121
|
+
/** @private */
|
|
122
|
+
this._keyInputObject = undefined;
|
|
123
|
+
/** @private */
|
|
124
|
+
this._onKeyDown = (e) => this._keyInputObject?.onKeyDown(e);
|
|
123
125
|
|
|
124
126
|
engineAddPlugin(uiUpdate, uiRender);
|
|
125
127
|
|
|
126
|
-
// key down handler
|
|
127
|
-
function onKeyDown(e)
|
|
128
|
-
{ uiSystem.keyInputObject?.onKeyDown(e); }
|
|
129
|
-
document.addEventListener('keydown', onKeyDown);
|
|
130
|
-
|
|
131
128
|
// set object position in parent space
|
|
132
129
|
function updateTransforms(o)
|
|
133
130
|
{
|
|
@@ -469,8 +466,23 @@ class UISystemPlugin
|
|
|
469
466
|
return p;
|
|
470
467
|
}
|
|
471
468
|
|
|
469
|
+
/** Object to send keyboard input to (typically a UITextInput).
|
|
470
|
+
* The document keydown listener is only attached while this is set,
|
|
471
|
+
* so games that never use text input pay no event-handling cost.
|
|
472
|
+
* @type {UIObject} */
|
|
473
|
+
get keyInputObject() { return this._keyInputObject; }
|
|
474
|
+
set keyInputObject(obj)
|
|
475
|
+
{
|
|
476
|
+
const had = !!this._keyInputObject;
|
|
477
|
+
this._keyInputObject = obj;
|
|
478
|
+
if (!had && obj)
|
|
479
|
+
document.addEventListener('keydown', this._onKeyDown);
|
|
480
|
+
else if (had && !obj)
|
|
481
|
+
document.removeEventListener('keydown', this._onKeyDown);
|
|
482
|
+
}
|
|
483
|
+
|
|
472
484
|
/** Destroy and remove all objects
|
|
473
|
-
* @memberof
|
|
485
|
+
* @memberof UISystem */
|
|
474
486
|
destroyObjects()
|
|
475
487
|
{
|
|
476
488
|
for (const o of this.uiObjects)
|
|
@@ -893,15 +905,13 @@ class UIObject
|
|
|
893
905
|
* @return {string} */
|
|
894
906
|
toString()
|
|
895
907
|
{
|
|
896
|
-
if (!debug) return;
|
|
897
|
-
|
|
898
908
|
let text = 'type = ' + this.constructor.name;
|
|
899
909
|
if (this.text)
|
|
900
910
|
text += '\ntext = ' + this.text;
|
|
901
911
|
if (this.pos.x || this.pos.y)
|
|
902
912
|
text += '\npos = ' + this.pos;
|
|
903
913
|
if (this.localPos.x || this.localPos.y)
|
|
904
|
-
text += '\
|
|
914
|
+
text += '\nlocalPos = ' + this.localPos;
|
|
905
915
|
if (this.size.x || this.size.y)
|
|
906
916
|
text += '\nsize = ' + this.size;
|
|
907
917
|
if (this.color)
|
|
@@ -924,7 +934,7 @@ class UIObject
|
|
|
924
934
|
|
|
925
935
|
/** Internal function called when object is clicked
|
|
926
936
|
* @param {boolean} [playSound] */
|
|
927
|
-
click(playSound)
|
|
937
|
+
click(playSound=true)
|
|
928
938
|
{
|
|
929
939
|
this.onClick();
|
|
930
940
|
if (playSound && this.soundClick)
|
|
@@ -1495,4 +1505,133 @@ class UIVideo extends UIObject
|
|
|
1495
1505
|
this.video.remove();
|
|
1496
1506
|
super.destroy();
|
|
1497
1507
|
}
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
1511
|
+
/**
|
|
1512
|
+
* UILayout - A container that auto-arranges children in a vertical list, horizontal list, or grid
|
|
1513
|
+
* - Set columns to 1 for a vertical list (default)
|
|
1514
|
+
* - Set columns to children.length for a horizontal list
|
|
1515
|
+
* - Set columns to N (1 < N < children.length) for a grid with N columns
|
|
1516
|
+
* - Per-child sizing: each row's height = max child.size.y in that row, each column's width = max child.size.x in that column
|
|
1517
|
+
* - Children are positioned centered in their cell
|
|
1518
|
+
* - Container auto-sizes to fit children plus padding
|
|
1519
|
+
* @extends UIObject
|
|
1520
|
+
* @memberof UISystem
|
|
1521
|
+
*/
|
|
1522
|
+
class UILayout extends UIObject
|
|
1523
|
+
{
|
|
1524
|
+
/** Create a UILayout container that auto-arranges children
|
|
1525
|
+
* @param {Vector2} [pos]
|
|
1526
|
+
* @param {number} [columns=1] - Number of columns (1 = vertical list)
|
|
1527
|
+
* @param {number} [gap=10] - Space between children
|
|
1528
|
+
* @param {number} [padding=10] - Space between container border and children
|
|
1529
|
+
* @param {boolean} [transparent=false] - If true, draws no background, outline, or shadow
|
|
1530
|
+
*/
|
|
1531
|
+
constructor(pos, columns=1, gap=10, padding=10, transparent=false)
|
|
1532
|
+
{
|
|
1533
|
+
super(pos);
|
|
1534
|
+
|
|
1535
|
+
ASSERT(isNumber(columns) && columns >= 1, 'ui layout columns must be a number >= 1');
|
|
1536
|
+
ASSERT(isNumber(gap), 'ui layout gap must be a number');
|
|
1537
|
+
ASSERT(isNumber(padding), 'ui layout padding must be a number');
|
|
1538
|
+
|
|
1539
|
+
/** @property {number} - Number of columns in the layout */
|
|
1540
|
+
this.columns = columns;
|
|
1541
|
+
/** @property {number} - Space between children */
|
|
1542
|
+
this.gap = gap;
|
|
1543
|
+
/** @property {number} - Space between container border and children */
|
|
1544
|
+
this.padding = padding;
|
|
1545
|
+
|
|
1546
|
+
if (transparent)
|
|
1547
|
+
{
|
|
1548
|
+
// pure positioning helper - skip background, outline, and shadow
|
|
1549
|
+
this.color = CLEAR_BLACK;
|
|
1550
|
+
this.gradientColor = undefined;
|
|
1551
|
+
this.lineWidth = 0;
|
|
1552
|
+
this.shadowColor = CLEAR_BLACK;
|
|
1553
|
+
}
|
|
1554
|
+
this.relayout();
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
/** Add a child UIObject and re-layout
|
|
1558
|
+
* @param {UIObject} child
|
|
1559
|
+
* @return {UIObject} The child object added */
|
|
1560
|
+
addChild(child)
|
|
1561
|
+
{
|
|
1562
|
+
super.addChild(child);
|
|
1563
|
+
this.relayout();
|
|
1564
|
+
return child;
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
/** Remove a child UIObject and re-layout
|
|
1568
|
+
* @param {UIObject} child */
|
|
1569
|
+
removeChild(child)
|
|
1570
|
+
{
|
|
1571
|
+
super.removeChild(child);
|
|
1572
|
+
this.relayout();
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
/** Recompute child positions and container size based on per-child sizes.
|
|
1576
|
+
* Called automatically by addChild and removeChild. Call manually if you
|
|
1577
|
+
* mutate a child's size or change columns, gap, or padding. */
|
|
1578
|
+
relayout()
|
|
1579
|
+
{
|
|
1580
|
+
const n = this.children.length;
|
|
1581
|
+
if (!n)
|
|
1582
|
+
{
|
|
1583
|
+
this.size = vec2(this.padding * 2);
|
|
1584
|
+
return;
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
const cols = this.columns;
|
|
1588
|
+
const rows = ceil(n / cols);
|
|
1589
|
+
const colWidths = new Array(cols).fill(0);
|
|
1590
|
+
const rowHeights = new Array(rows).fill(0);
|
|
1591
|
+
|
|
1592
|
+
// first pass: compute column widths and row heights from child sizes
|
|
1593
|
+
for (let i = 0; i < n; ++i)
|
|
1594
|
+
{
|
|
1595
|
+
const col = i % cols;
|
|
1596
|
+
const row = floor(i / cols);
|
|
1597
|
+
const child = this.children[i];
|
|
1598
|
+
colWidths[col] = max(colWidths[col], child.size.x);
|
|
1599
|
+
rowHeights[row] = max(rowHeights[row], child.size.y);
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
// total content size (sum of column widths/row heights plus gaps between them)
|
|
1603
|
+
let contentWidth = this.gap * (cols - 1);
|
|
1604
|
+
for (const w of colWidths) contentWidth += w;
|
|
1605
|
+
let contentHeight = this.gap * (rows - 1);
|
|
1606
|
+
for (const h of rowHeights) contentHeight += h;
|
|
1607
|
+
|
|
1608
|
+
// cumulative column/row offsets so positioning is O(n) not O(n^2)
|
|
1609
|
+
const colOffsets = new Array(cols);
|
|
1610
|
+
let xAcc = 0;
|
|
1611
|
+
for (let c = 0; c < cols; ++c)
|
|
1612
|
+
{
|
|
1613
|
+
colOffsets[c] = xAcc;
|
|
1614
|
+
xAcc += colWidths[c];
|
|
1615
|
+
}
|
|
1616
|
+
const rowOffsets = new Array(rows);
|
|
1617
|
+
let yAcc = 0;
|
|
1618
|
+
for (let r = 0; r < rows; ++r)
|
|
1619
|
+
{
|
|
1620
|
+
rowOffsets[r] = yAcc;
|
|
1621
|
+
yAcc += rowHeights[r];
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
// second pass: position each child centered in its cell
|
|
1625
|
+
for (let i = 0; i < n; ++i)
|
|
1626
|
+
{
|
|
1627
|
+
const col = i % cols;
|
|
1628
|
+
const row = floor(i / cols);
|
|
1629
|
+
const x = -contentWidth/2 + colOffsets[col] + this.gap * col + colWidths[col] / 2;
|
|
1630
|
+
const y = -contentHeight/2 + rowOffsets[row] + this.gap * row + rowHeights[row] / 2;
|
|
1631
|
+
this.children[i].localPos = vec2(x, y);
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
// container size = content + padding on all sides
|
|
1635
|
+
this.size = vec2(contentWidth + this.padding * 2, contentHeight + this.padding * 2);
|
|
1636
|
+
}
|
|
1498
1637
|
}
|
package/src/engine.js
CHANGED
|
@@ -32,7 +32,7 @@ const engineName = 'LittleJS';
|
|
|
32
32
|
* @type {string}
|
|
33
33
|
* @default
|
|
34
34
|
* @memberof Engine */
|
|
35
|
-
const engineVersion = '1.18.
|
|
35
|
+
const engineVersion = '1.18.7';
|
|
36
36
|
|
|
37
37
|
/** Frames per second to update
|
|
38
38
|
* @type {number}
|
|
@@ -66,7 +66,7 @@ let frame = 0;
|
|
|
66
66
|
* @memberof Engine */
|
|
67
67
|
let time = 0;
|
|
68
68
|
|
|
69
|
-
/** Actual clock time since start in seconds (not affected by pause or frame rate clamping)
|
|
69
|
+
/** Actual clock time since start in seconds (not affected by pause, timescale, or frame rate clamping)
|
|
70
70
|
* @type {number}
|
|
71
71
|
* @memberof Engine */
|
|
72
72
|
let timeReal = 0;
|
|
@@ -197,11 +197,14 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
197
197
|
averageFPS = lerp(averageFPS, 1e3/(frameTimeDeltaMS||1), .05);
|
|
198
198
|
const debugSpeedUp = debug && keyIsDown('Equal'); // +
|
|
199
199
|
const debugSpeedDown = debug && keyIsDown('Minus'); // -
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
200
|
+
const debugScale = debugSpeedUp ? 10 : debugSpeedDown ? .1 : 1;
|
|
201
|
+
|
|
202
|
+
// apply time deltas
|
|
203
|
+
timeReal += frameTimeDeltaMS * debugScale / 1e3;
|
|
204
|
+
const combinedScale = timeScale * debugScale;
|
|
205
|
+
frameTimeDeltaMS *= combinedScale;
|
|
203
206
|
frameTimeBufferMS += paused ? 0 : frameTimeDeltaMS;
|
|
204
|
-
if (
|
|
207
|
+
if (debugScale <= 1)
|
|
205
208
|
frameTimeBufferMS = min(frameTimeBufferMS, 50); // clamp min framerate
|
|
206
209
|
|
|
207
210
|
let wasUpdated = false;
|
|
@@ -559,143 +562,4 @@ function engineObjectsRaycast(start, end, objects=engineObjects)
|
|
|
559
562
|
|
|
560
563
|
debugRaycast && debugLine(start, end, hitObjects.length ? '#f00' : '#00f', .02);
|
|
561
564
|
return hitObjects;
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
565
|
-
function drawEngineLogo(t)
|
|
566
|
-
{
|
|
567
|
-
const blackAndWhite = 0;
|
|
568
|
-
const showName = 1;
|
|
569
|
-
|
|
570
|
-
// LittleJS Logo and Splash Screen
|
|
571
|
-
const x = mainContext;
|
|
572
|
-
const dpr = canvasPixelRatio ?? (devicePixelRatio || 1);
|
|
573
|
-
const w = mainCanvas.width = innerWidth * dpr;
|
|
574
|
-
const h = mainCanvas.height = innerHeight * dpr;
|
|
575
|
-
{
|
|
576
|
-
// background
|
|
577
|
-
const p3 = percent(t, 1, .8);
|
|
578
|
-
const p4 = percent(t, 0, .5);
|
|
579
|
-
const g = x.createRadialGradient(w/2,h/2,0,w/2,h/2,hypot(w,h)*.6);
|
|
580
|
-
g.addColorStop(0,hsl(0,0,lerp(0,p3/2,p4),p3).toString());
|
|
581
|
-
g.addColorStop(1,hsl(0,0,0,p3).toString());
|
|
582
|
-
x.save();
|
|
583
|
-
x.fillStyle = g;
|
|
584
|
-
x.fillRect(0,0,w,h);
|
|
585
|
-
}
|
|
586
|
-
const gradient = (X1,Y1,X2,Y2,C,S=1)=>
|
|
587
|
-
{
|
|
588
|
-
if (C >= 0)
|
|
589
|
-
{
|
|
590
|
-
if (blackAndWhite)
|
|
591
|
-
x.fillStyle = '#fff';
|
|
592
|
-
else
|
|
593
|
-
{
|
|
594
|
-
const g = x.fillStyle = x.createLinearGradient(X1,Y1,X2,Y2);
|
|
595
|
-
g.addColorStop(0,color(C,2));
|
|
596
|
-
g.addColorStop(1,color(C,1));
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
else
|
|
600
|
-
x.fillStyle = '#000';
|
|
601
|
-
C >= -1 ? (x.fill(), S && x.stroke()) : x.stroke();
|
|
602
|
-
}
|
|
603
|
-
const circle = (X,Y,R,A=0,B=2*PI,C,S)=>
|
|
604
|
-
{
|
|
605
|
-
x.beginPath();
|
|
606
|
-
x.arc(X,Y,R,p*A,p*B);
|
|
607
|
-
gradient(X,Y-R,X,Y+R,C,S);
|
|
608
|
-
}
|
|
609
|
-
const rect = (X,Y,W,H,C)=>
|
|
610
|
-
{
|
|
611
|
-
x.beginPath();
|
|
612
|
-
x.rect(X,Y,W,H*p);
|
|
613
|
-
gradient(X,Y+H,X+W,Y,C);
|
|
614
|
-
}
|
|
615
|
-
const poly = (points,C,Y,H)=>
|
|
616
|
-
{
|
|
617
|
-
x.beginPath();
|
|
618
|
-
for (const p of points)
|
|
619
|
-
x.lineTo(p.x, p.y);
|
|
620
|
-
x.closePath();
|
|
621
|
-
gradient(0, Y, 0, Y+H,C);
|
|
622
|
-
}
|
|
623
|
-
const color = (c,l)=> l?`hsl(${[.95,.56,.13][c%3]*360} 99%${[0,50,75][l]}%`:'#000';
|
|
624
|
-
|
|
625
|
-
// center and fit tos screen
|
|
626
|
-
const alpha = oscillate(1,1,t);
|
|
627
|
-
const p = percent(alpha, .1, .5);
|
|
628
|
-
const size = min(6, min(w,h)/99);
|
|
629
|
-
x.translate(w/2,h/2);
|
|
630
|
-
x.scale(size,size);
|
|
631
|
-
x.translate(-40,-35);
|
|
632
|
-
p < 1 && x.setLineDash([99*p,99]);
|
|
633
|
-
x.lineJoin = x.lineCap = 'round';
|
|
634
|
-
x.lineWidth = .1 + p*1.9;
|
|
635
|
-
//x.strokeStyle='#fff7';
|
|
636
|
-
|
|
637
|
-
if (showName)
|
|
638
|
-
{
|
|
639
|
-
// engine name text
|
|
640
|
-
const Y = 54;
|
|
641
|
-
const s = 'LittleJS';
|
|
642
|
-
x.font = '900 15.5px arial';
|
|
643
|
-
x.lineWidth = .1+p*3.9;
|
|
644
|
-
x.textAlign = 'center';
|
|
645
|
-
x.textBaseline = 'top';
|
|
646
|
-
rect(11,Y+1,59,8*p,-1);
|
|
647
|
-
x.beginPath();
|
|
648
|
-
|
|
649
|
-
let w2 = 0;
|
|
650
|
-
for (let i=0;i<s.length;++i)
|
|
651
|
-
w2 += x.measureText(s[i]).width;
|
|
652
|
-
for (let j=2;j--;)
|
|
653
|
-
for (let i=0,X=40-w2/2;i<s.length;++i)
|
|
654
|
-
{
|
|
655
|
-
const w = x.measureText(s[i]).width, X2 = X+w/2;
|
|
656
|
-
gradient(X2,Y,X2+2,Y+13,i>5?1:0);
|
|
657
|
-
x[j?'strokeText':'fillText'](s[i],X2,Y+.5,17*p);
|
|
658
|
-
X += w;
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
x.lineWidth = .1 + p*1.9;
|
|
662
|
-
rect(3,Y,73,0); // bottom
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
rect(7,15,26,-7,0); // cab top
|
|
666
|
-
rect(25,15,8,25,-1); // cab front
|
|
667
|
-
rect(10,40,15,-25,1); // cab back
|
|
668
|
-
rect(14,21,7,9,2); // cab window
|
|
669
|
-
rect(38,20,6,-6,2); // little stack
|
|
670
|
-
|
|
671
|
-
// big stack
|
|
672
|
-
rect(49,20,10,-6,0);
|
|
673
|
-
const stackPoints = [vec2(44,8),vec2(64,8),vec2(59,8+6*p),vec2(49,8+6*p)];
|
|
674
|
-
poly(stackPoints,2,8,6*p);
|
|
675
|
-
rect(44,8,20,-7,0);
|
|
676
|
-
|
|
677
|
-
// engine
|
|
678
|
-
for (let i=5;i--;) circle(59-i*6*p,30,10,0,2*PI,1,0);
|
|
679
|
-
circle(59,30,4,0,7,2); // light
|
|
680
|
-
|
|
681
|
-
// engine outline
|
|
682
|
-
rect(35,20,24,0); // top
|
|
683
|
-
circle(59,30,10); // front
|
|
684
|
-
circle(47,30,10,PI/2,PI*3/2); // middle
|
|
685
|
-
circle(35,30,10,PI/2,PI*3/2); // back
|
|
686
|
-
rect(7,40,13,7,-1); // bottom back
|
|
687
|
-
rect(17,40,43,14,-1); // bottom center
|
|
688
|
-
|
|
689
|
-
// wheels
|
|
690
|
-
for (let i=3;i--;) for (let j=2;j--;) circle(17+15*i,47,j?7:1,0,2*PI,2);
|
|
691
|
-
|
|
692
|
-
// cowcatcher
|
|
693
|
-
for (let i=2;i--;)
|
|
694
|
-
{
|
|
695
|
-
let w=6, s=7, o=53+w*p*i
|
|
696
|
-
const points = [vec2(o+s,54),vec2(o,40),vec2(o+w*p,40),vec2(o+s+w*p,54)];
|
|
697
|
-
poly(points,0,40,14);
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
x.restore();
|
|
701
565
|
}
|
package/src/engineAudio.js
CHANGED
|
@@ -14,10 +14,12 @@
|
|
|
14
14
|
|
|
15
15
|
'use strict';
|
|
16
16
|
|
|
17
|
-
/** Audio context used by the engine
|
|
17
|
+
/** Audio context used by the engine. Created lazily in audioInit() to avoid
|
|
18
|
+
* browser autoplay warnings about constructing an AudioContext before any
|
|
19
|
+
* user gesture.
|
|
18
20
|
* @type {AudioContext}
|
|
19
21
|
* @memberof Audio */
|
|
20
|
-
let audioContext
|
|
22
|
+
let audioContext;
|
|
21
23
|
|
|
22
24
|
/** Master gain node for all audio to pass through
|
|
23
25
|
* @type {GainNode}
|
|
@@ -33,12 +35,13 @@ const audioDefaultSampleRate = 44100;
|
|
|
33
35
|
* @return {boolean} - True if the audio context is running
|
|
34
36
|
* @memberof Audio */
|
|
35
37
|
function audioIsRunning()
|
|
36
|
-
{ return audioContext
|
|
38
|
+
{ return audioContext?.state === 'running'; }
|
|
37
39
|
|
|
38
40
|
function audioInit()
|
|
39
41
|
{
|
|
40
42
|
if (!soundEnable || headlessMode) return;
|
|
41
43
|
|
|
44
|
+
audioContext = new AudioContext;
|
|
42
45
|
audioMasterGain = audioContext.createGain();
|
|
43
46
|
audioMasterGain.connect(audioContext.destination);
|
|
44
47
|
audioMasterGain.gain.value = soundVolume; // set starting value
|
|
@@ -105,8 +108,8 @@ class Sound
|
|
|
105
108
|
|
|
106
109
|
if (Array.isArray(asset))
|
|
107
110
|
{
|
|
108
|
-
// generate zzfx sound
|
|
109
|
-
const zzfxSound = asset;
|
|
111
|
+
// generate zzfx sound — copy so we don't mutate the caller's array
|
|
112
|
+
const zzfxSound = asset.slice();
|
|
110
113
|
|
|
111
114
|
// remove randomness so it can be applied on playback
|
|
112
115
|
const defaultRandomness = randomness ?? .05;
|
package/src/engineBuild.mjs
CHANGED
|
@@ -33,17 +33,20 @@ const engineSourceFiles =
|
|
|
33
33
|
`${SOURCE_FOLDER}/engineAudio.js`,
|
|
34
34
|
`${SOURCE_FOLDER}/engineTileLayer.js`,
|
|
35
35
|
`${SOURCE_FOLDER}/engineParticles.js`,
|
|
36
|
-
`${SOURCE_FOLDER}/engineMedals.js`,
|
|
37
36
|
`${SOURCE_FOLDER}/engineWebGL.js`,
|
|
37
|
+
`${SOURCE_FOLDER}/engineLogo.js`,
|
|
38
38
|
];
|
|
39
39
|
const enginePluginFiles =
|
|
40
40
|
[
|
|
41
|
+
`${PLUGIN_FOLDER}/medalSystem.js`,
|
|
41
42
|
`${PLUGIN_FOLDER}/newgrounds.js`,
|
|
42
43
|
`${PLUGIN_FOLDER}/postProcess.js`,
|
|
43
44
|
`${PLUGIN_FOLDER}/zzfxm.js`,
|
|
44
45
|
`${PLUGIN_FOLDER}/uiSystem.js`,
|
|
45
46
|
`${PLUGIN_FOLDER}/box2d.js`,
|
|
46
47
|
`${PLUGIN_FOLDER}/drawUtilities.js`,
|
|
48
|
+
`${PLUGIN_FOLDER}/tweenSystem.js`,
|
|
49
|
+
`${PLUGIN_FOLDER}/pathFinder.js`,
|
|
47
50
|
];
|
|
48
51
|
const engineExtraFiles =
|
|
49
52
|
[
|
package/src/engineDebug.js
CHANGED
|
@@ -44,7 +44,7 @@ let debugKey = 'Escape';
|
|
|
44
44
|
let debugOverlay = false;
|
|
45
45
|
|
|
46
46
|
// Engine internal variables not exposed to documentation
|
|
47
|
-
let debugPrimitives = [], debugPhysics = false, debugRaycast = false, debugParticles = false, debugGamepads = false,
|
|
47
|
+
let debugPrimitives = [], debugPhysics = false, debugRaycast = false, debugParticles = false, debugGamepads = false, debugTakeScreenshot;
|
|
48
48
|
|
|
49
49
|
///////////////////////////////////////////////////////////////////////////////
|
|
50
50
|
// Debug helper functions
|
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
|
/**
|
|
@@ -211,8 +217,9 @@ class TextureInfo
|
|
|
211
217
|
* Create a TextureInfo, called automatically by the engine
|
|
212
218
|
* @param {HTMLImageElement|OffscreenCanvas} image
|
|
213
219
|
* @param {boolean} [useWebGL] - Should use WebGL if available?
|
|
220
|
+
* @param {boolean} [wrap] - Should the texture wrap (REPEAT) or clamp (CLAMP_TO_EDGE)?
|
|
214
221
|
*/
|
|
215
|
-
constructor(image, useWebGL=true)
|
|
222
|
+
constructor(image, useWebGL=true, wrap=false)
|
|
216
223
|
{
|
|
217
224
|
/** @property {HTMLImageElement|OffscreenCanvas} - image source */
|
|
218
225
|
this.image = image;
|
|
@@ -222,6 +229,8 @@ class TextureInfo
|
|
|
222
229
|
this.sizeInverse = image ? vec2(1/image.width, 1/image.height) : vec2();
|
|
223
230
|
/** @property {WebGLTexture} - WebGL texture */
|
|
224
231
|
this.glTexture = undefined;
|
|
232
|
+
/** @property {boolean} - true for REPEAT wrap mode, false for CLAMP_TO_EDGE */
|
|
233
|
+
this.wrap = wrap;
|
|
225
234
|
useWebGL && this.createWebGLTexture();
|
|
226
235
|
}
|
|
227
236
|
|
|
@@ -234,6 +243,14 @@ class TextureInfo
|
|
|
234
243
|
/** Check if the texture is webgl enabled
|
|
235
244
|
* @return {boolean} */
|
|
236
245
|
hasWebGL() { return !!this.glTexture; }
|
|
246
|
+
|
|
247
|
+
/** Set the wrap mode for this texture
|
|
248
|
+
* @param {boolean} [wrap] - true for REPEAT, false for CLAMP_TO_EDGE */
|
|
249
|
+
setWrap(wrap=true)
|
|
250
|
+
{
|
|
251
|
+
this.wrap = wrap;
|
|
252
|
+
glSetTextureWrap(this.glTexture, wrap);
|
|
253
|
+
}
|
|
237
254
|
}
|
|
238
255
|
|
|
239
256
|
///////////////////////////////////////////////////////////////////////////////
|
|
@@ -295,8 +312,13 @@ function drawTile(pos, size=vec2(1), tileInfo, color=WHITE,
|
|
|
295
312
|
}
|
|
296
313
|
else
|
|
297
314
|
{
|
|
298
|
-
// if no tile info, force untextured
|
|
299
|
-
|
|
315
|
+
// if no tile info, force untextured by zeroing rgba (so whatever
|
|
316
|
+
// texture is bound doesn't leak in) and folding color+additive
|
|
317
|
+
// into the additive slot — matches the Canvas2D path's
|
|
318
|
+
// color.add(additiveColor) on line ~337.
|
|
319
|
+
const combined = additiveColor ? color.add(additiveColor) : color;
|
|
320
|
+
glDraw(pos.x, pos.y, size.x, size.y, angle, 0, 0, 0, 0,
|
|
321
|
+
0, combined.rgbaInt());
|
|
300
322
|
}
|
|
301
323
|
}
|
|
302
324
|
else
|
|
@@ -401,6 +423,98 @@ function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=BLACK, angle=0,
|
|
|
401
423
|
}
|
|
402
424
|
}
|
|
403
425
|
|
|
426
|
+
/** Draw a texture tiled (wrapped) across a rectangle in world space.
|
|
427
|
+
* Useful for backgrounds, repeating patterns, and seamless fills.
|
|
428
|
+
* The whole texture is tiled — sub-region (TileInfo) wrapping is not supported.
|
|
429
|
+
* @param {Vector2} pos - Center of the rect in world space
|
|
430
|
+
* @param {Vector2} size - Size of the rect in world space
|
|
431
|
+
* @param {Vector2} wrapCount - How many times the texture repeats (x, y)
|
|
432
|
+
* @param {TextureInfo|number} [texture=0] - TextureInfo or texture index into textureInfos
|
|
433
|
+
* @param {Color} [color=WHITE] - Color to modulate with
|
|
434
|
+
* @param {number} [angle=0] - Angle to rotate by
|
|
435
|
+
* @param {Color} [additiveColor] - Additive color to be applied if any
|
|
436
|
+
* @param {boolean} [useWebGL=glEnable] - Use accelerated WebGL rendering?
|
|
437
|
+
* @param {boolean} [screenSpace=false] - Are pos and size in screen space?
|
|
438
|
+
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context] - Canvas 2D context to draw to
|
|
439
|
+
* @memberof Draw */
|
|
440
|
+
function drawTextureWrapped(pos, size, wrapCount, texture=0, color=WHITE,
|
|
441
|
+
angle=0, additiveColor, useWebGL=glEnable, screenSpace=false, context)
|
|
442
|
+
{
|
|
443
|
+
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
444
|
+
ASSERT(isVector2(size), 'size must be a vec2');
|
|
445
|
+
ASSERT(isVector2(wrapCount), 'wrapCount must be a vec2');
|
|
446
|
+
ASSERT(isColor(color), 'color is invalid');
|
|
447
|
+
ASSERT(isNumber(angle), 'angle must be a number');
|
|
448
|
+
ASSERT(!additiveColor || isColor(additiveColor), 'additiveColor must be a color');
|
|
449
|
+
ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
|
|
450
|
+
ASSERT(!(texture instanceof TileInfo),
|
|
451
|
+
'pass a TextureInfo or texture index, not a TileInfo — use tileInfo.textureInfo');
|
|
452
|
+
|
|
453
|
+
// short-circuit before texture lookup — textureInfos[0] is undefined in headless mode
|
|
454
|
+
if (headlessMode) return;
|
|
455
|
+
|
|
456
|
+
// resolve texture argument: TextureInfo or index
|
|
457
|
+
const textureInfo = typeof texture === 'number' ? textureInfos[texture] : texture;
|
|
458
|
+
ASSERT(textureInfo instanceof TextureInfo, 'texture not loaded');
|
|
459
|
+
ASSERT(textureInfo.size.x > 0, 'texture not loaded');
|
|
460
|
+
ASSERT(textureInfo.wrap,
|
|
461
|
+
'drawTextureWrapped requires a wrap-enabled texture; call textureInfo.setWrap(true) first');
|
|
462
|
+
|
|
463
|
+
if (useWebGL && glEnable)
|
|
464
|
+
{
|
|
465
|
+
ASSERT(!!glContext, 'WebGL is not enabled!');
|
|
466
|
+
if (screenSpace)
|
|
467
|
+
[pos, size, angle] = screenToWorldTransform(pos, size, angle);
|
|
468
|
+
glSetTexture(textureInfo.glTexture);
|
|
469
|
+
glDraw(pos.x, pos.y, size.x, size.y, angle,
|
|
470
|
+
0, 0, wrapCount.x, wrapCount.y,
|
|
471
|
+
color.rgbaInt(), additiveColor && additiveColor.rgbaInt());
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Canvas2D path — increment drawCount here (WebGL batch counts via glBatchCount)
|
|
476
|
+
++drawCount;
|
|
477
|
+
|
|
478
|
+
if (!screenSpace)
|
|
479
|
+
{
|
|
480
|
+
pos = worldToScreen(pos);
|
|
481
|
+
size = size.scale(cameraScale);
|
|
482
|
+
angle -= cameraAngle;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// pick image source: raw, or tinted bake. Match drawImageColor's
|
|
486
|
+
// "no tint needed" predicate so behavior stays consistent.
|
|
487
|
+
const noTint = !canvasColorTiles ||
|
|
488
|
+
(additiveColor
|
|
489
|
+
? isWhite(color.add(additiveColor)) && additiveColor.a <= 0
|
|
490
|
+
: isWhite(color));
|
|
491
|
+
// alpha is baked into pixels by bakeTintedImage's additive branch;
|
|
492
|
+
// in that case globalAlpha must NOT also apply color.a
|
|
493
|
+
const alphaBaked = !noTint && additiveColor && !isBlack(additiveColor);
|
|
494
|
+
const source = noTint
|
|
495
|
+
? textureInfo.image
|
|
496
|
+
: bakeTintedImage(textureInfo.image, color, additiveColor);
|
|
497
|
+
|
|
498
|
+
context = context || drawContext;
|
|
499
|
+
context.save();
|
|
500
|
+
context.translate(pos.x + .5, pos.y + .5);
|
|
501
|
+
context.rotate(angle);
|
|
502
|
+
context.globalAlpha = alphaBaked ? 1 : color.a;
|
|
503
|
+
|
|
504
|
+
const pattern = context.createPattern(source, 'repeat');
|
|
505
|
+
// map pattern-source pixels into user space so the rect contains
|
|
506
|
+
// wrapCount.x × wrapCount.y repeats
|
|
507
|
+
const m = new DOMMatrix()
|
|
508
|
+
.translate(-size.x/2, -size.y/2)
|
|
509
|
+
.scale(size.x / (wrapCount.x * source.width),
|
|
510
|
+
size.y / (wrapCount.y * source.height));
|
|
511
|
+
pattern.setTransform(m);
|
|
512
|
+
context.fillStyle = pattern;
|
|
513
|
+
context.fillRect(-size.x/2, -size.y/2, size.x, size.y);
|
|
514
|
+
context.globalAlpha = 1;
|
|
515
|
+
context.restore();
|
|
516
|
+
}
|
|
517
|
+
|
|
404
518
|
/** Draw connected lines between a series of points
|
|
405
519
|
* @param {Array<Vector2>} points
|
|
406
520
|
* @param {number} [width]
|
|
@@ -924,6 +1038,43 @@ function combineCanvases()
|
|
|
924
1038
|
mainContext.drawImage(workCanvas, 0, 0);
|
|
925
1039
|
}
|
|
926
1040
|
|
|
1041
|
+
// Internal: bake a color/additive-color tint into workReadCanvas at the
|
|
1042
|
+
// image's native resolution. Returns the work canvas, suitable for
|
|
1043
|
+
// passing to context.createPattern. Used by drawTextureWrapped's
|
|
1044
|
+
// Canvas2D path. Caller is responsible for short-circuiting when no
|
|
1045
|
+
// tint is needed (i.e. color is white and additiveColor is black/none).
|
|
1046
|
+
function bakeTintedImage(image, color, additiveColor)
|
|
1047
|
+
{
|
|
1048
|
+
const w = image.width|0, h = image.height|0;
|
|
1049
|
+
workReadCanvas.width = w;
|
|
1050
|
+
workReadCanvas.height = h;
|
|
1051
|
+
workReadContext.drawImage(image, 0, 0);
|
|
1052
|
+
|
|
1053
|
+
const imageData = workReadContext.getImageData(0, 0, w, h);
|
|
1054
|
+
const data = imageData.data;
|
|
1055
|
+
if (additiveColor && !isBlack(additiveColor))
|
|
1056
|
+
{
|
|
1057
|
+
// multiply + additive (slower)
|
|
1058
|
+
const colorMultiply = [color.r, color.g, color.b, color.a];
|
|
1059
|
+
const colorAdd = [additiveColor.r * 255, additiveColor.g * 255,
|
|
1060
|
+
additiveColor.b * 255, additiveColor.a * 255];
|
|
1061
|
+
for (let i = 0; i < data.length; ++i)
|
|
1062
|
+
data[i] = data[i] * colorMultiply[i&3] + colorAdd[i&3] |0;
|
|
1063
|
+
}
|
|
1064
|
+
else
|
|
1065
|
+
{
|
|
1066
|
+
// RGB only, faster — alpha left intact for the caller
|
|
1067
|
+
for (let i = 0; i < data.length; i+=4)
|
|
1068
|
+
{
|
|
1069
|
+
data[i ] *= color.r;
|
|
1070
|
+
data[i+1] *= color.g;
|
|
1071
|
+
data[i+2] *= color.b;
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
workReadContext.putImageData(imageData, 0, 0);
|
|
1075
|
+
return workReadCanvas;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
927
1078
|
/** Helper function to draw an image with color and additive color applied
|
|
928
1079
|
* This is slower then normal drawImage when color is applied
|
|
929
1080
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} context
|
|
@@ -942,8 +1093,6 @@ function combineCanvases()
|
|
|
942
1093
|
* @memberof Draw */
|
|
943
1094
|
function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight, color, additiveColor, bleed=0)
|
|
944
1095
|
{
|
|
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
1096
|
const sx2 = bleed;
|
|
948
1097
|
const sy2 = bleed;
|
|
949
1098
|
sWidth = max(1,sWidth|0);
|