npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@axieinfinity/mixer

v1.4.9

Published

Axie Infinity Mixer

Readme

@axieinfinity/mixer

NPM version

A utility for rendering 2D Axie with support for accessories, animations, and PixiJS integration.

📦 Installation

npm install @axieinfinity/mixer

or with yarn

yarn add @axieinfinity/mixer

📋 Prerequisites

To render Axies with PixiJS, you'll need these dependencies:

{
  "dependencies": {
    "pixi.js": "7.2.4",
    "pixi-spine": "4.0.3",
    "@axieinfinity/mixer": "lasted"
  }
}

Install them:

npm install or yarn install

⚠️ Important Notes

  • This version does not include data by default. You must import the required JSON data files and call initAxieMixer before using the utility functions.
  • To render evolved parts, use version 1.4.1 or later.
  • Accessories use Spine 2D format and require proper y-axis flipping for correct orientation.

🆕 Features

  • Images URL: https://axiecdn.axieinfinity.com/mixer-stuffs/v6
  • Accessory spines URL: https://axiecdn.axieinfinity.com/mixer-stuffs/accessory-spines/v1
  • Full support for Nightmare parts
  • All accessories migrated to Spine 2D
  • PixiJS v7+ compatibility
  • Complete animation system

🚀 Complete Working Example

package.json

{
  "name": "axie-mixer-demo",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "pixi.js": "7.2.4",
    "pixi-spine": "4.0.3",
    "@axieinfinity/mixer": "1.4.8"
  },
  "devDependencies": {
    "typescript": "5.6.0",
    "vite": "5.4.0"
  }
}

HTML Setup

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Axie Mixer Demo</title>
    <style>
      :root {
        font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
        line-height: 1.5;
        font-weight: 400;

        color-scheme: light dark;
        color: rgba(255, 255, 255, 0.87);
        background-color: #242424;

        font-synthesis: none;
        text-rendering: optimizeLegibility;
        -webkit-font-smoothing: antialiased;
        -moz-osx-font-smoothing: grayscale;
      }

      body {
        margin: 0;
        min-width: 320px;
        min-height: 100vh;
      }

      #app {
        max-width: 1280px;
        margin: 0 auto;
        padding: 2rem;
        text-align: center;
      }

      h1 {
        font-size: 3.2em;
        line-height: 1.1;
        margin-bottom: 1em;
      }

      #controls {
        margin-bottom: 2rem;
        display: flex;
        gap: 1rem;
        justify-content: center;
        align-items: center;
      }

      #pixi-container {
        border: 2px solid #646cff;
        border-radius: 8px;
        margin: 0 auto;
        background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
        position: relative;
        overflow: hidden;
        width: 800px;
        height: 600px;
      }

      #pixi-canvas {
        width: 100%;
        height: 100%;
      }

      #loading {
        position: absolute;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
        background: rgba(0, 0, 0, 0.8);
        color: white;
        padding: 1rem 2rem;
        border-radius: 8px;
        z-index: 1000;
      }

      button {
        border-radius: 8px;
        border: 1px solid transparent;
        padding: 0.6em 1.2em;
        font-size: 1em;
        font-weight: 500;
        font-family: inherit;
        background-color: #646cff;
        color: white;
        cursor: pointer;
        transition: all 0.25s;
      }

      button:hover {
        background-color: #535bf2;
        transform: translateY(-2px);
      }

      button:disabled {
        opacity: 0.6;
        cursor: not-allowed;
        transform: none;
      }

      button:focus,
      button:focus-visible {
        outline: 4px auto -webkit-focus-ring-color;
      }

      .error {
        color: #ff6b6b;
        background-color: rgba(255, 107, 107, 0.1);
        border: 1px solid #ff6b6b;
        border-radius: 8px;
        padding: 1rem;
        margin: 1rem 0;
      }

      @media (prefers-color-scheme: light) {
        :root {
          color: #213547;
          background-color: #ffffff;
        }
        
        button {
          background-color: #646cff;
          color: white;
        }
        
        button:hover {
          background-color: #535bf2;
        }
      }
    </style>
  </head>
  <body>
    <div id="app">
      <h1>Axie Mixer Demo</h1>
      <div id="controls">
        <button id="load-axie">Load Axie with Accessories</button>
        <button id="change-animation">Change Animation</button>
        <div id="loading" style="display: none;">Loading...</div>
      </div>
      <div id="pixi-container">
        <canvas id="pixi-canvas"></canvas>
      </div>
    </div>
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>

