pixijs / pixi-react

Write PIXI apps using React declarative style
https://pixijs.io/pixi-react/
MIT License
2.36k stars 179 forks source link

Pixi React v8 #493

Open trezy opened 3 months ago

trezy commented 3 months ago

Overview

This issue will track progress on version 8 of Pixi React. This new major version is a complete rewrite of the library and will support Pixi.js v8.

Thesis

The v7 codebase of Pixi React has served its purpose, but it has become burdensome to maintain. Especially with the release of Pixi.js v8 and the significant number of breaking changes it introduced, it makes more sense to rebuild this library from scratch than to continue supporting the legacy codebase.

For this complete rewrite, we'll take a new approach to the library's implementation by introducing a custom React Pixi pragma, an extend API, and reflecting all Pixi components as React components. This rewrite is heavily influenced by the prior art of @react-three/fiber, and I've been receiving significant help from @CodyJasonBennett and @krispya.

A new pragma

React allows custom reconcilers to tap into its re/rendering logic. React Pixi has been using a custom reconciler for some time, but the library provided custom components that used this reconciler to manage their own rendering lifecycle. These components need to be maintained, and their logic could require changes depending on how the core Pixi.js library changed.

With a new JSX pragma, we can eliminate the need for custom components. Instead, we reflect Pixi.js components directly into the pragma and proxy their JSX props back as Pixi.js component props. This allows us to expose all Pixi.js components now and in the future with no changes to React Pixi.

The extend API

To provide all components as a pragma would typically require us to import the entirety of Pixi.js into the Pixi React library, making tree shaking difficult and significantly increasing the build size for anybody trying to use Pixi.js v8. Instead, we're leveraging an extend API allowing users to import only the Pixi.js APIs they require, thus cutting down on bundle sizes.

The API will be available as both a Vanilla extend method and a useExtend React hook. An example of what this will look like:

import { 
  Canvas,
  useExtend,
} from '@pixi/react'
import { Sprite } from 'pixi.js'

export function App() {
  useExtend({ Sprite })

  return (
    <main>
      <Canvas>
        <sprite x={0} y={0} texture={texture} />
      </Canvas>
    </main>
  )
}

Exposing all of Pixi.js

Combining the new pragma and the extend API, we can use the entirety of Pixi.js via JSX. Any properties that you would normally set directly on a Pixi.js component will be managed via JSX props, while the components will be exposed directly through their refs. This enables lots of creative use cases, e.g. defining Pixi.js Filters in JSX...

import { 
  useEffect,
  useRef,
  useState,
} from 'react'

export function App() {
  const [filters, setFilters] = useState([])

  const filterRef = useRef(null)

  useEffect(() => {
    setFilters(previousState => {
      const filter = filterRef.current

      if (!previousState.includes(filter)) {
        return [
          ...previousState,
          filterRef.current,
        ]
      }

      return previousState
    })
  }, [])

  return (
    <>
      <blurFilter 
        ref={filterRef}
        quality={10}
        strength={10} />
      <sprite 
        filters={filters}
        texture={texture}
        x={0} 
        y={0} />
    </>
  )
}

More intuitive support down the road

The BlurFilter implementation above is a great example of something I've already got an eye towards improving. I'd like to create an attach API similar to that of @react-three/fiber, allowing non-directly-rendered components (like filters, textures, etc) to be automatically attached to their parent components. I'd also like to add support for creating Text components with normal JSX text nodes.

In short, I'd like to make the library as intuitive as possible. I'll add basic documentation for React Pixi, but the hope is that most APIs will be best served by the core Pixi.js documentation.

Development process

I've been using the dev branch as a sort of test bed for this update. Every update that's pushed to the dev branch of the repo will be deployed to the dev tag on npm. That said, these builds are not stable. Many of them will be completely broken until we're in a more stable place with the update (hopefully soon!). I do not recommend installing from the dev tag. You have been warned.

Once we're ready for user feedback, I'll post to the Official Pixi.js Discord. Make sure to join and enable notifications if you want to know when new versions are ready for testing. 😁

### Tasks
- [x] Add support for event handlers
- [x] Create the `<Application />` component
- [x] Fix types for nested components
- [x] Fix types for `useExtend` hook
- [x] Restore the `useApp` hook
- [x] Restore the `useTick` hook
- [x] Add support for prefixed components
- [x] Update "Getting Started" documentation
- [x] Write documentation for the `<Application />` component
- [x] Write documentation for the `extend` function
- [x] Write documentation for the `useExtend` hook
- [x] Write documentation for the `useAsset` hook
- [ ] https://github.com/pixijs/pixi-react/issues/494
- [ ] https://github.com/pixijs/pixi-react/issues/495
- [ ] https://github.com/pixijs/pixi-react/issues/499
- [ ] https://github.com/pixijs/pixi-react/issues/500
- [ ] https://github.com/pixijs/pixi-react/issues/506
- [ ] https://github.com/pixijs/pixi-react/issues/501
- [ ] https://github.com/pixijs/pixi-react/issues/511
- [ ] https://github.com/pixijs/pixi-react/issues/514
Nantris commented 3 months ago

Thanks for your work on this @trezy!

I wonder how much the pixi/react API night change? I know the Pixi 8 API changes a bit, but I'm wondering if the next version of pixi/react is expected to have its own set of API changes on top of that?

trezy commented 3 months ago

@Nantris: Yeah, the new API for Pixi React will be subtly different from v7. For example, here's a basic application with v7 vs the same application with v8:

// Pixi React v7
import { Container, Sprite, Stage, Text } from '@pixi/react';

export const MyComponent = () => {
  return (
    <Stage options={{ background: 0xffffff }}>
      <Sprite
        anchor={{ x: 0.5, y: 0.5 }}
        image="https://pixijs.io/pixi-react/img/bunny.png"
        x={400}
        y={270} />

      <Container x={400} y={330}>
        <Text 
          anchor={{ x: 0.5, y: 0.5 }} 
          text="Hello World" />
      </Container>
    </Stage>
  );
};
// Pixi React v8
import { Container, Sprite, Text } from 'pixi.js'
import { Application, useAsset, useExtend } from '@pixi/react';

export const MyComponent = () => {
  useExtend({ Container, Sprite, Text })

  const texture = useAsset({ src: 'https://pixijs.com/assets/bunny.png' })

  return (
    <Application background={0xffffff} >
      {texture && (
        <sprite
          anchor={{ x: 0.5, y: 0.5 }}
          texture
          x={400}
          y={270} />
      )}

      <container x={400} y={330}>
        <text 
          anchor={{ x: 0.5, y: 0.5 }} 
          text={'Hello World'} />
      </container>
    </Application>
  );
};

A few important differences to notice:

More additions

Once released, this syntax will always work. However, I intend to add a couple of other features that will add even more utility to the library:

github-actions[bot] commented 2 months ago

:tada: This issue has been resolved in version 8.0.0-alpha.1 :tada:

The release is available on GitHub release

Your semantic-release bot :package::rocket:

github-actions[bot] commented 2 months ago

:tada: This issue has been resolved in version 8.0.0-beta.1 :tada:

The release is available on GitHub release

Your semantic-release bot :package::rocket:

tpolito commented 2 months ago

Just wanted to drop in and say thank you for the hard work. I've been using the v8 beta and its such a huge improvement. Very excited for the release ❤️

danvayn commented 2 months ago

+1 to the above. Very excited to see this drop!

livemehere commented 2 months ago

This is exactly the feature I wanted. The points I found lacking in Pixi were documentation and React compatibility, and I had the same thought while using @react-three/fiber. I’m very excited and if I can contribute in any way, I would love to!

nightgrey commented 2 months ago

Bug report: refs

I sadly don't have time to debug further, but I wanted to report anyway: refs are seemingly broken?

