@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.
@@ -12629,9 +12629,26 @@ var init_imageCache = __esm({
12629
12629
  });
12630
12630
 
12631
12631
  // lib/webPainter2d.ts
12632
+ function makeNoiseTile(alpha, color) {
12633
+ const tile = document.createElement("canvas");
12634
+ tile.width = NOISE_CELLS;
12635
+ tile.height = NOISE_CELLS;
12636
+ const t = tile.getContext("2d");
12637
+ if (t) {
12638
+ t.fillStyle = color;
12639
+ for (let y = 0; y < NOISE_CELLS; y++) {
12640
+ for (let x = 0; x < NOISE_CELLS; x++) {
12641
+ t.globalAlpha = noiseHash(x, y) * alpha;
12642
+ t.fillRect(x, y, 1, 1);
12643
+ }
12644
+ }
12645
+ }
12646
+ return tile;
12647
+ }
12632
12648
  function createWebPainter(ctx, onAssetLoad) {
12633
12649
  let vw = 0;
12634
12650
  let vh = 0;
12651
+ const patternCache = /* @__PURE__ */ new Map();
12635
12652
  const tracePoly = (points, closed) => {
12636
12653
  if (points.length === 0) return;
12637
12654
  ctx.beginPath();
@@ -12639,6 +12656,31 @@ function createWebPainter(ctx, onAssetLoad) {
12639
12656
  for (let i = 1; i < points.length; i++) ctx.lineTo(points[i].x, points[i].y);
12640
12657
  if (closed) ctx.closePath();
12641
12658
  };
12659
+ const toCanvasPattern = (style) => {
12660
+ const key = JSON.stringify(style);
12661
+ let pattern = patternCache.get(key);
12662
+ if (pattern === void 0) {
12663
+ if (style.kind === "noise") {
12664
+ pattern = ctx.createPattern(makeNoiseTile(style.alpha ?? 0.12, style.color ?? "#000000"), "repeat");
12665
+ } else {
12666
+ const img = getOrLoadImage(style.url, onAssetLoad);
12667
+ if (!img) return "rgba(0,0,0,0)";
12668
+ pattern = ctx.createPattern(img, "repeat");
12669
+ }
12670
+ if (pattern && style.scale !== void 0 && style.scale !== 1) {
12671
+ pattern.setTransform(new DOMMatrix().scale(style.scale));
12672
+ }
12673
+ patternCache.set(key, pattern);
12674
+ }
12675
+ return pattern ?? "rgba(0,0,0,0)";
12676
+ };
12677
+ const toCanvasStyle = (style) => {
12678
+ if (typeof style === "string") return style;
12679
+ if (style.kind === "noise" || style.kind === "image") return toCanvasPattern(style);
12680
+ 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);
12681
+ for (const stop of style.stops) g.addColorStop(stop.offset, stop.color);
12682
+ return g;
12683
+ };
12642
12684
  return {
12643
12685
  setViewport(width, height, dpr) {
12644
12686
  vw = width;
@@ -12670,6 +12712,19 @@ function createWebPainter(ctx, onAssetLoad) {
12670
12712
  ctx.shadowColor = shadow ? shadow.color : "transparent";
12671
12713
  ctx.shadowBlur = shadow ? shadow.blur : 0;
12672
12714
  },
12715
+ setBlend(mode) {
12716
+ ctx.globalCompositeOperation = mode ?? "source-over";
12717
+ },
12718
+ setLineDash(pattern, offset = 0) {
12719
+ ctx.setLineDash(pattern ? [...pattern] : []);
12720
+ ctx.lineDashOffset = offset;
12721
+ },
12722
+ setBlur(px) {
12723
+ ctx.filter = px && px > 0 ? `blur(${px}px)` : "none";
12724
+ },
12725
+ clipPath(d) {
12726
+ ctx.clip(new Path2D(d));
12727
+ },
12673
12728
  resolveTexture(url) {
12674
12729
  const img = getOrLoadImage(url, onAssetLoad);
12675
12730
  if (!img) return null;
@@ -12692,47 +12747,47 @@ function createWebPainter(ctx, onAssetLoad) {
12692
12747
  ctx.drawImage(img, dest.x, dest.y, dw, dh);
12693
12748
  }
12694
12749
  },
12695
- fillRect(x, y, w, h, color) {
12696
- ctx.fillStyle = color;
12750
+ fillRect(x, y, w, h, style) {
12751
+ ctx.fillStyle = toCanvasStyle(style);
12697
12752
  ctx.fillRect(x, y, w, h);
12698
12753
  },
12699
- strokeRect(x, y, w, h, color, lineWidth = 1) {
12700
- ctx.strokeStyle = color;
12754
+ strokeRect(x, y, w, h, style, lineWidth = 1) {
12755
+ ctx.strokeStyle = toCanvasStyle(style);
12701
12756
  ctx.lineWidth = lineWidth;
12702
12757
  ctx.strokeRect(x, y, w, h);
12703
12758
  },
12704
- fillPoly(points, color) {
12759
+ fillPoly(points, style) {
12705
12760
  if (points.length === 0) return;
12706
12761
  tracePoly(points, true);
12707
- ctx.fillStyle = color;
12762
+ ctx.fillStyle = toCanvasStyle(style);
12708
12763
  ctx.fill();
12709
12764
  },
12710
- strokePoly(points, color, lineWidth = 1, closed = false) {
12765
+ strokePoly(points, style, lineWidth = 1, closed = false) {
12711
12766
  if (points.length === 0) return;
12712
12767
  tracePoly(points, closed);
12713
- ctx.strokeStyle = color;
12768
+ ctx.strokeStyle = toCanvasStyle(style);
12714
12769
  ctx.lineWidth = lineWidth;
12715
12770
  ctx.stroke();
12716
12771
  },
12717
- fillEllipse(cx, cy, rx, ry, color) {
12772
+ fillEllipse(cx, cy, rx, ry, style) {
12718
12773
  ctx.beginPath();
12719
12774
  ctx.ellipse(cx, cy, rx, ry, 0, 0, Math.PI * 2);
12720
- ctx.fillStyle = color;
12775
+ ctx.fillStyle = toCanvasStyle(style);
12721
12776
  ctx.fill();
12722
12777
  },
12723
- strokeEllipse(cx, cy, rx, ry, color, lineWidth = 1) {
12778
+ strokeEllipse(cx, cy, rx, ry, style, lineWidth = 1) {
12724
12779
  ctx.beginPath();
12725
12780
  ctx.ellipse(cx, cy, rx, ry, 0, 0, Math.PI * 2);
12726
- ctx.strokeStyle = color;
12781
+ ctx.strokeStyle = toCanvasStyle(style);
12727
12782
  ctx.lineWidth = lineWidth;
12728
12783
  ctx.stroke();
12729
12784
  },
12730
- fillPath(d, color) {
12731
- ctx.fillStyle = color;
12785
+ fillPath(d, style) {
12786
+ ctx.fillStyle = toCanvasStyle(style);
12732
12787
  ctx.fill(new Path2D(d));
12733
12788
  },
12734
- strokePath(d, color, lineWidth = 1) {
12735
- ctx.strokeStyle = color;
12789
+ strokePath(d, style, lineWidth = 1) {
12790
+ ctx.strokeStyle = toCanvasStyle(style);
12736
12791
  ctx.lineWidth = lineWidth;
12737
12792
  ctx.stroke(new Path2D(d));
12738
12793
  },
@@ -12745,13 +12800,18 @@ function createWebPainter(ctx, onAssetLoad) {
12745
12800
  }
12746
12801
  };
12747
12802
  }
12748
- var handleByImage, imageByHandle;
12803
+ var handleByImage, imageByHandle, noiseHash, NOISE_CELLS;
12749
12804
  var init_webPainter2d = __esm({
12750
12805
  "lib/webPainter2d.ts"() {
12751
12806
  "use client";
12752
12807
  init_imageCache();
12753
12808
  handleByImage = /* @__PURE__ */ new WeakMap();
12754
12809
  imageByHandle = /* @__PURE__ */ new WeakMap();
12810
+ noiseHash = (x, y) => {
12811
+ const s = Math.sin(x * 12.9898 + y * 78.233) * 43758.5453;
12812
+ return s - Math.floor(s);
12813
+ };
12814
+ NOISE_CELLS = 32;
12755
12815
  }
12756
12816
  });
12757
12817
 
@@ -12907,6 +12967,101 @@ var init_registry = __esm({
12907
12967
  DrawableRegistryContext = React93.createContext(null);
12908
12968
  }
12909
12969
  });
12970
+ function isAnimatedShape(node) {
12971
+ return Boolean(node.animation && node.animation.durationMs > 0 && node.animation.keyframes.length > 0);
12972
+ }
12973
+ function applyShapeAnimation(node, timeMs) {
12974
+ const anim = node.animation;
12975
+ if (!anim || !(anim.durationMs > 0) || anim.keyframes.length === 0) return node;
12976
+ const frames = [...anim.keyframes].sort((a, b) => a.at - b.at);
12977
+ const cycle = timeMs / anim.durationMs;
12978
+ const t = anim.loop === false ? Math.min(cycle, 1) : cycle - Math.floor(cycle);
12979
+ const out = { ...node };
12980
+ const trackValue = (key) => {
12981
+ const defined = frames.filter((f3) => f3[key] !== void 0);
12982
+ if (defined.length === 0) return void 0;
12983
+ let prev;
12984
+ let next;
12985
+ for (const f3 of defined) {
12986
+ if (f3.at <= t) prev = f3;
12987
+ else if (!next) next = f3;
12988
+ }
12989
+ if (!prev) return defined[0][key];
12990
+ if (!next) return prev[key];
12991
+ const span = next.at - prev.at;
12992
+ const k = span > 0 ? (t - prev.at) / span : 1;
12993
+ const a = prev[key];
12994
+ const b = next[key];
12995
+ if (typeof a === "number" && typeof b === "number") return lerp(a, b, k);
12996
+ return a;
12997
+ };
12998
+ for (const key of NUMERIC_TRACKS) {
12999
+ const v = trackValue(key);
13000
+ if (v !== void 0) out[key] = v;
13001
+ }
13002
+ const fill = trackValue("fill");
13003
+ if (fill !== void 0) out.fill = fill;
13004
+ const stroke = trackValue("stroke");
13005
+ if (stroke !== void 0) out.stroke = stroke;
13006
+ const shadowFrames = frames.filter((f3) => f3.shadow !== void 0);
13007
+ if (shadowFrames.length > 0) {
13008
+ const sh = trackValue("shadow");
13009
+ if (sh !== void 0) {
13010
+ let prevSh;
13011
+ let nextSh;
13012
+ for (const f3 of shadowFrames) {
13013
+ if (f3.at <= t) prevSh = f3;
13014
+ else if (!nextSh) nextSh = f3;
13015
+ }
13016
+ if (prevSh?.shadow && nextSh?.shadow) {
13017
+ const span = nextSh.at - prevSh.at;
13018
+ const k = span > 0 ? (t - prevSh.at) / span : 1;
13019
+ out.shadow = { color: prevSh.shadow.color, blur: lerp(prevSh.shadow.blur, nextSh.shadow.blur, k) };
13020
+ } else {
13021
+ out.shadow = sh;
13022
+ }
13023
+ }
13024
+ }
13025
+ return out;
13026
+ }
13027
+ function gradientStyle(g, ox, oy, scale) {
13028
+ if (g.kind === "linear") {
13029
+ if (!g.from || !g.to) return void 0;
13030
+ return {
13031
+ kind: "linear",
13032
+ x1: ox + g.from.x * scale,
13033
+ y1: oy + g.from.y * scale,
13034
+ x2: ox + g.to.x * scale,
13035
+ y2: oy + g.to.y * scale,
13036
+ stops: g.stops
13037
+ };
13038
+ }
13039
+ if (g.kind === "conic") {
13040
+ if (!g.center) return void 0;
13041
+ return {
13042
+ kind: "conic",
13043
+ cx: ox + g.center.x * scale,
13044
+ cy: oy + g.center.y * scale,
13045
+ angle: g.angle ?? 0,
13046
+ stops: g.stops
13047
+ };
13048
+ }
13049
+ if (!g.center || g.radius === void 0) return void 0;
13050
+ return {
13051
+ kind: "radial",
13052
+ cx: ox + g.center.x * scale,
13053
+ cy: oy + g.center.y * scale,
13054
+ r: g.radius * scale,
13055
+ stops: g.stops
13056
+ };
13057
+ }
13058
+ function patternStyle(p, unit) {
13059
+ if (p.kind === "image") {
13060
+ if (!p.url) return void 0;
13061
+ return { kind: "image", url: p.url, scale: (p.scale ?? 1) / unit };
13062
+ }
13063
+ return { kind: "noise", scale: (p.scale ?? 2) / unit, alpha: p.alpha, color: p.color };
13064
+ }
12910
13065
  function DrawShape(props) {
12911
13066
  const register = React93.useContext(DrawableRegistryContext);
12912
13067
  if (register) {
@@ -12917,25 +13072,66 @@ function DrawShape(props) {
12917
13072
  ...opacity !== void 0 && opacity > 0 ? { opacity } : {}
12918
13073
  };
12919
13074
  register(node);
12920
- return /* @__PURE__ */ jsxRuntime.jsx("div", { "data-draw-shape-debug": "", style: { position: "absolute", width: 4, height: 4, background: "red", zIndex: 9999 } });
12921
13075
  }
12922
13076
  return null;
12923
13077
  }
12924
- var paintShape;
13078
+ var NUMERIC_TRACKS, lerp, paintShape;
12925
13079
  var init_DrawShape = __esm({
12926
13080
  "components/game/atoms/DrawShape.tsx"() {
12927
13081
  "use client";
12928
13082
  init_contract();
12929
13083
  init_registry();
12930
- paintShape = (painter, node, dctx) => {
13084
+ NUMERIC_TRACKS = [
13085
+ "offsetX",
13086
+ "offsetY",
13087
+ "rotate",
13088
+ "opacity",
13089
+ "radiusX",
13090
+ "radiusY",
13091
+ "width",
13092
+ "height",
13093
+ "strokeWidth",
13094
+ "strokeDashOffset",
13095
+ "blur"
13096
+ ];
13097
+ lerp = (a, b, k) => a + (b - a) * k;
13098
+ paintShape = (painter, rawNode, dctx) => {
13099
+ const node = dctx.time > 0 ? applyShapeAnimation(rawNode, dctx.time) : rawNode;
12931
13100
  if (!isValidScenePos(node.position)) return;
12932
13101
  painter.save();
12933
13102
  if (node.opacity !== void 0 && node.opacity !== 1) painter.setAlpha(node.opacity);
13103
+ const origin = dctx.projector.project(node.position);
13104
+ const tileWidth = dctx.projector.tileWidth;
13105
+ const groupScale = dctx.groupScale ?? 1;
13106
+ const strokePx = (node.strokeWidth ?? 1) / groupScale;
13107
+ if (node.blendMode) painter.setBlend(node.blendMode);
13108
+ if (node.strokeDash && node.strokeDash.length > 0) {
13109
+ painter.setLineDash(
13110
+ node.strokeDash.map((v) => v / groupScale),
13111
+ (node.strokeDashOffset ?? 0) / groupScale
13112
+ );
13113
+ }
13114
+ if (node.blur !== void 0 && node.blur > 0) painter.setBlur(node.blur / groupScale);
13115
+ if (node.rotate) {
13116
+ const pivot = node.pivot ?? { x: 0.5, y: 0.5 };
13117
+ const px = origin.x + pivot.x * tileWidth;
13118
+ const py = origin.y + pivot.y * tileWidth;
13119
+ painter.translate(px, py);
13120
+ painter.rotate(node.rotate);
13121
+ painter.translate(-px, -py);
13122
+ }
13123
+ if (node.shadow) painter.setShadow({ color: node.shadow.color, blur: node.shadow.blur * tileWidth * groupScale });
13124
+ const fill = node.fill === "none" ? void 0 : node.fill;
13125
+ const stroke = node.stroke === "none" ? void 0 : node.stroke;
13126
+ const pxFill = node.gradient ? gradientStyle(node.gradient, origin.x, origin.y, tileWidth) ?? fill : fill;
13127
+ const pxStroke = node.strokeGradient ? gradientStyle(node.strokeGradient, origin.x, origin.y, tileWidth) ?? stroke : stroke;
13128
+ const patFill = node.fillPattern ? patternStyle(node.fillPattern, groupScale) : void 0;
12934
13129
  switch (node.shape) {
12935
13130
  case "cell": {
12936
13131
  const pts = dctx.projector.cellPath(node.position);
12937
- if (node.fill) painter.fillPoly(pts, node.fill);
12938
- if (node.stroke) painter.strokePoly(pts, node.stroke, node.strokeWidth ?? 1, true);
13132
+ if (pxFill) painter.fillPoly(pts, pxFill);
13133
+ if (patFill) painter.fillPoly(pts, patFill);
13134
+ if (pxStroke) painter.strokePoly(pts, pxStroke, strokePx, true);
12939
13135
  break;
12940
13136
  }
12941
13137
  case "rect": {
@@ -12945,8 +13141,9 @@ var init_DrawShape = __esm({
12945
13141
  const y = p.y + (node.offsetY ?? 0) * tw;
12946
13142
  const w = (node.width ?? 0) * tw;
12947
13143
  const h = (node.height ?? 0) * tw;
12948
- if (node.fill) painter.fillRect(x, y, w, h, node.fill);
12949
- if (node.stroke) painter.strokeRect(x, y, w, h, node.stroke, node.strokeWidth ?? 1);
13144
+ if (pxFill) painter.fillRect(x, y, w, h, pxFill);
13145
+ if (patFill) painter.fillRect(x, y, w, h, patFill);
13146
+ if (pxStroke) painter.strokeRect(x, y, w, h, pxStroke, strokePx);
12950
13147
  break;
12951
13148
  }
12952
13149
  case "ellipse": {
@@ -12956,26 +13153,38 @@ var init_DrawShape = __esm({
12956
13153
  const cy = p.y + (node.offsetY ?? 0) * tw;
12957
13154
  const rx = (node.radiusX ?? 0) * tw;
12958
13155
  const ry = (node.radiusY ?? rx) * tw;
12959
- if (node.fill) painter.fillEllipse(cx, cy, rx, ry, node.fill);
12960
- if (node.stroke) painter.strokeEllipse(cx, cy, rx, ry, node.stroke, node.strokeWidth ?? 1);
13156
+ if (pxFill) painter.fillEllipse(cx, cy, rx, ry, pxFill);
13157
+ if (patFill) painter.fillEllipse(cx, cy, rx, ry, patFill);
13158
+ if (pxStroke) painter.strokeEllipse(cx, cy, rx, ry, pxStroke, strokePx);
12961
13159
  break;
12962
13160
  }
12963
13161
  case "poly": {
12964
- const base = dctx.projector.project(node.position);
12965
- const tw = dctx.projector.tileWidth;
12966
- const pts = (node.points ?? []).map((pt) => ({ x: base.x + pt.x * tw, y: base.y + pt.y * tw }));
12967
- if (node.fill) painter.fillPoly(pts, node.fill);
12968
- if (node.stroke) painter.strokePoly(pts, node.stroke, node.strokeWidth ?? 1, true);
13162
+ const pts = (node.points ?? []).map((pt) => ({
13163
+ x: origin.x + ((node.offsetX ?? 0) + pt.x) * tileWidth,
13164
+ y: origin.y + ((node.offsetY ?? 0) + pt.y) * tileWidth
13165
+ }));
13166
+ if (pxFill) painter.fillPoly(pts, pxFill);
13167
+ if (patFill) painter.fillPoly(pts, patFill);
13168
+ if (pxStroke) painter.strokePoly(pts, pxStroke, strokePx, true);
12969
13169
  break;
12970
13170
  }
12971
13171
  case "path": {
12972
13172
  if (!node.d) break;
12973
- const base = dctx.projector.project(node.position);
12974
- const tw = dctx.projector.tileWidth;
12975
- painter.translate(base.x, base.y);
12976
- painter.scale(tw, tw);
12977
- if (node.fill) painter.fillPath(node.d, node.fill);
12978
- if (node.stroke) painter.strokePath(node.d, node.stroke, (node.strokeWidth ?? 1) / tw);
13173
+ painter.translate(origin.x + (node.offsetX ?? 0) * tileWidth, origin.y + (node.offsetY ?? 0) * tileWidth);
13174
+ painter.scale(tileWidth, tileWidth);
13175
+ if (node.strokeDash && node.strokeDash.length > 0) {
13176
+ painter.setLineDash(
13177
+ node.strokeDash.map((v) => v / (groupScale * tileWidth)),
13178
+ (node.strokeDashOffset ?? 0) / (groupScale * tileWidth)
13179
+ );
13180
+ }
13181
+ if (node.blur !== void 0 && node.blur > 0) painter.setBlur(node.blur / (groupScale * tileWidth));
13182
+ const localFill = node.gradient ? gradientStyle(node.gradient, 0, 0, 1) ?? fill : fill;
13183
+ const localStroke = node.strokeGradient ? gradientStyle(node.strokeGradient, 0, 0, 1) ?? stroke : stroke;
13184
+ const localPat = node.fillPattern ? patternStyle(node.fillPattern, groupScale * tileWidth) : void 0;
13185
+ if (localFill) painter.fillPath(node.d, localFill);
13186
+ if (localPat) painter.fillPath(node.d, localPat);
13187
+ if (localStroke) painter.strokePath(node.d, localStroke, strokePx / tileWidth);
12979
13188
  break;
12980
13189
  }
12981
13190
  }
@@ -13056,8 +13265,6 @@ var init_DrawTextLayer = __esm({
13056
13265
  };
13057
13266
  }
13058
13267
  });
13059
-
13060
- // lib/drawable/paintDispatch.ts
13061
13268
  function paintDrawable(painter, node, dctx) {
13062
13269
  switch (node.type) {
13063
13270
  case "draw-sprite":
@@ -13078,10 +13285,20 @@ function paintDrawable(painter, node, dctx) {
13078
13285
  if (node.scale !== void 0) painter.scale(node.scale, node.scale);
13079
13286
  if (node.rotate !== void 0) painter.rotate(node.rotate);
13080
13287
  if (node.opacity !== void 0 && node.opacity !== 1) painter.setAlpha(node.opacity);
13081
- for (const item of node.items) paintDrawable(painter, item, dctx);
13288
+ if (node.clip) {
13289
+ const tw = dctx.projector.tileWidth;
13290
+ painter.scale(tw, tw);
13291
+ painter.clipPath(node.clip);
13292
+ painter.scale(1 / tw, 1 / tw);
13293
+ }
13294
+ const childCtx = node.scale !== void 0 && node.scale !== 1 ? { ...dctx, groupScale: (dctx.groupScale ?? 1) * node.scale } : dctx;
13295
+ for (const item of node.items) paintDrawable(painter, item, childCtx);
13082
13296
  painter.restore();
13083
13297
  break;
13084
13298
  }
13299
+ case "draw-mesh":
13300
+ warnUnsupported2d("draw-mesh");
13301
+ break;
13085
13302
  case "draw-sprite-layer":
13086
13303
  paintSpriteLayer(painter, node, dctx);
13087
13304
  break;
@@ -13093,6 +13310,7 @@ function paintDrawable(painter, node, dctx) {
13093
13310
  break;
13094
13311
  }
13095
13312
  }
13313
+ var paint2dLog, warnedUnsupported2d, warnUnsupported2d;
13096
13314
  var init_paintDispatch = __esm({
13097
13315
  "lib/drawable/paintDispatch.ts"() {
13098
13316
  init_contract();
@@ -13102,6 +13320,13 @@ var init_paintDispatch = __esm({
13102
13320
  init_DrawSpriteLayer();
13103
13321
  init_DrawShapeLayer();
13104
13322
  init_DrawTextLayer();
13323
+ paint2dLog = logger.createLogger("almadar:ui:drawable-2d");
13324
+ warnedUnsupported2d = /* @__PURE__ */ new Set();
13325
+ warnUnsupported2d = (kind) => {
13326
+ if (warnedUnsupported2d.has(kind)) return;
13327
+ warnedUnsupported2d.add(kind);
13328
+ paint2dLog.warn("unsupported drawable kind on the 2D painter \u2014 skipped", { kind });
13329
+ };
13105
13330
  }
13106
13331
  });
13107
13332
 
@@ -13116,6 +13341,7 @@ function collectDrawnItems(nodes) {
13116
13341
  case "draw-shape":
13117
13342
  case "draw-text":
13118
13343
  case "draw-group":
13344
+ case "draw-mesh":
13119
13345
  if (isValidScenePos(n.position)) out.push({ pos: n.position, id: n.id });
13120
13346
  break;
13121
13347
  case "draw-sprite-layer":
@@ -13187,7 +13413,6 @@ function Canvas2D({
13187
13413
  childDrawablesRef.current.push(node);
13188
13414
  }, []);
13189
13415
  const hasJsxChildren = React93__namespace.Children.count(children) > 0;
13190
- drawables && drawables.length > 0 ? drawables : childDrawablesRef.current;
13191
13416
  function isDrawableLayer(node) {
13192
13417
  return node.type === "draw-sprite-layer" || node.type === "draw-shape-layer" || node.type === "draw-text-layer";
13193
13418
  }
@@ -13336,11 +13561,24 @@ function Canvas2D({
13336
13561
  }, [showMinimap, scenePositions]);
13337
13562
  const miniMapWidth = gridExtent.width || 10;
13338
13563
  const miniMapHeight = gridExtent.height || 10;
13339
- const draw = React93.useCallback(() => {
13564
+ const drawableIsAnimated = (node) => {
13565
+ if (node.type === "draw-shape") return isAnimatedShape(node);
13566
+ if (node.type === "draw-group") return Array.isArray(node.items) && node.items.some(drawableIsAnimated);
13567
+ if (node.type === "draw-shape-layer") return Array.isArray(node.items) && node.items.some(isAnimatedShape);
13568
+ return false;
13569
+ };
13570
+ const animRafRef = React93.useRef(0);
13571
+ const drawTimeRef = React93.useRef(() => void 0);
13572
+ const draw = React93.useCallback((timeMs = 0) => {
13340
13573
  const canvas = canvasRef.current;
13341
13574
  if (!canvas) return;
13342
13575
  const ctx = canvas.getContext("2d");
13343
13576
  if (!ctx) return;
13577
+ const scheduleAnimation = (nodes) => {
13578
+ if (!nodes.some(drawableIsAnimated)) return;
13579
+ cancelAnimationFrame(animRafRef.current);
13580
+ animRafRef.current = requestAnimationFrame(() => drawTimeRef.current(performance.now()));
13581
+ };
13344
13582
  const dpr = window.devicePixelRatio || 1;
13345
13583
  canvas.width = viewportSize.width * dpr;
13346
13584
  canvas.height = viewportSize.height * dpr;
@@ -13373,14 +13611,24 @@ function Canvas2D({
13373
13611
  if (!drawables || drawables.length === 0) {
13374
13612
  const childDrawables = childDrawablesRef.current;
13375
13613
  if (childDrawables.length === 0) return;
13614
+ const cam0 = cameraRef.current;
13615
+ if (camera !== "follow" && dragDistance() === 0) {
13616
+ const focus = cameraPos ?? defaultGridFocus;
13617
+ if (focus) {
13618
+ const p = projector.anchorPoint(focus, "center");
13619
+ cam0.x = p.x - viewportSize.width / 2;
13620
+ cam0.y = p.y - viewportSize.height / 2;
13621
+ }
13622
+ }
13376
13623
  const painter0 = createWebPainter(ctx, bumpAtlas);
13377
13624
  painter0.save();
13378
13625
  painter0.translate(viewportSize.width / 2, viewportSize.height / 2);
13379
- painter0.scale(cameraRef.current.zoom, cameraRef.current.zoom);
13380
- painter0.translate(-viewportSize.width / 2, -viewportSize.height / 2);
13381
- const dctx0 = { projector, time: 0, invalidate: bumpAtlas };
13626
+ painter0.scale(cam0.zoom, cam0.zoom);
13627
+ painter0.translate(-viewportSize.width / 2 - cam0.x, -viewportSize.height / 2 - cam0.y);
13628
+ const dctx0 = { projector, time: timeMs, invalidate: bumpAtlas };
13382
13629
  for (const node of childDrawables) paintDrawable(painter0, node, dctx0);
13383
13630
  painter0.restore();
13631
+ scheduleAnimation(childDrawables);
13384
13632
  return;
13385
13633
  }
13386
13634
  const cam = cameraRef.current;
@@ -13400,10 +13648,15 @@ function Canvas2D({
13400
13648
  painter.translate(viewportSize.width / 2, viewportSize.height / 2);
13401
13649
  painter.scale(cam.zoom, cam.zoom);
13402
13650
  painter.translate(-viewportSize.width / 2 - cam.x, -viewportSize.height / 2 - cam.y);
13403
- const dctx = { projector, time: 0, invalidate: bumpAtlas };
13651
+ const dctx = { projector, time: timeMs, invalidate: bumpAtlas };
13404
13652
  for (const node of drawables) paintDrawable(painter, node, dctx);
13405
13653
  painter.restore();
13654
+ scheduleAnimation(drawables);
13406
13655
  }, [viewportSize, backgroundImage, bgColor, drawables, projector, cameraRef, bumpAtlas, getImage, cameraPos, defaultGridFocus, camera, dragDistance]);
13656
+ React93.useEffect(() => {
13657
+ drawTimeRef.current = draw;
13658
+ }, [draw]);
13659
+ React93.useEffect(() => () => cancelAnimationFrame(animRafRef.current), []);
13407
13660
  React93.useEffect(() => {
13408
13661
  if (camera !== "follow" || !followTarget) return;
13409
13662
  const p = projector.anchorPoint(followTarget, "center");
@@ -13626,15 +13879,7 @@ function Canvas2D({
13626
13879
  mapHeight: miniMapHeight
13627
13880
  }
13628
13881
  ) }),
13629
- hasJsxChildren && /* @__PURE__ */ jsxRuntime.jsx("div", { "aria-hidden": "true", style: { position: "absolute", width: 0, height: 0, overflow: "hidden" }, children }),
13630
- /* @__PURE__ */ jsxRuntime.jsxs("div", { "data-debug": "", style: { position: "absolute", top: 0, left: 0, background: "yellow", color: "black", zIndex: 9999, fontSize: 24, padding: 8 }, children: [
13631
- "C=",
13632
- React93__namespace.Children.count(children),
13633
- " J=",
13634
- String(hasJsxChildren),
13635
- " D=",
13636
- drawables?.length ?? -1
13637
- ] })
13882
+ hasJsxChildren && /* @__PURE__ */ jsxRuntime.jsx("div", { "aria-hidden": "true", style: { position: "absolute", width: 0, height: 0, overflow: "hidden" }, children })
13638
13883
  ]
13639
13884
  }
13640
13885
  ) });
@@ -13659,6 +13904,7 @@ var init_Canvas2D = __esm({
13659
13904
  init_webPainter2d();
13660
13905
  init_projector();
13661
13906
  init_paintDispatch();
13907
+ init_DrawShape();
13662
13908
  init_registry();
13663
13909
  init_hitTest();
13664
13910
  init_isometric();
@@ -13713,6 +13959,7 @@ function Canvas({
13713
13959
  isLoading,
13714
13960
  cameraMode: to3DCameraMode(camera?.mode),
13715
13961
  ...zoom !== void 0 ? { scale: zoom } : {},
13962
+ ...camera?.fov !== void 0 ? { fov: camera.fov } : {},
13716
13963
  ...camera?.target !== void 0 ? { followTarget: camera.target } : {},
13717
13964
  unitScale,
13718
13965
  backgroundColor,
@@ -40654,14 +40901,26 @@ var init_DetailPanel = __esm({
40654
40901
  DetailPanel.displayName = "DetailPanel";
40655
40902
  }
40656
40903
  });
40657
-
40658
- // components/game/atoms/DrawGroup.tsx
40659
- function DrawGroup(_props) {
40904
+ function DrawGroup(props) {
40905
+ const register = React93.useContext(DrawableRegistryContext);
40906
+ if (register) register({ ...props, type: "draw-group" });
40660
40907
  return null;
40661
40908
  }
40662
40909
  var init_DrawGroup = __esm({
40663
40910
  "components/game/atoms/DrawGroup.tsx"() {
40664
40911
  "use client";
40912
+ init_registry();
40913
+ }
40914
+ });
40915
+ function DrawMesh(props) {
40916
+ const register = React93.useContext(DrawableRegistryContext);
40917
+ if (register) register({ ...props, type: "draw-mesh" });
40918
+ return null;
40919
+ }
40920
+ var init_DrawMesh = __esm({
40921
+ "components/game/atoms/DrawMesh.tsx"() {
40922
+ "use client";
40923
+ init_registry();
40665
40924
  }
40666
40925
  });
40667
40926
  function extractTitle(children) {
@@ -46387,6 +46646,7 @@ var init_component_registry_generated = __esm({
46387
46646
  init_DocTOC();
46388
46647
  init_DocumentViewer();
46389
46648
  init_DrawGroup();
46649
+ init_DrawMesh();
46390
46650
  init_DrawShape();
46391
46651
  init_DrawShapeLayer();
46392
46652
  init_DrawSprite();
@@ -46655,6 +46915,7 @@ var init_component_registry_generated = __esm({
46655
46915
  "DocTOC": DocTOC,
46656
46916
  "DocumentViewer": DocumentViewer,
46657
46917
  "DrawGroup": DrawGroup,
46918
+ "DrawMesh": DrawMesh,
46658
46919
  "DrawShape": DrawShape,
46659
46920
  "DrawShapeLayer": DrawShapeLayer,
46660
46921
  "DrawSprite": DrawSprite,
@@ -47563,8 +47824,13 @@ function SlotContentRenderer({
47563
47824
  const isSingleChild = typeof childrenConfig === "string" || typeof childrenConfig === "object" && childrenConfig !== null && !Array.isArray(childrenConfig) && "type" in childrenConfig;
47564
47825
  const hasChildren = PATTERNS_WITH_CHILDREN.has(content.pattern) || Array.isArray(childrenConfig) && childrenConfig.length > 0 || isSingleChild;
47565
47826
  const isDrawHost = patterns.isDrawHostPattern(content.pattern);
47827
+ const arr = Array.isArray(childrenConfig) ? childrenConfig : childrenConfig ? [childrenConfig] : [];
47828
+ const hasTraitChildren = arr.some(
47829
+ (c) => typeof c === "string" && TRAIT_BINDING_RE.test(c)
47830
+ );
47831
+ const drawHostUsesReactChildren = isDrawHost && hasTraitChildren;
47566
47832
  const myPath = patternPath ?? "root";
47567
- const renderedChildren = hasChildren && !isDrawHost ? renderPatternChildren(childrenConfig, onDismiss, content.id, myPath, content.sourceTrait, {
47833
+ const renderedChildren = hasChildren && (!isDrawHost || drawHostUsesReactChildren) ? renderPatternChildren(childrenConfig, onDismiss, content.id, myPath, content.sourceTrait, {
47568
47834
  slot: content.slot,
47569
47835
  transitionEvent: content.transitionEvent,
47570
47836
  fromState: content.fromState,
@@ -47625,7 +47891,7 @@ function SlotContentRenderer({
47625
47891
  for (const [k, v] of Object.entries(nodeSlotOverrides)) {
47626
47892
  finalProps[k] = v;
47627
47893
  }
47628
- if (isDrawHost && Array.isArray(childrenConfig) && childrenConfig.length > 0) {
47894
+ if (isDrawHost && !drawHostUsesReactChildren && Array.isArray(childrenConfig) && childrenConfig.length > 0) {
47629
47895
  finalProps.drawables = toDrawableNodes(childrenConfig);
47630
47896
  }
47631
47897
  const entityVal = finalProps.entity;
@@ -47804,6 +48070,8 @@ var init_UISlotRenderer = __esm({
47804
48070
  "vstack",
47805
48071
  "hstack",
47806
48072
  "box",
48073
+ "canvas",
48074
+ "canvas-2d",
47807
48075
  "grid",
47808
48076
  "center",
47809
48077
  "card",