🏠 back to Observable

Is it possible to read a cell from a generator without becoming "reactive" to that cell?


#1

I’m working on a notebook right now where I would like to do something like the following:

speed = viewof html`<input min="0" max="10">` // units per second
position = {
  let position = 0;
  let lastUpdated = performance.now();
  while (true) {
    const currentTime = performance.now();
    // it's important that the simulation run at an accurate multiple of
    // "real time", hence why I'm computing the amount of time passed
    // rather than just assuming the loop is running at 60 FPS.
    position += speed * (currentTime - lastUpdated) / 1000;
    lastUpdated = currentTime;
    yield position;
  }
}
car = {
  let car = this;

  if (!car) {
    car = new THREE.Mesh();
    // ... additional setup omitted ...
    scene.add(car);
  }

  car.position.x = position;
}

Then, I’m rendering the scene to a canvas in a loop, like this:

display = {
  const renderer = new THREE.WebGLRenderer({antialias: true});

  try {
    while (true) {
      renderer.render(scene, camera);
      yield renderer.domElement;
    }
  } finally {
    renderer.dispose();
  }
}

This works fine. The loop isn’t reactive to the car, but because we attach the car to the scene and then mutate its position, the loop ends up seeing new values for the car’s position each time it runs.

The problem with this setup is that when the user moves the speed slider, the car’s position resets. I’d like the position to be preserved.

I’ve found a workaround, which is to do something like this:

mutable position = 0
{
  let lastUpdated = performance.now()
  while (true) {
    const currentTime = performance.now();
    mutable position += speed * (currentTime - lastUpdated) / 1000;
    lastUpdated = currentTime;
    await Promises.delay(1000 / 60);
  }
}

This seems to solve the problem, since position is no longer reactive to speed. The anonymous cell which is updating position gets rerun whenever speed changes, but this doesn’t reset the position itself.

However, this seems ugly to me. Expressing position as a generator seems more natural, but this would require somehow fetching the value of speed within the generator’s loop without making the generator cell reactive to changes in speed.

Is there a recommended pattern for dealing with these sorts of issues? I have a dim sense that a custom view (as demonstrated in @mbostock/synchronized-views) might solve my problem, but I don’t understand them in enough depth to really know.

Thanks,
Jake


#2

You can reference the a view’s value non-reactively inside your loop:

{
  let s = viewof speed.valueAsNumber;
}

Related tutorial:


#3

Thanks for the reply! I read the notebook you linked. Using viewof x.value looks promising, but I just realized I omitted part of the problem. My notebook actually looks more like this:

speedInMilesPerHour = viewof html`<input min="0" max="10">`
speedInMetersPerSecond = speedInMilesPerHour * 0.44704
position = {
  let position = 0;
  let lastUpdated = performance.now();
  while (true) {
    const currentTime = performance.now();
    position += speedInMetersPerSecond * (currentTime - lastUpdated) / 1000;
    lastUpdated = currentTime;
    yield position;
  }
}

Is there any way to get the value of speedInMetersPerSecond non-reactively?


#4

You have to switch to mutable values (views, mutables, or objects) if you want to circumvent reactivity.

I’m not sure I totally understand your use case—like, why not just have an input that specifies the speed in meters per second, if that’s what you want to use?

Alternatively, you could have a function (or a constant) that to convert units.

function mph2mps(value) {
  return value * 0.44704;
}
{
  let s = mph2mps(viewof speed.valueAsNumber);
}

Or

mps2mph = 0.44704
{
  let s = mph2mps * viewof speed.valueAsNumber;
}

Here’s how you’d do it with a mutable… First define your view:

speedInMilesPerHour = viewof html`<input min="0" max="10">`

Then define a mutable to store the derived value:

mutable speedInMetersPerSecond = undefined

Then an anonymous cell to read from the view (reactively) and write to the mutable:

(mutable speedInMetersPerSecond = speedInMilesPerHour * 0.44704)

The parens mean this is an expression cell rather than a duplicate definition of mutable speedInMetersPerSecond; you could use curly braces if you prefer.

Then you can read the speed non-reactively:

{
  let s = mutable speedInMetersPerSecond;
}

#5

Thank you for writing up those examples, they ended up helping me find a solution. Just wanted to follow up.

I’m not sure I totally understand your use case—like, why not just have an input that specifies the speed in meters per second, if that’s what you want to use?

Sorry, I should have made more effort to describe my actual use case instead of distilling it down to something simpler (and less representative). In the notebook I was working on (which is now finished), the input was changing orbital altitude, and speed was computed based on that altitude, something like this:

viewof satelliteAltitude = slider({
  min: 300 * 1000,
  max: 35793 * 1000,
  value: 834 * 1000,
  step: 1000
})
satelliteVelocity = Math.sqrt(G * earthMass / (earthRadius + satelliteAltitude)) // meters per second
satelliteAngularVelocity = satelliteVelocity / (earthRadius + satelliteAltitude) // radians per second

My original goal was to compute position using satelliteAngularVelocity like this:

satelliteAngularPosition = {
  let angle = 0;
  let lastUpdated = performance.now();
  while (true) {
    const currentTime = performance.now();
    angle += satelliteAngularVelocity * (currentTime - lastUpdated) / 1000;
    lastUpdated = currentTime;
    yield angle;
  }
}

But as I mentioned, I didn’t want this generator to reset to zero when satelliteAngularVelocity changed (which would happen whenever the user moved the satelliteAltitude input).

Your comment helped me find a solution that I like – in particular, your observation that objects are mutable and assigning a value to a key in an object doesn’t trigger cells that depend on that object to re-run.

So instead of computing satelliteAngularPosition and then updating my Three.js Object3d by reacting to that cell, I went with this:

satellite = {
   const satellite = new THREE.Mesh();
   // ... more setup of the satellite object (omitted) ...
   return satellite;
}
{
  let lastUpdated = performance.now();
  while (true) {
    const currentTime = performance.now();
    satellite.rotation.y += satelliteAngularVelocity * (currentTime - lastUpdated) / 1000;
    lastUpdated = currentTime;
    yield;
  }
}

The anonymous generator above will still be restarted whenever satelliteAngularVelocity changes, but now it doesn’t matter because the current angular position is stored in the satellite Three.js object itself, which isn’t reactive to changes in any of the input sliders. There are other sliders, and other objects in the scene, but by following this pattern any slider can be changed by the user without resetting any part of the simulation to its initial state.

Anyway, just wanted to say thanks for your suggestions; I really appreciate the detailed examples you provided.