@almadar/ui 5.41.0 → 5.43.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.
@@ -28609,6 +28609,78 @@ var init_GameOverScreen = __esm({
28609
28609
  GameOverScreen.displayName = "GameOverScreen";
28610
28610
  }
28611
28611
  });
28612
+ function useRenderInterpolation(options = {}) {
28613
+ const tickIntervalMs = options.tickIntervalMs ?? 1e3 / 30;
28614
+ const prevRef = React83.useRef(null);
28615
+ const currRef = React83.useRef(null);
28616
+ const rafRef = React83.useRef(null);
28617
+ const onSnapshot = React83.useCallback((entities) => {
28618
+ prevRef.current = currRef.current;
28619
+ currRef.current = { entities, arrivedAt: performance.now() };
28620
+ }, []);
28621
+ const getInterpolated = React83.useCallback((now) => {
28622
+ const out = /* @__PURE__ */ new Map();
28623
+ const curr = currRef.current;
28624
+ if (!curr) return out;
28625
+ const prev = prevRef.current;
28626
+ if (!prev) {
28627
+ for (const e of curr.entities) out.set(e.id, { x: e.x, y: e.y });
28628
+ return out;
28629
+ }
28630
+ const rawAlpha = (now - curr.arrivedAt) / tickIntervalMs;
28631
+ const alpha = rawAlpha < 0 ? 0 : rawAlpha > 1 ? 1 : rawAlpha;
28632
+ const prevMap = /* @__PURE__ */ new Map();
28633
+ for (const e of prev.entities) prevMap.set(e.id, e);
28634
+ for (const c of curr.entities) {
28635
+ const p2 = prevMap.get(c.id);
28636
+ if (!p2) {
28637
+ out.set(c.id, { x: c.x, y: c.y });
28638
+ } else {
28639
+ out.set(c.id, {
28640
+ x: p2.x + (c.x - p2.x) * alpha,
28641
+ y: p2.y + (c.y - p2.y) * alpha
28642
+ });
28643
+ }
28644
+ }
28645
+ return out;
28646
+ }, [tickIntervalMs]);
28647
+ const startLoop = React83.useCallback(
28648
+ (draw) => {
28649
+ let active = true;
28650
+ const loop = () => {
28651
+ if (!active) return;
28652
+ try {
28653
+ draw(getInterpolated(performance.now()));
28654
+ } catch {
28655
+ }
28656
+ rafRef.current = requestAnimationFrame(loop);
28657
+ };
28658
+ rafRef.current = requestAnimationFrame(loop);
28659
+ return () => {
28660
+ active = false;
28661
+ if (rafRef.current !== null) {
28662
+ cancelAnimationFrame(rafRef.current);
28663
+ rafRef.current = null;
28664
+ }
28665
+ };
28666
+ },
28667
+ [getInterpolated]
28668
+ );
28669
+ React83.useEffect(() => {
28670
+ return () => {
28671
+ if (rafRef.current !== null) {
28672
+ cancelAnimationFrame(rafRef.current);
28673
+ rafRef.current = null;
28674
+ }
28675
+ };
28676
+ }, []);
28677
+ return { onSnapshot, getInterpolated, startLoop };
28678
+ }
28679
+ var init_useRenderInterpolation = __esm({
28680
+ "hooks/useRenderInterpolation.ts"() {
28681
+ "use client";
28682
+ }
28683
+ });
28612
28684
  function PlatformerCanvas({
28613
28685
  player,
28614
28686
  platforms = [
@@ -28677,8 +28749,43 @@ function PlatformerCanvas({
28677
28749
  y: 336,
28678
28750
  width: 32,
28679
28751
  height: 48,
28752
+ vx: 0,
28753
+ vy: 0,
28754
+ grounded: true,
28680
28755
  facingRight: true
28681
28756
  };
28757
+ const playerRef = React83.useRef(resolvedPlayer);
28758
+ playerRef.current = resolvedPlayer;
28759
+ const interp = useRenderInterpolation();
28760
+ React83.useEffect(() => {
28761
+ interp.onSnapshot([{ id: "player", x: resolvedPlayer.x, y: resolvedPlayer.y }]);
28762
+ }, [resolvedPlayer.x, resolvedPlayer.y]);
28763
+ const propsRef = React83.useRef({
28764
+ platforms,
28765
+ worldWidth,
28766
+ worldHeight,
28767
+ canvasWidth,
28768
+ canvasHeight,
28769
+ followCamera,
28770
+ bgColor,
28771
+ playerSprite,
28772
+ tileSprites,
28773
+ backgroundImage,
28774
+ assetBaseUrl
28775
+ });
28776
+ propsRef.current = {
28777
+ platforms,
28778
+ worldWidth,
28779
+ worldHeight,
28780
+ canvasWidth,
28781
+ canvasHeight,
28782
+ followCamera,
28783
+ bgColor,
28784
+ playerSprite,
28785
+ tileSprites,
28786
+ backgroundImage,
28787
+ assetBaseUrl
28788
+ };
28682
28789
  const handleKeyDown = React83.useCallback((e) => {
28683
28790
  if (keysRef.current.has(e.code)) return;
28684
28791
  keysRef.current.add(e.code);
@@ -28727,124 +28834,149 @@ function PlatformerCanvas({
28727
28834
  canvas.width = canvasWidth * dpr;
28728
28835
  canvas.height = canvasHeight * dpr;
28729
28836
  ctx.scale(dpr, dpr);
28730
- let camX = 0;
28731
- let camY = 0;
28732
- if (followCamera) {
28733
- camX = Math.max(0, Math.min(resolvedPlayer.x - canvasWidth / 2, worldWidth - canvasWidth));
28734
- camY = Math.max(0, Math.min(resolvedPlayer.y - canvasHeight / 2 - 50, worldHeight - canvasHeight));
28735
- }
28736
- const bgImg = backgroundImage ? loadImage(backgroundImage) : null;
28737
- if (bgImg) {
28738
- ctx.drawImage(bgImg, 0, 0, canvasWidth, canvasHeight);
28739
- } else if (bgColor) {
28740
- ctx.fillStyle = bgColor;
28741
- ctx.fillRect(0, 0, canvasWidth, canvasHeight);
28742
- } else {
28743
- const grad = ctx.createLinearGradient(0, 0, 0, canvasHeight);
28744
- grad.addColorStop(0, SKY_GRADIENT_TOP);
28745
- grad.addColorStop(1, SKY_GRADIENT_BOTTOM);
28746
- ctx.fillStyle = grad;
28747
- ctx.fillRect(0, 0, canvasWidth, canvasHeight);
28748
- }
28749
- ctx.strokeStyle = GRID_COLOR;
28750
- ctx.lineWidth = 1;
28751
- const gridSize = 32;
28752
- for (let gx = -camX % gridSize; gx < canvasWidth; gx += gridSize) {
28753
- ctx.beginPath();
28754
- ctx.moveTo(gx, 0);
28755
- ctx.lineTo(gx, canvasHeight);
28756
- ctx.stroke();
28757
- }
28758
- for (let gy = -camY % gridSize; gy < canvasHeight; gy += gridSize) {
28759
- ctx.beginPath();
28760
- ctx.moveTo(0, gy);
28761
- ctx.lineTo(canvasWidth, gy);
28762
- ctx.stroke();
28763
- }
28764
- for (const plat of platforms) {
28765
- const px = plat.x - camX;
28766
- const py = plat.y - camY;
28767
- const platType = plat.type ?? "ground";
28768
- const spriteUrl = tileSprites?.[platType];
28769
- const tileImg = spriteUrl ? loadImage(spriteUrl) : null;
28770
- if (tileImg) {
28771
- const tileW = tileImg.naturalWidth;
28772
- const tileH = tileImg.naturalHeight;
28773
- const scaleH = plat.height / tileH;
28774
- const scaledW = tileW * scaleH;
28775
- for (let tx = 0; tx < plat.width; tx += scaledW) {
28776
- const drawW = Math.min(scaledW, plat.width - tx);
28777
- const srcW = drawW / scaleH;
28778
- ctx.drawImage(tileImg, 0, 0, srcW, tileH, px + tx, py, drawW, plat.height);
28779
- }
28837
+ }, [canvasWidth, canvasHeight]);
28838
+ React83.useEffect(() => {
28839
+ const drawFrame = (positions) => {
28840
+ const canvas = canvasRef.current;
28841
+ if (!canvas) return;
28842
+ const ctx = canvas.getContext("2d");
28843
+ if (!ctx) return;
28844
+ const {
28845
+ platforms: plats,
28846
+ worldWidth: ww,
28847
+ worldHeight: wh,
28848
+ canvasWidth: cw,
28849
+ canvasHeight: ch,
28850
+ followCamera: fc,
28851
+ bgColor: bg,
28852
+ playerSprite: pSprite,
28853
+ tileSprites: tSprites,
28854
+ backgroundImage: bgImg
28855
+ } = propsRef.current;
28856
+ const auth = playerRef.current;
28857
+ const interped = positions.get("player");
28858
+ const px = interped?.x ?? auth.x;
28859
+ const py = interped?.y ?? auth.y;
28860
+ let camX = 0;
28861
+ let camY = 0;
28862
+ if (fc) {
28863
+ camX = Math.max(0, Math.min(px - cw / 2, ww - cw));
28864
+ camY = Math.max(0, Math.min(py - ch / 2 - 50, wh - ch));
28865
+ }
28866
+ const bgImage = bgImg ? loadImage(bgImg) : null;
28867
+ if (bgImage) {
28868
+ ctx.drawImage(bgImage, 0, 0, cw, ch);
28869
+ } else if (bg) {
28870
+ ctx.fillStyle = bg;
28871
+ ctx.fillRect(0, 0, cw, ch);
28780
28872
  } else {
28781
- const color = PLATFORM_COLORS[platType] ?? PLATFORM_COLORS.ground;
28782
- ctx.fillStyle = color;
28783
- ctx.fillRect(px, py, plat.width, plat.height);
28784
- ctx.fillStyle = "rgba(255, 255, 255, 0.15)";
28785
- ctx.fillRect(px, py, plat.width, 3);
28786
- ctx.fillStyle = "rgba(0, 0, 0, 0.3)";
28787
- ctx.fillRect(px, py + plat.height - 2, plat.width, 2);
28788
- if (platType === "hazard") {
28789
- ctx.strokeStyle = "#e74c3c";
28790
- ctx.lineWidth = 2;
28791
- for (let sx = px; sx < px + plat.width; sx += 12) {
28873
+ const grad = ctx.createLinearGradient(0, 0, 0, ch);
28874
+ grad.addColorStop(0, SKY_GRADIENT_TOP);
28875
+ grad.addColorStop(1, SKY_GRADIENT_BOTTOM);
28876
+ ctx.fillStyle = grad;
28877
+ ctx.fillRect(0, 0, cw, ch);
28878
+ }
28879
+ ctx.strokeStyle = GRID_COLOR;
28880
+ ctx.lineWidth = 1;
28881
+ const gridSize = 32;
28882
+ for (let gx = -camX % gridSize; gx < cw; gx += gridSize) {
28883
+ ctx.beginPath();
28884
+ ctx.moveTo(gx, 0);
28885
+ ctx.lineTo(gx, ch);
28886
+ ctx.stroke();
28887
+ }
28888
+ for (let gy = -camY % gridSize; gy < ch; gy += gridSize) {
28889
+ ctx.beginPath();
28890
+ ctx.moveTo(0, gy);
28891
+ ctx.lineTo(cw, gy);
28892
+ ctx.stroke();
28893
+ }
28894
+ for (const plat of plats) {
28895
+ const platX = plat.x - camX;
28896
+ const platY = plat.y - camY;
28897
+ const platType = plat.type ?? "ground";
28898
+ const spriteUrl = tSprites?.[platType];
28899
+ const tileImg = spriteUrl ? loadImage(spriteUrl) : null;
28900
+ if (tileImg) {
28901
+ const tileW = tileImg.naturalWidth;
28902
+ const tileH = tileImg.naturalHeight;
28903
+ const scaleH = plat.height / tileH;
28904
+ const scaledW = tileW * scaleH;
28905
+ for (let tx = 0; tx < plat.width; tx += scaledW) {
28906
+ const drawW = Math.min(scaledW, plat.width - tx);
28907
+ const srcW = drawW / scaleH;
28908
+ ctx.drawImage(tileImg, 0, 0, srcW, tileH, platX + tx, platY, drawW, plat.height);
28909
+ }
28910
+ } else {
28911
+ const color = PLATFORM_COLORS[platType] ?? PLATFORM_COLORS.ground;
28912
+ ctx.fillStyle = color;
28913
+ ctx.fillRect(platX, platY, plat.width, plat.height);
28914
+ ctx.fillStyle = "rgba(255, 255, 255, 0.15)";
28915
+ ctx.fillRect(platX, platY, plat.width, 3);
28916
+ ctx.fillStyle = "rgba(0, 0, 0, 0.3)";
28917
+ ctx.fillRect(platX, platY + plat.height - 2, plat.width, 2);
28918
+ if (platType === "hazard") {
28919
+ ctx.strokeStyle = "#e74c3c";
28920
+ ctx.lineWidth = 2;
28921
+ for (let sx = platX; sx < platX + plat.width; sx += 12) {
28922
+ ctx.beginPath();
28923
+ ctx.moveTo(sx, platY);
28924
+ ctx.lineTo(sx + 6, platY + plat.height);
28925
+ ctx.stroke();
28926
+ }
28927
+ }
28928
+ if (platType === "goal") {
28929
+ ctx.fillStyle = "rgba(241, 196, 15, 0.5)";
28792
28930
  ctx.beginPath();
28793
- ctx.moveTo(sx, py);
28794
- ctx.lineTo(sx + 6, py + plat.height);
28795
- ctx.stroke();
28931
+ ctx.arc(platX + plat.width / 2, platY - 10, 8, 0, Math.PI * 2);
28932
+ ctx.fill();
28796
28933
  }
28797
28934
  }
28798
- if (platType === "goal") {
28799
- ctx.fillStyle = "rgba(241, 196, 15, 0.5)";
28800
- ctx.beginPath();
28801
- ctx.arc(px + plat.width / 2, py - 10, 8, 0, Math.PI * 2);
28802
- ctx.fill();
28803
- }
28804
28935
  }
28805
- }
28806
- const pw = resolvedPlayer.width ?? 24;
28807
- const ph = resolvedPlayer.height ?? 32;
28808
- const ppx = resolvedPlayer.x - camX;
28809
- const ppy = resolvedPlayer.y - camY;
28810
- const facingRight = resolvedPlayer.facingRight ?? true;
28811
- const playerImg = playerSprite ? loadImage(playerSprite) : null;
28812
- if (playerImg) {
28813
- ctx.save();
28814
- if (!facingRight) {
28815
- ctx.translate(ppx + pw, ppy);
28816
- ctx.scale(-1, 1);
28817
- ctx.drawImage(playerImg, 0, 0, pw, ph);
28936
+ const pw = auth.width ?? 24;
28937
+ const ph = auth.height ?? 32;
28938
+ const ppx = px - camX;
28939
+ const ppy = py - camY;
28940
+ const facingRight = auth.facingRight ?? true;
28941
+ const playerImg = pSprite ? loadImage(pSprite) : null;
28942
+ if (playerImg) {
28943
+ ctx.save();
28944
+ if (!facingRight) {
28945
+ ctx.translate(ppx + pw, ppy);
28946
+ ctx.scale(-1, 1);
28947
+ ctx.drawImage(playerImg, 0, 0, pw, ph);
28948
+ } else {
28949
+ ctx.drawImage(playerImg, ppx, ppy, pw, ph);
28950
+ }
28951
+ ctx.restore();
28818
28952
  } else {
28819
- ctx.drawImage(playerImg, ppx, ppy, pw, ph);
28953
+ ctx.fillStyle = PLAYER_COLOR;
28954
+ const radius = Math.min(pw, ph) * 0.25;
28955
+ ctx.beginPath();
28956
+ ctx.moveTo(ppx + radius, ppy);
28957
+ ctx.lineTo(ppx + pw - radius, ppy);
28958
+ ctx.quadraticCurveTo(ppx + pw, ppy, ppx + pw, ppy + radius);
28959
+ ctx.lineTo(ppx + pw, ppy + ph - radius);
28960
+ ctx.quadraticCurveTo(ppx + pw, ppy + ph, ppx + pw - radius, ppy + ph);
28961
+ ctx.lineTo(ppx + radius, ppy + ph);
28962
+ ctx.quadraticCurveTo(ppx, ppy + ph, ppx, ppy + ph - radius);
28963
+ ctx.lineTo(ppx, ppy + radius);
28964
+ ctx.quadraticCurveTo(ppx, ppy, ppx + radius, ppy);
28965
+ ctx.fill();
28966
+ const eyeY = ppy + ph * 0.3;
28967
+ const eyeSize = 3;
28968
+ const eyeOffsetX = facingRight ? pw * 0.55 : pw * 0.2;
28969
+ ctx.fillStyle = PLAYER_EYE_COLOR;
28970
+ ctx.beginPath();
28971
+ ctx.arc(ppx + eyeOffsetX, eyeY, eyeSize, 0, Math.PI * 2);
28972
+ ctx.fill();
28973
+ ctx.beginPath();
28974
+ ctx.arc(ppx + eyeOffsetX + 7, eyeY, eyeSize, 0, Math.PI * 2);
28975
+ ctx.fill();
28820
28976
  }
28821
- ctx.restore();
28822
- } else {
28823
- ctx.fillStyle = PLAYER_COLOR;
28824
- const radius = Math.min(pw, ph) * 0.25;
28825
- ctx.beginPath();
28826
- ctx.moveTo(ppx + radius, ppy);
28827
- ctx.lineTo(ppx + pw - radius, ppy);
28828
- ctx.quadraticCurveTo(ppx + pw, ppy, ppx + pw, ppy + radius);
28829
- ctx.lineTo(ppx + pw, ppy + ph - radius);
28830
- ctx.quadraticCurveTo(ppx + pw, ppy + ph, ppx + pw - radius, ppy + ph);
28831
- ctx.lineTo(ppx + radius, ppy + ph);
28832
- ctx.quadraticCurveTo(ppx, ppy + ph, ppx, ppy + ph - radius);
28833
- ctx.lineTo(ppx, ppy + radius);
28834
- ctx.quadraticCurveTo(ppx, ppy, ppx + radius, ppy);
28835
- ctx.fill();
28836
- const eyeY = ppy + ph * 0.3;
28837
- const eyeSize = 3;
28838
- const eyeOffsetX = facingRight ? pw * 0.55 : pw * 0.2;
28839
- ctx.fillStyle = PLAYER_EYE_COLOR;
28840
- ctx.beginPath();
28841
- ctx.arc(ppx + eyeOffsetX, eyeY, eyeSize, 0, Math.PI * 2);
28842
- ctx.fill();
28843
- ctx.beginPath();
28844
- ctx.arc(ppx + eyeOffsetX + 7, eyeY, eyeSize, 0, Math.PI * 2);
28845
- ctx.fill();
28846
- }
28847
- }, [player, platforms, worldWidth, worldHeight, canvasWidth, canvasHeight, followCamera, bgColor, playerSprite, tileSprites, backgroundImage, assetBaseUrl, loadedImages]);
28977
+ };
28978
+ return interp.startLoop(drawFrame);
28979
+ }, [interp.startLoop, loadImage]);
28848
28980
  return /* @__PURE__ */ jsxRuntime.jsx(
28849
28981
  "canvas",
28850
28982
  {
@@ -28862,6 +28994,7 @@ var init_PlatformerCanvas = __esm({
28862
28994
  init_cn();
28863
28995
  init_useEventBus();
28864
28996
  init_verificationRegistry();
28997
+ init_useRenderInterpolation();
28865
28998
  PLATFORM_COLORS = {
28866
28999
  ground: "#4a7c59",
28867
29000
  platform: "#7c6b4a",
@@ -29244,13 +29377,13 @@ var init_MapView = __esm({
29244
29377
  shadowSize: [41, 41]
29245
29378
  });
29246
29379
  L.Marker.prototype.options.icon = defaultIcon;
29247
- const { useEffect: useEffect73, useRef: useRef68, useCallback: useCallback111, useState: useState104 } = React83__namespace.default;
29380
+ const { useEffect: useEffect74, useRef: useRef69, useCallback: useCallback112, useState: useState104 } = React83__namespace.default;
29248
29381
  const { Typography: Typography2 } = await Promise.resolve().then(() => (init_Typography(), Typography_exports));
29249
29382
  const { useEventBus: useEventBus2 } = await Promise.resolve().then(() => (init_useEventBus(), useEventBus_exports));
29250
29383
  function MapUpdater({ centerLat, centerLng, zoom }) {
29251
29384
  const map = useMap();
29252
- const prevRef = useRef68({ centerLat, centerLng, zoom });
29253
- useEffect73(() => {
29385
+ const prevRef = useRef69({ centerLat, centerLng, zoom });
29386
+ useEffect74(() => {
29254
29387
  const prev = prevRef.current;
29255
29388
  if (prev.centerLat !== centerLat || prev.centerLng !== centerLng || prev.zoom !== zoom) {
29256
29389
  map.setView([centerLat, centerLng], zoom);
@@ -29261,7 +29394,7 @@ var init_MapView = __esm({
29261
29394
  }
29262
29395
  function MapClickHandler({ onMapClick }) {
29263
29396
  const map = useMap();
29264
- useEffect73(() => {
29397
+ useEffect74(() => {
29265
29398
  if (!onMapClick) return;
29266
29399
  const handler = (e) => {
29267
29400
  onMapClick(e.latlng.lat, e.latlng.lng);
@@ -29290,7 +29423,7 @@ var init_MapView = __esm({
29290
29423
  }) {
29291
29424
  const eventBus = useEventBus2();
29292
29425
  const [clickedPosition, setClickedPosition] = useState104(null);
29293
- const handleMapClick = useCallback111((lat, lng) => {
29426
+ const handleMapClick = useCallback112((lat, lng) => {
29294
29427
  if (showClickedPin) {
29295
29428
  setClickedPosition({ lat, lng });
29296
29429
  }
@@ -29299,7 +29432,7 @@ var init_MapView = __esm({
29299
29432
  eventBus.emit(`UI:${mapClickEvent}`, { latitude: lat, longitude: lng });
29300
29433
  }
29301
29434
  }, [onMapClick, mapClickEvent, eventBus, showClickedPin]);
29302
- const handleMarkerClick = useCallback111((marker) => {
29435
+ const handleMarkerClick = useCallback112((marker) => {
29303
29436
  onMarkerClick?.(marker);
29304
29437
  if (markerClickEvent) {
29305
29438
  eventBus.emit(`UI:${markerClickEvent}`, { ...marker });
@@ -44784,6 +44917,10 @@ function SequencerBoard({
44784
44917
  stepDurationMs = 1e3,
44785
44918
  playEvent,
44786
44919
  completeEvent,
44920
+ placeEvent,
44921
+ removeEvent,
44922
+ checkEvent,
44923
+ playAgainEvent,
44787
44924
  className
44788
44925
  }) {
44789
44926
  const { emit } = useEventBus();
@@ -44819,7 +44956,8 @@ function SequencerBoard({
44819
44956
  return next;
44820
44957
  });
44821
44958
  emit("UI:PLAY_SOUND", { key: "drop_slot" });
44822
- }, [emit]);
44959
+ if (placeEvent) emit(`UI:${placeEvent}`, { slotIndex: index, actionId: item.id });
44960
+ }, [emit, placeEvent]);
44823
44961
  const handleSlotRemove = React83.useCallback((index) => {
44824
44962
  setSlots((prev) => {
44825
44963
  const next = [...prev];
@@ -44832,7 +44970,8 @@ function SequencerBoard({
44832
44970
  return next;
44833
44971
  });
44834
44972
  emit("UI:PLAY_SOUND", { key: "back" });
44835
- }, [emit]);
44973
+ if (removeEvent) emit(`UI:${removeEvent}`, { slotIndex: index });
44974
+ }, [emit, removeEvent]);
44836
44975
  const handleReset = React83.useCallback(() => {
44837
44976
  if (timerRef.current) clearTimeout(timerRef.current);
44838
44977
  setSlots(Array.from({ length: maxSlots }, () => void 0));
@@ -44840,7 +44979,8 @@ function SequencerBoard({
44840
44979
  setCurrentStep(-1);
44841
44980
  setAttempts(0);
44842
44981
  setSlotFeedback(Array.from({ length: maxSlots }, () => null));
44843
- }, [maxSlots]);
44982
+ if (playAgainEvent) emit(`UI:${playAgainEvent}`, {});
44983
+ }, [maxSlots, playAgainEvent, emit]);
44844
44984
  const filledSlots = slots.filter((s) => !!s);
44845
44985
  const canPlay = filledSlots.length > 0 && playState === "idle";
44846
44986
  const handlePlay = React83.useCallback(() => {
@@ -44862,6 +45002,7 @@ function SequencerBoard({
44862
45002
  const success = solutions.some(
44863
45003
  (sol) => sol.length === playerIds.length && sol.every((id, i) => id === playerIds[i])
44864
45004
  );
45005
+ if (checkEvent) emit(`UI:${checkEvent}`, { sequence: playerIds });
44865
45006
  if (success) {
44866
45007
  setPlayState("success");
44867
45008
  setCurrentStep(-1);
@@ -44889,7 +45030,7 @@ function SequencerBoard({
44889
45030
  }
44890
45031
  };
44891
45032
  timerRef.current = setTimeout(advance, stepDurationMs);
44892
- }, [canPlay, slots, maxSlots, solutions, stepDurationMs, playEvent, completeEvent, emit]);
45033
+ }, [canPlay, slots, maxSlots, solutions, stepDurationMs, playEvent, completeEvent, checkEvent, emit]);
44893
45034
  const machine = {
44894
45035
  name: str(resolved?.title),
44895
45036
  description: str(resolved?.description),
@@ -45144,11 +45285,13 @@ function SimulationCanvas({
45144
45285
  height = 400,
45145
45286
  running,
45146
45287
  speed = 1,
45288
+ bodies: externalBodies,
45147
45289
  className
45148
45290
  }) {
45149
45291
  const preset = React83.useMemo(() => resolvePreset(presetProp), [presetProp]);
45150
45292
  const canvasRef = React83.useRef(null);
45151
45293
  const bodiesRef = React83.useRef(structuredClone(preset.bodies));
45294
+ const interp = useRenderInterpolation();
45152
45295
  React83.useEffect(() => {
45153
45296
  bodiesRef.current = structuredClone(preset.bodies);
45154
45297
  }, [preset]);
@@ -45246,6 +45389,67 @@ function SimulationCanvas({
45246
45389
  }
45247
45390
  }, [width, height, preset]);
45248
45391
  React83.useEffect(() => {
45392
+ if (!externalBodies) return;
45393
+ interp.onSnapshot(externalBodies.map((b) => ({ id: b.id, x: b.x, y: b.y })));
45394
+ }, [externalBodies]);
45395
+ const presetRef = React83.useRef(preset);
45396
+ presetRef.current = preset;
45397
+ const widthRef = React83.useRef(width);
45398
+ widthRef.current = width;
45399
+ const heightRef = React83.useRef(height);
45400
+ heightRef.current = height;
45401
+ React83.useEffect(() => {
45402
+ if (!externalBodies) return;
45403
+ const drawInterpolated = (positions) => {
45404
+ const canvas = canvasRef.current;
45405
+ if (!canvas) return;
45406
+ const ctx = canvas.getContext("2d");
45407
+ if (!ctx) return;
45408
+ const bodies = bodiesRef.current;
45409
+ const p2 = presetRef.current;
45410
+ const w = widthRef.current;
45411
+ const h = heightRef.current;
45412
+ ctx.clearRect(0, 0, w, h);
45413
+ ctx.fillStyle = p2.backgroundColor ?? "#1a1a2e";
45414
+ ctx.fillRect(0, 0, w, h);
45415
+ if (p2.constraints) {
45416
+ for (const c of p2.constraints) {
45417
+ const a = bodies[c.bodyA];
45418
+ const b = bodies[c.bodyB];
45419
+ if (a && b) {
45420
+ const aPos = positions.get(a.id) ?? a;
45421
+ const bPos = positions.get(b.id) ?? b;
45422
+ ctx.beginPath();
45423
+ ctx.moveTo(aPos.x, aPos.y);
45424
+ ctx.lineTo(bPos.x, bPos.y);
45425
+ ctx.strokeStyle = "#533483";
45426
+ ctx.lineWidth = 1;
45427
+ ctx.setLineDash([4, 4]);
45428
+ ctx.stroke();
45429
+ ctx.setLineDash([]);
45430
+ }
45431
+ }
45432
+ }
45433
+ for (const body of bodies) {
45434
+ const pos = positions.get(body.id) ?? body;
45435
+ ctx.beginPath();
45436
+ ctx.arc(pos.x, pos.y, body.radius, 0, Math.PI * 2);
45437
+ ctx.fillStyle = body.color ?? "#e94560";
45438
+ ctx.fill();
45439
+ if (p2.showVelocity) {
45440
+ ctx.beginPath();
45441
+ ctx.moveTo(pos.x, pos.y);
45442
+ ctx.lineTo(pos.x + body.vx * 0.1, pos.y + body.vy * 0.1);
45443
+ ctx.strokeStyle = "#16213e";
45444
+ ctx.lineWidth = 2;
45445
+ ctx.stroke();
45446
+ }
45447
+ }
45448
+ };
45449
+ return interp.startLoop(drawInterpolated);
45450
+ }, [externalBodies !== void 0, interp.startLoop]);
45451
+ React83.useEffect(() => {
45452
+ if (externalBodies !== void 0) return;
45249
45453
  if (!running) return;
45250
45454
  let raf;
45251
45455
  const loop = () => {
@@ -45255,10 +45459,11 @@ function SimulationCanvas({
45255
45459
  };
45256
45460
  raf = requestAnimationFrame(loop);
45257
45461
  return () => cancelAnimationFrame(raf);
45258
- }, [running, step, draw]);
45462
+ }, [running, step, draw, externalBodies]);
45259
45463
  React83.useEffect(() => {
45464
+ if (externalBodies !== void 0) return;
45260
45465
  draw();
45261
- }, [draw]);
45466
+ }, [draw, externalBodies]);
45262
45467
  React83.useEffect(() => {
45263
45468
  if (typeof window === "undefined") return;
45264
45469
  const canvas = canvasRef.current;
@@ -45284,6 +45489,7 @@ var init_SimulationCanvas = __esm({
45284
45489
  init_atoms2();
45285
45490
  init_verificationRegistry();
45286
45491
  init_presets();
45492
+ init_useRenderInterpolation();
45287
45493
  SimulationCanvas.displayName = "SimulationCanvas";
45288
45494
  }
45289
45495
  });