updated pixijs

This commit is contained in:
MasterGordon 2025-09-16 16:03:36 +02:00
parent ac318f51f2
commit aa7b8569d7
6 changed files with 129 additions and 115 deletions

BIN
bun.lockb

Binary file not shown.

View File

@ -28,6 +28,7 @@ export default [
"react/react-in-jsx-scope": "off", "react/react-in-jsx-scope": "off",
"react/prop-types": "off", "react/prop-types": "off",
"react/display-name": "off", "react/display-name": "off",
"react/no-unknown-property": "off",
}, },
}, },
{ {

View File

@ -16,9 +16,9 @@
}, },
"dependencies": { "dependencies": {
"@msgpack/msgpack": "^3.1.2", "@msgpack/msgpack": "^3.1.2",
"@pixi/canvas-display": "^7.4.2", "@pixi/canvas-display": "^7.4.3",
"@pixi/canvas-renderer": "^7.4.2", "@pixi/canvas-renderer": "^7.4.3",
"@pixi/react": "^7.1.2", "@pixi/react": "^8.0.3",
"@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-popover": "^1.1.15",
@ -38,9 +38,9 @@
"jotai": "^2.14.0", "jotai": "^2.14.0",
"lucide-react": "^0.544.0", "lucide-react": "^0.544.0",
"motion": "^12.23.12", "motion": "^12.23.12",
"pixi-viewport": "^5.0.3", "pixi-viewport": "^6.0.3",
"pixi.js": "^7.0.0", "pixi.js": "^8.13.2",
"pixi.js-legacy": "^7.4.2", "pixi.js-legacy": "^7.4.3",
"prom-client": "^15.1.3", "prom-client": "^15.1.3",
"random-seed": "^0.3.0", "random-seed": "^0.3.0",
"react": "^19.1.1", "react": "^19.1.1",

View File

@ -12,9 +12,10 @@ import {
type Theme, type Theme,
} from "../themes/Theme"; } from "../themes/Theme";
import { useTheme } from "../themes/useTheme"; import { useTheme } from "../themes/useTheme";
import { Container, Sprite, Stage, useTick } from "@pixi/react"; import { extend, useTick, Application } from "@pixi/react";
import Viewport from "./pixi/PixiViewport"; import Viewport from "./pixi/PixiViewport";
import type { Viewport as PixiViewport } from "pixi-viewport"; import type { Viewport as PixiViewport } from "pixi-viewport";
import { Container, FederatedPointerEvent, Sprite, Texture } from "pixi.js";
import { import {
type ClientGame, type ClientGame,
getValue, getValue,
@ -22,7 +23,6 @@ import {
type ServerGame, type ServerGame,
} from "../../shared/game"; } from "../../shared/game";
import { useWSQuery } from "../hooks"; import { useWSQuery } from "../hooks";
import { Texture } from "pixi.js";
import { useAtom } from "jotai"; import { useAtom } from "jotai";
import { cursorXAtom, cursorYAtom } from "../atoms"; import { cursorXAtom, cursorYAtom } from "../atoms";
import Coords from "./Coords"; import Coords from "./Coords";
@ -46,6 +46,11 @@ import { weightedPickRandom } from "../../shared/utils";
import gen from "random-seed"; import gen from "random-seed";
import type { UserSettings } from "../../shared/user-settings"; import type { UserSettings } from "../../shared/user-settings";
extend({
Container,
Sprite,
});
interface BoardProps { interface BoardProps {
className?: string; className?: string;
game: ServerGame | ClientGame; game: ServerGame | ClientGame;
@ -157,6 +162,26 @@ const Board: React.FC<BoardProps> = (props) => {
}; };
}, []); }, []);
const clamp = useMemo(
() =>
theme &&
(props.width || props.height
? { left: 0, right: boardWidth, top: 0, bottom: boardHeight }
: {
left: -theme.size,
right: boardWidth + theme.size,
top: -theme.size,
bottom: boardHeight + theme.size,
}),
[boardHeight, boardWidth, props.height, props.width, theme],
);
const clampZoom = useMemo(
() => ({
minScale: 1,
}),
[],
);
return ( return (
<div className="flex flex-col w-full"> <div className="flex flex-col w-full">
<div <div
@ -199,11 +224,13 @@ const Board: React.FC<BoardProps> = (props) => {
)} )}
</div> </div>
{theme && ( {theme && (
<Stage <Application
options={{ hello: true, forceCanvas: !!props.width }} hello
forceFallbackAdapter={!!props.width}
width={width} width={width}
height={height} height={height}
className="select-none" className="select-none"
preference="webgl"
> >
<Viewport <Viewport
viewportRef={viewportRef} viewportRef={viewportRef}
@ -211,19 +238,8 @@ const Board: React.FC<BoardProps> = (props) => {
worldHeight={boardHeight} worldHeight={boardHeight}
width={width} width={width}
height={height} height={height}
clamp={ clamp={clamp}
props.width || props.height clampZoom={clampZoom}
? { left: 0, right: boardWidth, top: 0, bottom: boardHeight }
: {
left: -theme.size,
right: boardWidth + theme.size,
top: -theme.size,
bottom: boardHeight + theme.size,
}
}
clampZoom={{
minScale: 1,
}}
onViewportChange={onViewportChange} onViewportChange={onViewportChange}
> >
{Array.from({ length: game.width }).map((_, i) => { {Array.from({ length: game.width }).map((_, i) => {
@ -254,7 +270,7 @@ const Board: React.FC<BoardProps> = (props) => {
}); });
})} })}
</Viewport> </Viewport>
</Stage> </Application>
)} )}
</div> </div>
{!props.width && !props.height && <Coords />} {!props.width && !props.height && <Coords />}
@ -311,13 +327,14 @@ const Tile = ({
const isQuestionMark = game.isQuestionMark[i][j]; const isQuestionMark = game.isQuestionMark[i][j];
const base = const base =
isRevealed || (isMine && !isFlagged) ? ( isRevealed || (isMine && !isFlagged) ? (
<Sprite key="b" texture={resolveSprite(theme.revealed)} /> <pixiSprite key="b" texture={resolveSprite(theme.revealed)} />
) : ( ) : (
<Sprite key="b" texture={resolveSprite(theme.tile)} /> <pixiSprite key="b" texture={resolveSprite(theme.tile)} />
); );
const extra = isLastPos ? ( const extra = isLastPos ? (
<Sprite key="e" texture={resolveSprite(theme.lastPos)} /> <pixiSprite key="e" texture={resolveSprite(theme.lastPos)} />
) : null; ) : null;
const touchStart = useRef<number>(0); const touchStart = useRef<number>(0);
const isMove = useRef<boolean>(false); const isMove = useRef<boolean>(false);
const startX = useRef<number>(0); const startX = useRef<number>(0);
@ -334,13 +351,16 @@ const Tile = ({
setDoTick(true); setDoTick(true);
} }
}, [isMine, isRevealed, userSettings?.showRevealAnimation, value]); }, [isMine, isRevealed, userSettings?.showRevealAnimation, value]);
useTick((delta) => { useTick({
frame.current += delta * 0.1; callback: (delta) => {
if (frame.current > 3) { frame.current += delta.count * 0.1;
setDoTick(false); if (frame.current > 3) {
} setDoTick(false);
setScale(Math.max(1, -2 * Math.pow(frame.current - 0.5, 2) + 1.2)); }
}, doTick); setScale(Math.max(1, -2 * Math.pow(frame.current - 0.5, 2) + 1.2));
},
isEnabled: doTick,
});
const baseProps = useMemo( const baseProps = useMemo(
() => ({ () => ({
scale, scale,
@ -353,18 +373,18 @@ const Tile = ({
let content: ReactNode = null; let content: ReactNode = null;
if (isFlagged) { if (isFlagged) {
content = ( content = (
<Sprite key="c" texture={resolveSprite(theme.flag)} {...baseProps} /> <pixiSprite key="c" texture={resolveSprite(theme.flag)} {...baseProps} />
); );
} else if (isMine) { } else if (isMine) {
content = ( content = (
<Sprite key="c" texture={resolveSprite(theme.mine)} {...baseProps} /> <pixiSprite key="c" texture={resolveSprite(theme.mine)} {...baseProps} />
); );
} else if (value !== -1 && isRevealed) { } else if (value !== -1 && isRevealed) {
const img = theme[value.toString() as keyof Theme] as Texture; const img = theme[value.toString() as keyof Theme] as Texture;
content = img ? <Sprite key="c" texture={img} {...baseProps} /> : null; content = img ? <pixiSprite key="c" texture={img} {...baseProps} /> : null;
} else if (isQuestionMark) { } else if (isQuestionMark) {
content = ( content = (
<Sprite <pixiSprite
key="c" key="c"
texture={resolveSprite(theme.questionMark)} texture={resolveSprite(theme.questionMark)}
{...baseProps} {...baseProps}
@ -375,16 +395,16 @@ const Tile = ({
const [, setCursorY] = useAtom(cursorYAtom); const [, setCursorY] = useAtom(cursorYAtom);
return ( return (
<Container <pixiContainer
eventMode="static" eventMode="static"
interactive interactive
x={i * theme.size} x={i * theme.size}
y={j * theme.size} y={j * theme.size}
key={`${i},${j}`} key={`${i},${j}`}
onrightup={() => { onRightUp={() => {
onRightClick(i, j); onRightClick(i, j);
}} }}
onpointerup={(e) => { onPointerUp={(e: FederatedPointerEvent) => {
if (e.button !== 0) return; if (e.button !== 0) return;
if (isMove.current) return; if (isMove.current) return;
if (Date.now() - touchStart.current > 300) { if (Date.now() - touchStart.current > 300) {
@ -393,17 +413,17 @@ const Tile = ({
onLeftClick(i, j); onLeftClick(i, j);
} }
}} }}
onpointerdown={(e) => { onPointerDown={(e: FederatedPointerEvent) => {
isMove.current = false; isMove.current = false;
touchStart.current = Date.now(); touchStart.current = Date.now();
startX.current = e.global.x; startX.current = e.global.x;
startY.current = e.global.y; startY.current = e.global.y;
}} }}
onpointerenter={() => { onPointerEnter={() => {
setCursorX(i); setCursorX(i);
setCursorY(j); setCursorY(j);
}} }}
onpointermove={(e) => { onPointerMove={(e: FederatedPointerEvent) => {
if ( if (
Math.abs(startX.current - e.global.x) > 10 || Math.abs(startX.current - e.global.x) > 10 ||
Math.abs(startY.current - e.global.y) > 10 Math.abs(startY.current - e.global.y) > 10
@ -415,7 +435,7 @@ const Tile = ({
{base} {base}
{content} {content}
{extra} {extra}
</Container> </pixiContainer>
); );
}; };

View File

@ -1,14 +1,18 @@
import React from "react"; import React, { useLayoutEffect } from "react";
import type { Application } from "pixi.js"; import { type IClampZoomOptions, Viewport } from "pixi-viewport";
import { import { extend, useApplication } from "@pixi/react";
type IClampZoomOptions,
Viewport as PixiViewport,
} from "pixi-viewport";
import { PixiComponent, useApp } from "@pixi/react";
import { BaseTexture, SCALE_MODES } from "pixi.js";
BaseTexture.defaultOptions.scaleMode = SCALE_MODES.NEAREST;
export interface ViewportProps { extend({ Viewport });
import { type PixiReactElementProps } from "@pixi/react";
declare module "@pixi/react" {
interface PixiElements {
pixiViewport: PixiReactElementProps<typeof Viewport>;
}
}
interface ViewportProps {
width: number; width: number;
height: number; height: number;
worldWidth: number; worldWidth: number;
@ -21,26 +25,18 @@ export interface ViewportProps {
bottom: number; bottom: number;
}; };
clampZoom?: IClampZoomOptions; clampZoom?: IClampZoomOptions;
onViewportChange?: (viewport: PixiViewport) => void; onViewportChange?: (viewport: Viewport) => void;
viewportRef?: React.RefObject<PixiViewport | null>; viewportRef?: React.RefObject<Viewport | null>;
} }
export interface PixiComponentViewportProps extends ViewportProps { const PixiViewport = (props: ViewportProps) => {
app: Application; const { app } = useApplication();
} const ref = React.useRef<Viewport | null>(null);
const { clamp, clampZoom, onViewportChange, viewportRef } = props;
const PixiComponentViewport = PixiComponent("Viewport", { useLayoutEffect(() => {
create: (props: PixiComponentViewportProps) => { void app.renderer;
const viewport = new PixiViewport({ const viewport = ref.current;
screenWidth: props.width, if (!viewport) return () => {};
screenHeight: props.height,
worldWidth: props.worldWidth,
worldHeight: props.worldHeight,
ticker: props.app.ticker,
events: props.app.renderer.events,
disableOnContextMenu: true,
allowPreserveDragOutside: true,
});
viewport viewport
.drag({ .drag({
ignoreKeyToPressOnTouch: true, ignoreKeyToPressOnTouch: true,
@ -48,52 +44,43 @@ const PixiComponentViewport = PixiComponent("Viewport", {
}) })
.pinch() .pinch()
.wheel(); .wheel();
if (props.clamp) { if (clamp) {
viewport.clamp(props.clamp); viewport.clamp(clamp);
} }
if (props.clampZoom) { if (clampZoom) {
viewport.clampZoom(props.clampZoom); viewport.clampZoom(clampZoom);
} }
viewport.on("moved", () => { viewport.on("moved", () => {
props.onViewportChange?.(viewport); onViewportChange?.(viewport);
}); });
viewport.on("zoomed-end", () => { viewport.on("zoomed-end", () => {
props.onViewportChange?.(viewport); onViewportChange?.(viewport);
}); });
if (props.viewportRef) { if (viewportRef) {
props.viewportRef.current = viewport; viewportRef.current = viewport;
} }
return () => {
return viewport; viewport.off("moved");
}, viewport.off("zoomed-end");
applyProps: ( };
viewport: PixiViewport, }, [clamp, clampZoom, onViewportChange, viewportRef, app.renderer]);
oldProps: ViewportProps, if (!app.renderer) return null;
newProps: ViewportProps, return (
) => { <pixiViewport
if ( ref={ref}
oldProps.width !== newProps.width || screenWidth={props.width}
oldProps.height !== newProps.height || screenHeight={props.height}
oldProps.worldWidth !== newProps.worldWidth || worldWidth={props.worldWidth}
oldProps.worldHeight !== newProps.worldHeight worldHeight={props.worldHeight}
) { ticker={app.ticker}
viewport.resize( events={app.renderer.events}
newProps.width, disableOnContextMenu
newProps.height, allowPreserveDragOutside
newProps.worldWidth, >
newProps.worldHeight, {props.children}
); </pixiViewport>
} );
if (oldProps.clamp !== newProps.clamp) {
viewport.clamp(newProps.clamp);
}
},
});
const Viewport = (props: ViewportProps) => {
const app = useApp();
return <PixiComponentViewport app={app} {...props} />;
}; };
export default Viewport; export default PixiViewport;

View File

@ -1,4 +1,4 @@
import { Assets } from "pixi.js"; import { Assets, Texture } from "pixi.js";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import type { Theme, LoadedTheme, WeightedLazySprites } from "./Theme"; import type { Theme, LoadedTheme, WeightedLazySprites } from "./Theme";
@ -12,14 +12,20 @@ export const useTheme = (theme: Theme) => {
Object.entries(theme).map(async ([key, value]) => { Object.entries(theme).map(async ([key, value]) => {
let loaded = value; let loaded = value;
if (typeof value === "function") { if (typeof value === "function") {
loaded = await Assets.load((await value()).default); const texture = await Assets.load<Texture>((await value()).default);
texture.source.scaleMode = "nearest";
loaded = texture;
} }
if (Array.isArray(value)) { if (Array.isArray(value)) {
loaded = await Promise.all( loaded = await Promise.all(
loaded.map(async (sprite: WeightedLazySprites) => { loaded.map(async (sprite: WeightedLazySprites) => {
const texture = await Assets.load<Texture>(
(await sprite.sprite()).default,
);
texture.source.scaleMode = "nearest";
return { return {
weight: sprite.weight, weight: sprite.weight,
sprite: await Assets.load((await sprite.sprite()).default), sprite: texture,
}; };
}), }),
); );