Full TypeScript Implementation

import { Application, Assets, Texture } from 'pixi.js';
import { Spine, TextureAtlas } from 'pixi-spine';
import type { ISkeletonData } from '@pixi-spine/base';
import type { ISpineResource } from '@pixi-spine/loader-base';
import { AtlasAttachmentLoader, SkeletonJson } from '@pixi-spine/runtime-3.8';
import {
  initAxieMixer,
  getAxieSpineFromGenes,
  getAxieColorPartShift,
  getVariantAttachmentPath,
  AxieBuilderResult,
} from '@axieinfinity/mixer';

// Import required data files
import GenesData from '@axieinfinity/mixer/dist/data/axie-2d-v3-stuff-genes.json';
import SamplesData from '@axieinfinity/mixer/dist/data/axie-2d-v3-stuff-samples.json';
import VariantsData from '@axieinfinity/mixer/dist/data/axie-2d-v3-stuff-variant.json';
import AnimationsData from '@axieinfinity/mixer/dist/data/axie-2d-v3-stuff-animations.json';

// CDN URLs
const AXIE_IMAGES_URL = 'https://axiecdn.axieinfinity.com/mixer-stuffs/v6/';
const ACCESSORY_SPINES_URL = 'https://axiecdn.axieinfinity.com/mixer-stuffs/accessory-spines/v1';

// Sample equipped accessories
const EQUIPPED_ACCESSORIES = [
  {
    "attributes": {
      "collection": [
        "Nightmare"
      ],
      "key": "air2a",
      "placement": "Air",
      "rarity": "Epic",
      "token_id": "25"
    },
    "description": "",
    "imageUrl": "https://cdn.axieinfinity.com/marketplace-website/accessories/25.png",
    "name": "Eyewyrm",
    "tokenId": "25",
    "tokenType": "Accessory",
    "__typename": "Erc1155Token"
  },
  {
    "attributes": {
      "collection": [
        "Nightmare"
      ],
      "key": "cheek2a",
      "placement": "Cheek",
      "rarity": "Common",
      "token_id": "23"
    },
    "description": "",
    "imageUrl": "https://cdn.axieinfinity.com/marketplace-website/accessories/23.png",
    "name": "Bloodmaw",
    "tokenId": "23",
    "tokenType": "Accessory",
    "__typename": "Erc1155Token"
  },
  {
    "attributes": {
      "collection": [
        "Japanese"
      ],
      "key": "ground3a",
      "placement": "Ground",
      "rarity": "Epic",
      "token_id": "26"
    },
    "description": "",
    "imageUrl": "https://cdn.axieinfinity.com/marketplace-website/accessories/26.png",
    "name": "Daruma",
    "tokenId": "26",
    "tokenType": "Accessory",
    "__typename": "Erc1155Token"
  }
]

type AccessoryConfig = typeof EQUIPPED_ACCESSORIES[number];

class AxieRenderer {
  private app: Application;
  private axieSpine?: Spine;
  private currentAnimationIndex = 0;
  private animations = [
    'action/idle/normal',
    'action/idle/random-02',
    'draft/run-origin',
    'attack/ranged/cast-fly',
    'attack/melee/tail-smash'
  ];

