Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

An example of exposing imperative methods from the sketch? #258

Closed
neongreen opened this issue Jul 7, 2023 · 9 comments
Closed

An example of exposing imperative methods from the sketch? #258

neongreen opened this issue Jul 7, 2023 · 9 comments

Comments

@neongreen
Copy link

I am writing a game with p5 and I want to expose methods like “resetGame()” or “getPosition()” etc from the sketch. The methods are modifying/querying internal sketch state that React doesn’t know about.

I’m not sure how to describe my issue. Just everything that I’ve tried so far fails in some random way.

If you have an example of how to expose methods in a non-hacky way, that would help me a lot.

@jamesrweb
Copy link
Collaborator

I don't understand what you mean exactly by "expose methods". Could you provide an example implementation of what you have in mind?

@neongreen
Copy link
Author

neongreen commented Jul 8, 2023

Sure.

Sketch:

// sketch.ts

type Methods = {
  resetGame: () => void,
  getPosition: () => number,
}

export function sketch(p5): Methods = {
  ...p5 code...
  return {
    resetGame: () => ...
    getPosition: () => ...
  }

React:

// index.tsx

const game = (p5) => {
  const methods = sketch(p5)
  window.methods = methods
}

function Page() {
  return (
    <>
      <button onClick={window.methods.reset}>Reset</button>
      ...
      <NextReactP5Wrapper sketch={game} />
    </>
  )
}

Now I can call window.methods.[whatever] to manipulate the sketch.

For sure, I can also achieve sketch control by using props and p5.updateWithProps. But specifically for things like reset(), I think it's nicer to have an imperative handle instead of [resetFlag, setResetFlag] = useState(false) or whatever would be the React way of doing it.

See also: useImperativeHandle() is very close to what I want.

@jamesrweb
Copy link
Collaborator

Sure.

Sketch:

// sketch.ts



type Methods = {

  resetGame: () => void,

  getPosition: () => number,

}



export function sketch(p5): Methods = {

  ...p5 code...

  return {

    resetGame: () => ...

    getPosition: () => ...

  }

React:

// index.tsx



const game = (p5) => {

  const methods = sketch(p5)

  window.methods = methods

}



function Page() {

  return (

    <>

      <button onClick={window.methods.reset}>Reset</button>

      ...

      <NextReactP5Wrapper sketch={game} />

    </>

  )

}

Now I can call window.methods.[whatever] to manipulate the sketch.

For sure, I can also achieve sketch control by using props and p5.updateWithProps. But specifically for things like reset(), I think it's nicer to have an imperative handle instead of [resetFlag, setResetFlag] = useState(false) or whatever would be the React way of doing it.

See also: useImperativeHandle() is very close to what I want.

I'm not sure this brings any benefits that composition couldn't already give you. For example when we want to have a draw function, we create it in the global scope and pass the p5 instance through to it as shown in the documentation.

I also don't see a use case where you would want to return such functions from the sketch itself, perhaps you can elaborate on this?

The useImperativeHandle hook brings a lot of overhead at runtime and is warned against in the docs themselves:

Do not overuse refs. You should only use refs for imperative behaviors that you can’t express as props: for example, scrolling to a node, focusing a node, triggering an animation, selecting text, and so on.

If you can express something as a prop, you should not use a ref. For example, instead of exposing an imperative handle like { open, close } from a Modal component, it is better to take isOpen as a prop like . Effects can help you expose imperative behaviors via props.

I think generally what I see from your example code is that we could have a reset function for example in the global scope which receives the p5 instance and the current game state and can then return the new game state accordingly.

This would be much cleaner and more predictable in terms of behaviour and readability.

Perhaps I missed your point and if so, I would love to hear more so I can help further but it seems to me that composition over inheritance would be the principle to go with here as things stand. What do you think? 😊

@neongreen
Copy link
Author

I also don't see a use case where you would want to return such functions from the sketch itself, perhaps you can elaborate on this?

I'll try to elaborate.

  • I have a game written as a p5 sketch. The game has some kind of internal state.
  • I want to continue to use p5 to display the game canvas itself (because p5 is good at drawing).
  • However, I also want to use React for the rest of the interface (because React is good at UIs).

This means that if I want to bind some in-game actions to React elements (buttons, etc), I have two options as far as I see:

  1. I can continue storing my in-game state in p5. I will also construct and return some kind of a "handle" in my p5 code. This handle will contain a bunch of methods (in-game actions) that React can call. Think of an HTML5 <video> element having methods like play() and pause().

  2. Alternatively, I can move all my state to React. Then the sketch will just purely/declaratively display this state. All state modifications will happen in React.

I don't see these options as "composition vs inheritance", but rather "who manages the state" — React or p5. I want the state to be managed by p5. I understand that React prefers controlled components and moving all state into React, but I don't want to move all my game state into React just because I want to have a button that asks my game to do something.

I also don't mind the runtime overhead (I think?).

@jamesrweb
Copy link
Collaborator

jamesrweb commented Aug 13, 2023

Can you provide a minimal code example of how you'd propose this would work? I know you provided one above but due to how p5 itself works, I don't see this is possible with instance mode without rewiring the internals or factory functioning the implementation. Even then, I am still concerned about the runtime cost and the developer experience of implementing things this way.

It would be to open a PR, if you're willing to create one, to discuss this more concretely on an implementation level as to what you're hoping for.

@neongreen
Copy link
Author

I don't know how to make this work.. but I also wouldn't expect myself to know, given that I'm not an advanced user of either p5 or React.

due to how p5 itself works, I don't see this is possible with instance mode without rewiring the internals or factory functioning the implementation.

Hmmm. Can you elaborate?

@jamesrweb
Copy link
Collaborator

P5 has two modes: instance mode and global mode.

Global mode is where you have all the P5 functions, etc in the global scope like most p5 projects have.

Instance mode is where you create a new instance of the P5 class and then use the functions (methods) on that class instance. In the case of libraries such as this one, instance mode is the only and best option!

In instance mode you need to provide a sketch function which will recieve the p5 instance and this is exactly what you are passing into the component exposed from this library. In turn you then use the methods on that provided p5 object such as p5.setup, etc.

That function cannot return arbitrary functions to you as the user as it is not intended for that purpose on a p5 level. You can however create a factory to generate your desired functions and to return your sketch if you really must, something like:

function someFactory() {
  let var1 = 0;
  let var2 = "test";

  return {
    func1() {...},
    func3() {...},
    game(p5) { 
      p5.updateWithProps = props => {
         var1 = props.whatever;
      } 
    }
  }
}

const { func1, func3, game } = someFactory();

...
 <NextReactP5Wrapper sketch={game} />
...

Something like that is what I assume you are looking for and it's the closest you can get to what you seem to be describing. Library wise there is really nothing we could change based on my current understanding of your wishes, it would need to be done on your end with something like the above.

@jamesrweb
Copy link
Collaborator

Closing due to inactivity. Please reply to re-open if necessary.

@neongreen
Copy link
Author

Yeah, that's fair. If there's no easy way then sure

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants