@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.js CHANGED
@@ -16,7 +16,7 @@ function createRender(engine, canvas, config) {
16
16
  width: config.bounds.right - config.bounds.left,
17
17
  height: config.bounds.bottom - config.bounds.top,
18
18
  wireframes: config.debug ?? false,
19
- background: config.background ?? "transparent"
19
+ background: config.background?.color ?? "transparent"
20
20
  }
21
21
  });
22
22
  return render;
@@ -1110,6 +1110,269 @@ var EffectManager = class {
1110
1110
  }
1111
1111
  };
1112
1112
 
1113
+ // src/backgroundManager.ts
1114
+ var LOG_PREFIX4 = "BackgroundManager";
1115
+ var imageCache = /* @__PURE__ */ new Map();
1116
+ var BackgroundManager = class {
1117
+ constructor(managerConfig) {
1118
+ this.config = null;
1119
+ this.loadedImage = null;
1120
+ // Offscreen canvas for caching pre-rendered base layers
1121
+ this.baseLayerCanvas = null;
1122
+ this.baseLayerCtx = null;
1123
+ this.baseLayerDirty = true;
1124
+ this.canvas = managerConfig.canvas;
1125
+ const ctx = managerConfig.canvas.getContext("2d");
1126
+ if (!ctx) {
1127
+ throw new Error("Failed to get 2D context from canvas");
1128
+ }
1129
+ this.ctx = ctx;
1130
+ this.width = managerConfig.width;
1131
+ this.height = managerConfig.height;
1132
+ this.createOffscreenCanvas();
1133
+ }
1134
+ /**
1135
+ * Create or recreate the offscreen canvas for base layer caching.
1136
+ */
1137
+ createOffscreenCanvas() {
1138
+ this.baseLayerCanvas = document.createElement("canvas");
1139
+ this.baseLayerCanvas.width = this.width;
1140
+ this.baseLayerCanvas.height = this.height;
1141
+ this.baseLayerCtx = this.baseLayerCanvas.getContext("2d");
1142
+ this.baseLayerDirty = true;
1143
+ }
1144
+ /**
1145
+ * Set the background configuration.
1146
+ */
1147
+ async setConfig(config) {
1148
+ if (!config) {
1149
+ this.config = null;
1150
+ this.loadedImage = null;
1151
+ this.baseLayerDirty = true;
1152
+ return;
1153
+ }
1154
+ this.config = config;
1155
+ if (config.image?.url) {
1156
+ await this.loadBackgroundImage(config.image.url);
1157
+ } else {
1158
+ this.loadedImage = null;
1159
+ }
1160
+ this.baseLayerDirty = true;
1161
+ }
1162
+ /**
1163
+ * Load and cache a background image.
1164
+ */
1165
+ async loadBackgroundImage(url) {
1166
+ const cached = imageCache.get(url);
1167
+ if (cached) {
1168
+ logger.debug(LOG_PREFIX4, `Using cached image: ${url}`);
1169
+ this.loadedImage = cached;
1170
+ return;
1171
+ }
1172
+ try {
1173
+ logger.debug(LOG_PREFIX4, `Loading background image: ${url}`);
1174
+ const img = await loadImage(url);
1175
+ imageCache.set(url, img);
1176
+ this.loadedImage = img;
1177
+ this.baseLayerDirty = true;
1178
+ logger.debug(LOG_PREFIX4, `Background image loaded`, {
1179
+ width: img.width,
1180
+ height: img.height
1181
+ });
1182
+ } catch (err) {
1183
+ logger.error(LOG_PREFIX4, `Failed to load background image: ${url}`, {
1184
+ error: String(err)
1185
+ });
1186
+ this.loadedImage = null;
1187
+ }
1188
+ }
1189
+ /**
1190
+ * Update the canvas dimensions (call on resize).
1191
+ */
1192
+ resize(width, height) {
1193
+ this.width = width;
1194
+ this.height = height;
1195
+ this.createOffscreenCanvas();
1196
+ }
1197
+ /**
1198
+ * Get the current background config.
1199
+ */
1200
+ getConfig() {
1201
+ return this.config;
1202
+ }
1203
+ /**
1204
+ * Check if we have custom layers to render.
1205
+ * Returns true if we need to handle background ourselves.
1206
+ */
1207
+ hasCustomLayers() {
1208
+ if (!this.config) return false;
1209
+ if (this.config.transparency?.opacity === 1) return true;
1210
+ return !!(this.config.image || this.config.transparency);
1211
+ }
1212
+ /**
1213
+ * Check if background is fully transparent (no color, no image, opacity = 0 or no transparency config).
1214
+ */
1215
+ isFullyTransparent() {
1216
+ if (!this.config) return true;
1217
+ const noColor = !this.config.color;
1218
+ const noImage = !this.config.image;
1219
+ const noOverlay = !this.config.transparency || this.config.transparency.opacity <= 0;
1220
+ return noColor && noImage && noOverlay;
1221
+ }
1222
+ /**
1223
+ * Pre-render the base layers (color + image) to the offscreen canvas.
1224
+ * Only called when the config changes (baseLayerDirty is true).
1225
+ */
1226
+ prerenderBaseLayers() {
1227
+ if (!this.baseLayerCtx || !this.baseLayerCanvas) return;
1228
+ const ctx = this.baseLayerCtx;
1229
+ ctx.clearRect(0, 0, this.width, this.height);
1230
+ if (!this.config) {
1231
+ this.baseLayerDirty = false;
1232
+ return;
1233
+ }
1234
+ if (this.config.color && this.config.color !== "transparent") {
1235
+ ctx.fillStyle = this.config.color;
1236
+ ctx.fillRect(0, 0, this.width, this.height);
1237
+ }
1238
+ if (this.loadedImage && this.config.image) {
1239
+ this.renderImageLayerToContext(ctx, this.loadedImage, this.config.image);
1240
+ }
1241
+ this.baseLayerDirty = false;
1242
+ logger.debug(LOG_PREFIX4, "Base layers pre-rendered to offscreen canvas");
1243
+ }
1244
+ /**
1245
+ * Render the base layers (color + image).
1246
+ * Called BEFORE Matter.js renders physics objects.
1247
+ * Uses cached offscreen canvas for performance.
1248
+ */
1249
+ renderBaseLayers() {
1250
+ if (this.isFullyTransparent()) return;
1251
+ if (this.baseLayerDirty) {
1252
+ this.prerenderBaseLayers();
1253
+ }
1254
+ if (this.baseLayerCanvas && this.config) {
1255
+ this.ctx.drawImage(this.baseLayerCanvas, 0, 0);
1256
+ }
1257
+ }
1258
+ /**
1259
+ * Render the transparency/frosted glass layer.
1260
+ * Called AFTER Matter.js renders physics objects.
1261
+ * This must render every frame since it overlays moving objects.
1262
+ */
1263
+ renderOverlay() {
1264
+ if (!this.config?.transparency) return;
1265
+ this.renderTransparencyLayer(this.config.transparency);
1266
+ }
1267
+ /**
1268
+ * Render the background image to a given context with specified sizing.
1269
+ */
1270
+ renderImageLayerToContext(ctx, img, config) {
1271
+ const sizing = config.sizing ?? "cover";
1272
+ ctx.save();
1273
+ switch (sizing) {
1274
+ case "stretch":
1275
+ ctx.drawImage(img, 0, 0, this.width, this.height);
1276
+ break;
1277
+ case "center":
1278
+ this.drawCenter(ctx, img);
1279
+ break;
1280
+ case "tile":
1281
+ this.drawTile(ctx, img);
1282
+ break;
1283
+ case "cover":
1284
+ this.drawCover(ctx, img);
1285
+ break;
1286
+ case "contain":
1287
+ this.drawContain(ctx, img);
1288
+ break;
1289
+ }
1290
+ ctx.restore();
1291
+ }
1292
+ /**
1293
+ * Draw image centered at original size.
1294
+ */
1295
+ drawCenter(ctx, img) {
1296
+ const x = (this.width - img.width) / 2;
1297
+ const y = (this.height - img.height) / 2;
1298
+ ctx.drawImage(img, x, y);
1299
+ }
1300
+ /**
1301
+ * Draw image tiled (repeating pattern).
1302
+ */
1303
+ drawTile(ctx, img) {
1304
+ const pattern = ctx.createPattern(img, "repeat");
1305
+ if (pattern) {
1306
+ ctx.fillStyle = pattern;
1307
+ ctx.fillRect(0, 0, this.width, this.height);
1308
+ }
1309
+ }
1310
+ /**
1311
+ * Draw image to cover entire area (may crop, maintains aspect ratio).
1312
+ */
1313
+ drawCover(ctx, img) {
1314
+ const imgRatio = img.width / img.height;
1315
+ const canvasRatio = this.width / this.height;
1316
+ let drawWidth, drawHeight;
1317
+ let offsetX = 0, offsetY = 0;
1318
+ if (imgRatio > canvasRatio) {
1319
+ drawHeight = this.height;
1320
+ drawWidth = this.height * imgRatio;
1321
+ offsetX = (this.width - drawWidth) / 2;
1322
+ } else {
1323
+ drawWidth = this.width;
1324
+ drawHeight = this.width / imgRatio;
1325
+ offsetY = (this.height - drawHeight) / 2;
1326
+ }
1327
+ ctx.drawImage(img, offsetX, offsetY, drawWidth, drawHeight);
1328
+ }
1329
+ /**
1330
+ * Draw image to fit within area (may have gaps, maintains aspect ratio).
1331
+ * Gaps are filled by the color layer underneath.
1332
+ */
1333
+ drawContain(ctx, img) {
1334
+ const imgRatio = img.width / img.height;
1335
+ const canvasRatio = this.width / this.height;
1336
+ let drawWidth, drawHeight;
1337
+ let offsetX = 0, offsetY = 0;
1338
+ if (imgRatio > canvasRatio) {
1339
+ drawWidth = this.width;
1340
+ drawHeight = this.width / imgRatio;
1341
+ offsetY = (this.height - drawHeight) / 2;
1342
+ } else {
1343
+ drawHeight = this.height;
1344
+ drawWidth = this.height * imgRatio;
1345
+ offsetX = (this.width - drawWidth) / 2;
1346
+ }
1347
+ ctx.drawImage(img, offsetX, offsetY, drawWidth, drawHeight);
1348
+ }
1349
+ /**
1350
+ * Render the transparency/frosted glass layer.
1351
+ * opacity = 0 means fully transparent (nothing drawn)
1352
+ * opacity = 1 means fully opaque overlay
1353
+ * opacity = 0.3 means light frosted glass
1354
+ */
1355
+ renderTransparencyLayer(config) {
1356
+ if (config.opacity <= 0) return;
1357
+ this.ctx.save();
1358
+ this.ctx.globalAlpha = config.opacity;
1359
+ if (config.tintColor) {
1360
+ this.ctx.fillStyle = config.tintColor;
1361
+ } else {
1362
+ this.ctx.fillStyle = "#ffffff";
1363
+ }
1364
+ this.ctx.fillRect(0, 0, this.width, this.height);
1365
+ this.ctx.restore();
1366
+ }
1367
+ /**
1368
+ * Clear the background image cache.
1369
+ */
1370
+ static clearCache() {
1371
+ imageCache.clear();
1372
+ logger.debug(LOG_PREFIX4, "Image cache cleared");
1373
+ }
1374
+ };
1375
+
1113
1376
  // src/OverlayScene.ts
