## Table of Contents - [Overview](#overview) - [Installation](#installation) - [Integration](#integration-guide) - [Asset Loading](#asset-loading-system) - [AutoPlay](#autoplay-system) - [Responsive Grid](#responsive-grid-system) - [Spine Animation](#spin
npm install @darkcore/game-libraryDracoFusion is a game development library built on top of PixiJS, providing components and utilities for creating web-based games with @pixi/react.
This library requires specific versions of its peer dependencies:
``json`
"peerDependencies": {
"@pixi/react": "7.1.1",
"howler": "2.2.4",
"pixi-spine": "4.0.4",
"pixi.js": "7.4.0",
"react": "^18.3.1",
"react-dom": "^18.3.1"
}
If you encounter React version conflicts, update your Vite configuration:
`js
import tailwindcss from '@tailwindcss/vite'
import path from "path"
import react from "@vitejs/plugin-react"
import { defineConfig } from "vite"
export default defineConfig({
plugins: [react(), tailwindcss()],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
react: path.resolve(__dirname, "node_modules/react"),
"react-dom": path.resolve(__dirname, "node_modules/react-dom")
},
},
})
`
`bash1. Cleanup old installs
rm -rf node_modules pnpm-lock.yaml
pnpm store prune
Integration Guide
$3
DracoFusion uses a layered approach:
1. DarkCoreProvider: Top-level provider for theming
2. Board & BoardContent: Layout components from @darkcore/ui
3. GameCanvas: The Stage capsule for @pixi/react components
4. GameBoard: Your custom @pixi/react components
Basic implementation:
`tsx
import { DarkCoreProvider, Board, BoardContent } from "@darkcore/ui";
import GameCanvas from "./components/GameCanvas";
import { createRoot } from "react-dom/client";createRoot(document.getElementById("root")!).render(
theme options / }}>
);
`$3
The
GameCanvas component manages the WebGL rendering context:`tsx
import { useEffect } from "react";
import { GameCanvasProvider } from "@darkcore/game-library";
import { useBoardSizes } from "@darkcore/ui";
import { useGameStore } from "@/store/game";
import { STAGE_OPTIONS } from "@/lib/constants";
import { Container } from "@pixi/react";export default function GameCanvas() {
// Get layout bounds from the BoardContent component
const { width, height } = useBoardSizes();
// Canvas dimensions in global state
const { canvasWidth, canvasHeight } = useGameStore((s) => s);
// Update canvas dimensions in global state
useEffect(() => {
setTimeout(() => {
useGameStore.setState({ canvasWidth: width, canvasHeight: height });
}, 25);
}, [width, height]);
return (
settings={{
width: canvasWidth,
height: canvasHeight,
options: STAGE_OPTIONS
}}
>
{/ Your @pixi/react components /}
);
}
`#### GameCanvasProvider Configuration
`typescript
// Types
export type SettingsProps = {
/* canvas pixel width /
width: number;
/* canvas pixel height /
height: number;
/* Partial PIXI.ApplicationOptions /
options: Partial;
/* optional FPS cap; defaults to 120 /
maxFPS?: number;
};// Component type
export const GameCanvasProvider: React.FC<{
settings: SettingsProps;
children: React.ReactNode;
}>;
`Asset Loading System
The asset loading system manages images, sounds, fonts, and spine animations.
$3
`typescript
// Type definitions
export type ImagesResult = Record;
export type SpinesResult = Record;
export type SoundsResult = Record;
export type FontsResult = Record;
export type AssetsRequest = Record;
export type FontsRequest = Record;
`$3
Define your assets in a file:
`tsx
// lib/assets.ts
export const ASSETS: UseAssetsLoaderConfig = {
images: {
background: "/assets/images/background.webp",
},
fonts: {
Inter: { url: "/assets/fonts/Inter.ttf", weight: "600" }
},
spines: {
dice: "/assets/spines/Dice.json"
},
sounds: {
SFX_click: "/assets/sounds/SFX_click.m4a",
}
}export interface UseAssetsLoaderConfig {
images?: AssetsRequest;
fonts?: FontsRequest;
spines?: AssetsRequest;
sounds?: AssetsRequest;
}
`$3
`typescript
export interface UseAssetsLoaderResult {
images: ImagesResult;
fonts: FontsResult;
spines: SpinesResult;
sounds: SoundsResult;
isAllAssetsLoaded: boolean;
error?: Error | null;
}// The exported hook
export const useAssetsLoader: (config: UseAssetsLoaderConfig) => UseAssetsLoaderResult;
`$3
`typescript
// Specialized asset access hooks
export const useGetImages: () => ImagesResult;
export const useGetSounds: () => SoundsResult;
export const useGetSpines: () => SpinesResult;
export const useGetFonts: () => FontsResult;
export const useGetAllAssets: () => useGetAssetsResult;export interface useGetAssetsResult {
images: ImagesResult;
spines: SpinesResult;
sounds: SoundsResult;
fonts: FontsResult;
error: Error | null;
}
`$3
`tsx
// store/game.ts
import { create } from "zustand";type GameState = {
isAllAssetsLoaded: boolean;
canvasWidth: number;
canvasHeight: number;
};
export const useGameStore = create(() => ({
isAllAssetsLoaded: false,
canvasWidth: 0,
canvasHeight: 0,
}));
`$3
`tsx
// hooks/useInitializeGame.ts
import { useEffect } from "react";
import { useAssetsLoader } from "@darkcore/game-library";
import { ASSETS } from "@/lib/assets";
import { useGameStore } from "@/store/game";export const useInitializeGame = () => {
const { isAllAssetsLoaded } = useAssetsLoader(ASSETS);
useEffect(() => {
if (isAllAssetsLoaded) {
useGameStore.setState({
isAllAssetsLoaded: true
});
}
}, [isAllAssetsLoaded]);
};
`$3
`tsx
// components/IFrameGuard.tsx
import { useInitializeGame } from "@/hooks/useInitializeGame";
import Layout from "../_Layout/_Layout";export default function IFrameGuard() {
// Initialize game assets
useInitializeGame();
return ;
}
`Note: Place initialization code and one-time setup logic in the IFrameGuard component to prevent unnecessary re-renders.
`tsx
// components/_Layout/_Layout.tsx
import GameCanvas from "../GameCanvas/GameCanvas";
import { Loader, Board, BoardContent } from "@darkcore/ui";
import { useState, useEffect } from "react";
import { useGameStore } from "@/store/game.ts";export default function _Layout() {
const [loading, setLoading] = useState(true);
const isAllAssetsLoaded = useGameStore((s) => s.isAllAssetsLoaded);
useEffect(() => {
if (isAllAssetsLoaded) {
// Add delay for appearing
setTimeout(() => {
setLoading(false);
}, 500);
}
}, [isAllAssetsLoaded]);
return (
<>
{loading && isVisible={loading}
size={40}
placeholder="Just a moment"
/>}
{/ GameCanvas renders only when assets are loaded /}
{isAllAssetsLoaded && }
>
);
}
`$3
`tsx
import {
useGetImages,
useGetSounds,
useGetSpines,
useGetFonts,
useGetAllAssets
} from "@darkcore/game-library";
import { Container, Sprite, Text } from "@pixi/react";
import { TextStyle } from "pixi.js";export default function GameBoard() {
// Get only the assets you need
const images = useGetImages();
const sounds = useGetSounds();
const spines = useGetSpines();
const fonts = useGetFonts();
// Or get all assets at once
const allAssets = useGetAllAssets();
const handleOnPointerDown = () => {
sounds.SFX_click.play();
};
return (
image={images.background}
interactive={true}
onpointerdown={handleOnPointerDown}
/>
{/ Fonts can be used directly as strings /}
text="Hello World"
style={{ fill: '#ffffff', fontFamily: 'Inter' } as TextStyle}
/>
);
}
`AutoPlay System
The AutoPlay system provides an automated gameplay mechanism for games requiring repetitive actions.
$3
`typescript
import { AutoPlay } from "@darkcore/game-library";// Create an AutoPlay instance
const autoPlay = new AutoPlay({
totalIterations: 10, // Number of rounds (0 = infinite)
delayMs: 2500, // Delay between rounds in normal mode (ms)
instantDelayMs: 1500, // Delay between rounds in instant mode (ms)
getInstantPlay: () => true, // Function to check if instant mode is enabled
onPlay: async () => { // Function to execute each round
// Play one round of the game
await playRound();
},
onStop: () => { // Called when autoplay stops
console.log('Autoplay stopped');
},
onPause: () => { // Called when autoplay is paused
console.log('Autoplay paused');
},
onResume: () => { // Called when autoplay is resumed
console.log('Autoplay resumed');
},
onProgress: (completed, remaining) => {
console.log(
Completed: ${completed}, Remaining: ${remaining});
}
});// Start autoplay
autoPlay.start();
// Pause autoplay (preserves remaining delay time)
autoPlay.pause();
// Resume autoplay (continues with remaining delay)
autoPlay.resume();
// Check if autoplay is currently paused
const isPaused = autoPlay.isPaused();
// Stop autoplay
autoPlay.stop();
`$3
`typescript
export interface AutoPlayConfig {
/* Total number of iterations to run. 0 = infinite /
totalIterations: number;
/* Normal delay between rounds (ms) /
delayMs: number;
/* Delay when instant play is enabled (ms) /
instantDelayMs: number;
/* Function to get current instant play mode state /
getInstantPlay: () => boolean;
/* Function to execute for each round /
onPlay: () => Promise;
/* Called when autoplay starts /
onStart?: () => void;
/* Called when autoplay stops /
onStop?: () => void;
/* Called when autoplay is paused /
onPause?: () => void;
/* Called when autoplay is resumed /
onResume?: () => void;
/* Called when an error occurs /
onError?: (error: any) => void;
/* Feedback after each round completion: completed and remaining counts /
onProgress?: (completedIterations: number, remainingIterations: number) => void;
}
`$3
`typescript
export class AutoPlay {
/* Start autoplay from the beginning /
start(): void;
/* Stop autoplay completely /
stop(): void;
/* Pause autoplay (preserves remaining delay time) /
pause(): void;
/* Resume autoplay (continues with remaining delay) /
resume(): void;
/* Check if autoplay is currently paused /
isPaused(): boolean;
/* Get remaining iterations (Infinity for unlimited) /
getRemainingIterations(): number;
}
`$3
#### AutoPlay Store
`typescript
// store/autoplay.ts
import { create } from 'zustand'interface AutoPlayStore {
autoPlayStarted: boolean
stopRequested: boolean
totalProfit: number
startAutoBet: () => void
stopAutoBet: () => void
}
export const useAutoPlayStore = create()((set, get) => ({
autoPlayStarted: false,
stopRequested: false, // Special flag to show "Stopping..." until last round completes
totalProfit: 0,
startAutoBet: async () => {
set({
autoPlayStarted: true,
totalProfit: 0
});
},
stopAutoBet: async () => {
set({
autoPlayStarted: false,
totalProfit: 0
});
}
}));
`#### AutoPlay Hook
`typescript
// src/hooks/useAutoPlay.ts
import { useEffect, useRef } from "react";
import { AutoPlay } from "@darkcore/game-library";
import { useGameStore } from "@/store/game";
import { useAutoPlayStore } from "@/store/autoplay";export function useAutoPlay() {
const { autoPlayStarted, totalProfit } = useAutoPlayStore();
const { numberOfBets, onPlay, isOffline, onProfitStop, onLossStop, activeTab } = useGameStore();
const autoRef = useRef(null);
const start = () => {
if (autoRef.current) autoRef.current.stop();
autoRef.current = new AutoPlay({
totalIterations: numberOfBets,
delayMs: 2500,
instantDelayMs: 1500,
getInstantPlay: () => useGameStore.getState().instantPlay,
onPlay: onPlay,
onStop: () => {
useAutoPlayStore.setState({
stopRequested: false,
});
},
onProgress: (_completed, progress) => {
if(progress !== Infinity) {
useGameStore.setState({
numberOfBets: progress || 0,
});
}
if(progress === 0) {
useAutoPlayStore.setState({
autoPlayStarted: false,
stopRequested: true,
});
}
}
});
autoRef.current.start();
};
const stop = () => {
useAutoPlayStore.setState({
stopRequested: true,
});
autoRef.current?.stop();
};
useEffect(() => {
if(activeTab === "AUTO") {
autoPlayStarted ? start() : stop();
}
}, [autoPlayStarted]);
useEffect(() => {
if (isOffline) stop();
}, [isOffline]);
// Stop conditions: profit or loss limits reached
useEffect(() => {
if ((onProfitStop !== 0 && totalProfit >= onProfitStop) ||
(onLossStop !== 0 && totalProfit <= -onLossStop)) {
stop();
}
}, [totalProfit]);
}
`#### Usage in Components
`tsx
import { useAutoPlay } from "@/hooks/useAutoPlay";
import { useAutoPlayStore } from "@/store/autoplay";export default function GameControls() {
// Initialize the autoplay hook
useAutoPlay();
const { autoPlayStarted, stopRequested } = useAutoPlayStore();
return (
onClick={() => {
if (autoPlayStarted) {
useAutoPlayStore.getState().stopAutoBet();
} else {
useAutoPlayStore.getState().startAutoBet();
}
}}
>
{autoPlayStarted
? (stopRequested ? "Stopping..." : "Stop Auto")
: "Start Auto"}
);
}
`Responsive Grid System
A flexible, responsive grid utility for positioning game elements that adapts to any canvas size, including support for configurable margins and cover ratios.
$3
`tsx
import { useResponsiveGrid } from "@darkcore/game-library";
import { Container, Sprite } from "@pixi/react";
import * as PIXI from "pixi.js";const images = useGetImages();
const cellTexture = PIXI.Texture.from(images.cell);
const rows = 3;
const columns = 5;
const {
scale,
containerPosition,
cellCenterPositions,
gridRect,
gridEdges
} = useResponsiveGrid({
canvasWidth,
canvasHeight,
cellWidth: cellTexture.width,
cellHeight: cellTexture.height,
columns,
rows,
coverRatio: 0.9, // grid + margins will occupy 90% of the canvas
horizontalSpacing: 20,
verticalSpacing: 20,
marginX: 40, // 40px margin on left and right
marginY: 40 // 40px margin on top and bottom
});
return (
<>
{/ Main grid container or position={[gridRect.x, gridRect.y]} /}
{cellCenterPositions.map((pt, i) => (
))}
{/ Example UI panel at right edge /}
position={[gridEdges.right.x + 20, gridEdges.right.y]}
scale={scale}
>
>
);
`$3
`typescript
interface ResponsiveGridConfig {
canvasWidth: number;
canvasHeight: number;
cellWidth: number;
cellHeight: number;
columns: number;
rows: number;
coverRatio: number; // 0–1, total grid+margin coverage of canvas
horizontalSpacing?: number;
verticalSpacing?: number;
marginX?: number; // px margin on left/right
marginY?: number; // px margin on top/bottom
}interface ResponsiveGridResult {
/* Scale factor to apply to the grid container /
scale: number;
/* Top-left position (x,y) of the grid container /
containerPosition: Point;
/* Center positions for each cell, ensuring they are evenly centered within the grid based on row and column counts /
cellCenterPositions: Point[];
/**
* The actual on-screen rectangle of the grid after scaling
* { x, y, width, height }
*/
gridRect: { x: number; y: number; width: number; height: number };
/**
* Midpoints of each side of the grid rect:
* top, bottom, left, right
*/
gridEdges: {
top: Point;
bottom: Point;
left: Point;
right: Point;
};
}
`Spine Animation System
The library includes components and hooks for working with Spine animations (using pixi-spine).
$3
GameSpine is a reusable component that manages Spine animations within a Pixi Container:`tsx
import { GameSpine } from "@darkcore/game-library";
import { useGetSpines } from "@darkcore/game-library";export default function SpineExample() {
const spines = useGetSpines();
return (
spine={spines.dice}
animation="idle"
loop={true}
scale={0.5}
position={{ x: 0, y: 0 }}
zIndex={5}
opacity={1}
timeScale={1}
autoUpdate={true}
name="character"
isAnimating={true}
/>
);
}
`$3
`typescript
export interface GameSpineProps {
/* Spine asset (with .spineData) /
spine: Spine;
/* Animation key to play /
animation: string;
/* Loop the animation /
loop: boolean;
/* Scale factors /
scale: number | { x: number; y: number };
/* Position inside parent container /
position: { x: number; y: number };
/* Z-index for sorting /
zIndex?: number;
/* Opacity (0–1) /
opacity?: number;
/* Playback speed multiplier /
timeScale?: number;
/* Controls whether animation plays (true) or stops (false) /
autoUpdate?: boolean;
/* Unique name for the instance /
name: string;
/* Counter to force recreation of spine instance /
restartCounter?: number;
/* External ref to access spine instance /
outerSpineRef?: React.RefObject;
}
`$3
The library provides a hook to extract animation information from a loaded Spine asset:
`tsx
import { useSpineInfo } from "@darkcore/game-library";
import { useGetSpines } from "@darkcore/game-library";// Get all animations from the Spine asset
const spines = useGetSpines();
const animations = useSpineInfo(spines.dice);
console.log(animations);
// Returns:
// [
// { name: "Dice_Left", duration: 0.3332999944686889 },
// { name: "Dice_Right", duration: 0.3332999944686889 }
// ]
`The
useSpineInfo hook returns an array of animation information objects:`typescript
export interface SpineAnimationInfo {
/* Animation name as defined in the Spine data /
name: string;
/* Duration of the animation in seconds /
duration: number;
}
`$3
The library provides a controller hook for managing Spine animations with a convenient API:
`tsx
import { useSpineController } from "@darkcore/game-library";
import { useGetSpines, useSpineInfo } from "@darkcore/game-library";
import { GameSpine } from "@darkcore/game-library";function SpineExample() {
const diceSpine = useGetSpines().dice;
const animations = useSpineInfo(diceSpine);
const controller = useSpineController({
initialAnimation: animations[0].name,
initialLoop: true,
initialTimeScale: 1,
initialScale: { x: 1, y: 1 },
initialPosition: { x: 0, y: 0 },
});
// Start animation
controller.start();
// Change animation
controller.setAnimation(animations[1].name);
// Stop animation
controller.stop();
// Reset animation
controller.reset();
// Other available methods:
// controller.setLoop(true/false)
// controller.setTimeScale(1.5)
// controller.setScale({ x: 2, y: 2 })
// controller.setPosition({ x: 100, y: 100 })
return (
spine={diceSpine}
animation={controller.props.animation}
loop={controller.props.loop}
scale={controller.props.scale}
position={controller.props.position}
timeScale={controller.props.timeScale}
name={controller.props.name}
autoUpdate={controller.props.autoUpdate}
isAnimating={controller.isAnimating}
/>
);
}
`The
useSpineController hook provides a simple interface for controlling Spine animations:`typescript
// Configuration options
interface SpineControllerOptions {
initialAnimation: string;
initialLoop?: boolean;
initialTimeScale?: number;
initialScale?: { x: number; y: number };
initialPosition?: { x: number; y: number };
}// Returned controller object
interface SpineController {
// Direct props for GameSpine component
props: {
animation: string;
loop: boolean;
autoUpdate: boolean;
timeScale: number;
scale: { x: number; y: number };
position: { x: number; y: number };
name: string;
restartCounter: number;
};
// Control methods
/* Start playing the animation from beginning /
start: () => void;
/* Stop the animation by setting timeScale to 0 /
stop: () => void;
/* Resume the animation by restoring timeScale /
resume: () => void;
/* Reset the animation /
reset: () => void;
/* Change the current animation /
setAnimation: (animationName: string) => void;
/* Toggle looping (It affects animation stopping and starting) /
setLoop: (loop: boolean) => void;
/* Change animation playback speed /
setTimeScale: (timeScale: number) => void;
/* Adjust the size of the animation /
setScale: (scale: { x: number; y: number }) => void;
/* Change the position of the animation /
setPosition: (position: { x: number; y: number }) => void;
// Direct spine instance access
/* Reference to the spine instance for advanced control /
spineRef: React.RefObject;
}
`Timers System
The library provides a timeout management system with pause/resume capabilities and ID-based timeout tracking.
$3
The
createTimeoutManager function creates a timeout manager instance with ID-based timeout control:`tsx
import { createTimeoutManager } from "@darkcore/game-library";// Create a timeout manager instance
const timeoutManager = createTimeoutManager();
// Create timeouts with unique IDs
timeoutManager.createTimeout("timer1", () => {
console.log("Timer 1 executed");
}, 3000);
timeoutManager.createTimeout("timer2", () => {
console.log("Timer 2 executed");
}, 5000);
// Clear a specific timeout by ID
timeoutManager.clearTimeout("timer1");
// Check if a timeout exists
const exists = timeoutManager.hasTimeout("timer2");
// Get number of active timeouts
const count = timeoutManager.size();
// Pause all timeouts
timeoutManager.pauseAll();
// Resume all paused timeouts
timeoutManager.resumeAll();
// Clear all timeouts
timeoutManager.clearAll();
`$3
- ID-based Management: Each timeout has a unique string identifier
- Pause/Resume: Pause and resume all timeouts while preserving remaining time
- Automatic Cleanup: Completed timeouts are automatically removed
- Status Checking: Check existence and count of active timeouts
$3
The library provides a global timeout manager instance that can be used across components:
`tsx
import { timeoutManager } from "@darkcore/game-library";function GameComponent() {
useEffect(() => {
// Set multiple timers with IDs
timeoutManager.createTimeout("action1", () => {
console.log("Action 1 completed");
}, 1000);
timeoutManager.createTimeout("action2", () => {
console.log("Action 2 completed");
}, 2000);
// Clean up on component unmount
return () => {
timeoutManager.clearTimeout("action1");
timeoutManager.clearTimeout("action2");
};
}, []);
return (
// Your component JSX
);
}
`$3
You can also create your own timeout manager instance:
`tsx
import { createTimeoutManager } from "@darkcore/game-library";
import { useEffect } from "react";function GameComponent() {
useEffect(() => {
const timeoutManager = createTimeoutManager();
// Set multiple timers with IDs
timeoutManager.createTimeout("action1", () => {
console.log("Action 1 completed");
}, 1000);
timeoutManager.createTimeout("action2", () => {
console.log("Action 2 completed");
}, 2000);
// Clean up on component unmount
return () => {
timeoutManager.clearAll();
};
}, []);
return (
// Your component JSX
);
}
`$3
`typescript
export function createTimeoutManager() {
return {
/**
* Create or restart a timeout with a unique ID
* @param id Unique identifier for the timeout
* @param callback Function to execute when timer completes
* @param delay Delay in milliseconds
*/
createTimeout(id: string, callback: () => void, delay: number): void;
/**
* Clear a timeout by its ID
* @param id Timeout identifier
*/
clearTimeout(id: string): void;
/**
* Pause all active timeouts
*/
pauseAll(): void;
/**
* Resume all paused timeouts
*/
resumeAll(): void;
/**
* Clear all timeouts
*/
clearAll(): void;
/**
* Check if a timeout exists
* @param id Timeout identifier
* @returns True if timeout exists
*/
hasTimeout(id: string): boolean;
/**
* Get number of active timeouts
* @returns Number of managed timeouts
*/
size(): number;
};
}
`Tick System
The library provides a shared tick system that optimizes performance by sharing a single ticker across multiple components, automatically handling browser tab visibility changes.
$3
The
useSharedTickManager hook creates a shared ticker instance that can be used by multiple components. This is more efficient than creating individual tickers for each component.`tsx
import { useSharedTickManager } from "@darkcore/game-library";function App() {
// Start shared tick manager once at the top level
useSharedTickManager();
return (
{/ All child components can now use useSharedTick /}
);
}
`$3
Components can use the shared ticker via the
useSharedTick hook:`tsx
import { useSharedTick } from "@darkcore/game-library";
import { useState } from "react";function AnimatedComponent() {
const [rotation, setRotation] = useState(0);
useSharedTick((deltaMS) => {
// deltaMS accounts for pause time
setRotation(prev => prev + (deltaMS * 0.001));
});
return ;
}
`$3
- Shared Performance: Single ticker shared across all components
- Auto Pause/Resume: Pauses when tab is hidden, resumes when visible
- Accurate Timing: Accounts for paused time to maintain consistent speed
- Easy Integration: Simple drop-in replacement for
useTick$3
`typescript
// Initialize shared tick manager (call once at app level)
export function useSharedTickManager(): void;// Use shared tick in components
export function useSharedTick(onTick: (deltaMS: number) => void): void;
`This ensures animations continue correctly regardless of tab visibility changes while maintaining optimal performance.
Custom UI Components
$3
The
CustomButton component provides an advanced, interactive button with smooth press animations and multiple visual layers.#### Features
- Smooth Press Animation: Y-axis animation when button is pressed/released
- Hover Effect: Visual feedback with tint change when mouse hovers over button
- External Hover Control: Support for external hover state management
- Hover Event Callbacks: Optional hover event handlers for custom interactions
- Multi-layer Design: Support for plate, mask, image, and text layers
- Disabled State: Built-in disabled state handling with separate disabled image
- Interactive: Pointer events with cursor changes
- Customizable: All visual elements can be customized
#### Usage
`tsx
import { CustomButton } from "@darkcore/game-library";
import { useGetImages } from "@darkcore/game-library";
import * as PIXI from "pixi.js";export default function ButtonExample() {
const images = useGetImages();
const [isDisabled, setIsDisabled] = useState(false);
return (
position={[100, 100]}
scale={[1, 1]}
anchor={[0.5, 0.5]}
disabled={isDisabled}
onClick={() => {
console.log("Button clicked!");
setIsDisabled(!isDisabled);
}}
onHover={(hovered) => {
console.log("Button hover:", hovered);
}}
externalHovered={false}
buttonPlateProps={{
x: 0,
y: 0,
anchor: 0.5,
image: images.buttonPlate,
}}
buttonMaskProps={{
x: 0,
y: 0,
anchor: 0.5,
image: images.buttonMask,
buttonImagePressedY: 20, // Y offset when pressed
}}
buttonImageProps={{
x: 0,
y: 0,
anchor: 0.5,
image: images.greenButton,
disabledImage: images.grayButton,
}}
textProps={{
x: 0,
y: -10,
anchor: 0.5,
text: "PLAY",
style: {
fontFamily: "Inter",
fontSize: 24,
fill: "#ffffff",
} as PIXI.TextStyle
}}
/>
);
}
`#### Props
`typescript
interface CustomButtonType {
/* Button position [x, y] /
position?: [number, number];
/* Button scale [x, y] /
scale?: [number, number];
/* Button anchor [x, y] /
anchor?: [number, number];
/* Disable button interactions /
disabled: boolean;
/* Click handler function /
onClick: () => void;
/* Hover event handler (called when pointer enters/leaves) /
onHover?: (hovered: boolean) => void;
/* External hover state control /
externalHovered?: boolean;
/* Props for the button plate (background layer) /
buttonPlateProps?: React.ComponentProps;
/* Props for the button mask with press animation /
buttonMaskProps?: React.ComponentProps & {
/* Y offset when button is pressed /
buttonImagePressedY: number;
};
/* Props for the main button image /
buttonImageProps?: React.ComponentProps & {
/* Image to show when button is disabled /
disabledImage?: string;
};
/* Props for the button text /
textProps?: React.ComponentProps;
/* Child components to render inside the button /
children?: React.ReactNode;
/* Enable mask test mode to visualize the button mask (debug feature) /
maskTestMode?: boolean;
}
`#### Animation System
The CustomButton uses a smooth animation system:
1. Press Animation: When pressed, the button image moves to
buttonImagePressedY position
2. Release Animation: When released, returns to original position
3. Smooth Interpolation: Uses useTick for frame-based animation with 20% lerp factor
4. Automatic Reset: On pointer leave, immediately resets to original position#### Visual Layers
The button consists of multiple layers (from bottom to top):
1. Button Plate: Background/base layer
2. Button Mask: Defines the clipping area for the button image
3. Button Image: Main visual element (gets masked and animated, automatically switches to disabledImage when disabled)
4. Button Text: Text overlay
#### States
- Normal: Default interactive state
- Hover: Pointer over button with visual tint effect (maintains press capability)
- Pressed: During pointer down (image moves down)
- Disabled: Non-interactive state (cursor changes to default)
#### Debug Features
##### Mask Test Mode
The button includes a debug feature to visualize the mask area. When
maskTestMode is enabled, a semi-transparent red overlay shows the exact mask boundaries:`tsx
maskTestMode={true} // Enable debug mode
buttonMaskProps={{
x: 0,
y: 0,
anchor: 0.5,
image: images.buttonMask,
buttonImagePressedY: 20,
}}
// ... other props
/>
`#### Button Hooks
##### useAnimatedPosition
The
useAnimatedPosition hook provides smooth position animation for any UI element, with customizable easing functions and duration.Features:
- Smooth Position Transition: Animated movement between target positions
- Customizable Duration: Configurable animation duration in milliseconds
- Easing Support: Built-in smooth step easing with option for custom easing functions
- Frame-based Animation: Uses @pixi/react's useTick for consistent 60fps animations
- Automatic Completion: Automatically stops when animation reaches target
- Interruption Handling: Smoothly handles new targets during ongoing animations
Usage:
`tsx
import { useAnimatedPosition } from "@darkcore/game-library";
import { Container, Sprite } from "@pixi/react";export default function AnimatedElement() {
const [targetPos, setTargetPos] = useState<[number, number]>([100, 100]);
const images = useGetImages();
const { position, isAnimating } = useAnimatedPosition({
targetPosition: targetPos,
duration: 500, // 500ms animation
easing: (t) => t t (3 - 2 * t) // smooth step (default)
});
const handleClick = () => {
const newX = Math.random() * 800;
const newY = Math.random() * 600;
setTargetPos([newX, newY]);
};
return (
image={images.button}
position={position}
anchor={0.5}
interactive={true}
onpointerdown={handleClick}
alpha={isAnimating ? 0.8 : 1.0}
/>
);
}
`API Reference:
`typescript
export type Vec2 = [number, number];interface UseAnimatedPositionConfig {
/* Target position [x, y] to animate towards /
targetPosition: Vec2;
/* Animation duration in milliseconds (default: 200) /
duration?: number;
/* Easing function (t: 0-1) => (0-1), default: smooth step /
easing?: (t: number) => number;
}
interface UseAnimatedPositionResult {
/* Current animated position [x, y] /
position: Vec2;
/* Whether animation is currently running /
isAnimating: boolean;
}
export function useAnimatedPosition(config: UseAnimatedPositionConfig): UseAnimatedPositionResult;
`Default Easing:
- The default easing function is smooth step:
t t (3 - 2 * t)
- Provides natural acceleration and deceleration
- Custom easing functions can be provided for different animation stylesAnimation Behavior:
- When
targetPosition changes, automatically starts new animation from current position
- If animation is already running when target changes, current position becomes new start point
- Animation completes when time reaches specified duration
- Position updates every frame using Pixi's render loop##### useModalAnimation
The
useModalAnimation hook provides smooth modal opening/closing animations with scale and alpha transitions, supporting custom easing functions and configurable delays.Features:
- Scale & Alpha Animation: Smooth transitions for both scale and opacity
- Custom Easing: Built-in smooth step easing with option for custom easing functions
- Configurable Delays: Optional delay before animation starts
- State Tracking: Provides animation state and completion status
- Frame-based Animation: Uses @pixi/react's useTick for consistent 60fps animations
- Flexible Configuration: Customizable open/closed values for scale and alpha
Usage:
`tsx
const { scale, alpha, isAnimating, isFullyOpened, isFullyClosed } = useModalAnimation({
isOpened: isOpen,
duration: 300,
openScale: 1,
closedScale: 0.8,
openAlpha: 1,
closedAlpha: 0,
delay: 100,
easing: (t) => t t (3 - 2 * t) // smooth step (default)
});
`API Reference:
`typescript
export interface ModalAnimationConfig {
/* Whether modal should be in opened state /
isOpened: boolean;
/* Animation duration in milliseconds (default: 300) /
duration?: number;
/* Easing function (t: 0-1) => (0-1), default: smooth step /
easing?: (t: number) => number;
/* Scale value when modal is open (default: 1) /
openScale?: number;
/* Scale value when modal is closed (default: 0.8) /
closedScale?: number;
/* Alpha value when modal is open (default: 1) /
openAlpha?: number;
/* Alpha value when modal is closed (default: 0) /
closedAlpha?: number;
/* Delay before animation starts in milliseconds (default: 0) /
delay?: number;
}export interface ModalAnimationResult {
/* Current animated scale value /
scale: number;
/* Current animated alpha value /
alpha: number;
/* Whether animation is currently running /
isAnimating: boolean;
/* Whether modal is fully opened (not animating and at open values) /
isFullyOpened: boolean;
/* Whether modal is fully closed (not animating and at closed values) /
isFullyClosed: boolean;
}
export function useModalAnimation(config: ModalAnimationConfig): ModalAnimationResult;
`Default Values:
- Duration: 300ms
- Open Scale: 1
- Closed Scale: 0.8
- Open Alpha: 1
- Closed Alpha: 0
- Delay: 0ms
- Easing: smooth step function
t t (3 - 2 * t)Animation Behavior:
- When
isOpened changes, automatically starts animation with optional delay
- Animates from current values to target values smoothly
- Provides state flags for managing modal visibility and interactions
- Animation updates every frame using Pixi's render loop$3
The
CustomPanel component provides a flexible panel container with background image and optional header text support.#### Features
- Flexible Layout: Customizable position, scale, and anchor points
- Background Support: Sprite-based background with full customization
- Header Text: Optional header text with independent styling
- Child Components: Support for nested components within the panel
- Interactive: Click and hover event support with pointer interactions
- State Management: Built-in press state handling for visual feedback
- Responsive: Handles different anchor points and scaling options
#### Usage
`tsx
import { CustomPanel } from "@darkcore/game-library";
import { useGetImages } from "@darkcore/game-library";
import * as PIXI from "pixi.js";export default function PanelExample() {
const images = useGetImages();
const [isDisabled, setIsDisabled] = useState(false);
return (
position={[400, 300]}
scale={1.5}
anchor={0.5}
disabled={isDisabled}
onClick={() => {
console.log("Panel clicked!");
setIsDisabled(!isDisabled);
}}
onHover={(hovered) => {
console.log("Panel hover:", hovered);
}}
bgSpriteProps={{
x: 0,
y: 0,
anchor: 0.5,
image: images.panelBg,
}}
headerTextProps={{
x: 0,
y: -100,
anchor: 0.5,
text: "GAME SETTINGS",
style: {
fontFamily: "Inter",
fontSize: 32,
fill: "#ffffff",
} as PIXI.TextStyle
}}
>
{/ Child components go here /}
x={0}
y={0}
anchor={0.5}
text="Panel Content"
style={{ fontSize: 24, fill: "#ffffff" } as PIXI.TextStyle}
/>
);
}
`#### Props
`typescript
interface CustomPanelType {
/* Panel position [x, y] /
position?: [number, number];
/* Panel scale (number for uniform, [x, y] for separate axes) /
scale?: number | [number, number];
/* Panel anchor (number for uniform, [x, y] for separate axes) /
anchor?: number | [number, number];
/* Panel alpha/opacity (0-1) /
alpha?: number;
/* Disable panel interactions /
disabled?: boolean;
/* Props for the optional header text /
headerTextProps?: React.ComponentProps;
/* Props for the background sprite (required) /
bgSpriteProps: React.ComponentProps;
/* Child components to render inside the panel /
children?: React.ReactNode;
/* Click handler function /
onClick?: () => void;
/* Hover event handler (called when pointer enters/leaves) /
onHover?: (hovered: boolean) => void;
}
`#### Structure
The CustomPanel creates a layered structure:
1. Container: Main container with position, scale, and anchor
2. Background Sprite: Named "PanelBg" for easy identification
3. Header Text: Optional text element positioned above content
4. Children: Custom content rendered last (on top)
$3
The
CustomCounter component provides an interactive value selector with increment/decrement buttons from a predefined list of values.#### Features
- Predefined Values: Works with an array of string values (e.g., bet amounts, mine counts)
- Interactive Buttons: Plus and minus buttons with customizable styling
- Number Formatting: Optional currency and locale formatting support
- State Management: Internal state management with external change callbacks
- Button States: Automatic button disabling when limits are reached
- Flexible Layout: Customizable positioning and scaling
#### Type Definitions
`typescript
export interface CustomCounterType {
position?: [number, number];
scale?: number | [number, number];
anchor?: number | [number, number];
values: string[];
startValue: string;
onChange: ({value, index}: {value: string, index: number}) => void;
textProps: React.ComponentProps;
plusButtonProps: React.ComponentProps;
minusButtonProps: React.ComponentProps;
}
`#### Usage
`tsx
import { CustomCounter, numberFormatter } from "@darkcore/game-library";
import { useGetImages } from "@darkcore/game-library";
import * as PIXI from "pixi.js";export default function CounterExample() {
const images = useGetImages();
const [currentValue, setCurrentValue] = useState("1.00");
const buttonProps = {
position: [0, 0] as [number, number],
scale: 0.8,
disabled: false,
onClick: () => {},
buttonPlateProps: {
x: 0,
y: 0,
anchor: 0.5,
image: images.buttonPlate,
},
buttonMaskProps: {
x: 0,
y: 0,
anchor: 0.5,
image: images.buttonMask,
buttonImagePressedY: 15,
},
buttonImageProps: {
x: 0,
y: 0,
anchor: 0.5,
image: images.greenButton,
disabledImage: images.grayButton,
},
textProps: {
x: 0,
y: -5,
anchor: 0.5,
text: "+",
style: {
fontFamily: "Inter",
fontSize: 20,
fill: "#ffffff",
} as PIXI.TextStyle
}
};
return (
position={[400, 300]}
scale={[1, 1]}
anchor={0.5}
values={["0.50", "1.00", "2.50", "5.00", "10.00"]}
startValue="1.00"
onChange={({ value, index }) => {
setCurrentValue(newValue);
console.log("Counter value changed:", value);
console.log("Counter index changed:", index);
}}
textProps={{
x: 0,
y: -50,
anchor: 0.5,
text: "", // Will be overridden with formatted value
style: {
fontFamily: "Inter",
fontSize: 24,
fill: "#ffffff",
} as PIXI.TextStyle
}}
plusButtonProps={{
...buttonProps,
position: [80, 20],
}}
minusButtonProps={{
...buttonProps,
position: [-80, 20],
textProps: {
...buttonProps.textProps,
text: "-"
}
}}
/>
);
}
`#### Props
`typescript
interface CustomCounterType {
/* Counter position [x, y] /
position?: [number, number];
/* Counter scale (number for uniform, [x, y] for separate axes) /
scale?: number | [number, number];
/* Counter anchor (number for uniform, [x, y] for separate axes) /
anchor?: number | [number, number];
/* Array of selectable values as strings /
values: string[];
/* Initial/starting value (must exist in values array) /
startValue: string;
/* Currency code for number formatting (e.g., "USD", "EUR") /
currency?: string;
/* Callback when counter value changes /
onChange: (value: string) => void;
/* Props for the counter value text /
textProps: React.ComponentProps;
/* Props for the plus/increment button /
plusButtonProps: React.ComponentProps;
/* Props for the minus/decrement button /
minusButtonProps: React.ComponentProps;
}
`#### Number Formatting
The
numberFormatter function provides locale-aware number formatting with currency support:`tsx
import { numberFormatter } from "@darkcore/game-library";// Example formatter usage
const formattedValue = numberFormatter({
number: 1234.56,
locale: "en-US",
style: "currency",
currency: "USD",
maximumFractionDigits: 2
});
// Result: "$1,234.56"
`#### Behavior
- Value Management: Maintains internal state with array index tracking
- Boundary Checking: Disables buttons when at first/last values in array
- Change Callbacks: Calls
onChange with new string value when buttons are clicked
- Button Customization: Both plus and minus buttons support full CustomButton props#### Structure
The CustomCounter creates the following structure:
1. Container: Main container with position, scale, and anchor
2. Value Text: Displays current counter value (formatted if formatter provided)
3. Plus Button: Increment button (moves to next value in array)
4. Minus Button: Decrement button (moves to previous value in array)
#### Example Usage with Panel
`tsx
// Complete example combining CustomPanel and CustomCounter (Mine Counter)
export const MineCounter = ({ cellCenterPositions }: MineCounterProps) => {
const images = useGetImages();
const [mineCount, setMineCount] = useState("0"); const buttonProps = {
position: [0, 0] as [number, number],
scale: 0.5,
disabled: false,
onClick: () => {},
buttonPlateProps: {
x: 0,
y: 0,
anchor: 0.5,
image: images.buttonPlate,
},
buttonMaskProps: {
x: 0,
y: 0,
anchor: 0.5,
image: images.buttonMask,
buttonImagePressedY: 20,
},
buttonImageProps: {
x: 0,
y: 0,
anchor: 0.5,
image: images.greenButton,
disabledImage: images.grayButton,
},
textProps: {
x: 0,
y: -100,
anchor: 0.5,
scale: 2,
text: "",
style: {
fontFamily: "Inter",
fontSize: 200,
fill: "#ffffff",
} as PIXI.TextStyle
}
};
return (
bgSpriteProps={{
x: cellCenterPositions[0].x,
y: cellCenterPositions[0].y,
anchor: 0.5,
scale: 1.7,
image: images.panelBg,
}}
headerTextProps={{
x: cellCenterPositions[0].x,
y: cellCenterPositions[0].y - 250,
anchor: 0.5,
scale: 1,
text: "MINES",
style: {
fontFamily: "Inter",
fontSize: 120,
fill: "#ffffff",
} as PIXI.TextStyle
}}
>
position={[cellCenterPositions[0].x, cellCenterPositions[0].y + 200]}
scale={[0.8, 0.8]}
values={["0", "1", "2", "3", "4"]}
startValue="1"
onChange={(value) => {
setMineCount(value);
console.log("Mine count changed:", value);
}}
textProps={{
x: 0,
y: -270,
anchor: 0.5,
scale: 2,
style: {
fontFamily: "Inter",
fontSize: 110,
fill: "#ffffff",
} as PIXI.TextStyle
}}
plusButtonProps={{
...buttonProps,
position: [185, 50],
}}
minusButtonProps={{
...buttonProps,
position: [-185, 50],
}}
/>
);
};
`$3
The
CustomProfit component provides an advanced profit display with win/loss animations, value increment animations, and win text effects using the shared tick system for optimal performance.#### Features
- Win/Loss Toggle: Alternates between win text and profit value display
- Value Increment Animation: Smooth animation when profit value increases
- Instant Value Decrease: Immediate update when profit value decreases
- Win Text Scale Animation: Optional scale animation for win text with customizable easing
- Multiple Win Cycles: Support for multiple win text animations with configurable count
- Number Formatting: Configurable number formatter function for currency display with locale support
- Configurable Timings: Customizable animation durations and pause intervals
- Shared Tick Performance: Uses shared tick system for optimal performance
- Smooth Transitions: Frame-based animations with proper handling of rapid value changes
#### Usage
`tsx
import { CustomProfit } from "@darkcore/game-library";
import { useGetImages } from "@darkcore/game-library";
import * as PIXI from "pixi.js";
import { numberFormatter } from "./utils"export default function ProfitExample() {
const images = useGetImages();
const [profitValue, setProfitValue] = useState(0);
const [isWin, setIsWin] = useState(false);
return (
customPanelProps={{
position: [400, 100],
scale: 1.2,
anchor: 0.5,
onClick: () => {
setIsWin(!isWin);
setProfitValue(profitValue + 1.24);
},
bgSpriteProps: {
x: 0,
y: 0,
anchor: 0.5,
image: images.profitBg,
}
}}
valueTextProps={{
x: 0,
y: 0,
anchor: 0.5,
style: {
fontFamily: "Inter",
fontSize: 24,
fill: "#ffffff",
} as PIXI.TextStyle
}}
winTextProps={{
x: 0,
y: 0,
anchor: 0.5,
style: {
fontFamily: "Inter",
fontSize: 28,
fill: "#00ff00",
} as PIXI.TextStyle
}}
goodLuckTextProps={{
x: 0,
y: 0,
anchor: 0.5,
style: {
fontFamily: "Inter",
fontSize: 24,
fill: "#ffff00",
} as PIXI.TextStyle
}}
numberFormatter={numberFormatter} // Currency formatting function
currency={"USD"}
profitValue={profitValue}
winText="WIN!"
goodLuckText="GOOD LUCK!"
isGoodLuck={false}
isWin={isWin}
winCount={2} // Number of win text cycles
pauseDuration={600} // Pause between animations
counterAnimationDuration={250} // Duration for value increment
enableTextScaleAnimation={true} // Enable win text scale animation
scaleAmplitude={0.4} // Scale amplitude for win text animation
decimalPlaces={2} // Number of decimal places for counter
/>
);
}
`#### Props
`typescript
interface CustomProfitType {
/* Props for the CustomPanel container /
customPanelProps: React.ComponentProps;
/* Props for the profit value text /
valueTextProps: React.ComponentProps;
/* Props for the win text /
winTextProps: React.ComponentProps;
/* Props for the good luck text /
goodLuckTextProps: React.ComponentProps;
/* Number formatter function for currency display /
numberFormatter: ({ number, locale, style, currency, maximumFractionDigits }: NumberFormatterType) => string;
/* Currency code for number formatting (e.g., "USD", "EUR") /
currency: string;
/* Current profit value to display /
profitValue: number;
/* Text to display when showing win state /
winText: string;
/* Text to display when showing good luck state /
goodLuckText: string;
/* Whether to show good luck text /
isGoodLuck: boolean;
/* Whether to show win animation /
isWin: boolean;
/* Number of win text animation cycles (default: 1) /
winCount?: number;
/* Duration between win text and value text phases (ms) /
pauseDuration: number;
/* Duration for value increment animation (ms) /
counterAnimationDuration: number;
/* Enable/disable win text scale animation /
enableTextScaleAnimation?: boolean;
/* Scale amplitude for win text animation (default: 0.4) /
scaleAmplitude?: number;
/* Number of decimal places for counter animation (default: 2) /
decimalPlaces?: number;
}
`#### Animation System
The CustomProfit component features multiple animation systems with sophisticated phase management:
##### Text State Display
The component displays different text based on state priority:
1. Win State: When
isWin is true, alternates between win text and profit value
2. Good Luck State: When isGoodLuck is true and isWin is false, shows good luck text
3. Normal State: When neither isWin nor isGoodLuck is true, shows the profit value##### Animation Phase Flow
The component uses a four-phase animation system:
1. Count Phase: Animates profit value changes over
counterAnimationDuration
2. Pre-pause Phase: Shows final value for pauseDuration before win text
3. Scale Phase: Displays win text with optional scale animation
4. Pause Phase: Shows profit value between win text cycles##### Value Increment Animation
- When
profitValue increases: Smooth animation over counterAnimationDuration
- When profitValue decreases: Instant update (typically when resetting to 0)
- Handles rapid value changes by completing current animation to target, then starting new animation##### Win Text Scale Animation
- Optional scale animation when
enableTextScaleAnimation is true
- Uses ease-in-out curve for natural motion: p < 0.5 ? 2 p p : 1 - Math.pow(-2 * p + 2, 2) / 2
- Scale range: base scale to base scale + scaleAmplitude (default: 0.4)
- Configurable amplitude via scaleAmplitude prop
- Runs for each win text cycle defined by winCount##### Multiple Win Cycles
- When
winCount is greater than 1, win text animation repeats multiple times
- Each cycle consists of scale animation followed by pause showing profit value
- Cycles through: Scale → Pause → Scale → Pause → ... until winCount is reached$3
The
CustomHistory component provides an animated history display system that shows a list of values with smooth animations when new entries are added or removed. The component is optimized with React.memo for performance.#### Features
- Smooth Animations: Frame-based animations with easing for position and scale
- Vertical/Horizontal Layout: Configurable orientation for different layouts
- Maximum Count Limit: Automatically removes old entries when limit is reached
- Duplicate Support: Handles duplicate values with unique identification
- Configurable Spacing: Adjustable spacing between history items
- Background Support: Optional background sprites for each history item
- Animation Tuning: Customizable easing factors and animation thresholds
- Performance Optimized: Uses React.memo for efficient re-renders
- Memoized Items: Individual history items are memoized for optimal performance
#### Usage
`tsx
import { CustomHistory } from "@darkcore/game-library";
import { useGetImages } from "@darkcore/game-library";
import * as PIXI from "pixi.js";export default function HistoryExample() {
const images = useGetImages();
const [historyValues, setHistoryValues] = useState([]);
const addHistoryValue = () => {
const newValue =
${Math.floor(Math.random() * 10) + 1}x;
setHistoryValues([...historyValues, newValue]);
}; return (
position={[400, 300]}
historyValues={historyValues}
maxCount={6}
isVertical={true}
betweenSpacing={80}
historyValueBgSpriteProps={{
x: 0,
y: 0,
anchor: 0.5,
scale: 0.8,
image: images.historyTextBg,
}}
historyValueTextProps={{
x: 0,
y: 0,
anchor: 0.5,
style: {
fontFamily: "Inter",
fontSize: 20,
fill: "#ffffff",
} as PIXI.TextStyle
}}
animationConfig={{
easingFactor: 0.12,
scaleThreshold: 0.01,
positionThreshold: 0.1,
}}
/>
);
}
`#### Props
`typescript
interface CustomHistoryType {
/* History container position [x, y] /
position?: [number, number];
/* History scale (number for uniform, [x, y] for separate axes) /
scale?: number | [number, number];
/* History anchor (number for uniform, [x, y] for separate axes) /
anchor?: number | [number, number];
/* Array of history values to display /
historyValues: any[];
/* Optional props for history item background sprites /
historyValueBgSpriteProps?: React.ComponentProps;
/* Props for history item text /
historyValueTextProps: React.ComponentProps;
/* Whether to arrange items vertically or horizontally /
isVertical?: boolean;
/* Spacing between history items in pixels /
betweenSpacing?: number;
/* Maximum number of history items to display /
maxCount?: number;
/* Animation configuration options /
animationConfig?: {
/* Easing speed factor (0-1, lower = slower) /
easingFactor?: number;
/* Scale animation threshold for completion /
scaleThreshold?: number;
/* Position animation threshold for completion /
positionThreshold?: number;
};
}
`#### Animation System
The CustomHistory uses the
useHistoryAnimation hook that provides sophisticated animation handling:##### New Entry Animation
- Scale Animation: New entries start with scale 0 and animate to scale 1
- Position Animation: New entries start off-screen and slide into position
- Smooth Easing: Uses configurable easing factor for natural motion
##### Position Updates
- Existing Items: When new entries are added, existing items smoothly move to new positions
- Removal Animation: When items exceed
maxCount, oldest items are removed smoothly
- Duplicate Handling: Duplicate values are treated as separate entries with unique IDs##### Animation Configuration
`typescript
const DEFAULT_CONFIG = {
easingFactor: 0.12, // Animation speed (0.05 = slow, 0.2 = fast)
scaleThreshold: 0.01, // When to complete scale animation
positionThreshold: 0.1, // When to complete position animation
};
`#### Advanced Usage with Panel
`tsx
export const HistoryBoard = ({ cellCenterPositions }: HistoryBoardProps) => {
const images = useGetImages();
const [historyValues, setHistoryValues] = useState([]); return (
bgSpriteProps={{
x: cellCenterPositions[0].x,
y: cellCenterPositions[0].y,
anchor: 0.5,
image: images.historyBg,
interactive: true,
onpointerdown: () => {
const newValue = (historyValues[historyValues.length - 1] || 0) + 1;
setHistoryValues([...historyValues, newValue]);
}
}}
headerTextProps={{
text: "History",
x: cellCenterPositions[0].x,
y: cellCenterPositions[0].y - 300,
anchor: 0.5,
style: {
fontSize: 65,
fill: "#ffffff",
fontFamily: "Inter"
} as PIXI.TextStyle,
}}
>
historyValues={historyValues.map(value => value.toString() + "x")}
position={[cellCenterPositions[0].x / 2, cellCenterPositions[0].y / 2 - 92]}
maxCount={6}
isVertical={true}
betweenSpacing={95}
historyValueBgSpriteProps={{
x: 0,
y: 0,
anchor: 0.5,
scale: 0.8,
image: images.historyTextBg,
}}
historyValueTextProps={{
x: 0,
y: 0,
anchor: 0.5,
style: {
fontSize: 60,
fill: "#ffffff",
fontFamily: "Inter"
} as PIXI.TextStyle,
}}
/>
);
};
`#### Animation Hook Details
The component internally uses
useHistoryAnimation which provides:- Duplicate Detection: Uses original array indices to uniquely identify entries
- Smooth Transitions: Frame-based animations with
useTick from @pixi/react
- Performance Optimization: Efficient diffing algorithm to minimize unnecessary updates
- State Management: Maintains animation state across renders$3
The
CustomWinModal component provides a win popup modal with dual spine animations for different types of wins. The component uses the shared tick system for optimal performance and timeout management for precise timing control.#### Features
- Multiple Win Types: Supports bigWin, megaWin, hugeWin, and epicWin animations
- Dual Spine Animations: Uses two separate spines for main popup and coin effects
- Animation Sequencing: Plays start animation then loops end animation
- Amount Text Display: Shows formatted win amount with configurable delay and styling
- Text Animation: Smooth pulsing animation for amount text using shared tick system
- Timeout Management: Uses timeout manager for precise timing and cleanup
- State Management: React state for active, clickable, and text visibility states
- Configurable Timing: Customizable animation durations and instant play mode
- Click to Close: Interactive modal with cooldown protection
- Auto-complete: Automatically completes after animation sequence
- Performance Optimized: Uses shared tick system and optimized state management
#### Usage
``tsxexport default function WinModalExample() {
const spines = useGetSpines();
const [showWin, setShowWin] = useState(false);
const [winType, setWinType] = useState<"bigWin" | "megaWin" | "hugeWin" | "epicWin">("bigWin");
const [winAmount, setWinAmount] = useState(1000);
const winAnimations = {
bigWin: {
startAnimationName: "big_win_start",
loopAnimationName: "big_win_loop",
coinStartAnimationName: "coin_start",
coinLoopAnimationName: "coin_loop"
},
megaWin: {
startAnimationName: "mega_win_start",
loopAnimationName: "mega_win_loop",
coinStartAnimationName: "coin_start",
coinLoopAnimationName: "coin_loop"
},
hugeWin: {
startAnimationName: "huge_win_start",
loopAnimationName: "huge_win_loop",
coinStartAnimationName: "coin_start",
coinLoopAnimationName: "coin_loop"
},
epicWin: {
startAnimationName: "epic_win_start",
loopAnimationName: "epic_win_loop",
coinStartAnimationName: "coin_start",
coinLoopAnimationName: "coin_loop"
}
};
const winPopupConfig = {
animation: "",
loop: false,
scale: 1,
position: [0, 0],
timeScale