@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.
@@ -28130,6 +28130,78 @@ var init_GameOverScreen = __esm({
28130
28130
  GameOverScreen.displayName = "GameOverScreen";
28131
28131
  }
28132
28132
  });
28133
+ function useRenderInterpolation(options = {}) {
28134
+ const tickIntervalMs = options.tickIntervalMs ?? 1e3 / 30;
28135
+ const prevRef = useRef(null);
28136
+ const currRef = useRef(null);
28137
+ const rafRef = useRef(null);
28138
+ const onSnapshot = useCallback((entities) => {
28139
+ prevRef.current = currRef.current;
28140
+ currRef.current = { entities, arrivedAt: performance.now() };
28141
+ }, []);
28142
+ const getInterpolated = useCallback((now2) => {
28143
+ const out = /* @__PURE__ */ new Map();
28144
+ const curr = currRef.current;
28145
+ if (!curr) return out;
28146
+ const prev = prevRef.current;
28147
+ if (!prev) {
28148
+ for (const e of curr.entities) out.set(e.id, { x: e.x, y: e.y });
28149
+ return out;
28150
+ }
28151
+ const rawAlpha = (now2 - curr.arrivedAt) / tickIntervalMs;
28152
+ const alpha = rawAlpha < 0 ? 0 : rawAlpha > 1 ? 1 : rawAlpha;
28153
+ const prevMap = /* @__PURE__ */ new Map();
28154
+ for (const e of prev.entities) prevMap.set(e.id, e);
28155
+ for (const c of curr.entities) {
28156
+ const p2 = prevMap.get(c.id);
28157
+ if (!p2) {
28158
+ out.set(c.id, { x: c.x, y: c.y });
28159
+ } else {
28160
+ out.set(c.id, {
28161
+ x: p2.x + (c.x - p2.x) * alpha,
28162
+ y: p2.y + (c.y - p2.y) * alpha
28163
+ });
28164
+ }
28165
+ }
28166
+ return out;
28167
+ }, [tickIntervalMs]);
28168
+ const startLoop = useCallback(
28169
+ (draw) => {
28170
+ let active = true;
28171
+ const loop = () => {
28172
+ if (!active) return;
28173
+ try {
28174
+ draw(getInterpolated(performance.now()));
28175
+ } catch {
28176
+ }
28177
+ rafRef.current = requestAnimationFrame(loop);
28178
+ };
28179
+ rafRef.current = requestAnimationFrame(loop);
28180
+ return () => {
28181
+ active = false;
28182
+ if (rafRef.current !== null) {
28183
+ cancelAnimationFrame(rafRef.current);
28184
+ rafRef.current = null;
28185
+ }
28186
+ };
28187
+ },
28188
+ [getInterpolated]
28189
+ );
28190
+ useEffect(() => {
28191
+ return () => {
28192
+ if (rafRef.current !== null) {
28193
+ cancelAnimationFrame(rafRef.current);
28194
+ rafRef.current = null;
28195
+ }
28196
+ };
28197
+ }, []);
28198
+ return { onSnapshot, getInterpolated, startLoop };
28199
+ }
28200
+ var init_useRenderInterpolation = __esm({
28201
+ "hooks/useRenderInterpolation.ts"() {
28202
+ "use client";
28203
+ }
28204
+ });
28133
28205
  function PlatformerCanvas({
28134
28206
  player,
28135
28207
  platforms = [
@@ -28198,8 +28270,43 @@ function PlatformerCanvas({
28198
28270
  y: 336,
28199
28271
  width: 32,
28200
28272
  height: 48,
28273
+ vx: 0,
28274
+ vy: 0,
28275
+ grounded: true,
28201
28276
  facingRight: true
28202
28277
  };
28278
+ const playerRef = useRef(resolvedPlayer);
28279
+ playerRef.current = resolvedPlayer;
28280
+ const interp = useRenderInterpolation();
28281
+ useEffect(() => {
28282
+ interp.onSnapshot([{ id: "player", x: resolvedPlayer.x, y: resolvedPlayer.y }]);
28283
+ }, [resolvedPlayer.x, resolvedPlayer.y]);
28284
+ const propsRef = useRef({
28285
+ platforms,
28286
+ worldWidth,
28287
+ worldHeight,
28288
+ canvasWidth,
28289
+ canvasHeight,
28290
+ followCamera,
28291
+ bgColor,
28292
+ playerSprite,
28293
+ tileSprites,
28294
+ backgroundImage,
28295
+ assetBaseUrl
28296
+ });
28297
+ propsRef.current = {
28298
+ platforms,
28299
+ worldWidth,
28300
+ worldHeight,
28301
+ canvasWidth,
28302
+ canvasHeight,
28303
+ followCamera,
28304
+ bgColor,
28305
+ playerSprite,
28306
+ tileSprites,
28307
+ backgroundImage,
28308
+ assetBaseUrl
28309
+ };
28203
28310
  const handleKeyDown = useCallback((e) => {
28204
28311
  if (keysRef.current.has(e.code)) return;
28205
28312
  keysRef.current.add(e.code);
@@ -28248,124 +28355,149 @@ function PlatformerCanvas({
28248
28355
  canvas.width = canvasWidth * dpr;
28249
28356
  canvas.height = canvasHeight * dpr;
28250
28357
  ctx.scale(dpr, dpr);
28251
- let camX = 0;
28252
- let camY = 0;
28253
- if (followCamera) {
28254
- camX = Math.max(0, Math.min(resolvedPlayer.x - canvasWidth / 2, worldWidth - canvasWidth));
28255
- camY = Math.max(0, Math.min(resolvedPlayer.y - canvasHeight / 2 - 50, worldHeight - canvasHeight));
28256
- }
28257
- const bgImg = backgroundImage ? loadImage(backgroundImage) : null;
28258
- if (bgImg) {
28259
- ctx.drawImage(bgImg, 0, 0, canvasWidth, canvasHeight);
28260
- } else if (bgColor) {
28261
- ctx.fillStyle = bgColor;
28262
- ctx.fillRect(0, 0, canvasWidth, canvasHeight);
28263
- } else {
28264
- const grad = ctx.createLinearGradient(0, 0, 0, canvasHeight);
28265
- grad.addColorStop(0, SKY_GRADIENT_TOP);
28266
- grad.addColorStop(1, SKY_GRADIENT_BOTTOM);
28267
- ctx.fillStyle = grad;
28268
- ctx.fillRect(0, 0, canvasWidth, canvasHeight);
28269
- }
28270
- ctx.strokeStyle = GRID_COLOR;
28271
- ctx.lineWidth = 1;
28272
- const gridSize = 32;
28273
- for (let gx = -camX % gridSize; gx < canvasWidth; gx += gridSize) {
28274
- ctx.beginPath();
28275
- ctx.moveTo(gx, 0);
28276
- ctx.lineTo(gx, canvasHeight);
28277
- ctx.stroke();
28278
- }
28279
- for (let gy = -camY % gridSize; gy < canvasHeight; gy += gridSize) {
28280
- ctx.beginPath();
28281
- ctx.moveTo(0, gy);
28282
- ctx.lineTo(canvasWidth, gy);
28283
- ctx.stroke();
28284
- }
28285
- for (const plat of platforms) {
28286
- const px = plat.x - camX;
28287
- const py = plat.y - camY;
28288
- const platType = plat.type ?? "ground";
28289
- const spriteUrl = tileSprites?.[platType];
28290
- const tileImg = spriteUrl ? loadImage(spriteUrl) : null;
28291
- if (tileImg) {
28292
- const tileW = tileImg.naturalWidth;
28293
- const tileH = tileImg.naturalHeight;
28294
- const scaleH = plat.height / tileH;
28295
- const scaledW = tileW * scaleH;
28296
- for (let tx = 0; tx < plat.width; tx += scaledW) {
28297
- const drawW = Math.min(scaledW, plat.width - tx);
28298
- const srcW = drawW / scaleH;
28299
- ctx.drawImage(tileImg, 0, 0, srcW, tileH, px + tx, py, drawW, plat.height);
28300
- }
28358
+ }, [canvasWidth, canvasHeight]);
28359
+ useEffect(() => {
28360
+ const drawFrame = (positions) => {
28361
+ const canvas = canvasRef.current;
28362
+ if (!canvas) return;
28363
+ const ctx = canvas.getContext("2d");
28364
+ if (!ctx) return;
28365
+ const {
28366
+ platforms: plats,
28367
+ worldWidth: ww,
28368
+ worldHeight: wh,
28369
+ canvasWidth: cw,
28370
+ canvasHeight: ch,
28371
+ followCamera: fc,
28372
+ bgColor: bg,
28373
+ playerSprite: pSprite,
28374
+ tileSprites: tSprites,
28375
+ backgroundImage: bgImg
28376
+ } = propsRef.current;
28377
+ const auth = playerRef.current;
28378
+ const interped = positions.get("player");
28379
+ const px = interped?.x ?? auth.x;
28380
+ const py = interped?.y ?? auth.y;
28381
+ let camX = 0;
28382
+ let camY = 0;
28383
+ if (fc) {
28384
+ camX = Math.max(0, Math.min(px - cw / 2, ww - cw));
28385
+ camY = Math.max(0, Math.min(py - ch / 2 - 50, wh - ch));
28386
+ }
28387
+ const bgImage = bgImg ? loadImage(bgImg) : null;
28388
+ if (bgImage) {
28389
+ ctx.drawImage(bgImage, 0, 0, cw, ch);
28390
+ } else if (bg) {
28391
+ ctx.fillStyle = bg;
28392
+ ctx.fillRect(0, 0, cw, ch);
28301
28393
  } else {
28302
- const color = PLATFORM_COLORS[platType] ?? PLATFORM_COLORS.ground;
28303
- ctx.fillStyle = color;
28304
- ctx.fillRect(px, py, plat.width, plat.height);
28305
- ctx.fillStyle = "rgba(255, 255, 255, 0.15)";
28306
- ctx.fillRect(px, py, plat.width, 3);
28307
- ctx.fillStyle = "rgba(0, 0, 0, 0.3)";
28308
- ctx.fillRect(px, py + plat.height - 2, plat.width, 2);
28309
- if (platType === "hazard") {
28310
- ctx.strokeStyle = "#e74c3c";
28311
- ctx.lineWidth = 2;
28312
- for (let sx = px; sx < px + plat.width; sx += 12) {
28394
+ const grad = ctx.createLinearGradient(0, 0, 0, ch);
28395
+ grad.addColorStop(0, SKY_GRADIENT_TOP);
28396
+ grad.addColorStop(1, SKY_GRADIENT_BOTTOM);
28397
+ ctx.fillStyle = grad;
28398
+ ctx.fillRect(0, 0, cw, ch);
28399
+ }
28400
+ ctx.strokeStyle = GRID_COLOR;
28401
+ ctx.lineWidth = 1;
28402
+ const gridSize = 32;
28403
+ for (let gx = -camX % gridSize; gx < cw; gx += gridSize) {
28404
+ ctx.beginPath();
28405
+ ctx.moveTo(gx, 0);
28406
+ ctx.lineTo(gx, ch);
28407
+ ctx.stroke();
28408
+ }
28409
+ for (let gy = -camY % gridSize; gy < ch; gy += gridSize) {
28410
+ ctx.beginPath();
28411
+ ctx.moveTo(0, gy);
28412
+ ctx.lineTo(cw, gy);
28413
+ ctx.stroke();
28414
+ }
28415
+ for (const plat of plats) {
28416
+ const platX = plat.x - camX;
28417
+ const platY = plat.y - camY;
28418
+ const platType = plat.type ?? "ground";
28419
+ const spriteUrl = tSprites?.[platType];
28420
+ const tileImg = spriteUrl ? loadImage(spriteUrl) : null;
28421
+ if (tileImg) {
28422
+ const tileW = tileImg.naturalWidth;
28423
+ const tileH = tileImg.naturalHeight;
28424
+ const scaleH = plat.height / tileH;
28425
+ const scaledW = tileW * scaleH;
28426
+ for (let tx = 0; tx < plat.width; tx += scaledW) {
28427
+ const drawW = Math.min(scaledW, plat.width - tx);
28428
+ const srcW = drawW / scaleH;
28429
+ ctx.drawImage(tileImg, 0, 0, srcW, tileH, platX + tx, platY, drawW, plat.height);
28430
+ }
28431
+ } else {
28432
+ const color = PLATFORM_COLORS[platType] ?? PLATFORM_COLORS.ground;
28433
+ ctx.fillStyle = color;
28434
+ ctx.fillRect(platX, platY, plat.width, plat.height);
28435
+ ctx.fillStyle = "rgba(255, 255, 255, 0.15)";
28436
+ ctx.fillRect(platX, platY, plat.width, 3);
28437
+ ctx.fillStyle = "rgba(0, 0, 0, 0.3)";
28438
+ ctx.fillRect(platX, platY + plat.height - 2, plat.width, 2);
28439
+ if (platType === "hazard") {
28440
+ ctx.strokeStyle = "#e74c3c";
28441
+ ctx.lineWidth = 2;
28442
+ for (let sx = platX; sx < platX + plat.width; sx += 12) {
28443
+ ctx.beginPath();
28444
+ ctx.moveTo(sx, platY);
28445
+ ctx.lineTo(sx + 6, platY + plat.height);
28446
+ ctx.stroke();
28447
+ }
28448
+ }
28449
+ if (platType === "goal") {
28450
+ ctx.fillStyle = "rgba(241, 196, 15, 0.5)";
28313
28451
  ctx.beginPath();
28314
- ctx.moveTo(sx, py);
28315
- ctx.lineTo(sx + 6, py + plat.height);
28316
- ctx.stroke();
28452
+ ctx.arc(platX + plat.width / 2, platY - 10, 8, 0, Math.PI * 2);
28453
+ ctx.fill();
28317
28454
  }
28318
28455
  }
28319
- if (platType === "goal") {
28320
- ctx.fillStyle = "rgba(241, 196, 15, 0.5)";
28321
- ctx.beginPath();
28322
- ctx.arc(px + plat.width / 2, py - 10, 8, 0, Math.PI * 2);
28323
- ctx.fill();
28324
- }
28325
28456
  }
28326
- }
28327
- const pw = resolvedPlayer.width ?? 24;
28328
- const ph = resolvedPlayer.height ?? 32;
28329
- const ppx = resolvedPlayer.x - camX;
28330
- const ppy = resolvedPlayer.y - camY;
28331
- const facingRight = resolvedPlayer.facingRight ?? true;
28332
- const playerImg = playerSprite ? loadImage(playerSprite) : null;
28333
- if (playerImg) {
28334
- ctx.save();
28335
- if (!facingRight) {
28336
- ctx.translate(ppx + pw, ppy);
28337
- ctx.scale(-1, 1);
28338
- ctx.drawImage(playerImg, 0, 0, pw, ph);
28457
+ const pw = auth.width ?? 24;
28458
+ const ph = auth.height ?? 32;
28459
+ const ppx = px - camX;
28460
+ const ppy = py - camY;
28461
+ const facingRight = auth.facingRight ?? true;
28462
+ const playerImg = pSprite ? loadImage(pSprite) : null;
28463
+ if (playerImg) {
28464
+ ctx.save();
28465
+ if (!facingRight) {
28466
+ ctx.translate(ppx + pw, ppy);
28467
+ ctx.scale(-1, 1);
28468
+ ctx.drawImage(playerImg, 0, 0, pw, ph);
28469
+ } else {
28470
+ ctx.drawImage(playerImg, ppx, ppy, pw, ph);
28471
+ }
28472
+ ctx.restore();
28339
28473
  } else {
28340
- ctx.drawImage(playerImg, ppx, ppy, pw, ph);
28474
+ ctx.fillStyle = PLAYER_COLOR;
28475
+ const radius = Math.min(pw, ph) * 0.25;
28476
+ ctx.beginPath();
28477
+ ctx.moveTo(ppx + radius, ppy);
28478
+ ctx.lineTo(ppx + pw - radius, ppy);
28479
+ ctx.quadraticCurveTo(ppx + pw, ppy, ppx + pw, ppy + radius);
28480
+ ctx.lineTo(ppx + pw, ppy + ph - radius);
28481
+ ctx.quadraticCurveTo(ppx + pw, ppy + ph, ppx + pw - radius, ppy + ph);
28482
+ ctx.lineTo(ppx + radius, ppy + ph);
28483
+ ctx.quadraticCurveTo(ppx, ppy + ph, ppx, ppy + ph - radius);
28484
+ ctx.lineTo(ppx, ppy + radius);
28485
+ ctx.quadraticCurveTo(ppx, ppy, ppx + radius, ppy);
28486
+ ctx.fill();
28487
+ const eyeY = ppy + ph * 0.3;
28488
+ const eyeSize = 3;
28489
+ const eyeOffsetX = facingRight ? pw * 0.55 : pw * 0.2;
28490
+ ctx.fillStyle = PLAYER_EYE_COLOR;
28491
+ ctx.beginPath();
28492
+ ctx.arc(ppx + eyeOffsetX, eyeY, eyeSize, 0, Math.PI * 2);
28493
+ ctx.fill();
28494
+ ctx.beginPath();
28495
+ ctx.arc(ppx + eyeOffsetX + 7, eyeY, eyeSize, 0, Math.PI * 2);
28496
+ ctx.fill();
28341
28497
  }
28342
- ctx.restore();
28343
- } else {
28344
- ctx.fillStyle = PLAYER_COLOR;
28345
- const radius = Math.min(pw, ph) * 0.25;
28346
- ctx.beginPath();
28347
- ctx.moveTo(ppx + radius, ppy);
28348
- ctx.lineTo(ppx + pw - radius, ppy);
28349
- ctx.quadraticCurveTo(ppx + pw, ppy, ppx + pw, ppy + radius);
28350
- ctx.lineTo(ppx + pw, ppy + ph - radius);
28351
- ctx.quadraticCurveTo(ppx + pw, ppy + ph, ppx + pw - radius, ppy + ph);
28352
- ctx.lineTo(ppx + radius, ppy + ph);
28353
- ctx.quadraticCurveTo(ppx, ppy + ph, ppx, ppy + ph - radius);
28354
- ctx.lineTo(ppx, ppy + radius);
28355
- ctx.quadraticCurveTo(ppx, ppy, ppx + radius, ppy);
28356
- ctx.fill();
28357
- const eyeY = ppy + ph * 0.3;
28358
- const eyeSize = 3;
28359
- const eyeOffsetX = facingRight ? pw * 0.55 : pw * 0.2;
28360
- ctx.fillStyle = PLAYER_EYE_COLOR;
28361
- ctx.beginPath();
28362
- ctx.arc(ppx + eyeOffsetX, eyeY, eyeSize, 0, Math.PI * 2);
28363
- ctx.fill();
28364
- ctx.beginPath();
28365
- ctx.arc(ppx + eyeOffsetX + 7, eyeY, eyeSize, 0, Math.PI * 2);
28366
- ctx.fill();
28367
- }
28368
- }, [player, platforms, worldWidth, worldHeight, canvasWidth, canvasHeight, followCamera, bgColor, playerSprite, tileSprites, backgroundImage, assetBaseUrl, loadedImages]);
28498
+ };
28499
+ return interp.startLoop(drawFrame);
28500
+ }, [interp.startLoop, loadImage]);
28369
28501
  return /* @__PURE__ */ jsx(
28370
28502
  "canvas",
28371
28503
  {
@@ -28383,6 +28515,7 @@ var init_PlatformerCanvas = __esm({
28383
28515
  init_cn();
28384
28516
  init_useEventBus();
28385
28517
  init_verificationRegistry();
28518
+ init_useRenderInterpolation();
28386
28519
  PLATFORM_COLORS = {
28387
28520
  ground: "#4a7c59",
28388
28521
  platform: "#7c6b4a",
@@ -28765,13 +28898,13 @@ var init_MapView = __esm({
28765
28898
  shadowSize: [41, 41]
28766
28899
  });
28767
28900
  L.Marker.prototype.options.icon = defaultIcon;
28768
- const { useEffect: useEffect74, useRef: useRef68, useCallback: useCallback111, useState: useState107 } = React82__default;
28901
+ const { useEffect: useEffect75, useRef: useRef69, useCallback: useCallback112, useState: useState107 } = React82__default;
28769
28902
  const { Typography: Typography2 } = await Promise.resolve().then(() => (init_Typography(), Typography_exports));
28770
28903
  const { useEventBus: useEventBus3 } = await Promise.resolve().then(() => (init_useEventBus(), useEventBus_exports));
28771
28904
  function MapUpdater({ centerLat, centerLng, zoom }) {
28772
28905
  const map = useMap();
28773
- const prevRef = useRef68({ centerLat, centerLng, zoom });
28774
- useEffect74(() => {
28906
+ const prevRef = useRef69({ centerLat, centerLng, zoom });
28907
+ useEffect75(() => {
28775
28908
  const prev = prevRef.current;
28776
28909
  if (prev.centerLat !== centerLat || prev.centerLng !== centerLng || prev.zoom !== zoom) {
28777
28910
  map.setView([centerLat, centerLng], zoom);
@@ -28782,7 +28915,7 @@ var init_MapView = __esm({
28782
28915
  }
28783
28916
  function MapClickHandler({ onMapClick }) {
28784
28917
  const map = useMap();
28785
- useEffect74(() => {
28918
+ useEffect75(() => {
28786
28919
  if (!onMapClick) return;
28787
28920
  const handler = (e) => {
28788
28921
  onMapClick(e.latlng.lat, e.latlng.lng);
@@ -28811,7 +28944,7 @@ var init_MapView = __esm({
28811
28944
  }) {
28812
28945
  const eventBus = useEventBus3();
28813
28946
  const [clickedPosition, setClickedPosition] = useState107(null);
28814
- const handleMapClick = useCallback111((lat, lng) => {
28947
+ const handleMapClick = useCallback112((lat, lng) => {
28815
28948
  if (showClickedPin) {
28816
28949
  setClickedPosition({ lat, lng });
28817
28950
  }
@@ -28820,7 +28953,7 @@ var init_MapView = __esm({
28820
28953
  eventBus.emit(`UI:${mapClickEvent}`, { latitude: lat, longitude: lng });
28821
28954
  }
28822
28955
  }, [onMapClick, mapClickEvent, eventBus, showClickedPin]);
28823
- const handleMarkerClick = useCallback111((marker) => {
28956
+ const handleMarkerClick = useCallback112((marker) => {
28824
28957
  onMarkerClick?.(marker);
28825
28958
  if (markerClickEvent) {
28826
28959
  eventBus.emit(`UI:${markerClickEvent}`, { ...marker });
@@ -44324,6 +44457,10 @@ function SequencerBoard({
44324
44457
  stepDurationMs = 1e3,
44325
44458
  playEvent,
44326
44459
  completeEvent,
44460
+ placeEvent,
44461
+ removeEvent,
44462
+ checkEvent,
44463
+ playAgainEvent,
44327
44464
  className
44328
44465
  }) {
44329
44466
  const { emit } = useEventBus();
@@ -44359,7 +44496,8 @@ function SequencerBoard({
44359
44496
  return next;
44360
44497
  });
44361
44498
  emit("UI:PLAY_SOUND", { key: "drop_slot" });
44362
- }, [emit]);
44499
+ if (placeEvent) emit(`UI:${placeEvent}`, { slotIndex: index, actionId: item.id });
44500
+ }, [emit, placeEvent]);
44363
44501
  const handleSlotRemove = useCallback((index) => {
44364
44502
  setSlots((prev) => {
44365
44503
  const next = [...prev];
@@ -44372,7 +44510,8 @@ function SequencerBoard({
44372
44510
  return next;
44373
44511
  });
44374
44512
  emit("UI:PLAY_SOUND", { key: "back" });
44375
- }, [emit]);
44513
+ if (removeEvent) emit(`UI:${removeEvent}`, { slotIndex: index });
44514
+ }, [emit, removeEvent]);
44376
44515
  const handleReset = useCallback(() => {
44377
44516
  if (timerRef.current) clearTimeout(timerRef.current);
44378
44517
  setSlots(Array.from({ length: maxSlots }, () => void 0));
@@ -44380,7 +44519,8 @@ function SequencerBoard({
44380
44519
  setCurrentStep(-1);
44381
44520
  setAttempts(0);
44382
44521
  setSlotFeedback(Array.from({ length: maxSlots }, () => null));
44383
- }, [maxSlots]);
44522
+ if (playAgainEvent) emit(`UI:${playAgainEvent}`, {});
44523
+ }, [maxSlots, playAgainEvent, emit]);
44384
44524
  const filledSlots = slots.filter((s) => !!s);
44385
44525
  const canPlay = filledSlots.length > 0 && playState === "idle";
44386
44526
  const handlePlay = useCallback(() => {
@@ -44402,6 +44542,7 @@ function SequencerBoard({
44402
44542
  const success = solutions.some(
44403
44543
  (sol) => sol.length === playerIds.length && sol.every((id, i) => id === playerIds[i])
44404
44544
  );
44545
+ if (checkEvent) emit(`UI:${checkEvent}`, { sequence: playerIds });
44405
44546
  if (success) {
44406
44547
  setPlayState("success");
44407
44548
  setCurrentStep(-1);
@@ -44429,7 +44570,7 @@ function SequencerBoard({
44429
44570
  }
44430
44571
  };
44431
44572
  timerRef.current = setTimeout(advance, stepDurationMs);
44432
- }, [canPlay, slots, maxSlots, solutions, stepDurationMs, playEvent, completeEvent, emit]);
44573
+ }, [canPlay, slots, maxSlots, solutions, stepDurationMs, playEvent, completeEvent, checkEvent, emit]);
44433
44574
  const machine = {
44434
44575
  name: str(resolved?.title),
44435
44576
  description: str(resolved?.description),
@@ -44684,11 +44825,13 @@ function SimulationCanvas({
44684
44825
  height = 400,
44685
44826
  running,
44686
44827
  speed = 1,
44828
+ bodies: externalBodies,
44687
44829
  className
44688
44830
  }) {
44689
44831
  const preset = useMemo(() => resolvePreset(presetProp), [presetProp]);
44690
44832
  const canvasRef = useRef(null);
44691
44833
  const bodiesRef = useRef(structuredClone(preset.bodies));
44834
+ const interp = useRenderInterpolation();
44692
44835
  useEffect(() => {
44693
44836
  bodiesRef.current = structuredClone(preset.bodies);
44694
44837
  }, [preset]);
@@ -44786,6 +44929,67 @@ function SimulationCanvas({
44786
44929
  }
44787
44930
  }, [width, height, preset]);
44788
44931
  useEffect(() => {
44932
+ if (!externalBodies) return;
44933
+ interp.onSnapshot(externalBodies.map((b) => ({ id: b.id, x: b.x, y: b.y })));
44934
+ }, [externalBodies]);
44935
+ const presetRef = useRef(preset);
44936
+ presetRef.current = preset;
44937
+ const widthRef = useRef(width);
44938
+ widthRef.current = width;
44939
+ const heightRef = useRef(height);
44940
+ heightRef.current = height;
44941
+ useEffect(() => {
44942
+ if (!externalBodies) return;
44943
+ const drawInterpolated = (positions) => {
44944
+ const canvas = canvasRef.current;
44945
+ if (!canvas) return;
44946
+ const ctx = canvas.getContext("2d");
44947
+ if (!ctx) return;
44948
+ const bodies = bodiesRef.current;
44949
+ const p2 = presetRef.current;
44950
+ const w = widthRef.current;
44951
+ const h = heightRef.current;
44952
+ ctx.clearRect(0, 0, w, h);
44953
+ ctx.fillStyle = p2.backgroundColor ?? "#1a1a2e";
44954
+ ctx.fillRect(0, 0, w, h);
44955
+ if (p2.constraints) {
44956
+ for (const c of p2.constraints) {
44957
+ const a = bodies[c.bodyA];
44958
+ const b = bodies[c.bodyB];
44959
+ if (a && b) {
44960
+ const aPos = positions.get(a.id) ?? a;
44961
+ const bPos = positions.get(b.id) ?? b;
44962
+ ctx.beginPath();
44963
+ ctx.moveTo(aPos.x, aPos.y);
44964
+ ctx.lineTo(bPos.x, bPos.y);
44965
+ ctx.strokeStyle = "#533483";
44966
+ ctx.lineWidth = 1;
44967
+ ctx.setLineDash([4, 4]);
44968
+ ctx.stroke();
44969
+ ctx.setLineDash([]);
44970
+ }
44971
+ }
44972
+ }
44973
+ for (const body of bodies) {
44974
+ const pos = positions.get(body.id) ?? body;
44975
+ ctx.beginPath();
44976
+ ctx.arc(pos.x, pos.y, body.radius, 0, Math.PI * 2);
44977
+ ctx.fillStyle = body.color ?? "#e94560";
44978
+ ctx.fill();
44979
+ if (p2.showVelocity) {
44980
+ ctx.beginPath();
44981
+ ctx.moveTo(pos.x, pos.y);
44982
+ ctx.lineTo(pos.x + body.vx * 0.1, pos.y + body.vy * 0.1);
44983
+ ctx.strokeStyle = "#16213e";
44984
+ ctx.lineWidth = 2;
44985
+ ctx.stroke();
44986
+ }
44987
+ }
44988
+ };
44989
+ return interp.startLoop(drawInterpolated);
44990
+ }, [externalBodies !== void 0, interp.startLoop]);
44991
+ useEffect(() => {
44992
+ if (externalBodies !== void 0) return;
44789
44993
  if (!running) return;
44790
44994
  let raf;
44791
44995
  const loop = () => {
@@ -44795,10 +44999,11 @@ function SimulationCanvas({
44795
44999
  };
44796
45000
  raf = requestAnimationFrame(loop);
44797
45001
  return () => cancelAnimationFrame(raf);
44798
- }, [running, step, draw]);
45002
+ }, [running, step, draw, externalBodies]);
44799
45003
  useEffect(() => {
45004
+ if (externalBodies !== void 0) return;
44800
45005
  draw();
44801
- }, [draw]);
45006
+ }, [draw, externalBodies]);
44802
45007
  useEffect(() => {
44803
45008
  if (typeof window === "undefined") return;
44804
45009
  const canvas = canvasRef.current;
@@ -44824,6 +45029,7 @@ var init_SimulationCanvas = __esm({
44824
45029
  init_atoms2();
44825
45030
  init_verificationRegistry();
44826
45031
  init_presets();
45032
+ init_useRenderInterpolation();
44827
45033
  SimulationCanvas.displayName = "SimulationCanvas";
44828
45034
  }
44829
45035
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@almadar/ui",
3
- "version": "5.41.0",
3
+ "version": "5.43.0",
4
4
  "description": "React UI components, hooks, and providers for Almadar",
5
5
  "type": "module",
6
6
  "sideEffects": [