pixijs / pixi-react

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

Bug: The codepen example for pixi-viewport is outdated and does not work #449

Open ben4d85 opened 11 months ago

ben4d85 commented 11 months ago

Current Behavior

I have taken the code from the pixi-viewport codepen example and tried to get it to work in a fresh installation of Next.js 13 (React 18).

I have received a lot of errors. I have come fairly close to solving almost all errors, but ultimately cannot get the section to work that actually uses pixi-viewport. The code in the example is clearly outdated and doesn't use standard import syntax, especially for pixi-viewport, and also lacks accurate types, making it unclear how to use this in a React app in practice.

FYI: A similar example can be found in the React Pixi Custom Components docs.

Expected Behavior

Steps to Reproduce

Environment

I don't know about the versions used in the codepen, but I am using the following:

"pixi.js": "^7.2.4", "pixi-viewport": "^5.0.1", "@pixi/react": "^7.1.0", "react": "18.2.0", "react-dom": "18.2.0", "next": "13.4.3", "typescript": "5.0.4",

Possible Solution

Additional Information

Here is how far I got with updating the example. This runs. However, if you comment the PixiViewport component back in, it stops working.

// ========================================================
// Imports
// ========================================================

import { Stage, Container, Sprite, PixiComponent, useApp, useTick } from '@pixi/react';
import { Viewport } from 'pixi-viewport'

import { useState, useEffect, useCallback, useRef, forwardRef } from 'react';

// ========================================================
// Misc
// ========================================================

const stageOptions = {
    antialias: true,
    autoDensity: true,
    backgroundColor: 0x222222,
};

type AreasArrIndex = 'world' | 'center' | 'tl' | 'tr' | 'bl' | 'br';
const areas: {
    [K in AreasArrIndex]: Array<number>;
} = {
    'world': [1000, 1000, 2000, 2000],
    'center': [1000, 1000, 400, 400],
    'tl': [100, 100, 200, 200],
    'tr': [1900, 100, 200, 200],
    'bl': [100, 1900, 200, 200],
    'br': [1900, 1900, 200, 200]
}

const useIteration = (incr = 0.1) => {
    const [i, setI] = useState(0);

    useTick((delta) => {
        setI(i => i + incr * delta);
    });

    return i;
};

const useResize = () => {
    const [size, setSize] = useState([global?.window.innerWidth, global?.window.innerHeight]);

    useEffect(() => {
        const onResize = () => {
            requestAnimationFrame(() => {
                setSize([global?.window.innerWidth, global?.window.innerHeight])
            })
        };

        global?.window.addEventListener('resize', onResize);

        return () => {
            global?.window.removeEventListener('resize', onResize);
        }
    }, []);

    return size;
};

// create and instantiate the viewport component
// we share the ticker and interaction from app
// FIXME: This is the culprit!
const PixiViewportComponent = PixiComponent("Viewport", {

    create(props) {
        const { app, ...viewportProps } = props;

        const viewport = new Viewport({
            ticker: props.app.ticker,
            // interaction: props.app.renderer.plugins.interaction, // deprecated
            events: props.app.renderer.events,
            ...viewportProps
        });

        //activate plugins
        // viewport.drag();
        // viewport.pinch();
        // viewport.wheel();
        // viewport.decelerate();
        // (props.plugins || []).forEach((plugin: string) => {
        //     viewport[plugin]();
        // });

        return viewport;
    },
    // applyProps(viewport, _oldProps, _newProps) {
    //     const { plugins: oldPlugins, children: oldChildren, ...oldProps } = _oldProps;
    //     const { plugins: newPlugins, children: newChildren, ...newProps } = _newProps;

    //     // Object.keys(newProps).forEach((p) => {
    //     //     if (oldProps[p] !== newProps[p]) {
    //     //         viewport[p] = newProps[p];
    //     //     }
    //     // });
    // },
    didMount() {
        console.log("viewport mounted");
    }
});

// create a component that can be consumed
// that automatically pass down the app
const PixiViewport = forwardRef(function MyPixiViewport(props: any, ref: any) {
    return (
        <PixiViewportComponent ref={ref} app={useApp()} {...props} />
    )
});

