@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.
@@ -31086,6 +31086,78 @@ var init_GameOverScreen = __esm({
31086
31086
  GameOverScreen.displayName = "GameOverScreen";
31087
31087
  }
31088
31088
  });
31089
+ function useRenderInterpolation(options = {}) {
31090
+ const tickIntervalMs = options.tickIntervalMs ?? 1e3 / 30;
31091
+ const prevRef = React91.useRef(null);
31092
+ const currRef = React91.useRef(null);
31093
+ const rafRef = React91.useRef(null);
31094
+ const onSnapshot = React91.useCallback((entities) => {
31095
+ prevRef.current = currRef.current;
31096
+ currRef.current = { entities, arrivedAt: performance.now() };
31097
+ }, []);
31098
+ const getInterpolated = React91.useCallback((now2) => {
31099
+ const out = /* @__PURE__ */ new Map();
31100
+ const curr = currRef.current;
31101
+ if (!curr) return out;
31102
+ const prev = prevRef.current;
31103
+ if (!prev) {
31104
+ for (const e of curr.entities) out.set(e.id, { x: e.x, y: e.y });
31105
+ return out;
31106
+ }
31107
+ const rawAlpha = (now2 - curr.arrivedAt) / tickIntervalMs;
31108
+ const alpha = rawAlpha < 0 ? 0 : rawAlpha > 1 ? 1 : rawAlpha;
31109
+ const prevMap = /* @__PURE__ */ new Map();
31110
+ for (const e of prev.entities) prevMap.set(e.id, e);
31111
+ for (const c of curr.entities) {
31112
+ const p2 = prevMap.get(c.id);
31113
+ if (!p2) {
31114
+ out.set(c.id, { x: c.x, y: c.y });
31115
+ } else {
31116
+ out.set(c.id, {
31117
+ x: p2.x + (c.x - p2.x) * alpha,
31118
+ y: p2.y + (c.y - p2.y) * alpha
31119
+ });
31120
+ }
31121
+ }
31122
+ return out;
31123
+ }, [tickIntervalMs]);
31124
+ const startLoop = React91.useCallback(
31125
+ (draw) => {
31126
+ let active = true;
31127
+ const loop = () => {
31128
+ if (!active) return;
31129
+ try {
31130
+ draw(getInterpolated(performance.now()));
31131
+ } catch {
31132
+ }
31133
+ rafRef.current = requestAnimationFrame(loop);
31134
+ };
31135
+ rafRef.current = requestAnimationFrame(loop);
31136
+ return () => {
31137
+ active = false;
31138
+ if (rafRef.current !== null) {
31139
+ cancelAnimationFrame(rafRef.current);
31140
+ rafRef.current = null;
31141
+ }
31142
+ };
31143
+ },
31144
+ [getInterpolated]
31145
+ );
31146
+ React91.useEffect(() => {
31147
+ return () => {
31148
+ if (rafRef.current !== null) {
31149
+ cancelAnimationFrame(rafRef.current);
31150
+ rafRef.current = null;
31151
+ }
31152
+ };
31153
+ }, []);
31154
+ return { onSnapshot, getInterpolated, startLoop };
31155
+ }
31156
+ var init_useRenderInterpolation = __esm({
31157
+ "hooks/useRenderInterpolation.ts"() {
31158
+ "use client";
31159
+ }
31160
+ });
31089
31161
  function PlatformerCanvas({
31090
31162
  player,
31091
31163
  platforms = [
@@ -31154,8 +31226,43 @@ function PlatformerCanvas({
31154
31226
  y: 336,
31155
31227
  width: 32,
31156
31228
  height: 48,
31229
+ vx: 0,
31230
+ vy: 0,
31231
+ grounded: true,
31157
31232
  facingRight: true
31158
31233
  };
31234
+ const playerRef = React91.useRef(resolvedPlayer);
31235
+ playerRef.current = resolvedPlayer;
31236
+ const interp = useRenderInterpolation();
31237
+ React91.useEffect(() => {
31238
+ interp.onSnapshot([{ id: "player", x: resolvedPlayer.x, y: resolvedPlayer.y }]);
31239
+ }, [resolvedPlayer.x, resolvedPlayer.y]);
31240
+ const propsRef = React91.useRef({
31241
+ platforms,
31242
+ worldWidth,
31243
+ worldHeight,
31244
+ canvasWidth,
31245
+ canvasHeight,
31246
+ followCamera,
31247
+ bgColor,
31248
+ playerSprite,
31249
+ tileSprites,
31250
+ backgroundImage,
31251
+ assetBaseUrl
31252
+ });
31253
+ propsRef.current = {
31254
+ platforms,
31255
+ worldWidth,
31256
+ worldHeight,
31257
+ canvasWidth,
31258
+ canvasHeight,
31259
+ followCamera,
31260
+ bgColor,
31261
+ playerSprite,
31262
+ tileSprites,
31263
+ backgroundImage,
31264
+ assetBaseUrl
31265
+ };
31159
31266
  const handleKeyDown = React91.useCallback((e) => {
31160
31267
  if (keysRef.current.has(e.code)) return;
31161
31268
  keysRef.current.add(e.code);
@@ -31204,124 +31311,149 @@ function PlatformerCanvas({
31204
31311
  canvas.width = canvasWidth * dpr;
31205
31312
  canvas.height = canvasHeight * dpr;
31206
31313
  ctx.scale(dpr, dpr);
31207
- let camX = 0;
31208
- let camY = 0;
31209
- if (followCamera) {
31210
- camX = Math.max(0, Math.min(resolvedPlayer.x - canvasWidth / 2, worldWidth - canvasWidth));
31211
- camY = Math.max(0, Math.min(resolvedPlayer.y - canvasHeight / 2 - 50, worldHeight - canvasHeight));
31212
- }
31213
- const bgImg = backgroundImage ? loadImage(backgroundImage) : null;
31214
- if (bgImg) {
31215
- ctx.drawImage(bgImg, 0, 0, canvasWidth, canvasHeight);
31216
- } else if (bgColor) {
31217
- ctx.fillStyle = bgColor;
31218
- ctx.fillRect(0, 0, canvasWidth, canvasHeight);
31219
- } else {
31220
- const grad = ctx.createLinearGradient(0, 0, 0, canvasHeight);
31221
- grad.addColorStop(0, SKY_GRADIENT_TOP);
31222
- grad.addColorStop(1, SKY_GRADIENT_BOTTOM);
31223
- ctx.fillStyle = grad;
31224
- ctx.fillRect(0, 0, canvasWidth, canvasHeight);
31225
- }
31226
- ctx.strokeStyle = GRID_COLOR;
31227
- ctx.lineWidth = 1;
31228
- const gridSize = 32;
31229
- for (let gx = -camX % gridSize; gx < canvasWidth; gx += gridSize) {
31230
- ctx.beginPath();
31231
- ctx.moveTo(gx, 0);
31232
- ctx.lineTo(gx, canvasHeight);
31233
- ctx.stroke();
31234
- }
31235
- for (let gy = -camY % gridSize; gy < canvasHeight; gy += gridSize) {
31236
- ctx.beginPath();
31237
- ctx.moveTo(0, gy);
31238
- ctx.lineTo(canvasWidth, gy);
31239
- ctx.stroke();
31240
- }
31241
- for (const plat of platforms) {
31242
- const px = plat.x - camX;
31243
- const py = plat.y - camY;
31244
- const platType = plat.type ?? "ground";
31245
- const spriteUrl = tileSprites?.[platType];
31246
- const tileImg = spriteUrl ? loadImage(spriteUrl) : null;
31247
- if (tileImg) {
31248
- const tileW = tileImg.naturalWidth;
31249
- const tileH = tileImg.naturalHeight;
31250
- const scaleH = plat.height / tileH;
31251
- const scaledW = tileW * scaleH;
31252
- for (let tx = 0; tx < plat.width; tx += scaledW) {
31253
- const drawW = Math.min(scaledW, plat.width - tx);
31254
- const srcW = drawW / scaleH;
31255
- ctx.drawImage(tileImg, 0, 0, srcW, tileH, px + tx, py, drawW, plat.height);
31256
- }
31314
+ }, [canvasWidth, canvasHeight]);
31315
+ React91.useEffect(() => {
31316
+ const drawFrame = (positions) => {
31317
+ const canvas = canvasRef.current;
31318
+ if (!canvas) return;
31319
+ const ctx = canvas.getContext("2d");
31320
+ if (!ctx) return;
31321
+ const {
31322
+ platforms: plats,
31323
+ worldWidth: ww,
31324
+ worldHeight: wh,
31325
+ canvasWidth: cw,
31326
+ canvasHeight: ch,
31327
+ followCamera: fc,
31328
+ bgColor: bg,
31329
+ playerSprite: pSprite,
31330
+ tileSprites: tSprites,
31331
+ backgroundImage: bgImg
31332
+ } = propsRef.current;
31333
+ const auth = playerRef.current;
31334
+ const interped = positions.get("player");
31335
+ const px = interped?.x ?? auth.x;
31336
+ const py = interped?.y ?? auth.y;
31337
+ let camX = 0;
31338
+ let camY = 0;
31339
+ if (fc) {
31340
+ camX = Math.max(0, Math.min(px - cw / 2, ww - cw));
31341
+ camY = Math.max(0, Math.min(py - ch / 2 - 50, wh - ch));
31342
+ }
31343
+ const bgImage = bgImg ? loadImage(bgImg) : null;
31344
+ if (bgImage) {
31345
+ ctx.drawImage(bgImage, 0, 0, cw, ch);
31346
+ } else if (bg) {
31347
+ ctx.fillStyle = bg;
31348
+ ctx.fillRect(0, 0, cw, ch);
31257
31349
  } else {
31258
- const color = PLATFORM_COLORS[platType] ?? PLATFORM_COLORS.ground;
31259
- ctx.fillStyle = color;
31260
- ctx.fillRect(px, py, plat.width, plat.height);
31261
- ctx.fillStyle = "rgba(255, 255, 255, 0.15)";
31262
- ctx.fillRect(px, py, plat.width, 3);
31263
- ctx.fillStyle = "rgba(0, 0, 0, 0.3)";
31264
- ctx.fillRect(px, py + plat.height - 2, plat.width, 2);
31265
- if (platType === "hazard") {
31266
- ctx.strokeStyle = "#e74c3c";
31267
- ctx.lineWidth = 2;
31268
- for (let sx = px; sx < px + plat.width; sx += 12) {
31350
+ const grad = ctx.createLinearGradient(0, 0, 0, ch);
31351
+ grad.addColorStop(0, SKY_GRADIENT_TOP);
31352
+ grad.addColorStop(1, SKY_GRADIENT_BOTTOM);
31353
+ ctx.fillStyle = grad;
31354
+ ctx.fillRect(0, 0, cw, ch);
31355
+ }
31356
+ ctx.strokeStyle = GRID_COLOR;
31357
+ ctx.lineWidth = 1;
31358
+ const gridSize = 32;
31359
+ for (let gx = -camX % gridSize; gx < cw; gx += gridSize) {
31360
+ ctx.beginPath();
31361
+ ctx.moveTo(gx, 0);
31362
+ ctx.lineTo(gx, ch);
31363
+ ctx.stroke();
31364
+ }
31365
+ for (let gy = -camY % gridSize; gy < ch; gy += gridSize) {
31366
+ ctx.beginPath();
31367
+ ctx.moveTo(0, gy);
31368
+ ctx.lineTo(cw, gy);
31369
+ ctx.stroke();
31370
+ }
31371
+ for (const plat of plats) {
31372
+ const platX = plat.x - camX;
31373
+ const platY = plat.y - camY;
31374
+ const platType = plat.type ?? "ground";
31375
+ const spriteUrl = tSprites?.[platType];
31376
+ const tileImg = spriteUrl ? loadImage(spriteUrl) : null;
31377
+ if (tileImg) {
31378
+ const tileW = tileImg.naturalWidth;
31379
+ const tileH = tileImg.naturalHeight;
31380
+ const scaleH = plat.height / tileH;
31381
+ const scaledW = tileW * scaleH;
31382
+ for (let tx = 0; tx < plat.width; tx += scaledW) {
31383
+ const drawW = Math.min(scaledW, plat.width - tx);
31384
+ const srcW = drawW / scaleH;
31385
+ ctx.drawImage(tileImg, 0, 0, srcW, tileH, platX + tx, platY, drawW, plat.height);
31386
+ }
31387
+ } else {
31388
+ const color = PLATFORM_COLORS[platType] ?? PLATFORM_COLORS.ground;
31389
+ ctx.fillStyle = color;
31390
+ ctx.fillRect(platX, platY, plat.width, plat.height);
31391
+ ctx.fillStyle = "rgba(255, 255, 255, 0.15)";
31392
+ ctx.fillRect(platX, platY, plat.width, 3);
31393
+ ctx.fillStyle = "rgba(0, 0, 0, 0.3)";
31394
+ ctx.fillRect(platX, platY + plat.height - 2, plat.width, 2);
31395
+ if (platType === "hazard") {
31396
+ ctx.strokeStyle = "#e74c3c";
31397
+ ctx.lineWidth = 2;
31398
+ for (let sx = platX; sx < platX + plat.width; sx += 12) {
31399
+ ctx.beginPath();
31400
+ ctx.moveTo(sx, platY);
31401
+ ctx.lineTo(sx + 6, platY + plat.height);
31402
+ ctx.stroke();
31403
+ }
31404
+ }
31405
+ if (platType === "goal") {
31406
+ ctx.fillStyle = "rgba(241, 196, 15, 0.5)";
31269
31407
  ctx.beginPath();
31270
- ctx.moveTo(sx, py);
31271
- ctx.lineTo(sx + 6, py + plat.height);
31272
- ctx.stroke();
31408
+ ctx.arc(platX + plat.width / 2, platY - 10, 8, 0, Math.PI * 2);
31409
+ ctx.fill();
31273
31410
  }
31274
31411
  }
31275
- if (platType === "goal") {
31276
- ctx.fillStyle = "rgba(241, 196, 15, 0.5)";
31277
- ctx.beginPath();
31278
- ctx.arc(px + plat.width / 2, py - 10, 8, 0, Math.PI * 2);
31279
- ctx.fill();
31280
- }
31281
31412
  }
31282
- }
31283
- const pw = resolvedPlayer.width ?? 24;
31284
- const ph = resolvedPlayer.height ?? 32;
31285
- const ppx = resolvedPlayer.x - camX;
31286
- const ppy = resolvedPlayer.y - camY;
31287
- const facingRight = resolvedPlayer.facingRight ?? true;
31288
- const playerImg = playerSprite ? loadImage(playerSprite) : null;
31289
- if (playerImg) {
31290
- ctx.save();
31291
- if (!facingRight) {
31292
- ctx.translate(ppx + pw, ppy);
31293
- ctx.scale(-1, 1);
31294
- ctx.drawImage(playerImg, 0, 0, pw, ph);
31413
+ const pw = auth.width ?? 24;
31414
+ const ph = auth.height ?? 32;
31415
+ const ppx = px - camX;
31416
+ const ppy = py - camY;
31417
+ const facingRight = auth.facingRight ?? true;
31418
+ const playerImg = pSprite ? loadImage(pSprite) : null;
31419
+ if (playerImg) {
31420
+ ctx.save();
31421
+ if (!facingRight) {
31422
+ ctx.translate(ppx + pw, ppy);
31423
+ ctx.scale(-1, 1);
31424
+ ctx.drawImage(playerImg, 0, 0, pw, ph);
31425
+ } else {
31426
+ ctx.drawImage(playerImg, ppx, ppy, pw, ph);
31427
+ }
31428
+ ctx.restore();
31295
31429
  } else {
31296
- ctx.drawImage(playerImg, ppx, ppy, pw, ph);
31430
+ ctx.fillStyle = PLAYER_COLOR;
31431
+ const radius = Math.min(pw, ph) * 0.25;
31432
+ ctx.beginPath();
31433
+ ctx.moveTo(ppx + radius, ppy);
31434
+ ctx.lineTo(ppx + pw - radius, ppy);
31435
+ ctx.quadraticCurveTo(ppx + pw, ppy, ppx + pw, ppy + radius);
31436
+ ctx.lineTo(ppx + pw, ppy + ph - radius);
31437
+ ctx.quadraticCurveTo(ppx + pw, ppy + ph, ppx + pw - radius, ppy + ph);
31438
+ ctx.lineTo(ppx + radius, ppy + ph);
31439
+ ctx.quadraticCurveTo(ppx, ppy + ph, ppx, ppy + ph - radius);
31440
+ ctx.lineTo(ppx, ppy + radius);
31441
+ ctx.quadraticCurveTo(ppx, ppy, ppx + radius, ppy);
31442
+ ctx.fill();
31443
+ const eyeY = ppy + ph * 0.3;
31444
+ const eyeSize = 3;
31445
+ const eyeOffsetX = facingRight ? pw * 0.55 : pw * 0.2;
31446
+ ctx.fillStyle = PLAYER_EYE_COLOR;
31447
+ ctx.beginPath();
31448
+ ctx.arc(ppx + eyeOffsetX, eyeY, eyeSize, 0, Math.PI * 2);
31449
+ ctx.fill();
31450
+ ctx.beginPath();
31451
+ ctx.arc(ppx + eyeOffsetX + 7, eyeY, eyeSize, 0, Math.PI * 2);
31452
+ ctx.fill();
31297
31453
  }
31298
- ctx.restore();
31299
- } else {
31300
- ctx.fillStyle = PLAYER_COLOR;
31301
- const radius = Math.min(pw, ph) * 0.25;
31302
- ctx.beginPath();
31303
- ctx.moveTo(ppx + radius, ppy);
31304
- ctx.lineTo(ppx + pw - radius, ppy);
31305
- ctx.quadraticCurveTo(ppx + pw, ppy, ppx + pw, ppy + radius);
31306
- ctx.lineTo(ppx + pw, ppy + ph - radius);
31307
- ctx.quadraticCurveTo(ppx + pw, ppy + ph, ppx + pw - radius, ppy + ph);
31308
- ctx.lineTo(ppx + radius, ppy + ph);
31309
- ctx.quadraticCurveTo(ppx, ppy + ph, ppx, ppy + ph - radius);
31310
- ctx.lineTo(ppx, ppy + radius);
31311
- ctx.quadraticCurveTo(ppx, ppy, ppx + radius, ppy);
31312
- ctx.fill();
31313
- const eyeY = ppy + ph * 0.3;
31314
- const eyeSize = 3;
31315
- const eyeOffsetX = facingRight ? pw * 0.55 : pw * 0.2;
31316
- ctx.fillStyle = PLAYER_EYE_COLOR;
31317
- ctx.beginPath();
31318
- ctx.arc(ppx + eyeOffsetX, eyeY, eyeSize, 0, Math.PI * 2);
31319
- ctx.fill();
31320
- ctx.beginPath();
31321
- ctx.arc(ppx + eyeOffsetX + 7, eyeY, eyeSize, 0, Math.PI * 2);
31322
- ctx.fill();
31323
- }
31324
- }, [player, platforms, worldWidth, worldHeight, canvasWidth, canvasHeight, followCamera, bgColor, playerSprite, tileSprites, backgroundImage, assetBaseUrl, loadedImages]);
31454
+ };
31455
+ return interp.startLoop(drawFrame);
31456
+ }, [interp.startLoop, loadImage]);
31325
31457
  return /* @__PURE__ */ jsxRuntime.jsx(
31326
31458
  "canvas",
31327
31459
  {
@@ -31339,6 +31471,7 @@ var init_PlatformerCanvas = __esm({
31339
31471
  init_cn();
31340
31472
  init_useEventBus();
31341
31473
  init_verificationRegistry();
31474
+ init_useRenderInterpolation();
31342
31475
  PLATFORM_COLORS = {
31343
31476
  ground: "#4a7c59",
31344
31477
  platform: "#7c6b4a",
@@ -31721,13 +31854,13 @@ var init_MapView = __esm({
31721
31854
  shadowSize: [41, 41]
31722
31855
  });
31723
31856
  L.Marker.prototype.options.icon = defaultIcon;
31724
- const { useEffect: useEffect78, useRef: useRef70, useCallback: useCallback117, useState: useState115 } = React91__namespace.default;
31857
+ const { useEffect: useEffect79, useRef: useRef71, useCallback: useCallback118, useState: useState115 } = React91__namespace.default;
31725
31858
  const { Typography: Typography2 } = await Promise.resolve().then(() => (init_Typography(), Typography_exports));
31726
31859
  const { useEventBus: useEventBus3 } = await Promise.resolve().then(() => (init_useEventBus(), useEventBus_exports));
31727
31860
  function MapUpdater({ centerLat, centerLng, zoom }) {
31728
31861
  const map = useMap();
31729
- const prevRef = useRef70({ centerLat, centerLng, zoom });
31730
- useEffect78(() => {
31862
+ const prevRef = useRef71({ centerLat, centerLng, zoom });
31863
+ useEffect79(() => {
31731
31864
  const prev = prevRef.current;
31732
31865
  if (prev.centerLat !== centerLat || prev.centerLng !== centerLng || prev.zoom !== zoom) {
31733
31866
  map.setView([centerLat, centerLng], zoom);
@@ -31738,7 +31871,7 @@ var init_MapView = __esm({
31738
31871
  }
31739
31872
  function MapClickHandler({ onMapClick }) {
31740
31873
  const map = useMap();
31741
- useEffect78(() => {
31874
+ useEffect79(() => {
31742
31875
  if (!onMapClick) return;
31743
31876
  const handler = (e) => {
31744
31877
  onMapClick(e.latlng.lat, e.latlng.lng);
@@ -31767,7 +31900,7 @@ var init_MapView = __esm({
31767
31900
  }) {
31768
31901
  const eventBus = useEventBus3();
31769
31902
  const [clickedPosition, setClickedPosition] = useState115(null);
31770
- const handleMapClick = useCallback117((lat, lng) => {
31903
+ const handleMapClick = useCallback118((lat, lng) => {
31771
31904
  if (showClickedPin) {
31772
31905
  setClickedPosition({ lat, lng });
31773
31906
  }
@@ -31776,7 +31909,7 @@ var init_MapView = __esm({
31776
31909
  eventBus.emit(`UI:${mapClickEvent}`, { latitude: lat, longitude: lng });
31777
31910
  }
31778
31911
  }, [onMapClick, mapClickEvent, eventBus, showClickedPin]);
31779
- const handleMarkerClick = useCallback117((marker) => {
31912
+ const handleMarkerClick = useCallback118((marker) => {
31780
31913
  onMarkerClick?.(marker);
31781
31914
  if (markerClickEvent) {
31782
31915
  eventBus.emit(`UI:${markerClickEvent}`, { ...marker });
@@ -46871,6 +47004,10 @@ function SequencerBoard({
46871
47004
  stepDurationMs = 1e3,
46872
47005
  playEvent,
46873
47006
  completeEvent,
47007
+ placeEvent,
47008
+ removeEvent,
47009
+ checkEvent,
47010
+ playAgainEvent,
46874
47011
  className
46875
47012
  }) {
46876
47013
  const { emit } = useEventBus();
@@ -46906,7 +47043,8 @@ function SequencerBoard({
46906
47043
  return next;
46907
47044
  });
46908
47045
  emit("UI:PLAY_SOUND", { key: "drop_slot" });
46909
- }, [emit]);
47046
+ if (placeEvent) emit(`UI:${placeEvent}`, { slotIndex: index, actionId: item.id });
47047
+ }, [emit, placeEvent]);
46910
47048
  const handleSlotRemove = React91.useCallback((index) => {
46911
47049
  setSlots((prev) => {
46912
47050
  const next = [...prev];
@@ -46919,7 +47057,8 @@ function SequencerBoard({
46919
47057
  return next;
46920
47058
  });
46921
47059
  emit("UI:PLAY_SOUND", { key: "back" });
46922
- }, [emit]);
47060
+ if (removeEvent) emit(`UI:${removeEvent}`, { slotIndex: index });
47061
+ }, [emit, removeEvent]);
46923
47062
  const handleReset = React91.useCallback(() => {
46924
47063
  if (timerRef.current) clearTimeout(timerRef.current);
46925
47064
  setSlots(Array.from({ length: maxSlots }, () => void 0));
@@ -46927,7 +47066,8 @@ function SequencerBoard({
46927
47066
  setCurrentStep(-1);
46928
47067
  setAttempts(0);
46929
47068
  setSlotFeedback(Array.from({ length: maxSlots }, () => null));
46930
- }, [maxSlots]);
47069
+ if (playAgainEvent) emit(`UI:${playAgainEvent}`, {});
47070
+ }, [maxSlots, playAgainEvent, emit]);
46931
47071
  const filledSlots = slots.filter((s) => !!s);
46932
47072
  const canPlay = filledSlots.length > 0 && playState === "idle";
46933
47073
  const handlePlay = React91.useCallback(() => {
@@ -46949,6 +47089,7 @@ function SequencerBoard({
46949
47089
  const success = solutions.some(
46950
47090
  (sol) => sol.length === playerIds.length && sol.every((id, i) => id === playerIds[i])
46951
47091
  );
47092
+ if (checkEvent) emit(`UI:${checkEvent}`, { sequence: playerIds });
46952
47093
  if (success) {
46953
47094
  setPlayState("success");
46954
47095
  setCurrentStep(-1);
@@ -46976,7 +47117,7 @@ function SequencerBoard({
46976
47117
  }
46977
47118
  };
46978
47119
  timerRef.current = setTimeout(advance, stepDurationMs);
46979
- }, [canPlay, slots, maxSlots, solutions, stepDurationMs, playEvent, completeEvent, emit]);
47120
+ }, [canPlay, slots, maxSlots, solutions, stepDurationMs, playEvent, completeEvent, checkEvent, emit]);
46980
47121
  const machine = {
46981
47122
  name: str(resolved?.title),
46982
47123
  description: str(resolved?.description),
@@ -47231,11 +47372,13 @@ function SimulationCanvas({
47231
47372
  height = 400,
47232
47373
  running,
47233
47374
  speed = 1,
47375
+ bodies: externalBodies,
47234
47376
  className
47235
47377
  }) {
47236
47378
  const preset = React91.useMemo(() => resolvePreset(presetProp), [presetProp]);
47237
47379
  const canvasRef = React91.useRef(null);
47238
47380
  const bodiesRef = React91.useRef(structuredClone(preset.bodies));
47381
+ const interp = useRenderInterpolation();
47239
47382
  React91.useEffect(() => {
47240
47383
  bodiesRef.current = structuredClone(preset.bodies);
47241
47384
  }, [preset]);
@@ -47333,6 +47476,67 @@ function SimulationCanvas({
47333
47476
  }
47334
47477
  }, [width, height, preset]);
47335
47478
  React91.useEffect(() => {
47479
+ if (!externalBodies) return;
47480
+ interp.onSnapshot(externalBodies.map((b) => ({ id: b.id, x: b.x, y: b.y })));
47481
+ }, [externalBodies]);
47482
+ const presetRef = React91.useRef(preset);
47483
+ presetRef.current = preset;
47484
+ const widthRef = React91.useRef(width);
47485
+ widthRef.current = width;
47486
+ const heightRef = React91.useRef(height);
47487
+ heightRef.current = height;
47488
+ React91.useEffect(() => {
47489
+ if (!externalBodies) return;
47490
+ const drawInterpolated = (positions) => {
47491
+ const canvas = canvasRef.current;
47492
+ if (!canvas) return;
47493
+ const ctx = canvas.getContext("2d");
47494
+ if (!ctx) return;
47495
+ const bodies = bodiesRef.current;
47496
+ const p2 = presetRef.current;
47497
+ const w = widthRef.current;
47498
+ const h = heightRef.current;
47499
+ ctx.clearRect(0, 0, w, h);
47500
+ ctx.fillStyle = p2.backgroundColor ?? "#1a1a2e";
47501
+ ctx.fillRect(0, 0, w, h);
47502
+ if (p2.constraints) {
47503
+ for (const c of p2.constraints) {
47504
+ const a = bodies[c.bodyA];
47505
+ const b = bodies[c.bodyB];
47506
+ if (a && b) {
47507
+ const aPos = positions.get(a.id) ?? a;
47508
+ const bPos = positions.get(b.id) ?? b;
47509
+ ctx.beginPath();
47510
+ ctx.moveTo(aPos.x, aPos.y);
47511
+ ctx.lineTo(bPos.x, bPos.y);
47512
+ ctx.strokeStyle = "#533483";
47513
+ ctx.lineWidth = 1;
47514
+ ctx.setLineDash([4, 4]);
47515
+ ctx.stroke();
47516
+ ctx.setLineDash([]);
47517
+ }
47518
+ }
47519
+ }
47520
+ for (const body of bodies) {
47521
+ const pos = positions.get(body.id) ?? body;
47522
+ ctx.beginPath();
47523
+ ctx.arc(pos.x, pos.y, body.radius, 0, Math.PI * 2);
47524
+ ctx.fillStyle = body.color ?? "#e94560";
47525
+ ctx.fill();
47526
+ if (p2.showVelocity) {
47527
+ ctx.beginPath();
47528
+ ctx.moveTo(pos.x, pos.y);
47529
+ ctx.lineTo(pos.x + body.vx * 0.1, pos.y + body.vy * 0.1);
47530
+ ctx.strokeStyle = "#16213e";
47531
+ ctx.lineWidth = 2;
47532
+ ctx.stroke();
47533
+ }
47534
+ }
47535
+ };
47536
+ return interp.startLoop(drawInterpolated);
47537
+ }, [externalBodies !== void 0, interp.startLoop]);
47538
+ React91.useEffect(() => {
47539
+ if (externalBodies !== void 0) return;
47336
47540
  if (!running) return;
47337
47541
  let raf;
47338
47542
  const loop = () => {
@@ -47342,10 +47546,11 @@ function SimulationCanvas({
47342
47546
  };
47343
47547
  raf = requestAnimationFrame(loop);
47344
47548
  return () => cancelAnimationFrame(raf);
47345
- }, [running, step, draw]);
47549
+ }, [running, step, draw, externalBodies]);
47346
47550
  React91.useEffect(() => {
47551
+ if (externalBodies !== void 0) return;
47347
47552
  draw();
47348
- }, [draw]);
47553
+ }, [draw, externalBodies]);
47349
47554
  React91.useEffect(() => {
47350
47555
  if (typeof window === "undefined") return;
47351
47556
  const canvas = canvasRef.current;
@@ -47371,6 +47576,7 @@ var init_SimulationCanvas = __esm({
47371
47576
  init_atoms2();
47372
47577
  init_verificationRegistry();
47373
47578
  init_presets();
47579
+ init_useRenderInterpolation();
47374
47580
  SimulationCanvas.displayName = "SimulationCanvas";
47375
47581
  }
47376
47582
  });