@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.
@@ -28563,6 +28563,78 @@ var init_GameOverScreen = __esm({
28563
28563
  GameOverScreen.displayName = "GameOverScreen";
28564
28564
  }
28565
28565
  });
28566
+ function useRenderInterpolation(options = {}) {
28567
+ const tickIntervalMs = options.tickIntervalMs ?? 1e3 / 30;
28568
+ const prevRef = useRef(null);
28569
+ const currRef = useRef(null);
28570
+ const rafRef = useRef(null);
28571
+ const onSnapshot = useCallback((entities) => {
28572
+ prevRef.current = currRef.current;
28573
+ currRef.current = { entities, arrivedAt: performance.now() };
28574
+ }, []);
28575
+ const getInterpolated = useCallback((now) => {
28576
+ const out = /* @__PURE__ */ new Map();
28577
+ const curr = currRef.current;
28578
+ if (!curr) return out;
28579
+ const prev = prevRef.current;
28580
+ if (!prev) {
28581
+ for (const e of curr.entities) out.set(e.id, { x: e.x, y: e.y });
28582
+ return out;
28583
+ }
28584
+ const rawAlpha = (now - curr.arrivedAt) / tickIntervalMs;
28585
+ const alpha = rawAlpha < 0 ? 0 : rawAlpha > 1 ? 1 : rawAlpha;
28586
+ const prevMap = /* @__PURE__ */ new Map();
28587
+ for (const e of prev.entities) prevMap.set(e.id, e);
28588
+ for (const c of curr.entities) {
28589
+ const p2 = prevMap.get(c.id);
28590
+ if (!p2) {
28591
+ out.set(c.id, { x: c.x, y: c.y });
28592
+ } else {
28593
+ out.set(c.id, {
28594
+ x: p2.x + (c.x - p2.x) * alpha,
28595
+ y: p2.y + (c.y - p2.y) * alpha
28596
+ });
28597
+ }
28598
+ }
28599
+ return out;
28600
+ }, [tickIntervalMs]);
28601
+ const startLoop = useCallback(
28602
+ (draw) => {
28603
+ let active = true;
28604
+ const loop = () => {
28605
+ if (!active) return;
28606
+ try {
28607
+ draw(getInterpolated(performance.now()));
28608
+ } catch {
28609
+ }
28610
+ rafRef.current = requestAnimationFrame(loop);
28611
+ };
28612
+ rafRef.current = requestAnimationFrame(loop);
28613
+ return () => {
28614
+ active = false;
28615
+ if (rafRef.current !== null) {
28616
+ cancelAnimationFrame(rafRef.current);
28617
+ rafRef.current = null;
28618
+ }
28619
+ };
28620
+ },
28621
+ [getInterpolated]
28622
+ );
28623
+ useEffect(() => {
28624
+ return () => {
28625
+ if (rafRef.current !== null) {
28626
+ cancelAnimationFrame(rafRef.current);
28627
+ rafRef.current = null;
28628
+ }
28629
+ };
28630
+ }, []);
28631
+ return { onSnapshot, getInterpolated, startLoop };
28632
+ }
28633
+ var init_useRenderInterpolation = __esm({
28634
+ "hooks/useRenderInterpolation.ts"() {
28635
+ "use client";
28636
+ }
28637
+ });
28566
28638
  function PlatformerCanvas({
28567
28639
  player,
28568
28640
  platforms = [
@@ -28631,8 +28703,43 @@ function PlatformerCanvas({
28631
28703
  y: 336,
28632
28704
  width: 32,
28633
28705
  height: 48,
28706
+ vx: 0,
28707
+ vy: 0,
28708
+ grounded: true,
28634
28709
  facingRight: true
28635
28710
  };
28711
+ const playerRef = useRef(resolvedPlayer);
28712
+ playerRef.current = resolvedPlayer;
28713
+ const interp = useRenderInterpolation();
28714
+ useEffect(() => {
28715
+ interp.onSnapshot([{ id: "player", x: resolvedPlayer.x, y: resolvedPlayer.y }]);
28716
+ }, [resolvedPlayer.x, resolvedPlayer.y]);
28717
+ const propsRef = useRef({
28718
+ platforms,
28719
+ worldWidth,
28720
+ worldHeight,
28721
+ canvasWidth,
28722
+ canvasHeight,
28723
+ followCamera,
28724
+ bgColor,
28725
+ playerSprite,
28726
+ tileSprites,
28727
+ backgroundImage,
28728
+ assetBaseUrl
28729
+ });
28730
+ propsRef.current = {
28731
+ platforms,
28732
+ worldWidth,
28733
+ worldHeight,
28734
+ canvasWidth,
28735
+ canvasHeight,
28736
+ followCamera,
28737
+ bgColor,
28738
+ playerSprite,
28739
+ tileSprites,
28740
+ backgroundImage,
28741
+ assetBaseUrl
28742
+ };
28636
28743
  const handleKeyDown = useCallback((e) => {
28637
28744
  if (keysRef.current.has(e.code)) return;
28638
28745
  keysRef.current.add(e.code);
@@ -28681,124 +28788,149 @@ function PlatformerCanvas({
28681
28788
  canvas.width = canvasWidth * dpr;
28682
28789
  canvas.height = canvasHeight * dpr;
28683
28790
  ctx.scale(dpr, dpr);
28684
- let camX = 0;
28685
- let camY = 0;
28686
- if (followCamera) {
28687
- camX = Math.max(0, Math.min(resolvedPlayer.x - canvasWidth / 2, worldWidth - canvasWidth));
28688
- camY = Math.max(0, Math.min(resolvedPlayer.y - canvasHeight / 2 - 50, worldHeight - canvasHeight));
28689
- }
28690
- const bgImg = backgroundImage ? loadImage(backgroundImage) : null;
28691
- if (bgImg) {
28692
- ctx.drawImage(bgImg, 0, 0, canvasWidth, canvasHeight);
28693
- } else if (bgColor) {
28694
- ctx.fillStyle = bgColor;
28695
- ctx.fillRect(0, 0, canvasWidth, canvasHeight);
28696
- } else {
28697
- const grad = ctx.createLinearGradient(0, 0, 0, canvasHeight);
28698
- grad.addColorStop(0, SKY_GRADIENT_TOP);
28699
- grad.addColorStop(1, SKY_GRADIENT_BOTTOM);
28700
- ctx.fillStyle = grad;
28701
- ctx.fillRect(0, 0, canvasWidth, canvasHeight);
28702
- }
28703
- ctx.strokeStyle = GRID_COLOR;
28704
- ctx.lineWidth = 1;
28705
- const gridSize = 32;
28706
- for (let gx = -camX % gridSize; gx < canvasWidth; gx += gridSize) {
28707
- ctx.beginPath();
28708
- ctx.moveTo(gx, 0);
28709
- ctx.lineTo(gx, canvasHeight);
28710
- ctx.stroke();
28711
- }
28712
- for (let gy = -camY % gridSize; gy < canvasHeight; gy += gridSize) {
28713
- ctx.beginPath();
28714
- ctx.moveTo(0, gy);
28715
- ctx.lineTo(canvasWidth, gy);
28716
- ctx.stroke();
28717
- }
28718
- for (const plat of platforms) {
28719
- const px = plat.x - camX;
28720
- const py = plat.y - camY;
28721
- const platType = plat.type ?? "ground";
28722
- const spriteUrl = tileSprites?.[platType];
28723
- const tileImg = spriteUrl ? loadImage(spriteUrl) : null;
28724
- if (tileImg) {
28725
- const tileW = tileImg.naturalWidth;
28726
- const tileH = tileImg.naturalHeight;
28727
- const scaleH = plat.height / tileH;
28728
- const scaledW = tileW * scaleH;
28729
- for (let tx = 0; tx < plat.width; tx += scaledW) {
28730
- const drawW = Math.min(scaledW, plat.width - tx);
28731
- const srcW = drawW / scaleH;
28732
- ctx.drawImage(tileImg, 0, 0, srcW, tileH, px + tx, py, drawW, plat.height);
28733
- }
28791
+ }, [canvasWidth, canvasHeight]);
28792
+ useEffect(() => {
28793
+ const drawFrame = (positions) => {
28794
+ const canvas = canvasRef.current;
28795
+ if (!canvas) return;
28796
+ const ctx = canvas.getContext("2d");
28797
+ if (!ctx) return;
28798
+ const {
28799
+ platforms: plats,
28800
+ worldWidth: ww,
28801
+ worldHeight: wh,
28802
+ canvasWidth: cw,
28803
+ canvasHeight: ch,
28804
+ followCamera: fc,
28805
+ bgColor: bg,
28806
+ playerSprite: pSprite,
28807
+ tileSprites: tSprites,
28808
+ backgroundImage: bgImg
28809
+ } = propsRef.current;
28810
+ const auth = playerRef.current;
28811
+ const interped = positions.get("player");
28812
+ const px = interped?.x ?? auth.x;
28813
+ const py = interped?.y ?? auth.y;
28814
+ let camX = 0;
28815
+ let camY = 0;
28816
+ if (fc) {
28817
+ camX = Math.max(0, Math.min(px - cw / 2, ww - cw));
28818
+ camY = Math.max(0, Math.min(py - ch / 2 - 50, wh - ch));
28819
+ }
28820
+ const bgImage = bgImg ? loadImage(bgImg) : null;
28821
+ if (bgImage) {
28822
+ ctx.drawImage(bgImage, 0, 0, cw, ch);
28823
+ } else if (bg) {
28824
+ ctx.fillStyle = bg;
28825
+ ctx.fillRect(0, 0, cw, ch);
28734
28826
  } else {
28735
- const color = PLATFORM_COLORS[platType] ?? PLATFORM_COLORS.ground;
28736
- ctx.fillStyle = color;
28737
- ctx.fillRect(px, py, plat.width, plat.height);
28738
- ctx.fillStyle = "rgba(255, 255, 255, 0.15)";
28739
- ctx.fillRect(px, py, plat.width, 3);
28740
- ctx.fillStyle = "rgba(0, 0, 0, 0.3)";
28741
- ctx.fillRect(px, py + plat.height - 2, plat.width, 2);
28742
- if (platType === "hazard") {
28743
- ctx.strokeStyle = "#e74c3c";
28744
- ctx.lineWidth = 2;
28745
- for (let sx = px; sx < px + plat.width; sx += 12) {
28827
+ const grad = ctx.createLinearGradient(0, 0, 0, ch);
28828
+ grad.addColorStop(0, SKY_GRADIENT_TOP);
28829
+ grad.addColorStop(1, SKY_GRADIENT_BOTTOM);
28830
+ ctx.fillStyle = grad;
28831
+ ctx.fillRect(0, 0, cw, ch);
28832
+ }
28833
+ ctx.strokeStyle = GRID_COLOR;
28834
+ ctx.lineWidth = 1;
28835
+ const gridSize = 32;
28836
+ for (let gx = -camX % gridSize; gx < cw; gx += gridSize) {
28837
+ ctx.beginPath();
28838
+ ctx.moveTo(gx, 0);
28839
+ ctx.lineTo(gx, ch);
28840
+ ctx.stroke();
28841
+ }
28842
+ for (let gy = -camY % gridSize; gy < ch; gy += gridSize) {
28843
+ ctx.beginPath();
28844
+ ctx.moveTo(0, gy);
28845
+ ctx.lineTo(cw, gy);
28846
+ ctx.stroke();
28847
+ }
28848
+ for (const plat of plats) {
28849
+ const platX = plat.x - camX;
28850
+ const platY = plat.y - camY;
28851
+ const platType = plat.type ?? "ground";
28852
+ const spriteUrl = tSprites?.[platType];
28853
+ const tileImg = spriteUrl ? loadImage(spriteUrl) : null;
28854
+ if (tileImg) {
28855
+ const tileW = tileImg.naturalWidth;
28856
+ const tileH = tileImg.naturalHeight;
28857
+ const scaleH = plat.height / tileH;
28858
+ const scaledW = tileW * scaleH;
28859
+ for (let tx = 0; tx < plat.width; tx += scaledW) {
28860
+ const drawW = Math.min(scaledW, plat.width - tx);
28861
+ const srcW = drawW / scaleH;
28862
+ ctx.drawImage(tileImg, 0, 0, srcW, tileH, platX + tx, platY, drawW, plat.height);
28863
+ }
28864
+ } else {
28865
+ const color = PLATFORM_COLORS[platType] ?? PLATFORM_COLORS.ground;
28866
+ ctx.fillStyle = color;
28867
+ ctx.fillRect(platX, platY, plat.width, plat.height);
28868
+ ctx.fillStyle = "rgba(255, 255, 255, 0.15)";
28869
+ ctx.fillRect(platX, platY, plat.width, 3);
28870
+ ctx.fillStyle = "rgba(0, 0, 0, 0.3)";
28871
+ ctx.fillRect(platX, platY + plat.height - 2, plat.width, 2);
28872
+ if (platType === "hazard") {
28873
+ ctx.strokeStyle = "#e74c3c";
28874
+ ctx.lineWidth = 2;
28875
+ for (let sx = platX; sx < platX + plat.width; sx += 12) {
28876
+ ctx.beginPath();
28877
+ ctx.moveTo(sx, platY);
28878
+ ctx.lineTo(sx + 6, platY + plat.height);
28879
+ ctx.stroke();
28880
+ }
28881
+ }
28882
+ if (platType === "goal") {
28883
+ ctx.fillStyle = "rgba(241, 196, 15, 0.5)";
28746
28884
  ctx.beginPath();
28747
- ctx.moveTo(sx, py);
28748
- ctx.lineTo(sx + 6, py + plat.height);
28749
- ctx.stroke();
28885
+ ctx.arc(platX + plat.width / 2, platY - 10, 8, 0, Math.PI * 2);
28886
+ ctx.fill();
28750
28887
  }
28751
28888
  }
28752
- if (platType === "goal") {
28753
- ctx.fillStyle = "rgba(241, 196, 15, 0.5)";
28754
- ctx.beginPath();
28755
- ctx.arc(px + plat.width / 2, py - 10, 8, 0, Math.PI * 2);
28756
- ctx.fill();
28757
- }
28758
28889
  }
28759
- }
28760
- const pw = resolvedPlayer.width ?? 24;
28761
- const ph = resolvedPlayer.height ?? 32;
28762
- const ppx = resolvedPlayer.x - camX;
28763
- const ppy = resolvedPlayer.y - camY;
28764
- const facingRight = resolvedPlayer.facingRight ?? true;
28765
- const playerImg = playerSprite ? loadImage(playerSprite) : null;
28766
- if (playerImg) {
28767
- ctx.save();
28768
- if (!facingRight) {
28769
- ctx.translate(ppx + pw, ppy);
28770
- ctx.scale(-1, 1);
28771
- ctx.drawImage(playerImg, 0, 0, pw, ph);
28890
+ const pw = auth.width ?? 24;
28891
+ const ph = auth.height ?? 32;
28892
+ const ppx = px - camX;
28893
+ const ppy = py - camY;
28894
+ const facingRight = auth.facingRight ?? true;
28895
+ const playerImg = pSprite ? loadImage(pSprite) : null;
28896
+ if (playerImg) {
28897
+ ctx.save();
28898
+ if (!facingRight) {
28899
+ ctx.translate(ppx + pw, ppy);
28900
+ ctx.scale(-1, 1);
28901
+ ctx.drawImage(playerImg, 0, 0, pw, ph);
28902
+ } else {
28903
+ ctx.drawImage(playerImg, ppx, ppy, pw, ph);
28904
+ }
28905
+ ctx.restore();
28772
28906
  } else {
28773
- ctx.drawImage(playerImg, ppx, ppy, pw, ph);
28907
+ ctx.fillStyle = PLAYER_COLOR;
28908
+ const radius = Math.min(pw, ph) * 0.25;
28909
+ ctx.beginPath();
28910
+ ctx.moveTo(ppx + radius, ppy);
28911
+ ctx.lineTo(ppx + pw - radius, ppy);
28912
+ ctx.quadraticCurveTo(ppx + pw, ppy, ppx + pw, ppy + radius);
28913
+ ctx.lineTo(ppx + pw, ppy + ph - radius);
28914
+ ctx.quadraticCurveTo(ppx + pw, ppy + ph, ppx + pw - radius, ppy + ph);
28915
+ ctx.lineTo(ppx + radius, ppy + ph);
28916
+ ctx.quadraticCurveTo(ppx, ppy + ph, ppx, ppy + ph - radius);
28917
+ ctx.lineTo(ppx, ppy + radius);
28918
+ ctx.quadraticCurveTo(ppx, ppy, ppx + radius, ppy);
28919
+ ctx.fill();
28920
+ const eyeY = ppy + ph * 0.3;
28921
+ const eyeSize = 3;
28922
+ const eyeOffsetX = facingRight ? pw * 0.55 : pw * 0.2;
28923
+ ctx.fillStyle = PLAYER_EYE_COLOR;
28924
+ ctx.beginPath();
28925
+ ctx.arc(ppx + eyeOffsetX, eyeY, eyeSize, 0, Math.PI * 2);
28926
+ ctx.fill();
28927
+ ctx.beginPath();
28928
+ ctx.arc(ppx + eyeOffsetX + 7, eyeY, eyeSize, 0, Math.PI * 2);
28929
+ ctx.fill();
28774
28930
  }
28775
- ctx.restore();
28776
- } else {
28777
- ctx.fillStyle = PLAYER_COLOR;
28778
- const radius = Math.min(pw, ph) * 0.25;
28779
- ctx.beginPath();
28780
- ctx.moveTo(ppx + radius, ppy);
28781
- ctx.lineTo(ppx + pw - radius, ppy);
28782
- ctx.quadraticCurveTo(ppx + pw, ppy, ppx + pw, ppy + radius);
28783
- ctx.lineTo(ppx + pw, ppy + ph - radius);
28784
- ctx.quadraticCurveTo(ppx + pw, ppy + ph, ppx + pw - radius, ppy + ph);
28785
- ctx.lineTo(ppx + radius, ppy + ph);
28786
- ctx.quadraticCurveTo(ppx, ppy + ph, ppx, ppy + ph - radius);
28787
- ctx.lineTo(ppx, ppy + radius);
28788
- ctx.quadraticCurveTo(ppx, ppy, ppx + radius, ppy);
28789
- ctx.fill();
28790
- const eyeY = ppy + ph * 0.3;
28791
- const eyeSize = 3;
28792
- const eyeOffsetX = facingRight ? pw * 0.55 : pw * 0.2;
28793
- ctx.fillStyle = PLAYER_EYE_COLOR;
28794
- ctx.beginPath();
28795
- ctx.arc(ppx + eyeOffsetX, eyeY, eyeSize, 0, Math.PI * 2);
28796
- ctx.fill();
28797
- ctx.beginPath();
28798
- ctx.arc(ppx + eyeOffsetX + 7, eyeY, eyeSize, 0, Math.PI * 2);
28799
- ctx.fill();
28800
- }
28801
- }, [player, platforms, worldWidth, worldHeight, canvasWidth, canvasHeight, followCamera, bgColor, playerSprite, tileSprites, backgroundImage, assetBaseUrl, loadedImages]);
28931
+ };
28932
+ return interp.startLoop(drawFrame);
28933
+ }, [interp.startLoop, loadImage]);
28802
28934
  return /* @__PURE__ */ jsx(
28803
28935
  "canvas",
28804
28936
  {
@@ -28816,6 +28948,7 @@ var init_PlatformerCanvas = __esm({
28816
28948
  init_cn();
28817
28949
  init_useEventBus();
28818
28950
  init_verificationRegistry();
28951
+ init_useRenderInterpolation();
28819
28952
  PLATFORM_COLORS = {
28820
28953
  ground: "#4a7c59",
28821
28954
  platform: "#7c6b4a",
@@ -29198,13 +29331,13 @@ var init_MapView = __esm({
29198
29331
  shadowSize: [41, 41]
29199
29332
  });
29200
29333
  L.Marker.prototype.options.icon = defaultIcon;
29201
- const { useEffect: useEffect73, useRef: useRef68, useCallback: useCallback111, useState: useState104 } = React83__default;
29334
+ const { useEffect: useEffect74, useRef: useRef69, useCallback: useCallback112, useState: useState104 } = React83__default;
29202
29335
  const { Typography: Typography2 } = await Promise.resolve().then(() => (init_Typography(), Typography_exports));
29203
29336
  const { useEventBus: useEventBus2 } = await Promise.resolve().then(() => (init_useEventBus(), useEventBus_exports));
29204
29337
  function MapUpdater({ centerLat, centerLng, zoom }) {
29205
29338
  const map = useMap();
29206
- const prevRef = useRef68({ centerLat, centerLng, zoom });
29207
- useEffect73(() => {
29339
+ const prevRef = useRef69({ centerLat, centerLng, zoom });
29340
+ useEffect74(() => {
29208
29341
  const prev = prevRef.current;
29209
29342
  if (prev.centerLat !== centerLat || prev.centerLng !== centerLng || prev.zoom !== zoom) {
29210
29343
  map.setView([centerLat, centerLng], zoom);
@@ -29215,7 +29348,7 @@ var init_MapView = __esm({
29215
29348
  }
29216
29349
  function MapClickHandler({ onMapClick }) {
29217
29350
  const map = useMap();
29218
- useEffect73(() => {
29351
+ useEffect74(() => {
29219
29352
  if (!onMapClick) return;
29220
29353
  const handler = (e) => {
29221
29354
  onMapClick(e.latlng.lat, e.latlng.lng);
@@ -29244,7 +29377,7 @@ var init_MapView = __esm({
29244
29377
  }) {
29245
29378
  const eventBus = useEventBus2();
29246
29379
  const [clickedPosition, setClickedPosition] = useState104(null);
29247
- const handleMapClick = useCallback111((lat, lng) => {
29380
+ const handleMapClick = useCallback112((lat, lng) => {
29248
29381
  if (showClickedPin) {
29249
29382
  setClickedPosition({ lat, lng });
29250
29383
  }
@@ -29253,7 +29386,7 @@ var init_MapView = __esm({
29253
29386
  eventBus.emit(`UI:${mapClickEvent}`, { latitude: lat, longitude: lng });
29254
29387
  }
29255
29388
  }, [onMapClick, mapClickEvent, eventBus, showClickedPin]);
29256
- const handleMarkerClick = useCallback111((marker) => {
29389
+ const handleMarkerClick = useCallback112((marker) => {
29257
29390
  onMarkerClick?.(marker);
29258
29391
  if (markerClickEvent) {
29259
29392
  eventBus.emit(`UI:${markerClickEvent}`, { ...marker });
@@ -44738,6 +44871,10 @@ function SequencerBoard({
44738
44871
  stepDurationMs = 1e3,
44739
44872
  playEvent,
44740
44873
  completeEvent,
44874
+ placeEvent,
44875
+ removeEvent,
44876
+ checkEvent,
44877
+ playAgainEvent,
44741
44878
  className
44742
44879
  }) {
44743
44880
  const { emit } = useEventBus();
@@ -44773,7 +44910,8 @@ function SequencerBoard({
44773
44910
  return next;
44774
44911
  });
44775
44912
  emit("UI:PLAY_SOUND", { key: "drop_slot" });
44776
- }, [emit]);
44913
+ if (placeEvent) emit(`UI:${placeEvent}`, { slotIndex: index, actionId: item.id });
44914
+ }, [emit, placeEvent]);
44777
44915
  const handleSlotRemove = useCallback((index) => {
44778
44916
  setSlots((prev) => {
44779
44917
  const next = [...prev];
@@ -44786,7 +44924,8 @@ function SequencerBoard({
44786
44924
  return next;
44787
44925
  });
44788
44926
  emit("UI:PLAY_SOUND", { key: "back" });
44789
- }, [emit]);
44927
+ if (removeEvent) emit(`UI:${removeEvent}`, { slotIndex: index });
44928
+ }, [emit, removeEvent]);
44790
44929
  const handleReset = useCallback(() => {
44791
44930
  if (timerRef.current) clearTimeout(timerRef.current);
44792
44931
  setSlots(Array.from({ length: maxSlots }, () => void 0));
@@ -44794,7 +44933,8 @@ function SequencerBoard({
44794
44933
  setCurrentStep(-1);
44795
44934
  setAttempts(0);
44796
44935
  setSlotFeedback(Array.from({ length: maxSlots }, () => null));
44797
- }, [maxSlots]);
44936
+ if (playAgainEvent) emit(`UI:${playAgainEvent}`, {});
44937
+ }, [maxSlots, playAgainEvent, emit]);
44798
44938
  const filledSlots = slots.filter((s) => !!s);
44799
44939
  const canPlay = filledSlots.length > 0 && playState === "idle";
44800
44940
  const handlePlay = useCallback(() => {
@@ -44816,6 +44956,7 @@ function SequencerBoard({
44816
44956
  const success = solutions.some(
44817
44957
  (sol) => sol.length === playerIds.length && sol.every((id, i) => id === playerIds[i])
44818
44958
  );
44959
+ if (checkEvent) emit(`UI:${checkEvent}`, { sequence: playerIds });
44819
44960
  if (success) {
44820
44961
  setPlayState("success");
44821
44962
  setCurrentStep(-1);
@@ -44843,7 +44984,7 @@ function SequencerBoard({
44843
44984
  }
44844
44985
  };
44845
44986
  timerRef.current = setTimeout(advance, stepDurationMs);
44846
- }, [canPlay, slots, maxSlots, solutions, stepDurationMs, playEvent, completeEvent, emit]);
44987
+ }, [canPlay, slots, maxSlots, solutions, stepDurationMs, playEvent, completeEvent, checkEvent, emit]);
44847
44988
  const machine = {
44848
44989
  name: str(resolved?.title),
44849
44990
  description: str(resolved?.description),
@@ -45098,11 +45239,13 @@ function SimulationCanvas({
45098
45239
  height = 400,
45099
45240
  running,
45100
45241
  speed = 1,
45242
+ bodies: externalBodies,
45101
45243
  className
45102
45244
  }) {
45103
45245
  const preset = useMemo(() => resolvePreset(presetProp), [presetProp]);
45104
45246
  const canvasRef = useRef(null);
45105
45247
  const bodiesRef = useRef(structuredClone(preset.bodies));
45248
+ const interp = useRenderInterpolation();
45106
45249
  useEffect(() => {
45107
45250
  bodiesRef.current = structuredClone(preset.bodies);
45108
45251
  }, [preset]);
@@ -45200,6 +45343,67 @@ function SimulationCanvas({
45200
45343
  }
45201
45344
  }, [width, height, preset]);
45202
45345
  useEffect(() => {
45346
+ if (!externalBodies) return;
45347
+ interp.onSnapshot(externalBodies.map((b) => ({ id: b.id, x: b.x, y: b.y })));
45348
+ }, [externalBodies]);
45349
+ const presetRef = useRef(preset);
45350
+ presetRef.current = preset;
45351
+ const widthRef = useRef(width);
45352
+ widthRef.current = width;
45353
+ const heightRef = useRef(height);
45354
+ heightRef.current = height;
45355
+ useEffect(() => {
45356
+ if (!externalBodies) return;
45357
+ const drawInterpolated = (positions) => {
45358
+ const canvas = canvasRef.current;
45359
+ if (!canvas) return;
45360
+ const ctx = canvas.getContext("2d");
45361
+ if (!ctx) return;
45362
+ const bodies = bodiesRef.current;
45363
+ const p2 = presetRef.current;
45364
+ const w = widthRef.current;
45365
+ const h = heightRef.current;
45366
+ ctx.clearRect(0, 0, w, h);
45367
+ ctx.fillStyle = p2.backgroundColor ?? "#1a1a2e";
45368
+ ctx.fillRect(0, 0, w, h);
45369
+ if (p2.constraints) {
45370
+ for (const c of p2.constraints) {
45371
+ const a = bodies[c.bodyA];
45372
+ const b = bodies[c.bodyB];
45373
+ if (a && b) {
45374
+ const aPos = positions.get(a.id) ?? a;
45375
+ const bPos = positions.get(b.id) ?? b;
45376
+ ctx.beginPath();
45377
+ ctx.moveTo(aPos.x, aPos.y);
45378
+ ctx.lineTo(bPos.x, bPos.y);
45379
+ ctx.strokeStyle = "#533483";
45380
+ ctx.lineWidth = 1;
45381
+ ctx.setLineDash([4, 4]);
45382
+ ctx.stroke();
45383
+ ctx.setLineDash([]);
45384
+ }
45385
+ }
45386
+ }
45387
+ for (const body of bodies) {
45388
+ const pos = positions.get(body.id) ?? body;
45389
+ ctx.beginPath();
45390
+ ctx.arc(pos.x, pos.y, body.radius, 0, Math.PI * 2);
45391
+ ctx.fillStyle = body.color ?? "#e94560";
45392
+ ctx.fill();
45393
+ if (p2.showVelocity) {
45394
+ ctx.beginPath();
45395
+ ctx.moveTo(pos.x, pos.y);
45396
+ ctx.lineTo(pos.x + body.vx * 0.1, pos.y + body.vy * 0.1);
45397
+ ctx.strokeStyle = "#16213e";
45398
+ ctx.lineWidth = 2;
45399
+ ctx.stroke();
45400
+ }
45401
+ }
45402
+ };
45403
+ return interp.startLoop(drawInterpolated);
45404
+ }, [externalBodies !== void 0, interp.startLoop]);
45405
+ useEffect(() => {
45406
+ if (externalBodies !== void 0) return;
45203
45407
  if (!running) return;
45204
45408
  let raf;
45205
45409
  const loop = () => {
@@ -45209,10 +45413,11 @@ function SimulationCanvas({
45209
45413
  };
45210
45414
  raf = requestAnimationFrame(loop);
45211
45415
  return () => cancelAnimationFrame(raf);
45212
- }, [running, step, draw]);
45416
+ }, [running, step, draw, externalBodies]);
45213
45417
  useEffect(() => {
45418
+ if (externalBodies !== void 0) return;
45214
45419
  draw();
45215
- }, [draw]);
45420
+ }, [draw, externalBodies]);
45216
45421
  useEffect(() => {
45217
45422
  if (typeof window === "undefined") return;
45218
45423
  const canvas = canvasRef.current;
@@ -45238,6 +45443,7 @@ var init_SimulationCanvas = __esm({
45238
45443
  init_atoms2();
45239
45444
  init_verificationRegistry();
45240
45445
  init_presets();
45446
+ init_useRenderInterpolation();
45241
45447
  SimulationCanvas.displayName = "SimulationCanvas";
45242
45448
  }
45243
45449
  });