  constructor() {
    this.app = new Application({
      width: 800,
      view: document.getElementById('pixi-canvas') as HTMLCanvasElement,
      height: 600,
      backgroundColor: 0x1099bb,
      resolution: window.devicePixelRatio || 1,
    });
  }

  async initialize(): Promise<void> {
    // Initialize the PIXI Application
    // Initialize the mixer with data
    initAxieMixer(GenesData, SamplesData, VariantsData, AnimationsData);
    console.log('AxieRenderer initialized successfully');
  }

  async renderAxieWithAccessories(): Promise<void> {
    try {
      this.showLoading(true);
      this.app.stage.removeChildren();

      // Sample Axie genes
      const axieGenes = '0x20000000000003000181a09082040000000100040800800400000090086044020001000010008002000100100840450200010004186044020001001008808404';
      
      // Configure accessories
      const meta = new Map<string, string>();
      EQUIPPED_ACCESSORIES.forEach(accessory => {
        meta.set(`accessory-${accessory.attributes.placement.toLowerCase()}`, `accessory-${accessory.attributes.key}`);
      });

      console.log('Generating Axie spine data...');

      // Generate Axie spine data
      const { skeletonDataAsset, variant }: AxieBuilderResult = 
        getAxieSpineFromGenes(axieGenes, meta, false);

      if (!skeletonDataAsset) {
        throw new Error('Failed to generate Axie skeleton data');
      }

      console.log('Creating Axie spine...');

      // Create and display the Axie
      this.axieSpine = await this.createAxieSpine(skeletonDataAsset, variant);
      this.axieSpine.position.set(400, 400);
      this.axieSpine.scale.set(0.3);
      
      // Start idle animation
      this.axieSpine.state.setAnimation(0, 'action/idle/normal', true);
      
      this.app.stage.addChild(this.axieSpine);

      console.log('Adding accessories...');

      // Add accessories
      await this.addAccessories(EQUIPPED_ACCESSORIES);

      console.log('Axie rendered successfully!');
      this.showLoading(false);

    } catch (error) {
      console.error('Failed to render Axie:', error);
      this.showError(`Failed to render Axie: ${error instanceof Error ? error.message : 'Unknown error'}`);
      this.showLoading(false);
    }
  }

  private async createAxieSpine(skeletonData: AxieBuilderResult['skeletonDataAsset'], variant: string): Promise<Spine> {
    // Get all required textures for this Axie
    const resources = this.getRequiredTextures(skeletonData, variant);
    
    console.log(`Loading ${resources.length} textures...`);
    
    // Load all textures
    const texturePromises = resources.map(async (resource) => {
      try {
        const texture = await Assets.load(resource.imagePath);
        return { key: resource.key, texture };
      } catch (error) {
        console.warn(`Failed to load texture ${resource.key}:`, error);
        // Return a placeholder or skip this texture
        return null;
      }
    });
    
    const loadedTextures = (await Promise.all(texturePromises)).filter(Boolean);
    
    // Create texture hash
    const allTextures: { [key: string]: Texture } = {};
    loadedTextures.forEach(record => {
      if (!record || !record.key || !record.texture) return;
      const { key, texture } = record;
      allTextures[key] = texture;
    });

    console.log(`Loaded ${Object.keys(allTextures).length} textures`);

    // Create Spine atlas and loader
    const spineAtlas = new TextureAtlas();
    spineAtlas.addTextureHash(allTextures, false);
    
    const spineAtlasLoader = new AtlasAttachmentLoader(spineAtlas);
    const spineJsonParser = new SkeletonJson(spineAtlasLoader);
    
    // Parse skeleton data and create Spine
    const spineData = spineJsonParser.readSkeletonData(skeletonData);
    return new Spine(spineData);
  }