Application ref

const Issue = () => {
  const ref = useRef(new Application());

  return <Application ref={ref} onInit={(app) => {
     console.log(ref.current, app); // `app` is not the ref!
  }}/>
}

Elements (<container />, etc.) ref

const Issue = () => {
  const ref = useRef(new Container());

  const [state, setState] = useState(1); // cause re-renders somehow, and then inspect actual UIDs (see below)

  return <container ref={ref} />
}

-<container /> (and so far, every other element) re-creates a new instance of their respective Container / Sprite etc. class

Note: Just to make this clear, I gave all of those components a fixed, pre-created ref that was unused, and they still re-created itself.

You can verify this by inspecting the uids of the Pixi containers in the Application instance:

Screenshot of a small debug utility image

These UIDs should be 2,3,4, etc.

Code to reproduce this small menu Note: Quick and dirty.

interface ContainerInfo {
  constructor: string;
  uid: Container["uid"];
  label: Container["label"];
  scale: { x: number; y: number };
  position: { x: number; y: number };
  eventMode: Container["eventMode"];
  cursor: Container["cursor"];
  interactiveChildren: Container["interactiveChildren"];
  visible: Container["visible"];
  alpha: Container["alpha"];
  children: ContainerInfo[];
}

const buildContainerInfo = (container: Container): ContainerInfo => {
  const children = container.children.map((child) => buildContainerInfo(child));

  return {
    constructor: container.constructor.name.startsWith("_")
      ? container.constructor.name.slice(1)
      : container.constructor.name,
    uid: container.uid,
    label: container.label,
    scale: { x: container.scale.x, y: container.scale.y },
    position: { x: container.x, y: container.y },
    eventMode: container.eventMode,
    cursor: container.cursor,
    interactiveChildren: container.interactiveChildren,
    visible: container.visible,
    alpha: container.alpha,
    children,
  };
};

const Debug = () => {
  const app = useApp();

  const [info, setInfo] = useState<ContainerInfo | null>(null);
  useTick(() => {
    debounce(() => {
      setInfo(buildContainerInfo(app.stage));
    }, 200)();
  });

  if (!info) return null;

  return (
    <>
      <div>
        Stage{" "}
        <small>
          {info.constructor} ({info.uid})
        </small>
      </div>
      <div>
        {info.children.map((child) => (
          <div key={child.uid}>
            {child.label}
            <small>
              {child.constructor} ({child.uid})
            </small>
          </div>
        ))}
      </div>
    </>
  );
};
trezy commented 2 months ago

@nightgrey Please create a new issue with these details. Leaving it in the comments of another ticket will result in the issue being lost in the clutter.

P3ntest commented 1 month ago

@trezy Thank you so much for your work! Would you say this is stable enough to start making hobby projects with it already? I would really love the new v8 performance

trezy commented 1 month ago

@P3ntest Yup! I've been using it in hobby projects already. 😁

AndrewJSchoen commented 1 week ago

This looks super cool! Currently looking at this option as a replacement for our current setup with svg elements and react-spring. Any thoughts on how the new architecture would handle something like the react-spring library for elements that are reactive and dynamic, or if it would still be necessary in the first place? For example, react-three/fiber suggests that for rapidly changing values, you either use something like react-spring or their useFrame hook. Thanks!

guillaumebrunerie commented 1 week ago

This looks super cool! Currently looking at this option as a replacement for our current setup with svg elements and react-spring. Any thoughts on how the new architecture would handle something like the react-spring library for elements that are reactive and dynamic, or if it would still be necessary in the first place? For example, react-three/fiber suggests that for rapidly changing values, you either use something like react-spring or their useFrame hook. Thanks!

For what it's worth, I've used both pixi-react v7 and v8 in small games with a lot of animations, once with only regular React state, and once with MobX, and it worked just fine. Contrary to popular opinion, React is perfectly capable of handling 60 fps, although you might have to keep an eye on performance and optimize rerenders sometimes.