Skip to main content

Quick Start: Rendering Preon NFTs

Immediate Integration (5 minutes)

1. Add to Any Page

import PreonCanvas from '@/features/preon/components/PreonCanvas';

export default function MyPage() {
  return (
    <div className="w-full h-[500px] bg-black rounded-lg">
      <PreonCanvas />
    </div>
  );
}

2. View Another User’s Preon

<PreonCanvas userId="0x400f656219894306dfb76928579a185c688E1Aa5" />

3. Show Traits Data

'use client';
import { usePreon } from '@/hooks/usePreon';

export function PreonStats() {
  const { traits, hasPreon, isLoading } = usePreon();

  if (isLoading) return <div>Loading...</div>;
  if (!hasPreon) return <div>No Preon</div>;

  return (
    <div className="grid grid-cols-3 gap-4">
      <div>
        <p className="text-sm text-gray-500">Species</p>
        <p className="text-xl font-bold">
          {traits.species === 1 ? '🐺 Wolf' : traits.species === 2 ? '🦉 Owl' : '🦈 Shark'}
        </p>
      </div>
      <div>
        <p className="text-sm text-gray-500">Texture</p>
        <p className="text-xl font-bold">
          {['Clay', 'Stone', 'Metal', 'Crystal'][traits.texture]}
        </p>
      </div>
      <div>
        <p className="text-sm text-gray-500">Accuracy</p>
        <p className="text-xl font-bold">{(traits.resolution / 100).toFixed(1)}%</p>
      </div>
    </div>
  );
}

Adding 3D Models (Optional)

Step 1: Get/Create Models

Free Resources: Create in Blender:
  1. Model your species (Wolf/Owl/Shark)
  2. File → Export → glTF 2.0 (.glb)
  3. Enable “Apply Modifiers” and “Compression”

Step 2: Add to Project

public/
  models/
    species/
      1.glb  ← Wolf model
      2.glb  ← Owl model
      3.glb  ← Shark model

Step 3: Optimize (Optional)

# Install gltf-transform
npm install -g @gltf-transform/cli

# Optimize each model
gltf-transform optimize public/models/species/1.glb 1.glb --compress

Step 4: Test

The PreonModel component will automatically:
  • Load the correct model based on species trait
  • Fall back to geometric shapes if model not found
  • Apply dynamic materials based on traits

Advanced: Custom Rendering

Method 1: Static Image Badge

export function PreonBadge({ address }: { address: string }) {
  const { traits, hasPreon } = usePreon(address);
  
  if (!hasPreon) return null;

  const speciesEmoji = ['', '🐺', '🦉', '🦈'][traits.species];
  const textureEmoji = ['🪨', '🗿', '⚙️', '💎'][traits.texture];

  return (
    <div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-gradient-to-r from-purple-600 to-blue-600">
      <span className="text-2xl">{speciesEmoji}</span>
      <span className="text-sm font-semibold text-white">
        {textureEmoji} Lv.{traits.armorTier}
      </span>
    </div>
  );
}

Method 2: Canvas 2D Dynamic

'use client';
import { useEffect, useRef } from 'react';
import { usePreon } from '@/hooks/usePreon';

export function Preon2DCanvas({ address }: { address?: string }) {
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const { traits, hasPreon } = usePreon(address);

  useEffect(() => {
    if (!hasPreon || !traits || !canvasRef.current) return;

    const canvas = canvasRef.current;
    const ctx = canvas.getContext('2d')!;
    
    // Clear
    ctx.clearRect(0, 0, canvas.width, canvas.height);

    // Draw aura (color from trustHue)
    const hue = (traits.trustHue / 255) * 360;
    const gradient = ctx.createRadialGradient(150, 150, 0, 150, 150, 150);
    gradient.addColorStop(0, `hsla(${hue}, 100%, 50%, 0.8)`);
    gradient.addColorStop(1, `hsla(${hue}, 100%, 50%, 0)`);
    ctx.fillStyle = gradient;
    ctx.fillRect(0, 0, 300, 300);

    // Draw shape (species-based)
    ctx.fillStyle = `hsl(${hue}, 80%, 60%)`;
    ctx.beginPath();
    if (traits.species === 1) {
      // Wolf - triangle
      ctx.moveTo(150, 50);
      ctx.lineTo(50, 250);
      ctx.lineTo(250, 250);
    } else if (traits.species === 2) {
      // Owl - circle
      ctx.arc(150, 150, 80, 0, Math.PI * 2);
    } else {
      // Shark - diamond
      ctx.moveTo(150, 50);
      ctx.lineTo(250, 150);
      ctx.lineTo(150, 250);
      ctx.lineTo(50, 150);
    }
    ctx.closePath();
    ctx.fill();

    // Add texture overlay
    const textures = ['Clay', 'Stone', 'Metal', 'Crystal'];
    ctx.fillStyle = 'white';
    ctx.font = 'bold 16px monospace';
    ctx.textAlign = 'center';
    ctx.fillText(textures[traits.texture], 150, 290);

  }, [traits, hasPreon]);

  if (!hasPreon) return null;

  return <canvas ref={canvasRef} width={300} height={300} className="rounded-lg" />;
}

