@almadar/ui 6.58.0 → 6.61.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.
Files changed (94) hide show
  1. package/dist/BehaviorValueFrame-3ORZHPWQ.css +1252 -0
  2. package/dist/BehaviorValueFrame-QTD7BWOI.js +18 -0
  3. package/dist/{Canvas3DHost-BTvRPfsr.d.ts → Canvas3DHost-CNjANoaU.d.ts} +1 -1
  4. package/dist/{GameAudioProvider-kXbonWTv.d.ts → GameAudioProvider-Dv_L-PHt.d.ts} +1 -1
  5. package/dist/TraitValueFrame-2WMYJOA5.css +1252 -0
  6. package/dist/TraitValueFrame-WOXNLLMM.js +17 -0
  7. package/dist/Typography-BY4A2NBY.js +5 -0
  8. package/dist/{UISlotRenderer-N5DFYE6K.js → UISlotRenderer-EEOXSJCI.js} +10 -8
  9. package/dist/avl/index.cjs +31960 -27831
  10. package/dist/avl/index.d.ts +36 -13
  11. package/dist/avl/index.js +331 -6147
  12. package/dist/{avl-3d-context-BpBKZIbc.d.ts → avl-3d-context-Dxun3-ri.d.ts} +2 -0
  13. package/dist/{chunk-I2ZNE5TD.js → chunk-6PV4LASE.js} +9495 -639
  14. package/dist/{chunk-723MJUU3.js → chunk-6ZCU3BQN.js} +1 -1
  15. package/dist/{chunk-GZGDBS4S.js → chunk-7NIU5GDC.js} +2 -2
  16. package/dist/{chunk-YYYHH5BA.js → chunk-CB64IOYD.js} +0 -4
  17. package/dist/{chunk-MZIDOJMD.js → chunk-EIIQGOZW.js} +2 -1
  18. package/dist/chunk-F7NZ3KZQ.js +434 -0
  19. package/dist/{chunk-LZRW6GXG.js → chunk-FYX64CDL.js} +0 -2
  20. package/dist/{chunk-4ZGXIW3B.js → chunk-KZRAMPM6.js} +1 -1
  21. package/dist/{chunk-VHTJRS2N.js → chunk-L3MTZ3RH.js} +7 -0
  22. package/dist/chunk-LUREMDI3.js +49 -0
  23. package/dist/{chunk-HA7CJWQX.js → chunk-MGN6B6OQ.js} +1477 -1177
  24. package/dist/{chunk-Q47ZN2DW.js → chunk-N23SVGTH.js} +83 -1
  25. package/dist/{chunk-KJGVLXEN.js → chunk-N4IJS5DD.js} +50 -116
  26. package/dist/{chunk-I5QOYW2Z.js → chunk-UTSDPJUP.js} +205 -2
  27. package/dist/{chunk-V3FGTQUE.js → chunk-ZR45FGW3.js} +7 -1
  28. package/dist/{cn-DIql5232.d.ts → cn-CDIhHCUC.d.ts} +40 -16
  29. package/dist/components/index.cjs +14455 -4099
  30. package/dist/components/index.d.ts +165 -177
  31. package/dist/components/index.js +14 -15
  32. package/dist/context/index.cjs +16 -1
  33. package/dist/context/index.js +4 -2
  34. package/dist/hooks/index.cjs +79 -6
  35. package/dist/hooks/index.d.ts +43 -2
  36. package/dist/hooks/index.js +6 -7
  37. package/dist/lib/drawable/three/index.cjs +350 -26
  38. package/dist/lib/drawable/three/index.d.ts +5 -5
  39. package/dist/lib/drawable/three/index.js +28 -7
  40. package/dist/lib/index.cjs +90 -4
  41. package/dist/lib/index.d.ts +3 -3
  42. package/dist/lib/index.js +74 -6
  43. package/dist/locales/index.cjs +0 -6
  44. package/dist/locales/index.js +2 -2
  45. package/dist/{paintDispatch-BN8Vbzgs.d.ts → paintDispatch-Cp1_S93j.d.ts} +30 -2
  46. package/dist/providers/index.cjs +13853 -1075
  47. package/dist/providers/index.d.ts +38 -6
  48. package/dist/providers/index.js +40 -30
  49. package/dist/runtime/index.cjs +28369 -18287
  50. package/dist/runtime/index.d.ts +65 -3
  51. package/dist/runtime/index.js +13 -379
  52. package/dist/ssr/index.cjs +508 -133
  53. package/dist/ssr/index.d.cts +56 -4
  54. package/dist/ssr/index.d.ts +56 -4
  55. package/dist/ssr/index.js +446 -71
  56. package/dist/{verificationRegistry-CK76GUfH.d.ts → verificationRegistry-B5aGl7WF.d.ts} +3 -2
  57. package/locales/ar.json +0 -2
  58. package/locales/en.json +0 -2
  59. package/locales/sl.json +0 -2
  60. package/package.json +7 -7
  61. package/tailwind-preset.cjs +78 -8
  62. package/themes/_contract.md +6 -0
  63. package/themes/_fonts.css +4 -4
  64. package/themes/_foundation.css +11 -0
  65. package/themes/_presets.css +2 -0
  66. package/themes/almadar-website.css +10 -0
  67. package/themes/almadar.css +10 -0
  68. package/themes/atelier.css +10 -0
  69. package/themes/bauhaus.css +10 -0
  70. package/themes/bloomberg-dense.css +10 -0
  71. package/themes/clay.css +10 -0
  72. package/themes/corporate.css +10 -0
  73. package/themes/gazette.css +10 -0
  74. package/themes/glass.css +10 -0
  75. package/themes/kflow.css +5 -0
  76. package/themes/kiosk.css +10 -0
  77. package/themes/linear-clean.css +10 -0
  78. package/themes/material.css +10 -0
  79. package/themes/minimalist.css +10 -0
  80. package/themes/nata.css +304 -0
  81. package/themes/neon.css +10 -0
  82. package/themes/notion-editorial.css +10 -0
  83. package/themes/orb.css +10 -0
  84. package/themes/prism.css +10 -0
  85. package/themes/rawi.css +304 -0
  86. package/themes/retro.css +10 -0
  87. package/themes/risograph.css +10 -0
  88. package/themes/scandi.css +10 -0
  89. package/themes/solarpunk.css +10 -0
  90. package/themes/terminal.css +138 -128
  91. package/dist/Typography-LZTP72Y4.js +0 -5
  92. package/dist/chunk-D2E44ODB.js +0 -350
  93. package/dist/chunk-HB3D3HUJ.js +0 -2265
  94. package/dist/chunk-N7JIG57B.js +0 -138
@@ -1,12 +1,12 @@
1
1
  import { useTapReveal } from './chunk-XJTHMW4G.js';
2
2
  import { useEventBus } from './chunk-CHNC4UYE.js';
3
- import { updateAssetStatus } from './chunk-Q47ZN2DW.js';
3
+ import { isMotionEnabled, updateAssetStatus } from './chunk-N23SVGTH.js';
4
4
  import { cn, domPassthrough } from './chunk-64WACXM7.js';
5
5
  import { __publicField } from './chunk-QPXIAUJD.js';
6
6
  import { createLogger } from '@almadar/logger';
7
- import React, { useCallback, createContext, useState, useLayoutEffect, useContext } from 'react';
7
+ import React, { useRef, useCallback, createContext, useEffect, useContext, useState, useLayoutEffect } from 'react';
8
8
  import { useThemeScope, useTheme } from '@almadar/ui/context';
9
- import { jsx } from 'react/jsx-runtime';
9
+ import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
10
10
  import { renderUiEntriesOf } from '@almadar/core/patterns';
11
11
  import { DIAGRAM_TONES, isInlineTrait, DIAGRAM_TONE_COLOR_KEYS, DIAGRAM_TEXT_SIZES } from '@almadar/core';
12
12
  import { parseListenSource } from '@almadar/runtime';
@@ -223,17 +223,20 @@ var EFFECT_OPERATOR_CATEGORY = {
223
223
  "workspace/write-schema": "system",
224
224
  "workspace/write-plan": "system",
225
225
  "workspace/archive-orbital": "system",
226
- "lolo/emit-body": "system"
226
+ "behavior/catalog": "system",
227
+ "behavior/describe": "system",
228
+ "behavior/source": "system",
229
+ "program/read": "system",
230
+ "program/print": "system",
231
+ "program/eval": "system",
232
+ "program/compose": "system"
227
233
  };
