pixijs / pixi-react

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

RFC: provide app ticker in custom components #314

Closed qpwo closed 2 years ago

qpwo commented 2 years ago

I'm aware of useTick but want to manage my animations in plain js without useStates.

In my ideal world I could write this code:

export default Spinner<{ imgSrc: string }, Sprite>('Spinner', (() => {
    let sprite: Sprite = null
    return {
        create: (props) => {
            sprite = Sprite.from(props.imgSrc)
            return sprite
        },
        onTick: (elapsed) => {
            if (sprite == null) return
            sprite.rotation -= 0.01 * elapsed
        }
    }
})())

I don't know if this would work but if it's possible could be very useful. I imagine it's more performant when a page has a lot of animations.

inlet commented 2 years ago

Hi Luke,

What you want is already there, have a look at Custom Components. As custom components are created in the reconciliation process, it does not have direct access to any PIXI.Application specific logic like loaders or tickers.

In your example you use a simple <Sprite> component, in that case I'd recommend you compose a simple React component to handle logic via a ref if you're determined to control it imperatively.

function CustomSprite() {
  const ref = useRef();
  const { ticker } = useApp();

  useEffect(() => {
    const sprite = ref.current;

    function tick(elapsed) {
      sprite.rotation -= 0.01 * elapsed;
    }

    ticker.add(tick);
    return () => ticker.remove(tick);
  }, []);

  return <Sprite image="image.png" ref={ref} />;
}

I'll close this issue., please let me know if you think we should reopen it

qpwo commented 2 years ago

For future readers, I think you want your effect to depend on Sprite as well, since refs don't trigger re-renders. Complete example in typescript:

import React, { useEffect, useState } from 'react'
import { Sprite as PXSprite, } from 'pixi.js'
import {  Sprite, useApp } from '@inlet/react-pixi'

type Callback = () => void
export function Spinner(props: {imgSrc: string}): JSX.Element {
    const { ticker } = useApp()
    const [sprite, setSprite] = useState<PXSprite | null>(null)

    useEffect((): void | Callback => {
        if (sprite == null) return undefined
        const tick = (elapsed: number) => {
            sprite.rotation -= 0.01 * elapsed
        }
        ticker.add(tick)
        return () => ticker.remove(tick)
    }, [sprite, ticker])

    return <Sprite image={props.imgSrc} ref={s => setSprite(s)} />
}
qpwo commented 2 years ago

And here's the start of a generic version although it's not quite right

export function PixiAppComponent<
    T extends typeof PixiComponent,
    S extends DisplayObject>
    (props:
        {
            Component: T,
            componentProps: React.Props<T>
        }): JSX.Element {
    const { ticker } = useApp()
    const [sprite, setSprite] = useState<S | null>(null)

    useEffect((): void | Callback => {
        if (sprite == null) return undefined
        const tick = (elapsed: number) => {
            sprite.rotation -= 0.01 * elapsed
        }
        ticker.add(tick)
        return () => ticker.remove(tick)
    }, [sprite, ticker])

    const { Component, componentProps } = props
    return <Component {...componentProps} ref={s => setSprite(s)} />
}