@almadar/ui 5.139.0 → 5.141.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/avl/index.js CHANGED
@@ -12553,9 +12553,26 @@ var init_imageCache = __esm({
12553
12553
  });
12554
12554
 
12555
12555
  // lib/webPainter2d.ts
12556
+ function makeNoiseTile(alpha, color) {
12557
+ const tile = document.createElement("canvas");
12558
+ tile.width = NOISE_CELLS;
12559
+ tile.height = NOISE_CELLS;
12560
+ const t = tile.getContext("2d");
12561
+ if (t) {
12562
+ t.fillStyle = color;
12563
+ for (let y = 0; y < NOISE_CELLS; y++) {
12564
+ for (let x = 0; x < NOISE_CELLS; x++) {
12565
+ t.globalAlpha = noiseHash(x, y) * alpha;
12566
+ t.fillRect(x, y, 1, 1);
12567
+ }
12568
+ }
12569
+ }
12570
+ return tile;
12571
+ }
12556
12572
  function createWebPainter(ctx, onAssetLoad) {
12557
12573
  let vw = 0;
12558
12574
  let vh = 0;
12575
+ const patternCache = /* @__PURE__ */ new Map();
12559
12576
  const tracePoly = (points, closed) => {
12560
12577
  if (points.length === 0) return;
12561
12578
  ctx.beginPath();
@@ -12563,6 +12580,31 @@ function createWebPainter(ctx, onAssetLoad) {
12563
12580
  for (let i = 1; i < points.length; i++) ctx.lineTo(points[i].x, points[i].y);
12564
12581
  if (closed) ctx.closePath();
12565
12582
  };
12583
+ const toCanvasPattern = (style) => {
12584
+ const key = JSON.stringify(style);
12585
+ let pattern = patternCache.get(key);
12586
+ if (pattern === void 0) {
12587
+ if (style.kind === "noise") {
12588
+ pattern = ctx.createPattern(makeNoiseTile(style.alpha ?? 0.12, style.color ?? "#000000"), "repeat");
12589
+ } else {
12590
+ const img = getOrLoadImage(style.url, onAssetLoad);
12591
+ if (!img) return "rgba(0,0,0,0)";
12592
+ pattern = ctx.createPattern(img, "repeat");
12593
+ }
12594
+ if (pattern && style.scale !== void 0 && style.scale !== 1) {
12595
+ pattern.setTransform(new DOMMatrix().scale(style.scale));
12596
+ }
12597
+ patternCache.set(key, pattern);
12598
+ }
12599
+ return pattern ?? "rgba(0,0,0,0)";
12600
+ };
12601
+ const toCanvasStyle = (style) => {
12602
+ if (typeof style === "string") return style;
12603
+ if (style.kind === "noise" || style.kind === "image") return toCanvasPattern(style);
12604
+ const g = style.kind === "linear" ? ctx.createLinearGradient(style.x1, style.y1, style.x2, style.y2) : style.kind === "conic" ? ctx.createConicGradient(style.angle, style.cx, style.cy) : ctx.createRadialGradient(style.cx, style.cy, 0, style.cx, style.cy, style.r);
12605
+ for (const stop of style.stops) g.addColorStop(stop.offset, stop.color);
12606
+ return g;
12607
+ };
12566
12608
  return {
12567
12609
  setViewport(width, height, dpr) {
12568
12610
  vw = width;
@@ -12594,6 +12636,19 @@ function createWebPainter(ctx, onAssetLoad) {
12594
12636
  ctx.shadowColor = shadow ? shadow.color : "transparent";
12595
12637
  ctx.shadowBlur = shadow ? shadow.blur : 0;
12596
12638
  },
12639
+ setBlend(mode) {
12640
+ ctx.globalCompositeOperation = mode ?? "source-over";
12641
+ },
12642
+ setLineDash(pattern, offset = 0) {
12643
+ ctx.setLineDash(pattern ? [...pattern] : []);
12644
+ ctx.lineDashOffset = offset;
12645
+ },
12646
+ setBlur(px) {
12647
+ ctx.filter = px && px > 0 ? `blur(${px}px)` : "none";
12648
+ },
12649
+ clipPath(d) {
12650
+ ctx.clip(new Path2D(d));
12651
+ },
12597
12652
  resolveTexture(url) {
12598
12653
  const img = getOrLoadImage(url, onAssetLoad);
12599
12654
  if (!img) return null;
@@ -12616,47 +12671,47 @@ function createWebPainter(ctx, onAssetLoad) {
12616
12671
  ctx.drawImage(img, dest.x, dest.y, dw, dh);
12617
12672
  }
12618
12673
  },
12619
- fillRect(x, y, w, h, color) {
12620
- ctx.fillStyle = color;
12674
+ fillRect(x, y, w, h, style) {
12675
+ ctx.fillStyle = toCanvasStyle(style);
12621
12676
  ctx.fillRect(x, y, w, h);
12622
12677
  },
12623
- strokeRect(x, y, w, h, color, lineWidth = 1) {
12624
- ctx.strokeStyle = color;
12678
+ strokeRect(x, y, w, h, style, lineWidth = 1) {
12679
+ ctx.strokeStyle = toCanvasStyle(style);
12625
12680
  ctx.lineWidth = lineWidth;
12626
12681
  ctx.strokeRect(x, y, w, h);
12627
12682
  },
12628
- fillPoly(points, color) {
12683
+ fillPoly(points, style) {
12629
12684
  if (points.length === 0) return;
12630
12685
  tracePoly(points, true);
12631
- ctx.fillStyle = color;
12686
+ ctx.fillStyle = toCanvasStyle(style);
12632
12687
  ctx.fill();
12633
12688
  },
12634
- strokePoly(points, color, lineWidth = 1, closed = false) {
12689
+ strokePoly(points, style, lineWidth = 1, closed = false) {
12635
12690
  if (points.length === 0) return;
12636
12691
  tracePoly(points, closed);
12637
- ctx.strokeStyle = color;
12692
+ ctx.strokeStyle = toCanvasStyle(style);
12638
12693
  ctx.lineWidth = lineWidth;
12639
12694
  ctx.stroke();
12640
12695
  },
12641
- fillEllipse(cx, cy, rx, ry, color) {
12696
+ fillEllipse(cx, cy, rx, ry, style) {
12642
12697
  ctx.beginPath();
12643
12698
  ctx.ellipse(cx, cy, rx, ry, 0, 0, Math.PI * 2);
12644
- ctx.fillStyle = color;
12699
+ ctx.fillStyle = toCanvasStyle(style);
12645
12700
  ctx.fill();
12646
12701
  },
12647
- strokeEllipse(cx, cy, rx, ry, color, lineWidth = 1) {
12702
+ strokeEllipse(cx, cy, rx, ry, style, lineWidth = 1) {
12648
12703
  ctx.beginPath();
12649
12704
  ctx.ellipse(cx, cy, rx, ry, 0, 0, Math.PI * 2);
12650
- ctx.strokeStyle = color;
12705
+ ctx.strokeStyle = toCanvasStyle(style);
12651
12706
  ctx.lineWidth = lineWidth;
12652
12707
  ctx.stroke();
12653
12708
  },
12654
- fillPath(d, color) {
12655
- ctx.fillStyle = color;
12709
+ fillPath(d, style) {
12710
+ ctx.fillStyle = toCanvasStyle(style);
12656
12711
  ctx.fill(new Path2D(d));
12657
12712
  },
12658
- strokePath(d, color, lineWidth = 1) {
12659
- ctx.strokeStyle = color;
12713
+ strokePath(d, style, lineWidth = 1) {
12714
+ ctx.strokeStyle = toCanvasStyle(style);
12660
12715
  ctx.lineWidth = lineWidth;
12661
12716
  ctx.stroke(new Path2D(d));
12662
12717
  },
@@ -12669,13 +12724,18 @@ function createWebPainter(ctx, onAssetLoad) {
12669
12724
  }
12670
12725
  };
12671
12726
  }
12672
- var handleByImage, imageByHandle;
12727
+ var handleByImage, imageByHandle, noiseHash, NOISE_CELLS;
12673
12728
  var init_webPainter2d = __esm({
12674
12729
  "lib/webPainter2d.ts"() {
12675
12730
  "use client";
12676
12731
  init_imageCache();
12677
12732
  handleByImage = /* @__PURE__ */ new WeakMap();
12678
12733
  imageByHandle = /* @__PURE__ */ new WeakMap();
12734
+ noiseHash = (x, y) => {
12735
+ const s = Math.sin(x * 12.9898 + y * 78.233) * 43758.5453;
12736
+ return s - Math.floor(s);
12737
+ };
12738
+ NOISE_CELLS = 32;
12679
12739
  }
12680
12740
  });
12681
12741
 
@@ -12831,6 +12891,101 @@ var init_registry = __esm({
12831
12891
  DrawableRegistryContext = createContext(null);
12832
12892
  }
12833
12893
  });
12894
+ function isAnimatedShape(node) {
12895
+ return Boolean(node.animation && node.animation.durationMs > 0 && node.animation.keyframes.length > 0);
12896
+ }
12897
+ function applyShapeAnimation(node, timeMs) {
12898
+ const anim = node.animation;
12899
+ if (!anim || !(anim.durationMs > 0) || anim.keyframes.length === 0) return node;
12900
+ const frames = [...anim.keyframes].sort((a, b) => a.at - b.at);
12901
+ const cycle = timeMs / anim.durationMs;
12902
+ const t = anim.loop === false ? Math.min(cycle, 1) : cycle - Math.floor(cycle);
12903
+ const out = { ...node };
12904
+ const trackValue = (key) => {
12905
+ const defined = frames.filter((f3) => f3[key] !== void 0);
12906
+ if (defined.length === 0) return void 0;
12907
+ let prev;
12908
+ let next;
12909
+ for (const f3 of defined) {
12910
+ if (f3.at <= t) prev = f3;
12911
+ else if (!next) next = f3;
12912
+ }
12913
+ if (!prev) return defined[0][key];
12914
+ if (!next) return prev[key];
12915
+ const span = next.at - prev.at;
12916
+ const k = span > 0 ? (t - prev.at) / span : 1;
12917
+ const a = prev[key];
12918
+ const b = next[key];
12919
+ if (typeof a === "number" && typeof b === "number") return lerp(a, b, k);
12920
+ return a;
12921
+ };
12922
+ for (const key of NUMERIC_TRACKS) {
12923
+ const v = trackValue(key);
12924
+ if (v !== void 0) out[key] = v;
12925
+ }
12926
+ const fill = trackValue("fill");
12927
+ if (fill !== void 0) out.fill = fill;
12928
+ const stroke = trackValue("stroke");
12929
+ if (stroke !== void 0) out.stroke = stroke;
12930
+ const shadowFrames = frames.filter((f3) => f3.shadow !== void 0);
12931
+ if (shadowFrames.length > 0) {
12932
+ const sh = trackValue("shadow");
12933
+ if (sh !== void 0) {
12934
+ let prevSh;
12935
+ let nextSh;
12936
+ for (const f3 of shadowFrames) {
12937
+ if (f3.at <= t) prevSh = f3;
12938
+ else if (!nextSh) nextSh = f3;
12939
+ }
12940
+ if (prevSh?.shadow && nextSh?.shadow) {
12941
+ const span = nextSh.at - prevSh.at;
12942
+ const k = span > 0 ? (t - prevSh.at) / span : 1;
12943
+ out.shadow = { color: prevSh.shadow.color, blur: lerp(prevSh.shadow.blur, nextSh.shadow.blur, k) };
12944
+ } else {
12945
+ out.shadow = sh;
12946
+ }
12947
+ }
12948
+ }
12949
+ return out;
12950
+ }
12951
+ function gradientStyle(g, ox, oy, scale) {
12952
+ if (g.kind === "linear") {
12953
+ if (!g.from || !g.to) return void 0;
12954
+ return {
12955
+ kind: "linear",
12956
+ x1: ox + g.from.x * scale,
12957
+ y1: oy + g.from.y * scale,
12958
+ x2: ox + g.to.x * scale,
12959
+ y2: oy + g.to.y * scale,
12960
+ stops: g.stops
12961
+ };
12962
+ }
12963
+ if (g.kind === "conic") {
12964
+ if (!g.center) return void 0;
12965
+ return {
12966
+ kind: "conic",
12967
+ cx: ox + g.center.x * scale,
12968
+ cy: oy + g.center.y * scale,
12969
+ angle: g.angle ?? 0,
12970
+ stops: g.stops
12971
+ };
12972
+ }
12973
+ if (!g.center || g.radius === void 0) return void 0;
12974
+ return {
12975
+ kind: "radial",
12976
+ cx: ox + g.center.x * scale,
12977
+ cy: oy + g.center.y * scale,
12978
+ r: g.radius * scale,
12979
+ stops: g.stops
12980
+ };
12981
+ }
12982
+ function patternStyle(p, unit) {
12983
+ if (p.kind === "image") {
12984
+ if (!p.url) return void 0;
12985
+ return { kind: "image", url: p.url, scale: (p.scale ?? 1) / unit };
12986
+ }
12987
+ return { kind: "noise", scale: (p.scale ?? 2) / unit, alpha: p.alpha, color: p.color };
12988
+ }
12834
12989
  function DrawShape(props) {
12835
12990
  const register = useContext(DrawableRegistryContext);
12836
12991
  if (register) {
@@ -12841,25 +12996,66 @@ function DrawShape(props) {
12841
12996
  ...opacity !== void 0 && opacity > 0 ? { opacity } : {}
12842
12997
  };
12843
12998
  register(node);
12844
- return /* @__PURE__ */ jsx("div", { "data-draw-shape-debug": "", style: { position: "absolute", width: 4, height: 4, background: "red", zIndex: 9999 } });
12845
12999
  }
12846
13000
  return null;
12847
13001
  }
12848
- var paintShape;
13002
+ var NUMERIC_TRACKS, lerp, paintShape;
12849
13003
  var init_DrawShape = __esm({
12850
13004
  "components/game/atoms/DrawShape.tsx"() {
12851
13005
  "use client";
12852
13006
  init_contract();
12853
13007
  init_registry();
12854
- paintShape = (painter, node, dctx) => {
13008
+ NUMERIC_TRACKS = [
13009
+ "offsetX",
13010
+ "offsetY",
13011
+ "rotate",
13012
+ "opacity",
13013
+ "radiusX",
13014
+ "radiusY",
13015
+ "width",
13016
+ "height",
13017
+ "strokeWidth",
13018
+ "strokeDashOffset",
13019
+ "blur"
13020
+ ];
13021
+ lerp = (a, b, k) => a + (b - a) * k;
13022
+ paintShape = (painter, rawNode, dctx) => {
13023
+ const node = dctx.time > 0 ? applyShapeAnimation(rawNode, dctx.time) : rawNode;
12855
13024
  if (!isValidScenePos(node.position)) return;
12856
13025
  painter.save();
12857
13026
  if (node.opacity !== void 0 && node.opacity !== 1) painter.setAlpha(node.opacity);
13027
+ const origin = dctx.projector.project(node.position);
13028
+ const tileWidth = dctx.projector.tileWidth;
13029
+ const groupScale = dctx.groupScale ?? 1;
13030
+ const strokePx = (node.strokeWidth ?? 1) / groupScale;
13031
+ if (node.blendMode) painter.setBlend(node.blendMode);
13032
+ if (node.strokeDash && node.strokeDash.length > 0) {
13033
+ painter.setLineDash(
13034
+ node.strokeDash.map((v) => v / groupScale),
13035
+ (node.strokeDashOffset ?? 0) / groupScale
13036
+ );
13037
+ }
13038
+ if (node.blur !== void 0 && node.blur > 0) painter.setBlur(node.blur / groupScale);
13039
+ if (node.rotate) {
13040
+ const pivot = node.pivot ?? { x: 0.5, y: 0.5 };
13041
+ const px = origin.x + pivot.x * tileWidth;
13042
+ const py = origin.y + pivot.y * tileWidth;
13043
+ painter.translate(px, py);
13044
+ painter.rotate(node.rotate);
13045
+ painter.translate(-px, -py);
13046
+ }
13047
+ if (node.shadow) painter.setShadow({ color: node.shadow.color, blur: node.shadow.blur * tileWidth * groupScale });
13048
+ const fill = node.fill === "none" ? void 0 : node.fill;
13049
+ const stroke = node.stroke === "none" ? void 0 : node.stroke;
13050
+ const pxFill = node.gradient ? gradientStyle(node.gradient, origin.x, origin.y, tileWidth) ?? fill : fill;
13051
+ const pxStroke = node.strokeGradient ? gradientStyle(node.strokeGradient, origin.x, origin.y, tileWidth) ?? stroke : stroke;
13052
+ const patFill = node.fillPattern ? patternStyle(node.fillPattern, groupScale) : void 0;
12858
13053
  switch (node.shape) {
12859
13054
  case "cell": {
12860
13055
  const pts = dctx.projector.cellPath(node.position);
12861
- if (node.fill) painter.fillPoly(pts, node.fill);
12862
- if (node.stroke) painter.strokePoly(pts, node.stroke, node.strokeWidth ?? 1, true);
13056
+ if (pxFill) painter.fillPoly(pts, pxFill);
13057
+ if (patFill) painter.fillPoly(pts, patFill);
13058
+ if (pxStroke) painter.strokePoly(pts, pxStroke, strokePx, true);
12863
13059
  break;
12864
13060
  }
12865
13061
  case "rect": {
@@ -12869,8 +13065,9 @@ var init_DrawShape = __esm({
12869
13065
  const y = p.y + (node.offsetY ?? 0) * tw;
12870
13066
  const w = (node.width ?? 0) * tw;
12871
13067
  const h = (node.height ?? 0) * tw;
12872
- if (node.fill) painter.fillRect(x, y, w, h, node.fill);
12873
- if (node.stroke) painter.strokeRect(x, y, w, h, node.stroke, node.strokeWidth ?? 1);
13068
+ if (pxFill) painter.fillRect(x, y, w, h, pxFill);
13069
+ if (patFill) painter.fillRect(x, y, w, h, patFill);
13070
+ if (pxStroke) painter.strokeRect(x, y, w, h, pxStroke, strokePx);
12874
13071
  break;
12875
13072
  }
12876
13073
  case "ellipse": {
@@ -12880,26 +13077,38 @@ var init_DrawShape = __esm({
12880
13077
  const cy = p.y + (node.offsetY ?? 0) * tw;
12881
13078
  const rx = (node.radiusX ?? 0) * tw;
12882
13079
  const ry = (node.radiusY ?? rx) * tw;
12883
- if (node.fill) painter.fillEllipse(cx, cy, rx, ry, node.fill);
12884
- if (node.stroke) painter.strokeEllipse(cx, cy, rx, ry, node.stroke, node.strokeWidth ?? 1);
13080
+ if (pxFill) painter.fillEllipse(cx, cy, rx, ry, pxFill);
13081
+ if (patFill) painter.fillEllipse(cx, cy, rx, ry, patFill);
13082
+ if (pxStroke) painter.strokeEllipse(cx, cy, rx, ry, pxStroke, strokePx);
12885
13083
  break;
12886
13084
  }
12887
13085
  case "poly": {
12888
- const base = dctx.projector.project(node.position);
12889
- const tw = dctx.projector.tileWidth;
12890
- const pts = (node.points ?? []).map((pt) => ({ x: base.x + pt.x * tw, y: base.y + pt.y * tw }));
12891
- if (node.fill) painter.fillPoly(pts, node.fill);
12892
- if (node.stroke) painter.strokePoly(pts, node.stroke, node.strokeWidth ?? 1, true);
13086
+ const pts = (node.points ?? []).map((pt) => ({
13087
+ x: origin.x + ((node.offsetX ?? 0) + pt.x) * tileWidth,
13088
+ y: origin.y + ((node.offsetY ?? 0) + pt.y) * tileWidth
13089
+ }));
13090
+ if (pxFill) painter.fillPoly(pts, pxFill);
13091
+ if (patFill) painter.fillPoly(pts, patFill);
13092
+ if (pxStroke) painter.strokePoly(pts, pxStroke, strokePx, true);
12893
13093
  break;
12894
13094
  }
12895
13095
  case "path": {
12896
13096
  if (!node.d) break;
12897
- const base = dctx.projector.project(node.position);
12898
- const tw = dctx.projector.tileWidth;
12899
- painter.translate(base.x, base.y);
12900
- painter.scale(tw, tw);
12901
- if (node.fill) painter.fillPath(node.d, node.fill);
12902
- if (node.stroke) painter.strokePath(node.d, node.stroke, (node.strokeWidth ?? 1) / tw);
13097
+ painter.translate(origin.x + (node.offsetX ?? 0) * tileWidth, origin.y + (node.offsetY ?? 0) * tileWidth);
13098
+ painter.scale(tileWidth, tileWidth);
13099
+ if (node.strokeDash && node.strokeDash.length > 0) {
13100
+ painter.setLineDash(
13101
+ node.strokeDash.map((v) => v / (groupScale * tileWidth)),
13102
+ (node.strokeDashOffset ?? 0) / (groupScale * tileWidth)
13103
+ );
13104
+ }
13105
+ if (node.blur !== void 0 && node.blur > 0) painter.setBlur(node.blur / (groupScale * tileWidth));
13106
+ const localFill = node.gradient ? gradientStyle(node.gradient, 0, 0, 1) ?? fill : fill;
13107
+ const localStroke = node.strokeGradient ? gradientStyle(node.strokeGradient, 0, 0, 1) ?? stroke : stroke;
13108
+ const localPat = node.fillPattern ? patternStyle(node.fillPattern, groupScale * tileWidth) : void 0;
13109
+ if (localFill) painter.fillPath(node.d, localFill);
13110
+ if (localPat) painter.fillPath(node.d, localPat);
13111
+ if (localStroke) painter.strokePath(node.d, localStroke, strokePx / tileWidth);
12903
13112
  break;
12904
13113
  }
12905
13114
  }
@@ -12980,8 +13189,6 @@ var init_DrawTextLayer = __esm({
12980
13189
  };
12981
13190
  }
12982
13191
  });
12983
-
12984
- // lib/drawable/paintDispatch.ts
12985
13192
  function paintDrawable(painter, node, dctx) {
12986
13193
  switch (node.type) {
12987
13194
  case "draw-sprite":
@@ -13002,10 +13209,20 @@ function paintDrawable(painter, node, dctx) {
13002
13209
  if (node.scale !== void 0) painter.scale(node.scale, node.scale);
13003
13210
  if (node.rotate !== void 0) painter.rotate(node.rotate);
13004
13211
  if (node.opacity !== void 0 && node.opacity !== 1) painter.setAlpha(node.opacity);
13005
- for (const item of node.items) paintDrawable(painter, item, dctx);
13212
+ if (node.clip) {
13213
+ const tw = dctx.projector.tileWidth;
13214
+ painter.scale(tw, tw);
13215
+ painter.clipPath(node.clip);
13216
+ painter.scale(1 / tw, 1 / tw);
13217
+ }
13218
+ const childCtx = node.scale !== void 0 && node.scale !== 1 ? { ...dctx, groupScale: (dctx.groupScale ?? 1) * node.scale } : dctx;
13219
+ for (const item of node.items) paintDrawable(painter, item, childCtx);
13006
13220
  painter.restore();
13007
13221
  break;
13008
13222
  }
13223
+ case "draw-mesh":
13224
+ warnUnsupported2d("draw-mesh");
13225
+ break;
13009
13226
  case "draw-sprite-layer":
13010
13227
  paintSpriteLayer(painter, node, dctx);
13011
13228
  break;
@@ -13017,6 +13234,7 @@ function paintDrawable(painter, node, dctx) {
13017
13234
  break;
13018
13235
  }
13019
13236
  }
13237
+ var paint2dLog, warnedUnsupported2d, warnUnsupported2d;
13020
13238
  var init_paintDispatch = __esm({
13021
13239
  "lib/drawable/paintDispatch.ts"() {
13022
13240
  init_contract();
@@ -13026,6 +13244,13 @@ var init_paintDispatch = __esm({
13026
13244
  init_DrawSpriteLayer();
13027
13245
  init_DrawShapeLayer();
13028
13246
  init_DrawTextLayer();
13247
+ paint2dLog = createLogger("almadar:ui:drawable-2d");
13248
+ warnedUnsupported2d = /* @__PURE__ */ new Set();
13249
+ warnUnsupported2d = (kind) => {
13250
+ if (warnedUnsupported2d.has(kind)) return;
13251
+ warnedUnsupported2d.add(kind);
13252
+ paint2dLog.warn("unsupported drawable kind on the 2D painter \u2014 skipped", { kind });
13253
+ };
13029
13254
  }
13030
13255
  });
13031
13256
 
@@ -13040,6 +13265,7 @@ function collectDrawnItems(nodes) {
13040
13265
  case "draw-shape":
13041
13266
  case "draw-text":
13042
13267
  case "draw-group":
13268
+ case "draw-mesh":
13043
13269
  if (isValidScenePos(n.position)) out.push({ pos: n.position, id: n.id });
13044
13270
  break;
13045
13271
  case "draw-sprite-layer":
@@ -13111,7 +13337,6 @@ function Canvas2D({
13111
13337
  childDrawablesRef.current.push(node);
13112
13338
  }, []);
13113
13339
  const hasJsxChildren = React93.Children.count(children) > 0;
13114
- drawables && drawables.length > 0 ? drawables : childDrawablesRef.current;
13115
13340
  function isDrawableLayer(node) {
13116
13341
  return node.type === "draw-sprite-layer" || node.type === "draw-shape-layer" || node.type === "draw-text-layer";
13117
13342
  }
@@ -13260,11 +13485,24 @@ function Canvas2D({
13260
13485
  }, [showMinimap, scenePositions]);
13261
13486
  const miniMapWidth = gridExtent.width || 10;
13262
13487
  const miniMapHeight = gridExtent.height || 10;
13263
- const draw = useCallback(() => {
13488
+ const drawableIsAnimated = (node) => {
13489
+ if (node.type === "draw-shape") return isAnimatedShape(node);
13490
+ if (node.type === "draw-group") return Array.isArray(node.items) && node.items.some(drawableIsAnimated);
13491
+ if (node.type === "draw-shape-layer") return Array.isArray(node.items) && node.items.some(isAnimatedShape);
13492
+ return false;
13493
+ };
13494
+ const animRafRef = useRef(0);
13495
+ const drawTimeRef = useRef(() => void 0);
13496
+ const draw = useCallback((timeMs = 0) => {
13264
13497
  const canvas = canvasRef.current;
13265
13498
  if (!canvas) return;
13266
13499
  const ctx = canvas.getContext("2d");
13267
13500
  if (!ctx) return;
13501
+ const scheduleAnimation = (nodes) => {
13502
+ if (!nodes.some(drawableIsAnimated)) return;
13503
+ cancelAnimationFrame(animRafRef.current);
13504
+ animRafRef.current = requestAnimationFrame(() => drawTimeRef.current(performance.now()));
13505
+ };
13268
13506
  const dpr = window.devicePixelRatio || 1;
13269
13507
  canvas.width = viewportSize.width * dpr;
13270
13508
  canvas.height = viewportSize.height * dpr;
@@ -13297,14 +13535,24 @@ function Canvas2D({
13297
13535
  if (!drawables || drawables.length === 0) {
13298
13536
  const childDrawables = childDrawablesRef.current;
13299
13537
  if (childDrawables.length === 0) return;
13538
+ const cam0 = cameraRef.current;
13539
+ if (camera !== "follow" && dragDistance() === 0) {
13540
+ const focus = cameraPos ?? defaultGridFocus;
13541
+ if (focus) {
13542
+ const p = projector.anchorPoint(focus, "center");
13543
+ cam0.x = p.x - viewportSize.width / 2;
13544
+ cam0.y = p.y - viewportSize.height / 2;
13545
+ }
13546
+ }
13300
13547
  const painter0 = createWebPainter(ctx, bumpAtlas);
13301
13548
  painter0.save();
13302
13549
  painter0.translate(viewportSize.width / 2, viewportSize.height / 2);
13303
- painter0.scale(cameraRef.current.zoom, cameraRef.current.zoom);
13304
- painter0.translate(-viewportSize.width / 2, -viewportSize.height / 2);
13305
- const dctx0 = { projector, time: 0, invalidate: bumpAtlas };
13550
+ painter0.scale(cam0.zoom, cam0.zoom);
13551
+ painter0.translate(-viewportSize.width / 2 - cam0.x, -viewportSize.height / 2 - cam0.y);
13552
+ const dctx0 = { projector, time: timeMs, invalidate: bumpAtlas };
13306
13553
  for (const node of childDrawables) paintDrawable(painter0, node, dctx0);
13307
13554
  painter0.restore();
13555
+ scheduleAnimation(childDrawables);
13308
13556
  return;
13309
13557
  }
13310
13558
  const cam = cameraRef.current;
@@ -13324,10 +13572,15 @@ function Canvas2D({
13324
13572
  painter.translate(viewportSize.width / 2, viewportSize.height / 2);
13325
13573
  painter.scale(cam.zoom, cam.zoom);
13326
13574
  painter.translate(-viewportSize.width / 2 - cam.x, -viewportSize.height / 2 - cam.y);
13327
- const dctx = { projector, time: 0, invalidate: bumpAtlas };
13575
+ const dctx = { projector, time: timeMs, invalidate: bumpAtlas };
13328
13576
  for (const node of drawables) paintDrawable(painter, node, dctx);
13329
13577
  painter.restore();
13578
+ scheduleAnimation(drawables);
13330
13579
  }, [viewportSize, backgroundImage, bgColor, drawables, projector, cameraRef, bumpAtlas, getImage, cameraPos, defaultGridFocus, camera, dragDistance]);
13580
+ useEffect(() => {
13581
+ drawTimeRef.current = draw;
13582
+ }, [draw]);
13583
+ useEffect(() => () => cancelAnimationFrame(animRafRef.current), []);
13331
13584
  useEffect(() => {
13332
13585
  if (camera !== "follow" || !followTarget) return;
13333
13586
  const p = projector.anchorPoint(followTarget, "center");
@@ -13550,15 +13803,7 @@ function Canvas2D({
13550
13803
  mapHeight: miniMapHeight
13551
13804
  }
13552
13805
  ) }),
13553
- hasJsxChildren && /* @__PURE__ */ jsx("div", { "aria-hidden": "true", style: { position: "absolute", width: 0, height: 0, overflow: "hidden" }, children }),
13554
- /* @__PURE__ */ jsxs("div", { "data-debug": "", style: { position: "absolute", top: 0, left: 0, background: "yellow", color: "black", zIndex: 9999, fontSize: 24, padding: 8 }, children: [
13555
- "C=",
13556
- React93.Children.count(children),
13557
- " J=",
13558
- String(hasJsxChildren),
13559
- " D=",
13560
- drawables?.length ?? -1
13561
- ] })
13806
+ hasJsxChildren && /* @__PURE__ */ jsx("div", { "aria-hidden": "true", style: { position: "absolute", width: 0, height: 0, overflow: "hidden" }, children })
13562
13807
  ]
13563
13808
  }
13564
13809
  ) });
@@ -13583,6 +13828,7 @@ var init_Canvas2D = __esm({
13583
13828
  init_webPainter2d();
13584
13829
  init_projector();
13585
13830
  init_paintDispatch();
13831
+ init_DrawShape();
13586
13832
  init_registry();
13587
13833
  init_hitTest();
13588
13834
  init_isometric();
@@ -13637,6 +13883,7 @@ function Canvas({
13637
13883
  isLoading,
13638
13884
  cameraMode: to3DCameraMode(camera?.mode),
13639
13885
  ...zoom !== void 0 ? { scale: zoom } : {},
13886
+ ...camera?.fov !== void 0 ? { fov: camera.fov } : {},
13640
13887
  ...camera?.target !== void 0 ? { followTarget: camera.target } : {},
13641
13888
  unitScale,
13642
13889
  backgroundColor,
@@ -40578,14 +40825,26 @@ var init_DetailPanel = __esm({
40578
40825
  DetailPanel.displayName = "DetailPanel";
40579
40826
  }
40580
40827
  });
40581
-
40582
- // components/game/atoms/DrawGroup.tsx
40583
- function DrawGroup(_props) {
40828
+ function DrawGroup(props) {
40829
+ const register = useContext(DrawableRegistryContext);
40830
+ if (register) register({ ...props, type: "draw-group" });
40584
40831
  return null;
40585
40832
  }
40586
40833
  var init_DrawGroup = __esm({
40587
40834
  "components/game/atoms/DrawGroup.tsx"() {
40588
40835
  "use client";
40836
+ init_registry();
40837
+ }
40838
+ });
40839
+ function DrawMesh(props) {
40840
+ const register = useContext(DrawableRegistryContext);
40841
+ if (register) register({ ...props, type: "draw-mesh" });
40842
+ return null;
40843
+ }
40844
+ var init_DrawMesh = __esm({
40845
+ "components/game/atoms/DrawMesh.tsx"() {
40846
+ "use client";
40847
+ init_registry();
40589
40848
  }
40590
40849
  });
40591
40850
  function extractTitle(children) {
@@ -46311,6 +46570,7 @@ var init_component_registry_generated = __esm({
46311
46570
  init_DocTOC();
46312
46571
  init_DocumentViewer();
46313
46572
  init_DrawGroup();
46573
+ init_DrawMesh();
46314
46574
  init_DrawShape();
46315
46575
  init_DrawShapeLayer();
46316
46576
  init_DrawSprite();
@@ -46579,6 +46839,7 @@ var init_component_registry_generated = __esm({
46579
46839
  "DocTOC": DocTOC,
46580
46840
  "DocumentViewer": DocumentViewer,
46581
46841
  "DrawGroup": DrawGroup,
46842
+ "DrawMesh": DrawMesh,
46582
46843
  "DrawShape": DrawShape,
46583
46844
  "DrawShapeLayer": DrawShapeLayer,
46584
46845
  "DrawSprite": DrawSprite,
@@ -47487,8 +47748,13 @@ function SlotContentRenderer({
47487
47748
  const isSingleChild = typeof childrenConfig === "string" || typeof childrenConfig === "object" && childrenConfig !== null && !Array.isArray(childrenConfig) && "type" in childrenConfig;
47488
47749
  const hasChildren = PATTERNS_WITH_CHILDREN.has(content.pattern) || Array.isArray(childrenConfig) && childrenConfig.length > 0 || isSingleChild;
47489
47750
  const isDrawHost = isDrawHostPattern(content.pattern);
47751
+ const arr = Array.isArray(childrenConfig) ? childrenConfig : childrenConfig ? [childrenConfig] : [];
47752
+ const hasTraitChildren = arr.some(
47753
+ (c) => typeof c === "string" && TRAIT_BINDING_RE.test(c)
47754
+ );
47755
+ const drawHostUsesReactChildren = isDrawHost && hasTraitChildren;
47490
47756
  const myPath = patternPath ?? "root";
47491
- const renderedChildren = hasChildren && !isDrawHost ? renderPatternChildren(childrenConfig, onDismiss, content.id, myPath, content.sourceTrait, {
47757
+ const renderedChildren = hasChildren && (!isDrawHost || drawHostUsesReactChildren) ? renderPatternChildren(childrenConfig, onDismiss, content.id, myPath, content.sourceTrait, {
47492
47758
  slot: content.slot,
47493
47759
  transitionEvent: content.transitionEvent,
47494
47760
  fromState: content.fromState,
@@ -47549,7 +47815,7 @@ function SlotContentRenderer({
47549
47815
  for (const [k, v] of Object.entries(nodeSlotOverrides)) {
47550
47816
  finalProps[k] = v;
47551
47817
  }
47552
- if (isDrawHost && Array.isArray(childrenConfig) && childrenConfig.length > 0) {
47818
+ if (isDrawHost && !drawHostUsesReactChildren && Array.isArray(childrenConfig) && childrenConfig.length > 0) {
47553
47819
  finalProps.drawables = toDrawableNodes(childrenConfig);
47554
47820
  }
47555
47821
  const entityVal = finalProps.entity;
@@ -47728,6 +47994,8 @@ var init_UISlotRenderer = __esm({
47728
47994
  "vstack",
47729
47995
  "hstack",
47730
47996
  "box",
47997
+ "canvas",
47998
+ "canvas-2d",
47731
47999
  "grid",
47732
48000
  "center",
47733
48001
  "card",
@@ -1,5 +1,5 @@
1
1
  import { OrbitalSchema, SExpr, Effect, OrbitalVerificationAPI, TraitStateSnapshot, EventPayload, BusEvent, VerificationCheck, BridgeHealth, VerificationSnapshot, VerificationSummary, TransitionTrace, ServerResponseTrace, CheckStatus, AssetLoadStatus } from '@almadar/core';
2
- import { D as DrawableNode } from './paintDispatch-DXygiK7M.js';
2
+ import { D as DrawableNode } from './paintDispatch-BQn5Lyx1.js';
3
3
  import { ClassValue } from 'clsx';
4
4
 
5
5
  /**
@@ -1,5 +1,5 @@
1
1
  import { OrbitalSchema, SExpr, Effect, OrbitalVerificationAPI, TraitStateSnapshot, EventPayload, BusEvent, VerificationCheck, BridgeHealth, VerificationSnapshot, VerificationSummary, TransitionTrace, ServerResponseTrace, CheckStatus, AssetLoadStatus } from '@almadar/core';
2
- import { D as DrawableNode } from './paintDispatch-DXygiK7M.cjs';
2
+ import { D as DrawableNode } from './paintDispatch-BQn5Lyx1.cjs';
3
3
  import { ClassValue } from 'clsx';
4
4
 
5
5
  /**