228
234
  var EFFECT_FAMILY_CATEGORY = {
229
235
  agent: "system",
230
236
  os: "system",
231
237
  browser: "system",
232
238
  llm: "system",
233
- behavior: "system",
234
- validate: "control",
235
239
  session: "system",
236
- compose: "system",
237
240
  trace: "control",
238
241
  memory: "data",
239
242
  application: "system"
@@ -295,17 +298,20 @@ var EFFECT_OPERATOR_ZONE = {
295
298
  "workspace/write-schema": "system",
296
299
  "workspace/write-plan": "system",
297
300
  "workspace/archive-orbital": "system",
298
- "lolo/emit-body": "system"
301
+ "behavior/catalog": "system",
302
+ "behavior/describe": "system",
303
+ "behavior/source": "system",
304
+ "program/read": "system",
305
+ "program/print": "system",
306
+ "program/eval": "system",
307
+ "program/compose": "system"
299
308
  };
300
309
  var EFFECT_FAMILY_ZONE = {
301
310
  agent: "system",
302
311
  os: "system",
303
312
  browser: "system",
304
313
  llm: "remote",
305
- behavior: "system",
306
- validate: "flow",
307
314
  session: "system",
308
- compose: "system",
309
315
  trace: "flow",
310
316
  memory: "store",
311
317
  application: "system"
@@ -328,7 +334,12 @@ function effectCategoryOf(head) {
328
334
  function asEffectOperator(head) {
329
335
  return isEffectOperator(head) ? head : null;
330
336
  }
337
+ var PENDING_STD_CATEGORY_COLORS = {
338
+ "std-behavior-value": avlBlend("var(--color-accent)", 40, "var(--color-success)"),
339
+ "std-program": avlBlend("var(--color-accent)", 60, "var(--color-info)")
340
+ };
331
341
  var OPERATOR_CATEGORY_COLORS = {
342
+ ...PENDING_STD_CATEGORY_COLORS,
332
343
  "arithmetic": "var(--color-primary)",
333
344
  "comparison": "var(--color-warning)",
334
345
  "logic": "var(--color-accent)",
@@ -383,6 +394,44 @@ function effectZoneOf(head) {
383
394
  return null;
384
395
  }
385
396
 
397
+ // lib/bandShapes.ts
398
+ var shape = (d) => `url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1440 100' preserveAspectRatio='none'><path d='${d}'/></svg>")`;
399
+ var BAND_SHAPES = {
400
+ wave: shape("M0 60 C240 100 480 100 720 60 C960 20 1200 20 1440 60 V100 H0 Z"),
401
+ arc: shape("M0 100 Q720 0 1440 100 Z"),
402
+ tilt: shape("M0 100 L1440 0 V100 Z"),
403
+ step: shape("M0 100 V75 H360 V50 H720 V25 H1080 V0 H1440 V100 Z"),
404
+ scallop: shape("M0 100 V40 a60 40 0 0 1 120 0 a60 40 0 0 1 120 0 a60 40 0 0 1 120 0 a60 40 0 0 1 120 0 a60 40 0 0 1 120 0 a60 40 0 0 1 120 0 a60 40 0 0 1 120 0 a60 40 0 0 1 120 0 a60 40 0 0 1 120 0 a60 40 0 0 1 120 0 a60 40 0 0 1 120 0 a60 40 0 0 1 120 0 V100 Z")
405
+ };
406
+ var BAND_EDGE_HEIGHT = "clamp(24px, 6vw, 96px)";
407
+ var BAND_ACCENT_FALLBACK = "linear-gradient(180deg, color-mix(in oklab, var(--color-primary) 12%, var(--color-background)), var(--color-background))";
408
+ var BAND_EDGE_FILL = {
409
+ background: "var(--surface-page-image, none), var(--color-background)",
410
+ muted: "var(--color-muted)",
411
+ surface: "var(--color-card)",
412
+ gradient: `var(--surface-accent-image, ${BAND_ACCENT_FALLBACK}), var(--color-muted)`,
413
+ inverse: "var(--color-foreground)"
414
+ };
415
+ function bandEdgeMask(edge) {
416
+ return edge === "curve" ? "var(--surface-edge-mask, none)" : BAND_SHAPES[edge];
417
+ }
418
+ function bandEdgeHeight(edge) {
419
+ return edge === "curve" ? `var(--surface-edge-height, ${BAND_EDGE_HEIGHT})` : BAND_EDGE_HEIGHT;
420
+ }
421
+ function bandEdgeLayerStyle(edge, side, flip, color) {
422
+ const mask = bandEdgeMask(edge);
423
+ const turn = side === "top" ? flip ? "scaleY(-1)" : "rotate(180deg)" : flip ? "scaleX(-1)" : void 0;
424
+ const style = { height: bandEdgeHeight(edge), background: BAND_EDGE_FILL[color], maskImage: mask, WebkitMaskImage: mask, transform: turn };
425
+ if (color === "background") {
426
+ style.backgroundSize = "var(--surface-page-image-size, auto), auto";
427
+ style.backgroundAttachment = "fixed";
428
+ }
429
+ return style;
430
+ }
431
+ function bandPadding(basePad, edge) {
432
+ return edge === "none" ? void 0 : `calc(${basePad} + ${bandEdgeHeight(edge)})`;
433
+ }
434
+
386
435
  // lib/pressable.ts
387
436
  function clickElement(el) {
388
437
  el.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true }));
@@ -421,7 +470,215 @@ function rowOpenControlProps(enabled) {
421
470
  var STRETCHED_CONTROL = "after:absolute after:inset-0 after:content-[''] after:rounded-[inherit] focus-visible:outline-none focus-visible:after:ring-2 focus-visible:after:ring-primary focus-visible:after:ring-inset";
422
471
  var STRETCHED_ABOVE = "relative z-[1]";
423
472
 
424
- // components/core/atoms/Box.tsx
473
+ // lib/motion-tokens.ts
474
+ function cssTimeMs(value) {
475
+ const v = value.trim();
476
+ if (v.endsWith("ms")) return Number.parseFloat(v);
477
+ if (v.endsWith("s")) return Number.parseFloat(v) * 1e3;
478
+ return Number.NaN;
479
+ }
480
+ function tokenAt(el, name) {
481
+ for (let at = el; at !== null; at = at.parentElement) {
482
+ const value = getComputedStyle(at).getPropertyValue(name).trim();
483
+ if (value !== "") return value;
484
+ }
485
+ return "";
486
+ }
487
+ function motionTiming(durationToken, easingToken, scope) {
488
+ const duration = cssTimeMs(tokenAt(scope ?? document.documentElement, durationToken));
489
+ const easing = tokenAt(scope ?? document.documentElement, easingToken);
490
+ const reduced = typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
491
+ if (!isMotionEnabled() || reduced || !Number.isFinite(duration) || easing === "") return void 0;
492
+ return { duration, easing };
493
+ }
494
+ function layoutScale(el) {
495
+ const layoutWidth = el instanceof HTMLElement ? el.offsetWidth : 0;
496
+ const screenWidth = el.getBoundingClientRect().width;
497
+ return layoutWidth > 0 && screenWidth > 0 ? screenWidth / layoutWidth : 1;
498
+ }
499
+
500
+ // lib/animate-changes.ts
501
+ function ownMotion(el) {
502
+ return el.closest("[data-item-move]") !== null || /\balmadar-enter-/.test(el.getAttribute("class") ?? "");
503
+ }
504
+ function boxesOf(el) {
505
+ return getComputedStyle(el).display === "contents" ? boxedChildren(el) : [el];
506
+ }
507
+ function boxedChildren(el) {
508
+ return [...el.children].flatMap(boxesOf);
509
+ }
510
+ var GHOST_STYLE = ["font", "letterSpacing", "lineHeight", "color", "textAlign", "whiteSpace", "textTransform"];
511
+ function roll(el, oldText, timing) {
512
+ const box = el.getBoundingClientRect();
513
+ const scale = layoutScale(el);
514
+ const computed = getComputedStyle(el);
515
+ const holder = document.createElement("span");
516
+ holder.setAttribute("aria-hidden", "true");
517
+ Object.assign(holder.style, {
518
+ position: "fixed",
519
+ left: `${box.left}px`,
520
+ top: `${box.top}px`,
521
+ width: `${box.width / scale}px`,
522
+ height: `${box.height / scale}px`,
523
+ transform: scale === 1 ? "" : `scale(${scale})`,
524
+ transformOrigin: "top left",
525
+ pointerEvents: "none",
526
+ zIndex: "2147483647"
527
+ });
528
+ const ghost = document.createElement("span");
529
+ ghost.setAttribute("data-change-ghost", "");
530
+ ghost.setAttribute("aria-hidden", "true");
531
+ ghost.textContent = oldText;
532
+ for (const prop of GHOST_STYLE) ghost.style[prop] = computed[prop];
533
+ Object.assign(ghost.style, { display: "block", width: "100%", height: "100%", overflow: "visible" });
534
+ holder.appendChild(ghost);
535
+ document.body.appendChild(holder);
536
+ const out = ghost.animate(
537
+ [{ opacity: 1, transform: "none" }, { opacity: 0, transform: "translateY(-40%)", offset: 0.5 }, { opacity: 0, transform: "translateY(-40%)" }],
538
+ timing
539
+ );
540
+ if (out === void 0) holder.remove();
541
+ else {
542
+ out.addEventListener("finish", () => holder.remove());
543
+ out.addEventListener("cancel", () => holder.remove());
544
+ }
545
+ el.animate(
546
+ [{ opacity: 0, transform: "translateY(40%)" }, { opacity: 0, transform: "translateY(40%)", offset: 0.4 }, { opacity: 1, transform: "none" }],
547
+ timing
548
+ );
549
+ }
550
+ var textOnly = (el) => el instanceof HTMLElement && el.childNodes.length > 0 && [...el.childNodes].every((n) => n.nodeType === Node.TEXT_NODE);
551
+ function settle(root, added, changed) {
552
+ const timing = motionTiming("--duration-normal", "--easing-emphasized", root);
553
+ if (timing === void 0) return;
554
+ for (const el of added) {
555
+ if (ownMotion(el) || el.parentElement !== null && [...added].some((other) => other !== el && other.contains(el))) continue;
556
+ for (const box of boxesOf(el)) {
557
+ if (ownMotion(box)) continue;
558
+ box.animate([{ opacity: 0, transform: "translateY(6px)" }, { opacity: 1, transform: "none" }], timing);
559
+ }
560
+ }
561
+ for (const [el, oldText] of changed) {
562
+ if (ownMotion(el) || [...added].some((a) => a.contains(el))) continue;
563
+ if (oldText !== void 0 && oldText !== el.textContent && textOnly(el)) roll(el, oldText, timing);
564
+ else el.animate([{ opacity: 0.25 }, { opacity: 1 }], timing);
565
+ }
566
+ }
567
+ function blocks(root) {
568
+ const seen = /* @__PURE__ */ new Set();
569
+ for (const wrapper of root.querySelectorAll("[data-pattern]")) {
570
+ for (const box of boxesOf(wrapper)) if (!ownMotion(box)) seen.add(box);
571
+ }
572
+ return [...seen];
573
+ }
574
+ function glidePushed(root, places) {
575
+ const timing = motionTiming("--duration-normal", "--easing-emphasized", root);
576
+ const origin = root.getBoundingClientRect();
577
+ const applied = /* @__PURE__ */ new Map();
578
+ const present = /* @__PURE__ */ new Set();
579
+ for (const el of blocks(root)) {
580
+ present.add(el);
581
+ if ((el.getAnimations?.() ?? []).length > 0) {
582
+ places.delete(el);
583
+ continue;
584
+ }
585
+ const box = el.getBoundingClientRect();
586
+ const now = { x: box.left - origin.left, y: box.top - origin.top };
587
+ const was = places.get(el);
588
+ places.set(el, now);
589
+ if (was === void 0 || timing === void 0) continue;
590
+ let inherited = { x: 0, y: 0 };
591
+ for (let up = el.parentElement; up !== null && up !== root; up = up.parentElement) {
592
+ const a = applied.get(up);
593
+ if (a !== void 0) {
594
+ inherited = a;
595
+ break;
596
+ }
597
+ }
598
+ const dx = was.x - now.x - inherited.x;
599
+ const dy = was.y - now.y - inherited.y;
600
+ if (Math.abs(dx) < 0.5 && Math.abs(dy) < 0.5) {
601
+ if (inherited.x !== 0 || inherited.y !== 0) applied.set(el, inherited);
602
+ continue;
603
+ }
604
+ const scale = layoutScale(el);
605
+ el.animate([{ transform: `translate(${dx / scale}px, ${dy / scale}px)` }, { transform: "none" }], { ...timing, composite: "add" });
606
+ applied.set(el, { x: inherited.x + dx, y: inherited.y + dy });
607
+ }
608
+ for (const el of places.keys()) if (!present.has(el)) places.delete(el);
609
+ }
610
+ function easeHeight(root) {
611
+ if (typeof ResizeObserver === "undefined") return () => void 0;
612
+ let rest;
613
+ let glide;
614
+ const sizes = new ResizeObserver(() => {
615
+ const scale = layoutScale(root);
616
+ const shown = root.getBoundingClientRect().height / scale;
617
+ const gliding = glide !== void 0 && glide.playState === "running";
618
+ glide?.cancel();
619
+ glide = void 0;
620
+ const height = root.getBoundingClientRect().height / scale;
621
+ const from = gliding ? shown : rest;
622
+ rest = height;
623
+ const timing = motionTiming("--duration-normal", "--easing-emphasized", root);
624
+ if (from === void 0 || timing === void 0 || Math.abs(from - height) < 1) return;
625
+ glide = root.animate(
626
+ [{ height: `${from}px`, overflow: "clip" }, { height: `${height}px`, overflow: "clip" }],
627
+ timing
628
+ ) ?? void 0;
629
+ });
630
+ let watched = [];
631
+ const watch = () => {
632
+ const next = boxedChildren(root);
633
+ if (next.length === watched.length && next.every((el, i) => el === watched[i])) return;
634
+ watched = next;
635
+ sizes.disconnect();
636
+ for (const el of next) sizes.observe(el);
637
+ };
638
+ watch();
639
+ const children = new MutationObserver(watch);
640
+ children.observe(root, { childList: true, subtree: true });
641
+ return () => {
642
+ children.disconnect();
643
+ sizes.disconnect();
644
+ };
645
+ }
646
+ function useAnimateChanges(ref, enabled) {
647
+ useEffect(() => {
648
+ const root = ref.current;
649
+ if (!enabled || root === null) return;
650
+ return easeHeight(root);
651
+ }, [ref, enabled]);
652
+ useEffect(() => {
653
+ const root = ref.current;
654
+ if (!enabled || root === null || typeof MutationObserver === "undefined") return;
655
+ const observer = new MutationObserver((records) => {
656
+ const added = /* @__PURE__ */ new Set();
657
+ const changed = /* @__PURE__ */ new Map();
658
+ for (const record of records) {
659
+ if (record.type === "childList") {
660
+ for (const node of record.addedNodes) {
661
+ if (node instanceof Element) added.add(node);
662
+ else if (node.nodeType === Node.TEXT_NODE && node.parentElement !== null && changed.get(node.parentElement) === void 0) {
663
+ const removed = [...record.removedNodes].filter((n) => n.nodeType === Node.TEXT_NODE);
664
+ changed.set(node.parentElement, removed.length > 0 ? removed.map((n) => n.textContent ?? "").join("") : void 0);
665
+ }
666
+ }
667
+ } else if (record.type === "characterData") {
668
+ const el = record.target.parentElement;
669
+ if (el === null || changed.get(el) !== void 0) continue;
670
+ changed.set(el, [...el.childNodes].map((n) => n === record.target ? record.oldValue ?? "" : n.textContent ?? "").join(""));
671
+ }
672
+ }
673
+ glidePushed(root, places);
674
+ settle(root, added, changed);
675
+ });
676
+ const places = /* @__PURE__ */ new Map();
677
+ glidePushed(root, places);
678
+ observer.observe(root, { subtree: true, childList: true, characterData: true, characterDataOldValue: true });
679
+ return () => observer.disconnect();
680
+ }, [ref, enabled]);
681
+ }
425
682
  var paddingStyles = {
426
683
  none: "p-0",
427
684
  xs: "p-1",
@@ -486,7 +743,18 @@ var bgStyles = {
486
743
  muted: "bg-muted text-foreground",
487
744
  accent: "bg-accent text-accent-foreground",
488
745
  surface: "bg-card",
489
- overlay: "bg-card/80 backdrop-blur-sm"
746
+ overlay: "bg-card/80 backdrop-blur-sm",
747
+ gradient: "band-gradient",
748
+ inverse: "band-inverse"
749
+ };
750
+ var paddingRem = {
751
+ none: "0rem",
752
+ xs: "0.25rem",
753
+ sm: "0.5rem",
754
+ md: "1rem",
755
+ lg: "1.5rem",
756
+ xl: "2rem",
757
+ "2xl": "3rem"
490
758
  };
491
759
  var roundedStyles = {
492
760
  none: "rounded-none",
@@ -549,6 +817,13 @@ var Box = React.forwardRef(
549
817
  hoverEvent,
550
818
  tapReveal = true,
551
819
  maxWidth,
820
+ edgeTop = "none",
821
+ edgeBottom = "none",
822
+ edgeColor = "background",
823
+ edgeFlip = false,
824
+ texture = false,
825
+ animateChanges = false,
826
+ backdrop,
552
827
  onClick,
553
828
  onMouseEnter,
554
829
  onMouseLeave,
@@ -556,6 +831,13 @@ var Box = React.forwardRef(
556
831
  ...rest
557
832
  }, ref) => {
558
833
  const eventBus = useEventBus();
834
+ const ownRef = useRef(null);
835
+ useAnimateChanges(ownRef, animateChanges);
836
+ const setRef = useCallback((node) => {
837
+ ownRef.current = node;
838
+ if (typeof ref === "function") ref(node);
839
+ else if (ref !== null) ref.current = node;
840
+ }, [ref]);
559
841
  const { override: themeOverride } = useThemeScope();
560
842
  const declaredTheme = rest["data-theme"];
561
843
  const dataTheme = declaredTheme !== void 0 && themeOverride ? themeOverride : declaredTheme;
@@ -591,12 +873,25 @@ var Box = React.forwardRef(
591
873
  if (hoverEvent && tapReveal) triggerProps.onPointerDown(e);
592
874
  onPointerDown?.(e);
593
875
  }, [hoverEvent, tapReveal, triggerProps, onPointerDown]);
876
+ const isBand = backdrop != null || texture || edgeTop !== "none" || edgeBottom !== "none";
877
+ const padKey = paddingY ?? padding;
878
+ const bandStyle = {};
879
+ const padTop = padKey ? bandPadding(paddingRem[padKey], edgeTop) : void 0;
880
+ const padBottom = padKey ? bandPadding(paddingRem[padKey], edgeBottom) : void 0;
881
+ if (padTop) bandStyle.paddingTop = padTop;
882
+ if (padBottom) bandStyle.paddingBottom = padBottom;
883
+ const bandLayers = isBand ? /* @__PURE__ */ jsxs(Fragment, { children: [
884
+ backdrop != null && /* @__PURE__ */ jsx(Box, { as: "span", "data-band-layer": "", "aria-hidden": true, className: "band-backdrop", children: backdrop }),
885
+ texture && /* @__PURE__ */ jsx(Box, { as: "span", "data-band-layer": "", "aria-hidden": true, className: "band-texture" }),
886
+ edgeTop !== "none" && /* @__PURE__ */ jsx(Box, { as: "span", "data-band-layer": "", "aria-hidden": true, className: "band-edge band-edge-top", style: bandEdgeLayerStyle(edgeTop, "top", edgeFlip, edgeColor) }),
887
+ edgeBottom !== "none" && /* @__PURE__ */ jsx(Box, { as: "span", "data-band-layer": "", "aria-hidden": true, className: "band-edge band-edge-bottom", style: bandEdgeLayerStyle(edgeBottom, "bottom", edgeFlip, edgeColor) })
888
+ ] }) : null;
594
889
  const isClickable = action || onClick;
595
890
  const actionControl = action && rest.role === void 0 && rest.tabIndex === void 0 ? pressableProps(handleClick) : {};
596
891
  return React.createElement(
597
892
  Component,
598
893
  {
599
- ref,
894
+ ref: setRef,
600
895
  className: cn(
601
896
  padding && paddingStyles[padding],
602
897
  paddingX && paddingXStyles[paddingX],
@@ -614,6 +909,7 @@ var Box = React.forwardRef(
614
909
  overflow && overflowStyles[overflow],
615
910
  position && positionStyles[position],
616
911
  isClickable && "cursor-pointer",
912
+ isBand && "band",
617
913
  className
618
914
  ),
619
915
  onClick: isClickable ? handleClick : void 0,
@@ -621,11 +917,15 @@ var Box = React.forwardRef(
621
917
  onMouseEnter: hoverEvent || onMouseEnter ? handleMouseEnter : void 0,
622
918
  onMouseLeave: hoverEvent || onMouseLeave ? handleMouseLeave : void 0,
623
919
  onPointerDown: hoverEvent && tapReveal || onPointerDown ? handlePointerDown : void 0,
624
- style: maxWidth ? { maxWidth, ...rest.style } : rest.style,
920
+ style: maxWidth || isBand ? { ...maxWidth ? { maxWidth } : {}, ...bandStyle, ...rest.style } : rest.style,
625
921
  ...rest,
626
- "data-theme": dataTheme
922
+ "data-theme": dataTheme,
923
+ "data-motion": animateChanges ? "presentation" : void 0
627
924
  },
628
- children
925
+ isBand ? /* @__PURE__ */ jsxs(Fragment, { children: [
926
+ bandLayers,
927
+ children
928
+ ] }) : children
629
929
  );
630
930
  }
631
931
  );
@@ -1000,560 +1300,592 @@ function parseTransitionLevel(schema, orbitalName, traitName, transitionIndex) {
1000
1300
  };
1001
1301
  }
1002
1302
 
1003
- // lib/atlasSlice.ts
1004
- var atlasCache = /* @__PURE__ */ new Map();
1005
- function isTilesheet(a) {
1006
- return typeof a.tileWidth === "number";
1303
+ // lib/drawable/contract.ts
1304
+ function isValidScenePos(pos) {
1305
+ return Number.isFinite(pos?.x) && Number.isFinite(pos?.y);
1007
1306
  }
1008
- function isSpriteSheetAtlas(a) {
1009
- const s = a;
1010
- return typeof s.frameWidth === "number" && typeof s.frameHeight === "number" && typeof s.animations === "object";
1307
+ function spriteAssetState(node) {
1308
+ if (!isValidScenePos(node.position)) return "invalid-position";
1309
+ return typeof node.asset?.url === "string" && node.asset.url.length > 0 ? "ready" : "asset-unset";
1011
1310
  }
1012
- function getAtlas(url, onReady) {
1013
- if (atlasCache.has(url)) return atlasCache.get(url) ?? void 0;
1014
- atlasCache.set(url, void 0);
1015
- fetch(url).then((r) => r.json()).then((json) => {
1016
- atlasCache.set(url, json);
1017
- onReady();
1018
- }).catch(() => {
1019
- atlasCache.set(url, null);
1020
- onReady();
1021
- });
1022
- return void 0;
1311
+ var SPRITE_FALLBACK_FILL = "#9b8f7f";
1312
+ var SPRITE_FALLBACK_STROKE = "#5e564b";
1313
+ function spriteRect(projector, node, natural) {
1314
+ const tw = projector.tileWidth;
1315
+ const fallbackW = projector.worldPixelDirect ? natural?.w ?? 0 : tw;
1316
+ const fallbackH = projector.worldPixelDirect ? natural?.h ?? 0 : tw;
1317
+ const w = node.width !== void 0 ? node.width * tw : fallbackW;
1318
+ const h = node.height !== void 0 ? node.height * tw : fallbackH;
1319
+ const anchor = node.anchor ?? "top-left";
1320
+ const p = projector.anchorPoint(node.position, anchor);
1321
+ return {
1322
+ x: anchor === "top-left" ? p.x : p.x - w / 2,
1323
+ y: anchor === "ground" ? p.y - h : anchor === "center" ? p.y - h / 2 : p.y,
1324
+ w,
1325
+ h
1326
+ };
1023
1327
  }
1024
- function atlasFailed(url) {
1025
- return atlasCache.get(url) === null;
1328
+ var DrawableRegistryContext = createContext(null);
1329
+ var NUMERIC_TRACKS = [
1330
+ "offsetX",
1331
+ "offsetY",
1332
+ "rotate",
1333
+ "opacity",
1334
+ "radiusX",
1335
+ "radiusY",
1336
+ "width",
1337
+ "height",
1338
+ "strokeWidth",
1339
+ "strokeDashOffset",
1340
+ "blur"
1341
+ ];
1342
+ function isAnimatedShape(node) {
1343
+ return Boolean(node.animation && node.animation.durationMs > 0 && node.animation.keyframes.length > 0);
1026
1344
  }
1027
- function subRectFor(atlas, sprite) {
1028
- if (isTilesheet(atlas)) {
1029
- let col;
1030
- let row;
1031
- if (sprite.includes(",")) {
1032
- const [c, r] = sprite.split(",").map((n) => Number(n.trim()));
1033
- col = c;
1034
- row = r;
1035
- } else {
1036
- const i = Number(sprite);
1037
- if (!Number.isFinite(i)) return null;
1038
- col = i % atlas.columns;
1039
- row = Math.floor(i / atlas.columns);
1345
+ var lerp = (a, b, k) => a + (b - a) * k;
1346
+ function applyShapeAnimation(node, timeMs) {
1347
+ const anim = node.animation;
1348
+ if (!anim || !(anim.durationMs > 0) || anim.keyframes.length === 0) return node;
1349
+ const frames = [...anim.keyframes].sort((a, b) => a.at - b.at);
1350
+ const cycle = timeMs / anim.durationMs;
1351
+ const t = anim.loop === false ? Math.min(cycle, 1) : cycle - Math.floor(cycle);
1352
+ const out = { ...node };
1353
+ const trackValue = (key) => {
1354
+ const defined = frames.filter((f) => f[key] !== void 0);
1355
+ if (defined.length === 0) return void 0;
1356
+ let prev;
1357
+ let next;
1358
+ for (const f of defined) {
1359
+ if (f.at <= t) prev = f;
1360
+ else if (!next) next = f;
1040
1361
  }
1041
- if (!Number.isFinite(col) || !Number.isFinite(row) || col < 0 || row < 0 || col >= atlas.columns || row >= atlas.rows) return null;
1042
- const margin = atlas.margin ?? 0;
1043
- const spacing = atlas.spacing ?? 0;
1362
+ if (!prev) return defined[0][key];
1363
+ if (!next) return prev[key];
1364
+ const span = next.at - prev.at;
1365
+ const k = span > 0 ? (t - prev.at) / span : 1;
1366
+ const a = prev[key];
1367
+ const b = next[key];
1368
+ if (typeof a === "number" && typeof b === "number") return lerp(a, b, k);
1369
+ return a;
1370
+ };
1371
+ for (const key of NUMERIC_TRACKS) {
1372
+ const v = trackValue(key);
1373
+ if (v !== void 0) out[key] = v;
1374
+ }
1375
+ const fill = trackValue("fill");
1376
+ if (fill !== void 0) out.fill = fill;
1377
+ const stroke = trackValue("stroke");
1378
+ if (stroke !== void 0) out.stroke = stroke;
1379
+ const shadowFrames = frames.filter((f) => f.shadow !== void 0);
1380
+ if (shadowFrames.length > 0) {
1381
+ const sh = trackValue("shadow");
1382
+ if (sh !== void 0) {
1383
+ let prevSh;
1384
+ let nextSh;
1385
+ for (const f of shadowFrames) {
1386
+ if (f.at <= t) prevSh = f;
1387
+ else if (!nextSh) nextSh = f;
1388
+ }
1389
+ if (prevSh?.shadow && nextSh?.shadow) {
1390
+ const span = nextSh.at - prevSh.at;
1391
+ const k = span > 0 ? (t - prevSh.at) / span : 1;
1392
+ out.shadow = { color: prevSh.shadow.color, blur: lerp(prevSh.shadow.blur, nextSh.shadow.blur, k) };
1393
+ } else {
1394
+ out.shadow = sh;
1395
+ }
1396
+ }
1397
+ }
1398
+ return out;
1399
+ }
1400
+ function gradientStyle(g, ox, oy, scale) {
1401
+ if (g.kind === "linear") {
1402
+ if (!g.from || !g.to) return void 0;
1044
1403
  return {
1045
- sx: margin + col * (atlas.tileWidth + spacing),
1046
- sy: margin + row * (atlas.tileHeight + spacing),
1047
- sw: atlas.tileWidth,
1048
- sh: atlas.tileHeight
1404
+ kind: "linear",
1405
+ x1: ox + g.from.x * scale,
1406
+ y1: oy + g.from.y * scale,
1407
+ x2: ox + g.to.x * scale,
1408
+ y2: oy + g.to.y * scale,
1409
+ stops: g.stops
1049
1410
  };
1050
1411
  }
1051
- if (isSpriteSheetAtlas(atlas)) return null;
1052
- const st = atlas.subTextures[sprite];
1053
- if (!st) return null;
1054
- return { sx: st.x, sy: st.y, sw: st.width, sh: st.height };
1055
- }
1056
- function atlasUrlOf(asset) {
1057
- return typeof asset?.atlas === "string" && asset.atlas.length > 0 ? asset.atlas : void 0;
1058
- }
1059
- function isAtlasAsset(asset) {
1060
- return atlasUrlOf(asset) !== void 0 && typeof asset?.sprite === "string" && asset.sprite.length > 0;
1061
- }
1062
- function resolveAssetSource(img, asset, onReady) {
1063
- if (isAtlasAsset(asset)) {
1064
- const atlas = getAtlas(asset.atlas, onReady);
1065
- if (!atlas) return null;
1066
- const rect = subRectFor(atlas, asset.sprite);
1067
- if (!rect) return null;
1068
- return { img, rect, aspect: rect.sw / rect.sh };
1412
+ if (g.kind === "conic") {
1413
+ if (!g.center) return void 0;
1414
+ return {
1415
+ kind: "conic",
1416
+ cx: ox + g.center.x * scale,
1417
+ cy: oy + g.center.y * scale,
1418
+ angle: g.angle ?? 0,
1419
+ stops: g.stops
1420
+ };
1069
1421
  }
1070
- const natW = img.naturalWidth || 1;
1071
- const natH = img.naturalHeight || 1;
1072
- return { img, rect: null, aspect: natW / natH };
1073
- }
1074
- function blit(ctx, src, dx, dy, dw, dh) {
1075
- if (src.rect) ctx.drawImage(src.img, src.rect.sx, src.rect.sy, src.rect.sw, src.rect.sh, dx, dy, dw, dh);
1076
- else ctx.drawImage(src.img, dx, dy, dw, dh);
1077
- }
1078
-
1079
- // components/core/atoms/fx.ts
1080
- function resolveFxView(item, presets) {
1081
- const row = presets?.find((p) => p.type === item.type);
1082
- if (!row) return item;
1422
+ if (!g.center || g.radius === void 0) return void 0;
1083
1423
  return {
1084
- ...item,
1085
- space: item.space ?? row.space,
1086
- effect: item.effect ?? row.effect,
1087
- color: item.color ?? row.color,
1088
- size: item.size ?? row.size,
1089
- vx: item.vx ?? row.vx,
1090
- vy: item.vy ?? row.vy,
1091
- vz: item.vz ?? row.vz,
1092
- particleCount: item.particleCount ?? row.particleCount,
1093
- animation: item.animation ?? row.animation,
1094
- shape: row.shape,
1095
- count: row.count,
1096
- glow: row.glow,
1097
- gravity: row.gravity,
1098
- color2: row.color2
1424
+ kind: "radial",
1425
+ cx: ox + g.center.x * scale,
1426
+ cy: oy + g.center.y * scale,
1427
+ r: g.radius * scale,
1428
+ stops: g.stops
1099
1429
  };
1100
1430
  }
1101
- function fxLifecycle(item, epochNowMs, tickMs) {
1102
- const maxTtl = Math.max(item.maxTtl ?? item.ttl, item.ttl, 1);
1103
- const lifeMs = maxTtl * tickMs;
1104
- const ageMs = item.bornAt !== void 0 && epochNowMs > 0 ? Math.max(0, epochNowMs - item.bornAt) : (1 - item.ttl / maxTtl) * lifeMs;
1105
- const progress = Math.min(1, Math.max(0, ageMs / lifeMs));
1106
- return { lifeMs, ageMs, progress, fade: 1 - progress };
1107
- }
1108
- function fxHash01(seed, salt) {
1109
- let h = 2166136261 ^ salt;
1110
- for (let i = 0; i < seed.length; i++) {
1111
- h ^= seed.charCodeAt(i);
1112
- h = Math.imul(h, 16777619);
1431
+ function patternStyle(p, unit) {
1432
+ if (p.kind === "image") {
1433
+ if (!p.url) return void 0;
1434
+ return { kind: "image", url: p.url, scale: (p.scale ?? 1) / unit };
1113
1435
  }
1114
- h ^= h >>> 13;
1115
- h = Math.imul(h, 1274126177);
1116
- h ^= h >>> 16;
1117
- return (h >>> 0) / 4294967296;
1118
- }
1119
- var CONFETTI_COLORS = [
1120
- "var(--color-primary)",
1121
- "var(--color-success)",
1122
- "var(--color-warning)",
1123
- "var(--color-error)",
1124
- "gold",
1125
- "dodgerblue"
1126
- ];
1127
- function createConfettiParticles(count, seed) {
1128
- return Array.from({ length: count }, (_, i) => ({
1129
- id: i,
1130
- color: CONFETTI_COLORS[Math.floor(fxHash01(seed, i * 7 + 1) * CONFETTI_COLORS.length)],
1131
- left: 30 + fxHash01(seed, i * 7 + 2) * 40,
1132
- delay: fxHash01(seed, i * 7 + 3) * 300,
1133
- angle: fxHash01(seed, i * 7 + 4) * 360,
1134
- distance: 40 + fxHash01(seed, i * 7 + 5) * 80,
1135
- rotation: fxHash01(seed, i * 7 + 6) * 720 - 360,
1136
- size: 4 + fxHash01(seed, i * 7 + 7) * 6
1137
- }));
1436
+ return { kind: "noise", scale: (p.scale ?? 2) / unit, alpha: p.alpha, color: p.color };
1138
1437
  }
1139
- var CONFETTI_BURST_KEYFRAMES = `
1140
- @keyframes confetti-burst {
1141
- 0% {
1142
- opacity: 1;
1143
- transform: translate(0, 0) rotate(0deg) scale(1);
1438
+ var paintShape = (painter, rawNode, dctx) => {
1439
+ const node = dctx.time > 0 ? applyShapeAnimation(rawNode, dctx.time) : rawNode;
1440
+ if (!isValidScenePos(node.position)) return;
1441
+ painter.save();
1442
+ if (node.opacity !== void 0 && node.opacity !== 1) painter.setAlpha(node.opacity);
1443
+ const origin = dctx.projector.project(node.position);
1444
+ const tileWidth = dctx.projector.tileWidth;
1445
+ const groupScale = dctx.groupScale ?? 1;
1446
+ const strokePx = (node.strokeWidth ?? 1) / groupScale;
1447
+ if (node.blendMode) painter.setBlend(node.blendMode);
1448
+ if (node.strokeDash && node.strokeDash.length > 0) {
1449
+ painter.setLineDash(
1450
+ node.strokeDash.map((v) => v / groupScale),
1451
+ (node.strokeDashOffset ?? 0) / groupScale
1452
+ );
1144
1453
  }
1145
- 70% {
1146
- opacity: 1;
1454
+ if (node.blur !== void 0 && node.blur > 0) painter.setBlur(node.blur / groupScale);
1455
+ if (node.rotate) {
1456
+ const pivot = node.pivot ?? { x: 0.5, y: 0.5 };
1457
+ const px = origin.x + pivot.x * tileWidth;
1458
+ const py = origin.y + pivot.y * tileWidth;
1459
+ painter.translate(px, py);
1460
+ painter.rotate(node.rotate);
1461
+ painter.translate(-px, -py);
1147
1462
  }
1148
- 100% {
1149
- opacity: 0;
1150
- transform: translate(var(--confetti-tx), var(--confetti-ty)) rotate(var(--confetti-rotate)) scale(0.5);
1463
+ if (node.shadow) painter.setShadow({ color: node.shadow.color, blur: node.shadow.blur * tileWidth * groupScale });
1464
+ const fill = node.fill === "none" ? void 0 : node.fill;
1465
+ const stroke = node.stroke === "none" ? void 0 : node.stroke;
1466
+ const pxFill = node.gradient ? gradientStyle(node.gradient, origin.x, origin.y, tileWidth) ?? fill : fill;
1467
+ const pxStroke = node.strokeGradient ? gradientStyle(node.strokeGradient, origin.x, origin.y, tileWidth) ?? stroke : stroke;
1468
+ const patFill = node.fillPattern ? patternStyle(node.fillPattern, groupScale) : void 0;
1469
+ switch (node.shape) {
1470
+ case "cell": {
1471
+ const pts = dctx.projector.cellPath(node.position);
1472
+ if (pxFill) painter.fillPoly(pts, pxFill);
1473
+ if (patFill) painter.fillPoly(pts, patFill);
1474
+ if (pxStroke) painter.strokePoly(pts, pxStroke, strokePx, true);
1475
+ break;
1476
+ }
1477
+ case "rect": {
1478
+ const p = dctx.projector.anchorPoint(node.position, node.anchor ?? "top-left");
1479
+ const tw = dctx.projector.tileWidth;
1480
+ const x = p.x + (node.offsetX ?? 0) * tw;
1481
+ const y = p.y + (node.offsetY ?? 0) * tw;
1482
+ const w = (node.width ?? 0) * tw;
1483
+ const h = (node.height ?? 0) * tw;
1484
+ if (pxFill) painter.fillRect(x, y, w, h, pxFill);
1485
+ if (patFill) painter.fillRect(x, y, w, h, patFill);
1486
+ if (pxStroke) painter.strokeRect(x, y, w, h, pxStroke, strokePx);
1487
+ break;
1488
+ }
1489
+ case "ellipse": {
1490
+ const p = dctx.projector.anchorPoint(node.position, node.anchor ?? "ground");
1491
+ const tw = dctx.projector.tileWidth;
1492
+ const cx = p.x + (node.offsetX ?? 0) * tw;
1493
+ const cy = p.y + (node.offsetY ?? 0) * tw;
1494
+ const rx = (node.radiusX ?? 0) * tw;
1495
+ const ry = (node.radiusY ?? node.radiusX ?? 0) * tw;
1496
+ if (pxFill) painter.fillEllipse(cx, cy, rx, ry, pxFill);
1497
+ if (patFill) painter.fillEllipse(cx, cy, rx, ry, patFill);
1498
+ if (pxStroke) painter.strokeEllipse(cx, cy, rx, ry, pxStroke, strokePx);
1499
+ break;
1500
+ }
1501
+ case "poly": {
1502
+ const pts = (node.points ?? []).map((pt) => ({
1503
+ x: origin.x + ((node.offsetX ?? 0) + pt.x) * tileWidth,
1504
+ y: origin.y + ((node.offsetY ?? 0) + pt.y) * tileWidth
1505
+ }));
1506
+ if (pxFill) painter.fillPoly(pts, pxFill);
1507
+ if (patFill) painter.fillPoly(pts, patFill);
1508
+ if (pxStroke) painter.strokePoly(pts, pxStroke, strokePx, true);
1509
+ break;
1510
+ }
1511
+ case "path": {
1512
+ if (!node.d) break;
1513
+ painter.translate(origin.x + (node.offsetX ?? 0) * tileWidth, origin.y + (node.offsetY ?? 0) * tileWidth);
1514
+ painter.scale(tileWidth, tileWidth);
1515
+ if (node.strokeDash && node.strokeDash.length > 0) {
1516
+ painter.setLineDash(
1517
+ node.strokeDash.map((v) => v / (groupScale * tileWidth)),
1518
+ (node.strokeDashOffset ?? 0) / (groupScale * tileWidth)
1519
+ );
1520
+ }
1521
+ if (node.blur !== void 0 && node.blur > 0) painter.setBlur(node.blur / (groupScale * tileWidth));
1522
+ const localFill = node.gradient ? gradientStyle(node.gradient, 0, 0, 1) ?? fill : fill;
1523
+ const localStroke = node.strokeGradient ? gradientStyle(node.strokeGradient, 0, 0, 1) ?? stroke : stroke;
1524
+ const localPat = node.fillPattern ? patternStyle(node.fillPattern, groupScale * tileWidth) : void 0;
1525
+ if (localFill) painter.fillPath(node.d, localFill);
1526
+ if (localPat) painter.fillPath(node.d, localPat);
1527
+ if (localStroke) painter.strokePath(node.d, localStroke, strokePx / tileWidth);
1528
+ break;
1529
+ }
1530
+ }
1531
+ painter.restore();
1532
+ };
1533
+ function DrawShape(props) {
1534
+ const register = useContext(DrawableRegistryContext);
1535
+ if (register) {
1536
+ const { opacity, ...rest } = props;
1537
+ const node = {
1538
+ ...rest,
1539
+ type: "draw-shape",
1540
+ ...opacity !== void 0 && opacity > 0 ? { opacity } : {}
1541
+ };
1542
+ register(node);
1151
1543
  }
1544
+ return null;
1152
1545
  }
1153
- `;
1154
- var log2 = createLogger("almadar:ui:canvas-theme");
1155
- var toneVar = (tone) => `--color-${DIAGRAM_TONE_COLOR_KEYS[tone]}`;
1156
- var SERIES_TONES = ["series-1", "series-2", "series-3", "series-4", "series-5", "series-6", "series-7", "series-8"];
1157
- var TONE_SET = new Set(DIAGRAM_TONES);
1158
- function isDiagramTone(value) {
1159
- return TONE_SET.has(value);
1546
+ function isAnimatedGroup(node) {
1547
+ return Boolean(node.animation && node.animation.durationMs > 0 && node.animation.keyframes.length > 0);
1160
1548
  }
1161
- function colorCss(c) {
1162
- return c.a >= 1 ? c.css : `rgba(${c.r}, ${c.g}, ${c.b}, ${c.a})`;
1549
+ function DrawGroup(props) {
1550
+ const register = useContext(DrawableRegistryContext);
1551
+ if (register) register({ ...props, type: "draw-group" });
1552
+ return null;
1163
1553
  }
1164
- function oneOf(value, allowed, fallback, name) {
1165
- const hit = allowed.find((a) => a === value);
1166
- if (hit !== void 0) return hit;
1167
- if (value !== "") log2.warn("invalid-theme-keyword", { var: name, value, allowed });
1168
- return fallback;
1554
+
1555
+ // lib/imageCache.ts
1556
+ var RETRY_AFTER_MS = 5e3;
1557
+ var cache = /* @__PURE__ */ new Map();
1558
+ function startLoad(url, onReady, existing) {
1559
+ const img = new Image();
1560
+ img.crossOrigin = "anonymous";
1561
+ const entry = existing ?? { img, status: "pending" };
1562
+ entry.img = img;
1563
+ if (onReady) entry.onReady = onReady;
1564
+ img.onload = () => {
1565
+ entry.status = "loaded";
1566
+ updateAssetStatus(url, "loaded");
1567
+ entry.onReady?.();
1568
+ };
1569
+ img.onerror = () => {
1570
+ entry.status = "failed";
1571
+ entry.failedAt = Date.now();
1572
+ updateAssetStatus(url, "failed");
1573
+ entry.onReady?.();
1574
+ };
1575
+ img.src = url;
1576
+ return entry;
1169
1577
  }
1170
- var PLAIN_LENGTH = /^(-?[\d.]+)(px)?$/;
1171
- var ScopeReader = class {
1172
- constructor(scope) {
1173
- __publicField(this, "scope", scope);
1174
- __publicField(this, "style");
1175
- __publicField(this, "probe");
1176
- this.style = getComputedStyle(scope);
1177
- const doc = scope.ownerDocument;
1178
- this.probe = doc.createElement("span");
1179
- this.probe.style.position = "absolute";
1180
- this.probe.style.visibility = "hidden";
1181
- this.probe.style.pointerEvents = "none";
1182
- scope.appendChild(this.probe);
1183
- }
1184
- dispose() {
1185
- this.probe.remove();
1186
- }
1187
- raw(name) {
1188
- return this.style.getPropertyValue(name).trim();
1189
- }
1190
- /** A length var in px: a plain value parses directly, a derived one (`calc`, `var`) through the probe. */
1191
- length(name, fallback) {
1192
- const raw = this.raw(name);
1193
- const plain = PLAIN_LENGTH.exec(raw);
1194
- if (plain) return Number(plain[1]);
1195
- if (raw === "") return fallback;
1196
- this.probe.style.width = `var(${name})`;
1197
- const computed = PLAIN_LENGTH.exec(getComputedStyle(this.probe).width.trim());
1198
- this.probe.style.width = "";
1199
- if (computed) return Number(computed[1]);
1200
- log2.warn("unresolvable-theme-length", { var: name, raw });
1201
- return fallback;
1202
- }
1203
- number(name, fallback) {
1204
- const raw = this.raw(name);
1205
- if (raw === "") return fallback;
1206
- const n = Number(raw);
1207
- if (Number.isFinite(n)) return n;
1208
- log2.warn("unresolvable-theme-number", { var: name, raw });
1209
- return fallback;
1210
- }
1211
- /** A duration var in ms (`250ms`, `0.25s`, or a `var()` alias resolved through the probe). */
1212
- durationMs(name, fallback) {
1213
- const parse = (v) => {
1214
- const m = /^([\d.]+)(ms|s)$/.exec(v.trim());
1215
- return m ? Number(m[1]) * (m[2] === "s" ? 1e3 : 1) : null;
1216
- };
1217
- const raw = this.raw(name);
1218
- const direct = parse(raw);
1219
- if (direct !== null) return direct;
1220
- if (raw === "") return fallback;
1221
- this.probe.style.transitionDuration = `var(${name})`;
1222
- const computed = parse(getComputedStyle(this.probe).transitionDuration.split(",")[0] ?? "");
1223
- this.probe.style.transitionDuration = "";
1224
- return computed ?? fallback;
1578
+ function getOrLoadImage(url, onReady) {
1579
+ if (!url || typeof window === "undefined") return null;
1580
+ const existing = cache.get(url);
1581
+ if (existing) {
1582
+ if (existing.status === "loaded") return existing.img;
1583
+ if (existing.status === "pending" && onReady) existing.onReady = onReady;
1584
+ if (existing.status === "failed" && Date.now() - (existing.failedAt ?? 0) >= RETRY_AFTER_MS) {
1585
+ existing.failedAt = Date.now();
1586
+ startLoad(url, onReady, existing);
1587
+ }
1588
+ return null;
1225
1589
  }
1226
- color(name) {
1227
- const raw = this.raw(name);
1228
- return parseComputedColor(raw) ?? resolveThemeColor(`var(${name})`, this.scope);
1229
- }
1230
- shadow(name) {
1231
- const raw = this.raw(name);
1232
- if (raw === "" || raw === "none") return null;
1233
- this.probe.style.boxShadow = `var(${name})`;
1234
- const computed = getComputedStyle(this.probe).boxShadow.trim() || raw;
1235
- this.probe.style.boxShadow = "";
1236
- const colorMatch = /(rgba?\([^)]*\)|#[0-9a-f]{3,8}|oklch\([^)]*\))/i.exec(computed);
1237
- const lengths = computed.replace(colorMatch?.[0] ?? "", "").trim().split(/\s+/).map((t) => PLAIN_LENGTH.exec(t)).filter((m) => m !== null).map((m) => Number(m[1]));
1238
- if (lengths.length < 2) return null;
1239
- const color = colorMatch ? parseComputedColor(colorMatch[0]) : null;
1240
- return { offsetX: lengths[0], offsetY: lengths[1], blur: lengths[2] ?? 0, color: color ? colorCss(color) : "rgba(0, 0, 0, 0.25)" };
1241
- }
1242
- family(name, fallback) {
1243
- return this.raw(name) || fallback;
1244
- }
1245
- };
1246
- function dashList(raw, fallback) {
1247
- if (raw === "") return fallback;
1248
- const parts = raw.split(/[\s,]+/).map(Number);
1249
- return parts.every((n) => Number.isFinite(n) && n >= 0) ? parts : fallback;
1590
+ const entry = startLoad(url, onReady);
1591
+ cache.set(url, entry);
1592
+ updateAssetStatus(url, "pending");
1593
+ return null;
1250
1594
  }
1251
- function prefersReducedMotion(scope) {
1252
- const view = scope.ownerDocument.defaultView;
1253
- return typeof view?.matchMedia === "function" && view.matchMedia("(prefers-reduced-motion: reduce)").matches;
1595
+ function getImageStatus(url) {
1596
+ return cache.get(url)?.status;
1254
1597
  }
1255
- function resolveCanvasTheme(scope) {
1256
- const r = new ScopeReader(scope);
1257
- try {
1258
- const tones = {};
1259
- for (const tone of DIAGRAM_TONES) {
1260
- const c = r.color(toneVar(tone));
1261
- tones[tone] = c ? colorCss(c) : "currentColor";
1262
- }
1263
- const ground = r.color("--surface-diagram");
1264
- const text = {};
1265
- const TEXT_FALLBACK = { xs: 12, sm: 14, base: 16, lg: 18, xl: 20, "2xl": 24 };
1266
- for (const size of DIAGRAM_TEXT_SIZES) text[size] = r.length(`--text-${size}`, TEXT_FALLBACK[size]);
1267
- const body = r.family("--font-family-body", r.family("--font-family", "system-ui, sans-serif"));
1268
- return {
1269
- ground: ground ? colorCss(ground) : "transparent",
1270
- tones,
1271
- series: SERIES_TONES.map((t) => tones[t]),
1272
- strokes: {
1273
- thin: r.length("--diagram-stroke-thin", 1),
1274
- normal: r.length("--diagram-stroke-normal", 1.5),
1275
- bold: r.length("--diagram-stroke-bold", 3)
1276
- },
1277
- lineCap: oneOf(r.raw("--diagram-line-cap"), ["butt", "round", "square"], "round", "--diagram-line-cap"),
1278
- lineJoin: oneOf(r.raw("--diagram-line-join"), ["miter", "round", "bevel"], "round", "--diagram-line-join"),
1279
- dash: dashList(r.raw("--diagram-dash"), [6, 4]),
1280
- dot: dashList(r.raw("--diagram-dot"), [1, 4]),
1281
- fillStyle: oneOf(r.raw("--diagram-fill-style"), ["solid", "tint", "outline", "hatch"], "tint", "--diagram-fill-style"),
1282
- fillOpacity: r.number("--diagram-fill-opacity", 0.18),
1283
- roughness: r.number("--diagram-roughness", 0),
1284
- glow: r.number("--diagram-glow", 0),
1285
- shadow: r.shadow("--diagram-shadow"),
1286
- marker: oneOf(r.raw("--diagram-marker"), ["triangle", "open", "line", "dot"], "triangle", "--diagram-marker"),
1287
- faces: {
1288
- body,
1289
- display: r.family("--font-family-display", body),
1290
- mono: r.family("--font-family-mono", "ui-monospace, monospace")
1291
- },
1292
- label: {
1293
- font: oneOf(r.raw("--diagram-label-font"), ["body", "display", "mono"], "body", "--diagram-label-font"),
1294
- size: oneOf(r.raw("--diagram-label-size"), DIAGRAM_TEXT_SIZES, "sm", "--diagram-label-size"),
1295
- weight: r.raw("--diagram-label-weight") || "500",
1296
- uppercase: oneOf(r.raw("--diagram-label-case"), ["none", "uppercase"], "none", "--diagram-label-case") === "uppercase"
1297
- },
1298
- text,
1299
- corner: r.length("--diagram-corner", 4),
1300
- motion: {
1301
- enabled: r.raw("--motion-enable") !== "off" && !prefersReducedMotion(scope),
1302
- fastMs: r.durationMs("--duration-fast", 150),
1303
- normalMs: r.durationMs("--duration-normal", 250),
1304
- slowMs: r.durationMs("--duration-slow", 400),
1305
- easing: r.raw("--easing-standard") || "ease"
1306
- },
1307
- scene: {
1308
- roughness: r.number("--material-roughness", 0.6),
1309
- metalness: r.number("--material-metalness", 0),
1310
- flat: r.number("--material-flat", 0) >= 1,
1311
- outline: r.number("--material-outline", 0),
1312
- ambient: r.number("--light-ambient", 0.55),
1313
- key: r.number("--light-key", 1),
1314
- keyColor: (() => {
1315
- const c = r.color("--light-key-color");
1316
- return c ? colorCss(c) : "#ffffff";
1317
- })(),
1318
- fog: r.number("--scene-fog", 0)
1319
- }
1320
- };
1321
- } finally {
1322
- r.dispose();
1323
- }
1598
+
1599
+ // lib/skinning.ts
1600
+ var IDENTITY = [1, 0, 0, 1, 0, 0];
1601
+ function composeAffine(a, b) {
1602
+ return [
1603
+ a[0] * b[0] + a[2] * b[1],
1604
+ a[1] * b[0] + a[3] * b[1],
1605
+ a[0] * b[2] + a[2] * b[3],
1606
+ a[1] * b[2] + a[3] * b[3],
1607
+ a[0] * b[4] + a[2] * b[5] + a[4],
1608
+ a[1] * b[4] + a[3] * b[5] + a[5]
1609
+ ];
1324
1610
  }
1325
- function withSeries(theme, series, scope) {
1326
- if (!theme || !series || series.length === 0 || !scope) return theme;
1327
- const colors = series.map((c, i) => markColor(theme, c, scope, SERIES_TONES[i % SERIES_TONES.length]));
1328
- const tones = { ...theme.tones };
1329
- SERIES_TONES.forEach((tone, i) => {
1330
- tones[tone] = colors[i % colors.length];
1331
- });
1332
- return { ...theme, tones, series: SERIES_TONES.map((t) => tones[t]) };
1611
+ function invertAffine(m) {
1612
+ const det = m[0] * m[3] - m[1] * m[2];
1613
+ if (!Number.isFinite(det) || Math.abs(det) < 1e-12) return IDENTITY;
1614
+ const inv = 1 / det;
1615
+ return [
1616
+ m[3] * inv,
1617
+ -m[1] * inv,
1618
+ -m[2] * inv,
1619
+ m[0] * inv,
1620
+ (m[2] * m[5] - m[3] * m[4]) * inv,
1621
+ (m[1] * m[4] - m[0] * m[5]) * inv
1622
+ ];
1333
1623
  }
1334
- function markColor(theme, value, scope, fallback) {
1335
- if (value === void 0 || value === "") return theme.tones[fallback];
1336
- if (isDiagramTone(value)) return theme.tones[value];
1337
- if (value.startsWith("var(") || value.startsWith("color-mix(")) {
1338
- const resolved = resolveThemeColor(value, scope);
1339
- return resolved ? colorCss(resolved) : theme.tones[fallback];
1340
- }
1341
- return value;
1624
+ function applyAffine(m, x, y) {
1625
+ return [m[0] * x + m[2] * y + m[4], m[1] * x + m[3] * y + m[5]];
1342
1626
  }
1343
- function markStrokeWidth(theme, literal, role, fallback = "normal") {
1344
- return literal ?? theme.strokes[role ?? fallback];
1627
+ function rotationAbout(pivot, degrees) {
1628
+ const r = degrees * Math.PI / 180;
1629
+ const cos = Math.cos(r);
1630
+ const sin = Math.sin(r);
1631
+ const [px, py] = pivot;
1632
+ return [cos, sin, -sin, cos, px - cos * px + sin * py, py - sin * px - cos * py];
1345
1633
  }
1346
- function labelFont(theme, roles, literalPx, literalFamily) {
1347
- const px = literalPx ?? theme.text[roles.textSize ?? theme.label.size];
1348
- const family = literalFamily ?? theme.faces[roles.font ?? theme.label.font];
1349
- return { font: `${theme.label.weight} ${px}px ${family}`, px };
1634
+ function computeWorldMatrices(bones, pose) {
1635
+ const indexByName = /* @__PURE__ */ new Map();
1636
+ bones.forEach((b, i) => indexByName.set(b.name, i));
1637
+ const world = new Array(bones.length);
1638
+ const visiting = /* @__PURE__ */ new Set();
1639
+ const resolve = (i) => {
1640
+ const cached = world[i];
1641
+ if (cached) return cached;
1642
+ if (visiting.has(i)) return IDENTITY;
1643
+ visiting.add(i);
1644
+ const b = bones[i];
1645
+ const parentIndex = b.parent === null ? void 0 : indexByName.get(b.parent);
1646
+ const parentWorld = parentIndex === void 0 ? IDENTITY : resolve(parentIndex);
1647
+ const local = rotationAbout(b.pivot, pose[b.name] ?? 0);
1648
+ const m = composeAffine(parentWorld, local);
1649
+ visiting.delete(i);
1650
+ world[i] = m;
1651
+ return m;
1652
+ };
1653
+ return bones.map((_, i) => resolve(i));
1350
1654
  }
1351
- function labelText(theme, text, textCase = "theme") {
1352
- return theme.label.uppercase && textCase === "theme" ? text.toUpperCase() : text;
1655
+ function skinVertices(mesh, bindWorld, curWorld) {
1656
+ const skinMats = /* @__PURE__ */ new Map();
1657
+ const skinMat = (bone) => {
1658
+ let m = skinMats.get(bone);
1659
+ if (!m) {
1660
+ m = composeAffine(curWorld[bone] ?? IDENTITY, invertAffine(bindWorld[bone] ?? IDENTITY));
1661
+ skinMats.set(bone, m);
1662
+ }
1663
+ return m;
1664
+ };
1665
+ return mesh.vertices.map((v) => {
1666
+ if (v.weights.length === 0) return [v.x, v.y];
1667
+ let x = 0;
1668
+ let y = 0;
1669
+ for (const { bone, w } of v.weights) {
1670
+ const [bx, by] = applyAffine(skinMat(bone), v.x, v.y);
1671
+ x += w * bx;
1672
+ y += w * by;
1673
+ }
1674
+ return [x, y];
1675
+ });
1353
1676
  }
1354
- function fillLabel(ctx, theme, text, x, y) {
1355
- if (!text) return;
1356
- ctx.save();
1357
- ctx.strokeStyle = theme.ground;
1358
- ctx.lineWidth = Math.max(2, theme.text.xs * 0.3);
1359
- ctx.lineJoin = "round";
1360
- ctx.strokeText(text, x, y);
1361
- ctx.restore();
1362
- ctx.fillText(text, x, y);
1677
+ function triangleAffine(src, dst) {
1678
+ const [s0, s1, s2] = src;
1679
+ const det = s0[0] * (s1[1] - s2[1]) + s1[0] * (s2[1] - s0[1]) + s2[0] * (s0[1] - s1[1]);
1680
+ if (!Number.isFinite(det) || Math.abs(det) < 1e-12) return null;
1681
+ const [d0, d1, d2] = dst;
1682
+ const a = (d0[0] * (s1[1] - s2[1]) + d1[0] * (s2[1] - s0[1]) + d2[0] * (s0[1] - s1[1])) / det;
1683
+ const c = (d0[0] * (s2[0] - s1[0]) + d1[0] * (s0[0] - s2[0]) + d2[0] * (s1[0] - s0[0])) / det;
1684
+ const e = (d0[0] * (s1[0] * s2[1] - s2[0] * s1[1]) + d1[0] * (s2[0] * s0[1] - s0[0] * s2[1]) + d2[0] * (s0[0] * s1[1] - s1[0] * s0[1])) / det;
1685
+ const b = (d0[1] * (s1[1] - s2[1]) + d1[1] * (s2[1] - s0[1]) + d2[1] * (s0[1] - s1[1])) / det;
1686
+ const d = (d0[1] * (s2[0] - s1[0]) + d1[1] * (s0[0] - s2[0]) + d2[1] * (s1[0] - s0[0])) / det;
1687
+ const f = (d0[1] * (s1[0] * s2[1] - s2[0] * s1[1]) + d1[1] * (s2[0] * s0[1] - s0[0] * s2[1]) + d2[1] * (s0[0] * s1[1] - s1[0] * s0[1])) / det;
1688
+ return [a, b, c, d, e, f];
1363
1689
  }
1364
- function outlineStroke(stroke, fill, style, fillTone) {
1365
- return outlineCarriesFill(fill, style, fillTone) && fill !== void 0 ? fill : stroke;
1690
+ var meshCache = /* @__PURE__ */ new Map();
1691
+ function isRigBundle(json) {
1692
+ return json.mesh !== void 0 || json.bones !== void 0;
1366
1693
  }
1367
- function outlineCarriesFill(fill, style, fillTone) {
1368
- return style === "outline" && fill !== void 0 && fillTone !== "fill";
1694
+ function getRigBundle(url, onReady) {
1695
+ if (meshCache.has(url)) return meshCache.get(url) ?? void 0;
1696
+ meshCache.set(url, void 0);
1697
+ fetch(url).then((r) => {
1698
+ if (!r.ok) throw new Error(`rigbundle fetch failed: HTTP ${r.status}`);
1699
+ return r.json();
1700
+ }).then((json) => {
1701
+ const ok = isRigBundle(json) || Array.isArray(json.vertices);
1702
+ if (!ok) throw new Error("rigbundle payload is neither a SkinMesh nor a rigbundle");
1703
+ meshCache.set(url, json);
1704
+ onReady();
1705
+ }).catch(() => {
1706
+ meshCache.set(url, null);
1707
+ onReady();
1708
+ });
1709
+ return void 0;
1369
1710
  }
1370
- function applyLineCharacter(ctx, theme, strokeColor) {
1371
- ctx.lineCap = theme.lineCap;
1372
- ctx.lineJoin = theme.lineJoin;
1373
- if (theme.glow > 0) {
1374
- ctx.shadowColor = strokeColor;
1375
- ctx.shadowBlur = theme.glow;
1376
- ctx.shadowOffsetX = 0;
1377
- ctx.shadowOffsetY = 0;
1378
- }
1711
+ function skinMeshFailed(url) {
1712
+ return meshCache.get(url) === null;
1379
1713
  }
1380
- function markDash(theme, dash) {
1381
- if (dash === "dashed") return theme.dash;
1382
- if (dash === "dotted") return theme.dot;
1383
- return [];
1714
+ function vertexBounds(points) {
1715
+ if (points.length === 0) return null;
1716
+ let minX = Infinity;
1717
+ let minY = Infinity;
1718
+ let maxX = -Infinity;
1719
+ let maxY = -Infinity;
1720
+ for (const [x, y] of points) {
1721
+ if (x < minX) minX = x;
1722
+ if (y < minY) minY = y;
1723
+ if (x > maxX) maxX = x;
1724
+ if (y > maxY) maxY = y;
1725
+ }
1726
+ return { minX, minY, maxX, maxY };
1384
1727
  }
1385
- function wobble(x, y, k) {
1386
- const s = Math.sin(x * 12.9898 + y * 78.233 + k * 37.719) * 43758.5453;
1387
- return (s - Math.floor(s)) * 2 - 1;
1728
+
1729
+ // components/game/atoms/DrawSkinnedMesh.tsx
1730
+ var skinnedLog = createLogger("almadar:ui:draw-skinned-mesh");
1731
+ var loggedMissing = /* @__PURE__ */ new Set();
1732
+ function warnMissingOnce(reason, node) {
1733
+ const key = `${reason}:${node.asset.url}:${String(node.meshUrl)}`;
1734
+ if (loggedMissing.has(key)) return;
1735
+ loggedMissing.add(key);
1736
+ skinnedLog.warn("draw-skinned-mesh unresolvable \u2014 painting fallback", { reason, url: node.asset.url, meshUrl: node.meshUrl });
1388
1737
  }
1389
- function tracePolyline(ctx, theme, points, closed) {
1390
- ctx.beginPath();
1391
- if (points.length === 0) return;
1392
- const amp = theme.roughness;
1393
- const pts = closed ? [...points, points[0]] : [...points];
1394
- ctx.moveTo(pts[0].x + amp * wobble(pts[0].x, pts[0].y, 0), pts[0].y + amp * wobble(pts[0].y, pts[0].x, 1));
1395
- for (let i = 1; i < pts.length; i++) {
1396
- const a = pts[i - 1];
1397
- const b = pts[i];
1398
- if (amp === 0) {
1399
- ctx.lineTo(b.x, b.y);
1400
- continue;
1401
- }
1402
- const steps = Math.max(1, Math.round(Math.hypot(b.x - a.x, b.y - a.y) / 24));
1403
- for (let s = 1; s <= steps; s++) {
1404
- const t = s / steps;
1405
- const x = a.x + (b.x - a.x) * t;
1406
- const y = a.y + (b.y - a.y) * t;
1407
- ctx.lineTo(x + amp * wobble(x, y, 2), y + amp * wobble(y, x, 3));
1738
+ var WEIGHT_PALETTE = Array.from({ length: 10 }, (_, i) => `hsl(${i * 36 % 360}, 70%, 55%)`);
1739
+ function dominantBone(v) {
1740
+ let best = -1;
1741
+ let bestW = 0;
1742
+ for (const { bone, w } of v.weights) {
1743
+ if (w > bestW) {
1744
+ bestW = w;
1745
+ best = bone;
1408
1746
  }
1409
1747
  }
1410
- if (closed) ctx.closePath();
1748
+ return best;
1411
1749
  }
1412
- function arcPoints(cx, cy, rx, ry, startRad = 0, endRad = Math.PI * 2) {
1413
- const steps = Math.max(12, Math.round(Math.abs(endRad - startRad) * Math.max(rx, ry) / 8));
1414
- const out = [];
1415
- for (let i = 0; i <= steps; i++) {
1416
- const t = startRad + (endRad - startRad) * i / steps;
1417
- out.push({ x: cx + rx * Math.cos(t), y: cy + ry * Math.sin(t) });
1750
+ function resolveSkinPose(node, clip) {
1751
+ const effectiveClip = clip ?? node.clip;
1752
+ if (effectiveClip && node.frame !== void 0 && effectiveClip.frames.length > 0) {
1753
+ return effectiveClip.frames[node.frame % effectiveClip.frames.length];
1418
1754
  }
1419
- return out;
1755
+ return node.pose ?? {};
1420
1756
  }
1421
- function fillMark(ctx, theme, color, style = theme.fillStyle) {
1422
- if (style === "outline") return;
1423
- ctx.save();
1424
- ctx.shadowBlur = 0;
1425
- ctx.shadowColor = "transparent";
1426
- if (theme.shadow) {
1427
- ctx.shadowColor = theme.shadow.color;
1428
- ctx.shadowOffsetX = theme.shadow.offsetX;
1429
- ctx.shadowOffsetY = theme.shadow.offsetY;
1430
- ctx.shadowBlur = theme.shadow.blur;
1431
- }
1432
- if (style === "hatch") {
1433
- ctx.clip();
1434
- const span = ctx.canvas.width + ctx.canvas.height;
1435
- ctx.strokeStyle = color;
1436
- ctx.lineWidth = theme.strokes.thin;
1437
- ctx.setLineDash([]);
1438
- ctx.beginPath();
1439
- for (let d = -span; d < span; d += 6) {
1440
- ctx.moveTo(d, 0);
1441
- ctx.lineTo(d + span, span);
1442
- }
1443
- ctx.stroke();
1444
- } else {
1445
- ctx.globalAlpha *= style === "tint" ? theme.fillOpacity : 1;
1446
- ctx.fillStyle = color;
1447
- ctx.fill();
1448
- }
1449
- ctx.restore();
1757
+ function resolveRig(node, dctx) {
1758
+ const fetched = node.meshUrl ? getRigBundle(node.meshUrl, dctx.invalidate) : void 0;
1759
+ if (node.meshUrl && fetched === void 0) return void 0;
1760
+ const rb = fetched && isRigBundle(fetched) ? fetched : void 0;
1761
+ const mesh = node.mesh ?? rb?.mesh ?? (!rb ? fetched : void 0);
1762
+ if (!mesh) return void 0;
1763
+ const bones = node.bones.length > 0 ? node.bones : rb?.bones ?? [];
1764
+ if (bones.length === 0) return void 0;
1765
+ const clip = node.clip ?? (node.clipName ? rb?.clips?.[node.clipName] : void 0);
1766
+ return { mesh, bones, clip };
1450
1767
  }
1451
- function drawMarker(ctx, theme, x1, y1, x2, y2, color, width) {
1452
- const size = Math.max(8, width * 4);
1453
- const angle = Math.atan2(y2 - y1, x2 - x1);
1454
- const left = { x: x2 - size * Math.cos(angle - Math.PI / 6), y: y2 - size * Math.sin(angle - Math.PI / 6) };
1455
- const right = { x: x2 - size * Math.cos(angle + Math.PI / 6), y: y2 - size * Math.sin(angle + Math.PI / 6) };
1456
- ctx.save();
1457
- ctx.strokeStyle = color;
1458
- ctx.fillStyle = color;
1459
- ctx.lineWidth = width;
1460
- ctx.setLineDash([]);
1461
- switch (theme.marker) {
1462
- case "triangle": {
1463
- ctx.beginPath();
1464
- ctx.moveTo(x2, y2);
1465
- ctx.lineTo(left.x, left.y);
1466
- ctx.lineTo(right.x, right.y);
1467
- ctx.closePath();
1468
- ctx.fill();
1469
- break;
1470
- }
1471
- case "open":
1472
- case "line": {
1473
- ctx.beginPath();
1474
- ctx.moveTo(left.x, left.y);
1475
- ctx.lineTo(x2, y2);
1476
- ctx.lineTo(right.x, right.y);
1477
- if (theme.marker === "open") ctx.stroke();
1478
- else {
1479
- ctx.moveTo(x2 - size * 0.5 * Math.cos(angle + Math.PI / 2), y2 - size * 0.5 * Math.sin(angle + Math.PI / 2));
1480
- ctx.lineTo(x2 + size * 0.5 * Math.cos(angle + Math.PI / 2), y2 + size * 0.5 * Math.sin(angle + Math.PI / 2));
1481
- ctx.stroke();
1482
- }
1768
+ var pixelUVByMesh = /* @__PURE__ */ new WeakMap();
1769
+ function meshHasPixelUVs(mesh) {
1770
+ const cached = pixelUVByMesh.get(mesh);
1771
+ if (cached !== void 0) return cached;
1772
+ let pixel = false;
1773
+ for (const v of mesh.vertices) {
1774
+ if (v.u < 0 || v.u > 1 || v.v < 0 || v.v > 1) {
1775
+ pixel = true;
1483
1776
  break;
1484
1777
  }
1485
- case "dot": {
1486
- ctx.beginPath();
1487
- ctx.arc(x2, y2, Math.max(2.5, width * 1.5), 0, Math.PI * 2);
1488
- ctx.fill();
1489
- break;
1778
+ }
1779
+ pixelUVByMesh.set(mesh, pixel);
1780
+ return pixel;
1781
+ }
1782
+ function paintMeshPass(painter, node, bones, mesh, tex, dctx, pose, opacity) {
1783
+ const bindWorld = computeWorldMatrices(bones, {});
1784
+ const curWorld = computeWorldMatrices(bones, pose);
1785
+ const skinned = skinVertices(mesh, bindWorld, curWorld);
1786
+ const origin = dctx.projector.project(node.position);
1787
+ const tw = dctx.projector.tileWidth;
1788
+ painter.save();
1789
+ if (opacity !== 1) painter.setAlpha(opacity);
1790
+ for (const [i0, i1, i2] of mesh.triangles) {
1791
+ const v0 = mesh.vertices[i0];
1792
+ const v1 = mesh.vertices[i1];
1793
+ const v2 = mesh.vertices[i2];
1794
+ const p0 = skinned[i0];
1795
+ const p1 = skinned[i1];
1796
+ const p2 = skinned[i2];
1797
+ if (!v0 || !v1 || !v2 || !p0 || !p1 || !p2) continue;
1798
+ const dst = [
1799
+ [origin.x + p0[0] * tw, origin.y + p0[1] * tw],
1800
+ [origin.x + p1[0] * tw, origin.y + p1[1] * tw],
1801
+ [origin.x + p2[0] * tw, origin.y + p2[1] * tw]
1802
+ ];
1803
+ if (node.weightsOverlay || !tex) {
1804
+ if (!node.weightsOverlay) continue;
1805
+ const bone = dominantBone(v0);
1806
+ painter.fillPoly(
1807
+ dst.map(([x, y]) => ({ x, y })),
1808
+ WEIGHT_PALETTE[(bone % WEIGHT_PALETTE.length + WEIGHT_PALETTE.length) % WEIGHT_PALETTE.length]
1809
+ );
1810
+ continue;
1490
1811
  }
1812
+ const pixelUV = meshHasPixelUVs(mesh);
1813
+ const uv = pixelUV ? [[v0.u, v0.v], [v1.u, v1.v], [v2.u, v2.v]] : [
1814
+ [v0.u * tex.width, v0.v * tex.height],
1815
+ [v1.u * tex.width, v1.v * tex.height],
1816
+ [v2.u * tex.width, v2.v * tex.height]
1817
+ ];
1818
+ const affine = triangleAffine(uv, dst);
1819
+ if (!affine) continue;
1820
+ painter.save();
1821
+ painter.clipPath(`M ${dst[0][0]} ${dst[0][1]} L ${dst[1][0]} ${dst[1][1]} L ${dst[2][0]} ${dst[2][1]} Z`);
1822
+ painter.blitTransformed(tex, { x: 0, y: 0, w: tex.width, h: tex.height }, affine);
1823
+ painter.restore();
1491
1824
  }
1492
- ctx.restore();
1825
+ painter.restore();
1493
1826
  }
1494
- function useCanvasTheme(scopeRef) {
1495
- const { appliedTheme } = useTheme();
1496
- const scopeKey = JSON.stringify(useThemeScope());
1497
- const [state, setState] = useState({ theme: null, version: 0 });
1498
- useLayoutEffect(() => {
1499
- const scope = scopeRef.current;
1500
- if (!scope) return;
1501
- const theme = resolveCanvasTheme(scope);
1502
- setState((prev) => ({ theme, version: prev.version + 1 }));
1503
- }, [appliedTheme, scopeKey, scopeRef]);
1504
- return state;
1827
+ var paintSkinnedMesh = (painter, node, dctx) => {
1828
+ if (!isValidScenePos(node.position)) return;
1829
+ const rig = resolveRig(node, dctx);
1830
+ if (!rig) {
1831
+ if (node.meshUrl && skinMeshFailed(node.meshUrl)) warnMissingOnce("mesh-failed", node);
1832
+ else if (!node.mesh && !node.meshUrl) warnMissingOnce("mesh-missing", node);
1833
+ return;
1834
+ }
1835
+ const tex = node.weightsOverlay ? null : painter.resolveTexture(node.asset.url);
1836
+ if (!node.weightsOverlay && !tex) {
1837
+ if (getImageStatus(node.asset.url) === "failed") warnMissingOnce("texture-failed", node);
1838
+ return;
1839
+ }
1840
+ if (node.ghost && rig.clip && rig.clip.frames.length > 0) {
1841
+ paintMeshPass(painter, node, rig.bones, rig.mesh, tex, dctx, rig.clip.frames[node.ghost.frame % rig.clip.frames.length], node.ghost.opacity);
1842
+ }
1843
+ paintMeshPass(painter, node, rig.bones, rig.mesh, tex, dctx, resolveSkinPose(node, rig.clip), node.opacity ?? 1);
1844
+ };
1845
+ function DrawSkinnedMesh(props) {
1846
+ const register = useContext(DrawableRegistryContext);
1847
+ if (register) register({ ...props, type: "draw-skinned-mesh" });
1848
+ return null;
1505
1849
  }
1506
-
1507
- // lib/drawable/contract.ts
1508
- function isValidScenePos(pos) {
1509
- return Number.isFinite(pos?.x) && Number.isFinite(pos?.y);
1850
+ var MESH_SEGMENTS_DEFAULT = 24;
1851
+ function polyhedronBounds(vertices) {
1852
+ if (!vertices || vertices.length < 3) return null;
1853
+ const min = [Infinity, Infinity, Infinity];
1854
+ const max = [-Infinity, -Infinity, -Infinity];
1855
+ for (const v of vertices) {
1856
+ if (v.length < 3 || !v.every((c) => Number.isFinite(c))) return null;
1857
+ for (let axis = 0; axis < 3; axis++) {
1858
+ min[axis] = Math.min(min[axis], v[axis]);
1859
+ max[axis] = Math.max(max[axis], v[axis]);
1860
+ }
1861
+ }
1862
+ return { min, max };
1510
1863
  }
1511
- function spriteAssetState(node) {
1512
- if (!isValidScenePos(node.position)) return "invalid-position";
1513
- return typeof node.asset?.url === "string" && node.asset.url.length > 0 ? "ready" : "asset-unset";
1864
+ function clampSegments(segments) {
1865
+ const s = segments ?? MESH_SEGMENTS_DEFAULT;
1866
+ return Math.min(64, Math.max(3, Math.round(s)));
1514
1867
  }
1515
- var SPRITE_FALLBACK_FILL = "#9b8f7f";
1516
- var SPRITE_FALLBACK_STROKE = "#5e564b";
1517
- function spriteRect(projector, node, natural) {
1518
- const tw = projector.tileWidth;
1519
- const fallbackW = projector.worldPixelDirect ? natural?.w ?? 0 : tw;
1520
- const fallbackH = projector.worldPixelDirect ? natural?.h ?? 0 : tw;
1521
- const w = node.width !== void 0 ? node.width * tw : fallbackW;
1522
- const h = node.height !== void 0 ? node.height * tw : fallbackH;
1523
- const anchor = node.anchor ?? "top-left";
1524
- const p = projector.anchorPoint(node.position, anchor);
1525
- return {
1526
- x: anchor === "top-left" ? p.x : p.x - w / 2,
1527
- y: anchor === "ground" ? p.y - h : anchor === "center" ? p.y - h / 2 : p.y,
1528
- w,
1529
- h
1530
- };
1868
+ function isAnimatedMesh(node) {
1869
+ return Boolean(node.animation && node.animation.durationMs > 0 && node.animation.keyframes.length > 0);
1531
1870
  }
1532
- var DrawableRegistryContext = createContext(null);
1533
- var NUMERIC_TRACKS = [
1871
+ var lerp2 = (a, b, k) => a + (b - a) * k;
1872
+ var NUMERIC_TRACKS2 = [
1534
1873
  "offsetX",
1535
1874
  "offsetY",
1536
- "rotate",
1875
+ "offsetZ",
1876
+ "rotateX",
1877
+ "rotateY",
1878
+ "rotateZ",
1879
+ "scale",
1537
1880
  "opacity",
1538
- "radiusX",
1539
- "radiusY",
1540
- "width",
1541
- "height",
1542
- "strokeWidth",
1543
- "strokeDashOffset",
1544
- "blur"
1881
+ "emissiveIntensity"
1545
1882
  ];
1546
- function isAnimatedShape(node) {
1547
- return Boolean(node.animation && node.animation.durationMs > 0 && node.animation.keyframes.length > 0);
1548
- }
1549
- var lerp = (a, b, k) => a + (b - a) * k;
1550
- function applyShapeAnimation(node, timeMs) {
1883
+ function applyMeshAnimation(node, timeMs) {
1551
1884
  const anim = node.animation;
1552
- if (!anim || !(anim.durationMs > 0) || anim.keyframes.length === 0) return node;
1885
+ if (!anim || !(anim.durationMs > 0) || anim.keyframes.length === 0) return null;
1553
1886
  const frames = [...anim.keyframes].sort((a, b) => a.at - b.at);
1554
1887
  const cycle = timeMs / anim.durationMs;
1555
1888
  const t = anim.loop === false ? Math.min(cycle, 1) : cycle - Math.floor(cycle);
1556
- const out = { ...node };
1557
1889
  const trackValue = (key) => {
1558
1890
  const defined = frames.filter((f) => f[key] !== void 0);
1559
1891
  if (defined.length === 0) return void 0;
@@ -1569,736 +1901,704 @@ function applyShapeAnimation(node, timeMs) {
1569
1901
  const k = span > 0 ? (t - prev.at) / span : 1;
1570
1902
  const a = prev[key];
1571
1903
  const b = next[key];
1572
- if (typeof a === "number" && typeof b === "number") return lerp(a, b, k);
1904
+ if (typeof a === "number" && typeof b === "number") return lerp2(a, b, k);
1573
1905
  return a;
1574
1906
  };
1575
- for (const key of NUMERIC_TRACKS) {
1907
+ const num = {};
1908
+ for (const key of NUMERIC_TRACKS2) {
1576
1909
  const v = trackValue(key);
1577
- if (v !== void 0) out[key] = v;
1910
+ if (v !== void 0) num[key] = v;
1578
1911
  }
1579
- const fill = trackValue("fill");
1580
- if (fill !== void 0) out.fill = fill;
1581
- const stroke = trackValue("stroke");
1582
- if (stroke !== void 0) out.stroke = stroke;
1583
- const shadowFrames = frames.filter((f) => f.shadow !== void 0);
1584
- if (shadowFrames.length > 0) {
1585
- const sh = trackValue("shadow");
1586
- if (sh !== void 0) {
1587
- let prevSh;
1588
- let nextSh;
1589
- for (const f of shadowFrames) {
1590
- if (f.at <= t) prevSh = f;
1591
- else if (!nextSh) nextSh = f;
1592
- }
1593
- if (prevSh?.shadow && nextSh?.shadow) {
1594
- const span = nextSh.at - prevSh.at;
1595
- const k = span > 0 ? (t - prevSh.at) / span : 1;
1596
- out.shadow = { color: prevSh.shadow.color, blur: lerp(prevSh.shadow.blur, nextSh.shadow.blur, k) };
1597
- } else {
1598
- out.shadow = sh;
1599
- }
1600
- }
1912
+ return {
1913
+ offset: [num.offsetX ?? 0, num.offsetY ?? 0, num.offsetZ ?? 0],
1914
+ rotate: [num.rotateX ?? 0, num.rotateY ?? 0, num.rotateZ ?? 0],
1915
+ scale: num.scale ?? 1,
1916
+ opacity: num.opacity,
1917
+ emissiveIntensity: num.emissiveIntensity,
1918
+ color: trackValue("color"),
1919
+ emissive: trackValue("emissive")
1920
+ };
1921
+ }
1922
+ function DrawMesh(props) {
1923
+ const register = useContext(DrawableRegistryContext);
1924
+ if (register) register({ ...props, type: "draw-mesh" });
1925
+ return null;
1926
+ }
1927
+
1928
+ // components/core/atoms/fx.ts
1929
+ function resolveFxView(item, presets) {
1930
+ const row = presets?.find((p) => p.type === item.type);
1931
+ if (!row) return item;
1932
+ return {
1933
+ ...item,
1934
+ space: item.space ?? row.space,
1935
+ effect: item.effect ?? row.effect,
1936
+ color: item.color ?? row.color,
1937
+ size: item.size ?? row.size,
1938
+ vx: item.vx ?? row.vx,
1939
+ vy: item.vy ?? row.vy,
1940
+ vz: item.vz ?? row.vz,
1941
+ particleCount: item.particleCount ?? row.particleCount,
1942
+ animation: item.animation ?? row.animation,
1943
+ shape: row.shape,
1944
+ count: row.count,
1945
+ glow: row.glow,
1946
+ gravity: row.gravity,
1947
+ color2: row.color2
1948
+ };
1949
+ }
1950
+ function fxLifecycle(item, epochNowMs, tickMs) {
1951
+ const maxTtl = Math.max(item.maxTtl ?? item.ttl, item.ttl, 1);
1952
+ const lifeMs = maxTtl * tickMs;
1953
+ const ageMs = item.bornAt !== void 0 && epochNowMs > 0 ? Math.max(0, epochNowMs - item.bornAt) : (1 - item.ttl / maxTtl) * lifeMs;
1954
+ const progress = Math.min(1, Math.max(0, ageMs / lifeMs));
1955
+ return { lifeMs, ageMs, progress, fade: 1 - progress };
1956
+ }
1957
+ function fxHash01(seed, salt) {
1958
+ let h = 2166136261 ^ salt;
1959
+ for (let i = 0; i < seed.length; i++) {
1960
+ h ^= seed.charCodeAt(i);
1961
+ h = Math.imul(h, 16777619);
1601
1962
  }
1602
- return out;
1963
+ h ^= h >>> 13;
1964
+ h = Math.imul(h, 1274126177);
1965
+ h ^= h >>> 16;
1966
+ return (h >>> 0) / 4294967296;
1603
1967
  }
1604
- function gradientStyle(g, ox, oy, scale) {
1605
- if (g.kind === "linear") {
1606
- if (!g.from || !g.to) return void 0;
1607
- return {
1608
- kind: "linear",
1609
- x1: ox + g.from.x * scale,
1610
- y1: oy + g.from.y * scale,
1611
- x2: ox + g.to.x * scale,
1612
- y2: oy + g.to.y * scale,
1613
- stops: g.stops
1614
- };
1968
+ var CONFETTI_COLORS = [
1969
+ "var(--color-primary)",
1970
+ "var(--color-success)",
1971
+ "var(--color-warning)",
1972
+ "var(--color-error)",
1973
+ "gold",
1974
+ "dodgerblue"
1975
+ ];
1976
+ function createConfettiParticles(count, seed) {
1977
+ return Array.from({ length: count }, (_, i) => ({
1978
+ id: i,
1979
+ color: CONFETTI_COLORS[Math.floor(fxHash01(seed, i * 7 + 1) * CONFETTI_COLORS.length)],
1980
+ left: 30 + fxHash01(seed, i * 7 + 2) * 40,
1981
+ delay: fxHash01(seed, i * 7 + 3) * 300,
1982
+ angle: fxHash01(seed, i * 7 + 4) * 360,
1983
+ distance: 40 + fxHash01(seed, i * 7 + 5) * 80,
1984
+ rotation: fxHash01(seed, i * 7 + 6) * 720 - 360,
1985
+ size: 4 + fxHash01(seed, i * 7 + 7) * 6
1986
+ }));
1987
+ }
1988
+ var CONFETTI_BURST_KEYFRAMES = `
1989
+ @keyframes confetti-burst {
1990
+ 0% {
1991
+ opacity: 1;
1992
+ transform: translate(0, 0) rotate(0deg) scale(1);
1615
1993
  }
1616
- if (g.kind === "conic") {
1617
- if (!g.center) return void 0;
1994
+ 70% {
1995
+ opacity: 1;
1996
+ }
1997
+ 100% {
1998
+ opacity: 0;
1999
+ transform: translate(var(--confetti-tx), var(--confetti-ty)) rotate(var(--confetti-rotate)) scale(0.5);
2000
+ }
2001
+ }
2002
+ `;
2003
+
2004
+ // components/game/molecules/DrawFxLayer.tsx
2005
+ var DEFAULT_TICK_MS = 500;
2006
+ var DEFAULT_TEXT_COLOR = "#ffe066";
2007
+ var DEFAULT_SPARK_COLOR = "#ffffff";
2008
+ var easeOut = (t) => 1 - (1 - t) * (1 - t);
2009
+ function fxPosition(view, dim, ageSec) {
2010
+ const g = view.gravity ?? 0;
2011
+ const fall = 0.5 * g * ageSec * ageSec;
2012
+ if (dim === "2d") {
2013
+ const base2 = view.position ?? { x: view.x, y: view.z ?? view.y ?? 0 };
2014
+ if (!Number.isFinite(base2.x) || !Number.isFinite(base2.y)) return void 0;
1618
2015
  return {
1619
- kind: "conic",
1620
- cx: ox + g.center.x * scale,
1621
- cy: oy + g.center.y * scale,
1622
- angle: g.angle ?? 0,
1623
- stops: g.stops
2016
+ x: base2.x + (view.vx ?? 0) * ageSec,
2017
+ y: base2.y + (view.vz ?? 0) * ageSec + fall
1624
2018
  };
1625
2019
  }
1626
- if (!g.center || g.radius === void 0) return void 0;
2020
+ const base = view.position ?? { x: view.x, y: view.z ?? 0, z: view.y };
2021
+ if (!Number.isFinite(base.x) || !Number.isFinite(base.y)) return void 0;
1627
2022
  return {
1628
- kind: "radial",
1629
- cx: ox + g.center.x * scale,
1630
- cy: oy + g.center.y * scale,
1631
- r: g.radius * scale,
1632
- stops: g.stops
2023
+ x: base.x + (view.vx ?? 0) * ageSec,
2024
+ y: base.y + (view.vz ?? 0) * ageSec,
2025
+ ...base.z !== void 0 || view.vy !== void 0 || g !== 0 ? { z: (base.z ?? 0) + (view.vy ?? 0) * ageSec - fall } : {}
1633
2026
  };
1634
2027
  }
1635
- function patternStyle(p, unit) {
1636
- if (p.kind === "image") {
1637
- if (!p.url) return void 0;
1638
- return { kind: "image", url: p.url, scale: (p.scale ?? 1) / unit };
1639
- }
1640
- return { kind: "noise", scale: (p.scale ?? 2) / unit, alpha: p.alpha, color: p.color };
2028
+ function sparkShape(view, pos, i, fade, progress) {
2029
+ const size = view.size ?? 0.5;
2030
+ const angle = fxHash01(view.id, i * 3) * Math.PI * 2;
2031
+ const dist = size * (0.4 + 0.6 * fxHash01(view.id, i * 3 + 1)) * easeOut(progress);
2032
+ const r = size * (0.06 + 0.06 * fxHash01(view.id, i * 3 + 2));
2033
+ const color = view.color ?? DEFAULT_SPARK_COLOR;
2034
+ return {
2035
+ type: "draw-shape",
2036
+ shape: "ellipse",
2037
+ position: { ...pos, x: pos.x + Math.cos(angle) * dist, y: pos.y + Math.sin(angle) * dist },
2038
+ anchor: "center",
2039
+ radiusX: r,
2040
+ fill: color,
2041
+ blendMode: "lighter",
2042
+ opacity: fade,
2043
+ ...view.glow ? { shadow: { color, blur: view.glow } } : {}
2044
+ };
1641
2045
  }
1642
- var paintShape = (painter, rawNode, dctx) => {
1643
- const node = dctx.time > 0 ? applyShapeAnimation(rawNode, dctx.time) : rawNode;
1644
- if (!isValidScenePos(node.position)) return;
1645
- painter.save();
1646
- if (node.opacity !== void 0 && node.opacity !== 1) painter.setAlpha(node.opacity);
1647
- const origin = dctx.projector.project(node.position);
1648
- const tileWidth = dctx.projector.tileWidth;
1649
- const groupScale = dctx.groupScale ?? 1;
1650
- const strokePx = (node.strokeWidth ?? 1) / groupScale;
1651
- if (node.blendMode) painter.setBlend(node.blendMode);
1652
- if (node.strokeDash && node.strokeDash.length > 0) {
1653
- painter.setLineDash(
1654
- node.strokeDash.map((v) => v / groupScale),
1655
- (node.strokeDashOffset ?? 0) / groupScale
1656
- );
1657
- }
1658
- if (node.blur !== void 0 && node.blur > 0) painter.setBlur(node.blur / groupScale);
1659
- if (node.rotate) {
1660
- const pivot = node.pivot ?? { x: 0.5, y: 0.5 };
1661
- const px = origin.x + pivot.x * tileWidth;
1662
- const py = origin.y + pivot.y * tileWidth;
1663
- painter.translate(px, py);
1664
- painter.rotate(node.rotate);
1665
- painter.translate(-px, -py);
1666
- }
1667
- if (node.shadow) painter.setShadow({ color: node.shadow.color, blur: node.shadow.blur * tileWidth * groupScale });
1668
- const fill = node.fill === "none" ? void 0 : node.fill;
1669
- const stroke = node.stroke === "none" ? void 0 : node.stroke;
1670
- const pxFill = node.gradient ? gradientStyle(node.gradient, origin.x, origin.y, tileWidth) ?? fill : fill;
1671
- const pxStroke = node.strokeGradient ? gradientStyle(node.strokeGradient, origin.x, origin.y, tileWidth) ?? stroke : stroke;
1672
- const patFill = node.fillPattern ? patternStyle(node.fillPattern, groupScale) : void 0;
1673
- switch (node.shape) {
1674
- case "cell": {
1675
- const pts = dctx.projector.cellPath(node.position);
1676
- if (pxFill) painter.fillPoly(pts, pxFill);
1677
- if (patFill) painter.fillPoly(pts, patFill);
1678
- if (pxStroke) painter.strokePoly(pts, pxStroke, strokePx, true);
1679
- break;
1680
- }
1681
- case "rect": {
1682
- const p = dctx.projector.anchorPoint(node.position, node.anchor ?? "top-left");
1683
- const tw = dctx.projector.tileWidth;
1684
- const x = p.x + (node.offsetX ?? 0) * tw;
1685
- const y = p.y + (node.offsetY ?? 0) * tw;
1686
- const w = (node.width ?? 0) * tw;
1687
- const h = (node.height ?? 0) * tw;
1688
- if (pxFill) painter.fillRect(x, y, w, h, pxFill);
1689
- if (patFill) painter.fillRect(x, y, w, h, patFill);
1690
- if (pxStroke) painter.strokeRect(x, y, w, h, pxStroke, strokePx);
1691
- break;
2046
+ function proceduralShapes(view, pos, fade, progress) {
2047
+ const size = view.size ?? 0.5;
2048
+ const color = view.color ?? DEFAULT_SPARK_COLOR;
2049
+ switch (view.shape ?? "spark") {
2050
+ case "ring": {
2051
+ return [
2052
+ {
2053
+ type: "draw-shape",
2054
+ shape: "ellipse",
2055
+ position: pos,
2056
+ anchor: "center",
2057
+ radiusX: size * easeOut(progress),
2058
+ stroke: view.color2 ?? color,
2059
+ strokeWidth: 2,
2060
+ blendMode: "lighter",
2061
+ opacity: fade,
2062
+ ...view.glow ? { shadow: { color, blur: view.glow } } : {}
2063
+ }
2064
+ ];
1692
2065
  }
1693
- case "ellipse": {
1694
- const p = dctx.projector.anchorPoint(node.position, node.anchor ?? "ground");
1695
- const tw = dctx.projector.tileWidth;
1696
- const cx = p.x + (node.offsetX ?? 0) * tw;
1697
- const cy = p.y + (node.offsetY ?? 0) * tw;
1698
- const rx = (node.radiusX ?? 0) * tw;
1699
- const ry = (node.radiusY ?? node.radiusX ?? 0) * tw;
1700
- if (pxFill) painter.fillEllipse(cx, cy, rx, ry, pxFill);
1701
- if (patFill) painter.fillEllipse(cx, cy, rx, ry, patFill);
1702
- if (pxStroke) painter.strokeEllipse(cx, cy, rx, ry, pxStroke, strokePx);
1703
- break;
2066
+ case "puff": {
2067
+ const count = view.count ?? 3;
2068
+ return Array.from({ length: count }, (_, i) => {
2069
+ const angle = fxHash01(view.id, i * 5) * Math.PI * 2;
2070
+ const drift = size * 0.3 * fxHash01(view.id, i * 5 + 1) * easeOut(progress);
2071
+ return {
2072
+ type: "draw-shape",
2073
+ shape: "ellipse",
2074
+ position: { ...pos, x: pos.x + Math.cos(angle) * drift, y: pos.y + Math.sin(angle) * drift },
2075
+ anchor: "center",
2076
+ radiusX: size * (0.15 + 0.35 * progress) * (0.7 + 0.6 * fxHash01(view.id, i * 5 + 2)),
2077
+ fill: i === 0 && view.color2 ? view.color2 : color,
2078
+ opacity: fade * 0.6,
2079
+ ...view.glow ? { shadow: { color, blur: view.glow } } : {}
2080
+ };
2081
+ });
1704
2082
  }
1705
- case "poly": {
1706
- const pts = (node.points ?? []).map((pt) => ({
1707
- x: origin.x + ((node.offsetX ?? 0) + pt.x) * tileWidth,
1708
- y: origin.y + ((node.offsetY ?? 0) + pt.y) * tileWidth
1709
- }));
1710
- if (pxFill) painter.fillPoly(pts, pxFill);
1711
- if (patFill) painter.fillPoly(pts, patFill);
1712
- if (pxStroke) painter.strokePoly(pts, pxStroke, strokePx, true);
1713
- break;
2083
+ case "streak": {
2084
+ const count = view.count ?? 5;
2085
+ return Array.from({ length: count }, (_, i) => {
2086
+ const angle = fxHash01(view.id, i * 3) * Math.PI * 2;
2087
+ const inner = size * (0.2 + 0.8 * easeOut(progress)) * (0.6 + 0.4 * fxHash01(view.id, i * 3 + 1));
2088
+ const len = size * 0.35;
2089
+ return {
2090
+ type: "draw-shape",
2091
+ shape: "ellipse",
2092
+ position: pos,
2093
+ anchor: "center",
2094
+ offsetX: inner + len / 2,
2095
+ offsetY: 0,
2096
+ radiusX: len / 2,
2097
+ radiusY: size * 0.04,
2098
+ rotate: angle,
2099
+ fill: color,
2100
+ blendMode: "lighter",
2101
+ opacity: fade,
2102
+ ...view.glow ? { shadow: { color, blur: view.glow } } : {}
2103
+ };
2104
+ });
1714
2105
  }
1715
- case "path": {
1716
- if (!node.d) break;
1717
- painter.translate(origin.x + (node.offsetX ?? 0) * tileWidth, origin.y + (node.offsetY ?? 0) * tileWidth);
1718
- painter.scale(tileWidth, tileWidth);
1719
- if (node.strokeDash && node.strokeDash.length > 0) {
1720
- painter.setLineDash(
1721
- node.strokeDash.map((v) => v / (groupScale * tileWidth)),
1722
- (node.strokeDashOffset ?? 0) / (groupScale * tileWidth)
1723
- );
1724
- }
1725
- if (node.blur !== void 0 && node.blur > 0) painter.setBlur(node.blur / (groupScale * tileWidth));
1726
- const localFill = node.gradient ? gradientStyle(node.gradient, 0, 0, 1) ?? fill : fill;
1727
- const localStroke = node.strokeGradient ? gradientStyle(node.strokeGradient, 0, 0, 1) ?? stroke : stroke;
1728
- const localPat = node.fillPattern ? patternStyle(node.fillPattern, groupScale * tileWidth) : void 0;
1729
- if (localFill) painter.fillPath(node.d, localFill);
1730
- if (localPat) painter.fillPath(node.d, localPat);
1731
- if (localStroke) painter.strokePath(node.d, localStroke, strokePx / tileWidth);
1732
- break;
2106
+ default: {
2107
+ const count = view.count ?? 6;
2108
+ return Array.from({ length: count }, (_, i) => sparkShape(view, pos, i, fade, progress));
1733
2109
  }
1734
2110
  }
1735
- painter.restore();
1736
- };
1737
- function DrawShape(props) {
1738
- const register = useContext(DrawableRegistryContext);
1739
- if (register) {
1740
- const { opacity, ...rest } = props;
1741
- const node = {
1742
- ...rest,
1743
- type: "draw-shape",
1744
- ...opacity !== void 0 && opacity > 0 ? { opacity } : {}
1745
- };
1746
- register(node);
1747
- }
1748
- return null;
1749
- }
1750
- function isAnimatedGroup(node) {
1751
- return Boolean(node.animation && node.animation.durationMs > 0 && node.animation.keyframes.length > 0);
1752
- }
1753
- function DrawGroup(props) {
1754
- const register = useContext(DrawableRegistryContext);
1755
- if (register) register({ ...props, type: "draw-group" });
1756
- return null;
1757
- }
1758
-
1759
- // lib/imageCache.ts
1760
- var RETRY_AFTER_MS = 5e3;
1761
- var cache = /* @__PURE__ */ new Map();
1762
- function startLoad(url, onReady, existing) {
1763
- const img = new Image();
1764
- img.crossOrigin = "anonymous";
1765
- const entry = existing ?? { img, status: "pending" };
1766
- entry.img = img;
1767
- if (onReady) entry.onReady = onReady;
1768
- img.onload = () => {
1769
- entry.status = "loaded";
1770
- updateAssetStatus(url, "loaded");
1771
- entry.onReady?.();
1772
- };
1773
- img.onerror = () => {
1774
- entry.status = "failed";
1775
- entry.failedAt = Date.now();
1776
- updateAssetStatus(url, "failed");
1777
- entry.onReady?.();
1778
- };
1779
- img.src = url;
1780
- return entry;
1781
2111
  }
1782
- function getOrLoadImage(url, onReady) {
1783
- if (!url || typeof window === "undefined") return null;
1784
- const existing = cache.get(url);
1785
- if (existing) {
1786
- if (existing.status === "loaded") return existing.img;
1787
- if (existing.status === "pending" && onReady) existing.onReady = onReady;
1788
- if (existing.status === "failed" && Date.now() - (existing.failedAt ?? 0) >= RETRY_AFTER_MS) {
1789
- existing.failedAt = Date.now();
1790
- startLoad(url, onReady, existing);
1791
- }
1792
- return null;
2112
+ function expandFxItem(view, node, epochNowMs, dim, projector, fontFamily) {
2113
+ if (view.space === "screen") return [];
2114
+ const tickMs = node.tickMs ?? DEFAULT_TICK_MS;
2115
+ const { ageMs, progress, fade } = fxLifecycle(view, epochNowMs, tickMs);
2116
+ if (progress >= 1) return [];
2117
+ const pos = fxPosition(view, dim, ageMs / 1e3);
2118
+ if (!pos) return [];
2119
+ const out = [];
2120
+ const artItems = node.art?.[view.type]?.["idle"];
2121
+ const sprite = node.sprites?.[view.type];
2122
+ if (Array.isArray(artItems)) {
2123
+ out.push({
2124
+ type: "draw-group",
2125
+ position: pos,
2126
+ opacity: fade,
2127
+ ...view.size !== void 0 ? { scale: view.size } : {},
2128
+ items: artItems
2129
+ });
2130
+ } else if (sprite?.url) {
2131
+ const spriteSize = view.size ?? 0.6;
2132
+ out.push({
2133
+ type: "draw-sprite",
2134
+ position: pos,
2135
+ asset: sprite,
2136
+ anchor: "center",
2137
+ width: spriteSize,
2138
+ height: spriteSize,
2139
+ opacity: fade,
2140
+ animation: view.animation ?? sprite.animations?.[0],
2141
+ // One-shot fx rows (burst) play from the item's birth, not wall-time.
2142
+ clockMs: ageMs
2143
+ });
2144
+ } else {
2145
+ out.push(...proceduralShapes(view, pos, fade, progress));
1793
2146
  }
1794
- const entry = startLoad(url, onReady);
1795
- cache.set(url, entry);
1796
- updateAssetStatus(url, "pending");
1797
- return null;
1798
- }
1799
- function getImageStatus(url) {
1800
- return cache.get(url)?.status;
1801
- }
1802
-
1803
- // lib/skinning.ts
1804
- var IDENTITY = [1, 0, 0, 1, 0, 0];
1805
- function composeAffine(a, b) {
1806
- return [
1807
- a[0] * b[0] + a[2] * b[1],
1808
- a[1] * b[0] + a[3] * b[1],
1809
- a[0] * b[2] + a[2] * b[3],
1810
- a[1] * b[2] + a[3] * b[3],
1811
- a[0] * b[4] + a[2] * b[5] + a[4],
1812
- a[1] * b[4] + a[3] * b[5] + a[5]
1813
- ];
1814
- }
1815
- function invertAffine(m) {
1816
- const det = m[0] * m[3] - m[1] * m[2];
1817
- if (!Number.isFinite(det) || Math.abs(det) < 1e-12) return IDENTITY;
1818
- const inv = 1 / det;
1819
- return [
1820
- m[3] * inv,
1821
- -m[1] * inv,
1822
- -m[2] * inv,
1823
- m[0] * inv,
1824
- (m[2] * m[5] - m[3] * m[4]) * inv,
1825
- (m[1] * m[4] - m[0] * m[5]) * inv
1826
- ];
1827
- }
1828
- function applyAffine(m, x, y) {
1829
- return [m[0] * x + m[2] * y + m[4], m[1] * x + m[3] * y + m[5]];
1830
- }
1831
- function rotationAbout(pivot, degrees) {
1832
- const r = degrees * Math.PI / 180;
1833
- const cos = Math.cos(r);
1834
- const sin = Math.sin(r);
1835
- const [px, py] = pivot;
1836
- return [cos, sin, -sin, cos, px - cos * px + sin * py, py - sin * px - cos * py];
1837
- }
1838
- function computeWorldMatrices(bones, pose) {
1839
- const indexByName = /* @__PURE__ */ new Map();
1840
- bones.forEach((b, i) => indexByName.set(b.name, i));
1841
- const world = new Array(bones.length);
1842
- const visiting = /* @__PURE__ */ new Set();
1843
- const resolve = (i) => {
1844
- const cached = world[i];
1845
- if (cached) return cached;
1846
- if (visiting.has(i)) return IDENTITY;
1847
- visiting.add(i);
1848
- const b = bones[i];
1849
- const parentIndex = b.parent === null ? void 0 : indexByName.get(b.parent);
1850
- const parentWorld = parentIndex === void 0 ? IDENTITY : resolve(parentIndex);
1851
- const local = rotationAbout(b.pivot, pose[b.name] ?? 0);
1852
- const m = composeAffine(parentWorld, local);
1853
- visiting.delete(i);
1854
- world[i] = m;
1855
- return m;
1856
- };
1857
- return bones.map((_, i) => resolve(i));
2147
+ if (view.message) {
2148
+ const pxSize = projector && view.size !== void 0 ? Math.max(10, Math.round(view.size * projector.tileWidth)) : void 0;
2149
+ const family = fontFamily ?? "system-ui, sans-serif";
2150
+ const text = {
2151
+ type: "draw-text",
2152
+ text: view.message,
2153
+ position: pos,
2154
+ offsetY: -(0.15 + 0.55 * progress),
2155
+ color: view.color ?? node.textColor ?? DEFAULT_TEXT_COLOR,
2156
+ opacity: fade,
2157
+ font: `bold ${pxSize ?? 14}px ${family}`
2158
+ };
2159
+ out.push(text);
2160
+ }
2161
+ return out;
1858
2162
  }
1859
- function skinVertices(mesh, bindWorld, curWorld) {
1860
- const skinMats = /* @__PURE__ */ new Map();
1861
- const skinMat = (bone) => {
1862
- let m = skinMats.get(bone);
1863
- if (!m) {
1864
- m = composeAffine(curWorld[bone] ?? IDENTITY, invertAffine(bindWorld[bone] ?? IDENTITY));
1865
- skinMats.set(bone, m);
1866
- }
1867
- return m;
1868
- };
1869
- return mesh.vertices.map((v) => {
1870
- if (v.weights.length === 0) return [v.x, v.y];
1871
- let x = 0;
1872
- let y = 0;
1873
- for (const { bone, w } of v.weights) {
1874
- const [bx, by] = applyAffine(skinMat(bone), v.x, v.y);
1875
- x += w * bx;
1876
- y += w * by;
1877
- }
1878
- return [x, y];
1879
- });
2163
+ function expandFxLayer(node, epochNowMs, dim, projector, fontFamily) {
2164
+ if (!Array.isArray(node.items)) return [];
2165
+ const out = [];
2166
+ for (const item of node.items) {
2167
+ if (!item || typeof item.id !== "string") continue;
2168
+ out.push(...expandFxItem(resolveFxView(item, node.presets), node, epochNowMs, dim, projector, fontFamily));
2169
+ }
2170
+ return out;
1880
2171
  }
1881
- function triangleAffine(src, dst) {
1882
- const [s0, s1, s2] = src;
1883
- const det = s0[0] * (s1[1] - s2[1]) + s1[0] * (s2[1] - s0[1]) + s2[0] * (s0[1] - s1[1]);
1884
- if (!Number.isFinite(det) || Math.abs(det) < 1e-12) return null;
1885
- const [d0, d1, d2] = dst;
1886
- const a = (d0[0] * (s1[1] - s2[1]) + d1[0] * (s2[1] - s0[1]) + d2[0] * (s0[1] - s1[1])) / det;
1887
- const c = (d0[0] * (s2[0] - s1[0]) + d1[0] * (s0[0] - s2[0]) + d2[0] * (s1[0] - s0[0])) / det;
1888
- const e = (d0[0] * (s1[0] * s2[1] - s2[0] * s1[1]) + d1[0] * (s2[0] * s0[1] - s0[0] * s2[1]) + d2[0] * (s0[0] * s1[1] - s1[0] * s0[1])) / det;
1889
- const b = (d0[1] * (s1[1] - s2[1]) + d1[1] * (s2[1] - s0[1]) + d2[1] * (s0[1] - s1[1])) / det;
1890
- const d = (d0[1] * (s2[0] - s1[0]) + d1[1] * (s0[0] - s2[0]) + d2[1] * (s1[0] - s0[0])) / det;
1891
- const f = (d0[1] * (s1[0] * s2[1] - s2[0] * s1[1]) + d1[1] * (s2[0] * s0[1] - s0[0] * s2[1]) + d2[1] * (s0[0] * s1[1] - s1[0] * s0[1])) / det;
1892
- return [a, b, c, d, e, f];
2172
+ function DrawFxLayer(_props) {
2173
+ return null;
1893
2174
  }
1894
- var meshCache = /* @__PURE__ */ new Map();
1895
- function isRigBundle(json) {
1896
- return json.mesh !== void 0 || json.bones !== void 0;
2175
+
2176
+ // lib/atlasSlice.ts
2177
+ var atlasCache = /* @__PURE__ */ new Map();
2178
+ function isTilesheet(a) {
2179
+ return typeof a.tileWidth === "number";
1897
2180
  }
1898
- function getRigBundle(url, onReady) {
1899
- if (meshCache.has(url)) return meshCache.get(url) ?? void 0;
1900
- meshCache.set(url, void 0);
1901
- fetch(url).then((r) => {
1902
- if (!r.ok) throw new Error(`rigbundle fetch failed: HTTP ${r.status}`);
1903
- return r.json();
1904
- }).then((json) => {
1905
- const ok = isRigBundle(json) || Array.isArray(json.vertices);
1906
- if (!ok) throw new Error("rigbundle payload is neither a SkinMesh nor a rigbundle");
1907
- meshCache.set(url, json);
2181
+ function isSpriteSheetAtlas(a) {
2182
+ const s = a;
2183
+ return typeof s.frameWidth === "number" && typeof s.frameHeight === "number" && typeof s.animations === "object";
2184
+ }
2185
+ function getAtlas(url, onReady) {
2186
+ if (atlasCache.has(url)) return atlasCache.get(url) ?? void 0;
2187
+ atlasCache.set(url, void 0);
2188
+ fetch(url).then((r) => r.json()).then((json) => {
2189
+ atlasCache.set(url, json);
1908
2190
  onReady();
1909
2191
  }).catch(() => {
1910
- meshCache.set(url, null);
2192
+ atlasCache.set(url, null);
1911
2193
  onReady();
1912
2194
  });
1913
2195
  return void 0;
1914
2196
  }
1915
- function skinMeshFailed(url) {
1916
- return meshCache.get(url) === null;
2197
+ function atlasFailed(url) {
2198
+ return atlasCache.get(url) === null;
1917
2199
  }
1918
- function vertexBounds(points) {
1919
- if (points.length === 0) return null;
1920
- let minX = Infinity;
1921
- let minY = Infinity;
1922
- let maxX = -Infinity;
1923
- let maxY = -Infinity;
1924
- for (const [x, y] of points) {
1925
- if (x < minX) minX = x;
1926
- if (y < minY) minY = y;
1927
- if (x > maxX) maxX = x;
1928
- if (y > maxY) maxY = y;
2200
+ function subRectFor(atlas, sprite) {
2201
+ if (isTilesheet(atlas)) {
2202
+ let col;
2203
+ let row;
2204
+ if (sprite.includes(",")) {
2205
+ const [c, r] = sprite.split(",").map((n) => Number(n.trim()));
2206
+ col = c;
2207
+ row = r;
2208
+ } else {
2209
+ const i = Number(sprite);
2210
+ if (!Number.isFinite(i)) return null;
2211
+ col = i % atlas.columns;
2212
+ row = Math.floor(i / atlas.columns);
2213
+ }
2214
+ if (!Number.isFinite(col) || !Number.isFinite(row) || col < 0 || row < 0 || col >= atlas.columns || row >= atlas.rows) return null;
2215
+ const margin = atlas.margin ?? 0;
2216
+ const spacing = atlas.spacing ?? 0;
2217
+ return {
2218
+ sx: margin + col * (atlas.tileWidth + spacing),
2219
+ sy: margin + row * (atlas.tileHeight + spacing),
2220
+ sw: atlas.tileWidth,
2221
+ sh: atlas.tileHeight
2222
+ };
1929
2223
  }
1930
- return { minX, minY, maxX, maxY };
2224
+ if (isSpriteSheetAtlas(atlas)) return null;
2225
+ const st = atlas.subTextures[sprite];
2226
+ if (!st) return null;
2227
+ return { sx: st.x, sy: st.y, sw: st.width, sh: st.height };
1931
2228
  }
1932
-
1933
- // components/game/atoms/DrawSkinnedMesh.tsx
1934
- var skinnedLog = createLogger("almadar:ui:draw-skinned-mesh");
1935
- var loggedMissing = /* @__PURE__ */ new Set();
1936
- function warnMissingOnce(reason, node) {
1937
- const key = `${reason}:${node.asset.url}:${String(node.meshUrl)}`;
1938
- if (loggedMissing.has(key)) return;
1939
- loggedMissing.add(key);
1940
- skinnedLog.warn("draw-skinned-mesh unresolvable \u2014 painting fallback", { reason, url: node.asset.url, meshUrl: node.meshUrl });
2229
+ function atlasUrlOf(asset) {
2230
+ return typeof asset?.atlas === "string" && asset.atlas.length > 0 ? asset.atlas : void 0;
1941
2231
  }
1942
- var WEIGHT_PALETTE = Array.from({ length: 10 }, (_, i) => `hsl(${i * 36 % 360}, 70%, 55%)`);
1943
- function dominantBone(v) {
1944
- let best = -1;
1945
- let bestW = 0;
1946
- for (const { bone, w } of v.weights) {
1947
- if (w > bestW) {
1948
- bestW = w;
1949
- best = bone;
1950
- }
1951
- }
1952
- return best;
2232
+ function isAtlasAsset(asset) {
2233
+ return atlasUrlOf(asset) !== void 0 && typeof asset?.sprite === "string" && asset.sprite.length > 0;
1953
2234
  }
1954
- function resolveSkinPose(node, clip) {
1955
- const effectiveClip = clip ?? node.clip;
1956
- if (effectiveClip && node.frame !== void 0 && effectiveClip.frames.length > 0) {
1957
- return effectiveClip.frames[node.frame % effectiveClip.frames.length];
2235
+ function resolveAssetSource(img, asset, onReady) {
2236
+ if (isAtlasAsset(asset)) {
2237
+ const atlas = getAtlas(asset.atlas, onReady);
2238
+ if (!atlas) return null;
2239
+ const rect = subRectFor(atlas, asset.sprite);
2240
+ if (!rect) return null;
2241
+ return { img, rect, aspect: rect.sw / rect.sh };
1958
2242
  }
1959
- return node.pose ?? {};
2243
+ const natW = img.naturalWidth || 1;
2244
+ const natH = img.naturalHeight || 1;
2245
+ return { img, rect: null, aspect: natW / natH };
1960
2246
  }
1961
- function resolveRig(node, dctx) {
1962
- const fetched = node.meshUrl ? getRigBundle(node.meshUrl, dctx.invalidate) : void 0;
1963
- if (node.meshUrl && fetched === void 0) return void 0;
1964
- const rb = fetched && isRigBundle(fetched) ? fetched : void 0;
1965
- const mesh = node.mesh ?? rb?.mesh ?? (!rb ? fetched : void 0);
1966
- if (!mesh) return void 0;
1967
- const bones = node.bones.length > 0 ? node.bones : rb?.bones ?? [];
1968
- if (bones.length === 0) return void 0;
1969
- const clip = node.clip ?? (node.clipName ? rb?.clips?.[node.clipName] : void 0);
1970
- return { mesh, bones, clip };
2247
+ function blit(ctx, src, dx, dy, dw, dh) {
2248
+ if (src.rect) ctx.drawImage(src.img, src.rect.sx, src.rect.sy, src.rect.sw, src.rect.sh, dx, dy, dw, dh);
2249
+ else ctx.drawImage(src.img, dx, dy, dw, dh);
1971
2250
  }
1972
- var pixelUVByMesh = /* @__PURE__ */ new WeakMap();
1973
- function meshHasPixelUVs(mesh) {
1974
- const cached = pixelUVByMesh.get(mesh);
1975
- if (cached !== void 0) return cached;
1976
- let pixel = false;
1977
- for (const v of mesh.vertices) {
1978
- if (v.u < 0 || v.u > 1 || v.v < 0 || v.v > 1) {
1979
- pixel = true;
1980
- break;
1981
- }
1982
- }
1983
- pixelUVByMesh.set(mesh, pixel);
1984
- return pixel;
2251
+ var log2 = createLogger("almadar:ui:canvas-theme");
2252
+ var toneVar = (tone) => `--color-${DIAGRAM_TONE_COLOR_KEYS[tone]}`;
2253
+ var SERIES_TONES = ["series-1", "series-2", "series-3", "series-4", "series-5", "series-6", "series-7", "series-8"];
2254
+ var TONE_SET = new Set(DIAGRAM_TONES);
2255
+ function isDiagramTone(value) {
2256
+ return TONE_SET.has(value);
1985
2257
  }
1986
- function paintMeshPass(painter, node, bones, mesh, tex, dctx, pose, opacity) {
1987
- const bindWorld = computeWorldMatrices(bones, {});
1988
- const curWorld = computeWorldMatrices(bones, pose);
1989
- const skinned = skinVertices(mesh, bindWorld, curWorld);
1990
- const origin = dctx.projector.project(node.position);
1991
- const tw = dctx.projector.tileWidth;
1992
- painter.save();
1993
- if (opacity !== 1) painter.setAlpha(opacity);
1994
- for (const [i0, i1, i2] of mesh.triangles) {
1995
- const v0 = mesh.vertices[i0];
1996
- const v1 = mesh.vertices[i1];
1997
- const v2 = mesh.vertices[i2];
1998
- const p0 = skinned[i0];
1999
- const p1 = skinned[i1];
2000
- const p2 = skinned[i2];
2001
- if (!v0 || !v1 || !v2 || !p0 || !p1 || !p2) continue;
2002
- const dst = [
2003
- [origin.x + p0[0] * tw, origin.y + p0[1] * tw],
2004
- [origin.x + p1[0] * tw, origin.y + p1[1] * tw],
2005
- [origin.x + p2[0] * tw, origin.y + p2[1] * tw]
2006
- ];
2007
- if (node.weightsOverlay || !tex) {
2008
- if (!node.weightsOverlay) continue;
2009
- const bone = dominantBone(v0);
2010
- painter.fillPoly(
2011
- dst.map(([x, y]) => ({ x, y })),
2012
- WEIGHT_PALETTE[(bone % WEIGHT_PALETTE.length + WEIGHT_PALETTE.length) % WEIGHT_PALETTE.length]
2013
- );
2014
- continue;
2015
- }
2016
- const pixelUV = meshHasPixelUVs(mesh);
2017
- const uv = pixelUV ? [[v0.u, v0.v], [v1.u, v1.v], [v2.u, v2.v]] : [
2018
- [v0.u * tex.width, v0.v * tex.height],
2019
- [v1.u * tex.width, v1.v * tex.height],
2020
- [v2.u * tex.width, v2.v * tex.height]
2021
- ];
2022
- const affine = triangleAffine(uv, dst);
2023
- if (!affine) continue;
2024
- painter.save();
2025
- painter.clipPath(`M ${dst[0][0]} ${dst[0][1]} L ${dst[1][0]} ${dst[1][1]} L ${dst[2][0]} ${dst[2][1]} Z`);
2026
- painter.blitTransformed(tex, { x: 0, y: 0, w: tex.width, h: tex.height }, affine);
2027
- painter.restore();
2258
+ function colorCss(c) {
2259
+ return c.a >= 1 ? c.css : `rgba(${c.r}, ${c.g}, ${c.b}, ${c.a})`;
2260
+ }
2261
+ function oneOf(value, allowed, fallback, name) {
2262
+ const hit = allowed.find((a) => a === value);
2263
+ if (hit !== void 0) return hit;
2264
+ if (value !== "") log2.warn("invalid-theme-keyword", { var: name, value, allowed });
2265
+ return fallback;
2266
+ }
2267
+ var PLAIN_LENGTH = /^(-?[\d.]+)(px)?$/;
2268
+ var ScopeReader = class {
2269
+ constructor(scope) {
2270
+ __publicField(this, "scope", scope);
2271
+ __publicField(this, "style");
2272
+ __publicField(this, "probe");
2273
+ this.style = getComputedStyle(scope);
2274
+ const doc = scope.ownerDocument;
2275
+ this.probe = doc.createElement("span");
2276
+ this.probe.style.position = "absolute";
2277
+ this.probe.style.visibility = "hidden";
2278
+ this.probe.style.pointerEvents = "none";
2279
+ scope.appendChild(this.probe);
2280
+ }
2281
+ dispose() {
2282
+ this.probe.remove();
2283
+ }
2284
+ raw(name) {
2285
+ return this.style.getPropertyValue(name).trim();
2286
+ }
2287
+ /** A length var in px: a plain value parses directly, a derived one (`calc`, `var`) through the probe. */
2288
+ length(name, fallback) {
2289
+ const raw = this.raw(name);
2290
+ const plain = PLAIN_LENGTH.exec(raw);
2291
+ if (plain) return Number(plain[1]);
2292
+ if (raw === "") return fallback;
2293
+ this.probe.style.width = `var(${name})`;
2294
+ const computed = PLAIN_LENGTH.exec(getComputedStyle(this.probe).width.trim());
2295
+ this.probe.style.width = "";
2296
+ if (computed) return Number(computed[1]);
2297
+ log2.warn("unresolvable-theme-length", { var: name, raw });
2298
+ return fallback;
2299
+ }
2300
+ number(name, fallback) {
2301
+ const raw = this.raw(name);
2302
+ if (raw === "") return fallback;
2303
+ const n = Number(raw);
2304
+ if (Number.isFinite(n)) return n;
2305
+ log2.warn("unresolvable-theme-number", { var: name, raw });
2306
+ return fallback;
2307
+ }
2308
+ /** A duration var in ms (`250ms`, `0.25s`, or a `var()` alias resolved through the probe). */
2309
+ durationMs(name, fallback) {
2310
+ const parse = (v) => {
2311
+ const m = /^([\d.]+)(ms|s)$/.exec(v.trim());
2312
+ return m ? Number(m[1]) * (m[2] === "s" ? 1e3 : 1) : null;
2313
+ };
2314
+ const raw = this.raw(name);
2315
+ const direct = parse(raw);
2316
+ if (direct !== null) return direct;
2317
+ if (raw === "") return fallback;
2318
+ this.probe.style.transitionDuration = `var(${name})`;
2319
+ const computed = parse(getComputedStyle(this.probe).transitionDuration.split(",")[0] ?? "");
2320
+ this.probe.style.transitionDuration = "";
2321
+ return computed ?? fallback;
2028
2322
  }
2029
- painter.restore();
2030
- }
2031
- var paintSkinnedMesh = (painter, node, dctx) => {
2032
- if (!isValidScenePos(node.position)) return;
2033
- const rig = resolveRig(node, dctx);
2034
- if (!rig) {
2035
- if (node.meshUrl && skinMeshFailed(node.meshUrl)) warnMissingOnce("mesh-failed", node);
2036
- else if (!node.mesh && !node.meshUrl) warnMissingOnce("mesh-missing", node);
2037
- return;
2323
+ color(name) {
2324
+ const raw = this.raw(name);
2325
+ return parseComputedColor(raw) ?? resolveThemeColor(`var(${name})`, this.scope);
2038
2326
  }
2039
- const tex = node.weightsOverlay ? null : painter.resolveTexture(node.asset.url);
2040
- if (!node.weightsOverlay && !tex) {
2041
- if (getImageStatus(node.asset.url) === "failed") warnMissingOnce("texture-failed", node);
2042
- return;
2327
+ shadow(name) {
2328
+ const raw = this.raw(name);
2329
+ if (raw === "" || raw === "none") return null;
2330
+ this.probe.style.boxShadow = `var(${name})`;
2331
+ const computed = getComputedStyle(this.probe).boxShadow.trim() || raw;
2332
+ this.probe.style.boxShadow = "";
2333
+ const colorMatch = /(rgba?\([^)]*\)|#[0-9a-f]{3,8}|oklch\([^)]*\))/i.exec(computed);
2334
+ const lengths = computed.replace(colorMatch?.[0] ?? "", "").trim().split(/\s+/).map((t) => PLAIN_LENGTH.exec(t)).filter((m) => m !== null).map((m) => Number(m[1]));
2335
+ if (lengths.length < 2) return null;
2336
+ const color = colorMatch ? parseComputedColor(colorMatch[0]) : null;
2337
+ return { offsetX: lengths[0], offsetY: lengths[1], blur: lengths[2] ?? 0, color: color ? colorCss(color) : "rgba(0, 0, 0, 0.25)" };
2043
2338
  }
2044
- if (node.ghost && rig.clip && rig.clip.frames.length > 0) {
2045
- paintMeshPass(painter, node, rig.bones, rig.mesh, tex, dctx, rig.clip.frames[node.ghost.frame % rig.clip.frames.length], node.ghost.opacity);
2339
+ family(name, fallback) {
2340
+ return this.raw(name) || fallback;
2046
2341
  }
2047
- paintMeshPass(painter, node, rig.bones, rig.mesh, tex, dctx, resolveSkinPose(node, rig.clip), node.opacity ?? 1);
2048
2342
  };
2049
- function DrawSkinnedMesh(props) {
2050
- const register = useContext(DrawableRegistryContext);
2051
- if (register) register({ ...props, type: "draw-skinned-mesh" });
2052
- return null;
2343
+ function dashList(raw, fallback) {
2344
+ if (raw === "") return fallback;
2345
+ const parts = raw.split(/[\s,]+/).map(Number);
2346
+ return parts.every((n) => Number.isFinite(n) && n >= 0) ? parts : fallback;
2053
2347
  }
2054
- var MESH_SEGMENTS_DEFAULT = 24;
2055
- function polyhedronBounds(vertices) {
2056
- if (!vertices || vertices.length < 3) return null;
2057
- const min = [Infinity, Infinity, Infinity];
2058
- const max = [-Infinity, -Infinity, -Infinity];
2059
- for (const v of vertices) {
2060
- if (v.length < 3 || !v.every((c) => Number.isFinite(c))) return null;
2061
- for (let axis = 0; axis < 3; axis++) {
2062
- min[axis] = Math.min(min[axis], v[axis]);
2063
- max[axis] = Math.max(max[axis], v[axis]);
2348
+ function prefersReducedMotion(scope) {
2349
+ const view = scope.ownerDocument.defaultView;
2350
+ return typeof view?.matchMedia === "function" && view.matchMedia("(prefers-reduced-motion: reduce)").matches;
2351
+ }
2352
+ function resolveCanvasTheme(scope) {
2353
+ const r = new ScopeReader(scope);
2354
+ try {
2355
+ const tones = {};
2356
+ for (const tone of DIAGRAM_TONES) {
2357
+ const c = r.color(toneVar(tone));
2358
+ tones[tone] = c ? colorCss(c) : "currentColor";
2064
2359
  }
2360
+ const ground = r.color("--surface-diagram");
2361
+ const text = {};
2362
+ const TEXT_FALLBACK = { xs: 12, sm: 14, base: 16, lg: 18, xl: 20, "2xl": 24 };
2363
+ for (const size of DIAGRAM_TEXT_SIZES) text[size] = r.length(`--text-${size}`, TEXT_FALLBACK[size]);
2364
+ const body = r.family("--font-family-body", r.family("--font-family", "system-ui, sans-serif"));
2365
+ return {
2366
+ ground: ground ? colorCss(ground) : "transparent",
2367
+ tones,
2368
+ series: SERIES_TONES.map((t) => tones[t]),
2369
+ strokes: {
2370
+ thin: r.length("--diagram-stroke-thin", 1),
2371
+ normal: r.length("--diagram-stroke-normal", 1.5),
2372
+ bold: r.length("--diagram-stroke-bold", 3)
2373
+ },
2374
+ lineCap: oneOf(r.raw("--diagram-line-cap"), ["butt", "round", "square"], "round", "--diagram-line-cap"),
2375
+ lineJoin: oneOf(r.raw("--diagram-line-join"), ["miter", "round", "bevel"], "round", "--diagram-line-join"),
2376
+ dash: dashList(r.raw("--diagram-dash"), [6, 4]),
2377
+ dot: dashList(r.raw("--diagram-dot"), [1, 4]),
2378
+ fillStyle: oneOf(r.raw("--diagram-fill-style"), ["solid", "tint", "outline", "hatch"], "tint", "--diagram-fill-style"),
2379
+ fillOpacity: r.number("--diagram-fill-opacity", 0.18),
2380
+ roughness: r.number("--diagram-roughness", 0),
2381
+ glow: r.number("--diagram-glow", 0),
2382
+ shadow: r.shadow("--diagram-shadow"),
2383
+ marker: oneOf(r.raw("--diagram-marker"), ["triangle", "open", "line", "dot"], "triangle", "--diagram-marker"),
2384
+ faces: {
2385
+ body,
2386
+ display: r.family("--font-family-display", body),
2387
+ mono: r.family("--font-family-mono", "ui-monospace, monospace")
2388
+ },
2389
+ label: {
2390
+ font: oneOf(r.raw("--diagram-label-font"), ["body", "display", "mono"], "body", "--diagram-label-font"),
2391
+ size: oneOf(r.raw("--diagram-label-size"), DIAGRAM_TEXT_SIZES, "sm", "--diagram-label-size"),
2392
+ weight: r.raw("--diagram-label-weight") || "500",
2393
+ uppercase: oneOf(r.raw("--diagram-label-case"), ["none", "uppercase"], "none", "--diagram-label-case") === "uppercase"
2394
+ },
2395
+ text,
2396
+ corner: r.length("--diagram-corner", 4),
2397
+ motion: {
2398
+ enabled: r.raw("--motion-enable") !== "off" && !prefersReducedMotion(scope),
2399
+ fastMs: r.durationMs("--duration-fast", 150),
2400
+ normalMs: r.durationMs("--duration-normal", 250),
2401
+ slowMs: r.durationMs("--duration-slow", 400),
2402
+ easing: r.raw("--easing-standard") || "ease"
2403
+ },
2404
+ scene: {
2405
+ roughness: r.number("--material-roughness", 0.6),
2406
+ metalness: r.number("--material-metalness", 0),
2407
+ flat: r.number("--material-flat", 0) >= 1,
2408
+ outline: r.number("--material-outline", 0),
2409
+ ambient: r.number("--light-ambient", 0.55),
2410
+ key: r.number("--light-key", 1),
2411
+ keyColor: (() => {
2412
+ const c = r.color("--light-key-color");
2413
+ return c ? colorCss(c) : "#ffffff";
2414
+ })(),
2415
+ fog: r.number("--scene-fog", 0)
2416
+ }
2417
+ };
2418
+ } finally {
2419
+ r.dispose();
2065
2420
  }
2066
- return { min, max };
2067
- }
2068
- function clampSegments(segments) {
2069
- const s = segments ?? MESH_SEGMENTS_DEFAULT;
2070
- return Math.min(64, Math.max(3, Math.round(s)));
2071
2421
  }
2072
- function isAnimatedMesh(node) {
2073
- return Boolean(node.animation && node.animation.durationMs > 0 && node.animation.keyframes.length > 0);
2422
+ function withSeries(theme, series, scope) {
2423
+ if (!theme || !series || series.length === 0 || !scope) return theme;
2424
+ const colors = series.map((c, i) => markColor(theme, c, scope, SERIES_TONES[i % SERIES_TONES.length]));
2425
+ const tones = { ...theme.tones };
2426
+ SERIES_TONES.forEach((tone, i) => {
2427
+ tones[tone] = colors[i % colors.length];
2428
+ });
2429
+ return { ...theme, tones, series: SERIES_TONES.map((t) => tones[t]) };
2074
2430
  }
2075
- var lerp2 = (a, b, k) => a + (b - a) * k;
2076
- var NUMERIC_TRACKS2 = [
2077
- "offsetX",
2078
- "offsetY",
2079
- "offsetZ",
2080
- "rotateX",
2081
- "rotateY",
2082
- "rotateZ",
2083
- "scale",
2084
- "opacity",
2085
- "emissiveIntensity"
2086
- ];
2087
- function applyMeshAnimation(node, timeMs) {
2088
- const anim = node.animation;
2089
- if (!anim || !(anim.durationMs > 0) || anim.keyframes.length === 0) return null;
2090
- const frames = [...anim.keyframes].sort((a, b) => a.at - b.at);
2091
- const cycle = timeMs / anim.durationMs;
2092
- const t = anim.loop === false ? Math.min(cycle, 1) : cycle - Math.floor(cycle);
2093
- const trackValue = (key) => {
2094
- const defined = frames.filter((f) => f[key] !== void 0);
2095
- if (defined.length === 0) return void 0;
2096
- let prev;
2097
- let next;
2098
- for (const f of defined) {
2099
- if (f.at <= t) prev = f;
2100
- else if (!next) next = f;
2101
- }
2102
- if (!prev) return defined[0][key];
2103
- if (!next) return prev[key];
2104
- const span = next.at - prev.at;
2105
- const k = span > 0 ? (t - prev.at) / span : 1;
2106
- const a = prev[key];
2107
- const b = next[key];
2108
- if (typeof a === "number" && typeof b === "number") return lerp2(a, b, k);
2109
- return a;
2110
- };
2111
- const num = {};
2112
- for (const key of NUMERIC_TRACKS2) {
2113
- const v = trackValue(key);
2114
- if (v !== void 0) num[key] = v;
2431
+ function markColor(theme, value, scope, fallback) {
2432
+ if (value === void 0 || value === "") return theme.tones[fallback];
2433
+ if (isDiagramTone(value)) return theme.tones[value];
2434
+ if (value.startsWith("var(") || value.startsWith("color-mix(")) {
2435
+ const resolved = resolveThemeColor(value, scope);
2436
+ return resolved ? colorCss(resolved) : theme.tones[fallback];
2115
2437
  }
2116
- return {
2117
- offset: [num.offsetX ?? 0, num.offsetY ?? 0, num.offsetZ ?? 0],
2118
- rotate: [num.rotateX ?? 0, num.rotateY ?? 0, num.rotateZ ?? 0],
2119
- scale: num.scale ?? 1,
2120
- opacity: num.opacity,
2121
- emissiveIntensity: num.emissiveIntensity,
2122
- color: trackValue("color"),
2123
- emissive: trackValue("emissive")
2124
- };
2438
+ return value;
2125
2439
  }
2126
- function DrawMesh(props) {
2127
- const register = useContext(DrawableRegistryContext);
2128
- if (register) register({ ...props, type: "draw-mesh" });
2129
- return null;
2440
+ function markStrokeWidth(theme, literal, role, fallback = "normal") {
2441
+ return literal ?? theme.strokes[role ?? fallback];
2130
2442
  }
2131
-
2132
- // components/game/molecules/DrawFxLayer.tsx
2133
- var DEFAULT_TICK_MS = 500;
2134
- var DEFAULT_TEXT_COLOR = "#ffe066";
2135
- var DEFAULT_SPARK_COLOR = "#ffffff";
2136
- var easeOut = (t) => 1 - (1 - t) * (1 - t);
2137
- function fxPosition(view, dim, ageSec) {
2138
- const g = view.gravity ?? 0;
2139
- const fall = 0.5 * g * ageSec * ageSec;
2140
- if (dim === "2d") {
2141
- const base2 = view.position ?? { x: view.x, y: view.z ?? view.y ?? 0 };
2142
- if (!Number.isFinite(base2.x) || !Number.isFinite(base2.y)) return void 0;
2143
- return {
2144
- x: base2.x + (view.vx ?? 0) * ageSec,
2145
- y: base2.y + (view.vz ?? 0) * ageSec + fall
2146
- };
2443
+ function labelFont(theme, roles, literalPx, literalFamily) {
2444
+ const px = literalPx ?? theme.text[roles.textSize ?? theme.label.size];
2445
+ const family = literalFamily ?? theme.faces[roles.font ?? theme.label.font];
2446
+ return { font: `${theme.label.weight} ${px}px ${family}`, px };
2447
+ }
2448
+ function labelText(theme, text, textCase = "theme") {
2449
+ return theme.label.uppercase && textCase === "theme" ? text.toUpperCase() : text;
2450
+ }
2451
+ function fillLabel(ctx, theme, text, x, y) {
2452
+ if (!text) return;
2453
+ ctx.save();
2454
+ ctx.strokeStyle = theme.ground;
2455
+ ctx.lineWidth = Math.max(2, theme.text.xs * 0.3);
2456
+ ctx.lineJoin = "round";
2457
+ ctx.strokeText(text, x, y);
2458
+ ctx.restore();
2459
+ ctx.fillText(text, x, y);
2460
+ }
2461
+ function outlineStroke(stroke, fill, style, fillTone) {
2462
+ return outlineCarriesFill(fill, style, fillTone) && fill !== void 0 ? fill : stroke;
2463
+ }
2464
+ function outlineCarriesFill(fill, style, fillTone) {
2465
+ return style === "outline" && fill !== void 0 && fillTone !== "fill";
2466
+ }
2467
+ function applyLineCharacter(ctx, theme, strokeColor) {
2468
+ ctx.lineCap = theme.lineCap;
2469
+ ctx.lineJoin = theme.lineJoin;
2470
+ if (theme.glow > 0) {
2471
+ ctx.shadowColor = strokeColor;
2472
+ ctx.shadowBlur = theme.glow;
2473
+ ctx.shadowOffsetX = 0;
2474
+ ctx.shadowOffsetY = 0;
2147
2475
  }
2148
- const base = view.position ?? { x: view.x, y: view.z ?? 0, z: view.y };
2149
- if (!Number.isFinite(base.x) || !Number.isFinite(base.y)) return void 0;
2150
- return {
2151
- x: base.x + (view.vx ?? 0) * ageSec,
2152
- y: base.y + (view.vz ?? 0) * ageSec,
2153
- ...base.z !== void 0 || view.vy !== void 0 || g !== 0 ? { z: (base.z ?? 0) + (view.vy ?? 0) * ageSec - fall } : {}
2154
- };
2155
2476
  }
2156
- function sparkShape(view, pos, i, fade, progress) {
2157
- const size = view.size ?? 0.5;
2158
- const angle = fxHash01(view.id, i * 3) * Math.PI * 2;
2159
- const dist = size * (0.4 + 0.6 * fxHash01(view.id, i * 3 + 1)) * easeOut(progress);
2160
- const r = size * (0.06 + 0.06 * fxHash01(view.id, i * 3 + 2));
2161
- const color = view.color ?? DEFAULT_SPARK_COLOR;
2162
- return {
2163
- type: "draw-shape",
2164
- shape: "ellipse",
2165
- position: { ...pos, x: pos.x + Math.cos(angle) * dist, y: pos.y + Math.sin(angle) * dist },
2166
- anchor: "center",
2167
- radiusX: r,
2168
- fill: color,
2169
- blendMode: "lighter",
2170
- opacity: fade,
2171
- ...view.glow ? { shadow: { color, blur: view.glow } } : {}
2172
- };
2477
+ function markDash(theme, dash) {
2478
+ if (dash === "dashed") return theme.dash;
2479
+ if (dash === "dotted") return theme.dot;
2480
+ return [];
2173
2481
  }
2174
- function proceduralShapes(view, pos, fade, progress) {
2175
- const size = view.size ?? 0.5;
2176
- const color = view.color ?? DEFAULT_SPARK_COLOR;
2177
- switch (view.shape ?? "spark") {
2178
- case "ring": {
2179
- return [
2180
- {
2181
- type: "draw-shape",
2182
- shape: "ellipse",
2183
- position: pos,
2184
- anchor: "center",
2185
- radiusX: size * easeOut(progress),
2186
- stroke: view.color2 ?? color,
2187
- strokeWidth: 2,
2188
- blendMode: "lighter",
2189
- opacity: fade,
2190
- ...view.glow ? { shadow: { color, blur: view.glow } } : {}
2191
- }
2192
- ];
2193
- }
2194
- case "puff": {
2195
- const count = view.count ?? 3;
2196
- return Array.from({ length: count }, (_, i) => {
2197
- const angle = fxHash01(view.id, i * 5) * Math.PI * 2;
2198
- const drift = size * 0.3 * fxHash01(view.id, i * 5 + 1) * easeOut(progress);
2199
- return {
2200
- type: "draw-shape",
2201
- shape: "ellipse",
2202
- position: { ...pos, x: pos.x + Math.cos(angle) * drift, y: pos.y + Math.sin(angle) * drift },
2203
- anchor: "center",
2204
- radiusX: size * (0.15 + 0.35 * progress) * (0.7 + 0.6 * fxHash01(view.id, i * 5 + 2)),
2205
- fill: i === 0 && view.color2 ? view.color2 : color,
2206
- opacity: fade * 0.6,
2207
- ...view.glow ? { shadow: { color, blur: view.glow } } : {}
2208
- };
2209
- });
2210
- }
2211
- case "streak": {
2212
- const count = view.count ?? 5;
2213
- return Array.from({ length: count }, (_, i) => {
2214
- const angle = fxHash01(view.id, i * 3) * Math.PI * 2;
2215
- const inner = size * (0.2 + 0.8 * easeOut(progress)) * (0.6 + 0.4 * fxHash01(view.id, i * 3 + 1));
2216
- const len = size * 0.35;
2217
- return {
2218
- type: "draw-shape",
2219
- shape: "ellipse",
2220
- position: pos,
2221
- anchor: "center",
2222
- offsetX: inner + len / 2,
2223
- offsetY: 0,
2224
- radiusX: len / 2,
2225
- radiusY: size * 0.04,
2226
- rotate: angle,
2227
- fill: color,
2228
- blendMode: "lighter",
2229
- opacity: fade,
2230
- ...view.glow ? { shadow: { color, blur: view.glow } } : {}
2231
- };
2232
- });
2482
+ function wobble(x, y, k) {
2483
+ const s = Math.sin(x * 12.9898 + y * 78.233 + k * 37.719) * 43758.5453;
2484
+ return (s - Math.floor(s)) * 2 - 1;
2485
+ }
2486
+ function tracePolyline(ctx, theme, points, closed) {
2487
+ ctx.beginPath();
2488
+ if (points.length === 0) return;
2489
+ const amp = theme.roughness;
2490
+ const pts = closed ? [...points, points[0]] : [...points];
2491
+ ctx.moveTo(pts[0].x + amp * wobble(pts[0].x, pts[0].y, 0), pts[0].y + amp * wobble(pts[0].y, pts[0].x, 1));
2492
+ for (let i = 1; i < pts.length; i++) {
2493
+ const a = pts[i - 1];
2494
+ const b = pts[i];
2495
+ if (amp === 0) {
2496
+ ctx.lineTo(b.x, b.y);
2497
+ continue;
2233
2498
  }
2234
- default: {
2235
- const count = view.count ?? 6;
2236
- return Array.from({ length: count }, (_, i) => sparkShape(view, pos, i, fade, progress));
2499
+ const steps = Math.max(1, Math.round(Math.hypot(b.x - a.x, b.y - a.y) / 24));
2500
+ for (let s = 1; s <= steps; s++) {
2501
+ const t = s / steps;
2502
+ const x = a.x + (b.x - a.x) * t;
2503
+ const y = a.y + (b.y - a.y) * t;
2504
+ ctx.lineTo(x + amp * wobble(x, y, 2), y + amp * wobble(y, x, 3));
2237
2505
  }
2238
2506
  }
2507
+ if (closed) ctx.closePath();
2239
2508
  }
2240
- function expandFxItem(view, node, epochNowMs, dim, projector, fontFamily) {
2241
- if (view.space === "screen") return [];
2242
- const tickMs = node.tickMs ?? DEFAULT_TICK_MS;
2243
- const { ageMs, progress, fade } = fxLifecycle(view, epochNowMs, tickMs);
2244
- if (progress >= 1) return [];
2245
- const pos = fxPosition(view, dim, ageMs / 1e3);
2246
- if (!pos) return [];
2509
+ function arcPoints(cx, cy, rx, ry, startRad = 0, endRad = Math.PI * 2) {
2510
+ const steps = Math.max(12, Math.round(Math.abs(endRad - startRad) * Math.max(rx, ry) / 8));
2247
2511
  const out = [];
2248
- const artItems = node.art?.[view.type]?.["idle"];
2249
- const sprite = node.sprites?.[view.type];
2250
- if (Array.isArray(artItems)) {
2251
- out.push({
2252
- type: "draw-group",
2253
- position: pos,
2254
- opacity: fade,
2255
- ...view.size !== void 0 ? { scale: view.size } : {},
2256
- items: artItems
2257
- });
2258
- } else if (sprite?.url) {
2259
- const spriteSize = view.size ?? 0.6;
2260
- out.push({
2261
- type: "draw-sprite",
2262
- position: pos,
2263
- asset: sprite,
2264
- anchor: "center",
2265
- width: spriteSize,
2266
- height: spriteSize,
2267
- opacity: fade,
2268
- animation: view.animation ?? sprite.animations?.[0],
2269
- // One-shot fx rows (burst) play from the item's birth, not wall-time.
2270
- clockMs: ageMs
2271
- });
2272
- } else {
2273
- out.push(...proceduralShapes(view, pos, fade, progress));
2274
- }
2275
- if (view.message) {
2276
- const pxSize = projector && view.size !== void 0 ? Math.max(10, Math.round(view.size * projector.tileWidth)) : void 0;
2277
- const family = fontFamily ?? "system-ui, sans-serif";
2278
- const text = {
2279
- type: "draw-text",
2280
- text: view.message,
2281
- position: pos,
2282
- offsetY: -(0.15 + 0.55 * progress),
2283
- color: view.color ?? node.textColor ?? DEFAULT_TEXT_COLOR,
2284
- opacity: fade,
2285
- font: `bold ${pxSize ?? 14}px ${family}`
2286
- };
2287
- out.push(text);
2512
+ for (let i = 0; i <= steps; i++) {
2513
+ const t = startRad + (endRad - startRad) * i / steps;
2514
+ out.push({ x: cx + rx * Math.cos(t), y: cy + ry * Math.sin(t) });
2288
2515
  }
2289
2516
  return out;
2290
2517
  }
2291
- function expandFxLayer(node, epochNowMs, dim, projector, fontFamily) {
2292
- if (!Array.isArray(node.items)) return [];
2293
- const out = [];
2294
- for (const item of node.items) {
2295
- if (!item || typeof item.id !== "string") continue;
2296
- out.push(...expandFxItem(resolveFxView(item, node.presets), node, epochNowMs, dim, projector, fontFamily));
2518
+ function fillMark(ctx, theme, color, style = theme.fillStyle) {
2519
+ if (style === "outline") return;
2520
+ ctx.save();
2521
+ ctx.shadowBlur = 0;
2522
+ ctx.shadowColor = "transparent";
2523
+ if (theme.shadow) {
2524
+ ctx.shadowColor = theme.shadow.color;
2525
+ ctx.shadowOffsetX = theme.shadow.offsetX;
2526
+ ctx.shadowOffsetY = theme.shadow.offsetY;
2527
+ ctx.shadowBlur = theme.shadow.blur;
2297
2528
  }
2298
- return out;
2529
+ if (style === "hatch") {
2530
+ ctx.clip();
2531
+ const span = ctx.canvas.width + ctx.canvas.height;
2532
+ ctx.strokeStyle = color;
2533
+ ctx.lineWidth = theme.strokes.thin;
2534
+ ctx.setLineDash([]);
2535
+ ctx.beginPath();
2536
+ for (let d = -span; d < span; d += 6) {
2537
+ ctx.moveTo(d, 0);
2538
+ ctx.lineTo(d + span, span);
2539
+ }
2540
+ ctx.stroke();
2541
+ } else {
2542
+ ctx.globalAlpha *= style === "tint" ? theme.fillOpacity : 1;
2543
+ ctx.fillStyle = color;
2544
+ ctx.fill();
2545
+ }
2546
+ ctx.restore();
2299
2547
  }
2300
- function DrawFxLayer(_props) {
2301
- return null;
2548
+ function drawMarker(ctx, theme, x1, y1, x2, y2, color, width) {
2549
+ const size = Math.max(8, width * 4);
2550
+ const angle = Math.atan2(y2 - y1, x2 - x1);
2551
+ const left = { x: x2 - size * Math.cos(angle - Math.PI / 6), y: y2 - size * Math.sin(angle - Math.PI / 6) };
2552
+ const right = { x: x2 - size * Math.cos(angle + Math.PI / 6), y: y2 - size * Math.sin(angle + Math.PI / 6) };
2553
+ ctx.save();
2554
+ ctx.strokeStyle = color;
2555
+ ctx.fillStyle = color;
2556
+ ctx.lineWidth = width;
2557
+ ctx.setLineDash([]);
2558
+ switch (theme.marker) {
2559
+ case "triangle": {
2560
+ ctx.beginPath();
2561
+ ctx.moveTo(x2, y2);
2562
+ ctx.lineTo(left.x, left.y);
2563
+ ctx.lineTo(right.x, right.y);
2564
+ ctx.closePath();
2565
+ ctx.fill();
2566
+ break;
2567
+ }
2568
+ case "open":
2569
+ case "line": {
2570
+ ctx.beginPath();
2571
+ ctx.moveTo(left.x, left.y);
2572
+ ctx.lineTo(x2, y2);
2573
+ ctx.lineTo(right.x, right.y);
2574
+ if (theme.marker === "open") ctx.stroke();
2575
+ else {
2576
+ ctx.moveTo(x2 - size * 0.5 * Math.cos(angle + Math.PI / 2), y2 - size * 0.5 * Math.sin(angle + Math.PI / 2));
2577
+ ctx.lineTo(x2 + size * 0.5 * Math.cos(angle + Math.PI / 2), y2 + size * 0.5 * Math.sin(angle + Math.PI / 2));
2578
+ ctx.stroke();
2579
+ }
2580
+ break;
2581
+ }
2582
+ case "dot": {
2583
+ ctx.beginPath();
2584
+ ctx.arc(x2, y2, Math.max(2.5, width * 1.5), 0, Math.PI * 2);
2585
+ ctx.fill();
2586
+ break;
2587
+ }
2588
+ }
2589
+ ctx.restore();
2590
+ }
2591
+ function useCanvasTheme(scopeRef) {
2592
+ const { appliedTheme } = useTheme();
2593
+ const scopeKey = JSON.stringify(useThemeScope());
2594
+ const [state, setState] = useState({ theme: null, version: 0 });
2595
+ useLayoutEffect(() => {
2596
+ const scope = scopeRef.current;
2597
+ if (!scope) return;
2598
+ const theme = resolveCanvasTheme(scope);
2599
+ setState((prev) => ({ theme, version: prev.version + 1 }));
2600
+ }, [appliedTheme, scopeKey, scopeRef]);
2601
+ return state;
2302
2602
  }
2303
2603
 
2304
2604
  // lib/drawable/themeDrawables.ts
@@ -2499,4 +2799,4 @@ function rotatePoint(p, center, radians) {
2499
2799
  return { x: center.x + dx * cos - dy * sin, y: center.y + dx * sin + dy * cos };
2500
2800
  }
2501
2801
 
2502
- export { AVL_FONT, AVL_INK, AVL_STROKE, Box, CONFETTI_BURST_KEYFRAMES, CONNECTION_COLORS, DrawFxLayer, DrawGroup, DrawMesh, DrawShape, DrawSkinnedMesh, DrawableRegistryContext, EFFECT_CATEGORY_COLORS, HStack, OPERATOR_CATEGORY_COLORS, SPRITE_FALLBACK_FILL, SPRITE_FALLBACK_STROKE, STATE_COLORS, STRETCHED_ABOVE, STRETCHED_CONTROL, Stack, THEME_SERIES, VStack, applyLineCharacter, applyMeshAnimation, applySceneMaterials, applyShapeAnimation, arcPoints, asEffectOperator, atlasFailed, atlasUrlOf, avlBlend, avlSeries, avlTint, blit, buildHitIndex, clampSegments, collectDrawnItems, createConfettiParticles, drawMarker, effectCategoryOf, effectZoneOf, expandFxLayer, fillLabel, fillMark, fxHash01, getAtlas, getImageStatus, getOrLoadImage, getStateRole, hitTestSprites, isAnimatedGroup, isAnimatedMesh, isAnimatedShape, isAtlasAsset, isSpriteSheetAtlas, isValidScenePos, labelFont, labelText, markColor, markDash, markStrokeWidth, outlineCarriesFill, outlineStroke, paintShape, paintSkinnedMesh, parseApplicationLevel, parseOrbitalLevel, parseTraitLevel, parseTransitionLevel, polyhedronBounds, pressableProps, resolveAssetSource, resolveDrawableColors, resolveThemeColor, rowOpenControlProps, spriteAssetState, spriteRect, subRectFor, tracePolyline, traitEventWires, traitLevelFromTrait, traitTransitionRenderUi, useCanvasTheme, withPreviewPosition, withSeries };
2802
+ export { AVL_FONT, AVL_INK, AVL_STROKE, Box, CONFETTI_BURST_KEYFRAMES, CONNECTION_COLORS, DrawFxLayer, DrawGroup, DrawMesh, DrawShape, DrawSkinnedMesh, DrawableRegistryContext, EFFECT_CATEGORY_COLORS, HStack, OPERATOR_CATEGORY_COLORS, SPRITE_FALLBACK_FILL, SPRITE_FALLBACK_STROKE, STATE_COLORS, STRETCHED_ABOVE, STRETCHED_CONTROL, Stack, THEME_SERIES, VStack, applyLineCharacter, applyMeshAnimation, applySceneMaterials, applyShapeAnimation, arcPoints, asEffectOperator, atlasFailed, atlasUrlOf, avlBlend, avlSeries, avlTint, blit, buildHitIndex, clampSegments, collectDrawnItems, createConfettiParticles, drawMarker, effectCategoryOf, effectZoneOf, expandFxLayer, fillLabel, fillMark, fxHash01, getAtlas, getImageStatus, getOrLoadImage, getStateRole, hitTestSprites, isAnimatedGroup, isAnimatedMesh, isAnimatedShape, isAtlasAsset, isSpriteSheetAtlas, isValidScenePos, labelFont, labelText, layoutScale, markColor, markDash, markStrokeWidth, motionTiming, outlineCarriesFill, outlineStroke, paintShape, paintSkinnedMesh, parseApplicationLevel, parseOrbitalLevel, parseTraitLevel, parseTransitionLevel, polyhedronBounds, pressableProps, resolveAssetSource, resolveDrawableColors, resolveThemeColor, rowOpenControlProps, spriteAssetState, spriteRect, subRectFor, tracePolyline, traitEventWires, traitLevelFromTrait, traitTransitionRenderUi, useCanvasTheme, withPreviewPosition, withSeries };