  private getRequiredTextures(skeletonData: AxieBuilderResult['skeletonDataAsset'], variant: string): Array<{ key: string; imagePath: string }> {
    const skinAttachments = skeletonData.skins[0].attachments;
    const imagesToLoad: Array<{ key: string; imagePath: string }> = [];
    
    const partColorShift = getAxieColorPartShift(variant);
    
    for (const slotName in skinAttachments) {
      const skinSlotAttachments = skinAttachments[slotName];
      for (const attachmentName in skinSlotAttachments) {
        const path = skinSlotAttachments[attachmentName].path;
        const imagePath = AXIE_IMAGES_URL + 
          getVariantAttachmentPath(slotName, path, variant, partColorShift);
        
        imagesToLoad.push({ key: path, imagePath });
      }
    }
    
    return imagesToLoad;
  }

  private async addAccessories(accessories: AccessoryConfig[]): Promise<void> {
    if (!this.axieSpine) return;

    for (const accessory of accessories) {
      try {
        console.log(`Loading accessory: ${accessory.name}`);
        
        // Load accessory spine data
        const accessoryUrl = `${ACCESSORY_SPINES_URL}/${accessory.attributes.key}/${accessory.attributes.key}.json`;
        const skeletonData: ISpineResource<ISkeletonData> = await Assets.load(accessoryUrl);
        const spineData = skeletonData.spineData;
        console.log('spineData', spineData);
        
        // Create accessory spine
        const accessorySpine = new Spine(spineData);
        accessorySpine.scale.set(1, -1); // Flip Y axis for correct orientation
        
        // Set animation if available
        if (spineData.animations.length > 0) {
          accessorySpine.state.setAnimation(0, spineData.animations[0].name, true);
        }
        
        // Find the correct slot and attach
        const slotName = `body-${accessory.attributes.placement.toLowerCase()}`;
        const slotIndex = this.axieSpine.skeleton.findSlotIndex(slotName);
        
        if (slotIndex >= 0) {
          this.axieSpine.slotContainers[slotIndex].addChild(accessorySpine);
          console.log(`✓ Attached ${accessory.name} to ${slotName}`);
        } else {
          console.warn(`Slot ${slotName} not found for accessory ${accessory.name}`);
        }
      } catch (error) {
        console.error(`Failed to load accessory ${accessory.name}:`, error);
      }
    }
  }

  // Animation control methods
  changeAnimation(): void {
    if (!this.axieSpine) return;

    this.currentAnimationIndex = (this.currentAnimationIndex + 1) % this.animations.length;
    const animationName = this.animations[this.currentAnimationIndex];
    
    console.log(`Changing animation to: ${animationName}`);
    
    // For non-looping animations, add idle afterwards
    const isLooping = animationName.includes('idle') || animationName.includes('run');
    
    this.axieSpine.state.setAnimation(0, animationName, isLooping);
    
    if (!isLooping) {
      this.axieSpine.state.addAnimation(0, 'action/idle/normal', true, 1.0);
    }
  }

  private showLoading(show: boolean): void {
    const loadingElement = document.getElementById('loading');
    if (loadingElement) {
      loadingElement.style.display = show ? 'block' : 'none';
    }
  }

  private showError(message: string): void {
    const container = document.getElementById('pixi-container');
    if (container) {
      const errorDiv = document.createElement('div');
      errorDiv.className = 'error';
      errorDiv.textContent = message;
      container.parentElement?.insertBefore(errorDiv, container);
      
      // Remove error after 5 seconds
      setTimeout(() => {
        errorDiv.remove();
      }, 5000);
    }
  }
}

// Initialize the demo
async function main() {
  try {
    console.log('Initializing Axie Mixer Demo...');
    
    const renderer = new AxieRenderer();
    await renderer.initialize();
    
    // Setup event listeners
    const loadAxieButton = document.getElementById('load-axie') as HTMLButtonElement;
    const changeAnimationButton = document.getElementById('change-animation') as HTMLButtonElement;
    
    if (loadAxieButton) {
      loadAxieButton.addEventListener('click', async () => {
        loadAxieButton.disabled = true;
        changeAnimationButton.disabled = true;
        
        try {
          await renderer.renderAxieWithAccessories();
          changeAnimationButton.disabled = false;
        } catch (error) {
          console.error('Error loading Axie:', error);
        } finally {
          loadAxieButton.disabled = false;
        }
      });
    }
    
    if (changeAnimationButton) {
      changeAnimationButton.addEventListener('click', () => {
        renderer.changeAnimation();
      });
      changeAnimationButton.disabled = true; // Enable after Axie is loaded
    }
    
    console.log('Demo ready! Click "Load Axie with Accessories" to start.');
    
  } catch (error) {
    console.error('Failed to initialize demo:', error);
    
    const app = document.getElementById('app');
    if (app) {
      const errorDiv = document.createElement('div');
      errorDiv.className = 'error';
      errorDiv.innerHTML = `
        <h3>Initialization Error</h3>
        <p>${error instanceof Error ? error.message : 'Unknown error'}</p>
        <p>Please check the console for more details.</p>
      `;
      app.appendChild(errorDiv);
    }
  }
}

// Start the demo
main();

📚 Available Accessories

| Placement | Meta Key | Slot Name | Example Keys | |-----------|----------|-----------|--------------| | Air | accessory-air | body-air | air1a, air1b, air1c, air1d | | Cheek | accessory-cheek | body-cheek | cheek1a, cheek2a | | Ground | accessory-ground | body-ground | ground1a | | Hip | accessory-hip | body-hip | hip1a | | Neck | accessory-neck | body-neck | neck1a |

Special Meta Keys

  • accessory-suit-off: Set to 'true' or 'false' to toggle mystic suit
  • accessory-id: Optional Axie ID for accessories

🎮 Animation System

Common animations you can use:

// Idle animations
renderer.changeAnimation('action/idle/normal', true);
renderer.changeAnimation('action/idle/random-02', false);

// Movement animations  
renderer.changeAnimation('draft/run-origin', true);

// Attack animations
renderer.changeAnimation('attack/ranged/cast-fly', false);
renderer.changeAnimation('attack/melee/tail-smash', false);

// Chain animations with delay
renderer.addAnimation('action/idle/normal', true, 1.0);

🔧 Troubleshooting

Common Issues

Accessory appears upside down

  • Solution: Ensure you set accessorySpine.scale.set(1, -1) to flip the Y axis

Textures not loading

  • Solution: Make sure all textures are loaded before creating the Spine object
  • Check that CDN URLs are accessible and correct

Animation not found errors

  • Solution: Verify animation names exist in the skeleton data
  • Use skeletonData.animations to list available animations

Slot not found for accessory

  • Solution: Check that the slot name matches exactly (e.g., body-air, body-cheek)
  • Ensure the Axie spine was generated with accessory support

Performance issues

  • Solution: Use texture atlasing and avoid recreating Spine objects frequently
  • Consider using skipAnimation: true for static displays

Debug Tips

// List available animations
console.log('Available animations:', skeletonData.animations.map(a => a.name));

// List available slots
console.log('Available slots:', axieSpine.skeleton.slots.map(s => s.data.name));

// Check if accessory loaded correctly
const slotIndex = axieSpine.skeleton.findSlotIndex('body-air');
console.log('Air slot index:', slotIndex);

📖 API Reference

Core Functions

  • initAxieMixer(genes, samples, variants, animations) - Initialize the mixer with data
  • getAxieSpineFromGenes(genes, meta, skipAnimation) - Generate Axie spine from genes
  • getAxieColorPartShift(variant) - Get color shift for variant
  • getVariantAttachmentPath(slot, path, variant, colorShift) - Build texture path

Types

  • AxieBuilderResult - Result from getAxieSpineFromGenes
  • MixedSkeletonData - Spine skeleton data format
  • AxieBodyStructure - Parsed gene structure

For complete API documentation, see the TypeScript definitions included with the package.