@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.
@@ -28176,6 +28176,78 @@ var init_GameOverScreen = __esm({
28176
28176
  GameOverScreen.displayName = "GameOverScreen";
28177
28177
  }
28178
28178
  });
28179
+ function useRenderInterpolation(options = {}) {
28180
+ const tickIntervalMs = options.tickIntervalMs ?? 1e3 / 30;
28181
+ const prevRef = React82.useRef(null);
28182
+ const currRef = React82.useRef(null);
28183
+ const rafRef = React82.useRef(null);
28184
+ const onSnapshot = React82.useCallback((entities) => {
28185
+ prevRef.current = currRef.current;
28186
+ currRef.current = { entities, arrivedAt: performance.now() };
28187
+ }, []);
28188
+ const getInterpolated = React82.useCallback((now2) => {
28189
+ const out = /* @__PURE__ */ new Map();
28190
+ const curr = currRef.current;
28191
+ if (!curr) return out;
28192
+ const prev = prevRef.current;
28193
+ if (!prev) {
28194
+ for (const e of curr.entities) out.set(e.id, { x: e.x, y: e.y });
28195
+ return out;
28196
+ }
28197
+ const rawAlpha = (now2 - curr.arrivedAt) / tickIntervalMs;
28198
+ const alpha = rawAlpha < 0 ? 0 : rawAlpha > 1 ? 1 : rawAlpha;
28199
+ const prevMap = /* @__PURE__ */ new Map();
28200
+ for (const e of prev.entities) prevMap.set(e.id, e);
28201
+ for (const c of curr.entities) {
28202
+ const p2 = prevMap.get(c.id);
28203
+ if (!p2) {
28204
+ out.set(c.id, { x: c.x, y: c.y });
28205
+ } else {
28206
+ out.set(c.id, {
28207
+ x: p2.x + (c.x - p2.x) * alpha,
28208
+ y: p2.y + (c.y - p2.y) * alpha
28209
+ });
28210
+ }
28211
+ }
28212
+ return out;
28213
+ }, [tickIntervalMs]);
28214
+ const startLoop = React82.useCallback(
28215
+ (draw) => {
28216
+ let active = true;
28217
+ const loop = () => {
28218
+ if (!active) return;
28219
+ try {
28220
+ draw(getInterpolated(performance.now()));
28221
+ } catch {
28222
+ }
28223
+ rafRef.current = requestAnimationFrame(loop);
28224
+ };
28225
+ rafRef.current = requestAnimationFrame(loop);
28226
+ return () => {
28227
+ active = false;
28228
+ if (rafRef.current !== null) {
28229
+ cancelAnimationFrame(rafRef.current);
28230
+ rafRef.current = null;
28231
+ }
28232
+ };
28233
+ },
28234
+ [getInterpolated]
28235
+ );
28236
+ React82.useEffect(() => {
28237
+ return () => {
28238
+ if (rafRef.current !== null) {
28239
+ cancelAnimationFrame(rafRef.current);
28240
+ rafRef.current = null;
28241
+ }
28242
+ };
28243
+ }, []);
28244
+ return { onSnapshot, getInterpolated, startLoop };
28245
+ }
28246
+ var init_useRenderInterpolation = __esm({
28247
+ "hooks/useRenderInterpolation.ts"() {
28248
+ "use client";
28249
+ }
28250
+ });
28179
28251
  function PlatformerCanvas({
28180
28252
  player,
28181
28253
  platforms = [
@@ -28244,8 +28316,43 @@ function PlatformerCanvas({
28244
28316
  y: 336,
28245
28317
  width: 32,
28246
28318
  height: 48,
28319
+ vx: 0,
28320
+ vy: 0,
28321
+ grounded: true,
28247
28322
  facingRight: true
28248
28323
  };
28324
+ const playerRef = React82.useRef(resolvedPlayer);
28325
+ playerRef.current = resolvedPlayer;
28326
+ const interp = useRenderInterpolation();
28327
+ React82.useEffect(() => {
28328
+ interp.onSnapshot([{ id: "player", x: resolvedPlayer.x, y: resolvedPlayer.y }]);
28329
+ }, [resolvedPlayer.x, resolvedPlayer.y]);
28330
+ const propsRef = React82.useRef({
28331
+ platforms,
28332
+ worldWidth,
28333
+ worldHeight,
28334
+ canvasWidth,
28335
+ canvasHeight,
28336
+ followCamera,
28337
+ bgColor,
28338
+ playerSprite,
28339
+ tileSprites,
28340
+ backgroundImage,
28341
+ assetBaseUrl
28342
+ });
28343
+ propsRef.current = {
28344
+ platforms,
28345
+ worldWidth,
28346
+ worldHeight,
28347
+ canvasWidth,
28348
+ canvasHeight,
28349
+ followCamera,
28350
+ bgColor,
28351
+ playerSprite,
28352
+ tileSprites,
28353
+ backgroundImage,
28354
+ assetBaseUrl
28355
+ };
28249
28356
  const handleKeyDown = React82.useCallback((e) => {
28250
28357
  if (keysRef.current.has(e.code)) return;
28251
28358
  keysRef.current.add(e.code);
@@ -28294,124 +28401,149 @@ function PlatformerCanvas({
28294
28401
  canvas.width = canvasWidth * dpr;
28295
28402
  canvas.height = canvasHeight * dpr;
28296
28403
  ctx.scale(dpr, dpr);
28297
- let camX = 0;
28298
- let camY = 0;
28299
- if (followCamera) {
28300
- camX = Math.max(0, Math.min(resolvedPlayer.x - canvasWidth / 2, worldWidth - canvasWidth));
28301
- camY = Math.max(0, Math.min(resolvedPlayer.y - canvasHeight / 2 - 50, worldHeight - canvasHeight));
28302
- }
28303
- const bgImg = backgroundImage ? loadImage(backgroundImage) : null;
28304
- if (bgImg) {
28305
- ctx.drawImage(bgImg, 0, 0, canvasWidth, canvasHeight);
28306
- } else if (bgColor) {
28307
- ctx.fillStyle = bgColor;
28308
- ctx.fillRect(0, 0, canvasWidth, canvasHeight);
28309
- } else {
28310
- const grad = ctx.createLinearGradient(0, 0, 0, canvasHeight);
28311
- grad.addColorStop(0, SKY_GRADIENT_TOP);
28312
- grad.addColorStop(1, SKY_GRADIENT_BOTTOM);
28313
- ctx.fillStyle = grad;
28314
- ctx.fillRect(0, 0, canvasWidth, canvasHeight);
28315
- }
28316
- ctx.strokeStyle = GRID_COLOR;
28317
- ctx.lineWidth = 1;
28318
- const gridSize = 32;
28319
- for (let gx = -camX % gridSize; gx < canvasWidth; gx += gridSize) {
28320
- ctx.beginPath();
28321
- ctx.moveTo(gx, 0);
28322
- ctx.lineTo(gx, canvasHeight);
28323
- ctx.stroke();
28324
- }
28325
- for (let gy = -camY % gridSize; gy < canvasHeight; gy += gridSize) {
28326
- ctx.beginPath();
28327
- ctx.moveTo(0, gy);
28328
- ctx.lineTo(canvasWidth, gy);
28329
- ctx.stroke();
28330
- }
28331
- for (const plat of platforms) {
28332
- const px = plat.x - camX;
28333
- const py = plat.y - camY;
28334
- const platType = plat.type ?? "ground";
28335
- const spriteUrl = tileSprites?.[platType];
28336
- const tileImg = spriteUrl ? loadImage(spriteUrl) : null;
28337
- if (tileImg) {
28338
- const tileW = tileImg.naturalWidth;
28339
- const tileH = tileImg.naturalHeight;
28340
- const scaleH = plat.height / tileH;
28341
- const scaledW = tileW * scaleH;
28342
- for (let tx = 0; tx < plat.width; tx += scaledW) {
28343
- const drawW = Math.min(scaledW, plat.width - tx);
28344
- const srcW = drawW / scaleH;
28345
- ctx.drawImage(tileImg, 0, 0, srcW, tileH, px + tx, py, drawW, plat.height);
28346
- }
28404
+ }, [canvasWidth, canvasHeight]);
28405
+ React82.useEffect(() => {
28406
+ const drawFrame = (positions) => {
28407
+ const canvas = canvasRef.current;
28408
+ if (!canvas) return;
28409
+ const ctx = canvas.getContext("2d");
28410
+ if (!ctx) return;
28411
+ const {
28412
+ platforms: plats,
28413
+ worldWidth: ww,
28414
+ worldHeight: wh,
28415
+ canvasWidth: cw,
28416
+ canvasHeight: ch,
28417
+ followCamera: fc,
28418
+ bgColor: bg,
28419
+ playerSprite: pSprite,
28420
+ tileSprites: tSprites,
28421
+ backgroundImage: bgImg
28422
+ } = propsRef.current;
28423
+ const auth = playerRef.current;
28424
+ const interped = positions.get("player");
28425
+ const px = interped?.x ?? auth.x;
28426
+ const py = interped?.y ?? auth.y;
28427
+ let camX = 0;
28428
+ let camY = 0;
28429
+ if (fc) {
28430
+ camX = Math.max(0, Math.min(px - cw / 2, ww - cw));
28431
+ camY = Math.max(0, Math.min(py - ch / 2 - 50, wh - ch));
28432
+ }
28433
+ const bgImage = bgImg ? loadImage(bgImg) : null;
28434
+ if (bgImage) {
28435
+ ctx.drawImage(bgImage, 0, 0, cw, ch);
28436
+ } else if (bg) {
28437
+ ctx.fillStyle = bg;
28438
+ ctx.fillRect(0, 0, cw, ch);
28347
28439
  } else {
28348
- const color = PLATFORM_COLORS[platType] ?? PLATFORM_COLORS.ground;
28349
- ctx.fillStyle = color;
28350
- ctx.fillRect(px, py, plat.width, plat.height);
28351
- ctx.fillStyle = "rgba(255, 255, 255, 0.15)";
28352
- ctx.fillRect(px, py, plat.width, 3);
28353
- ctx.fillStyle = "rgba(0, 0, 0, 0.3)";
28354
- ctx.fillRect(px, py + plat.height - 2, plat.width, 2);
28355
- if (platType === "hazard") {
28356
- ctx.strokeStyle = "#e74c3c";
28357
- ctx.lineWidth = 2;
28358
- for (let sx = px; sx < px + plat.width; sx += 12) {
28440
+ const grad = ctx.createLinearGradient(0, 0, 0, ch);
28441
+ grad.addColorStop(0, SKY_GRADIENT_TOP);
28442
+ grad.addColorStop(1, SKY_GRADIENT_BOTTOM);
28443
+ ctx.fillStyle = grad;
28444
+ ctx.fillRect(0, 0, cw, ch);
28445
+ }
28446
+ ctx.strokeStyle = GRID_COLOR;
28447
+ ctx.lineWidth = 1;
28448
+ const gridSize = 32;
28449
+ for (let gx = -camX % gridSize; gx < cw; gx += gridSize) {
28450
+ ctx.beginPath();
28451
+ ctx.moveTo(gx, 0);
28452
+ ctx.lineTo(gx, ch);
28453
+ ctx.stroke();
28454
+ }
28455
+ for (let gy = -camY % gridSize; gy < ch; gy += gridSize) {
28456
+ ctx.beginPath();
28457
+ ctx.moveTo(0, gy);
28458
+ ctx.lineTo(cw, gy);
28459
+ ctx.stroke();
28460
+ }
28461
+ for (const plat of plats) {
28462
+ const platX = plat.x - camX;
28463
+ const platY = plat.y - camY;
28464
+ const platType = plat.type ?? "ground";
28465
+ const spriteUrl = tSprites?.[platType];
28466
+ const tileImg = spriteUrl ? loadImage(spriteUrl) : null;
28467
+ if (tileImg) {
28468
+ const tileW = tileImg.naturalWidth;
28469
+ const tileH = tileImg.naturalHeight;
28470
+ const scaleH = plat.height / tileH;
28471
+ const scaledW = tileW * scaleH;
28472
+ for (let tx = 0; tx < plat.width; tx += scaledW) {
28473
+ const drawW = Math.min(scaledW, plat.width - tx);
28474
+ const srcW = drawW / scaleH;
28475
+ ctx.drawImage(tileImg, 0, 0, srcW, tileH, platX + tx, platY, drawW, plat.height);
28476
+ }
28477
+ } else {
28478
+ const color = PLATFORM_COLORS[platType] ?? PLATFORM_COLORS.ground;
28479
+ ctx.fillStyle = color;
28480
+ ctx.fillRect(platX, platY, plat.width, plat.height);
28481
+ ctx.fillStyle = "rgba(255, 255, 255, 0.15)";
28482
+ ctx.fillRect(platX, platY, plat.width, 3);
28483
+ ctx.fillStyle = "rgba(0, 0, 0, 0.3)";
28484
+ ctx.fillRect(platX, platY + plat.height - 2, plat.width, 2);
28485
+ if (platType === "hazard") {
28486
+ ctx.strokeStyle = "#e74c3c";
28487
+ ctx.lineWidth = 2;
28488
+ for (let sx = platX; sx < platX + plat.width; sx += 12) {
28489
+ ctx.beginPath();
28490
+ ctx.moveTo(sx, platY);
28491
+ ctx.lineTo(sx + 6, platY + plat.height);
28492
+ ctx.stroke();
28493
+ }
28494
+ }
28495
+ if (platType === "goal") {
28496
+ ctx.fillStyle = "rgba(241, 196, 15, 0.5)";
28359
28497
  ctx.beginPath();
28360
- ctx.moveTo(sx, py);
28361
- ctx.lineTo(sx + 6, py + plat.height);
28362
- ctx.stroke();
28498
+ ctx.arc(platX + plat.width / 2, platY - 10, 8, 0, Math.PI * 2);
28499
+ ctx.fill();
28363
28500
  }
28364
28501
  }
28365
- if (platType === "goal") {
28366
- ctx.fillStyle = "rgba(241, 196, 15, 0.5)";
28367
- ctx.beginPath();
28368
- ctx.arc(px + plat.width / 2, py - 10, 8, 0, Math.PI * 2);
28369
- ctx.fill();
28370
- }
28371
28502
  }
28372
- }
28373
- const pw = resolvedPlayer.width ?? 24;
28374
- const ph = resolvedPlayer.height ?? 32;
28375
- const ppx = resolvedPlayer.x - camX;
28376
- const ppy = resolvedPlayer.y - camY;
28377
- const facingRight = resolvedPlayer.facingRight ?? true;
28378
- const playerImg = playerSprite ? loadImage(playerSprite) : null;
28379
- if (playerImg) {
28380
- ctx.save();
28381
- if (!facingRight) {
28382
- ctx.translate(ppx + pw, ppy);
28383
- ctx.scale(-1, 1);
28384
- ctx.drawImage(playerImg, 0, 0, pw, ph);
28503
+ const pw = auth.width ?? 24;
28504
+ const ph = auth.height ?? 32;
28505
+ const ppx = px - camX;
28506
+ const ppy = py - camY;
28507
+ const facingRight = auth.facingRight ?? true;
28508
+ const playerImg = pSprite ? loadImage(pSprite) : null;
28509
+ if (playerImg) {
28510
+ ctx.save();
28511
+ if (!facingRight) {
28512
+ ctx.translate(ppx + pw, ppy);
28513
+ ctx.scale(-1, 1);
28514
+ ctx.drawImage(playerImg, 0, 0, pw, ph);
28515
+ } else {
28516
+ ctx.drawImage(playerImg, ppx, ppy, pw, ph);
28517
+ }
28518
+ ctx.restore();
28385
28519
  } else {
28386
- ctx.drawImage(playerImg, ppx, ppy, pw, ph);
28520
+ ctx.fillStyle = PLAYER_COLOR;
28521
+ const radius = Math.min(pw, ph) * 0.25;
28522
+ ctx.beginPath();
28523
+ ctx.moveTo(ppx + radius, ppy);
28524
+ ctx.lineTo(ppx + pw - radius, ppy);
28525
+ ctx.quadraticCurveTo(ppx + pw, ppy, ppx + pw, ppy + radius);
28526
+ ctx.lineTo(ppx + pw, ppy + ph - radius);
28527
+ ctx.quadraticCurveTo(ppx + pw, ppy + ph, ppx + pw - radius, ppy + ph);
28528
+ ctx.lineTo(ppx + radius, ppy + ph);
28529
+ ctx.quadraticCurveTo(ppx, ppy + ph, ppx, ppy + ph - radius);
28530
+ ctx.lineTo(ppx, ppy + radius);
28531
+ ctx.quadraticCurveTo(ppx, ppy, ppx + radius, ppy);
28532
+ ctx.fill();
28533
+ const eyeY = ppy + ph * 0.3;
28534
+ const eyeSize = 3;
28535
+ const eyeOffsetX = facingRight ? pw * 0.55 : pw * 0.2;
28536
+ ctx.fillStyle = PLAYER_EYE_COLOR;
28537
+ ctx.beginPath();
28538
+ ctx.arc(ppx + eyeOffsetX, eyeY, eyeSize, 0, Math.PI * 2);
28539
+ ctx.fill();
28540
+ ctx.beginPath();
28541
+ ctx.arc(ppx + eyeOffsetX + 7, eyeY, eyeSize, 0, Math.PI * 2);
28542
+ ctx.fill();
28387
28543
  }
28388
- ctx.restore();
28389
- } else {
28390
- ctx.fillStyle = PLAYER_COLOR;
28391
- const radius = Math.min(pw, ph) * 0.25;
28392
- ctx.beginPath();
28393
- ctx.moveTo(ppx + radius, ppy);
28394
- ctx.lineTo(ppx + pw - radius, ppy);
28395
- ctx.quadraticCurveTo(ppx + pw, ppy, ppx + pw, ppy + radius);
28396
- ctx.lineTo(ppx + pw, ppy + ph - radius);
28397
- ctx.quadraticCurveTo(ppx + pw, ppy + ph, ppx + pw - radius, ppy + ph);
28398
- ctx.lineTo(ppx + radius, ppy + ph);
28399
- ctx.quadraticCurveTo(ppx, ppy + ph, ppx, ppy + ph - radius);
28400
- ctx.lineTo(ppx, ppy + radius);
28401
- ctx.quadraticCurveTo(ppx, ppy, ppx + radius, ppy);
28402
- ctx.fill();
28403
- const eyeY = ppy + ph * 0.3;
28404
- const eyeSize = 3;
28405
- const eyeOffsetX = facingRight ? pw * 0.55 : pw * 0.2;
28406
- ctx.fillStyle = PLAYER_EYE_COLOR;
28407
- ctx.beginPath();
28408
- ctx.arc(ppx + eyeOffsetX, eyeY, eyeSize, 0, Math.PI * 2);
28409
- ctx.fill();
28410
- ctx.beginPath();
28411
- ctx.arc(ppx + eyeOffsetX + 7, eyeY, eyeSize, 0, Math.PI * 2);
28412
- ctx.fill();
28413
- }
28414
- }, [player, platforms, worldWidth, worldHeight, canvasWidth, canvasHeight, followCamera, bgColor, playerSprite, tileSprites, backgroundImage, assetBaseUrl, loadedImages]);
28544
+ };
28545
+ return interp.startLoop(drawFrame);
28546
+ }, [interp.startLoop, loadImage]);
28415
28547
  return /* @__PURE__ */ jsxRuntime.jsx(
28416
28548
  "canvas",
28417
28549
  {
@@ -28429,6 +28561,7 @@ var init_PlatformerCanvas = __esm({
28429
28561
  init_cn();
28430
28562
  init_useEventBus();
28431
28563
  init_verificationRegistry();
28564
+ init_useRenderInterpolation();
28432
28565
  PLATFORM_COLORS = {
28433
28566
  ground: "#4a7c59",
28434
28567
  platform: "#7c6b4a",
@@ -28811,13 +28944,13 @@ var init_MapView = __esm({
28811
28944
  shadowSize: [41, 41]
28812
28945
  });
28813
28946
  L.Marker.prototype.options.icon = defaultIcon;
28814
- const { useEffect: useEffect74, useRef: useRef68, useCallback: useCallback111, useState: useState107 } = React82__namespace.default;
28947
+ const { useEffect: useEffect75, useRef: useRef69, useCallback: useCallback112, useState: useState107 } = React82__namespace.default;
28815
28948
  const { Typography: Typography2 } = await Promise.resolve().then(() => (init_Typography(), Typography_exports));
28816
28949
  const { useEventBus: useEventBus3 } = await Promise.resolve().then(() => (init_useEventBus(), useEventBus_exports));
28817
28950
  function MapUpdater({ centerLat, centerLng, zoom }) {
28818
28951
  const map = useMap();
28819
- const prevRef = useRef68({ centerLat, centerLng, zoom });
28820
- useEffect74(() => {
28952
+ const prevRef = useRef69({ centerLat, centerLng, zoom });
28953
+ useEffect75(() => {
28821
28954
  const prev = prevRef.current;
28822
28955
  if (prev.centerLat !== centerLat || prev.centerLng !== centerLng || prev.zoom !== zoom) {
28823
28956
  map.setView([centerLat, centerLng], zoom);
@@ -28828,7 +28961,7 @@ var init_MapView = __esm({
28828
28961
  }
28829
28962
  function MapClickHandler({ onMapClick }) {
28830
28963
  const map = useMap();
28831
- useEffect74(() => {
28964
+ useEffect75(() => {
28832
28965
  if (!onMapClick) return;
28833
28966
  const handler = (e) => {
28834
28967
  onMapClick(e.latlng.lat, e.latlng.lng);
@@ -28857,7 +28990,7 @@ var init_MapView = __esm({
28857
28990
  }) {
28858
28991
  const eventBus = useEventBus3();
28859
28992
  const [clickedPosition, setClickedPosition] = useState107(null);
28860
- const handleMapClick = useCallback111((lat, lng) => {
28993
+ const handleMapClick = useCallback112((lat, lng) => {
28861
28994
  if (showClickedPin) {
28862
28995
  setClickedPosition({ lat, lng });
28863
28996
  }
@@ -28866,7 +28999,7 @@ var init_MapView = __esm({
28866
28999
  eventBus.emit(`UI:${mapClickEvent}`, { latitude: lat, longitude: lng });
28867
29000
  }
28868
29001
  }, [onMapClick, mapClickEvent, eventBus, showClickedPin]);
28869
- const handleMarkerClick = useCallback111((marker) => {
29002
+ const handleMarkerClick = useCallback112((marker) => {
28870
29003
  onMarkerClick?.(marker);
28871
29004
  if (markerClickEvent) {
28872
29005
  eventBus.emit(`UI:${markerClickEvent}`, { ...marker });
@@ -44370,6 +44503,10 @@ function SequencerBoard({
44370
44503
  stepDurationMs = 1e3,
44371
44504
  playEvent,
44372
44505
  completeEvent,
44506
+ placeEvent,
44507
+ removeEvent,
44508
+ checkEvent,
44509
+ playAgainEvent,
44373
44510
  className
44374
44511
  }) {
44375
44512
  const { emit } = useEventBus();
@@ -44405,7 +44542,8 @@ function SequencerBoard({
44405
44542
  return next;
44406
44543
  });
44407
44544
  emit("UI:PLAY_SOUND", { key: "drop_slot" });
44408
- }, [emit]);
44545
+ if (placeEvent) emit(`UI:${placeEvent}`, { slotIndex: index, actionId: item.id });
44546
+ }, [emit, placeEvent]);
44409
44547
  const handleSlotRemove = React82.useCallback((index) => {
44410
44548
  setSlots((prev) => {
44411
44549
  const next = [...prev];
@@ -44418,7 +44556,8 @@ function SequencerBoard({
44418
44556
  return next;
44419
44557
  });
44420
44558
  emit("UI:PLAY_SOUND", { key: "back" });
44421
- }, [emit]);
44559
+ if (removeEvent) emit(`UI:${removeEvent}`, { slotIndex: index });
44560
+ }, [emit, removeEvent]);
44422
44561
  const handleReset = React82.useCallback(() => {
44423
44562
  if (timerRef.current) clearTimeout(timerRef.current);
44424
44563
  setSlots(Array.from({ length: maxSlots }, () => void 0));
@@ -44426,7 +44565,8 @@ function SequencerBoard({
44426
44565
  setCurrentStep(-1);
44427
44566
  setAttempts(0);
44428
44567
  setSlotFeedback(Array.from({ length: maxSlots }, () => null));
44429
- }, [maxSlots]);
44568
+ if (playAgainEvent) emit(`UI:${playAgainEvent}`, {});
44569
+ }, [maxSlots, playAgainEvent, emit]);
44430
44570
  const filledSlots = slots.filter((s) => !!s);
44431
44571
  const canPlay = filledSlots.length > 0 && playState === "idle";
44432
44572
  const handlePlay = React82.useCallback(() => {
@@ -44448,6 +44588,7 @@ function SequencerBoard({
44448
44588
  const success = solutions.some(
44449
44589
  (sol) => sol.length === playerIds.length && sol.every((id, i) => id === playerIds[i])
44450
44590
  );
44591
+ if (checkEvent) emit(`UI:${checkEvent}`, { sequence: playerIds });
44451
44592
  if (success) {
44452
44593
  setPlayState("success");
44453
44594
  setCurrentStep(-1);
@@ -44475,7 +44616,7 @@ function SequencerBoard({
44475
44616
  }
44476
44617
  };
44477
44618
  timerRef.current = setTimeout(advance, stepDurationMs);
44478
- }, [canPlay, slots, maxSlots, solutions, stepDurationMs, playEvent, completeEvent, emit]);
44619
+ }, [canPlay, slots, maxSlots, solutions, stepDurationMs, playEvent, completeEvent, checkEvent, emit]);
44479
44620
  const machine = {
44480
44621
  name: str(resolved?.title),
44481
44622
  description: str(resolved?.description),
@@ -44730,11 +44871,13 @@ function SimulationCanvas({
44730
44871
  height = 400,
44731
44872
  running,
44732
44873
  speed = 1,
44874
+ bodies: externalBodies,
44733
44875
  className
44734
44876
  }) {
44735
44877
  const preset = React82.useMemo(() => resolvePreset(presetProp), [presetProp]);
44736
44878
  const canvasRef = React82.useRef(null);
44737
44879
  const bodiesRef = React82.useRef(structuredClone(preset.bodies));
44880
+ const interp = useRenderInterpolation();
44738
44881
  React82.useEffect(() => {
44739
44882
  bodiesRef.current = structuredClone(preset.bodies);
44740
44883
  }, [preset]);
@@ -44832,6 +44975,67 @@ function SimulationCanvas({
44832
44975
  }
44833
44976
  }, [width, height, preset]);
44834
44977
  React82.useEffect(() => {
44978
+ if (!externalBodies) return;
44979
+ interp.onSnapshot(externalBodies.map((b) => ({ id: b.id, x: b.x, y: b.y })));
44980
+ }, [externalBodies]);
44981
+ const presetRef = React82.useRef(preset);
44982
+ presetRef.current = preset;
44983
+ const widthRef = React82.useRef(width);
44984
+ widthRef.current = width;
44985
+ const heightRef = React82.useRef(height);
44986
+ heightRef.current = height;
44987
+ React82.useEffect(() => {
44988
+ if (!externalBodies) return;
44989
+ const drawInterpolated = (positions) => {
44990
+ const canvas = canvasRef.current;
44991
+ if (!canvas) return;
44992
+ const ctx = canvas.getContext("2d");
44993
+ if (!ctx) return;
44994
+ const bodies = bodiesRef.current;
44995
+ const p2 = presetRef.current;
44996
+ const w = widthRef.current;
44997
+ const h = heightRef.current;
44998
+ ctx.clearRect(0, 0, w, h);
44999
+ ctx.fillStyle = p2.backgroundColor ?? "#1a1a2e";
45000
+ ctx.fillRect(0, 0, w, h);
45001
+ if (p2.constraints) {
45002
+ for (const c of p2.constraints) {
45003
+ const a = bodies[c.bodyA];
45004
+ const b = bodies[c.bodyB];
45005
+ if (a && b) {
45006
+ const aPos = positions.get(a.id) ?? a;
45007
+ const bPos = positions.get(b.id) ?? b;
45008
+ ctx.beginPath();
45009
+ ctx.moveTo(aPos.x, aPos.y);
45010
+ ctx.lineTo(bPos.x, bPos.y);
45011
+ ctx.strokeStyle = "#533483";
45012
+ ctx.lineWidth = 1;
45013
+ ctx.setLineDash([4, 4]);
45014
+ ctx.stroke();
45015
+ ctx.setLineDash([]);
45016
+ }
45017
+ }
45018
+ }
45019
+ for (const body of bodies) {
45020
+ const pos = positions.get(body.id) ?? body;
45021
+ ctx.beginPath();
45022
+ ctx.arc(pos.x, pos.y, body.radius, 0, Math.PI * 2);
45023
+ ctx.fillStyle = body.color ?? "#e94560";
45024
+ ctx.fill();
45025
+ if (p2.showVelocity) {
45026
+ ctx.beginPath();
45027
+ ctx.moveTo(pos.x, pos.y);
45028
+ ctx.lineTo(pos.x + body.vx * 0.1, pos.y + body.vy * 0.1);
45029
+ ctx.strokeStyle = "#16213e";
45030
+ ctx.lineWidth = 2;
45031
+ ctx.stroke();
45032
+ }
45033
+ }
45034
+ };
45035
+ return interp.startLoop(drawInterpolated);
45036
+ }, [externalBodies !== void 0, interp.startLoop]);
45037
+ React82.useEffect(() => {
45038
+ if (externalBodies !== void 0) return;
44835
45039
  if (!running) return;
44836
45040
  let raf;
44837
45041
  const loop = () => {
@@ -44841,10 +45045,11 @@ function SimulationCanvas({
44841
45045
  };
44842
45046
  raf = requestAnimationFrame(loop);
44843
45047
  return () => cancelAnimationFrame(raf);
44844
- }, [running, step, draw]);
45048
+ }, [running, step, draw, externalBodies]);
44845
45049
  React82.useEffect(() => {
45050
+ if (externalBodies !== void 0) return;
44846
45051
  draw();
44847
- }, [draw]);
45052
+ }, [draw, externalBodies]);
44848
45053
  React82.useEffect(() => {
44849
45054
  if (typeof window === "undefined") return;
44850
45055
  const canvas = canvasRef.current;
@@ -44870,6 +45075,7 @@ var init_SimulationCanvas = __esm({
44870
45075
  init_atoms2();
44871
45076
  init_verificationRegistry();
44872
45077
  init_presets();
45078
+ init_useRenderInterpolation();
44873
45079
  SimulationCanvas.displayName = "SimulationCanvas";
44874
45080
  }
44875
45081
  });