1114
1377
  var OverlayScene = class {
1115
1378
  constructor(canvas, config) {
@@ -1167,6 +1430,22 @@ var OverlayScene = class {
1167
1430
  }
1168
1431
  }
1169
1432
  };
1433
+ /**
1434
+ * Handler for Matter.js beforeRender event.
1435
+ * Draws base background layers (color + image) before physics objects.
1436
+ */
1437
+ this.handleBeforeRender = () => {
1438
+ if (this.backgroundManager.hasCustomLayers()) {
1439
+ this.backgroundManager.renderBaseLayers();
1440
+ }
1441
+ };
1442
+ /**
1443
+ * Handler for Matter.js afterRender event.
1444
+ * Draws transparency/frosted glass layer after physics objects.
1445
+ */
1446
+ this.handleAfterRender = () => {
1447
+ this.backgroundManager.renderOverlay();
1448
+ };
1170
1449
  // ==================== PRIVATE ====================
1171
1450
  this.loop = () => {
1172
1451
  this.effectManager.update();
@@ -1200,7 +1479,6 @@ var OverlayScene = class {
1200
1479
  gravity: 1,
1201
1480
  wrapHorizontal: true,
1202
1481
  debug: false,
1203
- background: "transparent",
1204
1482
  ...config
1205
1483
  };
1206
1484
  this.engine = createEngine(this.config.gravity);
@@ -1235,6 +1513,23 @@ var OverlayScene = class {
1235
1513
  (cfg) => this.spawnObjectAsync(cfg),
1236
1514
  (id) => this.objects.get(id)?.body ?? null
1237
1515
  );
1516
+ const width = this.config.bounds.right - this.config.bounds.left;
1517
+ const height = this.config.bounds.bottom - this.config.bounds.top;
1518
+ this.backgroundManager = new BackgroundManager({
1519
+ canvas,
1520
+ width,
1521
+ height
1522
+ });
1523
+ this.backgroundManager.setConfig(this.config.background).catch((err) => {
1524
+ logger.error("OverlayScene", "Failed to initialize background", {
1525
+ error: String(err)
1526
+ });
1527
+ });
1528
+ if (this.backgroundManager.hasCustomLayers()) {
1529
+ this.render.options.background = "transparent";
1530
+ }
1531
+ Matter5.Events.on(this.render, "beforeRender", this.handleBeforeRender);
1532
+ Matter5.Events.on(this.render, "afterRender", this.handleAfterRender);
1238
1533
  }
