@darkcore/game-library
v1.9.29
Published
## 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
Readme
DracoFusion Game Library
Table of Contents
- Overview
- Installation
- Integration
- Asset Loading
- AutoPlay
- Responsive Grid
- Spine Animation
- Timers System
- Shared Tick System
- Custom UI Components
- Utility Hooks
Overview
DracoFusion is a game development library built on top of PixiJS, providing components and utilities for creating web-based games with @pixi/react.
Installation
Version Requirements
This library requires specific versions of its peer dependencies:
"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"
}React Version Resolution
If you encounter React version conflicts, update your Vite configuration:
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")
},
},
})Installation Steps
# 1. Cleanup old installs
rm -rf node_modules pnpm-lock.yaml
pnpm store prune
# 2. Install peer dependencies with exact versions
pnpm add @pixi/[email protected] [email protected] [email protected] [email protected]
# 3. Install everything
pnpm install
# 4. Verify only one @pixi/react is present
pnpm why @pixi/reactIntegration Guide
Application Structure
DracoFusion uses a layered approach:
- DarkCoreProvider: Top-level provider for theming
- Board & BoardContent: Layout components from @darkcore/ui
- GameCanvas: The Stage capsule for @pixi/react components
- GameBoard: Your custom @pixi/react components
Basic implementation:
import { DarkCoreProvider, Board, BoardContent } from "@darkcore/ui";
import GameCanvas from "./components/GameCanvas";
import { createRoot } from "react-dom/client";
createRoot(document.getElementById("root")!).render(
<DarkCoreProvider theme={{ /* theme options */ }}>
<Board>
<BoardContent>
<GameCanvas />
</BoardContent>
</Board>
</DarkCoreProvider>
);GameCanvas Component
The GameCanvas component manages the WebGL rendering context:
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 (
<GameCanvasProvider
settings={{
width: canvasWidth,
height: canvasHeight,
options: STAGE_OPTIONS
}}
>
<Container>
{/* Your @pixi/react components */}
</Container>
</GameCanvasProvider>
);
}GameCanvasProvider Configuration
// Types
export type SettingsProps = {
/** canvas pixel width */
width: number;
/** canvas pixel height */
height: number;
/** Partial PIXI.ApplicationOptions */
options: Partial<IApplicationOptions>;
/** 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.
Asset Types
// Type definitions
export type ImagesResult = Record<string, string>;
export type SpinesResult = Record<string, Spine>;
export type SoundsResult = Record<string, Howl>;
export type FontsResult = Record<string, { url: string; weight: string }>;
export type AssetsRequest = Record<string, string>;
export type FontsRequest = Record<string, { url: string; weight: string }>;Asset Configuration
Define your assets in a file:
// 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;
}Asset Loader Hook
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;Asset Access Hooks
// 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;
}Game State Store
// store/game.ts
import { create } from "zustand";
type GameState = {
isAllAssetsLoaded: boolean;
canvasWidth: number;
canvasHeight: number;
};
export const useGameStore = create<GameState>(() => ({
isAllAssetsLoaded: false,
canvasWidth: 0,
canvasHeight: 0,
}));Game Initialization Hook
// 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]);
};Application Initialization
// components/IFrameGuard.tsx
import { useInitializeGame } from "@/hooks/useInitializeGame";
import Layout from "../_Layout/_Layout";
export default function IFrameGuard() {
// Initialize game assets
useInitializeGame();
return <Layout />;
}Note: Place initialization code and one-time setup logic in the IFrameGuard component to prevent unnecessary re-renders.
// 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 && <Loader
isVisible={loading}
size={40}
placeholder="Just a moment"
/>}
<Board>
<BoardContent>
{/* GameCanvas renders only when assets are loaded */}
{isAllAssetsLoaded && <GameCanvas />}
</BoardContent>
</Board>
</>
);
}Using Assets in Components
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 (
<Container>
<Sprite
image={images.background}
interactive={true}
onpointerdown={handleOnPointerDown}
/>
{/* Fonts can be used directly as strings */}
<Text
text="Hello World"
style={{ fill: '#ffffff', fontFamily: 'Inter' } as TextStyle}
/>
</Container>
);
}AutoPlay System
The AutoPlay system provides an automated gameplay mechanism for games requiring repetitive actions.
AutoPlay Class
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();AutoPlay Type Definitions
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<void>;
/** 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;
}AutoPlay Methods
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;
}Implementation Example
AutoPlay Store
// store/autoplay.ts
import { create } from 'zustand'
interface AutoPlayStore {
autoPlayStarted: boolean
stopRequested: boolean
totalProfit: number
startAutoBet: () => void
stopAutoBet: () => void
}
export const useAutoPlayStore = create<AutoPlayStore>()((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
// 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<AutoPlay | null>(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
import { useAutoPlay } from "@/hooks/useAutoPlay";
import { useAutoPlayStore } from "@/store/autoplay";
export default function GameControls() {
// Initialize the autoplay hook
useAutoPlay();
const { autoPlayStarted, stopRequested } = useAutoPlayStore();
return (
<div>
<button
onClick={() => {
if (autoPlayStarted) {
useAutoPlayStore.getState().stopAutoBet();
} else {
useAutoPlayStore.getState().startAutoBet();
}
}}
>
{autoPlayStarted
? (stopRequested ? "Stopping..." : "Stop Auto")
: "Start Auto"}
</button>
</div>
);
}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.
Usage Example
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]} */}
<Container position={containerPosition} scale={scale}>
{cellCenterPositions.map((pt, i) => (
<Sprite key={i} texture={cellTexture} x={pt.x} y={pt.y} anchor={0.5} />
))}
</Container>
{/* Example UI panel at right edge */}
<Container
position={[gridEdges.right.x + 20, gridEdges.right.y]}
scale={scale}
>
<Sprite texture={uiTexture} anchor={0.5} />
</Container>
</>
);API
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).
GameSpine Component
GameSpine is a reusable component that manages Spine animations within a Pixi Container:
import { GameSpine } from "@darkcore/game-library";
import { useGetSpines } from "@darkcore/game-library";
export default function SpineExample() {
const spines = useGetSpines();
return (
<GameSpine
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}
/>
);
}GameSpine Props
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<Spine | null>;
}Spine Animation Info Hook
The library provides a hook to extract animation information from a loaded Spine asset:
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:
export interface SpineAnimationInfo {
/** Animation name as defined in the Spine data */
name: string;
/** Duration of the animation in seconds */
duration: number;
}Spine Animation Controller
The library provides a controller hook for managing Spine animations with a convenient API:
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 (
<GameSpine
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:
// 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<Spine | null>;
}Timers System
The library provides a timeout management system with pause/resume capabilities and ID-based timeout tracking.
Timeout Manager
The createTimeoutManager function creates a timeout manager instance with ID-based timeout control:
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();Key Features
- 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
Global Timeout Manager
The library provides a global timeout manager instance that can be used across components:
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
);
}Custom Timeout Manager
You can also create your own timeout manager instance:
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
);
}API Reference
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.
Shared Tick Manager
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.
import { useSharedTickManager } from "@darkcore/game-library";
function App() {
// Start shared tick manager once at the top level
useSharedTickManager();
return (
<Container>
{/* All child components can now use useSharedTick */}
<AnimatedComponent1 />
<AnimatedComponent2 />
<AnimatedComponent3 />
</Container>
);
}useSharedTick Hook
Components can use the shared ticker via the useSharedTick hook:
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 <Sprite image="character.png" rotation={rotation} />;
}Key Features
- 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
API
// 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
CustomButton
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
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 (
<CustomButton
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
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<typeof Sprite>;
/** Props for the button mask with press animation */
buttonMaskProps?: React.ComponentProps<typeof Sprite> & {
/** Y offset when button is pressed */
buttonImagePressedY: number;
};
/** Props for the main button image */
buttonImageProps?: React.ComponentProps<typeof Sprite> & {
/** Image to show when button is disabled */
disabledImage?: string;
};
/** Props for the button text */
textProps?: React.ComponentProps<typeof Text>;
/** 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:
- Press Animation: When pressed, the button image moves to
buttonImagePressedYposition - Release Animation: When released, returns to original position
- Smooth Interpolation: Uses
useTickfor frame-based animation with 20% lerp factor - Automatic Reset: On pointer leave, immediately resets to original position
Visual Layers
The button consists of multiple layers (from bottom to top):
- Button Plate: Background/base layer
- Button Mask: Defines the clipping area for the button image
- Button Image: Main visual element (gets masked and animated, automatically switches to disabledImage when disabled)
- 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:
<CustomButton
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:
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 (
<Container>
<Sprite
image={images.button}
position={position}
anchor={0.5}
interactive={true}
onpointerdown={handleClick}
alpha={isAnimating ? 0.8 : 1.0}
/>
</Container>
);
}API Reference:
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 styles
Animation Behavior:
- When
targetPositionchanges, 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:
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:
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
isOpenedchanges, 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
CustomPanel
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
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 (
<CustomPanel
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 */}
<Text
x={0}
y={0}
anchor={0.5}
text="Panel Content"
style={{ fontSize: 24, fill: "#ffffff" } as PIXI.TextStyle}
/>
</CustomPanel>
);
}Props
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<typeof Text>;
/** Props for the background sprite (required) */
bgSpriteProps: React.ComponentProps<typeof Sprite>;
/** 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:
- Container: Main container with position, scale, and anchor
- Background Sprite: Named "PanelBg" for easy identification
- Header Text: Optional text element positioned above content
- Children: Custom content rendered last (on top)
CustomCounter
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
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<typeof Text>;
plusButtonProps: React.ComponentProps<typeof CustomButton>;
minusButtonProps: React.ComponentProps<typeof CustomButton>;
}Usage
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 (
<CustomCounter
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
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<typeof Text>;
/** Props for the plus/increment button */
plusButtonProps: React.ComponentProps<typeof CustomButton>;
/** Props for the minus/decrement button */
minusButtonProps: React.ComponentProps<typeof CustomButton>;
}Number Formatting
The numberFormatter function provides locale-aware number formatting with currency support:
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
onChangewith new string value when buttons are clicked - Button Customization: Both plus and minus buttons support full
CustomButtonprops
Structure
The CustomCounter creates the following structure:
- Container: Main container with position, scale, and anchor
- Value Text: Displays current counter value (formatted if formatter provided)
- Plus Button: Increment button (moves to next value in array)
- Minus Button: Decrement button (moves to previous value in array)
Example Usage with Panel
// 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 (
<CustomPanel
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
}}
>
<CustomCounter
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],
}}
/>
</CustomPanel>
);
};CustomProfit
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
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 (
<CustomProfit
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
interface CustomProfitType {
/** Props for the CustomPanel container */
customPanelProps: React.ComponentProps<typeof CustomPanel>;
/** Props for the profit value text */
valueTextProps: React.ComponentProps<typeof Text>;
/** Props for the win text */
winTextProps: React.ComponentProps<typeof Text>;
/** Props for the good luck text */
goodLuckTextProps: React.ComponentProps<typeof Text>;
/** 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:
- Win State: When
isWinis true, alternates between win text and profit value - Good Luck State: When
isGoodLuckis true andisWinis false, shows good luck text - Normal State: When neither
isWinnorisGoodLuckis true, shows the profit value
Animation Phase Flow
The component uses a four-phase animation system:
- Count Phase: Animates profit value changes over
counterAnimationDuration - Pre-pause Phase: Shows final value for
pauseDurationbefore win text - Scale Phase: Displays win text with optional scale animation
- Pause Phase: Shows profit value between win text cycles
Value Increment Animation
- When
profitValueincreases: Smooth animation overcounterAnimationDuration - When
profitValuedecreases: 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
enableTextScaleAnimationis 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
scaleAmplitudeprop - Runs for each win text cycle defined by
winCount
Multiple Win Cycles
- When
winCountis 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
winCountis reached
CustomHistory
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
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<string[]>([]);
const addHistoryValue = () => {
const newValue = `${Math.floor(Math.random() * 10) + 1}x`;
setHistoryValues([...historyValues, newValue]);
};
return (
<CustomHistory
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
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<typeof Sprite>;
/** Props for history item text */
historyValueTextProps: React.ComponentProps<typeof Text>;
/** 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
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
export const HistoryBoard = ({ cellCenterPositions }: HistoryBoardProps) => {
const images = useGetImages();
const [historyValues, setHistoryValues] = useState<number[]>([]);
return (
<CustomPanel
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,
}}
>
<CustomHistory
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,
}}
/>
</CustomPanel>
);
};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
useTickfrom @pixi/react - Performance Optimization: Efficient diffing algorithm to minimize unnecessary updates
- State Management: Maintains animation state across renders
CustomWinModal
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
import { CustomWinModal } from "@darkcore/game-library";
import { useGetSpines } from "@darkcore/game-library";
import { useSpineController } from "@darkcore/game-library";
import { useDynamicFontSize } from "@darkcore/game-library";
import { TextStyle } from "pixi.js";
export 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