@almadar/ui 5.94.0 → 5.95.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.
@@ -8549,6 +8549,462 @@ var init_ComponentPatterns = __esm({
8549
8549
  AlertPattern.displayName = "AlertPattern";
8550
8550
  }
8551
8551
  });
8552
+ function resolveColor2(color, ctx, fallback) {
8553
+ if (!color) return fallback;
8554
+ if (color.startsWith("var(")) {
8555
+ ctx.canvas.style;
8556
+ const m = /^var\((--[^,)]+)(?:,\s*([^)]+))?\)$/.exec(color);
8557
+ if (m) {
8558
+ const computed = getComputedStyle(ctx.canvas).getPropertyValue(m[1]).trim();
8559
+ return computed || m[2]?.trim() || fallback;
8560
+ }
8561
+ }
8562
+ return color;
8563
+ }
8564
+ function shapeBounds(shape) {
8565
+ switch (shape.type) {
8566
+ case "line":
8567
+ case "arrow":
8568
+ if (shape.x1 == null || shape.y1 == null || shape.x2 == null || shape.y2 == null) return null;
8569
+ return {
8570
+ x: Math.min(shape.x1, shape.x2) - 6,
8571
+ y: Math.min(shape.y1, shape.y2) - 6,
8572
+ w: Math.abs(shape.x2 - shape.x1) + 12,
8573
+ h: Math.abs(shape.y2 - shape.y1) + 12
8574
+ };
8575
+ case "circle":
8576
+ if (shape.x == null || shape.y == null || shape.radius == null) return null;
8577
+ return {
8578
+ x: shape.x - shape.radius - 4,
8579
+ y: shape.y - shape.radius - 4,
8580
+ w: shape.radius * 2 + 8,
8581
+ h: shape.radius * 2 + 8
8582
+ };
8583
+ case "rect":
8584
+ if (shape.x == null || shape.y == null || shape.width == null || shape.height == null) return null;
8585
+ return { x: shape.x - 4, y: shape.y - 4, w: shape.width + 8, h: shape.height + 8 };
8586
+ case "polygon":
8587
+ if (!shape.points || shape.points.length === 0) return null;
8588
+ {
8589
+ const xs = shape.points.map((p) => p.x);
8590
+ const ys = shape.points.map((p) => p.y);
8591
+ const minX = Math.min(...xs);
8592
+ const minY = Math.min(...ys);
8593
+ return {
8594
+ x: minX - 4,
8595
+ y: minY - 4,
8596
+ w: Math.max(...xs) - minX + 8,
8597
+ h: Math.max(...ys) - minY + 8
8598
+ };
8599
+ }
8600
+ case "text":
8601
+ if (shape.x == null || shape.y == null) return null;
8602
+ return { x: shape.x - 4, y: shape.y - (shape.fontSize ?? 14) - 4, w: 120, h: (shape.fontSize ?? 14) + 8 };
8603
+ default:
8604
+ return null;
8605
+ }
8606
+ }
8607
+ function drawArrowHead(ctx, x1, y1, x2, y2, size) {
8608
+ const angle = Math.atan2(y2 - y1, x2 - x1);
8609
+ ctx.beginPath();
8610
+ ctx.moveTo(x2, y2);
8611
+ ctx.lineTo(x2 - size * Math.cos(angle - Math.PI / 6), y2 - size * Math.sin(angle - Math.PI / 6));
8612
+ ctx.lineTo(x2 - size * Math.cos(angle + Math.PI / 6), y2 - size * Math.sin(angle + Math.PI / 6));
8613
+ ctx.closePath();
8614
+ ctx.fill();
8615
+ }
8616
+ function drawShape(ctx, shape, width, height) {
8617
+ ctx.save();
8618
+ const opacity = shape.opacity ?? 1;
8619
+ ctx.globalAlpha = opacity;
8620
+ const stroke = resolveColor2(shape.color, ctx, "#333333");
8621
+ const fill = shape.fill ? resolveColor2(shape.fill, ctx, "#cccccc") : void 0;
8622
+ ctx.lineWidth = shape.lineWidth ?? 2;
8623
+ switch (shape.type) {
8624
+ case "grid": {
8625
+ const step = shape.step ?? 40;
8626
+ ctx.strokeStyle = stroke;
8627
+ ctx.globalAlpha = opacity * 0.25;
8628
+ ctx.lineWidth = 1;
8629
+ ctx.beginPath();
8630
+ for (let x = 0; x <= width; x += step) {
8631
+ ctx.moveTo(x, 0);
8632
+ ctx.lineTo(x, height);
8633
+ }
8634
+ for (let y = 0; y <= height; y += step) {
8635
+ ctx.moveTo(0, y);
8636
+ ctx.lineTo(width, y);
8637
+ }
8638
+ ctx.stroke();
8639
+ break;
8640
+ }
8641
+ case "axis": {
8642
+ const axis = shape.axis ?? "x";
8643
+ ctx.strokeStyle = stroke;
8644
+ ctx.lineWidth = shape.lineWidth ?? 2;
8645
+ ctx.beginPath();
8646
+ if (axis === "x") {
8647
+ ctx.moveTo(0, height / 2);
8648
+ ctx.lineTo(width, height / 2);
8649
+ } else {
8650
+ ctx.moveTo(width / 2, 0);
8651
+ ctx.lineTo(width / 2, height);
8652
+ }
8653
+ ctx.stroke();
8654
+ break;
8655
+ }
8656
+ case "line": {
8657
+ if (shape.x1 == null || shape.y1 == null || shape.x2 == null || shape.y2 == null) break;
8658
+ ctx.strokeStyle = stroke;
8659
+ ctx.beginPath();
8660
+ ctx.moveTo(shape.x1, shape.y1);
8661
+ ctx.lineTo(shape.x2, shape.y2);
8662
+ ctx.stroke();
8663
+ break;
8664
+ }
8665
+ case "arrow": {
8666
+ if (shape.x1 == null || shape.y1 == null || shape.x2 == null || shape.y2 == null) break;
8667
+ ctx.strokeStyle = stroke;
8668
+ ctx.fillStyle = stroke;
8669
+ ctx.beginPath();
8670
+ ctx.moveTo(shape.x1, shape.y1);
8671
+ ctx.lineTo(shape.x2, shape.y2);
8672
+ ctx.stroke();
8673
+ drawArrowHead(ctx, shape.x1, shape.y1, shape.x2, shape.y2, 10);
8674
+ break;
8675
+ }
8676
+ case "circle": {
8677
+ if (shape.x == null || shape.y == null || shape.radius == null) break;
8678
+ ctx.beginPath();
8679
+ ctx.arc(shape.x, shape.y, shape.radius, 0, Math.PI * 2);
8680
+ if (fill) {
8681
+ ctx.fillStyle = fill;
8682
+ ctx.fill();
8683
+ }
8684
+ ctx.strokeStyle = stroke;
8685
+ ctx.stroke();
8686
+ break;
8687
+ }
8688
+ case "rect": {
8689
+ if (shape.x == null || shape.y == null || shape.width == null || shape.height == null) break;
8690
+ if (fill) {
8691
+ ctx.fillStyle = fill;
8692
+ ctx.fillRect(shape.x, shape.y, shape.width, shape.height);
8693
+ }
8694
+ ctx.strokeStyle = stroke;
8695
+ ctx.strokeRect(shape.x, shape.y, shape.width, shape.height);
8696
+ break;
8697
+ }
8698
+ case "polygon": {
8699
+ if (!shape.points || shape.points.length < 2) break;
8700
+ ctx.beginPath();
8701
+ ctx.moveTo(shape.points[0].x, shape.points[0].y);
8702
+ for (let i = 1; i < shape.points.length; i++) {
8703
+ ctx.lineTo(shape.points[i].x, shape.points[i].y);
8704
+ }
8705
+ ctx.closePath();
8706
+ if (fill) {
8707
+ ctx.fillStyle = fill;
8708
+ ctx.fill();
8709
+ }
8710
+ ctx.strokeStyle = stroke;
8711
+ ctx.stroke();
8712
+ break;
8713
+ }
8714
+ case "path": {
8715
+ if (!shape.path) break;
8716
+ const p = new Path2D(shape.path);
8717
+ if (fill) {
8718
+ ctx.fillStyle = fill;
8719
+ ctx.fill(p);
8720
+ }
8721
+ ctx.strokeStyle = stroke;
8722
+ ctx.stroke(p);
8723
+ break;
8724
+ }
8725
+ case "text": {
8726
+ if (shape.x == null || shape.y == null || !shape.text) break;
8727
+ ctx.fillStyle = stroke;
8728
+ ctx.font = `${shape.fontSize ?? 14}px system-ui, sans-serif`;
8729
+ ctx.textAlign = shape.align ?? "left";
8730
+ ctx.textBaseline = "middle";
8731
+ ctx.fillText(shape.text, shape.x, shape.y);
8732
+ break;
8733
+ }
8734
+ }
8735
+ ctx.restore();
8736
+ }
8737
+ var LearningCanvas;
8738
+ var init_LearningCanvas = __esm({
8739
+ "components/learning/atoms/LearningCanvas.tsx"() {
8740
+ "use client";
8741
+ init_cn();
8742
+ init_useEventBus();
8743
+ LearningCanvas = ({
8744
+ className,
8745
+ width = 600,
8746
+ height = 400,
8747
+ backgroundColor,
8748
+ shapes = [],
8749
+ interactive = false,
8750
+ animate = false,
8751
+ onShapeClick,
8752
+ onShapeHover,
8753
+ isLoading,
8754
+ error
8755
+ }) => {
8756
+ const canvasRef = useRef(null);
8757
+ const eventBus = useEventBus();
8758
+ const animRef = useRef(0);
8759
+ const hoverIndexRef = useRef(-1);
8760
+ const findShapeAt = useCallback((clientX, clientY) => {
8761
+ const canvas = canvasRef.current;
8762
+ if (!canvas) return -1;
8763
+ const rect = canvas.getBoundingClientRect();
8764
+ const x = clientX - rect.left;
8765
+ const y = clientY - rect.top;
8766
+ for (let i = shapes.length - 1; i >= 0; i--) {
8767
+ const b = shapeBounds(shapes[i]);
8768
+ if (b && x >= b.x && x <= b.x + b.w && y >= b.y && y <= b.y + b.h) {
8769
+ return i;
8770
+ }
8771
+ }
8772
+ return -1;
8773
+ }, [shapes]);
8774
+ const draw = useCallback(() => {
8775
+ const canvas = canvasRef.current;
8776
+ if (!canvas) return;
8777
+ const ctx = canvas.getContext("2d");
8778
+ if (!ctx) return;
8779
+ const dpr = typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1;
8780
+ canvas.width = Math.max(1, Math.floor(width * dpr));
8781
+ canvas.height = Math.max(1, Math.floor(height * dpr));
8782
+ canvas.style.width = `${width}px`;
8783
+ canvas.style.height = `${height}px`;
8784
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
8785
+ ctx.clearRect(0, 0, width, height);
8786
+ if (backgroundColor) {
8787
+ ctx.fillStyle = backgroundColor;
8788
+ ctx.fillRect(0, 0, width, height);
8789
+ }
8790
+ for (const shape of shapes) {
8791
+ drawShape(ctx, shape, width, height);
8792
+ }
8793
+ }, [width, height, backgroundColor, shapes]);
8794
+ useEffect(() => {
8795
+ draw();
8796
+ }, [draw]);
8797
+ useEffect(() => {
8798
+ if (!animate) return;
8799
+ const loop = () => {
8800
+ draw();
8801
+ animRef.current = requestAnimationFrame(loop);
8802
+ };
8803
+ animRef.current = requestAnimationFrame(loop);
8804
+ return () => cancelAnimationFrame(animRef.current);
8805
+ }, [animate, draw]);
8806
+ const handlePointerMove = useCallback(
8807
+ (e) => {
8808
+ if (!interactive) return;
8809
+ const idx = findShapeAt(e.clientX, e.clientY);
8810
+ if (idx !== hoverIndexRef.current) {
8811
+ hoverIndexRef.current = idx;
8812
+ if (idx >= 0) {
8813
+ const shape = shapes[idx];
8814
+ const payload = { id: shape.id, type: shape.type, index: idx };
8815
+ if (onShapeHover) onShapeHover(payload);
8816
+ else if (eventBus) eventBus.emit(`UI:SHAPE_HOVER`, payload);
8817
+ }
8818
+ }
8819
+ },
8820
+ [interactive, onShapeHover, eventBus, findShapeAt, shapes]
8821
+ );
8822
+ const handleClick = useCallback(
8823
+ (e) => {
8824
+ if (!interactive) return;
8825
+ const idx = findShapeAt(e.clientX, e.clientY);
8826
+ if (idx >= 0) {
8827
+ const shape = shapes[idx];
8828
+ const payload = { id: shape.id, type: shape.type, index: idx };
8829
+ if (onShapeClick) onShapeClick(payload);
8830
+ else if (eventBus) eventBus.emit(`UI:SHAPE_CLICK`, payload);
8831
+ }
8832
+ },
8833
+ [interactive, onShapeClick, eventBus, findShapeAt, shapes]
8834
+ );
8835
+ if (isLoading || error) {
8836
+ return /* @__PURE__ */ jsx(
8837
+ "div",
8838
+ {
8839
+ className: cn(
8840
+ "flex items-center justify-center rounded border border-border bg-surface",
8841
+ className
8842
+ ),
8843
+ style: { width, height },
8844
+ children: error ? /* @__PURE__ */ jsx("span", { className: "text-sm text-destructive", children: error.message }) : /* @__PURE__ */ jsx("span", { className: "text-sm text-muted-foreground", children: "Loading canvas\u2026" })
8845
+ }
8846
+ );
8847
+ }
8848
+ return /* @__PURE__ */ jsx(
8849
+ "canvas",
8850
+ {
8851
+ ref: canvasRef,
8852
+ className: cn("block touch-none rounded border border-border", className),
8853
+ style: { width, height },
8854
+ onClick: handleClick,
8855
+ onPointerMove: handlePointerMove
8856
+ }
8857
+ );
8858
+ };
8859
+ }
8860
+ });
8861
+ var DEFAULT_BAR_COLOR, DEFAULT_CELL_COLOR, DEFAULT_POINTER_COLOR, POINTER_BAND, TOP_PAD, AlgorithmCanvas;
8862
+ var init_AlgorithmCanvas = __esm({
8863
+ "components/learning/molecules/AlgorithmCanvas.tsx"() {
8864
+ "use client";
8865
+ init_atoms();
8866
+ init_Stack();
8867
+ init_LearningCanvas();
8868
+ DEFAULT_BAR_COLOR = "#3b82f6";
8869
+ DEFAULT_CELL_COLOR = "#e5e7eb";
8870
+ DEFAULT_POINTER_COLOR = "#dc2626";
8871
+ POINTER_BAND = 34;
8872
+ TOP_PAD = 12;
8873
+ AlgorithmCanvas = ({
8874
+ className,
8875
+ width = 600,
8876
+ height = 400,
8877
+ title,
8878
+ backgroundColor,
8879
+ bars = [],
8880
+ cells = [],
8881
+ pointers = [],
8882
+ shapes = [],
8883
+ interactive = false,
8884
+ animate = false,
8885
+ onShapeClick,
8886
+ isLoading,
8887
+ error
8888
+ }) => {
8889
+ const derivedShapes = useMemo(() => {
8890
+ const out = [];
8891
+ if (bars.length > 0) {
8892
+ const slot = width / bars.length;
8893
+ const barW = slot * 0.8;
8894
+ const gap = slot * 0.1;
8895
+ const baseline = height - POINTER_BAND;
8896
+ const usableH = baseline - TOP_PAD;
8897
+ const maxV = Math.max(1, ...bars.map((b) => Number.isFinite(b.value) ? b.value : 0));
8898
+ bars.forEach((bar, i) => {
8899
+ const v = Number.isFinite(bar.value) ? bar.value : 0;
8900
+ const bh = Math.max(0, v / maxV * usableH);
8901
+ const x = i * slot + gap;
8902
+ const color = bar.color ?? DEFAULT_BAR_COLOR;
8903
+ out.push({
8904
+ type: "rect",
8905
+ id: `bar-${i}`,
8906
+ x,
8907
+ y: baseline - bh,
8908
+ width: barW,
8909
+ height: bh,
8910
+ color,
8911
+ fill: color
8912
+ });
8913
+ const label = bar.label ?? (bars.length <= 24 ? String(v) : void 0);
8914
+ if (label) {
8915
+ out.push({
8916
+ type: "text",
8917
+ x: x + barW / 2,
8918
+ y: baseline - bh - 8,
8919
+ text: label,
8920
+ color: "#374151",
8921
+ fontSize: 11,
8922
+ align: "center"
8923
+ });
8924
+ }
8925
+ });
8926
+ pointers.forEach((p) => {
8927
+ if (p.index < 0 || p.index >= bars.length) return;
8928
+ const cx = p.index * slot + slot / 2;
8929
+ const color = p.color ?? DEFAULT_POINTER_COLOR;
8930
+ out.push({
8931
+ type: "arrow",
8932
+ x1: cx,
8933
+ y1: height - 6,
8934
+ x2: cx,
8935
+ y2: baseline + 4,
8936
+ color,
8937
+ lineWidth: 2
8938
+ });
8939
+ if (p.label) {
8940
+ out.push({
8941
+ type: "text",
8942
+ x: cx,
8943
+ y: height - 22,
8944
+ text: p.label,
8945
+ color,
8946
+ fontSize: 11,
8947
+ align: "center"
8948
+ });
8949
+ }
8950
+ });
8951
+ }
8952
+ if (cells.length > 0) {
8953
+ const maxCol = Math.max(0, ...cells.map((c) => c.col)) + 1;
8954
+ const maxRow = Math.max(0, ...cells.map((c) => c.row)) + 1;
8955
+ const cw = width / maxCol;
8956
+ const ch = height / maxRow;
8957
+ cells.forEach((c, i) => {
8958
+ const x = c.col * cw;
8959
+ const y = c.row * ch;
8960
+ const color = c.color ?? DEFAULT_CELL_COLOR;
8961
+ out.push({
8962
+ type: "rect",
8963
+ id: `cell-${i}`,
8964
+ x: x + 1,
8965
+ y: y + 1,
8966
+ width: cw - 2,
8967
+ height: ch - 2,
8968
+ color: "#9ca3af",
8969
+ fill: color
8970
+ });
8971
+ const label = c.label ?? (c.value != null ? String(c.value) : void 0);
8972
+ if (label && cw >= 18 && ch >= 14) {
8973
+ out.push({
8974
+ type: "text",
8975
+ x: x + cw / 2,
8976
+ y: y + ch / 2,
8977
+ text: label,
8978
+ color: "#111827",
8979
+ fontSize: 12,
8980
+ align: "center"
8981
+ });
8982
+ }
8983
+ });
8984
+ }
8985
+ out.push(...shapes);
8986
+ return out;
8987
+ }, [bars, cells, pointers, shapes, width, height]);
8988
+ return /* @__PURE__ */ jsx(Card, { className, children: /* @__PURE__ */ jsxs(VStack, { gap: "sm", children: [
8989
+ title ? /* @__PURE__ */ jsx(Typography, { variant: "h4", children: title }) : null,
8990
+ /* @__PURE__ */ jsx(
8991
+ LearningCanvas,
8992
+ {
8993
+ width,
8994
+ height,
8995
+ backgroundColor,
8996
+ shapes: derivedShapes,
8997
+ interactive,
8998
+ animate,
8999
+ onShapeClick,
9000
+ isLoading,
9001
+ error
9002
+ }
9003
+ )
9004
+ ] }) });
9005
+ };
9006
+ }
9007
+ });
8552
9008
  var AuthLayout;
8553
9009
  var init_AuthLayout = __esm({
8554
9010
  "components/marketing/templates/AuthLayout.tsx"() {
@@ -9517,315 +9973,6 @@ var init_BehaviorView = __esm({
9517
9973
  BehaviorView.displayName = "BehaviorView";
9518
9974
  }
9519
9975
  });
9520
- function resolveColor2(color, ctx, fallback) {
9521
- if (!color) return fallback;
9522
- if (color.startsWith("var(")) {
9523
- ctx.canvas.style;
9524
- const m = /^var\((--[^,)]+)(?:,\s*([^)]+))?\)$/.exec(color);
9525
- if (m) {
9526
- const computed = getComputedStyle(ctx.canvas).getPropertyValue(m[1]).trim();
9527
- return computed || m[2]?.trim() || fallback;
9528
- }
9529
- }
9530
- return color;
9531
- }
9532
- function shapeBounds(shape) {
9533
- switch (shape.type) {
9534
- case "line":
9535
- case "arrow":
9536
- if (shape.x1 == null || shape.y1 == null || shape.x2 == null || shape.y2 == null) return null;
9537
- return {
9538
- x: Math.min(shape.x1, shape.x2) - 6,
9539
- y: Math.min(shape.y1, shape.y2) - 6,
9540
- w: Math.abs(shape.x2 - shape.x1) + 12,
9541
- h: Math.abs(shape.y2 - shape.y1) + 12
9542
- };
9543
- case "circle":
9544
- if (shape.x == null || shape.y == null || shape.radius == null) return null;
9545
- return {
9546
- x: shape.x - shape.radius - 4,
9547
- y: shape.y - shape.radius - 4,
9548
- w: shape.radius * 2 + 8,
9549
- h: shape.radius * 2 + 8
9550
- };
9551
- case "rect":
9552
- if (shape.x == null || shape.y == null || shape.width == null || shape.height == null) return null;
9553
- return { x: shape.x - 4, y: shape.y - 4, w: shape.width + 8, h: shape.height + 8 };
9554
- case "polygon":
9555
- if (!shape.points || shape.points.length === 0) return null;
9556
- {
9557
- const xs = shape.points.map((p) => p.x);
9558
- const ys = shape.points.map((p) => p.y);
9559
- const minX = Math.min(...xs);
9560
- const minY = Math.min(...ys);
9561
- return {
9562
- x: minX - 4,
9563
- y: minY - 4,
9564
- w: Math.max(...xs) - minX + 8,
9565
- h: Math.max(...ys) - minY + 8
9566
- };
9567
- }
9568
- case "text":
9569
- if (shape.x == null || shape.y == null) return null;
9570
- return { x: shape.x - 4, y: shape.y - (shape.fontSize ?? 14) - 4, w: 120, h: (shape.fontSize ?? 14) + 8 };
9571
- default:
9572
- return null;
9573
- }
9574
- }
9575
- function drawArrowHead(ctx, x1, y1, x2, y2, size) {
9576
- const angle = Math.atan2(y2 - y1, x2 - x1);
9577
- ctx.beginPath();
9578
- ctx.moveTo(x2, y2);
9579
- ctx.lineTo(x2 - size * Math.cos(angle - Math.PI / 6), y2 - size * Math.sin(angle - Math.PI / 6));
9580
- ctx.lineTo(x2 - size * Math.cos(angle + Math.PI / 6), y2 - size * Math.sin(angle + Math.PI / 6));
9581
- ctx.closePath();
9582
- ctx.fill();
9583
- }
9584
- function drawShape(ctx, shape, width, height) {
9585
- ctx.save();
9586
- const opacity = shape.opacity ?? 1;
9587
- ctx.globalAlpha = opacity;
9588
- const stroke = resolveColor2(shape.color, ctx, "#333333");
9589
- const fill = shape.fill ? resolveColor2(shape.fill, ctx, "#cccccc") : void 0;
9590
- ctx.lineWidth = shape.lineWidth ?? 2;
9591
- switch (shape.type) {
9592
- case "grid": {
9593
- const step = shape.step ?? 40;
9594
- ctx.strokeStyle = stroke;
9595
- ctx.globalAlpha = opacity * 0.25;
9596
- ctx.lineWidth = 1;
9597
- ctx.beginPath();
9598
- for (let x = 0; x <= width; x += step) {
9599
- ctx.moveTo(x, 0);
9600
- ctx.lineTo(x, height);
9601
- }
9602
- for (let y = 0; y <= height; y += step) {
9603
- ctx.moveTo(0, y);
9604
- ctx.lineTo(width, y);
9605
- }
9606
- ctx.stroke();
9607
- break;
9608
- }
9609
- case "axis": {
9610
- const axis = shape.axis ?? "x";
9611
- ctx.strokeStyle = stroke;
9612
- ctx.lineWidth = shape.lineWidth ?? 2;
9613
- ctx.beginPath();
9614
- if (axis === "x") {
9615
- ctx.moveTo(0, height / 2);
9616
- ctx.lineTo(width, height / 2);
9617
- } else {
9618
- ctx.moveTo(width / 2, 0);
9619
- ctx.lineTo(width / 2, height);
9620
- }
9621
- ctx.stroke();
9622
- break;
9623
- }
9624
- case "line": {
9625
- if (shape.x1 == null || shape.y1 == null || shape.x2 == null || shape.y2 == null) break;
9626
- ctx.strokeStyle = stroke;
9627
- ctx.beginPath();
9628
- ctx.moveTo(shape.x1, shape.y1);
9629
- ctx.lineTo(shape.x2, shape.y2);
9630
- ctx.stroke();
9631
- break;
9632
- }
9633
- case "arrow": {
9634
- if (shape.x1 == null || shape.y1 == null || shape.x2 == null || shape.y2 == null) break;
9635
- ctx.strokeStyle = stroke;
9636
- ctx.fillStyle = stroke;
9637
- ctx.beginPath();
9638
- ctx.moveTo(shape.x1, shape.y1);
9639
- ctx.lineTo(shape.x2, shape.y2);
9640
- ctx.stroke();
9641
- drawArrowHead(ctx, shape.x1, shape.y1, shape.x2, shape.y2, 10);
9642
- break;
9643
- }
9644
- case "circle": {
9645
- if (shape.x == null || shape.y == null || shape.radius == null) break;
9646
- ctx.beginPath();
9647
- ctx.arc(shape.x, shape.y, shape.radius, 0, Math.PI * 2);
9648
- if (fill) {
9649
- ctx.fillStyle = fill;
9650
- ctx.fill();
9651
- }
9652
- ctx.strokeStyle = stroke;
9653
- ctx.stroke();
9654
- break;
9655
- }
9656
- case "rect": {
9657
- if (shape.x == null || shape.y == null || shape.width == null || shape.height == null) break;
9658
- if (fill) {
9659
- ctx.fillStyle = fill;
9660
- ctx.fillRect(shape.x, shape.y, shape.width, shape.height);
9661
- }
9662
- ctx.strokeStyle = stroke;
9663
- ctx.strokeRect(shape.x, shape.y, shape.width, shape.height);
9664
- break;
9665
- }
9666
- case "polygon": {
9667
- if (!shape.points || shape.points.length < 2) break;
9668
- ctx.beginPath();
9669
- ctx.moveTo(shape.points[0].x, shape.points[0].y);
9670
- for (let i = 1; i < shape.points.length; i++) {
9671
- ctx.lineTo(shape.points[i].x, shape.points[i].y);
9672
- }
9673
- ctx.closePath();
9674
- if (fill) {
9675
- ctx.fillStyle = fill;
9676
- ctx.fill();
9677
- }
9678
- ctx.strokeStyle = stroke;
9679
- ctx.stroke();
9680
- break;
9681
- }
9682
- case "path": {
9683
- if (!shape.path) break;
9684
- const p = new Path2D(shape.path);
9685
- if (fill) {
9686
- ctx.fillStyle = fill;
9687
- ctx.fill(p);
9688
- }
9689
- ctx.strokeStyle = stroke;
9690
- ctx.stroke(p);
9691
- break;
9692
- }
9693
- case "text": {
9694
- if (shape.x == null || shape.y == null || !shape.text) break;
9695
- ctx.fillStyle = stroke;
9696
- ctx.font = `${shape.fontSize ?? 14}px system-ui, sans-serif`;
9697
- ctx.textAlign = shape.align ?? "left";
9698
- ctx.textBaseline = "middle";
9699
- ctx.fillText(shape.text, shape.x, shape.y);
9700
- break;
9701
- }
9702
- }
9703
- ctx.restore();
9704
- }
9705
- var LearningCanvas;
9706
- var init_LearningCanvas = __esm({
9707
- "components/learning/atoms/LearningCanvas.tsx"() {
9708
- "use client";
9709
- init_cn();
9710
- init_useEventBus();
9711
- LearningCanvas = ({
9712
- className,
9713
- width = 600,
9714
- height = 400,
9715
- backgroundColor,
9716
- shapes = [],
9717
- interactive = false,
9718
- animate = false,
9719
- onShapeClick,
9720
- onShapeHover,
9721
- isLoading,
9722
- error
9723
- }) => {
9724
- const canvasRef = useRef(null);
9725
- const eventBus = useEventBus();
9726
- const animRef = useRef(0);
9727
- const hoverIndexRef = useRef(-1);
9728
- const findShapeAt = useCallback((clientX, clientY) => {
9729
- const canvas = canvasRef.current;
9730
- if (!canvas) return -1;
9731
- const rect = canvas.getBoundingClientRect();
9732
- const x = clientX - rect.left;
9733
- const y = clientY - rect.top;
9734
- for (let i = shapes.length - 1; i >= 0; i--) {
9735
- const b = shapeBounds(shapes[i]);
9736
- if (b && x >= b.x && x <= b.x + b.w && y >= b.y && y <= b.y + b.h) {
9737
- return i;
9738
- }
9739
- }
9740
- return -1;
9741
- }, [shapes]);
9742
- const draw = useCallback(() => {
9743
- const canvas = canvasRef.current;
9744
- if (!canvas) return;
9745
- const ctx = canvas.getContext("2d");
9746
- if (!ctx) return;
9747
- const dpr = typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1;
9748
- canvas.width = Math.max(1, Math.floor(width * dpr));
9749
- canvas.height = Math.max(1, Math.floor(height * dpr));
9750
- canvas.style.width = `${width}px`;
9751
- canvas.style.height = `${height}px`;
9752
- ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
9753
- ctx.clearRect(0, 0, width, height);
9754
- if (backgroundColor) {
9755
- ctx.fillStyle = backgroundColor;
9756
- ctx.fillRect(0, 0, width, height);
9757
- }
9758
- for (const shape of shapes) {
9759
- drawShape(ctx, shape, width, height);
9760
- }
9761
- }, [width, height, backgroundColor, shapes]);
9762
- useEffect(() => {
9763
- draw();
9764
- }, [draw]);
9765
- useEffect(() => {
9766
- if (!animate) return;
9767
- const loop = () => {
9768
- draw();
9769
- animRef.current = requestAnimationFrame(loop);
9770
- };
9771
- animRef.current = requestAnimationFrame(loop);
9772
- return () => cancelAnimationFrame(animRef.current);
9773
- }, [animate, draw]);
9774
- const handlePointerMove = useCallback(
9775
- (e) => {
9776
- if (!interactive) return;
9777
- const idx = findShapeAt(e.clientX, e.clientY);
9778
- if (idx !== hoverIndexRef.current) {
9779
- hoverIndexRef.current = idx;
9780
- if (idx >= 0) {
9781
- const shape = shapes[idx];
9782
- const payload = { id: shape.id, type: shape.type, index: idx };
9783
- if (onShapeHover) onShapeHover(payload);
9784
- else if (eventBus) eventBus.emit(`UI:SHAPE_HOVER`, payload);
9785
- }
9786
- }
9787
- },
9788
- [interactive, onShapeHover, eventBus, findShapeAt, shapes]
9789
- );
9790
- const handleClick = useCallback(
9791
- (e) => {
9792
- if (!interactive) return;
9793
- const idx = findShapeAt(e.clientX, e.clientY);
9794
- if (idx >= 0) {
9795
- const shape = shapes[idx];
9796
- const payload = { id: shape.id, type: shape.type, index: idx };
9797
- if (onShapeClick) onShapeClick(payload);
9798
- else if (eventBus) eventBus.emit(`UI:SHAPE_CLICK`, payload);
9799
- }
9800
- },
9801
- [interactive, onShapeClick, eventBus, findShapeAt, shapes]
9802
- );
9803
- if (isLoading || error) {
9804
- return /* @__PURE__ */ jsx(
9805
- "div",
9806
- {
9807
- className: cn(
9808
- "flex items-center justify-center rounded border border-border bg-surface",
9809
- className
9810
- ),
9811
- style: { width, height },
9812
- children: error ? /* @__PURE__ */ jsx("span", { className: "text-sm text-destructive", children: error.message }) : /* @__PURE__ */ jsx("span", { className: "text-sm text-muted-foreground", children: "Loading canvas\u2026" })
9813
- }
9814
- );
9815
- }
9816
- return /* @__PURE__ */ jsx(
9817
- "canvas",
9818
- {
9819
- ref: canvasRef,
9820
- className: cn("block touch-none rounded border border-border", className),
9821
- style: { width, height },
9822
- onClick: handleClick,
9823
- onPointerMove: handlePointerMove
9824
- }
9825
- );
9826
- };
9827
- }
9828
- });
9829
9976
  var BiologyCanvas;