1239
1534
  static createContainer(parent, options = {}) {
1240
1535
  const canvas = document.createElement("canvas");
@@ -1637,6 +1932,8 @@ var OverlayScene = class {
1637
1932
  Matter5.Events.off(this.mouseConstraint, "startdrag", this.handleStartDrag);
1638
1933
  }
1639
1934
  this.canvas.removeEventListener("click", this.handleCanvasClick);
1935
+ Matter5.Events.off(this.render, "beforeRender", this.handleBeforeRender);
1936
+ Matter5.Events.off(this.render, "afterRender", this.handleAfterRender);
1640
1937
  Matter5.Engine.clear(this.engine);
1641
1938
  this.objects.clear();
1642
1939
  this.obstaclePressure.clear();
@@ -1655,6 +1952,18 @@ var OverlayScene = class {
1655
1952
  }
1656
1953
  }
1657
1954
  }
1955
+ /**
1956
+ * Update the background configuration at runtime.
1957
+ */
1958
+ async setBackground(config) {
1959
+ await this.backgroundManager.setConfig(config);
1960
+ if (this.backgroundManager.hasCustomLayers()) {
1961
+ this.render.options.background = "transparent";
1962
+ } else {
1963
+ const bgConfig = this.backgroundManager.getConfig();
1964
+ this.render.options.background = bgConfig?.color ?? "transparent";
1965
+ }
1966
+ }
1658
1967
  resize(width, height) {
1659
1968
  this.canvas.width = width;
1660
1969
  this.canvas.height = height;
@@ -1672,6 +1981,7 @@ var OverlayScene = class {
1672
1981
  this.render.canvas.width = width;
1673
1982
  this.render.canvas.height = height;
1674
1983
  this.effectManager.setBounds(this.config.bounds);
1984
+ this.backgroundManager.resize(width, height);
1675
1985
  }
1676
1986
  // ==================== OBJECT METHODS ====================
1677
1987
  /**
@@ -2270,7 +2580,8 @@ var OverlayScene = class {
2270
2580
  for (let i = 0; i < chars.length; i++) {
2271
2581
  const char = chars[i];
2272
2582
  if (char === " ") {
2273
- currentX += 20;
2583
+ const spaceWidth = config.letterSpacing ?? letterSize;
2584
+ currentX += spaceWidth;
2274
2585
  globalCharIndex++;
2275
2586
  if (inWord) {
2276
2587
  currentWordIndex++;
@@ -2793,6 +3104,7 @@ var OverlayScene = class {
2793
3104
  }
2794
3105
  };
2795
3106
  export {
3107
+ BackgroundManager,
2796
3108
  OverlayScene,
2797
3109
  clearFontCache,
2798
3110
  getGlyphData,