@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.
@@ -28834,6 +28834,78 @@ var init_GameOverScreen = __esm({
28834
28834
  GameOverScreen.displayName = "GameOverScreen";
28835
28835
  }
28836
28836
  });
28837
+ function useRenderInterpolation(options = {}) {
28838
+ const tickIntervalMs = options.tickIntervalMs ?? 1e3 / 30;
28839
+ const prevRef = React77.useRef(null);
28840
+ const currRef = React77.useRef(null);
28841
+ const rafRef = React77.useRef(null);
28842
+ const onSnapshot = React77.useCallback((entities) => {
28843
+ prevRef.current = currRef.current;
28844
+ currRef.current = { entities, arrivedAt: performance.now() };
28845
+ }, []);
28846
+ const getInterpolated = React77.useCallback((now) => {
28847
+ const out = /* @__PURE__ */ new Map();
28848
+ const curr = currRef.current;
28849
+ if (!curr) return out;
28850
+ const prev = prevRef.current;
28851
+ if (!prev) {
28852
+ for (const e of curr.entities) out.set(e.id, { x: e.x, y: e.y });
28853
+ return out;
28854
+ }
28855
+ const rawAlpha = (now - curr.arrivedAt) / tickIntervalMs;
28856
+ const alpha = rawAlpha < 0 ? 0 : rawAlpha > 1 ? 1 : rawAlpha;
28857
+ const prevMap = /* @__PURE__ */ new Map();
28858
+ for (const e of prev.entities) prevMap.set(e.id, e);
28859
+ for (const c of curr.entities) {
28860
+ const p2 = prevMap.get(c.id);
28861
+ if (!p2) {
28862
+ out.set(c.id, { x: c.x, y: c.y });
28863
+ } else {
28864
+ out.set(c.id, {
28865
+ x: p2.x + (c.x - p2.x) * alpha,
28866
+ y: p2.y + (c.y - p2.y) * alpha
28867
+ });
28868
+ }
28869
+ }
28870
+ return out;
28871
+ }, [tickIntervalMs]);
28872
+ const startLoop = React77.useCallback(
28873
+ (draw) => {
28874
+ let active = true;
28875
+ const loop = () => {
28876
+ if (!active) return;
28877
+ try {
28878
+ draw(getInterpolated(performance.now()));
28879
+ } catch {
28880
+ }
28881
+ rafRef.current = requestAnimationFrame(loop);
28882
+ };
28883
+ rafRef.current = requestAnimationFrame(loop);
28884
+ return () => {
28885
+ active = false;
28886
+ if (rafRef.current !== null) {
28887
+ cancelAnimationFrame(rafRef.current);
28888
+ rafRef.current = null;
28889
+ }
28890
+ };
28891
+ },
28892
+ [getInterpolated]
28893
+ );
28894
+ React77.useEffect(() => {
28895
+ return () => {
28896
+ if (rafRef.current !== null) {
28897
+ cancelAnimationFrame(rafRef.current);
28898
+ rafRef.current = null;
28899
+ }
28900
+ };
28901
+ }, []);
28902
+ return { onSnapshot, getInterpolated, startLoop };
28903
+ }
28904
+ var init_useRenderInterpolation = __esm({
28905
+ "hooks/useRenderInterpolation.ts"() {
28906
+ "use client";
28907
+ }
28908
+ });
28837
28909
  function PlatformerCanvas({
28838
28910
  player,
28839
28911
  platforms = [
@@ -28902,8 +28974,43 @@ function PlatformerCanvas({
28902
28974
  y: 336,
28903
28975
  width: 32,
28904
28976
  height: 48,
28977
+ vx: 0,
28978
+ vy: 0,
28979
+ grounded: true,
28905
28980
  facingRight: true
28906
28981
  };
28982
+ const playerRef = React77.useRef(resolvedPlayer);
28983
+ playerRef.current = resolvedPlayer;
28984
+ const interp = useRenderInterpolation();
28985
+ React77.useEffect(() => {
28986
+ interp.onSnapshot([{ id: "player", x: resolvedPlayer.x, y: resolvedPlayer.y }]);
28987
+ }, [resolvedPlayer.x, resolvedPlayer.y]);
28988
+ const propsRef = React77.useRef({
28989
+ platforms,
28990
+ worldWidth,
28991
+ worldHeight,
28992
+ canvasWidth,
28993
+ canvasHeight,
28994
+ followCamera,
28995
+ bgColor,
28996
+ playerSprite,
28997
+ tileSprites,
28998
+ backgroundImage,
28999
+ assetBaseUrl
29000
+ });
29001
+ propsRef.current = {
29002
+ platforms,
29003
+ worldWidth,
29004
+ worldHeight,
29005
+ canvasWidth,
29006
+ canvasHeight,
29007
+ followCamera,
29008
+ bgColor,
29009
+ playerSprite,
29010
+ tileSprites,
29011
+ backgroundImage,
29012
+ assetBaseUrl
29013
+ };
28907
29014
  const handleKeyDown = React77.useCallback((e) => {
28908
29015
  if (keysRef.current.has(e.code)) return;
28909
29016
  keysRef.current.add(e.code);
@@ -28952,124 +29059,149 @@ function PlatformerCanvas({
28952
29059
  canvas.width = canvasWidth * dpr;
28953
29060
  canvas.height = canvasHeight * dpr;
28954
29061
  ctx.scale(dpr, dpr);
28955
- let camX = 0;
28956
- let camY = 0;
28957
- if (followCamera) {
28958
- camX = Math.max(0, Math.min(resolvedPlayer.x - canvasWidth / 2, worldWidth - canvasWidth));
28959
- camY = Math.max(0, Math.min(resolvedPlayer.y - canvasHeight / 2 - 50, worldHeight - canvasHeight));
28960
- }
28961
- const bgImg = backgroundImage ? loadImage(backgroundImage) : null;
28962
- if (bgImg) {
28963
- ctx.drawImage(bgImg, 0, 0, canvasWidth, canvasHeight);
28964
- } else if (bgColor) {
28965
- ctx.fillStyle = bgColor;
28966
- ctx.fillRect(0, 0, canvasWidth, canvasHeight);
28967
- } else {
28968
- const grad = ctx.createLinearGradient(0, 0, 0, canvasHeight);
28969
- grad.addColorStop(0, SKY_GRADIENT_TOP);
28970
- grad.addColorStop(1, SKY_GRADIENT_BOTTOM);
28971
- ctx.fillStyle = grad;
28972
- ctx.fillRect(0, 0, canvasWidth, canvasHeight);
28973
- }
28974
- ctx.strokeStyle = GRID_COLOR;
28975
- ctx.lineWidth = 1;
28976
- const gridSize = 32;
28977
- for (let gx = -camX % gridSize; gx < canvasWidth; gx += gridSize) {
28978
- ctx.beginPath();
28979
- ctx.moveTo(gx, 0);
28980
- ctx.lineTo(gx, canvasHeight);
28981
- ctx.stroke();
28982
- }
28983
- for (let gy = -camY % gridSize; gy < canvasHeight; gy += gridSize) {
28984
- ctx.beginPath();
28985
- ctx.moveTo(0, gy);
28986
- ctx.lineTo(canvasWidth, gy);
28987
- ctx.stroke();
28988
- }
28989
- for (const plat of platforms) {
28990
- const px = plat.x - camX;
28991
- const py = plat.y - camY;
28992
- const platType = plat.type ?? "ground";
28993
- const spriteUrl = tileSprites?.[platType];
28994
- const tileImg = spriteUrl ? loadImage(spriteUrl) : null;
28995
- if (tileImg) {
28996
- const tileW = tileImg.naturalWidth;
28997
- const tileH = tileImg.naturalHeight;
28998
- const scaleH = plat.height / tileH;
28999
- const scaledW = tileW * scaleH;
29000
- for (let tx = 0; tx < plat.width; tx += scaledW) {
29001
- const drawW = Math.min(scaledW, plat.width - tx);
29002
- const srcW = drawW / scaleH;
29003
- ctx.drawImage(tileImg, 0, 0, srcW, tileH, px + tx, py, drawW, plat.height);
29004
- }
29062
+ }, [canvasWidth, canvasHeight]);
29063
+ React77.useEffect(() => {
29064
+ const drawFrame = (positions) => {
29065
+ const canvas = canvasRef.current;
29066
+ if (!canvas) return;
29067
+ const ctx = canvas.getContext("2d");
29068
+ if (!ctx) return;
29069
+ const {
29070
+ platforms: plats,
29071
+ worldWidth: ww,
29072
+ worldHeight: wh,
29073
+ canvasWidth: cw,
29074
+ canvasHeight: ch,
29075
+ followCamera: fc,
29076
+ bgColor: bg,
29077
+ playerSprite: pSprite,
29078
+ tileSprites: tSprites,
29079
+ backgroundImage: bgImg
29080
+ } = propsRef.current;
29081
+ const auth = playerRef.current;
29082
+ const interped = positions.get("player");
29083
+ const px = interped?.x ?? auth.x;
29084
+ const py = interped?.y ?? auth.y;
29085
+ let camX = 0;
29086
+ let camY = 0;
29087
+ if (fc) {
29088
+ camX = Math.max(0, Math.min(px - cw / 2, ww - cw));
29089
+ camY = Math.max(0, Math.min(py - ch / 2 - 50, wh - ch));
29090
+ }
29091
+ const bgImage = bgImg ? loadImage(bgImg) : null;
29092
+ if (bgImage) {
29093
+ ctx.drawImage(bgImage, 0, 0, cw, ch);
29094
+ } else if (bg) {
29095
+ ctx.fillStyle = bg;
29096
+ ctx.fillRect(0, 0, cw, ch);
29005
29097
  } else {
29006
- const color = PLATFORM_COLORS[platType] ?? PLATFORM_COLORS.ground;
29007
- ctx.fillStyle = color;
29008
- ctx.fillRect(px, py, plat.width, plat.height);
29009
- ctx.fillStyle = "rgba(255, 255, 255, 0.15)";
29010
- ctx.fillRect(px, py, plat.width, 3);
29011
- ctx.fillStyle = "rgba(0, 0, 0, 0.3)";
29012
- ctx.fillRect(px, py + plat.height - 2, plat.width, 2);
29013
- if (platType === "hazard") {
29014
- ctx.strokeStyle = "#e74c3c";
29015
- ctx.lineWidth = 2;
29016
- for (let sx = px; sx < px + plat.width; sx += 12) {
29098
+ const grad = ctx.createLinearGradient(0, 0, 0, ch);
29099
+ grad.addColorStop(0, SKY_GRADIENT_TOP);
29100
+ grad.addColorStop(1, SKY_GRADIENT_BOTTOM);
29101
+ ctx.fillStyle = grad;
29102
+ ctx.fillRect(0, 0, cw, ch);
29103
+ }
29104
+ ctx.strokeStyle = GRID_COLOR;
29105
+ ctx.lineWidth = 1;
29106
+ const gridSize = 32;
29107
+ for (let gx = -camX % gridSize; gx < cw; gx += gridSize) {
29108
+ ctx.beginPath();
29109
+ ctx.moveTo(gx, 0);
29110
+ ctx.lineTo(gx, ch);
29111
+ ctx.stroke();
29112
+ }
29113
+ for (let gy = -camY % gridSize; gy < ch; gy += gridSize) {
29114
+ ctx.beginPath();
29115
+ ctx.moveTo(0, gy);
29116
+ ctx.lineTo(cw, gy);
29117
+ ctx.stroke();
29118
+ }
29119
+ for (const plat of plats) {
29120
+ const platX = plat.x - camX;
29121
+ const platY = plat.y - camY;
29122
+ const platType = plat.type ?? "ground";
29123
+ const spriteUrl = tSprites?.[platType];
29124
+ const tileImg = spriteUrl ? loadImage(spriteUrl) : null;
29125
+ if (tileImg) {
29126
+ const tileW = tileImg.naturalWidth;
29127
+ const tileH = tileImg.naturalHeight;
29128
+ const scaleH = plat.height / tileH;
29129
+ const scaledW = tileW * scaleH;
29130
+ for (let tx = 0; tx < plat.width; tx += scaledW) {
29131
+ const drawW = Math.min(scaledW, plat.width - tx);
29132
+ const srcW = drawW / scaleH;
29133
+ ctx.drawImage(tileImg, 0, 0, srcW, tileH, platX + tx, platY, drawW, plat.height);
29134
+ }
29135
+ } else {
29136
+ const color = PLATFORM_COLORS[platType] ?? PLATFORM_COLORS.ground;
29137
+ ctx.fillStyle = color;
29138
+ ctx.fillRect(platX, platY, plat.width, plat.height);
29139
+ ctx.fillStyle = "rgba(255, 255, 255, 0.15)";
29140
+ ctx.fillRect(platX, platY, plat.width, 3);
29141
+ ctx.fillStyle = "rgba(0, 0, 0, 0.3)";
29142
+ ctx.fillRect(platX, platY + plat.height - 2, plat.width, 2);
29143
+ if (platType === "hazard") {
29144
+ ctx.strokeStyle = "#e74c3c";
29145
+ ctx.lineWidth = 2;
29146
+ for (let sx = platX; sx < platX + plat.width; sx += 12) {
29147
+ ctx.beginPath();
29148
+ ctx.moveTo(sx, platY);
29149
+ ctx.lineTo(sx + 6, platY + plat.height);
29150
+ ctx.stroke();
29151
+ }
29152
+ }
29153
+ if (platType === "goal") {
29154
+ ctx.fillStyle = "rgba(241, 196, 15, 0.5)";
29017
29155
  ctx.beginPath();
29018
- ctx.moveTo(sx, py);
29019
- ctx.lineTo(sx + 6, py + plat.height);
29020
- ctx.stroke();
29156
+ ctx.arc(platX + plat.width / 2, platY - 10, 8, 0, Math.PI * 2);
29157
+ ctx.fill();
29021
29158
  }
29022
29159
  }
29023
- if (platType === "goal") {
29024
- ctx.fillStyle = "rgba(241, 196, 15, 0.5)";
29025
- ctx.beginPath();
29026
- ctx.arc(px + plat.width / 2, py - 10, 8, 0, Math.PI * 2);
29027
- ctx.fill();
29028
- }
29029
29160
  }
29030
- }
29031
- const pw = resolvedPlayer.width ?? 24;
29032
- const ph = resolvedPlayer.height ?? 32;
29033
- const ppx = resolvedPlayer.x - camX;
29034
- const ppy = resolvedPlayer.y - camY;
29035
- const facingRight = resolvedPlayer.facingRight ?? true;
29036
- const playerImg = playerSprite ? loadImage(playerSprite) : null;
29037
- if (playerImg) {
29038
- ctx.save();
29039
- if (!facingRight) {
29040
- ctx.translate(ppx + pw, ppy);
29041
- ctx.scale(-1, 1);
29042
- ctx.drawImage(playerImg, 0, 0, pw, ph);
29161
+ const pw = auth.width ?? 24;
29162
+ const ph = auth.height ?? 32;
29163
+ const ppx = px - camX;
29164
+ const ppy = py - camY;
29165
+ const facingRight = auth.facingRight ?? true;
29166
+ const playerImg = pSprite ? loadImage(pSprite) : null;
29167
+ if (playerImg) {
29168
+ ctx.save();
29169
+ if (!facingRight) {
29170
+ ctx.translate(ppx + pw, ppy);
29171
+ ctx.scale(-1, 1);
29172
+ ctx.drawImage(playerImg, 0, 0, pw, ph);
29173
+ } else {
29174
+ ctx.drawImage(playerImg, ppx, ppy, pw, ph);
29175
+ }
29176
+ ctx.restore();
29043
29177
  } else {
29044
- ctx.drawImage(playerImg, ppx, ppy, pw, ph);
29178
+ ctx.fillStyle = PLAYER_COLOR;
29179
+ const radius = Math.min(pw, ph) * 0.25;
29180
+ ctx.beginPath();
29181
+ ctx.moveTo(ppx + radius, ppy);
29182
+ ctx.lineTo(ppx + pw - radius, ppy);
29183
+ ctx.quadraticCurveTo(ppx + pw, ppy, ppx + pw, ppy + radius);
29184
+ ctx.lineTo(ppx + pw, ppy + ph - radius);
29185
+ ctx.quadraticCurveTo(ppx + pw, ppy + ph, ppx + pw - radius, ppy + ph);
29186
+ ctx.lineTo(ppx + radius, ppy + ph);
29187
+ ctx.quadraticCurveTo(ppx, ppy + ph, ppx, ppy + ph - radius);
29188
+ ctx.lineTo(ppx, ppy + radius);
29189
+ ctx.quadraticCurveTo(ppx, ppy, ppx + radius, ppy);
29190
+ ctx.fill();
29191
+ const eyeY = ppy + ph * 0.3;
29192
+ const eyeSize = 3;
29193
+ const eyeOffsetX = facingRight ? pw * 0.55 : pw * 0.2;
29194
+ ctx.fillStyle = PLAYER_EYE_COLOR;
29195
+ ctx.beginPath();
29196
+ ctx.arc(ppx + eyeOffsetX, eyeY, eyeSize, 0, Math.PI * 2);
29197
+ ctx.fill();
29198
+ ctx.beginPath();
29199
+ ctx.arc(ppx + eyeOffsetX + 7, eyeY, eyeSize, 0, Math.PI * 2);
29200
+ ctx.fill();
29045
29201
  }
29046
- ctx.restore();
29047
- } else {
29048
- ctx.fillStyle = PLAYER_COLOR;
29049
- const radius = Math.min(pw, ph) * 0.25;
29050
- ctx.beginPath();
29051
- ctx.moveTo(ppx + radius, ppy);
29052
- ctx.lineTo(ppx + pw - radius, ppy);
29053
- ctx.quadraticCurveTo(ppx + pw, ppy, ppx + pw, ppy + radius);
29054
- ctx.lineTo(ppx + pw, ppy + ph - radius);
29055
- ctx.quadraticCurveTo(ppx + pw, ppy + ph, ppx + pw - radius, ppy + ph);
29056
- ctx.lineTo(ppx + radius, ppy + ph);
29057
- ctx.quadraticCurveTo(ppx, ppy + ph, ppx, ppy + ph - radius);
29058
- ctx.lineTo(ppx, ppy + radius);
29059
- ctx.quadraticCurveTo(ppx, ppy, ppx + radius, ppy);
29060
- ctx.fill();
29061
- const eyeY = ppy + ph * 0.3;
29062
- const eyeSize = 3;
29063
- const eyeOffsetX = facingRight ? pw * 0.55 : pw * 0.2;
29064
- ctx.fillStyle = PLAYER_EYE_COLOR;
29065
- ctx.beginPath();
29066
- ctx.arc(ppx + eyeOffsetX, eyeY, eyeSize, 0, Math.PI * 2);
29067
- ctx.fill();
29068
- ctx.beginPath();
29069
- ctx.arc(ppx + eyeOffsetX + 7, eyeY, eyeSize, 0, Math.PI * 2);
29070
- ctx.fill();
29071
- }
29072
- }, [player, platforms, worldWidth, worldHeight, canvasWidth, canvasHeight, followCamera, bgColor, playerSprite, tileSprites, backgroundImage, assetBaseUrl, loadedImages]);
29202
+ };
29203
+ return interp.startLoop(drawFrame);
29204
+ }, [interp.startLoop, loadImage]);
29073
29205
  return /* @__PURE__ */ jsxRuntime.jsx(
29074
29206
  "canvas",
29075
29207
  {
@@ -29087,6 +29219,7 @@ var init_PlatformerCanvas = __esm({
29087
29219
  init_cn();
29088
29220
  init_useEventBus();
29089
29221
  init_verificationRegistry();
29222
+ init_useRenderInterpolation();
29090
29223
  PLATFORM_COLORS = {
29091
29224
  ground: "#4a7c59",
29092
29225
  platform: "#7c6b4a",
@@ -29491,13 +29624,13 @@ var init_MapView = __esm({
29491
29624
  shadowSize: [41, 41]
29492
29625
  });
29493
29626
  L.Marker.prototype.options.icon = defaultIcon;
29494
- const { useEffect: useEffect73, useRef: useRef70, useCallback: useCallback113, useState: useState105 } = React77__namespace.default;
29627
+ const { useEffect: useEffect74, useRef: useRef71, useCallback: useCallback114, useState: useState105 } = React77__namespace.default;
29495
29628
  const { Typography: Typography2 } = await Promise.resolve().then(() => (init_Typography(), Typography_exports));
29496
29629
  const { useEventBus: useEventBus2 } = await Promise.resolve().then(() => (init_useEventBus(), useEventBus_exports));
29497
29630
  function MapUpdater({ centerLat, centerLng, zoom }) {
29498
29631
  const map = useMap();
29499
- const prevRef = useRef70({ centerLat, centerLng, zoom });
29500
- useEffect73(() => {
29632
+ const prevRef = useRef71({ centerLat, centerLng, zoom });
29633
+ useEffect74(() => {
29501
29634
  const prev = prevRef.current;
29502
29635
  if (prev.centerLat !== centerLat || prev.centerLng !== centerLng || prev.zoom !== zoom) {
29503
29636
  map.setView([centerLat, centerLng], zoom);
@@ -29508,7 +29641,7 @@ var init_MapView = __esm({
29508
29641
  }
29509
29642
  function MapClickHandler({ onMapClick }) {
29510
29643
  const map = useMap();
29511
- useEffect73(() => {
29644
+ useEffect74(() => {
29512
29645
  if (!onMapClick) return;
29513
29646
  const handler = (e) => {
29514
29647
  onMapClick(e.latlng.lat, e.latlng.lng);
@@ -29537,7 +29670,7 @@ var init_MapView = __esm({
29537
29670
  }) {
29538
29671
  const eventBus = useEventBus2();
29539
29672
  const [clickedPosition, setClickedPosition] = useState105(null);
29540
- const handleMapClick = useCallback113((lat, lng) => {
29673
+ const handleMapClick = useCallback114((lat, lng) => {
29541
29674
  if (showClickedPin) {
29542
29675
  setClickedPosition({ lat, lng });
29543
29676
  }
@@ -29546,7 +29679,7 @@ var init_MapView = __esm({
29546
29679
  eventBus.emit(`UI:${mapClickEvent}`, { latitude: lat, longitude: lng });
29547
29680
  }
29548
29681
  }, [onMapClick, mapClickEvent, eventBus, showClickedPin]);
29549
- const handleMarkerClick = useCallback113((marker) => {
29682
+ const handleMarkerClick = useCallback114((marker) => {
29550
29683
  onMarkerClick?.(marker);
29551
29684
  if (markerClickEvent) {
29552
29685
  eventBus.emit(`UI:${markerClickEvent}`, { ...marker });
@@ -45508,6 +45641,10 @@ function SequencerBoard({
45508
45641
  stepDurationMs = 1e3,
45509
45642
  playEvent,
45510
45643
  completeEvent,
45644
+ placeEvent,
45645
+ removeEvent,
45646
+ checkEvent,
45647
+ playAgainEvent,
45511
45648
  className
45512
45649
  }) {
45513
45650
  const { emit } = useEventBus();
@@ -45543,7 +45680,8 @@ function SequencerBoard({
45543
45680
  return next;
45544
45681
  });
45545
45682
  emit("UI:PLAY_SOUND", { key: "drop_slot" });
45546
- }, [emit]);
45683
+ if (placeEvent) emit(`UI:${placeEvent}`, { slotIndex: index, actionId: item.id });
45684
+ }, [emit, placeEvent]);
45547
45685
  const handleSlotRemove = React77.useCallback((index) => {
45548
45686
  setSlots((prev) => {
45549
45687
  const next = [...prev];
@@ -45556,7 +45694,8 @@ function SequencerBoard({
45556
45694
  return next;
45557
45695
  });
45558
45696
  emit("UI:PLAY_SOUND", { key: "back" });
45559
- }, [emit]);
45697
+ if (removeEvent) emit(`UI:${removeEvent}`, { slotIndex: index });
45698
+ }, [emit, removeEvent]);
45560
45699
  const handleReset = React77.useCallback(() => {
45561
45700
  if (timerRef.current) clearTimeout(timerRef.current);
45562
45701
  setSlots(Array.from({ length: maxSlots }, () => void 0));
@@ -45564,7 +45703,8 @@ function SequencerBoard({
45564
45703
  setCurrentStep(-1);
45565
45704
  setAttempts(0);
45566
45705
  setSlotFeedback(Array.from({ length: maxSlots }, () => null));
45567
- }, [maxSlots]);
45706
+ if (playAgainEvent) emit(`UI:${playAgainEvent}`, {});
45707
+ }, [maxSlots, playAgainEvent, emit]);
45568
45708
  const filledSlots = slots.filter((s) => !!s);
45569
45709
  const canPlay = filledSlots.length > 0 && playState === "idle";
45570
45710
  const handlePlay = React77.useCallback(() => {
@@ -45586,6 +45726,7 @@ function SequencerBoard({
45586
45726
  const success = solutions.some(
45587
45727
  (sol) => sol.length === playerIds.length && sol.every((id, i) => id === playerIds[i])
45588
45728
  );
45729
+ if (checkEvent) emit(`UI:${checkEvent}`, { sequence: playerIds });
45589
45730
  if (success) {
45590
45731
  setPlayState("success");
45591
45732
  setCurrentStep(-1);
@@ -45613,7 +45754,7 @@ function SequencerBoard({
45613
45754
  }
45614
45755
  };
45615
45756
  timerRef.current = setTimeout(advance, stepDurationMs);
45616
- }, [canPlay, slots, maxSlots, solutions, stepDurationMs, playEvent, completeEvent, emit]);
45757
+ }, [canPlay, slots, maxSlots, solutions, stepDurationMs, playEvent, completeEvent, checkEvent, emit]);
45617
45758
  const machine = {
45618
45759
  name: str(resolved?.title),
45619
45760
  description: str(resolved?.description),
@@ -45868,11 +46009,13 @@ function SimulationCanvas({
45868
46009
  height = 400,
45869
46010
  running,
45870
46011
  speed = 1,
46012
+ bodies: externalBodies,
45871
46013
  className
45872
46014
  }) {
45873
46015
  const preset = React77.useMemo(() => resolvePreset(presetProp), [presetProp]);
45874
46016
  const canvasRef = React77.useRef(null);
45875
46017
  const bodiesRef = React77.useRef(structuredClone(preset.bodies));
46018
+ const interp = useRenderInterpolation();
45876
46019
  React77.useEffect(() => {
45877
46020
  bodiesRef.current = structuredClone(preset.bodies);
45878
46021
  }, [preset]);
@@ -45970,6 +46113,67 @@ function SimulationCanvas({
45970
46113
  }
45971
46114
  }, [width, height, preset]);
45972
46115
  React77.useEffect(() => {
46116
+ if (!externalBodies) return;
46117
+ interp.onSnapshot(externalBodies.map((b) => ({ id: b.id, x: b.x, y: b.y })));
46118
+ }, [externalBodies]);
46119
+ const presetRef = React77.useRef(preset);
46120
+ presetRef.current = preset;
46121
+ const widthRef = React77.useRef(width);
46122
+ widthRef.current = width;
46123
+ const heightRef = React77.useRef(height);
46124
+ heightRef.current = height;
46125
+ React77.useEffect(() => {
46126
+ if (!externalBodies) return;
46127
+ const drawInterpolated = (positions) => {
46128
+ const canvas = canvasRef.current;
46129
+ if (!canvas) return;
46130
+ const ctx = canvas.getContext("2d");
46131
+ if (!ctx) return;
46132
+ const bodies = bodiesRef.current;
46133
+ const p2 = presetRef.current;
46134
+ const w = widthRef.current;
46135
+ const h = heightRef.current;
46136
+ ctx.clearRect(0, 0, w, h);
46137
+ ctx.fillStyle = p2.backgroundColor ?? "#1a1a2e";
46138
+ ctx.fillRect(0, 0, w, h);
46139
+ if (p2.constraints) {
46140
+ for (const c of p2.constraints) {
46141
+ const a = bodies[c.bodyA];
46142
+ const b = bodies[c.bodyB];
46143
+ if (a && b) {
46144
+ const aPos = positions.get(a.id) ?? a;
46145
+ const bPos = positions.get(b.id) ?? b;
46146
+ ctx.beginPath();
46147
+ ctx.moveTo(aPos.x, aPos.y);
46148
+ ctx.lineTo(bPos.x, bPos.y);
46149
+ ctx.strokeStyle = "#533483";
46150
+ ctx.lineWidth = 1;
46151
+ ctx.setLineDash([4, 4]);
46152
+ ctx.stroke();
46153
+ ctx.setLineDash([]);
46154
+ }
46155
+ }
46156
+ }
46157
+ for (const body of bodies) {
46158
+ const pos = positions.get(body.id) ?? body;
46159
+ ctx.beginPath();
46160
+ ctx.arc(pos.x, pos.y, body.radius, 0, Math.PI * 2);
46161
+ ctx.fillStyle = body.color ?? "#e94560";
46162
+ ctx.fill();
46163
+ if (p2.showVelocity) {
46164
+ ctx.beginPath();
46165
+ ctx.moveTo(pos.x, pos.y);
46166
+ ctx.lineTo(pos.x + body.vx * 0.1, pos.y + body.vy * 0.1);
46167
+ ctx.strokeStyle = "#16213e";
46168
+ ctx.lineWidth = 2;
46169
+ ctx.stroke();
46170
+ }
46171
+ }
46172
+ };
46173
+ return interp.startLoop(drawInterpolated);
46174
+ }, [externalBodies !== void 0, interp.startLoop]);
46175
+ React77.useEffect(() => {
46176
+ if (externalBodies !== void 0) return;
45973
46177
  if (!running) return;
45974
46178
  let raf;
45975
46179
  const loop = () => {
@@ -45979,10 +46183,11 @@ function SimulationCanvas({
45979
46183
  };
45980
46184
  raf = requestAnimationFrame(loop);
45981
46185
  return () => cancelAnimationFrame(raf);
45982
- }, [running, step, draw]);
46186
+ }, [running, step, draw, externalBodies]);
45983
46187
  React77.useEffect(() => {
46188
+ if (externalBodies !== void 0) return;
45984
46189
  draw();
45985
- }, [draw]);
46190
+ }, [draw, externalBodies]);
45986
46191
  React77.useEffect(() => {
45987
46192
  if (typeof window === "undefined") return;
45988
46193
  const canvas = canvasRef.current;
@@ -46008,6 +46213,7 @@ var init_SimulationCanvas = __esm({
46008
46213
  init_atoms2();
46009
46214
  init_verificationRegistry();
46010
46215
  init_presets();
46216
+ init_useRenderInterpolation();
46011
46217
  SimulationCanvas.displayName = "SimulationCanvas";
46012
46218
  }
46013
46219
  });