@flare-engine/render 0.3.1 → 0.4.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.mjs CHANGED
@@ -788,6 +788,8 @@ function flashIntensity(effect) {
788
788
  const G = globalThis;
789
789
  const BLEND_SRC_OVER = 3;
790
790
  const BLEND_SRC_IN = 5;
791
+ /** Skia BlendMode.Plus — additive: dst = clamp(src + dst). */
792
+ const BLEND_PLUS = 12;
791
793
  const OUTLINE_OFFSETS = [
792
794
  [1, 0],
793
795
  [-1, 0],
@@ -1253,12 +1255,17 @@ function createFrameRenderer(Skia) {
1253
1255
  if (current === null) return;
1254
1256
  const n = commands.length;
1255
1257
  if (n === 0) return;
1256
- const effect = commands[0]?.effect;
1257
- const alpha = commands[0]?.alpha ?? 1;
1258
- currentSpriteW = commands[0]?.dstW ?? 0;
1259
- currentSpriteH = commands[0]?.dstH ?? 0;
1258
+ const cmd0 = commands[0];
1259
+ const effect = cmd0.effect;
1260
+ const alpha = cmd0.alpha;
1261
+ const blendAdditive = cmd0.blend === "additive";
1262
+ const flipX = cmd0.flipX;
1263
+ const flipY = cmd0.flipY;
1264
+ currentSpriteW = cmd0.dstW;
1265
+ currentSpriteH = cmd0.dstH;
1260
1266
  if (effect?.type === "roofs-city" || effect?.type === "forest-canopy" || effect?.type === "river-wave" || effect?.type === "moon-crater" || effect?.type === "cave-interior" || effect?.type === "mountain-hills") {
1261
1267
  const paint = resolveEffectPaint(effect);
1268
+ if (blendAdditive) paint.setBlendMode?.(BLEND_PLUS);
1262
1269
  paint.setAlphaf(alpha);
1263
1270
  ensureZoneBgPoolCapacity(n);
1264
1271
  for (let i = 0; i < n; i++) {
@@ -1267,13 +1274,15 @@ function createFrameRenderer(Skia) {
1267
1274
  current.drawRect(zoneBgRects[i], paint);
1268
1275
  }
1269
1276
  paint.setAlphaf(1);
1277
+ if (blendAdditive) paint.setBlendMode?.(BLEND_SRC_OVER);
1270
1278
  return;
1271
1279
  }
1272
- if (commands[0]?.stretch) {
1280
+ if (cmd0.stretch || flipX || flipY) {
1273
1281
  ensureStretchPoolCapacity(n);
1274
1282
  if (stretchPaint === null) stretchPaint = Skia.Paint();
1275
1283
  const sp = stretchPaint;
1276
1284
  const sPaint = effect === void 0 || effect.type === "outline" ? sp : resolveEffectPaint(effect);
1285
+ if (blendAdditive) sPaint.setBlendMode?.(BLEND_PLUS);
1277
1286
  sPaint.setAlphaf(alpha);
1278
1287
  for (let i = 0; i < n; i++) {
1279
1288
  const cmd = commands[i];
@@ -1281,24 +1290,44 @@ function createFrameRenderer(Skia) {
1281
1290
  const dstR = stretchDstRects[i];
1282
1291
  srcR.setXYWH(cmd.srcX, cmd.srcY, cmd.srcW, cmd.srcH);
1283
1292
  dstR.setXYWH(cmd.dstX, cmd.dstY, cmd.dstW, cmd.dstH);
1284
- current.drawImageRect(image, srcR, dstR, sPaint);
1293
+ if (cmd.flipX || cmd.flipY) {
1294
+ const cx = cmd.dstX + cmd.dstW * .5;
1295
+ const cy = cmd.dstY + cmd.dstH * .5;
1296
+ current.save();
1297
+ current.translate(cx, cy);
1298
+ current.scale(cmd.flipX ? -1 : 1, cmd.flipY ? -1 : 1);
1299
+ current.translate(-cx, -cy);
1300
+ current.drawImageRect(image, srcR, dstR, sPaint);
1301
+ current.restore();
1302
+ } else current.drawImageRect(image, srcR, dstR, sPaint);
1285
1303
  }
1286
1304
  sPaint.setAlphaf(1);
1305
+ if (blendAdditive) sPaint.setBlendMode?.(BLEND_SRC_OVER);
1287
1306
  return;
1288
1307
  }
1289
1308
  ensurePoolCapacity(n);
1290
1309
  fillSrcRects(commands, n);
1291
1310
  if (effect?.type === "outline") {
1311
+ if (blendAdditive) {
1312
+ outlinePaint?.setBlendMode?.(BLEND_PLUS);
1313
+ defaultPaint.setBlendMode?.(BLEND_PLUS);
1314
+ }
1292
1315
  drawOutlined(current, image, commands, n, effect, alpha);
1316
+ if (blendAdditive) {
1317
+ outlinePaint?.setBlendMode?.(BLEND_SRC_OVER);
1318
+ defaultPaint.setBlendMode?.(BLEND_SRC_OVER);
1319
+ }
1293
1320
  return;
1294
1321
  }
1295
1322
  fillXforms(commands, n, 0, 0);
1296
1323
  const srcs = getCachedSrcSlice(n);
1297
1324
  const dsts = getCachedXformSlice(n);
1298
1325
  const paint = effect === void 0 ? defaultPaint : resolveEffectPaint(effect);
1326
+ if (blendAdditive) paint.setBlendMode?.(BLEND_PLUS);
1299
1327
  paint.setAlphaf(alpha);
1300
1328
  current.drawAtlas(image, srcs, dsts, paint);
1301
1329
  paint.setAlphaf(1);
1330
+ if (blendAdditive) paint.setBlendMode?.(BLEND_SRC_OVER);
1302
1331
  },
1303
1332
  setPostFxBinding(runtime, chain) {
1304
1333
  _boundRuntime = runtime;
@@ -1426,6 +1455,70 @@ function fillCustomUniforms(effect, shader, out) {
1426
1455
  }
1427
1456
  }
1428
1457
  //#endregion
1458
+ //#region src/nine-slice.ts
1459
+ /**
1460
+ * Draw a sprite using a 9-slice (scale-9) layout.
1461
+ *
1462
+ * The source frame is divided into a 3×3 grid by `inset` pixels from each
1463
+ * edge. Corners draw at natural size; edges stretch along one axis; the
1464
+ * centre stretches in both axes. This preserves crisp borders at any
1465
+ * destination size — the canonical use case is UI panels and buttons.
1466
+ *
1467
+ * All 9 slices use the `drawImageRect` (`stretch=true`) path, so they are
1468
+ * NOT batched with regular Atlas sprites; they form their own consecutive run
1469
+ * in the batch. The function calls `batch.drawRaw()` for each non-degenerate
1470
+ * slice (a slice with zero source or destination area is skipped silently).
1471
+ *
1472
+ * **Coarse culling:** if `batch.cullBounds` is set, the entire nine-slice is
1473
+ * skipped when the destination rectangle `[x, y, x+w, y+h]` does not
1474
+ * intersect the bounds. Individual slice culling is not performed — the
1475
+ * caller should set tight bounds if per-slice savings matter.
1476
+ *
1477
+ * @param batch Target SpriteBatch — must outlive this call.
1478
+ * @param image Source texture.
1479
+ * @param frame Sub-region of the texture to use as the nine-slice source.
1480
+ * @param x Destination left edge (screen / world coords).
1481
+ * @param y Destination top edge.
1482
+ * @param w Destination width.
1483
+ * @param h Destination height.
1484
+ * @param inset Border size in source-texture pixels, applied uniformly to
1485
+ * all four edges. Must be ≥ 0; clamped to half the frame
1486
+ * dimension so corners never overlap.
1487
+ * @param effect Optional sprite effect applied to every slice identically.
1488
+ * @param alpha Per-instance opacity multiplier in [0, 1]. Default 1.
1489
+ */
1490
+ function drawNineSlice(batch, image, frame, x, y, w, h, inset, effect, alpha = 1) {
1491
+ const cb = batch.cullBounds;
1492
+ if (cb !== null) {
1493
+ if (x + w <= cb.minX || x >= cb.maxX || y + h <= cb.minY || y >= cb.maxY) return;
1494
+ }
1495
+ const si = Math.min(inset, frame.w * .5, frame.h * .5);
1496
+ const di = Math.min(si, w * .5, h * .5);
1497
+ const fx = frame.x;
1498
+ const fy = frame.y;
1499
+ const fw = frame.w;
1500
+ const fh = frame.h;
1501
+ const srcMidW = fw - 2 * si;
1502
+ const srcMidH = fh - 2 * si;
1503
+ const dstMidW = w - 2 * di;
1504
+ const dstMidH = h - 2 * di;
1505
+ const a = alpha;
1506
+ const e = effect;
1507
+ _slice(batch, image, fx, fy, si, si, x, y, di, di, e, a);
1508
+ _slice(batch, image, fx + si, fy, srcMidW, si, x + di, y, dstMidW, di, e, a);
1509
+ _slice(batch, image, fx + fw - si, fy, si, si, x + w - di, y, di, di, e, a);
1510
+ _slice(batch, image, fx, fy + si, si, srcMidH, x, y + di, di, dstMidH, e, a);
1511
+ _slice(batch, image, fx + si, fy + si, srcMidW, srcMidH, x + di, y + di, dstMidW, dstMidH, e, a);
1512
+ _slice(batch, image, fx + fw - si, fy + si, si, srcMidH, x + w - di, y + di, di, dstMidH, e, a);
1513
+ _slice(batch, image, fx, fy + fh - si, si, si, x, y + h - di, di, di, e, a);
1514
+ _slice(batch, image, fx + si, fy + fh - si, srcMidW, si, x + di, y + h - di, dstMidW, di, e, a);
1515
+ _slice(batch, image, fx + fw - si, fy + fh - si, si, si, x + w - di, y + h - di, di, di, e, a);
1516
+ }
1517
+ function _slice(batch, image, sx, sy, sw, sh, dx, dy, dw, dh, effect, alpha) {
1518
+ if (sw <= 0 || sh <= 0 || dw <= 0 || dh <= 0) return;
1519
+ batch.drawRaw(image, sx, sy, sw, sh, dx, dy, dw, dh, effect, 0, 0, alpha, 0, true);
1520
+ }
1521
+ //#endregion
1429
1522
  //#region src/render-system.ts
1430
1523
  /**
1431
1524
  * Create a render system function.
@@ -1449,7 +1542,9 @@ function createRenderSystem(world, query, positionDef, spriteDef, batch) {
1449
1542
  const entries = [];
1450
1543
  return () => {
1451
1544
  let i = 0;
1452
- for (const entityId of query) {
1545
+ const ids = query.entities;
1546
+ for (let n = 0; n < ids.length; n++) {
1547
+ const entityId = ids[n];
1453
1548
  const sprite = world.get(entityId, spriteDef);
1454
1549
  if (!sprite) continue;
1455
1550
  if (i >= entries.length) entries.push({
@@ -1470,7 +1565,7 @@ function createRenderSystem(world, query, positionDef, spriteDef, batch) {
1470
1565
  const sprite = world.get(entry.entityId, spriteDef);
1471
1566
  if (!pos || !sprite) continue;
1472
1567
  const anchor = sprite.anchor;
1473
- batch.draw(sprite.image, sprite.frame, pos.x, pos.y, sprite.width, sprite.height, sprite.effect, anchor ? anchor.x : 0, anchor ? anchor.y : 0, sprite.alpha ?? 1, sprite.rotation ?? 0, sprite.stretch ?? false);
1568
+ batch.draw(sprite.image, sprite.frame, pos.x, pos.y, sprite.width, sprite.height, sprite.effect, anchor ? anchor.x : 0, anchor ? anchor.y : 0, sprite.alpha ?? 1, sprite.rotation ?? 0, sprite.stretch ?? false, sprite.blend ?? "normal", sprite.flipX ?? false, sprite.flipY ?? false);
1474
1569
  }
1475
1570
  };
1476
1571
  }
@@ -1577,6 +1672,7 @@ var SpriteBatch = class {
1577
1672
  _pool = [];
1578
1673
  _batches = [];
1579
1674
  _allocCount = 0;
1675
+ _cull = null;
1580
1676
  /** Number of queued draw commands. */
1581
1677
  get count() {
1582
1678
  return this._commands.length;
@@ -1586,6 +1682,34 @@ var SpriteBatch = class {
1586
1682
  get allocCount() {
1587
1683
  return this._allocCount;
1588
1684
  }
1685
+ /**
1686
+ * Current cull bounds, or `null` when culling is disabled.
1687
+ * Read by `drawNineSlice` for coarse AABB precheck.
1688
+ * Set via `setCullBounds()` / `clearCullBounds()`.
1689
+ */
1690
+ get cullBounds() {
1691
+ return this._cull;
1692
+ }
1693
+ /** Set the viewport cull bounds for this batch.
1694
+ * Zero-alloc in steady state — the internal object is allocated once and then mutated. */
1695
+ setCullBounds(minX, minY, maxX, maxY) {
1696
+ if (this._cull === null) this._cull = {
1697
+ minX,
1698
+ minY,
1699
+ maxX,
1700
+ maxY
1701
+ };
1702
+ else {
1703
+ this._cull.minX = minX;
1704
+ this._cull.minY = minY;
1705
+ this._cull.maxX = maxX;
1706
+ this._cull.maxY = maxY;
1707
+ }
1708
+ }
1709
+ /** Disable viewport culling (e.g. during UI rendering). */
1710
+ clearCullBounds() {
1711
+ this._cull = null;
1712
+ }
1589
1713
  /** Queue a sprite draw from a frame.
1590
1714
  *
1591
1715
  * `anchorX` / `anchorY` define the pivot point of the sprite as a
@@ -1594,7 +1718,7 @@ var SpriteBatch = class {
1594
1718
  * then refer to the anchor point instead of the top-left corner.
1595
1719
  *
1596
1720
  * `alpha` is a per-instance opacity multiplier in [0, 1]. Default 1. */
1597
- draw(image, frame, dstX, dstY, dstW, dstH, effect, anchorX = 0, anchorY = 0, alpha = 1, rotation = 0, stretch = false) {
1721
+ draw(image, frame, dstX, dstY, dstW, dstH, effect, anchorX = 0, anchorY = 0, alpha = 1, rotation = 0, stretch = false, blend = "normal", flipX = false, flipY = false) {
1598
1722
  const cmd = this._acquire();
1599
1723
  const w = dstW ?? frame.w;
1600
1724
  const h = dstH ?? frame.h;
@@ -1613,13 +1737,16 @@ var SpriteBatch = class {
1613
1737
  cmd.anchorX = anchorX;
1614
1738
  cmd.anchorY = anchorY;
1615
1739
  cmd.stretch = stretch;
1740
+ cmd.blend = blend;
1741
+ cmd.flipX = flipX;
1742
+ cmd.flipY = flipY;
1616
1743
  this._commands.push(cmd);
1617
1744
  }
1618
1745
  /** Queue a raw sprite draw with explicit src/dst rects. `dstX` / `dstY`
1619
1746
  * refer to the anchor point — offset by `anchorX * dstW` / `anchorY * dstH`.
1620
1747
  *
1621
1748
  * `alpha` is a per-instance opacity multiplier in [0, 1]. Default 1. */
1622
- drawRaw(image, srcX, srcY, srcW, srcH, dstX, dstY, dstW, dstH, effect, anchorX = 0, anchorY = 0, alpha = 1, rotation = 0, stretch = false) {
1749
+ drawRaw(image, srcX, srcY, srcW, srcH, dstX, dstY, dstW, dstH, effect, anchorX = 0, anchorY = 0, alpha = 1, rotation = 0, stretch = false, blend = "normal", flipX = false, flipY = false) {
1623
1750
  const cmd = this._acquire();
1624
1751
  cmd.image = image;
1625
1752
  cmd.srcX = srcX;
@@ -1636,6 +1763,9 @@ var SpriteBatch = class {
1636
1763
  cmd.anchorX = anchorX;
1637
1764
  cmd.anchorY = anchorY;
1638
1765
  cmd.stretch = stretch;
1766
+ cmd.blend = blend;
1767
+ cmd.flipX = flipX;
1768
+ cmd.flipY = flipY;
1639
1769
  this._commands.push(cmd);
1640
1770
  }
1641
1771
  /**
@@ -1659,12 +1789,15 @@ var SpriteBatch = class {
1659
1789
  effect: void 0,
1660
1790
  alpha: 1,
1661
1791
  stretch: false,
1792
+ blend: "normal",
1793
+ flipX: false,
1794
+ flipY: false,
1662
1795
  commands: []
1663
1796
  };
1664
1797
  this._batches[0] = run;
1665
1798
  }
1666
1799
  for (const cmd of this._commands) {
1667
- if (run.commands.length > 0 && (cmd.image !== run.image || cmd.effect !== run.effect || cmd.alpha !== run.alpha || cmd.stretch !== run.stretch)) {
1800
+ if (run.commands.length > 0 && (cmd.image !== run.image || cmd.effect !== run.effect || cmd.alpha !== run.alpha || cmd.stretch !== run.stretch || cmd.blend !== run.blend || cmd.flipX !== run.flipX || cmd.flipY !== run.flipY)) {
1668
1801
  drawAtlas(run.image, run.commands);
1669
1802
  if (renderer) {
1670
1803
  renderer.recordDrawCall();
@@ -1676,6 +1809,9 @@ var SpriteBatch = class {
1676
1809
  run.effect = cmd.effect;
1677
1810
  run.alpha = cmd.alpha;
1678
1811
  run.stretch = cmd.stretch;
1812
+ run.blend = cmd.blend;
1813
+ run.flipX = cmd.flipX;
1814
+ run.flipY = cmd.flipY;
1679
1815
  run.commands.push(cmd);
1680
1816
  }
1681
1817
  if (run.commands.length > 0) {
@@ -1712,7 +1848,10 @@ var SpriteBatch = class {
1712
1848
  rotation: 0,
1713
1849
  anchorX: 0,
1714
1850
  anchorY: 0,
1715
- stretch: false
1851
+ stretch: false,
1852
+ blend: "normal",
1853
+ flipX: false,
1854
+ flipY: false
1716
1855
  };
1717
1856
  }
1718
1857
  };
@@ -1779,6 +1918,6 @@ function drawText(batch, font, image, text, x, y, scale = 1) {
1779
1918
  }
1780
1919
  }
1781
1920
  //#endregion
1782
- export { CAVE_INTERIOR_SKSL, Camera2D, FIRE_SKSL, FOG_SKSL, FOREST_CANOPY_SKSL, MOON_CRATER_SKSL, MOUNTAIN_HILLS_SKSL, RIVER_WAVE_SKSL, ROOFS_CITY_SKSL, Renderer, Sprite, SpriteBatch, WATER_SKSL, caveInterior, createFrameRenderer, createRenderSystem, custom, drawOrderCompare, drawText, dropShadow, fire, flash, flashIntensity, fog, forestCanopy, getKerning, getXformCounters, grayscale, measureText, moonCrater, mountainHills, outline, parseBMFontJSON, resetXformCounters, riverWave, roofsCity, sortByDrawOrder, tint, updateEffect, water };
1921
+ export { CAVE_INTERIOR_SKSL, Camera2D, FIRE_SKSL, FOG_SKSL, FOREST_CANOPY_SKSL, MOON_CRATER_SKSL, MOUNTAIN_HILLS_SKSL, RIVER_WAVE_SKSL, ROOFS_CITY_SKSL, Renderer, Sprite, SpriteBatch, WATER_SKSL, caveInterior, createFrameRenderer, createRenderSystem, custom, drawNineSlice, drawOrderCompare, drawText, dropShadow, fire, flash, flashIntensity, fog, forestCanopy, getKerning, getXformCounters, grayscale, measureText, moonCrater, mountainHills, outline, parseBMFontJSON, resetXformCounters, riverWave, roofsCity, sortByDrawOrder, tint, updateEffect, water };
1783
1922
 
1784
1923
  //# sourceMappingURL=index.mjs.map