@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.
@@ -8887,9 +8887,26 @@ var init_imageCache = __esm({
8887
8887
  });
8888
8888
 
8889
8889
  // lib/webPainter2d.ts
8890
+ function makeNoiseTile(alpha, color) {
8891
+ const tile = document.createElement("canvas");
8892
+ tile.width = NOISE_CELLS;
8893
+ tile.height = NOISE_CELLS;
8894
+ const t = tile.getContext("2d");
8895
+ if (t) {
8896
+ t.fillStyle = color;
8897
+ for (let y = 0; y < NOISE_CELLS; y++) {
8898
+ for (let x = 0; x < NOISE_CELLS; x++) {
8899
+ t.globalAlpha = noiseHash(x, y) * alpha;
8900
+ t.fillRect(x, y, 1, 1);
8901
+ }
8902
+ }
8903
+ }
8904
+ return tile;
8905
+ }
8890
8906
  function createWebPainter(ctx, onAssetLoad) {
8891
8907
  let vw = 0;
8892
8908
  let vh = 0;
8909
+ const patternCache = /* @__PURE__ */ new Map();
8893
8910
  const tracePoly = (points, closed) => {
8894
8911
  if (points.length === 0) return;
8895
8912
  ctx.beginPath();
@@ -8897,6 +8914,31 @@ function createWebPainter(ctx, onAssetLoad) {
8897
8914
  for (let i = 1; i < points.length; i++) ctx.lineTo(points[i].x, points[i].y);
8898
8915
  if (closed) ctx.closePath();
8899
8916
  };
8917
+ const toCanvasPattern = (style) => {
8918
+ const key = JSON.stringify(style);
8919
+ let pattern = patternCache.get(key);
8920
+ if (pattern === void 0) {
8921
+ if (style.kind === "noise") {
8922
+ pattern = ctx.createPattern(makeNoiseTile(style.alpha ?? 0.12, style.color ?? "#000000"), "repeat");
8923
+ } else {
8924
+ const img = getOrLoadImage(style.url, onAssetLoad);
8925
+ if (!img) return "rgba(0,0,0,0)";
8926
+ pattern = ctx.createPattern(img, "repeat");
8927
+ }
8928
+ if (pattern && style.scale !== void 0 && style.scale !== 1) {
8929
+ pattern.setTransform(new DOMMatrix().scale(style.scale));
8930
+ }
8931
+ patternCache.set(key, pattern);
8932
+ }
8933
+ return pattern ?? "rgba(0,0,0,0)";
8934
+ };
8935
+ const toCanvasStyle = (style) => {
8936
+ if (typeof style === "string") return style;
8937
+ if (style.kind === "noise" || style.kind === "image") return toCanvasPattern(style);
8938
+ 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);
8939
+ for (const stop of style.stops) g.addColorStop(stop.offset, stop.color);
8940
+ return g;
8941
+ };
8900
8942
  return {
8901
8943
  setViewport(width, height, dpr) {
8902
8944
  vw = width;
@@ -8928,6 +8970,19 @@ function createWebPainter(ctx, onAssetLoad) {
8928
8970
  ctx.shadowColor = shadow ? shadow.color : "transparent";
8929
8971
  ctx.shadowBlur = shadow ? shadow.blur : 0;
8930
8972
  },
8973
+ setBlend(mode) {
8974
+ ctx.globalCompositeOperation = mode ?? "source-over";
8975
+ },
8976
+ setLineDash(pattern, offset = 0) {
8977
+ ctx.setLineDash(pattern ? [...pattern] : []);
8978
+ ctx.lineDashOffset = offset;
8979
+ },
8980
+ setBlur(px) {
8981
+ ctx.filter = px && px > 0 ? `blur(${px}px)` : "none";
8982
+ },
8983
+ clipPath(d) {
8984
+ ctx.clip(new Path2D(d));
8985
+ },
8931
8986
  resolveTexture(url) {
8932
8987
  const img = getOrLoadImage(url, onAssetLoad);
8933
8988
  if (!img) return null;
@@ -8950,47 +9005,47 @@ function createWebPainter(ctx, onAssetLoad) {
8950
9005
  ctx.drawImage(img, dest.x, dest.y, dw, dh);
8951
9006
  }
8952
9007
  },
8953
- fillRect(x, y, w, h, color) {
8954
- ctx.fillStyle = color;
9008
+ fillRect(x, y, w, h, style) {
9009
+ ctx.fillStyle = toCanvasStyle(style);
8955
9010
  ctx.fillRect(x, y, w, h);
8956
9011
  },
8957
- strokeRect(x, y, w, h, color, lineWidth = 1) {
8958
- ctx.strokeStyle = color;
9012
+ strokeRect(x, y, w, h, style, lineWidth = 1) {
9013
+ ctx.strokeStyle = toCanvasStyle(style);
8959
9014
  ctx.lineWidth = lineWidth;
8960
9015
  ctx.strokeRect(x, y, w, h);
8961
9016
  },
8962
- fillPoly(points, color) {
9017
+ fillPoly(points, style) {
8963
9018
  if (points.length === 0) return;
8964
9019
  tracePoly(points, true);
8965
- ctx.fillStyle = color;
9020
+ ctx.fillStyle = toCanvasStyle(style);
8966
9021
  ctx.fill();
8967
9022
  },
8968
- strokePoly(points, color, lineWidth = 1, closed = false) {
9023
+ strokePoly(points, style, lineWidth = 1, closed = false) {
8969
9024
  if (points.length === 0) return;
8970
9025
  tracePoly(points, closed);
8971
- ctx.strokeStyle = color;
9026
+ ctx.strokeStyle = toCanvasStyle(style);
8972
9027
  ctx.lineWidth = lineWidth;
8973
9028
  ctx.stroke();
8974
9029
  },
8975
- fillEllipse(cx, cy, rx, ry, color) {
9030
+ fillEllipse(cx, cy, rx, ry, style) {
8976
9031
  ctx.beginPath();
8977
9032
  ctx.ellipse(cx, cy, rx, ry, 0, 0, Math.PI * 2);
8978
- ctx.fillStyle = color;
9033
+ ctx.fillStyle = toCanvasStyle(style);
8979
9034
  ctx.fill();
8980
9035
  },
8981
- strokeEllipse(cx, cy, rx, ry, color, lineWidth = 1) {
9036
+ strokeEllipse(cx, cy, rx, ry, style, lineWidth = 1) {
8982
9037
  ctx.beginPath();
8983
9038
  ctx.ellipse(cx, cy, rx, ry, 0, 0, Math.PI * 2);
8984
- ctx.strokeStyle = color;
9039
+ ctx.strokeStyle = toCanvasStyle(style);
8985
9040
  ctx.lineWidth = lineWidth;
8986
9041
  ctx.stroke();
8987
9042
  },
8988
- fillPath(d, color) {
8989
- ctx.fillStyle = color;
9043
+ fillPath(d, style) {
9044
+ ctx.fillStyle = toCanvasStyle(style);
8990
9045
  ctx.fill(new Path2D(d));
8991
9046
  },
8992
- strokePath(d, color, lineWidth = 1) {
8993
- ctx.strokeStyle = color;
9047
+ strokePath(d, style, lineWidth = 1) {
9048
+ ctx.strokeStyle = toCanvasStyle(style);
8994
9049
  ctx.lineWidth = lineWidth;
8995
9050
  ctx.stroke(new Path2D(d));
8996
9051
  },
@@ -9003,13 +9058,18 @@ function createWebPainter(ctx, onAssetLoad) {
9003
9058
  }
9004
9059
  };
9005
9060
  }
9006
- var handleByImage, imageByHandle;
9061
+ var handleByImage, imageByHandle, noiseHash, NOISE_CELLS;
9007
9062
  var init_webPainter2d = __esm({
9008
9063
  "lib/webPainter2d.ts"() {
9009
9064
  "use client";
9010
9065
  init_imageCache();
9011
9066
  handleByImage = /* @__PURE__ */ new WeakMap();
9012
9067
  imageByHandle = /* @__PURE__ */ new WeakMap();
9068
+ noiseHash = (x, y) => {
9069
+ const s = Math.sin(x * 12.9898 + y * 78.233) * 43758.5453;
9070
+ return s - Math.floor(s);
9071
+ };
9072
+ NOISE_CELLS = 32;
9013
9073
  }
9014
9074
  });
9015
9075
 
@@ -9165,6 +9225,101 @@ var init_registry = __esm({
9165
9225
  DrawableRegistryContext = React86.createContext(null);
9166
9226
  }
9167
9227
  });
9228
+ function isAnimatedShape(node) {
9229
+ return Boolean(node.animation && node.animation.durationMs > 0 && node.animation.keyframes.length > 0);
9230
+ }
9231
+ function applyShapeAnimation(node, timeMs) {
9232
+ const anim = node.animation;
9233
+ if (!anim || !(anim.durationMs > 0) || anim.keyframes.length === 0) return node;
9234
+ const frames = [...anim.keyframes].sort((a, b) => a.at - b.at);
9235
+ const cycle = timeMs / anim.durationMs;
9236
+ const t = anim.loop === false ? Math.min(cycle, 1) : cycle - Math.floor(cycle);
9237
+ const out = { ...node };
9238
+ const trackValue = (key) => {
9239
+ const defined = frames.filter((f3) => f3[key] !== void 0);
9240
+ if (defined.length === 0) return void 0;
9241
+ let prev;
9242
+ let next;
9243
+ for (const f3 of defined) {
9244
+ if (f3.at <= t) prev = f3;
9245
+ else if (!next) next = f3;
9246
+ }
9247
+ if (!prev) return defined[0][key];
9248
+ if (!next) return prev[key];
9249
+ const span = next.at - prev.at;
9250
+ const k = span > 0 ? (t - prev.at) / span : 1;
9251
+ const a = prev[key];
9252
+ const b = next[key];
9253
+ if (typeof a === "number" && typeof b === "number") return lerp(a, b, k);
9254
+ return a;
9255
+ };
9256
+ for (const key of NUMERIC_TRACKS) {
9257
+ const v = trackValue(key);
9258
+ if (v !== void 0) out[key] = v;
9259
+ }
9260
+ const fill = trackValue("fill");
9261
+ if (fill !== void 0) out.fill = fill;
9262
+ const stroke = trackValue("stroke");
9263
+ if (stroke !== void 0) out.stroke = stroke;
9264
+ const shadowFrames = frames.filter((f3) => f3.shadow !== void 0);
9265
+ if (shadowFrames.length > 0) {
9266
+ const sh = trackValue("shadow");
9267
+ if (sh !== void 0) {
9268
+ let prevSh;
9269
+ let nextSh;
9270
+ for (const f3 of shadowFrames) {
9271
+ if (f3.at <= t) prevSh = f3;
9272
+ else if (!nextSh) nextSh = f3;
9273
+ }
9274
+ if (prevSh?.shadow && nextSh?.shadow) {
9275
+ const span = nextSh.at - prevSh.at;
9276
+ const k = span > 0 ? (t - prevSh.at) / span : 1;
9277
+ out.shadow = { color: prevSh.shadow.color, blur: lerp(prevSh.shadow.blur, nextSh.shadow.blur, k) };
9278
+ } else {
9279
+ out.shadow = sh;
9280
+ }
9281
+ }
9282
+ }
9283
+ return out;
9284
+ }
9285
+ function gradientStyle(g, ox, oy, scale) {
9286
+ if (g.kind === "linear") {
9287
+ if (!g.from || !g.to) return void 0;
9288
+ return {
9289
+ kind: "linear",
9290
+ x1: ox + g.from.x * scale,
9291
+ y1: oy + g.from.y * scale,
9292
+ x2: ox + g.to.x * scale,
9293
+ y2: oy + g.to.y * scale,
9294
+ stops: g.stops
9295
+ };
9296
+ }
9297
+ if (g.kind === "conic") {
9298
+ if (!g.center) return void 0;
9299
+ return {
9300
+ kind: "conic",
9301
+ cx: ox + g.center.x * scale,
9302
+ cy: oy + g.center.y * scale,
9303
+ angle: g.angle ?? 0,
9304
+ stops: g.stops
9305
+ };
9306
+ }
9307
+ if (!g.center || g.radius === void 0) return void 0;
9308
+ return {
9309
+ kind: "radial",
9310
+ cx: ox + g.center.x * scale,
9311
+ cy: oy + g.center.y * scale,
9312
+ r: g.radius * scale,
9313
+ stops: g.stops
9314
+ };
9315
+ }
9316
+ function patternStyle(p, unit) {
9317
+ if (p.kind === "image") {
9318
+ if (!p.url) return void 0;
9319
+ return { kind: "image", url: p.url, scale: (p.scale ?? 1) / unit };
9320
+ }
9321
+ return { kind: "noise", scale: (p.scale ?? 2) / unit, alpha: p.alpha, color: p.color };
9322
+ }
9168
9323
  function DrawShape(props) {
9169
9324
  const register = React86.useContext(DrawableRegistryContext);
9170
9325
  if (register) {
@@ -9175,25 +9330,66 @@ function DrawShape(props) {
9175
9330
  ...opacity !== void 0 && opacity > 0 ? { opacity } : {}
9176
9331
  };
9177
9332
  register(node);
9178
- return /* @__PURE__ */ jsxRuntime.jsx("div", { "data-draw-shape-debug": "", style: { position: "absolute", width: 4, height: 4, background: "red", zIndex: 9999 } });
9179
9333
  }
9180
9334
  return null;
9181
9335
  }
9182
- var paintShape;
9336
+ var NUMERIC_TRACKS, lerp, paintShape;
9183
9337
  var init_DrawShape = __esm({
9184
9338
  "components/game/atoms/DrawShape.tsx"() {
9185
9339
  "use client";
9186
9340
  init_contract();
9187
9341
  init_registry();
9188
- paintShape = (painter, node, dctx) => {
9342
+ NUMERIC_TRACKS = [
9343
+ "offsetX",
9344
+ "offsetY",
9345
+ "rotate",
9346
+ "opacity",
9347
+ "radiusX",
9348
+ "radiusY",
9349
+ "width",
9350
+ "height",
9351
+ "strokeWidth",
9352
+ "strokeDashOffset",
9353
+ "blur"
9354
+ ];
9355
+ lerp = (a, b, k) => a + (b - a) * k;
9356
+ paintShape = (painter, rawNode, dctx) => {
9357
+ const node = dctx.time > 0 ? applyShapeAnimation(rawNode, dctx.time) : rawNode;
9189
9358
  if (!isValidScenePos(node.position)) return;
9190
9359
  painter.save();
9191
9360
  if (node.opacity !== void 0 && node.opacity !== 1) painter.setAlpha(node.opacity);
9361
+ const origin = dctx.projector.project(node.position);
9362
+ const tileWidth = dctx.projector.tileWidth;
9363
+ const groupScale = dctx.groupScale ?? 1;
9364
+ const strokePx = (node.strokeWidth ?? 1) / groupScale;
9365
+ if (node.blendMode) painter.setBlend(node.blendMode);
9366
+ if (node.strokeDash && node.strokeDash.length > 0) {
9367
+ painter.setLineDash(
9368
+ node.strokeDash.map((v) => v / groupScale),
9369
+ (node.strokeDashOffset ?? 0) / groupScale
9370
+ );
9371
+ }
9372
+ if (node.blur !== void 0 && node.blur > 0) painter.setBlur(node.blur / groupScale);
9373
+ if (node.rotate) {
9374
+ const pivot = node.pivot ?? { x: 0.5, y: 0.5 };
9375
+ const px = origin.x + pivot.x * tileWidth;
9376
+ const py = origin.y + pivot.y * tileWidth;
9377
+ painter.translate(px, py);
9378
+ painter.rotate(node.rotate);
9379
+ painter.translate(-px, -py);
9380
+ }
9381
+ if (node.shadow) painter.setShadow({ color: node.shadow.color, blur: node.shadow.blur * tileWidth * groupScale });
9382
+ const fill = node.fill === "none" ? void 0 : node.fill;
9383
+ const stroke = node.stroke === "none" ? void 0 : node.stroke;
9384
+ const pxFill = node.gradient ? gradientStyle(node.gradient, origin.x, origin.y, tileWidth) ?? fill : fill;
9385
+ const pxStroke = node.strokeGradient ? gradientStyle(node.strokeGradient, origin.x, origin.y, tileWidth) ?? stroke : stroke;
9386
+ const patFill = node.fillPattern ? patternStyle(node.fillPattern, groupScale) : void 0;
9192
9387
  switch (node.shape) {
9193
9388
  case "cell": {
9194
9389
  const pts = dctx.projector.cellPath(node.position);
9195
- if (node.fill) painter.fillPoly(pts, node.fill);
9196
- if (node.stroke) painter.strokePoly(pts, node.stroke, node.strokeWidth ?? 1, true);
9390
+ if (pxFill) painter.fillPoly(pts, pxFill);
9391
+ if (patFill) painter.fillPoly(pts, patFill);
9392
+ if (pxStroke) painter.strokePoly(pts, pxStroke, strokePx, true);
9197
9393
  break;
9198
9394
  }
9199
9395
  case "rect": {
@@ -9203,8 +9399,9 @@ var init_DrawShape = __esm({
9203
9399
  const y = p.y + (node.offsetY ?? 0) * tw;
9204
9400
  const w = (node.width ?? 0) * tw;
9205
9401
  const h = (node.height ?? 0) * tw;
9206
- if (node.fill) painter.fillRect(x, y, w, h, node.fill);
9207
- if (node.stroke) painter.strokeRect(x, y, w, h, node.stroke, node.strokeWidth ?? 1);
9402
+ if (pxFill) painter.fillRect(x, y, w, h, pxFill);
9403
+ if (patFill) painter.fillRect(x, y, w, h, patFill);
9404
+ if (pxStroke) painter.strokeRect(x, y, w, h, pxStroke, strokePx);
9208
9405
  break;
9209
9406
  }
9210
9407
  case "ellipse": {
@@ -9214,26 +9411,38 @@ var init_DrawShape = __esm({
9214
9411
  const cy = p.y + (node.offsetY ?? 0) * tw;
9215
9412
  const rx = (node.radiusX ?? 0) * tw;
9216
9413
  const ry = (node.radiusY ?? rx) * tw;
9217
- if (node.fill) painter.fillEllipse(cx, cy, rx, ry, node.fill);
9218
- if (node.stroke) painter.strokeEllipse(cx, cy, rx, ry, node.stroke, node.strokeWidth ?? 1);
9414
+ if (pxFill) painter.fillEllipse(cx, cy, rx, ry, pxFill);
9415
+ if (patFill) painter.fillEllipse(cx, cy, rx, ry, patFill);
9416
+ if (pxStroke) painter.strokeEllipse(cx, cy, rx, ry, pxStroke, strokePx);
9219
9417
  break;
9220
9418
  }
9221
9419
  case "poly": {
9222
- const base = dctx.projector.project(node.position);
9223
- const tw = dctx.projector.tileWidth;
9224
- const pts = (node.points ?? []).map((pt) => ({ x: base.x + pt.x * tw, y: base.y + pt.y * tw }));
9225
- if (node.fill) painter.fillPoly(pts, node.fill);
9226
- if (node.stroke) painter.strokePoly(pts, node.stroke, node.strokeWidth ?? 1, true);
9420
+ const pts = (node.points ?? []).map((pt) => ({
9421
+ x: origin.x + ((node.offsetX ?? 0) + pt.x) * tileWidth,
9422
+ y: origin.y + ((node.offsetY ?? 0) + pt.y) * tileWidth
9423
+ }));
9424
+ if (pxFill) painter.fillPoly(pts, pxFill);
9425
+ if (patFill) painter.fillPoly(pts, patFill);
9426
+ if (pxStroke) painter.strokePoly(pts, pxStroke, strokePx, true);
9227
9427
  break;
9228
9428
  }
9229
9429
  case "path": {
9230
9430
  if (!node.d) break;
9231
- const base = dctx.projector.project(node.position);
9232
- const tw = dctx.projector.tileWidth;
9233
- painter.translate(base.x, base.y);
9234
- painter.scale(tw, tw);
9235
- if (node.fill) painter.fillPath(node.d, node.fill);
9236
- if (node.stroke) painter.strokePath(node.d, node.stroke, (node.strokeWidth ?? 1) / tw);
9431
+ painter.translate(origin.x + (node.offsetX ?? 0) * tileWidth, origin.y + (node.offsetY ?? 0) * tileWidth);
9432
+ painter.scale(tileWidth, tileWidth);
9433
+ if (node.strokeDash && node.strokeDash.length > 0) {
9434
+ painter.setLineDash(
9435
+ node.strokeDash.map((v) => v / (groupScale * tileWidth)),
9436
+ (node.strokeDashOffset ?? 0) / (groupScale * tileWidth)
9437
+ );
9438
+ }
9439
+ if (node.blur !== void 0 && node.blur > 0) painter.setBlur(node.blur / (groupScale * tileWidth));
9440
+ const localFill = node.gradient ? gradientStyle(node.gradient, 0, 0, 1) ?? fill : fill;
9441
+ const localStroke = node.strokeGradient ? gradientStyle(node.strokeGradient, 0, 0, 1) ?? stroke : stroke;
9442
+ const localPat = node.fillPattern ? patternStyle(node.fillPattern, groupScale * tileWidth) : void 0;
9443
+ if (localFill) painter.fillPath(node.d, localFill);
9444
+ if (localPat) painter.fillPath(node.d, localPat);
9445
+ if (localStroke) painter.strokePath(node.d, localStroke, strokePx / tileWidth);
9237
9446
  break;
9238
9447
  }
9239
9448
  }
@@ -9314,8 +9523,6 @@ var init_DrawTextLayer = __esm({
9314
9523
  };
9315
9524
  }
9316
9525
  });
9317
-
9318
- // lib/drawable/paintDispatch.ts
9319
9526
  function paintDrawable(painter, node, dctx) {
9320
9527
  switch (node.type) {
9321
9528
  case "draw-sprite":
@@ -9336,10 +9543,20 @@ function paintDrawable(painter, node, dctx) {
9336
9543
  if (node.scale !== void 0) painter.scale(node.scale, node.scale);
9337
9544
  if (node.rotate !== void 0) painter.rotate(node.rotate);
9338
9545
  if (node.opacity !== void 0 && node.opacity !== 1) painter.setAlpha(node.opacity);
9339
- for (const item of node.items) paintDrawable(painter, item, dctx);
9546
+ if (node.clip) {
9547
+ const tw = dctx.projector.tileWidth;
9548
+ painter.scale(tw, tw);
9549
+ painter.clipPath(node.clip);
9550
+ painter.scale(1 / tw, 1 / tw);
9551
+ }
9552
+ const childCtx = node.scale !== void 0 && node.scale !== 1 ? { ...dctx, groupScale: (dctx.groupScale ?? 1) * node.scale } : dctx;
9553
+ for (const item of node.items) paintDrawable(painter, item, childCtx);
9340
9554
  painter.restore();
9341
9555
  break;
9342
9556
  }
9557
+ case "draw-mesh":
9558
+ warnUnsupported2d("draw-mesh");
9559
+ break;
9343
9560
  case "draw-sprite-layer":
9344
9561
  paintSpriteLayer(painter, node, dctx);
9345
9562
  break;
@@ -9351,6 +9568,7 @@ function paintDrawable(painter, node, dctx) {
9351
9568
  break;
9352
9569
  }
9353
9570
  }
9571
+ var paint2dLog, warnedUnsupported2d, warnUnsupported2d;
9354
9572
  var init_paintDispatch = __esm({
9355
9573
  "lib/drawable/paintDispatch.ts"() {
9356
9574
  init_contract();
@@ -9360,6 +9578,13 @@ var init_paintDispatch = __esm({
9360
9578
  init_DrawSpriteLayer();
9361
9579
  init_DrawShapeLayer();
9362
9580
  init_DrawTextLayer();
9581
+ paint2dLog = logger.createLogger("almadar:ui:drawable-2d");
9582
+ warnedUnsupported2d = /* @__PURE__ */ new Set();
9583
+ warnUnsupported2d = (kind) => {
9584
+ if (warnedUnsupported2d.has(kind)) return;
9585
+ warnedUnsupported2d.add(kind);
9586
+ paint2dLog.warn("unsupported drawable kind on the 2D painter \u2014 skipped", { kind });
9587
+ };
9363
9588
  }
9364
9589
  });
9365
9590
 
@@ -9374,6 +9599,7 @@ function collectDrawnItems(nodes) {
9374
9599
  case "draw-shape":
9375
9600
  case "draw-text":
9376
9601
  case "draw-group":
9602
+ case "draw-mesh":
9377
9603
  if (isValidScenePos(n.position)) out.push({ pos: n.position, id: n.id });
9378
9604
  break;
9379
9605
  case "draw-sprite-layer":
@@ -9445,7 +9671,6 @@ function Canvas2D({
9445
9671
  childDrawablesRef.current.push(node);
9446
9672
  }, []);
9447
9673
  const hasJsxChildren = React86__namespace.Children.count(children) > 0;
9448
- drawables && drawables.length > 0 ? drawables : childDrawablesRef.current;
9449
9674
  function isDrawableLayer(node) {
9450
9675
  return node.type === "draw-sprite-layer" || node.type === "draw-shape-layer" || node.type === "draw-text-layer";
9451
9676
  }
@@ -9594,11 +9819,24 @@ function Canvas2D({
9594
9819
  }, [showMinimap, scenePositions]);
9595
9820
  const miniMapWidth = gridExtent.width || 10;
9596
9821
  const miniMapHeight = gridExtent.height || 10;
9597
- const draw = React86.useCallback(() => {
9822
+ const drawableIsAnimated = (node) => {
9823
+ if (node.type === "draw-shape") return isAnimatedShape(node);
9824
+ if (node.type === "draw-group") return Array.isArray(node.items) && node.items.some(drawableIsAnimated);
9825
+ if (node.type === "draw-shape-layer") return Array.isArray(node.items) && node.items.some(isAnimatedShape);
9826
+ return false;
9827
+ };
9828
+ const animRafRef = React86.useRef(0);
9829
+ const drawTimeRef = React86.useRef(() => void 0);
9830
+ const draw = React86.useCallback((timeMs = 0) => {
9598
9831
  const canvas = canvasRef.current;
9599
9832
  if (!canvas) return;
9600
9833
  const ctx = canvas.getContext("2d");
9601
9834
  if (!ctx) return;
9835
+ const scheduleAnimation = (nodes) => {
9836
+ if (!nodes.some(drawableIsAnimated)) return;
9837
+ cancelAnimationFrame(animRafRef.current);
9838
+ animRafRef.current = requestAnimationFrame(() => drawTimeRef.current(performance.now()));
9839
+ };
9602
9840
  const dpr = window.devicePixelRatio || 1;
9603
9841
  canvas.width = viewportSize.width * dpr;
9604
9842
  canvas.height = viewportSize.height * dpr;
@@ -9631,14 +9869,24 @@ function Canvas2D({
9631
9869
  if (!drawables || drawables.length === 0) {
9632
9870
  const childDrawables = childDrawablesRef.current;
9633
9871
  if (childDrawables.length === 0) return;
9872
+ const cam0 = cameraRef.current;
9873
+ if (camera !== "follow" && dragDistance() === 0) {
9874
+ const focus = cameraPos ?? defaultGridFocus;
9875
+ if (focus) {
9876
+ const p = projector.anchorPoint(focus, "center");
9877
+ cam0.x = p.x - viewportSize.width / 2;
9878
+ cam0.y = p.y - viewportSize.height / 2;
9879
+ }
9880
+ }
9634
9881
  const painter0 = createWebPainter(ctx, bumpAtlas);
9635
9882
  painter0.save();
9636
9883
  painter0.translate(viewportSize.width / 2, viewportSize.height / 2);
9637
- painter0.scale(cameraRef.current.zoom, cameraRef.current.zoom);
9638
- painter0.translate(-viewportSize.width / 2, -viewportSize.height / 2);
9639
- const dctx0 = { projector, time: 0, invalidate: bumpAtlas };
9884
+ painter0.scale(cam0.zoom, cam0.zoom);
9885
+ painter0.translate(-viewportSize.width / 2 - cam0.x, -viewportSize.height / 2 - cam0.y);
9886
+ const dctx0 = { projector, time: timeMs, invalidate: bumpAtlas };
9640
9887
  for (const node of childDrawables) paintDrawable(painter0, node, dctx0);
9641
9888
  painter0.restore();
9889
+ scheduleAnimation(childDrawables);
9642
9890
  return;
9643
9891
  }
9644
9892
  const cam = cameraRef.current;
@@ -9658,10 +9906,15 @@ function Canvas2D({
9658
9906
  painter.translate(viewportSize.width / 2, viewportSize.height / 2);
9659
9907
  painter.scale(cam.zoom, cam.zoom);
9660
9908
  painter.translate(-viewportSize.width / 2 - cam.x, -viewportSize.height / 2 - cam.y);
9661
- const dctx = { projector, time: 0, invalidate: bumpAtlas };
9909
+ const dctx = { projector, time: timeMs, invalidate: bumpAtlas };
9662
9910
  for (const node of drawables) paintDrawable(painter, node, dctx);
9663
9911
  painter.restore();
9912
+ scheduleAnimation(drawables);
9664
9913
  }, [viewportSize, backgroundImage, bgColor, drawables, projector, cameraRef, bumpAtlas, getImage, cameraPos, defaultGridFocus, camera, dragDistance]);
9914
+ React86.useEffect(() => {
9915
+ drawTimeRef.current = draw;
9916
+ }, [draw]);
9917
+ React86.useEffect(() => () => cancelAnimationFrame(animRafRef.current), []);
9665
9918
  React86.useEffect(() => {
9666
9919
  if (camera !== "follow" || !followTarget) return;
9667
9920
  const p = projector.anchorPoint(followTarget, "center");
@@ -9884,15 +10137,7 @@ function Canvas2D({
9884
10137
  mapHeight: miniMapHeight
9885
10138
  }
9886
10139
  ) }),
9887
- hasJsxChildren && /* @__PURE__ */ jsxRuntime.jsx("div", { "aria-hidden": "true", style: { position: "absolute", width: 0, height: 0, overflow: "hidden" }, children }),
9888
- /* @__PURE__ */ jsxRuntime.jsxs("div", { "data-debug": "", style: { position: "absolute", top: 0, left: 0, background: "yellow", color: "black", zIndex: 9999, fontSize: 24, padding: 8 }, children: [
9889
- "C=",
9890
- React86__namespace.Children.count(children),
9891
- " J=",
9892
- String(hasJsxChildren),
9893
- " D=",
9894
- drawables?.length ?? -1
9895
- ] })
10140
+ hasJsxChildren && /* @__PURE__ */ jsxRuntime.jsx("div", { "aria-hidden": "true", style: { position: "absolute", width: 0, height: 0, overflow: "hidden" }, children })
9896
10141
  ]
9897
10142
  }
9898
10143
  ) });
@@ -9917,6 +10162,7 @@ var init_Canvas2D = __esm({
9917
10162
  init_webPainter2d();
9918
10163
  init_projector();
9919
10164
  init_paintDispatch();
10165
+ init_DrawShape();
9920
10166
  init_registry();
9921
10167
  init_hitTest();
9922
10168
  init_isometric();
@@ -9971,6 +10217,7 @@ function Canvas({
9971
10217
  isLoading,
9972
10218
  cameraMode: to3DCameraMode(camera?.mode),
9973
10219
  ...zoom !== void 0 ? { scale: zoom } : {},
10220
+ ...camera?.fov !== void 0 ? { fov: camera.fov } : {},
9974
10221
  ...camera?.target !== void 0 ? { followTarget: camera.target } : {},
9975
10222
  unitScale,
9976
10223
  backgroundColor,
@@ -38798,14 +39045,26 @@ var init_DetailPanel = __esm({
38798
39045
  DetailPanel.displayName = "DetailPanel";
38799
39046
  }
38800
39047
  });
38801
-
38802
- // components/game/atoms/DrawGroup.tsx
38803
- function DrawGroup(_props) {
39048
+ function DrawGroup(props) {
39049
+ const register = React86.useContext(DrawableRegistryContext);
39050
+ if (register) register({ ...props, type: "draw-group" });
38804
39051
  return null;
38805
39052
  }
38806
39053
  var init_DrawGroup = __esm({
38807
39054
  "components/game/atoms/DrawGroup.tsx"() {
38808
39055
  "use client";
39056
+ init_registry();
39057
+ }
39058
+ });
39059
+ function DrawMesh(props) {
39060
+ const register = React86.useContext(DrawableRegistryContext);
39061
+ if (register) register({ ...props, type: "draw-mesh" });
39062
+ return null;
39063
+ }
39064
+ var init_DrawMesh = __esm({
39065
+ "components/game/atoms/DrawMesh.tsx"() {
39066
+ "use client";
39067
+ init_registry();
38809
39068
  }
38810
39069
  });
38811
39070
  function extractTitle(children) {
@@ -44512,6 +44771,7 @@ var init_component_registry_generated = __esm({
44512
44771
  init_DocTOC();
44513
44772
  init_DocumentViewer();
44514
44773
  init_DrawGroup();
44774
+ init_DrawMesh();
44515
44775
  init_DrawShape();
44516
44776
  init_DrawShapeLayer();
44517
44777
  init_DrawSprite();
@@ -44780,6 +45040,7 @@ var init_component_registry_generated = __esm({
44780
45040
  "DocTOC": DocTOC,
44781
45041
  "DocumentViewer": DocumentViewer,
44782
45042
  "DrawGroup": DrawGroup,
45043
+ "DrawMesh": DrawMesh,
44783
45044
  "DrawShape": DrawShape,
44784
45045
  "DrawShapeLayer": DrawShapeLayer,
44785
45046
  "DrawSprite": DrawSprite,
@@ -45688,8 +45949,13 @@ function SlotContentRenderer({
45688
45949
  const isSingleChild = typeof childrenConfig === "string" || typeof childrenConfig === "object" && childrenConfig !== null && !Array.isArray(childrenConfig) && "type" in childrenConfig;
45689
45950
  const hasChildren = PATTERNS_WITH_CHILDREN.has(content.pattern) || Array.isArray(childrenConfig) && childrenConfig.length > 0 || isSingleChild;
45690
45951
  const isDrawHost = patterns.isDrawHostPattern(content.pattern);
45952
+ const arr = Array.isArray(childrenConfig) ? childrenConfig : childrenConfig ? [childrenConfig] : [];
45953
+ const hasTraitChildren = arr.some(
45954
+ (c) => typeof c === "string" && TRAIT_BINDING_RE.test(c)
45955
+ );
45956
+ const drawHostUsesReactChildren = isDrawHost && hasTraitChildren;
45691
45957
  const myPath = patternPath ?? "root";
45692
- const renderedChildren = hasChildren && !isDrawHost ? renderPatternChildren(childrenConfig, onDismiss, content.id, myPath, content.sourceTrait, {
45958
+ const renderedChildren = hasChildren && (!isDrawHost || drawHostUsesReactChildren) ? renderPatternChildren(childrenConfig, onDismiss, content.id, myPath, content.sourceTrait, {
45693
45959
  slot: content.slot,
45694
45960
  transitionEvent: content.transitionEvent,
45695
45961
  fromState: content.fromState,
@@ -45750,7 +46016,7 @@ function SlotContentRenderer({
45750
46016
  for (const [k, v] of Object.entries(nodeSlotOverrides)) {
45751
46017
  finalProps[k] = v;
45752
46018
  }
45753
- if (isDrawHost && Array.isArray(childrenConfig) && childrenConfig.length > 0) {
46019
+ if (isDrawHost && !drawHostUsesReactChildren && Array.isArray(childrenConfig) && childrenConfig.length > 0) {
45754
46020
  finalProps.drawables = toDrawableNodes(childrenConfig);
45755
46021
  }
45756
46022
  const entityVal = finalProps.entity;
@@ -45929,6 +46195,8 @@ var init_UISlotRenderer = __esm({
45929
46195
  "vstack",
45930
46196
  "hstack",
45931
46197
  "box",
46198
+ "canvas",
46199
+ "canvas-2d",
45932
46200
  "grid",
45933
46201
  "center",
45934
46202
  "card",