9830
9977
  var init_BiologyCanvas = __esm({
9831
9978
  "components/learning/molecules/BiologyCanvas.tsx"() {
@@ -15411,6 +15558,76 @@ var init_useImageCache = __esm({
15411
15558
  init_verificationRegistry();
15412
15559
  }
15413
15560
  });
15561
+
15562
+ // components/game/shared/atlasSlice.ts
15563
+ function isTilesheet(a) {
15564
+ return typeof a.tileWidth === "number";
15565
+ }
15566
+ function getAtlas(url, onReady) {
15567
+ if (atlasCache.has(url)) return atlasCache.get(url) ?? void 0;
15568
+ atlasCache.set(url, void 0);
15569
+ fetch(url).then((r) => r.json()).then((json) => {
15570
+ atlasCache.set(url, json);
15571
+ onReady();
15572
+ }).catch(() => {
15573
+ atlasCache.set(url, null);
15574
+ });
15575
+ return void 0;
15576
+ }
15577
+ function subRectFor(atlas, sprite) {
15578
+ if (isTilesheet(atlas)) {
15579
+ let col;
15580
+ let row;
15581
+ if (sprite.includes(",")) {
15582
+ const [c, r] = sprite.split(",").map((n) => Number(n.trim()));
15583
+ col = c;
15584
+ row = r;
15585
+ } else {
15586
+ const i = Number(sprite);
15587
+ if (!Number.isFinite(i)) return null;
15588
+ col = i % atlas.columns;
15589
+ row = Math.floor(i / atlas.columns);
15590
+ }
15591
+ if (!Number.isFinite(col) || !Number.isFinite(row) || col < 0 || row < 0 || col >= atlas.columns || row >= atlas.rows) return null;
15592
+ const margin = atlas.margin ?? 0;
15593
+ const spacing = atlas.spacing ?? 0;
15594
+ return {
15595
+ sx: margin + col * (atlas.tileWidth + spacing),
15596
+ sy: margin + row * (atlas.tileHeight + spacing),
15597
+ sw: atlas.tileWidth,
15598
+ sh: atlas.tileHeight
15599
+ };
15600
+ }
15601
+ const st = atlas.subTextures[sprite];
15602
+ if (!st) return null;
15603
+ return { sx: st.x, sy: st.y, sw: st.width, sh: st.height };
15604
+ }
15605
+ function isAtlasAsset(asset) {
15606
+ return !!asset && typeof asset.atlas === "string" && typeof asset.sprite === "string";
15607
+ }
15608
+ function resolveAssetSource(img, asset, onReady) {
15609
+ if (isAtlasAsset(asset)) {
15610
+ const atlas = getAtlas(asset.atlas, onReady);
15611
+ if (!atlas) return null;
15612
+ const rect = subRectFor(atlas, asset.sprite);
15613
+ if (!rect) return null;
15614
+ return { img, rect, aspect: rect.sw / rect.sh };
15615
+ }
15616
+ const natW = img.naturalWidth || 1;
15617
+ const natH = img.naturalHeight || 1;
15618
+ return { img, rect: null, aspect: natW / natH };
15619
+ }
15620
+ function blit(ctx, src, dx, dy, dw, dh) {
15621
+ if (src.rect) ctx.drawImage(src.img, src.rect.sx, src.rect.sy, src.rect.sw, src.rect.sh, dx, dy, dw, dh);
15622
+ else ctx.drawImage(src.img, dx, dy, dw, dh);
15623
+ }
15624
+ var atlasCache;
15625
+ var init_atlasSlice = __esm({
15626
+ "components/game/shared/atlasSlice.ts"() {
15627
+ "use client";
15628
+ atlasCache = /* @__PURE__ */ new Map();
15629
+ }
15630
+ });
15414
15631
  function useCamera() {
15415
15632
  const cameraRef = useRef({ x: 0, y: 0, zoom: 1 });
15416
15633
  const targetCameraRef = useRef(null);
@@ -16166,15 +16383,18 @@ function SideView({
16166
16383
  const platType = plat.type ?? "ground";
16167
16384
  const spriteAsset = tSprites?.[platType];
16168
16385
  const tileImg = spriteAsset ? loadImage(spriteAsset.url) : null;
16169
- if (tileImg) {
16170
- const tileW = tileImg.naturalWidth;
16171
- const tileH = tileImg.naturalHeight;
16386
+ const tileSrc = tileImg ? resolveAssetSource(tileImg, spriteAsset, NOOP) : null;
16387
+ if (tileSrc) {
16388
+ const originX = tileSrc.rect ? tileSrc.rect.sx : 0;
16389
+ const originY = tileSrc.rect ? tileSrc.rect.sy : 0;
16390
+ const tileW = tileSrc.rect ? tileSrc.rect.sw : tileImg.naturalWidth;
16391
+ const tileH = tileSrc.rect ? tileSrc.rect.sh : tileImg.naturalHeight;
16172
16392
  const scaleH = plat.height / tileH;
16173
16393
  const scaledW = tileW * scaleH;
16174
16394
  for (let tx = 0; tx < plat.width; tx += scaledW) {
16175
16395
  const drawW = Math.min(scaledW, plat.width - tx);
16176
16396
  const srcW = drawW / scaleH;
16177
- ctx.drawImage(tileImg, 0, 0, srcW, tileH, platX + tx, platY, drawW, plat.height);
16397
+ ctx.drawImage(tileImg, originX, originY, srcW, tileH, platX + tx, platY, drawW, plat.height);
16178
16398
  }
16179
16399
  } else {
16180
16400
  const color = PLATFORM_COLORS[platType] ?? PLATFORM_COLORS.ground;
@@ -16208,14 +16428,15 @@ function SideView({
16208
16428
  const ppy = py - camY;
16209
16429
  const facingRight = auth.facingRight ?? true;
16210
16430
  const playerImg = pSprite ? loadImage(pSprite.url) : null;
16211
- if (playerImg) {
16431
+ const playerSrc = playerImg ? resolveAssetSource(playerImg, pSprite, NOOP) : null;
16432
+ if (playerSrc) {
16212
16433
  ctx.save();
16213
16434
  if (!facingRight) {
16214
16435
  ctx.translate(ppx + pw, ppy);
16215
16436
  ctx.scale(-1, 1);
16216
- ctx.drawImage(playerImg, 0, 0, pw, ph);
16437
+ blit(ctx, playerSrc, 0, 0, pw, ph);
16217
16438
  } else {
16218
- ctx.drawImage(playerImg, ppx, ppy, pw, ph);
16439
+ blit(ctx, playerSrc, ppx, ppy, pw, ph);
16219
16440
  }
16220
16441
  ctx.restore();
16221
16442
  } else {
@@ -16416,10 +16637,11 @@ function Canvas2D({
16416
16637
  const attackTargetSet = useMemo(() => new Set(attackTargets.map((p) => `${p.x},${p.y}`)), [attackTargets]);
16417
16638
  const spriteUrls = useMemo(() => {
16418
16639
  const urls = [];
16640
+ const toUrl = (x) => typeof x === "string" ? x : x?.url;
16419
16641
  for (const tile of sortedTiles) {
16420
16642
  if (tile.terrainSprite) urls.push(tile.terrainSprite.url);
16421
16643
  else if (getTerrainSprite) {
16422
- const url = getTerrainSprite(tile.terrain ?? "");
16644
+ const url = toUrl(getTerrainSprite(tile.terrain ?? ""));
16423
16645
  if (url) urls.push(url);
16424
16646
  } else {
16425
16647
  const url = assetManifest?.terrains?.[tile.terrain ?? ""]?.url;
@@ -16429,7 +16651,7 @@ function Canvas2D({
16429
16651
  for (const feature of features) {
16430
16652
  if (feature.sprite) urls.push(feature.sprite.url);
16431
16653
  else if (getFeatureSprite) {
16432
- const url = getFeatureSprite(feature.type);
16654
+ const url = toUrl(getFeatureSprite(feature.type));
16433
16655
  if (url) urls.push(url);
16434
16656
  } else {
16435
16657
  const url = assetManifest?.features?.[feature.type]?.url;
@@ -16439,7 +16661,7 @@ function Canvas2D({
16439
16661
  for (const unit of units) {
16440
16662
  if (unit.sprite) urls.push(unit.sprite.url);
16441
16663
  else if (getUnitSprite) {
16442
- const url = getUnitSprite(unit);
16664
+ const url = toUrl(getUnitSprite(unit));
16443
16665
  if (url) urls.push(url);
16444
16666
  } else if (unit.unitType) {
16445
16667
  const url = assetManifest?.units?.[unit.unitType]?.url;
@@ -16480,14 +16702,25 @@ function Canvas2D({
16480
16702
  screenToWorld,
16481
16703
  lerpToTarget
16482
16704
  } = useCamera();
16483
- const resolveTerrainSpriteUrl = useCallback((tile) => {
16484
- return tile.terrainSprite?.url || getTerrainSprite?.(tile.terrain ?? "") || assetManifest?.terrains?.[tile.terrain ?? ""]?.url;
16705
+ const [, setAtlasVersion] = useState(0);
16706
+ const bumpAtlas = useCallback(() => setAtlasVersion((v) => v + 1), []);
16707
+ const resolveTerrainAsset = useCallback((tile) => {
16708
+ if (tile.terrainSprite) return tile.terrainSprite;
16709
+ const s = getTerrainSprite?.(tile.terrain ?? "");
16710
+ if (s) return typeof s === "string" ? { url: s } : s;
16711
+ return assetManifest?.terrains?.[tile.terrain ?? ""];
16485
16712
  }, [getTerrainSprite, assetManifest]);
16486
- const resolveFeatureSpriteUrl = useCallback((featureType) => {
16487
- return getFeatureSprite?.(featureType) || assetManifest?.features?.[featureType]?.url;
16713
+ const resolveFeatureAsset = useCallback((feature) => {
16714
+ if (feature.sprite) return feature.sprite;
16715
+ const s = getFeatureSprite?.(feature.type);
16716
+ if (s) return typeof s === "string" ? { url: s } : s;
16717
+ return assetManifest?.features?.[feature.type];
16488
16718
  }, [getFeatureSprite, assetManifest]);
16489
- const resolveUnitSpriteUrl = useCallback((unit) => {
16490
- return unit.sprite?.url || getUnitSprite?.(unit) || (unit.unitType ? assetManifest?.units?.[unit.unitType]?.url : void 0);
16719
+ const resolveUnitAsset = useCallback((unit) => {
16720
+ if (unit.sprite) return unit.sprite;
16721
+ const s = getUnitSprite?.(unit);
16722
+ if (s) return typeof s === "string" ? { url: s } : s;
16723
+ return unit.unitType ? assetManifest?.units?.[unit.unitType] : void 0;
16491
16724
  }, [getUnitSprite, assetManifest]);
16492
16725
  const miniMapTiles = useMemo(() => {
16493
16726
  if (!showMinimap) return [];
@@ -16551,18 +16784,17 @@ function Canvas2D({
16551
16784
  if (pos.x + scaledTileWidth < visLeft || pos.x > visRight || pos.y + scaledTileHeight < visTop || pos.y > visBottom) {
16552
16785
  continue;
16553
16786
  }
16554
- const spriteUrl = resolveTerrainSpriteUrl(tile);
16555
- const img = spriteUrl ? getImage(spriteUrl) : null;
16556
- if (img) {
16557
- if (img.naturalWidth === 0) {
16558
- ctx.drawImage(img, pos.x, pos.y, scaledTileWidth, scaledTileHeight);
16559
- } else {
16560
- const drawW = scaledTileWidth;
16561
- const drawH = scaledTileWidth * (img.naturalHeight / img.naturalWidth);
16562
- const drawX = pos.x;
16563
- const drawY = flatLike ? pos.y : pos.y + scaledTileHeight - drawH;
16564
- ctx.drawImage(img, drawX, drawY, drawW, drawH);
16565
- }
16787
+ const terrainAsset = resolveTerrainAsset(tile);
16788
+ const img = terrainAsset?.url ? getImage(terrainAsset.url) : null;
16789
+ const src = img ? resolveAssetSource(img, terrainAsset, bumpAtlas) : null;
16790
+ if (src) {
16791
+ const drawW = scaledTileWidth;
16792
+ const drawH = scaledTileWidth / src.aspect;
16793
+ const drawX = pos.x;
16794
+ const drawY = flatLike ? pos.y : pos.y + scaledTileHeight - drawH;
16795
+ blit(ctx, src, drawX, drawY, drawW, drawH);
16796
+ } else if (img && img.naturalWidth === 0) {
16797
+ ctx.drawImage(img, pos.x, pos.y, scaledTileWidth, scaledTileHeight);
16566
16798
  } else {
16567
16799
  const centerX = pos.x + scaledTileWidth / 2;
16568
16800
  const topY = pos.y + scaledDiamondTopY;
@@ -16616,15 +16848,16 @@ function Canvas2D({
16616
16848
  if (pos.x + scaledTileWidth < visLeft || pos.x > visRight || pos.y + scaledTileHeight < visTop || pos.y > visBottom) {
16617
16849
  continue;
16618
16850
  }
16619
- const spriteUrl = feature.sprite?.url || resolveFeatureSpriteUrl(feature.type);
16620
- const img = spriteUrl ? getImage(spriteUrl) : null;
16851
+ const featureAsset = resolveFeatureAsset(feature);
16852
+ const img = featureAsset?.url ? getImage(featureAsset.url) : null;
16853
+ const src = img ? resolveAssetSource(img, featureAsset, bumpAtlas) : null;
16621
16854
  const centerX = pos.x + scaledTileWidth / 2;
16622
16855
  const featureGroundY = pos.y + scaledDiamondTopY + scaledFloorHeight * 0.5;
16623
16856
  const isCastle = feature.type === "castle";
16624
16857
  const featureDrawH = isCastle ? scaledFloorHeight * 3.5 : scaledFloorHeight * 1.6;
16625
16858
  const maxFeatureW = isCastle ? scaledTileWidth * 1.8 : scaledTileWidth * 0.7;
16626
- if (img) {
16627
- const ar = img.naturalWidth / img.naturalHeight;
16859
+ if (src) {
16860
+ const ar = src.aspect;
16628
16861
  let drawH = featureDrawH;
16629
16862
  let drawW = featureDrawH * ar;
16630
16863
  if (drawW > maxFeatureW) {
@@ -16633,7 +16866,7 @@ function Canvas2D({
16633
16866
  }
16634
16867
  const drawX = centerX - drawW / 2;
16635
16868
  const drawY = featureGroundY - drawH;
16636
- ctx.drawImage(img, drawX, drawY, drawW, drawH);
16869
+ blit(ctx, src, drawX, drawY, drawW, drawH);
16637
16870
  } else {
16638
16871
  const color = FEATURE_COLORS[feature.type] || FEATURE_COLORS.default;
16639
16872
  ctx.beginPath();
@@ -16662,17 +16895,18 @@ function Canvas2D({
16662
16895
  const centerX = pos.x + scaledTileWidth / 2;
16663
16896
  const groundY = pos.y + scaledDiamondTopY + scaledFloorHeight * 0.5;
16664
16897
  const breatheOffset = 0;
16665
- const unitSpriteUrl = resolveUnitSpriteUrl(unit);
16666
- const img = unitSpriteUrl ? getImage(unitSpriteUrl) : null;
16898
+ const unitAsset = resolveUnitAsset(unit);
16899
+ const img = unitAsset?.url ? getImage(unitAsset.url) : null;
16667
16900
  const unitDrawH = scaledFloorHeight * spriteHeightRatio * unitScale;
16668
16901
  const maxUnitW = scaledTileWidth * spriteMaxWidthRatio * unitScale;
16669
16902
  const unitIsSheet = unit.spriteSheet?.url !== void 0;
16903
+ const unitSrc = img && !unitIsSheet ? resolveAssetSource(img, unitAsset, bumpAtlas) : null;
16670
16904
  const SHEET_ROWS = 5;
16671
16905
  const sheetFrameW = img ? img.naturalWidth / SHEET_COLUMNS : 0;
16672
16906
  const sheetFrameH = img ? img.naturalHeight / SHEET_ROWS : 0;
16673
16907
  const frameW = unitIsSheet ? sheetFrameW : img?.naturalWidth ?? 1;
16674
16908
  const frameH = unitIsSheet ? sheetFrameH : img?.naturalHeight ?? 1;
16675
- const ar = frameW / (frameH || 1);
16909
+ const ar = unitIsSheet ? sheetFrameW / (sheetFrameH || 1) : unitSrc ? unitSrc.aspect : frameW / (frameH || 1);
16676
16910
  let drawH = unitDrawH;
16677
16911
  let drawW = unitDrawH * ar;
16678
16912
  if (drawW > maxUnitW) {
@@ -16688,6 +16922,8 @@ function Canvas2D({
16688
16922
  if (img) {
16689
16923
  if (unitIsSheet) {
16690
16924
  ctx.drawImage(img, 0, 0, sheetFrameW, sheetFrameH, ghostCenterX - drawW / 2, ghostGroundY - drawH, drawW, drawH);
16925
+ } else if (unitSrc) {
16926
+ blit(ctx, unitSrc, ghostCenterX - drawW / 2, ghostGroundY - drawH, drawW, drawH);
16691
16927
  } else {
16692
16928
  ctx.drawImage(img, ghostCenterX - drawW / 2, ghostGroundY - drawH, drawW, drawH);
16693
16929
  }
@@ -16736,6 +16972,8 @@ function Canvas2D({
16736
16972
  const drawUnit = (x) => {
16737
16973
  if (unitIsSheet) {
16738
16974
  ctx.drawImage(img, 0, 0, sheetFrameW, sheetFrameH, x, spriteY, drawW, drawH);
16975
+ } else if (unitSrc) {
16976
+ blit(ctx, unitSrc, x, spriteY, drawW, drawH);
16739
16977
  } else {
16740
16978
  ctx.drawImage(img, x, spriteY, drawW, drawH);
16741
16979
  }
@@ -16786,11 +17024,12 @@ function Canvas2D({
16786
17024
  flatLike,
16787
17025
  scale,
16788
17026
  debug2,
16789
- resolveTerrainSpriteUrl,
16790
- resolveFeatureSpriteUrl,
16791
- resolveUnitSpriteUrl,
17027
+ resolveTerrainAsset,
17028
+ resolveFeatureAsset,
17029
+ resolveUnitAsset,
16792
17030
  resolveFrameForUnit,
16793
17031
  getImage,
17032
+ bumpAtlas,
16794
17033
  baseOffsetX,
16795
17034
  scaledTileWidth,
16796
17035
  scaledTileHeight,
@@ -17104,7 +17343,7 @@ function Canvas2D({
17104
17343
  }
17105
17344
  );
17106
17345
  }
17107
- var PLATFORM_COLORS, PLAYER_COLOR, PLAYER_EYE_COLOR, SKY_GRADIENT_TOP, SKY_GRADIENT_BOTTOM, GRID_COLOR, DEFAULT_PLATFORMS;
17346
+ var PLATFORM_COLORS, NOOP, PLAYER_COLOR, PLAYER_EYE_COLOR, SKY_GRADIENT_TOP, SKY_GRADIENT_BOTTOM, GRID_COLOR, DEFAULT_PLATFORMS;
17108
17347
  var init_Canvas2D = __esm({
17109
17348
  "components/game/2d/molecules/Canvas2D.tsx"() {
17110
17349
  "use client";
@@ -17118,6 +17357,7 @@ var init_Canvas2D = __esm({
17118
17357
  init_MiniMap();
17119
17358
  init_HealthBar();
17120
17359
  init_useImageCache();
17360
+ init_atlasSlice();
17121
17361
  init_useCamera();
17122
17362
  init_useCanvasGestures();
17123
17363
  init_useRenderInterpolation();
@@ -17131,6 +17371,8 @@ var init_Canvas2D = __esm({
17131
17371
  hazard: "#c0392b",
17132
17372
  goal: "#f1c40f"
17133
17373
  };
17374
+ NOOP = () => {
17375
+ };
17134
17376
  PLAYER_COLOR = "#3498db";
17135
17377
  PLAYER_EYE_COLOR = "#ffffff";
17136
17378
  SKY_GRADIENT_TOP = "#1a1a2e";
@@ -40883,6 +41125,7 @@ var init_molecules2 = __esm({
40883
41125
  init_PhysicsCanvas();
40884
41126
  init_BiologyCanvas();
40885
41127
  init_ChemistryCanvas();
41128
+ init_AlgorithmCanvas();
40886
41129
  init_GraphView();
40887
41130
  init_MapView();
40888
41131
  init_NumberStepper();
@@ -47541,6 +47784,7 @@ var init_component_registry_generated = __esm({
47541
47784
  init_ActionTile();
47542
47785
  init_ActivationBlock();
47543
47786
  init_ComponentPatterns();
47787
+ init_AlgorithmCanvas();
47544
47788
  init_AnimatedCounter();
47545
47789
  init_AnimatedGraphic();
47546
47790
  init_AnimatedReveal();
@@ -47839,6 +48083,7 @@ var init_component_registry_generated = __esm({
47839
48083
  "ActivationBlock": ActivationBlock,
47840
48084
  "Alert": AlertPattern,
47841
48085
  "AlertPattern": AlertPattern,
48086
+ "AlgorithmCanvas": AlgorithmCanvas,
47842
48087
  "AnimatedCounter": AnimatedCounter,
47843
48088
  "AnimatedGraphic": AnimatedGraphic,
47844
48089
  "AnimatedReveal": AnimatedReveal,
@@ -51354,4 +51599,4 @@ function useGitHubBranches(owner, repo, enabled = true) {
51354
51599
  });
51355
51600
  }
51356
51601
 
51357
- export { ALL_PRESETS, ALMADAR_DND_MIME, AR_BOOK_FIELDS, AboutPageTemplate, Accordion, ActionButton, Card2 as ActionCard, ActionPalette, ActionTile, ActivationBlock, Alert, AnimatedCounter, AnimatedGraphic, AnimatedReveal, ArticleSection, Aside, AssetPicker, AuthLayout, Avatar, Badge, BehaviorView, BiologyCanvas, BloomQuizBlock, BookChapterView, BookCoverPage, BookNavBar, BookTableOfContents, BookViewer, Box, BranchingLogicBuilder, Breadcrumb, BuilderBoard, Button, ButtonGroup, CTABanner, CalendarGrid, Canvas2D, Card, CardBody, CardContent, CardFooter, CardGrid, CardHeader, CardTitle, Carousel, CaseStudyCard, CaseStudyOrganism, Center, Chart, ChartLegend, ChatBar, Checkbox, ChemistryCanvas, ChoiceButton, ClassifierBoard, Coachmark, CodeBlock, CodeRunnerPanel, CollapsibleSection, ComboCounter, CommunityLinks, ConditionalWrapper, ConfettiEffect, ConfirmDialog, ConnectionBlock, Container, ContentRenderer, ContentSection, ControlButton, ControlGrid, CounterTemplate, DEFAULT_LIKERT_OPTIONS, DEFAULT_MATRIX_COLUMNS, DEFAULT_SLOTS, DIAMOND_TOP_Y, DamageNumber, DashboardGrid, DashboardLayout, DataGrid, DataList, DataTable, DateRangePicker, DateRangeSelector, DayCell, DebuggerBoard, DetailPanel, Dialog, DialogueBubble, Divider, DocBreadcrumb, DocPagination, DocSearch, DocSidebar, DocTOC, DocumentViewer, StateMachineView as DomStateMachineVisualizer, Drawer, DrawerSlot, ELEMENT_SELECTED_EVENT, EMPTY_EFFECT_STATE, EdgeDecoration, EditorCheckbox, EditorSelect, EditorSlider, EditorTextInput, EditorToolbar, EmptyState, EntityDisplayEvents, ErrorBoundary, ErrorState, EventHandlerBoard, EventLog, FEATURE_COLORS, FEATURE_TYPES, FLOOR_HEIGHT, FeatureCard, FeatureDetailPageTemplate, FeatureGrid, FeatureGridOrganism, FileTree, FilterGroup, FilterPill, Flex, FlipCard, FlipContainer, FloatingActionButton, Form, FormActions, FormField, FormLayout, FormSection, FormSectionHeader, GameAudioContext, GameAudioProvider, GameAudioToggle, GameCard, GameHud, GameIcon, GameMenu, GameShell, GameTemplate, GenericAppTemplate, GeometricPattern, GradientDivider, GraphCanvas, GraphView, Grid, GridPicker, HStack, Header, HealthBar, HeroOrganism, HeroSection, I18nProvider, IDENTITY_BOOK_FIELDS, Icon, IconPicker, InfiniteScrollSentinel, Input, InputGroup, InstallBox, InventoryGrid, ItemSlot, JazariStateMachine, JsonTreeEditor, Label, LandingPageTemplate, LawReferenceTooltip, LearningCanvas, Lightbox, LikertScale, LineChart2 as LineChart, List3 as List, LoadingState, MapView, MarkdownContent, MarketingFooter, MarketingStatCard, MasterDetail, MasterDetailLayout, MathCanvas, MatrixQuestion, MediaGallery, Menu, Meter, MiniMap, Modal, ModalSlot, ModuleCard, Navigation, NegotiatorBoard, NodeSlotEditor, NotifyListener, NumberStepper, ObjectRulePanel, OnboardingSpotlight, OptionConstraintGroup, StateMachineView as OrbitalStateMachineView, OrbitalVisualization, Overlay, PageHeader, Pagination, PatternTile, PhysicsCanvas, Popover, PositionedCanvas, PricingCard, PricingGrid, PricingOrganism, PricingPageTemplate, ProgressBar, ProgressDots, PropertyInspector, PullQuote, PullToRefresh, QrScanner, QuizBlock, Radio, RangeSlider, ReflectionBlock, RelationSelect, RepeatableFormSection, ReplyTree, ResourceBar, ResourceCounter, RichBlockEditor, RuleEditor, RuntimeDebugger, SHEET_COLUMNS, SPRITE_SHEET_LAYOUT, ScaledDiagram, ScoreDisplay, SearchInput, Section, SectionHeader, SegmentRenderer, Select, SequenceBar, SequencerBoard, ServiceCatalog, ShowcaseCard, ShowcaseOrganism, SidePanel, Sidebar, SignaturePad, SimpleGrid, SimulatorBoard, Skeleton, SlotContentRenderer, SocialProof, SortableList, Spacer, Sparkline, Spinner, Split, SplitPane, SplitSection, Sprite, Stack, StarRating, StatBadge, StatCard, StatDisplay, StateArchitectBoard, StateGraph, StateIndicator, StateJsonView, StateMachineView, StateNode2 as StateNode, StatsGrid, StatsOrganism, StatusBar, StatusDot, StatusEffect, StepFlow, StepFlowOrganism, SubagentTracePanel, SvgBranch, SvgConnection, SvgFlow, SvgGrid, SvgLobe, SvgMesh, SvgMorph, SvgNode, SvgPulse, SvgRing, SvgShield, SvgStack, SwipeableRow, Switch, TERRAIN_COLORS, TILE_HEIGHT, TILE_WIDTH, TabbedContainer, TableView, Tabs, TagCloud, TagInput, TeamCard, TeamOrganism, TerrainPalette, TextHighlight, Textarea, ThemeToggle, TimeSlotCell, Timeline, TimerDisplay, Toast, ToastSlot, Tooltip, TraitFrame, TraitSlot, TraitStateViewer, TransitionArrow, TrendIndicator, TurnIndicator, TypewriterText, Typography, UISlotComponent, UISlotRenderer, UploadDropZone, VStack, VariablePanel, VersionDiff, ViolationAlert, VoteStack, WaypointMarker, WizardContainer, WizardNavigation, WizardProgress, boardEntity, bool, calculateAttackTargets, calculateValidMoves, cn, createInitialGameState, createTranslate, createUnitAnimationState, drawSprite, getCurrentFrame, getTileDimensions, inferDirection, isoToScreen, makeAsset, makeAssetMap, mapBookData, num, objAvailableActions, objAvailableEvents, objCurrentState, objIcon, objId, objMaxRules, objName, objRules, objStates, parseEditFocus, parseLessonSegments, parseMarkdownWithCodeBlocks, parseQueryBinding, pendulum, projectileMotion, resolveFieldMap, resolveFrame, resolveSheetDirection, rows, screenToIso, springOscillator, str, tickAnimationState, toCodeLanguage, transitionAnimation, unitHealth, unitPosition, unitTeam, useAgentChat, useAnchorRect, useAuthContext, useCamera, useCanvasGestures, useCompile, useConnectGitHub, useDeepAgentGeneration, useDisconnectGitHub, useDragReorder, useDraggable, useDropZone, useEmitEvent, useEventBus, useEventListener, useExtensions, useFileEditor, useFileSystem, useGameAudio, useGameAudioContext, useGitHubBranches, useGitHubRepo, useGitHubRepos, useGitHubStatus, useImageCache, useInfiniteScroll, useLongPress, useOrbitalHistory, usePreview, usePullToRefresh, useQuerySingleton, useRenderInterpolation, useSwipeGesture, useTapReveal, useTraitListens, useTranslate127 as useTranslate, useUIEvents, useUISlotManager, useUnitSpriteAtlas, useValidation, vec2 };
51602
+ export { ALL_PRESETS, ALMADAR_DND_MIME, AR_BOOK_FIELDS, AboutPageTemplate, Accordion, ActionButton, Card2 as ActionCard, ActionPalette, ActionTile, ActivationBlock, Alert, AlgorithmCanvas, AnimatedCounter, AnimatedGraphic, AnimatedReveal, ArticleSection, Aside, AssetPicker, AuthLayout, Avatar, Badge, BehaviorView, BiologyCanvas, BloomQuizBlock, BookChapterView, BookCoverPage, BookNavBar, BookTableOfContents, BookViewer, Box, BranchingLogicBuilder, Breadcrumb, BuilderBoard, Button, ButtonGroup, CTABanner, CalendarGrid, Canvas2D, Card, CardBody, CardContent, CardFooter, CardGrid, CardHeader, CardTitle, Carousel, CaseStudyCard, CaseStudyOrganism, Center, Chart, ChartLegend, ChatBar, Checkbox, ChemistryCanvas, ChoiceButton, ClassifierBoard, Coachmark, CodeBlock, CodeRunnerPanel, CollapsibleSection, ComboCounter, CommunityLinks, ConditionalWrapper, ConfettiEffect, ConfirmDialog, ConnectionBlock, Container, ContentRenderer, ContentSection, ControlButton, ControlGrid, CounterTemplate, DEFAULT_LIKERT_OPTIONS, DEFAULT_MATRIX_COLUMNS, DEFAULT_SLOTS, DIAMOND_TOP_Y, DamageNumber, DashboardGrid, DashboardLayout, DataGrid, DataList, DataTable, DateRangePicker, DateRangeSelector, DayCell, DebuggerBoard, DetailPanel, Dialog, DialogueBubble, Divider, DocBreadcrumb, DocPagination, DocSearch, DocSidebar, DocTOC, DocumentViewer, StateMachineView as DomStateMachineVisualizer, Drawer, DrawerSlot, ELEMENT_SELECTED_EVENT, EMPTY_EFFECT_STATE, EdgeDecoration, EditorCheckbox, EditorSelect, EditorSlider, EditorTextInput, EditorToolbar, EmptyState, EntityDisplayEvents, ErrorBoundary, ErrorState, EventHandlerBoard, EventLog, FEATURE_COLORS, FEATURE_TYPES, FLOOR_HEIGHT, FeatureCard, FeatureDetailPageTemplate, FeatureGrid, FeatureGridOrganism, FileTree, FilterGroup, FilterPill, Flex, FlipCard, FlipContainer, FloatingActionButton, Form, FormActions, FormField, FormLayout, FormSection, FormSectionHeader, GameAudioContext, GameAudioProvider, GameAudioToggle, GameCard, GameHud, GameIcon, GameMenu, GameShell, GameTemplate, GenericAppTemplate, GeometricPattern, GradientDivider, GraphCanvas, GraphView, Grid, GridPicker, HStack, Header, HealthBar, HeroOrganism, HeroSection, I18nProvider, IDENTITY_BOOK_FIELDS, Icon, IconPicker, InfiniteScrollSentinel, Input, InputGroup, InstallBox, InventoryGrid, ItemSlot, JazariStateMachine, JsonTreeEditor, Label, LandingPageTemplate, LawReferenceTooltip, LearningCanvas, Lightbox, LikertScale, LineChart2 as LineChart, List3 as List, LoadingState, MapView, MarkdownContent, MarketingFooter, MarketingStatCard, MasterDetail, MasterDetailLayout, MathCanvas, MatrixQuestion, MediaGallery, Menu, Meter, MiniMap, Modal, ModalSlot, ModuleCard, Navigation, NegotiatorBoard, NodeSlotEditor, NotifyListener, NumberStepper, ObjectRulePanel, OnboardingSpotlight, OptionConstraintGroup, StateMachineView as OrbitalStateMachineView, OrbitalVisualization, Overlay, PageHeader, Pagination, PatternTile, PhysicsCanvas, Popover, PositionedCanvas, PricingCard, PricingGrid, PricingOrganism, PricingPageTemplate, ProgressBar, ProgressDots, PropertyInspector, PullQuote, PullToRefresh, QrScanner, QuizBlock, Radio, RangeSlider, ReflectionBlock, RelationSelect, RepeatableFormSection, ReplyTree, ResourceBar, ResourceCounter, RichBlockEditor, RuleEditor, RuntimeDebugger, SHEET_COLUMNS, SPRITE_SHEET_LAYOUT, ScaledDiagram, ScoreDisplay, SearchInput, Section, SectionHeader, SegmentRenderer, Select, SequenceBar, SequencerBoard, ServiceCatalog, ShowcaseCard, ShowcaseOrganism, SidePanel, Sidebar, SignaturePad, SimpleGrid, SimulatorBoard, Skeleton, SlotContentRenderer, SocialProof, SortableList, Spacer, Sparkline, Spinner, Split, SplitPane, SplitSection, Sprite, Stack, StarRating, StatBadge, StatCard, StatDisplay, StateArchitectBoard, StateGraph, StateIndicator, StateJsonView, StateMachineView, StateNode2 as StateNode, StatsGrid, StatsOrganism, StatusBar, StatusDot, StatusEffect, StepFlow, StepFlowOrganism, SubagentTracePanel, SvgBranch, SvgConnection, SvgFlow, SvgGrid, SvgLobe, SvgMesh, SvgMorph, SvgNode, SvgPulse, SvgRing, SvgShield, SvgStack, SwipeableRow, Switch, TERRAIN_COLORS, TILE_HEIGHT, TILE_WIDTH, TabbedContainer, TableView, Tabs, TagCloud, TagInput, TeamCard, TeamOrganism, TerrainPalette, TextHighlight, Textarea, ThemeToggle, TimeSlotCell, Timeline, TimerDisplay, Toast, ToastSlot, Tooltip, TraitFrame, TraitSlot, TraitStateViewer, TransitionArrow, TrendIndicator, TurnIndicator, TypewriterText, Typography, UISlotComponent, UISlotRenderer, UploadDropZone, VStack, VariablePanel, VersionDiff, ViolationAlert, VoteStack, WaypointMarker, WizardContainer, WizardNavigation, WizardProgress, boardEntity, bool, calculateAttackTargets, calculateValidMoves, cn, createInitialGameState, createTranslate, createUnitAnimationState, drawSprite, getCurrentFrame, getTileDimensions, inferDirection, isoToScreen, makeAsset, makeAssetMap, mapBookData, num, objAvailableActions, objAvailableEvents, objCurrentState, objIcon, objId, objMaxRules, objName, objRules, objStates, parseEditFocus, parseLessonSegments, parseMarkdownWithCodeBlocks, parseQueryBinding, pendulum, projectileMotion, resolveFieldMap, resolveFrame, resolveSheetDirection, rows, screenToIso, springOscillator, str, tickAnimationState, toCodeLanguage, transitionAnimation, unitHealth, unitPosition, unitTeam, useAgentChat, useAnchorRect, useAuthContext, useCamera, useCanvasGestures, useCompile, useConnectGitHub, useDeepAgentGeneration, useDisconnectGitHub, useDragReorder, useDraggable, useDropZone, useEmitEvent, useEventBus, useEventListener, useExtensions, useFileEditor, useFileSystem, useGameAudio, useGameAudioContext, useGitHubBranches, useGitHubRepo, useGitHubRepos, useGitHubStatus, useImageCache, useInfiniteScroll, useLongPress, useOrbitalHistory, usePreview, usePullToRefresh, useQuerySingleton, useRenderInterpolation, useSwipeGesture, useTapReveal, useTraitListens, useTranslate127 as useTranslate, useUIEvents, useUISlotManager, useUnitSpriteAtlas, useValidation, vec2 };