@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.
@@ -28789,6 +28789,78 @@ var init_GameOverScreen = __esm({
28789
28789
  GameOverScreen.displayName = "GameOverScreen";
28790
28790
  }
28791
28791
  });
28792
+ function useRenderInterpolation(options = {}) {
28793
+ const tickIntervalMs = options.tickIntervalMs ?? 1e3 / 30;
28794
+ const prevRef = useRef(null);
28795
+ const currRef = useRef(null);
28796
+ const rafRef = useRef(null);
28797
+ const onSnapshot = useCallback((entities) => {
28798
+ prevRef.current = currRef.current;
28799
+ currRef.current = { entities, arrivedAt: performance.now() };
28800
+ }, []);
28801
+ const getInterpolated = useCallback((now) => {
28802
+ const out = /* @__PURE__ */ new Map();
28803
+ const curr = currRef.current;
28804
+ if (!curr) return out;
28805
+ const prev = prevRef.current;
28806
+ if (!prev) {
28807
+ for (const e of curr.entities) out.set(e.id, { x: e.x, y: e.y });
28808
+ return out;
28809
+ }
28810
+ const rawAlpha = (now - curr.arrivedAt) / tickIntervalMs;
28811
+ const alpha = rawAlpha < 0 ? 0 : rawAlpha > 1 ? 1 : rawAlpha;
28812
+ const prevMap = /* @__PURE__ */ new Map();
28813
+ for (const e of prev.entities) prevMap.set(e.id, e);
28814
+ for (const c of curr.entities) {
28815
+ const p2 = prevMap.get(c.id);
28816
+ if (!p2) {
28817
+ out.set(c.id, { x: c.x, y: c.y });
28818
+ } else {
28819
+ out.set(c.id, {
28820
+ x: p2.x + (c.x - p2.x) * alpha,
28821
+ y: p2.y + (c.y - p2.y) * alpha
28822
+ });
28823
+ }
28824
+ }
28825
+ return out;
28826
+ }, [tickIntervalMs]);
28827
+ const startLoop = useCallback(
28828
+ (draw) => {
28829
+ let active = true;
28830
+ const loop = () => {
28831
+ if (!active) return;
28832
+ try {
28833
+ draw(getInterpolated(performance.now()));
28834
+ } catch {
28835
+ }
28836
+ rafRef.current = requestAnimationFrame(loop);
28837
+ };
28838
+ rafRef.current = requestAnimationFrame(loop);
28839
+ return () => {
28840
+ active = false;
28841
+ if (rafRef.current !== null) {
28842
+ cancelAnimationFrame(rafRef.current);
28843
+ rafRef.current = null;
28844
+ }
28845
+ };
28846
+ },
28847
+ [getInterpolated]
28848
+ );
28849
+ useEffect(() => {
28850
+ return () => {
28851
+ if (rafRef.current !== null) {
28852
+ cancelAnimationFrame(rafRef.current);
28853
+ rafRef.current = null;
28854
+ }
28855
+ };
28856
+ }, []);
28857
+ return { onSnapshot, getInterpolated, startLoop };
28858
+ }
28859
+ var init_useRenderInterpolation = __esm({
28860
+ "hooks/useRenderInterpolation.ts"() {
28861
+ "use client";
28862
+ }
28863
+ });
28792
28864
  function PlatformerCanvas({
28793
28865
  player,
28794
28866
  platforms = [
@@ -28857,8 +28929,43 @@ function PlatformerCanvas({
28857
28929
  y: 336,
28858
28930
  width: 32,
28859
28931
  height: 48,
28932
+ vx: 0,
28933
+ vy: 0,
28934
+ grounded: true,
28860
28935
  facingRight: true
28861
28936
  };
28937
+ const playerRef = useRef(resolvedPlayer);
28938
+ playerRef.current = resolvedPlayer;
28939
+ const interp = useRenderInterpolation();
28940
+ useEffect(() => {
28941
+ interp.onSnapshot([{ id: "player", x: resolvedPlayer.x, y: resolvedPlayer.y }]);
28942
+ }, [resolvedPlayer.x, resolvedPlayer.y]);
28943
+ const propsRef = useRef({
28944
+ platforms,
28945
+ worldWidth,
28946
+ worldHeight,
28947
+ canvasWidth,
28948
+ canvasHeight,
28949
+ followCamera,
28950
+ bgColor,
28951
+ playerSprite,
28952
+ tileSprites,
28953
+ backgroundImage,
28954
+ assetBaseUrl
28955
+ });
28956
+ propsRef.current = {
28957
+ platforms,
28958
+ worldWidth,
28959
+ worldHeight,
28960
+ canvasWidth,
28961
+ canvasHeight,
28962
+ followCamera,
28963
+ bgColor,
28964
+ playerSprite,
28965
+ tileSprites,
28966
+ backgroundImage,
28967
+ assetBaseUrl
28968
+ };
28862
28969
  const handleKeyDown = useCallback((e) => {
28863
28970
  if (keysRef.current.has(e.code)) return;
28864
28971
  keysRef.current.add(e.code);
@@ -28907,124 +29014,149 @@ function PlatformerCanvas({
28907
29014
  canvas.width = canvasWidth * dpr;
28908
29015
  canvas.height = canvasHeight * dpr;
28909
29016
  ctx.scale(dpr, dpr);
28910
- let camX = 0;
28911
- let camY = 0;
28912
- if (followCamera) {
28913
- camX = Math.max(0, Math.min(resolvedPlayer.x - canvasWidth / 2, worldWidth - canvasWidth));
28914
- camY = Math.max(0, Math.min(resolvedPlayer.y - canvasHeight / 2 - 50, worldHeight - canvasHeight));
28915
- }
28916
- const bgImg = backgroundImage ? loadImage(backgroundImage) : null;
28917
- if (bgImg) {
28918
- ctx.drawImage(bgImg, 0, 0, canvasWidth, canvasHeight);
28919
- } else if (bgColor) {
28920
- ctx.fillStyle = bgColor;
28921
- ctx.fillRect(0, 0, canvasWidth, canvasHeight);
28922
- } else {
28923
- const grad = ctx.createLinearGradient(0, 0, 0, canvasHeight);
28924
- grad.addColorStop(0, SKY_GRADIENT_TOP);
28925
- grad.addColorStop(1, SKY_GRADIENT_BOTTOM);
28926
- ctx.fillStyle = grad;
28927
- ctx.fillRect(0, 0, canvasWidth, canvasHeight);
28928
- }
28929
- ctx.strokeStyle = GRID_COLOR;
28930
- ctx.lineWidth = 1;
28931
- const gridSize = 32;
28932
- for (let gx = -camX % gridSize; gx < canvasWidth; gx += gridSize) {
28933
- ctx.beginPath();
28934
- ctx.moveTo(gx, 0);
28935
- ctx.lineTo(gx, canvasHeight);
28936
- ctx.stroke();
28937
- }
28938
- for (let gy = -camY % gridSize; gy < canvasHeight; gy += gridSize) {
28939
- ctx.beginPath();
28940
- ctx.moveTo(0, gy);
28941
- ctx.lineTo(canvasWidth, gy);
28942
- ctx.stroke();
28943
- }
28944
- for (const plat of platforms) {
28945
- const px = plat.x - camX;
28946
- const py = plat.y - camY;
28947
- const platType = plat.type ?? "ground";
28948
- const spriteUrl = tileSprites?.[platType];
28949
- const tileImg = spriteUrl ? loadImage(spriteUrl) : null;
28950
- if (tileImg) {
28951
- const tileW = tileImg.naturalWidth;
28952
- const tileH = tileImg.naturalHeight;
28953
- const scaleH = plat.height / tileH;
28954
- const scaledW = tileW * scaleH;
28955
- for (let tx = 0; tx < plat.width; tx += scaledW) {
28956
- const drawW = Math.min(scaledW, plat.width - tx);
28957
- const srcW = drawW / scaleH;
28958
- ctx.drawImage(tileImg, 0, 0, srcW, tileH, px + tx, py, drawW, plat.height);
28959
- }
29017
+ }, [canvasWidth, canvasHeight]);
29018
+ useEffect(() => {
29019
+ const drawFrame = (positions) => {
29020
+ const canvas = canvasRef.current;
29021
+ if (!canvas) return;
29022
+ const ctx = canvas.getContext("2d");
29023
+ if (!ctx) return;
29024
+ const {
29025
+ platforms: plats,
29026
+ worldWidth: ww,
29027
+ worldHeight: wh,
29028
+ canvasWidth: cw,
29029
+ canvasHeight: ch,
29030
+ followCamera: fc,
29031
+ bgColor: bg,
29032
+ playerSprite: pSprite,
29033
+ tileSprites: tSprites,
29034
+ backgroundImage: bgImg
29035
+ } = propsRef.current;
29036
+ const auth = playerRef.current;
29037
+ const interped = positions.get("player");
29038
+ const px = interped?.x ?? auth.x;
29039
+ const py = interped?.y ?? auth.y;
29040
+ let camX = 0;
29041
+ let camY = 0;
29042
+ if (fc) {
29043
+ camX = Math.max(0, Math.min(px - cw / 2, ww - cw));
29044
+ camY = Math.max(0, Math.min(py - ch / 2 - 50, wh - ch));
29045
+ }
29046
+ const bgImage = bgImg ? loadImage(bgImg) : null;
29047
+ if (bgImage) {
29048
+ ctx.drawImage(bgImage, 0, 0, cw, ch);
29049
+ } else if (bg) {
29050
+ ctx.fillStyle = bg;
29051
+ ctx.fillRect(0, 0, cw, ch);
28960
29052
  } else {
28961
- const color = PLATFORM_COLORS[platType] ?? PLATFORM_COLORS.ground;
28962
- ctx.fillStyle = color;
28963
- ctx.fillRect(px, py, plat.width, plat.height);
28964
- ctx.fillStyle = "rgba(255, 255, 255, 0.15)";
28965
- ctx.fillRect(px, py, plat.width, 3);
28966
- ctx.fillStyle = "rgba(0, 0, 0, 0.3)";
28967
- ctx.fillRect(px, py + plat.height - 2, plat.width, 2);
28968
- if (platType === "hazard") {
28969
- ctx.strokeStyle = "#e74c3c";
28970
- ctx.lineWidth = 2;
28971
- for (let sx = px; sx < px + plat.width; sx += 12) {
29053
+ const grad = ctx.createLinearGradient(0, 0, 0, ch);
29054
+ grad.addColorStop(0, SKY_GRADIENT_TOP);
29055
+ grad.addColorStop(1, SKY_GRADIENT_BOTTOM);
29056
+ ctx.fillStyle = grad;
29057
+ ctx.fillRect(0, 0, cw, ch);
29058
+ }
29059
+ ctx.strokeStyle = GRID_COLOR;
29060
+ ctx.lineWidth = 1;
29061
+ const gridSize = 32;
29062
+ for (let gx = -camX % gridSize; gx < cw; gx += gridSize) {
29063
+ ctx.beginPath();
29064
+ ctx.moveTo(gx, 0);
29065
+ ctx.lineTo(gx, ch);
29066
+ ctx.stroke();
29067
+ }
29068
+ for (let gy = -camY % gridSize; gy < ch; gy += gridSize) {
29069
+ ctx.beginPath();
29070
+ ctx.moveTo(0, gy);
29071
+ ctx.lineTo(cw, gy);
29072
+ ctx.stroke();
29073
+ }
29074
+ for (const plat of plats) {
29075
+ const platX = plat.x - camX;
29076
+ const platY = plat.y - camY;
29077
+ const platType = plat.type ?? "ground";
29078
+ const spriteUrl = tSprites?.[platType];
29079
+ const tileImg = spriteUrl ? loadImage(spriteUrl) : null;
29080
+ if (tileImg) {
29081
+ const tileW = tileImg.naturalWidth;
29082
+ const tileH = tileImg.naturalHeight;
29083
+ const scaleH = plat.height / tileH;
29084
+ const scaledW = tileW * scaleH;
29085
+ for (let tx = 0; tx < plat.width; tx += scaledW) {
29086
+ const drawW = Math.min(scaledW, plat.width - tx);
29087
+ const srcW = drawW / scaleH;
29088
+ ctx.drawImage(tileImg, 0, 0, srcW, tileH, platX + tx, platY, drawW, plat.height);
29089
+ }
29090
+ } else {
29091
+ const color = PLATFORM_COLORS[platType] ?? PLATFORM_COLORS.ground;
29092
+ ctx.fillStyle = color;
29093
+ ctx.fillRect(platX, platY, plat.width, plat.height);
29094
+ ctx.fillStyle = "rgba(255, 255, 255, 0.15)";
29095
+ ctx.fillRect(platX, platY, plat.width, 3);
29096
+ ctx.fillStyle = "rgba(0, 0, 0, 0.3)";
29097
+ ctx.fillRect(platX, platY + plat.height - 2, plat.width, 2);
29098
+ if (platType === "hazard") {
29099
+ ctx.strokeStyle = "#e74c3c";
29100
+ ctx.lineWidth = 2;
29101
+ for (let sx = platX; sx < platX + plat.width; sx += 12) {
29102
+ ctx.beginPath();
29103
+ ctx.moveTo(sx, platY);
29104
+ ctx.lineTo(sx + 6, platY + plat.height);
29105
+ ctx.stroke();
29106
+ }
29107
+ }
29108
+ if (platType === "goal") {
29109
+ ctx.fillStyle = "rgba(241, 196, 15, 0.5)";
28972
29110
  ctx.beginPath();
28973
- ctx.moveTo(sx, py);
28974
- ctx.lineTo(sx + 6, py + plat.height);
28975
- ctx.stroke();
29111
+ ctx.arc(platX + plat.width / 2, platY - 10, 8, 0, Math.PI * 2);
29112
+ ctx.fill();
28976
29113
  }
28977
29114
  }
28978
- if (platType === "goal") {
28979
- ctx.fillStyle = "rgba(241, 196, 15, 0.5)";
28980
- ctx.beginPath();
28981
- ctx.arc(px + plat.width / 2, py - 10, 8, 0, Math.PI * 2);
28982
- ctx.fill();
28983
- }
28984
29115
  }
28985
- }
28986
- const pw = resolvedPlayer.width ?? 24;
28987
- const ph = resolvedPlayer.height ?? 32;
28988
- const ppx = resolvedPlayer.x - camX;
28989
- const ppy = resolvedPlayer.y - camY;
28990
- const facingRight = resolvedPlayer.facingRight ?? true;
28991
- const playerImg = playerSprite ? loadImage(playerSprite) : null;
28992
- if (playerImg) {
28993
- ctx.save();
28994
- if (!facingRight) {
28995
- ctx.translate(ppx + pw, ppy);
28996
- ctx.scale(-1, 1);
28997
- ctx.drawImage(playerImg, 0, 0, pw, ph);
29116
+ const pw = auth.width ?? 24;
29117
+ const ph = auth.height ?? 32;
29118
+ const ppx = px - camX;
29119
+ const ppy = py - camY;
29120
+ const facingRight = auth.facingRight ?? true;
29121
+ const playerImg = pSprite ? loadImage(pSprite) : null;
29122
+ if (playerImg) {
29123
+ ctx.save();
29124
+ if (!facingRight) {
29125
+ ctx.translate(ppx + pw, ppy);
29126
+ ctx.scale(-1, 1);
29127
+ ctx.drawImage(playerImg, 0, 0, pw, ph);
29128
+ } else {
29129
+ ctx.drawImage(playerImg, ppx, ppy, pw, ph);
29130
+ }
29131
+ ctx.restore();
28998
29132
  } else {
28999
- ctx.drawImage(playerImg, ppx, ppy, pw, ph);
29133
+ ctx.fillStyle = PLAYER_COLOR;
29134
+ const radius = Math.min(pw, ph) * 0.25;
29135
+ ctx.beginPath();
29136
+ ctx.moveTo(ppx + radius, ppy);
29137
+ ctx.lineTo(ppx + pw - radius, ppy);
29138
+ ctx.quadraticCurveTo(ppx + pw, ppy, ppx + pw, ppy + radius);
29139
+ ctx.lineTo(ppx + pw, ppy + ph - radius);
29140
+ ctx.quadraticCurveTo(ppx + pw, ppy + ph, ppx + pw - radius, ppy + ph);
29141
+ ctx.lineTo(ppx + radius, ppy + ph);
29142
+ ctx.quadraticCurveTo(ppx, ppy + ph, ppx, ppy + ph - radius);
29143
+ ctx.lineTo(ppx, ppy + radius);
29144
+ ctx.quadraticCurveTo(ppx, ppy, ppx + radius, ppy);
29145
+ ctx.fill();
29146
+ const eyeY = ppy + ph * 0.3;
29147
+ const eyeSize = 3;
29148
+ const eyeOffsetX = facingRight ? pw * 0.55 : pw * 0.2;
29149
+ ctx.fillStyle = PLAYER_EYE_COLOR;
29150
+ ctx.beginPath();
29151
+ ctx.arc(ppx + eyeOffsetX, eyeY, eyeSize, 0, Math.PI * 2);
29152
+ ctx.fill();
29153
+ ctx.beginPath();
29154
+ ctx.arc(ppx + eyeOffsetX + 7, eyeY, eyeSize, 0, Math.PI * 2);
29155
+ ctx.fill();
29000
29156
  }
29001
- ctx.restore();
29002
- } else {
29003
- ctx.fillStyle = PLAYER_COLOR;
29004
- const radius = Math.min(pw, ph) * 0.25;
29005
- ctx.beginPath();
29006
- ctx.moveTo(ppx + radius, ppy);
29007
- ctx.lineTo(ppx + pw - radius, ppy);
29008
- ctx.quadraticCurveTo(ppx + pw, ppy, ppx + pw, ppy + radius);
29009
- ctx.lineTo(ppx + pw, ppy + ph - radius);
29010
- ctx.quadraticCurveTo(ppx + pw, ppy + ph, ppx + pw - radius, ppy + ph);
29011
- ctx.lineTo(ppx + radius, ppy + ph);
29012
- ctx.quadraticCurveTo(ppx, ppy + ph, ppx, ppy + ph - radius);
29013
- ctx.lineTo(ppx, ppy + radius);
29014
- ctx.quadraticCurveTo(ppx, ppy, ppx + radius, ppy);
29015
- ctx.fill();
29016
- const eyeY = ppy + ph * 0.3;
29017
- const eyeSize = 3;
29018
- const eyeOffsetX = facingRight ? pw * 0.55 : pw * 0.2;
29019
- ctx.fillStyle = PLAYER_EYE_COLOR;
29020
- ctx.beginPath();
29021
- ctx.arc(ppx + eyeOffsetX, eyeY, eyeSize, 0, Math.PI * 2);
29022
- ctx.fill();
29023
- ctx.beginPath();
29024
- ctx.arc(ppx + eyeOffsetX + 7, eyeY, eyeSize, 0, Math.PI * 2);
29025
- ctx.fill();
29026
- }
29027
- }, [player, platforms, worldWidth, worldHeight, canvasWidth, canvasHeight, followCamera, bgColor, playerSprite, tileSprites, backgroundImage, assetBaseUrl, loadedImages]);
29157
+ };
29158
+ return interp.startLoop(drawFrame);
29159
+ }, [interp.startLoop, loadImage]);
29028
29160
  return /* @__PURE__ */ jsx(
29029
29161
  "canvas",
29030
29162
  {
@@ -29042,6 +29174,7 @@ var init_PlatformerCanvas = __esm({
29042
29174
  init_cn();
29043
29175
  init_useEventBus();
29044
29176
  init_verificationRegistry();
29177
+ init_useRenderInterpolation();
29045
29178
  PLATFORM_COLORS = {
29046
29179
  ground: "#4a7c59",
29047
29180
  platform: "#7c6b4a",
@@ -29446,13 +29579,13 @@ var init_MapView = __esm({
29446
29579
  shadowSize: [41, 41]
29447
29580
  });
29448
29581
  L.Marker.prototype.options.icon = defaultIcon;
29449
- const { useEffect: useEffect73, useRef: useRef70, useCallback: useCallback113, useState: useState105 } = React77__default;
29582
+ const { useEffect: useEffect74, useRef: useRef71, useCallback: useCallback114, useState: useState105 } = React77__default;
29450
29583
  const { Typography: Typography2 } = await Promise.resolve().then(() => (init_Typography(), Typography_exports));
29451
29584
  const { useEventBus: useEventBus2 } = await Promise.resolve().then(() => (init_useEventBus(), useEventBus_exports));
29452
29585
  function MapUpdater({ centerLat, centerLng, zoom }) {
29453
29586
  const map = useMap();
29454
- const prevRef = useRef70({ centerLat, centerLng, zoom });
29455
- useEffect73(() => {
29587
+ const prevRef = useRef71({ centerLat, centerLng, zoom });
29588
+ useEffect74(() => {
29456
29589
  const prev = prevRef.current;
29457
29590
  if (prev.centerLat !== centerLat || prev.centerLng !== centerLng || prev.zoom !== zoom) {
29458
29591
  map.setView([centerLat, centerLng], zoom);
@@ -29463,7 +29596,7 @@ var init_MapView = __esm({
29463
29596
  }
29464
29597
  function MapClickHandler({ onMapClick }) {
29465
29598
  const map = useMap();
29466
- useEffect73(() => {
29599
+ useEffect74(() => {
29467
29600
  if (!onMapClick) return;
29468
29601
  const handler = (e) => {
29469
29602
  onMapClick(e.latlng.lat, e.latlng.lng);
@@ -29492,7 +29625,7 @@ var init_MapView = __esm({
29492
29625
  }) {
29493
29626
  const eventBus = useEventBus2();
29494
29627
  const [clickedPosition, setClickedPosition] = useState105(null);
29495
- const handleMapClick = useCallback113((lat, lng) => {
29628
+ const handleMapClick = useCallback114((lat, lng) => {
29496
29629
  if (showClickedPin) {
29497
29630
  setClickedPosition({ lat, lng });
29498
29631
  }
@@ -29501,7 +29634,7 @@ var init_MapView = __esm({
29501
29634
  eventBus.emit(`UI:${mapClickEvent}`, { latitude: lat, longitude: lng });
29502
29635
  }
29503
29636
  }, [onMapClick, mapClickEvent, eventBus, showClickedPin]);
29504
- const handleMarkerClick = useCallback113((marker) => {
29637
+ const handleMarkerClick = useCallback114((marker) => {
29505
29638
  onMarkerClick?.(marker);
29506
29639
  if (markerClickEvent) {
29507
29640
  eventBus.emit(`UI:${markerClickEvent}`, { ...marker });
@@ -45463,6 +45596,10 @@ function SequencerBoard({
45463
45596
  stepDurationMs = 1e3,
45464
45597
  playEvent,
45465
45598
  completeEvent,
45599
+ placeEvent,
45600
+ removeEvent,
45601
+ checkEvent,
45602
+ playAgainEvent,
45466
45603
  className
45467
45604
  }) {
45468
45605
  const { emit } = useEventBus();
@@ -45498,7 +45635,8 @@ function SequencerBoard({
45498
45635
  return next;
45499
45636
  });
45500
45637
  emit("UI:PLAY_SOUND", { key: "drop_slot" });
45501
- }, [emit]);
45638
+ if (placeEvent) emit(`UI:${placeEvent}`, { slotIndex: index, actionId: item.id });
45639
+ }, [emit, placeEvent]);
45502
45640
  const handleSlotRemove = useCallback((index) => {
45503
45641
  setSlots((prev) => {
45504
45642
  const next = [...prev];
@@ -45511,7 +45649,8 @@ function SequencerBoard({
45511
45649
  return next;
45512
45650
  });
45513
45651
  emit("UI:PLAY_SOUND", { key: "back" });
45514
- }, [emit]);
45652
+ if (removeEvent) emit(`UI:${removeEvent}`, { slotIndex: index });
45653
+ }, [emit, removeEvent]);
45515
45654
  const handleReset = useCallback(() => {
45516
45655
  if (timerRef.current) clearTimeout(timerRef.current);
45517
45656
  setSlots(Array.from({ length: maxSlots }, () => void 0));
@@ -45519,7 +45658,8 @@ function SequencerBoard({
45519
45658
  setCurrentStep(-1);
45520
45659
  setAttempts(0);
45521
45660
  setSlotFeedback(Array.from({ length: maxSlots }, () => null));
45522
- }, [maxSlots]);
45661
+ if (playAgainEvent) emit(`UI:${playAgainEvent}`, {});
45662
+ }, [maxSlots, playAgainEvent, emit]);
45523
45663
  const filledSlots = slots.filter((s) => !!s);
45524
45664
  const canPlay = filledSlots.length > 0 && playState === "idle";
45525
45665
  const handlePlay = useCallback(() => {
@@ -45541,6 +45681,7 @@ function SequencerBoard({
45541
45681
  const success = solutions.some(
45542
45682
  (sol) => sol.length === playerIds.length && sol.every((id, i) => id === playerIds[i])
45543
45683
  );
45684
+ if (checkEvent) emit(`UI:${checkEvent}`, { sequence: playerIds });
45544
45685
  if (success) {
45545
45686
  setPlayState("success");
45546
45687
  setCurrentStep(-1);
@@ -45568,7 +45709,7 @@ function SequencerBoard({
45568
45709
  }
45569
45710
  };
45570
45711
  timerRef.current = setTimeout(advance, stepDurationMs);
45571
- }, [canPlay, slots, maxSlots, solutions, stepDurationMs, playEvent, completeEvent, emit]);
45712
+ }, [canPlay, slots, maxSlots, solutions, stepDurationMs, playEvent, completeEvent, checkEvent, emit]);
45572
45713
  const machine = {
45573
45714
  name: str(resolved?.title),
45574
45715
  description: str(resolved?.description),
@@ -45823,11 +45964,13 @@ function SimulationCanvas({
45823
45964
  height = 400,
45824
45965
  running,
45825
45966
  speed = 1,
45967
+ bodies: externalBodies,
45826
45968
  className
45827
45969
  }) {
45828
45970
  const preset = useMemo(() => resolvePreset(presetProp), [presetProp]);
45829
45971
  const canvasRef = useRef(null);
45830
45972
  const bodiesRef = useRef(structuredClone(preset.bodies));
45973
+ const interp = useRenderInterpolation();
45831
45974
  useEffect(() => {
45832
45975
  bodiesRef.current = structuredClone(preset.bodies);
45833
45976
  }, [preset]);
@@ -45925,6 +46068,67 @@ function SimulationCanvas({
45925
46068
  }
45926
46069
  }, [width, height, preset]);
45927
46070
  useEffect(() => {
46071
+ if (!externalBodies) return;
46072
+ interp.onSnapshot(externalBodies.map((b) => ({ id: b.id, x: b.x, y: b.y })));
46073
+ }, [externalBodies]);
46074
+ const presetRef = useRef(preset);
46075
+ presetRef.current = preset;
46076
+ const widthRef = useRef(width);
46077
+ widthRef.current = width;
46078
+ const heightRef = useRef(height);
46079
+ heightRef.current = height;
46080
+ useEffect(() => {
46081
+ if (!externalBodies) return;
46082
+ const drawInterpolated = (positions) => {
46083
+ const canvas = canvasRef.current;
46084
+ if (!canvas) return;
46085
+ const ctx = canvas.getContext("2d");
46086
+ if (!ctx) return;
46087
+ const bodies = bodiesRef.current;
46088
+ const p2 = presetRef.current;
46089
+ const w = widthRef.current;
46090
+ const h = heightRef.current;
46091
+ ctx.clearRect(0, 0, w, h);
46092
+ ctx.fillStyle = p2.backgroundColor ?? "#1a1a2e";
46093
+ ctx.fillRect(0, 0, w, h);
46094
+ if (p2.constraints) {
46095
+ for (const c of p2.constraints) {
46096
+ const a = bodies[c.bodyA];
46097
+ const b = bodies[c.bodyB];
46098
+ if (a && b) {
46099
+ const aPos = positions.get(a.id) ?? a;
46100
+ const bPos = positions.get(b.id) ?? b;
46101
+ ctx.beginPath();
46102
+ ctx.moveTo(aPos.x, aPos.y);
46103
+ ctx.lineTo(bPos.x, bPos.y);
46104
+ ctx.strokeStyle = "#533483";
46105
+ ctx.lineWidth = 1;
46106
+ ctx.setLineDash([4, 4]);
46107
+ ctx.stroke();
46108
+ ctx.setLineDash([]);
46109
+ }
46110
+ }
46111
+ }
46112
+ for (const body of bodies) {
46113
+ const pos = positions.get(body.id) ?? body;
46114
+ ctx.beginPath();
46115
+ ctx.arc(pos.x, pos.y, body.radius, 0, Math.PI * 2);
46116
+ ctx.fillStyle = body.color ?? "#e94560";
46117
+ ctx.fill();
46118
+ if (p2.showVelocity) {
46119
+ ctx.beginPath();
46120
+ ctx.moveTo(pos.x, pos.y);
46121
+ ctx.lineTo(pos.x + body.vx * 0.1, pos.y + body.vy * 0.1);
46122
+ ctx.strokeStyle = "#16213e";
46123
+ ctx.lineWidth = 2;
46124
+ ctx.stroke();
46125
+ }
46126
+ }
46127
+ };
46128
+ return interp.startLoop(drawInterpolated);
46129
+ }, [externalBodies !== void 0, interp.startLoop]);
46130
+ useEffect(() => {
46131
+ if (externalBodies !== void 0) return;
45928
46132
  if (!running) return;
45929
46133
  let raf;
45930
46134
  const loop = () => {
@@ -45934,10 +46138,11 @@ function SimulationCanvas({
45934
46138
  };
45935
46139
  raf = requestAnimationFrame(loop);
45936
46140
  return () => cancelAnimationFrame(raf);
45937
- }, [running, step, draw]);
46141
+ }, [running, step, draw, externalBodies]);
45938
46142
  useEffect(() => {
46143
+ if (externalBodies !== void 0) return;
45939
46144
  draw();
45940
- }, [draw]);
46145
+ }, [draw, externalBodies]);
45941
46146
  useEffect(() => {
45942
46147
  if (typeof window === "undefined") return;
45943
46148
  const canvas = canvasRef.current;
@@ -45963,6 +46168,7 @@ var init_SimulationCanvas = __esm({
45963
46168
  init_atoms2();
45964
46169
  init_verificationRegistry();
45965
46170
  init_presets();
46171
+ init_useRenderInterpolation();
45966
46172
  SimulationCanvas.displayName = "SimulationCanvas";
45967
46173
  }
45968
46174
  });
@@ -2504,6 +2504,73 @@ function useDropZone({ accepts, onDrop, disabled = false }) {
2504
2504
  );
2505
2505
  return { dropProps, isOver };
2506
2506
  }
2507
+ function useRenderInterpolation(options = {}) {
2508
+ const tickIntervalMs = options.tickIntervalMs ?? 1e3 / 30;
2509
+ const prevRef = react.useRef(null);
2510
+ const currRef = react.useRef(null);
2511
+ const rafRef = react.useRef(null);
2512
+ const onSnapshot = react.useCallback((entities2) => {
2513
+ prevRef.current = currRef.current;
2514
+ currRef.current = { entities: entities2, arrivedAt: performance.now() };
2515
+ }, []);
2516
+ const getInterpolated = react.useCallback((now) => {
2517
+ const out = /* @__PURE__ */ new Map();
2518
+ const curr = currRef.current;
2519
+ if (!curr) return out;
2520
+ const prev = prevRef.current;
2521
+ if (!prev) {
2522
+ for (const e of curr.entities) out.set(e.id, { x: e.x, y: e.y });
2523
+ return out;
2524
+ }
2525
+ const rawAlpha = (now - curr.arrivedAt) / tickIntervalMs;
2526
+ const alpha = rawAlpha < 0 ? 0 : rawAlpha > 1 ? 1 : rawAlpha;
2527
+ const prevMap = /* @__PURE__ */ new Map();
2528
+ for (const e of prev.entities) prevMap.set(e.id, e);
2529
+ for (const c of curr.entities) {
2530
+ const p = prevMap.get(c.id);
2531
+ if (!p) {
2532
+ out.set(c.id, { x: c.x, y: c.y });
2533
+ } else {
2534
+ out.set(c.id, {
2535
+ x: p.x + (c.x - p.x) * alpha,
2536
+ y: p.y + (c.y - p.y) * alpha
2537
+ });
2538
+ }
2539
+ }
2540
+ return out;
2541
+ }, [tickIntervalMs]);
2542
+ const startLoop = react.useCallback(
2543
+ (draw) => {
2544
+ let active = true;
2545
+ const loop = () => {
2546
+ if (!active) return;
2547
+ try {
2548
+ draw(getInterpolated(performance.now()));
2549
+ } catch {
2550
+ }
2551
+ rafRef.current = requestAnimationFrame(loop);
2552
+ };
2553
+ rafRef.current = requestAnimationFrame(loop);
2554
+ return () => {
2555
+ active = false;
2556
+ if (rafRef.current !== null) {
2557
+ cancelAnimationFrame(rafRef.current);
2558
+ rafRef.current = null;
2559
+ }
2560
+ };
2561
+ },
2562
+ [getInterpolated]
2563
+ );
2564
+ react.useEffect(() => {
2565
+ return () => {
2566
+ if (rafRef.current !== null) {
2567
+ cancelAnimationFrame(rafRef.current);
2568
+ rafRef.current = null;
2569
+ }
2570
+ };
2571
+ }, []);
2572
+ return { onSnapshot, getInterpolated, startLoop };
2573
+ }
2507
2574
  var API_BASE = typeof process !== "undefined" && process.env?.VITE_API_URL ? process.env.VITE_API_URL : "http://localhost:3000";
2508
2575
  function getUserId() {
2509
2576
  return localStorage.getItem("userId") || "anonymous";
@@ -2626,6 +2693,7 @@ exports.usePlayer = usePlayer;
2626
2693
  exports.usePreview = usePreview;
2627
2694
  exports.usePullToRefresh = usePullToRefresh;
2628
2695
  exports.useQuerySingleton = useQuerySingleton;
2696
+ exports.useRenderInterpolation = useRenderInterpolation;
2629
2697
  exports.useSingletonEntity = useSingletonEntity;
2630
2698
  exports.useSwipeGesture = useSwipeGesture;
2631
2699
  exports.useTraitListens = useTraitListens;
@@ -26,4 +26,5 @@ export { usePullToRefresh, type PullToRefreshOptions, type PullToRefreshResult,
26
26
  export { usePinchZoom, type PinchZoomOptions, type PinchZoomResult, } from './usePinchZoom';
27
27
  export { useDraggable, ALMADAR_DND_MIME, type DraggablePayload, type UseDraggableOptions, type UseDraggableResult, } from './useDraggable';
28
28
  export { useDropZone, type UseDropZoneOptions, type UseDropZoneResult, } from './useDropZone';
29
+ export { useRenderInterpolation, type Positioned, type RenderInterpolationOptions, type RenderInterpolationHandle, } from './useRenderInterpolation';
29
30
  export { useGitHubStatus, useConnectGitHub, useDisconnectGitHub, useGitHubRepos, useGitHubRepo, useGitHubBranches, type GitHubStatus, type GitHubRepo, } from './useGitHub';