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