continue re-design
This commit is contained in:
@@ -0,0 +1,222 @@
|
||||
"use client";
|
||||
import { animate } from "motion";
|
||||
import { nanoid } from "nanoid";
|
||||
import { Application, ApplicationOptions } from "pixi.js";
|
||||
import { HTMLAttributes, useMemo, useRef } from "react";
|
||||
|
||||
import useDebouncedEffect from "@/hooks/useDebouncedEffect";
|
||||
import { cn } from "@/utils/cn";
|
||||
|
||||
import { isDestroyed } from "./utils";
|
||||
|
||||
type TickerResult = void;
|
||||
|
||||
export type Ticker = ({
|
||||
app,
|
||||
canvas,
|
||||
}: {
|
||||
app: Application;
|
||||
canvas: HTMLCanvasElement;
|
||||
}) => TickerResult | Promise<TickerResult>;
|
||||
|
||||
export interface PixiProps {
|
||||
tickers: Ticker[];
|
||||
onBeforeInitialized?: (props: { canvas: HTMLCanvasElement }) => void;
|
||||
onInitialized?: (props: { canvas: HTMLCanvasElement }) => void;
|
||||
canvasAttrs?: HTMLAttributes<HTMLCanvasElement>;
|
||||
initOptions?: Partial<ApplicationOptions>;
|
||||
fps?: number;
|
||||
resolution?: number;
|
||||
smartStop?: boolean;
|
||||
}
|
||||
|
||||
export default function Pixi({
|
||||
tickers,
|
||||
onInitialized,
|
||||
onBeforeInitialized,
|
||||
canvasAttrs,
|
||||
initOptions,
|
||||
fps = 60,
|
||||
resolution: resolutionFromParams = 1,
|
||||
smartStop = true,
|
||||
}: PixiProps) {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
|
||||
useDebouncedEffect(
|
||||
() => {
|
||||
const canvas = canvasRef.current!;
|
||||
|
||||
if (!canvas) return;
|
||||
|
||||
const cleanupFunctions: (() => void)[] = [];
|
||||
|
||||
canvas.style.opacity = "0";
|
||||
|
||||
onBeforeInitialized?.({ canvas });
|
||||
|
||||
const resolution = window.devicePixelRatio || 1;
|
||||
|
||||
const app = new Application();
|
||||
|
||||
cleanupFunctions.push(() => {
|
||||
if (isDestroyed(app)) return;
|
||||
|
||||
app.destroy(
|
||||
{},
|
||||
{
|
||||
children: true,
|
||||
context: true,
|
||||
style: true,
|
||||
},
|
||||
);
|
||||
|
||||
canvas.style.opacity = "0";
|
||||
});
|
||||
|
||||
(async () => {
|
||||
await app.init({
|
||||
canvas: canvas,
|
||||
resolution: resolution * resolutionFromParams,
|
||||
width: canvas.clientWidth,
|
||||
height: canvas.clientHeight,
|
||||
antialias: false,
|
||||
hello: false,
|
||||
|
||||
autoStart: true,
|
||||
sharedTicker: false,
|
||||
clearBeforeRender: true,
|
||||
|
||||
eventMode: "passive",
|
||||
|
||||
...initOptions,
|
||||
});
|
||||
|
||||
let tickerCount = 0;
|
||||
const originalAdd = app.ticker.add;
|
||||
|
||||
if (fps !== Infinity) {
|
||||
app.ticker.maxFPS = fps;
|
||||
}
|
||||
|
||||
(app.ticker as any).safeAdd = function (...args: any[]) {
|
||||
if (!app.ticker) return undefined as any;
|
||||
|
||||
tickerCount += 1;
|
||||
|
||||
if (tickerCount === 1 && smartStop) startTicker();
|
||||
|
||||
return originalAdd.apply(app.ticker, args as any);
|
||||
};
|
||||
|
||||
const originalRemove = app.ticker.remove;
|
||||
|
||||
(app.ticker as any).safeRemove = function (...args: any[]) {
|
||||
if (!app.ticker) return undefined as any;
|
||||
|
||||
tickerCount -= 1;
|
||||
|
||||
if (tickerCount === 0 && smartStop) stopTicker();
|
||||
|
||||
return originalRemove.apply(app.ticker, args as any);
|
||||
};
|
||||
|
||||
const activeAnimations: ReturnType<typeof animate>[] = [];
|
||||
|
||||
const startTicker = () => {
|
||||
app.ticker.start();
|
||||
|
||||
activeAnimations.forEach((animation) => {
|
||||
animation.play();
|
||||
});
|
||||
};
|
||||
|
||||
const stopTicker = () => {
|
||||
app.ticker.stop();
|
||||
|
||||
activeAnimations.forEach((animation) => {
|
||||
animation.pause();
|
||||
});
|
||||
};
|
||||
|
||||
(app as any).animate = ((...args: any[]) => {
|
||||
const animation = (animate as any)(...args);
|
||||
|
||||
activeAnimations.push(animation);
|
||||
|
||||
animation.finished.then(() => {
|
||||
activeAnimations.splice(activeAnimations.indexOf(animation), 1);
|
||||
});
|
||||
|
||||
return animation;
|
||||
}) as typeof animate;
|
||||
|
||||
for (const ticker of tickers) {
|
||||
ticker({
|
||||
app,
|
||||
canvas,
|
||||
});
|
||||
}
|
||||
|
||||
app.stage.interactive = false;
|
||||
app.stage.cullable = true;
|
||||
app.stage.sortableChildren = false;
|
||||
app.stage.interactiveChildren = false;
|
||||
|
||||
app.render();
|
||||
|
||||
setTimeout(() => {
|
||||
onInitialized?.({ canvas });
|
||||
canvas.style.opacity = "1";
|
||||
}, 100);
|
||||
|
||||
const observer = new IntersectionObserver(([entry]) => {
|
||||
if (entry.isIntersecting) {
|
||||
if (tickerCount !== 0 || !smartStop) startTicker();
|
||||
} else {
|
||||
stopTicker();
|
||||
}
|
||||
});
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
app.renderer.resize(canvas.clientWidth, canvas.clientHeight);
|
||||
app.renderer.render(app.stage);
|
||||
});
|
||||
|
||||
observer.observe(canvas);
|
||||
resizeObserver.observe(canvas);
|
||||
|
||||
cleanupFunctions.push(() => {
|
||||
resizeObserver.disconnect();
|
||||
observer.disconnect();
|
||||
});
|
||||
})();
|
||||
|
||||
return () => {
|
||||
cleanupFunctions.forEach((fn) => fn());
|
||||
};
|
||||
},
|
||||
{
|
||||
timeout: 1,
|
||||
ignoreInitialCall: false,
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const key = useMemo(() => {
|
||||
return nanoid();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [tickers]);
|
||||
|
||||
return (
|
||||
<canvas
|
||||
{...canvasAttrs}
|
||||
className={cn(canvasAttrs?.className)}
|
||||
key={key}
|
||||
ref={canvasRef}
|
||||
style={{
|
||||
...canvasAttrs?.style,
|
||||
opacity: 0,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Assets } from "pixi.js";
|
||||
|
||||
class PixiAssetManager {
|
||||
/**
|
||||
* Loads assets from the given sources
|
||||
* @param sources The source URLs of the assets
|
||||
* @returns A promise that resolves with the loaded asset(s)
|
||||
*/
|
||||
public static load<T = any>(...sources: string[]): Promise<T> {
|
||||
if (sources.length === 0) {
|
||||
return Promise.reject(new Error("No sources provided"));
|
||||
}
|
||||
|
||||
if (sources.length === 1) {
|
||||
const src = sources[0];
|
||||
|
||||
return Assets.load(src) as Promise<T>;
|
||||
}
|
||||
|
||||
// Handle multiple sources
|
||||
return Promise.all(
|
||||
sources.map((src) => this.load(src)),
|
||||
) as unknown as Promise<T>;
|
||||
}
|
||||
}
|
||||
|
||||
export default PixiAssetManager;
|
||||
@@ -0,0 +1,60 @@
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-nocheck -- TODO: fix this
|
||||
|
||||
import { Application, Assets, Sprite, Texture } from "pixi.js";
|
||||
|
||||
export const isDestroyed = (app: Application) => {
|
||||
if (!app.ticker || !app.renderer || !app.stage || !app.renderer.gl)
|
||||
return true;
|
||||
|
||||
return app.renderer.gl.isContextLost();
|
||||
};
|
||||
|
||||
export const generateTexture = (app: Application, graphic: any) => {
|
||||
const renderer = app.renderer;
|
||||
|
||||
if (!isDestroyed(app)) {
|
||||
return renderer.generateTexture(graphic);
|
||||
}
|
||||
|
||||
return Texture.WHITE;
|
||||
};
|
||||
|
||||
export const degreesToRadians = (degrees: number) => {
|
||||
return degrees * (Math.PI / 180);
|
||||
};
|
||||
|
||||
export const imageToSprite = async (app: Application, path: string) => {
|
||||
let texture;
|
||||
|
||||
if (Assets.cache.has(path)) {
|
||||
texture = Assets.cache.get(path);
|
||||
} else {
|
||||
texture = await Assets.load(path);
|
||||
}
|
||||
|
||||
const sprite = Sprite.from(texture);
|
||||
|
||||
return sprite;
|
||||
};
|
||||
|
||||
export const createRenderWithFPS = (app: Application, fps: number) => {
|
||||
let lastUpdateTime = 0;
|
||||
|
||||
return () => {
|
||||
const currentTime = performance.now();
|
||||
const timeSinceLastUpdate = currentTime - lastUpdateTime;
|
||||
|
||||
if (timeSinceLastUpdate >= 1000 / fps) {
|
||||
app.ticker.update();
|
||||
app.render();
|
||||
lastUpdateTime = currentTime;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export const waitUntilPixiIsReady = (app: Application) => {
|
||||
return new Promise((resolve) => {
|
||||
app.canvas.addEventListener("pixi-initialized", resolve);
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user