@blorkfield/overlay-core 0.5.10 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +674 -0
- package/dist/index.cjs +316 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +176 -2
- package/dist/index.d.ts +176 -2
- package/dist/index.js +315 -3
- package/dist/index.js.map +1 -1
- package/package.json +11 -8
package/dist/index.cjs
CHANGED
|
@@ -30,6 +30,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
|
+
BackgroundManager: () => BackgroundManager,
|
|
33
34
|
OverlayScene: () => OverlayScene,
|
|
34
35
|
clearFontCache: () => clearFontCache,
|
|
35
36
|
getGlyphData: () => getGlyphData,
|
|
@@ -60,7 +61,7 @@ function createRender(engine, canvas, config) {
|
|
|
60
61
|
width: config.bounds.right - config.bounds.left,
|
|
61
62
|
height: config.bounds.bottom - config.bounds.top,
|
|
62
63
|
wireframes: config.debug ?? false,
|
|
63
|
-
background: config.background ?? "transparent"
|
|
64
|
+
background: config.background?.color ?? "transparent"
|
|
64
65
|
}
|
|
65
66
|
});
|
|
66
67
|
return render;
|
|
@@ -1154,6 +1155,269 @@ var EffectManager = class {
|
|
|
1154
1155
|
}
|
|
1155
1156
|
};
|
|
1156
1157
|
|
|
1158
|
+
// src/backgroundManager.ts
|
|
1159
|
+
var LOG_PREFIX4 = "BackgroundManager";
|
|
1160
|
+
var imageCache = /* @__PURE__ */ new Map();
|
|
1161
|
+
var BackgroundManager = class {
|
|
1162
|
+
constructor(managerConfig) {
|
|
1163
|
+
this.config = null;
|
|
1164
|
+
this.loadedImage = null;
|
|
1165
|
+
// Offscreen canvas for caching pre-rendered base layers
|
|
1166
|
+
this.baseLayerCanvas = null;
|
|
1167
|
+
this.baseLayerCtx = null;
|
|
1168
|
+
this.baseLayerDirty = true;
|
|
1169
|
+
this.canvas = managerConfig.canvas;
|
|
1170
|
+
const ctx = managerConfig.canvas.getContext("2d");
|
|
1171
|
+
if (!ctx) {
|
|
1172
|
+
throw new Error("Failed to get 2D context from canvas");
|
|
1173
|
+
}
|
|
1174
|
+
this.ctx = ctx;
|
|
1175
|
+
this.width = managerConfig.width;
|
|
1176
|
+
this.height = managerConfig.height;
|
|
1177
|
+
this.createOffscreenCanvas();
|
|
1178
|
+
}
|
|
1179
|
+
/**
|
|
1180
|
+
* Create or recreate the offscreen canvas for base layer caching.
|
|
1181
|
+
*/
|
|
1182
|
+
createOffscreenCanvas() {
|
|
1183
|
+
this.baseLayerCanvas = document.createElement("canvas");
|
|
1184
|
+
this.baseLayerCanvas.width = this.width;
|
|
1185
|
+
this.baseLayerCanvas.height = this.height;
|
|
1186
|
+
this.baseLayerCtx = this.baseLayerCanvas.getContext("2d");
|
|
1187
|
+
this.baseLayerDirty = true;
|
|
1188
|
+
}
|
|
1189
|
+
/**
|
|
1190
|
+
* Set the background configuration.
|
|
1191
|
+
*/
|
|
1192
|
+
async setConfig(config) {
|
|
1193
|
+
if (!config) {
|
|
1194
|
+
this.config = null;
|
|
1195
|
+
this.loadedImage = null;
|
|
1196
|
+
this.baseLayerDirty = true;
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1199
|
+
this.config = config;
|
|
1200
|
+
if (config.image?.url) {
|
|
1201
|
+
await this.loadBackgroundImage(config.image.url);
|
|
1202
|
+
} else {
|
|
1203
|
+
this.loadedImage = null;
|
|
1204
|
+
}
|
|
1205
|
+
this.baseLayerDirty = true;
|
|
1206
|
+
}
|
|
1207
|
+
/**
|
|
1208
|
+
* Load and cache a background image.
|
|
1209
|
+
*/
|
|
1210
|
+
async loadBackgroundImage(url) {
|
|
1211
|
+
const cached = imageCache.get(url);
|
|
1212
|
+
if (cached) {
|
|
1213
|
+
logger.debug(LOG_PREFIX4, `Using cached image: ${url}`);
|
|
1214
|
+
this.loadedImage = cached;
|
|
1215
|
+
return;
|
|
1216
|
+
}
|
|
1217
|
+
try {
|
|
1218
|
+
logger.debug(LOG_PREFIX4, `Loading background image: ${url}`);
|
|
1219
|
+
const img = await loadImage(url);
|
|
1220
|
+
imageCache.set(url, img);
|
|
1221
|
+
this.loadedImage = img;
|
|
1222
|
+
this.baseLayerDirty = true;
|
|
1223
|
+
logger.debug(LOG_PREFIX4, `Background image loaded`, {
|
|
1224
|
+
width: img.width,
|
|
1225
|
+
height: img.height
|
|
1226
|
+
});
|
|
1227
|
+
} catch (err) {
|
|
1228
|
+
logger.error(LOG_PREFIX4, `Failed to load background image: ${url}`, {
|
|
1229
|
+
error: String(err)
|
|
1230
|
+
});
|
|
1231
|
+
this.loadedImage = null;
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
/**
|
|
1235
|
+
* Update the canvas dimensions (call on resize).
|
|
1236
|
+
*/
|
|
1237
|
+
resize(width, height) {
|
|
1238
|
+
this.width = width;
|
|
1239
|
+
this.height = height;
|
|
1240
|
+
this.createOffscreenCanvas();
|
|
1241
|
+
}
|
|
1242
|
+
/**
|
|
1243
|
+
* Get the current background config.
|
|
1244
|
+
*/
|
|
1245
|
+
getConfig() {
|
|
1246
|
+
return this.config;
|
|
1247
|
+
}
|
|
1248
|
+
/**
|
|
1249
|
+
* Check if we have custom layers to render.
|
|
1250
|
+
* Returns true if we need to handle background ourselves.
|
|
1251
|
+
*/
|
|
1252
|
+
hasCustomLayers() {
|
|
1253
|
+
if (!this.config) return false;
|
|
1254
|
+
if (this.config.transparency?.opacity === 1) return true;
|
|
1255
|
+
return !!(this.config.image || this.config.transparency);
|
|
1256
|
+
}
|
|
1257
|
+
/**
|
|
1258
|
+
* Check if background is fully transparent (no color, no image, opacity = 0 or no transparency config).
|
|
1259
|
+
*/
|
|
1260
|
+
isFullyTransparent() {
|
|
1261
|
+
if (!this.config) return true;
|
|
1262
|
+
const noColor = !this.config.color;
|
|
1263
|
+
const noImage = !this.config.image;
|
|
1264
|
+
const noOverlay = !this.config.transparency || this.config.transparency.opacity <= 0;
|
|
1265
|
+
return noColor && noImage && noOverlay;
|
|
1266
|
+
}
|
|
1267
|
+
/**
|
|
1268
|
+
* Pre-render the base layers (color + image) to the offscreen canvas.
|
|
1269
|
+
* Only called when the config changes (baseLayerDirty is true).
|
|
1270
|
+
*/
|
|
1271
|
+
prerenderBaseLayers() {
|
|
1272
|
+
if (!this.baseLayerCtx || !this.baseLayerCanvas) return;
|
|
1273
|
+
const ctx = this.baseLayerCtx;
|
|
1274
|
+
ctx.clearRect(0, 0, this.width, this.height);
|
|
1275
|
+
if (!this.config) {
|
|
1276
|
+
this.baseLayerDirty = false;
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1279
|
+
if (this.config.color && this.config.color !== "transparent") {
|
|
1280
|
+
ctx.fillStyle = this.config.color;
|
|
1281
|
+
ctx.fillRect(0, 0, this.width, this.height);
|
|
1282
|
+
}
|
|
1283
|
+
if (this.loadedImage && this.config.image) {
|
|
1284
|
+
this.renderImageLayerToContext(ctx, this.loadedImage, this.config.image);
|
|
1285
|
+
}
|
|
1286
|
+
this.baseLayerDirty = false;
|
|
1287
|
+
logger.debug(LOG_PREFIX4, "Base layers pre-rendered to offscreen canvas");
|
|
1288
|
+
}
|
|
1289
|
+
/**
|
|
1290
|
+
* Render the base layers (color + image).
|
|
1291
|
+
* Called BEFORE Matter.js renders physics objects.
|
|
1292
|
+
* Uses cached offscreen canvas for performance.
|
|
1293
|
+
*/
|
|
1294
|
+
renderBaseLayers() {
|
|
1295
|
+
if (this.isFullyTransparent()) return;
|
|
1296
|
+
if (this.baseLayerDirty) {
|
|
1297
|
+
this.prerenderBaseLayers();
|
|
1298
|
+
}
|
|
1299
|
+
if (this.baseLayerCanvas && this.config) {
|
|
1300
|
+
this.ctx.drawImage(this.baseLayerCanvas, 0, 0);
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
/**
|
|
1304
|
+
* Render the transparency/frosted glass layer.
|
|
1305
|
+
* Called AFTER Matter.js renders physics objects.
|
|
1306
|
+
* This must render every frame since it overlays moving objects.
|
|
1307
|
+
*/
|
|
1308
|
+
renderOverlay() {
|
|
1309
|
+
if (!this.config?.transparency) return;
|
|
1310
|
+
this.renderTransparencyLayer(this.config.transparency);
|
|
1311
|
+
}
|
|
1312
|
+
/**
|
|
1313
|
+
* Render the background image to a given context with specified sizing.
|
|
1314
|
+
*/
|
|
1315
|
+
renderImageLayerToContext(ctx, img, config) {
|
|
1316
|
+
const sizing = config.sizing ?? "cover";
|
|
1317
|
+
ctx.save();
|
|
1318
|
+
switch (sizing) {
|
|
1319
|
+
case "stretch":
|
|
1320
|
+
ctx.drawImage(img, 0, 0, this.width, this.height);
|
|
1321
|
+
break;
|
|
1322
|
+
case "center":
|
|
1323
|
+
this.drawCenter(ctx, img);
|
|
1324
|
+
break;
|
|
1325
|
+
case "tile":
|
|
1326
|
+
this.drawTile(ctx, img);
|
|
1327
|
+
break;
|
|
1328
|
+
case "cover":
|
|
1329
|
+
this.drawCover(ctx, img);
|
|
1330
|
+
break;
|
|
1331
|
+
case "contain":
|
|
1332
|
+
this.drawContain(ctx, img);
|
|
1333
|
+
break;
|
|
1334
|
+
}
|
|
1335
|
+
ctx.restore();
|
|
1336
|
+
}
|
|
1337
|
+
/**
|
|
1338
|
+
* Draw image centered at original size.
|
|
1339
|
+
*/
|
|
1340
|
+
drawCenter(ctx, img) {
|
|
1341
|
+
const x = (this.width - img.width) / 2;
|
|
1342
|
+
const y = (this.height - img.height) / 2;
|
|
1343
|
+
ctx.drawImage(img, x, y);
|
|
1344
|
+
}
|
|
1345
|
+
/**
|
|
1346
|
+
* Draw image tiled (repeating pattern).
|
|
1347
|
+
*/
|
|
1348
|
+
drawTile(ctx, img) {
|
|
1349
|
+
const pattern = ctx.createPattern(img, "repeat");
|
|
1350
|
+
if (pattern) {
|
|
1351
|
+
ctx.fillStyle = pattern;
|
|
1352
|
+
ctx.fillRect(0, 0, this.width, this.height);
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
/**
|
|
1356
|
+
* Draw image to cover entire area (may crop, maintains aspect ratio).
|
|
1357
|
+
*/
|
|
1358
|
+
drawCover(ctx, img) {
|
|
1359
|
+
const imgRatio = img.width / img.height;
|
|
1360
|
+
const canvasRatio = this.width / this.height;
|
|
1361
|
+
let drawWidth, drawHeight;
|
|
1362
|
+
let offsetX = 0, offsetY = 0;
|
|
1363
|
+
if (imgRatio > canvasRatio) {
|
|
1364
|
+
drawHeight = this.height;
|
|
1365
|
+
drawWidth = this.height * imgRatio;
|
|
1366
|
+
offsetX = (this.width - drawWidth) / 2;
|
|
1367
|
+
} else {
|
|
1368
|
+
drawWidth = this.width;
|
|
1369
|
+
drawHeight = this.width / imgRatio;
|
|
1370
|
+
offsetY = (this.height - drawHeight) / 2;
|
|
1371
|
+
}
|
|
1372
|
+
ctx.drawImage(img, offsetX, offsetY, drawWidth, drawHeight);
|
|
1373
|
+
}
|
|
1374
|
+
/**
|
|
1375
|
+
* Draw image to fit within area (may have gaps, maintains aspect ratio).
|
|
1376
|
+
* Gaps are filled by the color layer underneath.
|
|
1377
|
+
*/
|
|
1378
|
+
drawContain(ctx, img) {
|
|
1379
|
+
const imgRatio = img.width / img.height;
|
|
1380
|
+
const canvasRatio = this.width / this.height;
|
|
1381
|
+
let drawWidth, drawHeight;
|
|
1382
|
+
let offsetX = 0, offsetY = 0;
|
|
1383
|
+
if (imgRatio > canvasRatio) {
|
|
1384
|
+
drawWidth = this.width;
|
|
1385
|
+
drawHeight = this.width / imgRatio;
|
|
1386
|
+
offsetY = (this.height - drawHeight) / 2;
|
|
1387
|
+
} else {
|
|
1388
|
+
drawHeight = this.height;
|
|
1389
|
+
drawWidth = this.height * imgRatio;
|
|
1390
|
+
offsetX = (this.width - drawWidth) / 2;
|
|
1391
|
+
}
|
|
1392
|
+
ctx.drawImage(img, offsetX, offsetY, drawWidth, drawHeight);
|
|
1393
|
+
}
|
|
1394
|
+
/**
|
|
1395
|
+
* Render the transparency/frosted glass layer.
|
|
1396
|
+
* opacity = 0 means fully transparent (nothing drawn)
|
|
1397
|
+
* opacity = 1 means fully opaque overlay
|
|
1398
|
+
* opacity = 0.3 means light frosted glass
|
|
1399
|
+
*/
|
|
1400
|
+
renderTransparencyLayer(config) {
|
|
1401
|
+
if (config.opacity <= 0) return;
|
|
1402
|
+
this.ctx.save();
|
|
1403
|
+
this.ctx.globalAlpha = config.opacity;
|
|
1404
|
+
if (config.tintColor) {
|
|
1405
|
+
this.ctx.fillStyle = config.tintColor;
|
|
1406
|
+
} else {
|
|
1407
|
+
this.ctx.fillStyle = "#ffffff";
|
|
1408
|
+
}
|
|
1409
|
+
this.ctx.fillRect(0, 0, this.width, this.height);
|
|
1410
|
+
this.ctx.restore();
|
|
1411
|
+
}
|
|
1412
|
+
/**
|
|
1413
|
+
* Clear the background image cache.
|
|
1414
|
+
*/
|
|
1415
|
+
static clearCache() {
|
|
1416
|
+
imageCache.clear();
|
|
1417
|
+
logger.debug(LOG_PREFIX4, "Image cache cleared");
|
|
1418
|
+
}
|
|
1419
|
+
};
|
|
1420
|
+
|
|
1157
1421
|
// src/OverlayScene.ts
|
|
1158
1422
|
var OverlayScene = class {
|
|
1159
1423
|
constructor(canvas, config) {
|
|
@@ -1211,6 +1475,22 @@ var OverlayScene = class {
|
|
|
1211
1475
|
}
|
|
1212
1476
|
}
|
|
1213
1477
|
};
|
|
1478
|
+
/**
|
|
1479
|
+
* Handler for Matter.js beforeRender event.
|
|
1480
|
+
* Draws base background layers (color + image) before physics objects.
|
|
1481
|
+
*/
|
|
1482
|
+
this.handleBeforeRender = () => {
|
|
1483
|
+
if (this.backgroundManager.hasCustomLayers()) {
|
|
1484
|
+
this.backgroundManager.renderBaseLayers();
|
|
1485
|
+
}
|
|
1486
|
+
};
|
|
1487
|
+
/**
|
|
1488
|
+
* Handler for Matter.js afterRender event.
|
|
1489
|
+
* Draws transparency/frosted glass layer after physics objects.
|
|
1490
|
+
*/
|
|
1491
|
+
this.handleAfterRender = () => {
|
|
1492
|
+
this.backgroundManager.renderOverlay();
|
|
1493
|
+
};
|
|
1214
1494
|
// ==================== PRIVATE ====================
|
|
1215
1495
|
this.loop = () => {
|
|
1216
1496
|
this.effectManager.update();
|
|
@@ -1244,7 +1524,6 @@ var OverlayScene = class {
|
|
|
1244
1524
|
gravity: 1,
|
|
1245
1525
|
wrapHorizontal: true,
|
|
1246
1526
|
debug: false,
|
|
1247
|
-
background: "transparent",
|
|
1248
1527
|
...config
|
|
1249
1528
|
};
|
|
1250
1529
|
this.engine = createEngine(this.config.gravity);
|
|
@@ -1279,6 +1558,23 @@ var OverlayScene = class {
|
|
|
1279
1558
|
(cfg) => this.spawnObjectAsync(cfg),
|
|
1280
1559
|
(id) => this.objects.get(id)?.body ?? null
|
|
1281
1560
|
);
|
|
1561
|
+
const width = this.config.bounds.right - this.config.bounds.left;
|
|
1562
|
+
const height = this.config.bounds.bottom - this.config.bounds.top;
|
|
1563
|
+
this.backgroundManager = new BackgroundManager({
|
|
1564
|
+
canvas,
|
|
1565
|
+
width,
|
|
1566
|
+
height
|
|
1567
|
+
});
|
|
1568
|
+
this.backgroundManager.setConfig(this.config.background).catch((err) => {
|
|
1569
|
+
logger.error("OverlayScene", "Failed to initialize background", {
|
|
1570
|
+
error: String(err)
|
|
1571
|
+
});
|
|
1572
|
+
});
|
|
1573
|
+
if (this.backgroundManager.hasCustomLayers()) {
|
|
1574
|
+
this.render.options.background = "transparent";
|
|
1575
|
+
}
|
|
1576
|
+
import_matter_js5.default.Events.on(this.render, "beforeRender", this.handleBeforeRender);
|
|
1577
|
+
import_matter_js5.default.Events.on(this.render, "afterRender", this.handleAfterRender);
|
|
1282
1578
|
}
|
|
1283
1579
|
static createContainer(parent, options = {}) {
|
|
1284
1580
|
const canvas = document.createElement("canvas");
|
|
@@ -1681,6 +1977,8 @@ var OverlayScene = class {
|
|
|
1681
1977
|
import_matter_js5.default.Events.off(this.mouseConstraint, "startdrag", this.handleStartDrag);
|
|
1682
1978
|
}
|
|
1683
1979
|
this.canvas.removeEventListener("click", this.handleCanvasClick);
|
|
1980
|
+
import_matter_js5.default.Events.off(this.render, "beforeRender", this.handleBeforeRender);
|
|
1981
|
+
import_matter_js5.default.Events.off(this.render, "afterRender", this.handleAfterRender);
|
|
1684
1982
|
import_matter_js5.default.Engine.clear(this.engine);
|
|
1685
1983
|
this.objects.clear();
|
|
1686
1984
|
this.obstaclePressure.clear();
|
|
@@ -1699,6 +1997,18 @@ var OverlayScene = class {
|
|
|
1699
1997
|
}
|
|
1700
1998
|
}
|
|
1701
1999
|
}
|
|
2000
|
+
/**
|
|
2001
|
+
* Update the background configuration at runtime.
|
|
2002
|
+
*/
|
|
2003
|
+
async setBackground(config) {
|
|
2004
|
+
await this.backgroundManager.setConfig(config);
|
|
2005
|
+
if (this.backgroundManager.hasCustomLayers()) {
|
|
2006
|
+
this.render.options.background = "transparent";
|
|
2007
|
+
} else {
|
|
2008
|
+
const bgConfig = this.backgroundManager.getConfig();
|
|
2009
|
+
this.render.options.background = bgConfig?.color ?? "transparent";
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
1702
2012
|
resize(width, height) {
|
|
1703
2013
|
this.canvas.width = width;
|
|
1704
2014
|
this.canvas.height = height;
|
|
@@ -1716,6 +2026,7 @@ var OverlayScene = class {
|
|
|
1716
2026
|
this.render.canvas.width = width;
|
|
1717
2027
|
this.render.canvas.height = height;
|
|
1718
2028
|
this.effectManager.setBounds(this.config.bounds);
|
|
2029
|
+
this.backgroundManager.resize(width, height);
|
|
1719
2030
|
}
|
|
1720
2031
|
// ==================== OBJECT METHODS ====================
|
|
1721
2032
|
/**
|
|
@@ -2314,7 +2625,8 @@ var OverlayScene = class {
|
|
|
2314
2625
|
for (let i = 0; i < chars.length; i++) {
|
|
2315
2626
|
const char = chars[i];
|
|
2316
2627
|
if (char === " ") {
|
|
2317
|
-
|
|
2628
|
+
const spaceWidth = config.letterSpacing ?? letterSize;
|
|
2629
|
+
currentX += spaceWidth;
|
|
2318
2630
|
globalCharIndex++;
|
|
2319
2631
|
if (inWord) {
|
|
2320
2632
|
currentWordIndex++;
|
|
@@ -2838,6 +3150,7 @@ var OverlayScene = class {
|
|
|
2838
3150
|
};
|
|
2839
3151
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2840
3152
|
0 && (module.exports = {
|
|
3153
|
+
BackgroundManager,
|
|
2841
3154
|
OverlayScene,
|
|
2842
3155
|
clearFontCache,
|
|
2843
3156
|
getGlyphData,
|