@almadar/ui 5.42.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 });
@@ -44738,11 +44871,13 @@ function SimulationCanvas({
44738
44871
  height = 400,
44739
44872
  running,
44740
44873
  speed = 1,
44874
+ bodies: externalBodies,
44741
44875
  className
44742
44876
  }) {
44743
44877
  const preset = React82.useMemo(() => resolvePreset(presetProp), [presetProp]);
44744
44878
  const canvasRef = React82.useRef(null);
44745
44879
  const bodiesRef = React82.useRef(structuredClone(preset.bodies));
44880
+ const interp = useRenderInterpolation();
44746
44881
  React82.useEffect(() => {
44747
44882
  bodiesRef.current = structuredClone(preset.bodies);
44748
44883
  }, [preset]);
@@ -44840,6 +44975,67 @@ function SimulationCanvas({
44840
44975
  }
44841
44976
  }, [width, height, preset]);
44842
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;
44843
45039
  if (!running) return;
44844
45040
  let raf;
44845
45041
  const loop = () => {
@@ -44849,10 +45045,11 @@ function SimulationCanvas({
44849
45045
  };
44850
45046
  raf = requestAnimationFrame(loop);
44851
45047
  return () => cancelAnimationFrame(raf);
44852
- }, [running, step, draw]);
45048
+ }, [running, step, draw, externalBodies]);
44853
45049
  React82.useEffect(() => {
45050
+ if (externalBodies !== void 0) return;
44854
45051
  draw();
44855
- }, [draw]);
45052
+ }, [draw, externalBodies]);
44856
45053
  React82.useEffect(() => {
44857
45054
  if (typeof window === "undefined") return;
44858
45055
  const canvas = canvasRef.current;
@@ -44878,6 +45075,7 @@ var init_SimulationCanvas = __esm({
44878
45075
  init_atoms2();
44879
45076
  init_verificationRegistry();
44880
45077
  init_presets();
45078
+ init_useRenderInterpolation();
44881
45079
  SimulationCanvas.displayName = "SimulationCanvas";
44882
45080
  }
44883
45081
  });