Method 3: SVG Dynamic Badge

export function PreonSVGBadge({ address }: { address?: string }) {
  const { traits, hasPreon } = usePreon(address);

  if (!hasPreon || !traits) return null;

  const hue = (traits.trustHue / 255) * 360;
  const speciesPath = {
    1: "M12 2L2 19h20L12 2z", // Triangle (Wolf)
    2: "M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2z", // Circle (Owl)
    3: "M12 2l5 10-5 10-5-10 5-10z", // Diamond (Shark)
  };

  return (
    <svg width="100" height="100" viewBox="0 0 24 24">
      <defs>
        <radialGradient id={`aura-${address}`}>
          <stop offset="0%" stopColor={`hsl(${hue}, 100%, 50%)`} stopOpacity="0.8" />
          <stop offset="100%" stopColor={`hsl(${hue}, 100%, 50%)`} stopOpacity="0" />
        </radialGradient>
      </defs>
      
      {/* Aura */}
      <circle cx="12" cy="12" r="11" fill={`url(#aura-${address})`} />
      
      {/* Species Shape */}
      <path 
        d={speciesPath[traits.species as 1 | 2 | 3] || speciesPath[1]} 
        fill={`hsl(${hue}, 80%, 60%)`}
        stroke={`hsl(${hue}, 100%, 40%)`}
        strokeWidth="0.5"
      />
    </svg>
  );
}

Minting New Preons

cd cortex

# Mint for all users
python3 -m run_oracle --all-users --force-mint --limit 100

# Mint for active users only
python3 -m run_oracle --limit 50

# Preview without transactions
python3 -m run_oracle --all-users --force-mint --dry-run

Programmatically (Advanced)

// Contract interaction for admin/testing
import { useWriteContract } from 'wagmi';
import PreonABI from '@/blockchain/abis/Preon.json';

const PREON_ADDRESS = '0xaa967c233102c4e16c3891e77d6e3c8ceedc1020';

export function useMintPreon() {
  const { writeContract } = useWriteContract();

  const mint = async (userAddress: string, species: number) => {
    await writeContract({
      address: PREON_ADDRESS,
      abi: PreonABI,
      functionName: 'mint',
      args: [userAddress, species],
    });
  };

  return { mint };
}

Display Options Comparison

Option 1: Full 3D Viewer (Current)

Best for: Main profile page, showcase
<div className="w-full h-[600px]">
  <PreonCanvas />
</div>
Pros: Immersive, interactive, impressive Cons: Heavy, slower on mobile

Option 2: 2D Canvas

Best for: Leaderboards, lists, compact views
<Preon2DCanvas address={user.address} />
Pros: Fast, lightweight, customizable Cons: Less impressive, no 3D depth

Option 3: Static Badge

Best for: Inline mentions, compact UI, mobile
<PreonBadge address={user.address} />
Pros: Instant load, minimal resources Cons: Not interactive, less detail
export function PreonDisplay({ address, mode = 'auto' }) {
  const isMobile = useIsMobile();
  const renderMode = mode === 'auto' ? (isMobile ? 'badge' : '3d') : mode;

  return (
    <>
      {renderMode === '3d' && <PreonCanvas userId={address} />}
      {renderMode === '2d' && <Preon2DCanvas address={address} />}
      {renderMode === 'badge' && <PreonBadge address={address} />}
    </>
  );
}

Testing Checklist

  • Preon exists on-chain (check with audit_preon_addresses.py)
  • User has smart wallet address in Firestore
  • usePreon hook returns hasPreon: true
  • Traits are populated (not null)
  • 3D canvas renders without errors
  • Loading states work correctly
  • Error boundaries catch missing models
  • Mobile performance is acceptable
  • Colors match trait values (trustHue)
  • Species shape matches trait (Wolf=1, Owl=2, Shark=3)

Troubleshooting

”No Preon found”

Check:
  1. Run audit: cd cortex && python3 audit_preon_addresses.py
  2. Verify user has smartWalletAddress in Firestore
  3. Run oracle: python3 -m run_oracle --all-users --force-mint

3D viewer is black/empty

Check:
  1. Browser console for errors
  2. Model file exists in public/models/species/{species}.glb
  3. Try different species (should fall back to geometry)
  4. Check browser WebGL support

Traits show “Not available”

Fix:
  1. Ensure using correct address (smart wallet, not EOA)
  2. Check contract on BaseScan: https://sepolia.basescan.org/address/0xaa967c233102c4e16c3891e77d6e3c8ceedc1020
  3. Verify traits were updated after minting

Performance issues

Optimize:
  1. Use lazy loading: dynamic(() => import('./PreonCanvas'), { ssr: false })
  2. Reduce particle count in PreonCanvas
  3. Implement viewport visibility detection
  4. Use smaller 3D models (< 2MB)

Next Steps

  1. Add real 3D models - See “Adding 3D Models” section
  2. Implement metadata API - For OpenSea compatibility
  3. Create showcase gallery - Display all Preons
  4. Add evolution animations - Trait updates with transitions
  5. Build comparison tool - Compare two Preons side-by-side
For complete details, see PREON_NFT_RENDERING_GUIDE.md