@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/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
- currentX += 20;
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,