// Wiggling bunny
const Bunny = forwardRef(function MyBunny(props: any, ref: any) {

    // abstracted away, see settings>js
    const i = useIteration(0.1);

    return (
        <Sprite
            ref={ref}
            image="https://s3-us-west-2.amazonaws.com/s.cdpn.io/693612/IaUrttj.png"
            anchor={0.5}
            scale={2}
            rotation={Math.cos(i) * 0.98}
            {...props}
        />
    );
});

// Bunny moving in a circle
const BunnyFollowingCircle = forwardRef(function MyBunnyFollowingCircle({ x, y, rad }: any, ref: any) {

    const i = useIteration(0.02);
    return <Bunny ref={ref} x={x + Math.cos(i) * rad} y={y + Math.sin(i) * rad} scale={6} />
});

// 4 squared bunnies
// positioned by its name
const BunniesContainer = ({
    pos,
    ...props
}: {
    pos: AreasArrIndex,
    [x: string]: any;
}) => {
    const [x, y] = areas[pos];
    return (
        <Container x={x} y={y} {...props}>
            <Bunny x={-50} y={-50} />
            <Bunny x={50} y={-50} />
            <Bunny x={-50} y={50} />
            <Bunny x={50} y={50} />
        </Container>
    );
}

// ========================================================
// Pixi app
// ========================================================

// the main app
export const PixiApp = () => {

    // get the actual viewport instance
    const viewportRef = useRef();

    // get ref of the bunny to follow
    const followBunny = useRef();

    // get the current window size
    const [width, height] = useResize();

    // interact with viewport directly
    // move and zoom to specified area
    const focus = useCallback((p: AreasArrIndex) => {
        const viewport = viewportRef.current;
        const [x, y, focusWidth, focusHeight] = areas[p];

        const f = Math.max(focusWidth / width, focusHeight / height);
        const w = width * f;
        const h = height * f;

        // pause following
        //viewport.plugins.pause('follow');

        // and snap to selected
        // viewport.snapZoom({ width: w, height: h, removeOnComplete: true, ease: 'easeInOutExpo' });
        // viewport.snap(x, y, { removeOnComplete: true, ease: 'easeInOutExpo' });
    }, [width, height]);

    const follow = useCallback(() => {
        const viewport = viewportRef.current;

        const focusWidth = 1000;
        const focusHeight = 1000;

        const f = Math.max(focusWidth / width, focusHeight / height);
        const w = width * f;
        const h = height * f;

        // viewport.snapZoom({ width: w, height: h, ease: 'easeInOutExpo' });
        // viewport.follow(followBunny.current, { speed: 20 });
    }, [width, height]);

    return (
        <div className="mapContainer">

            <div className="buttonsGroup">
                <button onClick={() => focus('world')}>Fit</button>
                <button onClick={() => focus('center')}>Center</button>
                <button onClick={() => focus('tl')}>TL</button>
                <button onClick={() => focus('tr')}>TR</button>
                <button onClick={() => focus('bl')}>BL</button>
                <button onClick={() => focus('br')}>BR</button>
                <button onClick={() => follow()}>Follow</button>
            </div>

            <Stage width={width} height={height} options={stageOptions}>

                {/* <PixiViewport
                    ref={viewportRef}
                    // plugins={["drag", "pinch", "wheel", "decelerate"]}
                    screenWidth={width}
                    screenHeight={height}
                    worldWidth={width * 4}
                    worldHeight={height * 4}
                > */}

                    <BunniesContainer pos="tl" />
                    <BunniesContainer pos="tr" />
                    <BunniesContainer pos="bl" />
                    <BunniesContainer pos="br" />
                    <BunniesContainer pos="center" scale={2} />

                    <BunnyFollowingCircle x={1000} y={1000} rad={500} ref={followBunny} />

                {/* </PixiViewport> */}

            </Stage >

        </div>
    );
}

PS: I have tried to set up an example of the above on StackBlitz, but could not get that to work either due to a different error: Error in /turbo_modules/@pixi/color@7.2.4/lib/Color.js (6:19) Unexpected token 'export'

ben4d85 commented 11 months ago

Update: This thread, https://github.com/davidfig/pixi-viewport/issues/438, provides a partial solution. However, it leads to an error when leaving the page.