diff --git a/bun.lockb b/bun.lockb index 1d4fcf7..cf944d5 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/eslint.config.js b/eslint.config.js index 0d9d41f..edef9da 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -28,6 +28,7 @@ export default [ "react/react-in-jsx-scope": "off", "react/prop-types": "off", "react/display-name": "off", + "react/no-unknown-property": "off", }, }, { diff --git a/package.json b/package.json index af4b95c..a0ff57b 100644 --- a/package.json +++ b/package.json @@ -16,9 +16,9 @@ }, "dependencies": { "@msgpack/msgpack": "^3.1.2", - "@pixi/canvas-display": "^7.4.2", - "@pixi/canvas-renderer": "^7.4.2", - "@pixi/react": "^7.1.2", + "@pixi/canvas-display": "^7.4.3", + "@pixi/canvas-renderer": "^7.4.3", + "@pixi/react": "^8.0.3", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-popover": "^1.1.15", @@ -38,9 +38,9 @@ "jotai": "^2.14.0", "lucide-react": "^0.544.0", "motion": "^12.23.12", - "pixi-viewport": "^5.0.3", - "pixi.js": "^7.0.0", - "pixi.js-legacy": "^7.4.2", + "pixi-viewport": "^6.0.3", + "pixi.js": "^8.13.2", + "pixi.js-legacy": "^7.4.3", "prom-client": "^15.1.3", "random-seed": "^0.3.0", "react": "^19.1.1", diff --git a/src/components/Board.tsx b/src/components/Board.tsx index 8eb0778..6e65bf9 100644 --- a/src/components/Board.tsx +++ b/src/components/Board.tsx @@ -12,9 +12,10 @@ import { type Theme, } from "../themes/Theme"; 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 type { Viewport as PixiViewport } from "pixi-viewport"; +import { Container, FederatedPointerEvent, Sprite, Texture } from "pixi.js"; import { type ClientGame, getValue, @@ -22,7 +23,6 @@ import { type ServerGame, } from "../../shared/game"; import { useWSQuery } from "../hooks"; -import { Texture } from "pixi.js"; import { useAtom } from "jotai"; import { cursorXAtom, cursorYAtom } from "../atoms"; import Coords from "./Coords"; @@ -46,6 +46,11 @@ import { weightedPickRandom } from "../../shared/utils"; import gen from "random-seed"; import type { UserSettings } from "../../shared/user-settings"; +extend({ + Container, + Sprite, +}); + interface BoardProps { className?: string; game: ServerGame | ClientGame; @@ -157,6 +162,26 @@ const Board: React.FC = (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 (
= (props) => { )}
{theme && ( - = (props) => { worldHeight={boardHeight} width={width} height={height} - clamp={ - 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, - } - } - clampZoom={{ - minScale: 1, - }} + clamp={clamp} + clampZoom={clampZoom} onViewportChange={onViewportChange} > {Array.from({ length: game.width }).map((_, i) => { @@ -254,7 +270,7 @@ const Board: React.FC = (props) => { }); })} - + )}
{!props.width && !props.height && } @@ -311,13 +327,14 @@ const Tile = ({ const isQuestionMark = game.isQuestionMark[i][j]; const base = isRevealed || (isMine && !isFlagged) ? ( - + ) : ( - + ); const extra = isLastPos ? ( - + ) : null; + const touchStart = useRef(0); const isMove = useRef(false); const startX = useRef(0); @@ -334,13 +351,16 @@ const Tile = ({ setDoTick(true); } }, [isMine, isRevealed, userSettings?.showRevealAnimation, value]); - useTick((delta) => { - frame.current += delta * 0.1; - if (frame.current > 3) { - setDoTick(false); - } - setScale(Math.max(1, -2 * Math.pow(frame.current - 0.5, 2) + 1.2)); - }, doTick); + useTick({ + callback: (delta) => { + frame.current += delta.count * 0.1; + if (frame.current > 3) { + setDoTick(false); + } + setScale(Math.max(1, -2 * Math.pow(frame.current - 0.5, 2) + 1.2)); + }, + isEnabled: doTick, + }); const baseProps = useMemo( () => ({ scale, @@ -353,18 +373,18 @@ const Tile = ({ let content: ReactNode = null; if (isFlagged) { content = ( - + ); } else if (isMine) { content = ( - + ); } else if (value !== -1 && isRevealed) { const img = theme[value.toString() as keyof Theme] as Texture; - content = img ? : null; + content = img ? : null; } else if (isQuestionMark) { content = ( - { + onRightUp={() => { onRightClick(i, j); }} - onpointerup={(e) => { + onPointerUp={(e: FederatedPointerEvent) => { if (e.button !== 0) return; if (isMove.current) return; if (Date.now() - touchStart.current > 300) { @@ -393,17 +413,17 @@ const Tile = ({ onLeftClick(i, j); } }} - onpointerdown={(e) => { + onPointerDown={(e: FederatedPointerEvent) => { isMove.current = false; touchStart.current = Date.now(); startX.current = e.global.x; startY.current = e.global.y; }} - onpointerenter={() => { + onPointerEnter={() => { setCursorX(i); setCursorY(j); }} - onpointermove={(e) => { + onPointerMove={(e: FederatedPointerEvent) => { if ( Math.abs(startX.current - e.global.x) > 10 || Math.abs(startY.current - e.global.y) > 10 @@ -415,7 +435,7 @@ const Tile = ({ {base} {content} {extra} - + ); }; diff --git a/src/components/pixi/PixiViewport.tsx b/src/components/pixi/PixiViewport.tsx index 2258854..ce8ac1f 100644 --- a/src/components/pixi/PixiViewport.tsx +++ b/src/components/pixi/PixiViewport.tsx @@ -1,14 +1,18 @@ -import React from "react"; -import type { Application } from "pixi.js"; -import { - 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; +import React, { useLayoutEffect } from "react"; +import { type IClampZoomOptions, Viewport } from "pixi-viewport"; +import { extend, useApplication } from "@pixi/react"; -export interface ViewportProps { +extend({ Viewport }); + +import { type PixiReactElementProps } from "@pixi/react"; + +declare module "@pixi/react" { + interface PixiElements { + pixiViewport: PixiReactElementProps; + } +} + +interface ViewportProps { width: number; height: number; worldWidth: number; @@ -21,26 +25,18 @@ export interface ViewportProps { bottom: number; }; clampZoom?: IClampZoomOptions; - onViewportChange?: (viewport: PixiViewport) => void; - viewportRef?: React.RefObject; + onViewportChange?: (viewport: Viewport) => void; + viewportRef?: React.RefObject; } -export interface PixiComponentViewportProps extends ViewportProps { - app: Application; -} - -const PixiComponentViewport = PixiComponent("Viewport", { - create: (props: PixiComponentViewportProps) => { - const viewport = new PixiViewport({ - screenWidth: props.width, - screenHeight: props.height, - worldWidth: props.worldWidth, - worldHeight: props.worldHeight, - ticker: props.app.ticker, - events: props.app.renderer.events, - disableOnContextMenu: true, - allowPreserveDragOutside: true, - }); +const PixiViewport = (props: ViewportProps) => { + const { app } = useApplication(); + const ref = React.useRef(null); + const { clamp, clampZoom, onViewportChange, viewportRef } = props; + useLayoutEffect(() => { + void app.renderer; + const viewport = ref.current; + if (!viewport) return () => {}; viewport .drag({ ignoreKeyToPressOnTouch: true, @@ -48,52 +44,43 @@ const PixiComponentViewport = PixiComponent("Viewport", { }) .pinch() .wheel(); - if (props.clamp) { - viewport.clamp(props.clamp); + if (clamp) { + viewport.clamp(clamp); } - if (props.clampZoom) { - viewport.clampZoom(props.clampZoom); + if (clampZoom) { + viewport.clampZoom(clampZoom); } viewport.on("moved", () => { - props.onViewportChange?.(viewport); + onViewportChange?.(viewport); }); viewport.on("zoomed-end", () => { - props.onViewportChange?.(viewport); + onViewportChange?.(viewport); }); - if (props.viewportRef) { - props.viewportRef.current = viewport; + if (viewportRef) { + viewportRef.current = viewport; } - - return viewport; - }, - applyProps: ( - viewport: PixiViewport, - oldProps: ViewportProps, - newProps: ViewportProps, - ) => { - if ( - oldProps.width !== newProps.width || - oldProps.height !== newProps.height || - oldProps.worldWidth !== newProps.worldWidth || - oldProps.worldHeight !== newProps.worldHeight - ) { - viewport.resize( - newProps.width, - newProps.height, - newProps.worldWidth, - newProps.worldHeight, - ); - } - if (oldProps.clamp !== newProps.clamp) { - viewport.clamp(newProps.clamp); - } - }, -}); - -const Viewport = (props: ViewportProps) => { - const app = useApp(); - return ; + return () => { + viewport.off("moved"); + viewport.off("zoomed-end"); + }; + }, [clamp, clampZoom, onViewportChange, viewportRef, app.renderer]); + if (!app.renderer) return null; + return ( + + {props.children} + + ); }; -export default Viewport; +export default PixiViewport; diff --git a/src/themes/useTheme.ts b/src/themes/useTheme.ts index ea7e76f..deab49b 100644 --- a/src/themes/useTheme.ts +++ b/src/themes/useTheme.ts @@ -1,4 +1,4 @@ -import { Assets } from "pixi.js"; +import { Assets, Texture } from "pixi.js"; import { useState, useEffect } from "react"; import type { Theme, LoadedTheme, WeightedLazySprites } from "./Theme"; @@ -12,14 +12,20 @@ export const useTheme = (theme: Theme) => { Object.entries(theme).map(async ([key, value]) => { let loaded = value; if (typeof value === "function") { - loaded = await Assets.load((await value()).default); + const texture = await Assets.load((await value()).default); + texture.source.scaleMode = "nearest"; + loaded = texture; } if (Array.isArray(value)) { loaded = await Promise.all( loaded.map(async (sprite: WeightedLazySprites) => { + const texture = await Assets.load( + (await sprite.sprite()).default, + ); + texture.source.scaleMode = "nearest"; return { weight: sprite.weight, - sprite: await Assets.load((await sprite.sprite()).default), + sprite: texture, }; }), );