@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.
@@ -9241,9 +9241,26 @@ var init_imageCache = __esm({
9241
9241
  });
9242
9242
 
9243
9243
  // lib/webPainter2d.ts
9244
+ function makeNoiseTile(alpha, color) {
9245
+ const tile = document.createElement("canvas");
9246
+ tile.width = NOISE_CELLS;
9247
+ tile.height = NOISE_CELLS;
9248
+ const t = tile.getContext("2d");
9249
+ if (t) {
9250
+ t.fillStyle = color;
9251
+ for (let y = 0; y < NOISE_CELLS; y++) {
9252
+ for (let x = 0; x < NOISE_CELLS; x++) {
9253
+ t.globalAlpha = noiseHash(x, y) * alpha;
9254
+ t.fillRect(x, y, 1, 1);
9255
+ }
9256
+ }
9257
+ }
9258
+ return tile;
9259
+ }
9244
9260
  function createWebPainter(ctx, onAssetLoad) {
9245
9261
  let vw = 0;
9246
9262
  let vh = 0;
9263
+ const patternCache = /* @__PURE__ */ new Map();
9247
9264
  const tracePoly = (points, closed) => {
9248
9265
  if (points.length === 0) return;
9249
9266
  ctx.beginPath();
@@ -9251,6 +9268,31 @@ function createWebPainter(ctx, onAssetLoad) {
9251
9268
  for (let i = 1; i < points.length; i++) ctx.lineTo(points[i].x, points[i].y);
9252
9269
  if (closed) ctx.closePath();
9253
9270
  };
9271
+ const toCanvasPattern = (style) => {
9272
+ const key = JSON.stringify(style);
9273
+ let pattern = patternCache.get(key);
9274
+ if (pattern === void 0) {
9275
+ if (style.kind === "noise") {
9276
+ pattern = ctx.createPattern(makeNoiseTile(style.alpha ?? 0.12, style.color ?? "#000000"), "repeat");
9277
+ } else {
9278
+ const img = getOrLoadImage(style.url, onAssetLoad);
9279
+ if (!img) return "rgba(0,0,0,0)";
9280
+ pattern = ctx.createPattern(img, "repeat");
9281
+ }
9282
+ if (pattern && style.scale !== void 0 && style.scale !== 1) {
9283
+ pattern.setTransform(new DOMMatrix().scale(style.scale));
9284
+ }
9285
+ patternCache.set(key, pattern);
9286
+ }
9287
+ return pattern ?? "rgba(0,0,0,0)";
9288
+ };
9289
+ const toCanvasStyle = (style) => {
9290
+ if (typeof style === "string") return style;
9291
+ if (style.kind === "noise" || style.kind === "image") return toCanvasPattern(style);
9292
+ 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);
9293
+ for (const stop of style.stops) g.addColorStop(stop.offset, stop.color);
9294
+ return g;
9295
+ };
9254
9296
  return {
9255
9297
  setViewport(width, height, dpr) {
9256
9298
  vw = width;
@@ -9282,6 +9324,19 @@ function createWebPainter(ctx, onAssetLoad) {
9282
9324
  ctx.shadowColor = shadow ? shadow.color : "transparent";
9283
9325
  ctx.shadowBlur = shadow ? shadow.blur : 0;
9284
9326
  },
9327
+ setBlend(mode) {
9328
+ ctx.globalCompositeOperation = mode ?? "source-over";
9329
+ },
9330
+ setLineDash(pattern, offset = 0) {
9331
+ ctx.setLineDash(pattern ? [...pattern] : []);
9332
+ ctx.lineDashOffset = offset;
9333
+ },
9334
+ setBlur(px) {
9335
+ ctx.filter = px && px > 0 ? `blur(${px}px)` : "none";
9336
+ },
9337
+ clipPath(d) {
9338
+ ctx.clip(new Path2D(d));
9339
+ },
9285
9340
  resolveTexture(url) {
9286
9341
  const img = getOrLoadImage(url, onAssetLoad);
9287
9342
  if (!img) return null;
@@ -9304,47 +9359,47 @@ function createWebPainter(ctx, onAssetLoad) {
9304
9359
  ctx.drawImage(img, dest.x, dest.y, dw, dh);
9305
9360
  }
9306
9361
  },
9307
- fillRect(x, y, w, h, color) {
9308
- ctx.fillStyle = color;
9362
+ fillRect(x, y, w, h, style) {
9363
+ ctx.fillStyle = toCanvasStyle(style);
9309
9364
  ctx.fillRect(x, y, w, h);
9310
9365
  },
9311
- strokeRect(x, y, w, h, color, lineWidth = 1) {
9312
- ctx.strokeStyle = color;
9366
+ strokeRect(x, y, w, h, style, lineWidth = 1) {
9367
+ ctx.strokeStyle = toCanvasStyle(style);
9313
9368
  ctx.lineWidth = lineWidth;
9314
9369
  ctx.strokeRect(x, y, w, h);
9315
9370
  },
9316
- fillPoly(points, color) {
9371
+ fillPoly(points, style) {
9317
9372
  if (points.length === 0) return;
9318
9373
  tracePoly(points, true);
9319
- ctx.fillStyle = color;
9374
+ ctx.fillStyle = toCanvasStyle(style);
9320
9375
  ctx.fill();
9321
9376
  },
9322
- strokePoly(points, color, lineWidth = 1, closed = false) {
9377
+ strokePoly(points, style, lineWidth = 1, closed = false) {
9323
9378
  if (points.length === 0) return;
9324
9379
  tracePoly(points, closed);
9325
- ctx.strokeStyle = color;
9380
+ ctx.strokeStyle = toCanvasStyle(style);
9326
9381
  ctx.lineWidth = lineWidth;
9327
9382
  ctx.stroke();
9328
9383
  },
9329
- fillEllipse(cx, cy, rx, ry, color) {
9384
+ fillEllipse(cx, cy, rx, ry, style) {
9330
9385
  ctx.beginPath();
9331
9386
  ctx.ellipse(cx, cy, rx, ry, 0, 0, Math.PI * 2);
9332
- ctx.fillStyle = color;
9387
+ ctx.fillStyle = toCanvasStyle(style);
9333
9388
  ctx.fill();
9334
9389
  },
9335
- strokeEllipse(cx, cy, rx, ry, color, lineWidth = 1) {
9390
+ strokeEllipse(cx, cy, rx, ry, style, lineWidth = 1) {
9336
9391
  ctx.beginPath();
9337
9392
  ctx.ellipse(cx, cy, rx, ry, 0, 0, Math.PI * 2);
9338
- ctx.strokeStyle = color;
9393
+ ctx.strokeStyle = toCanvasStyle(style);
9339
9394
  ctx.lineWidth = lineWidth;
9340
9395
  ctx.stroke();
9341
9396
  },
9342
- fillPath(d, color) {
9343
- ctx.fillStyle = color;
9397
+ fillPath(d, style) {
9398
+ ctx.fillStyle = toCanvasStyle(style);
9344
9399
  ctx.fill(new Path2D(d));
9345
9400
  },
9346
- strokePath(d, color, lineWidth = 1) {
9347
- ctx.strokeStyle = color;
9401
+ strokePath(d, style, lineWidth = 1) {
9402
+ ctx.strokeStyle = toCanvasStyle(style);
9348
9403
  ctx.lineWidth = lineWidth;
9349
9404
  ctx.stroke(new Path2D(d));
9350
9405
  },
@@ -9357,13 +9412,18 @@ function createWebPainter(ctx, onAssetLoad) {
9357
9412
  }
9358
9413
  };
9359
9414
  }
9360
- var handleByImage, imageByHandle;
9415
+ var handleByImage, imageByHandle, noiseHash, NOISE_CELLS;
9361
9416
  var init_webPainter2d = __esm({
9362
9417
  "lib/webPainter2d.ts"() {
9363
9418
  "use client";
9364
9419
  init_imageCache();
9365
9420
  handleByImage = /* @__PURE__ */ new WeakMap();
9366
9421
  imageByHandle = /* @__PURE__ */ new WeakMap();
9422
+ noiseHash = (x, y) => {
9423
+ const s = Math.sin(x * 12.9898 + y * 78.233) * 43758.5453;
9424
+ return s - Math.floor(s);
9425
+ };
9426
+ NOISE_CELLS = 32;
9367
9427
  }
9368
9428
  });
9369
9429
 
@@ -9519,6 +9579,101 @@ var init_registry = __esm({
9519
9579
  DrawableRegistryContext = React84.createContext(null);
9520
9580
  }
9521
9581
  });
9582
+ function isAnimatedShape(node) {
9583
+ return Boolean(node.animation && node.animation.durationMs > 0 && node.animation.keyframes.length > 0);
9584
+ }
9585
+ function applyShapeAnimation(node, timeMs) {
9586
+ const anim = node.animation;
9587
+ if (!anim || !(anim.durationMs > 0) || anim.keyframes.length === 0) return node;
9588
+ const frames = [...anim.keyframes].sort((a, b) => a.at - b.at);
9589
+ const cycle = timeMs / anim.durationMs;
9590
+ const t = anim.loop === false ? Math.min(cycle, 1) : cycle - Math.floor(cycle);
9591
+ const out = { ...node };
9592
+ const trackValue = (key) => {
9593
+ const defined = frames.filter((f3) => f3[key] !== void 0);
9594
+ if (defined.length === 0) return void 0;
9595
+ let prev;
9596
+ let next;
9597
+ for (const f3 of defined) {
9598
+ if (f3.at <= t) prev = f3;
9599
+ else if (!next) next = f3;
9600
+ }
9601
+ if (!prev) return defined[0][key];
9602
+ if (!next) return prev[key];
9603
+ const span = next.at - prev.at;
9604
+ const k = span > 0 ? (t - prev.at) / span : 1;
9605
+ const a = prev[key];
9606
+ const b = next[key];
9607
+ if (typeof a === "number" && typeof b === "number") return lerp(a, b, k);
9608
+ return a;
9609
+ };
9610
+ for (const key of NUMERIC_TRACKS) {
9611
+ const v = trackValue(key);
9612
+ if (v !== void 0) out[key] = v;
9613
+ }
9614
+ const fill = trackValue("fill");
9615
+ if (fill !== void 0) out.fill = fill;
9616
+ const stroke = trackValue("stroke");
9617
+ if (stroke !== void 0) out.stroke = stroke;
9618
+ const shadowFrames = frames.filter((f3) => f3.shadow !== void 0);
9619
+ if (shadowFrames.length > 0) {
9620
+ const sh = trackValue("shadow");
9621
+ if (sh !== void 0) {
9622
+ let prevSh;
9623
+ let nextSh;
9624
+ for (const f3 of shadowFrames) {
9625
+ if (f3.at <= t) prevSh = f3;
9626
+ else if (!nextSh) nextSh = f3;
9627
+ }
9628
+ if (prevSh?.shadow && nextSh?.shadow) {
9629
+ const span = nextSh.at - prevSh.at;
9630
+ const k = span > 0 ? (t - prevSh.at) / span : 1;
9631
+ out.shadow = { color: prevSh.shadow.color, blur: lerp(prevSh.shadow.blur, nextSh.shadow.blur, k) };
9632
+ } else {
9633
+ out.shadow = sh;
9634
+ }
9635
+ }
9636
+ }
9637
+ return out;
9638
+ }
9639
+ function gradientStyle(g, ox, oy, scale) {
9640
+ if (g.kind === "linear") {
9641
+ if (!g.from || !g.to) return void 0;
9642
+ return {
9643
+ kind: "linear",
9644
+ x1: ox + g.from.x * scale,
9645
+ y1: oy + g.from.y * scale,
9646
+ x2: ox + g.to.x * scale,
9647
+ y2: oy + g.to.y * scale,
9648
+ stops: g.stops
9649
+ };
9650
+ }
9651
+ if (g.kind === "conic") {
9652
+ if (!g.center) return void 0;
9653
+ return {
9654
+ kind: "conic",
9655
+ cx: ox + g.center.x * scale,
9656
+ cy: oy + g.center.y * scale,
9657
+ angle: g.angle ?? 0,
9658
+ stops: g.stops
9659
+ };
9660
+ }
9661
+ if (!g.center || g.radius === void 0) return void 0;
9662
+ return {
9663
+ kind: "radial",
9664
+ cx: ox + g.center.x * scale,
9665
+ cy: oy + g.center.y * scale,
9666
+ r: g.radius * scale,
9667
+ stops: g.stops
9668
+ };
9669
+ }
9670
+ function patternStyle(p, unit) {
9671
+ if (p.kind === "image") {
9672
+ if (!p.url) return void 0;
9673
+ return { kind: "image", url: p.url, scale: (p.scale ?? 1) / unit };
9674
+ }
9675
+ return { kind: "noise", scale: (p.scale ?? 2) / unit, alpha: p.alpha, color: p.color };
9676
+ }
9522
9677
  function DrawShape(props) {
9523
9678
  const register = React84.useContext(DrawableRegistryContext);
9524
9679
  if (register) {
@@ -9529,25 +9684,66 @@ function DrawShape(props) {
9529
9684
  ...opacity !== void 0 && opacity > 0 ? { opacity } : {}
9530
9685
  };
9531
9686
  register(node);
9532
- return /* @__PURE__ */ jsxRuntime.jsx("div", { "data-draw-shape-debug": "", style: { position: "absolute", width: 4, height: 4, background: "red", zIndex: 9999 } });
9533
9687
  }
9534
9688
  return null;
9535
9689
  }
9536
- var paintShape;
9690
+ var NUMERIC_TRACKS, lerp, paintShape;
9537
9691
  var init_DrawShape = __esm({
9538
9692
  "components/game/atoms/DrawShape.tsx"() {
9539
9693
  "use client";
9540
9694
  init_contract();
9541
9695
  init_registry();
9542
- paintShape = (painter, node, dctx) => {
9696
+ NUMERIC_TRACKS = [
9697
+ "offsetX",
9698
+ "offsetY",
9699
+ "rotate",
9700
+ "opacity",
9701
+ "radiusX",
9702
+ "radiusY",
9703
+ "width",
9704
+ "height",
9705
+ "strokeWidth",
9706
+ "strokeDashOffset",
9707
+ "blur"
9708
+ ];
9709
+ lerp = (a, b, k) => a + (b - a) * k;
9710
+ paintShape = (painter, rawNode, dctx) => {
9711
+ const node = dctx.time > 0 ? applyShapeAnimation(rawNode, dctx.time) : rawNode;
9543
9712
  if (!isValidScenePos(node.position)) return;
9544
9713
  painter.save();
9545
9714
  if (node.opacity !== void 0 && node.opacity !== 1) painter.setAlpha(node.opacity);
9715
+ const origin = dctx.projector.project(node.position);
9716
+ const tileWidth = dctx.projector.tileWidth;
9717
+ const groupScale = dctx.groupScale ?? 1;
9718
+ const strokePx = (node.strokeWidth ?? 1) / groupScale;
9719
+ if (node.blendMode) painter.setBlend(node.blendMode);
9720
+ if (node.strokeDash && node.strokeDash.length > 0) {
9721
+ painter.setLineDash(
9722
+ node.strokeDash.map((v) => v / groupScale),
9723
+ (node.strokeDashOffset ?? 0) / groupScale
9724
+ );
9725
+ }
9726
+ if (node.blur !== void 0 && node.blur > 0) painter.setBlur(node.blur / groupScale);
9727
+ if (node.rotate) {
9728
+ const pivot = node.pivot ?? { x: 0.5, y: 0.5 };
9729
+ const px = origin.x + pivot.x * tileWidth;
9730
+ const py = origin.y + pivot.y * tileWidth;
9731
+ painter.translate(px, py);
9732
+ painter.rotate(node.rotate);
9733
+ painter.translate(-px, -py);
9734
+ }
9735
+ if (node.shadow) painter.setShadow({ color: node.shadow.color, blur: node.shadow.blur * tileWidth * groupScale });
9736
+ const fill = node.fill === "none" ? void 0 : node.fill;
9737
+ const stroke = node.stroke === "none" ? void 0 : node.stroke;
9738
+ const pxFill = node.gradient ? gradientStyle(node.gradient, origin.x, origin.y, tileWidth) ?? fill : fill;
9739
+ const pxStroke = node.strokeGradient ? gradientStyle(node.strokeGradient, origin.x, origin.y, tileWidth) ?? stroke : stroke;
9740
+ const patFill = node.fillPattern ? patternStyle(node.fillPattern, groupScale) : void 0;
9546
9741
  switch (node.shape) {
9547
9742
  case "cell": {
9548
9743
  const pts = dctx.projector.cellPath(node.position);
9549
- if (node.fill) painter.fillPoly(pts, node.fill);
9550
- if (node.stroke) painter.strokePoly(pts, node.stroke, node.strokeWidth ?? 1, true);
9744
+ if (pxFill) painter.fillPoly(pts, pxFill);
9745
+ if (patFill) painter.fillPoly(pts, patFill);
9746
+ if (pxStroke) painter.strokePoly(pts, pxStroke, strokePx, true);
9551
9747
  break;
9552
9748
  }
9553
9749
  case "rect": {
@@ -9557,8 +9753,9 @@ var init_DrawShape = __esm({
9557
9753
  const y = p.y + (node.offsetY ?? 0) * tw;
9558
9754
  const w = (node.width ?? 0) * tw;
9559
9755
  const h = (node.height ?? 0) * tw;
9560
- if (node.fill) painter.fillRect(x, y, w, h, node.fill);
9561
- if (node.stroke) painter.strokeRect(x, y, w, h, node.stroke, node.strokeWidth ?? 1);
9756
+ if (pxFill) painter.fillRect(x, y, w, h, pxFill);
9757
+ if (patFill) painter.fillRect(x, y, w, h, patFill);
9758
+ if (pxStroke) painter.strokeRect(x, y, w, h, pxStroke, strokePx);
9562
9759
  break;
9563
9760
  }
9564
9761
  case "ellipse": {
@@ -9568,26 +9765,38 @@ var init_DrawShape = __esm({
9568
9765
  const cy = p.y + (node.offsetY ?? 0) * tw;
9569
9766
  const rx = (node.radiusX ?? 0) * tw;
9570
9767
  const ry = (node.radiusY ?? rx) * tw;
9571
- if (node.fill) painter.fillEllipse(cx, cy, rx, ry, node.fill);
9572
- if (node.stroke) painter.strokeEllipse(cx, cy, rx, ry, node.stroke, node.strokeWidth ?? 1);
9768
+ if (pxFill) painter.fillEllipse(cx, cy, rx, ry, pxFill);
9769
+ if (patFill) painter.fillEllipse(cx, cy, rx, ry, patFill);
9770
+ if (pxStroke) painter.strokeEllipse(cx, cy, rx, ry, pxStroke, strokePx);
9573
9771
  break;
9574
9772
  }
9575
9773
  case "poly": {
9576
- const base = dctx.projector.project(node.position);
9577
- const tw = dctx.projector.tileWidth;
9578
- const pts = (node.points ?? []).map((pt) => ({ x: base.x + pt.x * tw, y: base.y + pt.y * tw }));
9579
- if (node.fill) painter.fillPoly(pts, node.fill);
9580
- if (node.stroke) painter.strokePoly(pts, node.stroke, node.strokeWidth ?? 1, true);
9774
+ const pts = (node.points ?? []).map((pt) => ({
9775
+ x: origin.x + ((node.offsetX ?? 0) + pt.x) * tileWidth,
9776
+ y: origin.y + ((node.offsetY ?? 0) + pt.y) * tileWidth
9777
+ }));
9778
+ if (pxFill) painter.fillPoly(pts, pxFill);
9779
+ if (patFill) painter.fillPoly(pts, patFill);
9780
+ if (pxStroke) painter.strokePoly(pts, pxStroke, strokePx, true);
9581
9781
  break;
9582
9782
  }
9583
9783
  case "path": {
9584
9784
  if (!node.d) break;
9585
- const base = dctx.projector.project(node.position);
9586
- const tw = dctx.projector.tileWidth;
9587
- painter.translate(base.x, base.y);
9588
- painter.scale(tw, tw);
9589
- if (node.fill) painter.fillPath(node.d, node.fill);
9590
- if (node.stroke) painter.strokePath(node.d, node.stroke, (node.strokeWidth ?? 1) / tw);
9785
+ painter.translate(origin.x + (node.offsetX ?? 0) * tileWidth, origin.y + (node.offsetY ?? 0) * tileWidth);
9786
+ painter.scale(tileWidth, tileWidth);
9787
+ if (node.strokeDash && node.strokeDash.length > 0) {
9788
+ painter.setLineDash(
9789
+ node.strokeDash.map((v) => v / (groupScale * tileWidth)),
9790
+ (node.strokeDashOffset ?? 0) / (groupScale * tileWidth)
9791
+ );
9792
+ }
9793
+ if (node.blur !== void 0 && node.blur > 0) painter.setBlur(node.blur / (groupScale * tileWidth));
9794
+ const localFill = node.gradient ? gradientStyle(node.gradient, 0, 0, 1) ?? fill : fill;
9795
+ const localStroke = node.strokeGradient ? gradientStyle(node.strokeGradient, 0, 0, 1) ?? stroke : stroke;
9796
+ const localPat = node.fillPattern ? patternStyle(node.fillPattern, groupScale * tileWidth) : void 0;
9797
+ if (localFill) painter.fillPath(node.d, localFill);
9798
+ if (localPat) painter.fillPath(node.d, localPat);
9799
+ if (localStroke) painter.strokePath(node.d, localStroke, strokePx / tileWidth);
9591
9800
  break;
9592
9801
  }
9593
9802
  }
@@ -9668,8 +9877,6 @@ var init_DrawTextLayer = __esm({
9668
9877
  };
9669
9878
  }
9670
9879
  });
9671
-
9672
- // lib/drawable/paintDispatch.ts
9673
9880
  function paintDrawable(painter, node, dctx) {
9674
9881
  switch (node.type) {
9675
9882
  case "draw-sprite":
@@ -9690,10 +9897,20 @@ function paintDrawable(painter, node, dctx) {
9690
9897
  if (node.scale !== void 0) painter.scale(node.scale, node.scale);
9691
9898
  if (node.rotate !== void 0) painter.rotate(node.rotate);
9692
9899
  if (node.opacity !== void 0 && node.opacity !== 1) painter.setAlpha(node.opacity);
9693
- for (const item of node.items) paintDrawable(painter, item, dctx);
9900
+ if (node.clip) {
9901
+ const tw = dctx.projector.tileWidth;
9902
+ painter.scale(tw, tw);
9903
+ painter.clipPath(node.clip);
9904
+ painter.scale(1 / tw, 1 / tw);
9905
+ }
9906
+ const childCtx = node.scale !== void 0 && node.scale !== 1 ? { ...dctx, groupScale: (dctx.groupScale ?? 1) * node.scale } : dctx;
9907
+ for (const item of node.items) paintDrawable(painter, item, childCtx);
9694
9908
  painter.restore();
9695
9909
  break;
9696
9910
  }
9911
+ case "draw-mesh":
9912
+ warnUnsupported2d("draw-mesh");
9913
+ break;
9697
9914
  case "draw-sprite-layer":
9698
9915
  paintSpriteLayer(painter, node, dctx);
9699
9916
  break;
@@ -9705,6 +9922,7 @@ function paintDrawable(painter, node, dctx) {
9705
9922
  break;
9706
9923
  }
9707
9924
  }
9925
+ var paint2dLog, warnedUnsupported2d, warnUnsupported2d;
9708
9926
  var init_paintDispatch = __esm({
9709
9927
  "lib/drawable/paintDispatch.ts"() {
9710
9928
  init_contract();
@@ -9714,6 +9932,13 @@ var init_paintDispatch = __esm({
9714
9932
  init_DrawSpriteLayer();
9715
9933
  init_DrawShapeLayer();
9716
9934
  init_DrawTextLayer();
9935
+ paint2dLog = logger.createLogger("almadar:ui:drawable-2d");
9936
+ warnedUnsupported2d = /* @__PURE__ */ new Set();
9937
+ warnUnsupported2d = (kind) => {
9938
+ if (warnedUnsupported2d.has(kind)) return;
9939
+ warnedUnsupported2d.add(kind);
9940
+ paint2dLog.warn("unsupported drawable kind on the 2D painter \u2014 skipped", { kind });
9941
+ };
9717
9942
  }
9718
9943
  });
9719
9944
 
@@ -9728,6 +9953,7 @@ function collectDrawnItems(nodes) {
9728
9953
  case "draw-shape":
9729
9954
  case "draw-text":
9730
9955
  case "draw-group":
9956
+ case "draw-mesh":
9731
9957
  if (isValidScenePos(n.position)) out.push({ pos: n.position, id: n.id });
9732
9958
  break;
9733
9959
  case "draw-sprite-layer":
@@ -9799,7 +10025,6 @@ function Canvas2D({
9799
10025
  childDrawablesRef.current.push(node);
9800
10026
  }, []);
9801
10027
  const hasJsxChildren = React84__namespace.Children.count(children) > 0;
9802
- drawables && drawables.length > 0 ? drawables : childDrawablesRef.current;
9803
10028
  function isDrawableLayer(node) {
9804
10029
  return node.type === "draw-sprite-layer" || node.type === "draw-shape-layer" || node.type === "draw-text-layer";
9805
10030
  }
@@ -9948,11 +10173,24 @@ function Canvas2D({
9948
10173
  }, [showMinimap, scenePositions]);
9949
10174
  const miniMapWidth = gridExtent.width || 10;
9950
10175
  const miniMapHeight = gridExtent.height || 10;
9951
- const draw = React84.useCallback(() => {
10176
+ const drawableIsAnimated = (node) => {
10177
+ if (node.type === "draw-shape") return isAnimatedShape(node);
10178
+ if (node.type === "draw-group") return Array.isArray(node.items) && node.items.some(drawableIsAnimated);
10179
+ if (node.type === "draw-shape-layer") return Array.isArray(node.items) && node.items.some(isAnimatedShape);
10180
+ return false;
10181
+ };
10182
+ const animRafRef = React84.useRef(0);
10183
+ const drawTimeRef = React84.useRef(() => void 0);
10184
+ const draw = React84.useCallback((timeMs = 0) => {
9952
10185
  const canvas = canvasRef.current;
9953
10186
  if (!canvas) return;
9954
10187
  const ctx = canvas.getContext("2d");
9955
10188
  if (!ctx) return;
10189
+ const scheduleAnimation = (nodes) => {
10190
+ if (!nodes.some(drawableIsAnimated)) return;
10191
+ cancelAnimationFrame(animRafRef.current);
10192
+ animRafRef.current = requestAnimationFrame(() => drawTimeRef.current(performance.now()));
10193
+ };
9956
10194
  const dpr = window.devicePixelRatio || 1;
9957
10195
  canvas.width = viewportSize.width * dpr;
9958
10196
  canvas.height = viewportSize.height * dpr;
@@ -9985,14 +10223,24 @@ function Canvas2D({
9985
10223
  if (!drawables || drawables.length === 0) {
9986
10224
  const childDrawables = childDrawablesRef.current;
9987
10225
  if (childDrawables.length === 0) return;
10226
+ const cam0 = cameraRef.current;
10227
+ if (camera !== "follow" && dragDistance() === 0) {
10228
+ const focus = cameraPos ?? defaultGridFocus;
10229
+ if (focus) {
10230
+ const p = projector.anchorPoint(focus, "center");
10231
+ cam0.x = p.x - viewportSize.width / 2;
10232
+ cam0.y = p.y - viewportSize.height / 2;
10233
+ }
10234
+ }
9988
10235
  const painter0 = createWebPainter(ctx, bumpAtlas);
9989
10236
  painter0.save();
9990
10237
  painter0.translate(viewportSize.width / 2, viewportSize.height / 2);
9991
- painter0.scale(cameraRef.current.zoom, cameraRef.current.zoom);
9992
- painter0.translate(-viewportSize.width / 2, -viewportSize.height / 2);
9993
- const dctx0 = { projector, time: 0, invalidate: bumpAtlas };
10238
+ painter0.scale(cam0.zoom, cam0.zoom);
10239
+ painter0.translate(-viewportSize.width / 2 - cam0.x, -viewportSize.height / 2 - cam0.y);
10240
+ const dctx0 = { projector, time: timeMs, invalidate: bumpAtlas };
9994
10241
  for (const node of childDrawables) paintDrawable(painter0, node, dctx0);
9995
10242
  painter0.restore();
10243
+ scheduleAnimation(childDrawables);
9996
10244
  return;
9997
10245
  }
9998
10246
  const cam = cameraRef.current;
@@ -10012,10 +10260,15 @@ function Canvas2D({
10012
10260
  painter.translate(viewportSize.width / 2, viewportSize.height / 2);
10013
10261
  painter.scale(cam.zoom, cam.zoom);
10014
10262
  painter.translate(-viewportSize.width / 2 - cam.x, -viewportSize.height / 2 - cam.y);
10015
- const dctx = { projector, time: 0, invalidate: bumpAtlas };
10263
+ const dctx = { projector, time: timeMs, invalidate: bumpAtlas };
10016
10264
  for (const node of drawables) paintDrawable(painter, node, dctx);
10017
10265
  painter.restore();
10266
+ scheduleAnimation(drawables);
10018
10267
  }, [viewportSize, backgroundImage, bgColor, drawables, projector, cameraRef, bumpAtlas, getImage, cameraPos, defaultGridFocus, camera, dragDistance]);
10268
+ React84.useEffect(() => {
10269
+ drawTimeRef.current = draw;
10270
+ }, [draw]);
10271
+ React84.useEffect(() => () => cancelAnimationFrame(animRafRef.current), []);
10019
10272
  React84.useEffect(() => {
10020
10273
  if (camera !== "follow" || !followTarget) return;
10021
10274
  const p = projector.anchorPoint(followTarget, "center");
@@ -10238,15 +10491,7 @@ function Canvas2D({
10238
10491
  mapHeight: miniMapHeight
10239
10492
  }
10240
10493
  ) }),
10241
- hasJsxChildren && /* @__PURE__ */ jsxRuntime.jsx("div", { "aria-hidden": "true", style: { position: "absolute", width: 0, height: 0, overflow: "hidden" }, children }),
10242
- /* @__PURE__ */ jsxRuntime.jsxs("div", { "data-debug": "", style: { position: "absolute", top: 0, left: 0, background: "yellow", color: "black", zIndex: 9999, fontSize: 24, padding: 8 }, children: [
10243
- "C=",
10244
- React84__namespace.Children.count(children),
10245
- " J=",
10246
- String(hasJsxChildren),
10247
- " D=",
10248
- drawables?.length ?? -1
10249
- ] })
10494
+ hasJsxChildren && /* @__PURE__ */ jsxRuntime.jsx("div", { "aria-hidden": "true", style: { position: "absolute", width: 0, height: 0, overflow: "hidden" }, children })
10250
10495
  ]
10251
10496
  }
10252
10497
  ) });
@@ -10271,6 +10516,7 @@ var init_Canvas2D = __esm({
10271
10516
  init_webPainter2d();
10272
10517
  init_projector();
10273
10518
  init_paintDispatch();
10519
+ init_DrawShape();
10274
10520
  init_registry();
10275
10521
  init_hitTest();
10276
10522
  init_isometric();
@@ -10325,6 +10571,7 @@ function Canvas({
10325
10571
  isLoading,
10326
10572
  cameraMode: to3DCameraMode(camera?.mode),
10327
10573
  ...zoom !== void 0 ? { scale: zoom } : {},
10574
+ ...camera?.fov !== void 0 ? { fov: camera.fov } : {},
10328
10575
  ...camera?.target !== void 0 ? { followTarget: camera.target } : {},
10329
10576
  unitScale,
10330
10577
  backgroundColor,
@@ -38149,14 +38396,26 @@ var init_DetailPanel = __esm({
38149
38396
  DetailPanel.displayName = "DetailPanel";
38150
38397
  }
38151
38398
  });
38152
-
38153
- // components/game/atoms/DrawGroup.tsx
38154
- function DrawGroup(_props) {
38399
+ function DrawGroup(props) {
38400
+ const register = React84.useContext(DrawableRegistryContext);
38401
+ if (register) register({ ...props, type: "draw-group" });
38155
38402
  return null;
38156
38403
  }
38157
38404
  var init_DrawGroup = __esm({
38158
38405
  "components/game/atoms/DrawGroup.tsx"() {
38159
38406
  "use client";
38407
+ init_registry();
38408
+ }
38409
+ });
38410
+ function DrawMesh(props) {
38411
+ const register = React84.useContext(DrawableRegistryContext);
38412
+ if (register) register({ ...props, type: "draw-mesh" });
38413
+ return null;
38414
+ }
38415
+ var init_DrawMesh = __esm({
38416
+ "components/game/atoms/DrawMesh.tsx"() {
38417
+ "use client";
38418
+ init_registry();
38160
38419
  }
38161
38420
  });
38162
38421
  function extractTitle(children) {
@@ -43882,6 +44141,7 @@ var init_component_registry_generated = __esm({
43882
44141
  init_DocTOC();
43883
44142
  init_DocumentViewer();
43884
44143
  init_DrawGroup();
44144
+ init_DrawMesh();
43885
44145
  init_DrawShape();
43886
44146
  init_DrawShapeLayer();
43887
44147
  init_DrawSprite();
@@ -44150,6 +44410,7 @@ var init_component_registry_generated = __esm({
44150
44410
  "DocTOC": DocTOC,
44151
44411
  "DocumentViewer": DocumentViewer,
44152
44412
  "DrawGroup": DrawGroup,
44413
+ "DrawMesh": DrawMesh,
44153
44414
  "DrawShape": DrawShape,
44154
44415
  "DrawShapeLayer": DrawShapeLayer,
44155
44416
  "DrawSprite": DrawSprite,
@@ -45058,8 +45319,13 @@ function SlotContentRenderer({
45058
45319
  const isSingleChild = typeof childrenConfig === "string" || typeof childrenConfig === "object" && childrenConfig !== null && !Array.isArray(childrenConfig) && "type" in childrenConfig;
45059
45320
  const hasChildren = PATTERNS_WITH_CHILDREN.has(content.pattern) || Array.isArray(childrenConfig) && childrenConfig.length > 0 || isSingleChild;
45060
45321
  const isDrawHost = patterns.isDrawHostPattern(content.pattern);
45322
+ const arr = Array.isArray(childrenConfig) ? childrenConfig : childrenConfig ? [childrenConfig] : [];
45323
+ const hasTraitChildren = arr.some(
45324
+ (c) => typeof c === "string" && TRAIT_BINDING_RE.test(c)
45325
+ );
45326
+ const drawHostUsesReactChildren = isDrawHost && hasTraitChildren;
45061
45327
  const myPath = patternPath ?? "root";
45062
- const renderedChildren = hasChildren && !isDrawHost ? renderPatternChildren(childrenConfig, onDismiss, content.id, myPath, content.sourceTrait, {
45328
+ const renderedChildren = hasChildren && (!isDrawHost || drawHostUsesReactChildren) ? renderPatternChildren(childrenConfig, onDismiss, content.id, myPath, content.sourceTrait, {
45063
45329
  slot: content.slot,
45064
45330
  transitionEvent: content.transitionEvent,
45065
45331
  fromState: content.fromState,
@@ -45120,7 +45386,7 @@ function SlotContentRenderer({
45120
45386
  for (const [k, v] of Object.entries(nodeSlotOverrides)) {
45121
45387
  finalProps[k] = v;
45122
45388
  }
45123
- if (isDrawHost && Array.isArray(childrenConfig) && childrenConfig.length > 0) {
45389
+ if (isDrawHost && !drawHostUsesReactChildren && Array.isArray(childrenConfig) && childrenConfig.length > 0) {
45124
45390
  finalProps.drawables = toDrawableNodes(childrenConfig);
45125
45391
  }
45126
45392
  const entityVal = finalProps.entity;
@@ -45299,6 +45565,8 @@ var init_UISlotRenderer = __esm({
45299
45565
  "vstack",
45300
45566
  "hstack",
45301
45567
  "box",
45568
+ "canvas",
45569
+ "canvas-2d",
45302
45570
  "grid",
45303
45571
  "center",
45304
45572
  "card",