<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Raw Fun Gaming</title>
    <description>The latest articles on Forem by Raw Fun Gaming (@raw-fun-gaming).</description>
    <link>https://forem.com/raw-fun-gaming</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F3167%2F0401b209-fcc1-4c32-b792-84da19d8608e.png</url>
      <title>Forem: Raw Fun Gaming</title>
      <link>https://forem.com/raw-fun-gaming</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/raw-fun-gaming"/>
    <language>en</language>
    <item>
      <title>Building a Web-Unity WebGL Bridge: A Practical Guide</title>
      <dc:creator>Richard Fu</dc:creator>
      <pubDate>Sat, 13 Dec 2025 15:22:32 +0000</pubDate>
      <link>https://forem.com/raw-fun-gaming/building-a-web-unity-webgl-bridge-a-practical-guide-3nbe</link>
      <guid>https://forem.com/raw-fun-gaming/building-a-web-unity-webgl-bridge-a-practical-guide-3nbe</guid>
      <description>&lt;p&gt;When you’re building games that need both the power of Unity’s 3D engine and the flexibility of modern web technologies, you quickly discover that making them communicate isn’t as straightforward as it seems. After building several hybrid web-Unity applications, I’ve learned quite a few lessons about what works, what doesn’t, and what will make you want to throw your keyboard out the window.&lt;/p&gt;

&lt;p&gt;While the well-established &lt;a href="https://react-unity-webgl.dev/" rel="noopener noreferrer"&gt;React Unity WebGL&lt;/a&gt; library already does a great job, this guide walks you through building a robust communication bridge from scratch between a TypeScript/JavaScript web frontend and Unity WebGL builds. I’ll share the architecture we settled on, the mistakes we made along the way, and the solutions we found.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Build a Hybrid Architecture?
&lt;/h2&gt;

&lt;p&gt;Before diving into the technical details, let’s address the “why.” You might be wondering: if Unity can build for WebGL, why not just use Unity for everything?&lt;/p&gt;

&lt;p&gt;In our case, we were building multiple games that shared common functionality:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Authentication &amp;amp; session management&lt;/strong&gt; – handled by our backend SDK&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Internationalization&lt;/strong&gt; – 16+ languages with dynamic switching&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UI framework&lt;/strong&gt; – consistent menus, settings panels, modals across games&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audio system&lt;/strong&gt; – web audio API with Howler.js for UI sounds&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State persistence&lt;/strong&gt; – localStorage, session handling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each game had unique 3D gameplay, but 80% of the surrounding infrastructure was identical. Building all of this in Unity for each game would mean:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Duplicating shared code across projects&lt;/li&gt;
&lt;li&gt;Longer iteration cycles (Unity builds take time)&lt;/li&gt;
&lt;li&gt;Larger bundle sizes (Unity UI is heavy)&lt;/li&gt;
&lt;li&gt;Less flexibility in web-specific features&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Our solution: a shared web engine that handles the “chrome” around the game, while Unity handles the 3D gameplay. The web layer sends commands to Unity (“start the game”, “play this seed”), and Unity sends results back (“game complete”, “animation finished”).&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture at a Glance
&lt;/h2&gt;

&lt;p&gt;Here’s the high-level flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Web (TypeScript) Unity WebGL
----------------- -----------
GameplayManager GameController.cs
      | ^
      v |
UnityInterface -------SendMessage-------&amp;gt; WebInterface.cs
      | |
      | &amp;lt;------ExternalCall-------- |
      v v
   GameUI Game Scene
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The communication is bidirectional:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Web to Unity&lt;/strong&gt; : Uses Unity’s &lt;code&gt;SendMessage()&lt;/code&gt; API&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unity to Web&lt;/strong&gt; : Uses &lt;code&gt;Application.ExternalCall()&lt;/code&gt; (or the modern &lt;code&gt;jslib&lt;/code&gt; approach)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Messages are JSON strings in both directions, giving us type safety and flexibility.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Key Mistakes We Made (So You Don’t Have To)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Mistake #1: Inconsistent GameObject Naming
&lt;/h3&gt;

&lt;p&gt;Unity’s &lt;code&gt;SendMessage()&lt;/code&gt; API requires you to specify a GameObject name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;unityInstance.SendMessage('WebInterface', 'StartGame', data);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sounds simple, right? Here’s where we messed up: different developers named the receiving GameObject differently across projects. One called it “WebInterface”, another “WebBridge”, another “GameManager”.&lt;/p&gt;

&lt;p&gt;When we tried to create a reusable engine, nothing worked because each game expected a different name.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt; Establish a &lt;em&gt;strict naming convention&lt;/em&gt; and stick to it. We settled on pattern-based names:&lt;br&gt;&lt;br&gt;
 – &lt;code&gt;WebSeedInterfaces&lt;/code&gt; – for seed-based games&lt;br&gt;&lt;br&gt;
 – &lt;code&gt;WebRoundInterfaces&lt;/code&gt; – for round-based games&lt;br&gt;&lt;br&gt;
 – &lt;code&gt;WebSettingsInterface&lt;/code&gt; – for audio/language/settings&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The “Interfaces” suffix (plural) reminds us it’s a collection of interface methods, not a single one.&lt;/p&gt;
&lt;/blockquote&gt;


&lt;h3&gt;
  
  
  Mistake #2: Complex JSON Message Structures
&lt;/h3&gt;

&lt;p&gt;Our first implementation sent messages like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Web side - sending
unityInstance.SendMessage('WebInterface', 'ReceiveMessage', JSON.stringify({
    action: 'setDifficulty',
    data: {
        difficulty: 'hard',
        timestamp: Date.now()
    }
}));

// Unity side - receiving
public void ReceiveMessage(string json) {
    var msg = JsonUtility.FromJson&amp;lt;WebMessage&amp;gt;(json);
    switch(msg.action) {
        case "setDifficulty":
            var data = JsonUtility.FromJson&amp;lt;DifficultyData&amp;gt;(msg.data);
            SetDifficulty(data.difficulty);
            break;
        // ... 20 more cases
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This meant:&lt;br&gt;&lt;br&gt;
 – Unity needed to parse JSON twice (outer message + inner data)&lt;br&gt;&lt;br&gt;
 – Giant switch statements that grew with every feature&lt;br&gt;&lt;br&gt;
 – Type definitions on both sides had to stay in sync&lt;br&gt;&lt;br&gt;
 – Debugging was painful&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt; Use direct method calls with simple values. Unity’s &lt;code&gt;SendMessage()&lt;/code&gt; can call any public method directly:&lt;br&gt;
&lt;/p&gt;


&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Web side - much simpler!
unityInstance.SendMessage('WebRoundInterfaces', 'WebSetDifficulty', 'hard');
unityInstance.SendMessage('WebRoundInterfaces', 'WebStartRound', '');
unityInstance.SendMessage('WebSettingsInterface', 'WebSetLanguage', 'en'); 

// Unity side - clean methods
public void WebSetDifficulty(string difficulty) {
    _difficulty = difficulty;
    OnDifficultyChanged?.Invoke(difficulty);
}

public void WebStartRound(string _unused) {
    StartRound();
}

public void WebSetLanguage(string languageCode) {
    SetLanguage(languageCode);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We adopted a &lt;code&gt;Web[Verb][Noun]&lt;/code&gt; naming pattern for all web-callable methods. This makes it immediately clear which methods are called from the web side.&lt;/p&gt;




&lt;h3&gt;
  
  
  Mistake #3: Not Handling the “Unity Not Ready” State
&lt;/h3&gt;

&lt;p&gt;Unity WebGL takes time to load. If your web code tries to send messages before Unity is ready, they simply disappear into the void.&lt;/p&gt;

&lt;p&gt;Our first “fix” was to add arbitrary delays:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Don't do this
setTimeout(() =&amp;gt; {
    unityInstance.SendMessage('WebInterface', 'Initialize', '');
}, 3000); // Hope 3 seconds is enough...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Spoiler: it wasn’t always enough. And sometimes it was too much.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt; Implement a message queue that holds messages until Unity signals it’s ready:&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class UnityManager {
    private messageQueue: Array&amp;lt;{methodName: string; value?: any}&amp;gt; = [];
    private connectionState: 'disconnected' | 'loading' | 'ready' = 'disconnected';

    async sendMessage(methodName: string, value?: any): Promise&amp;lt;void&amp;gt; {
        if (this.connectionState !== 'ready') {
            // Queue message for later
            this.messageQueue.push({ methodName, value });
            return;
        }
        await this.sendMessageToUnity(methodName, value);
    }

    // Called when Unity sends 'gameReady' message
    private onUnityReady(): void {
        this.connectionState = 'ready';
        // Process queued messages
        this.processMessageQueue();
    }

    private async processMessageQueue(): Promise&amp;lt;void&amp;gt; {
        while (this.messageQueue.length &amp;gt; 0) {
            const message = this.messageQueue.shift();
            await this.sendMessageToUnity(message.methodName, message.value);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the Unity side, send a “ready” signal when the game is initialized:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;void Start() {
    // Send ready signal to web
    SendToWeb("gameReady", null);
}

public void SendToWeb(string action, object data) {
    var message = JsonUtility.ToJson(new { action, data });
    Application.ExternalCall("UnityMessageHandler", message);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Mistake #4: Forgetting That SendMessage Only Takes Strings
&lt;/h3&gt;

&lt;p&gt;This one bit us multiple times. Unity’s &lt;code&gt;SendMessage()&lt;/code&gt; can only pass string, int, or float parameters. No objects, no booleans, no arrays.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// This silently fails or behaves unexpectedly
unityInstance.SendMessage('WebInterface', 'SetEnabled', true);
unityInstance.SendMessage('WebInterface', 'SetConfig', { foo: 'bar' });
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt; Always convert to strings on the web side, parse on the Unity side:&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Web side
private async sendMessageToUnity(methodName: string, value?: any): Promise&amp;lt;void&amp;gt; {
    // Convert value to string - Unity SendMessage always expects string
    const messageData = value !== undefined ? String(value) : '';
    this.unityInstance.SendMessage(this.gameObjectName, methodName, messageData);
}

// Unity side - parse boolean from string
public void WebSetTurbo(string boolValue) {
    bool enabled = bool.Parse(boolValue); // "true" -&amp;gt; true
    SetTurboMode(enabled);
}

// Unity side - parse number from string
public void WebSetRound(string roundNumber) {
    int round = int.Parse(roundNumber); // "5" -&amp;gt; 5
    SetRound(round);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Unity Build Settings That Matter
&lt;/h2&gt;

&lt;p&gt;Your Unity WebGL build settings significantly impact how well the bridge works. Here’s what we learned:&lt;/p&gt;

&lt;h3&gt;
  
  
  Player Settings
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Edit &amp;gt; Project Settings &amp;gt; Player &amp;gt; WebGL Settings

Product Name: Game // Use a generic name for reusability
Compression Format: Gzip // Best balance of size and compatibility
Decompression Fallback: Yes // For older browsers
Run In Background: Yes // Keep running when tab loses focus
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;Product Name&lt;/code&gt; becomes part of your build filenames (&lt;code&gt;Game.wasm&lt;/code&gt;, &lt;code&gt;Game.framework.js&lt;/code&gt;, etc.). Using a generic name like “Game” means your web engine doesn’t need per-game configuration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Build Configuration
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;File &amp;gt; Build Settings &amp;gt; WebGL

Development Build: [checked for dev, unchecked for prod]
Code Optimization: Speed (for production)
Enable Exceptions: Explicitly Thrown Only
Strip Engine Code: Yes (reduces file size)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Vite Configuration for Unity WebGL
&lt;/h3&gt;

&lt;p&gt;If you’re using Vite (or similar bundler), you need specific configuration to handle Unity files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// vite.config.ts
export default defineConfig({
    server: {
        headers: {
            // Required for Unity WebGL SharedArrayBuffer support
            'Cross-Origin-Embedder-Policy': 'require-corp',
            'Cross-Origin-Opener-Policy': 'same-origin'
        }
    },
    build: {
        assetsInlineLimit: 0, // Don't inline Unity assets
        chunkSizeWarningLimit: 10000, // Unity files can be large
        rollupOptions: {
            output: {
                assetFileNames: (assetInfo) =&amp;gt; {
                    // Keep Unity files with their original names
                    if (assetInfo.name?.endsWith('.data') ||
                        assetInfo.name?.endsWith('.wasm') ||
                        assetInfo.name?.endsWith('.framework.js')) {
                        return '[name][extname]';
                    }
                    return 'assets/[name]-[hash][extname]';
                }
            }
        }
    }
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Silencing Unity’s Console Noise
&lt;/h2&gt;

&lt;p&gt;Unity WebGL builds are &lt;em&gt;chatty&lt;/em&gt;. Like, really chatty. Open your browser console and you’ll see hundreds of messages about memory allocation, WebGL state, physics initialization, and more.&lt;/p&gt;

&lt;p&gt;This isn’t just annoying – it can hide actual errors and slow down the browser’s developer tools.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Console Filter Approach
&lt;/h3&gt;

&lt;p&gt;We intercept console methods &lt;em&gt;before&lt;/em&gt; Unity loads and filter out the noise:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;!-- index.html - MUST be before any Unity scripts --&amp;gt;
&amp;lt;script&amp;gt;
(function() {
    // Store original console methods
    const originalConsole = {
        log: console.log.bind(console),
        warn: console.warn.bind(console),
        error: console.error.bind(console)
    };

    // Detect build mode (replaced by build tool)
    const isProduction = __VITE_BUILD_MODE__ ;

    // Unity internal patterns to suppress
    const unityPatterns = [
        /\[UnityMemory\]/, /\[Physics::Module\]/, /memorysetup-/,
        /Loading player data/, /Initialize engine version/, /Creating WebGL/,
        /^Renderer:/, /^Vendor:/, /^GLES:/, /OPENGL LOG:/,
        /UnloadTime:/, /JS_FileSystem_Sync/, /Configuration Parameters/,
        /\$func\d+ @ Game\.wasm/, /Module\._main @ Game\.framework\.js/
    ];

    // Custom Debug.Log patterns to KEEP (your game's logs)
    const customPatterns = [
        /\[WebRoundInterface\]/, /\[GameplayController\]/,
        /\[[A-Z][A-Za-z]*(?:Interface|Controller|Manager)\]/
    ];

    function shouldSuppress(message) {
        // Production: suppress everything
        if (isProduction) return true;

        // Development: suppress Unity internal, keep custom
        const isUnityInternal = unityPatterns.some(p =&amp;gt; p.test(message));
        if (isUnityInternal) {
            const isCustomLog = customPatterns.some(p =&amp;gt; p.test(message));
            return !isCustomLog;
        }
        return false;
    }

    // Override console methods
    ['log', 'warn', 'error'].forEach(level =&amp;gt; {
        console[level] = function(...args) {
            const message = args.map(String).join(' ');
            if (!shouldSuppress(message)) {
                originalConsole[level](...args);
            }
        };
    });

    // Store original for emergency access
    window.__originalConsole = originalConsole;
})();
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Suppressing WebGL Warnings at the Source
&lt;/h3&gt;

&lt;p&gt;Some WebGL warnings happen &lt;em&gt;during&lt;/em&gt; API calls, before any console filtering can catch them. Unity queries texture formats that may not be supported, and Chrome helpfully logs &lt;code&gt;WebGL: INVALID_ENUM&lt;/code&gt; warnings.&lt;/p&gt;

&lt;p&gt;The nuclear option: patch the WebGL API itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if (typeof WebGL2RenderingContext !== 'undefined') {
    const original = WebGL2RenderingContext.prototype.getInternalformatParameter;

    // Known invalid formats Unity queries
    const invalidFormats = new Set([
        36756, 36757, 36759, 36760, 36761, 36763 // Compressed texture formats
    ]);

    WebGL2RenderingContext.prototype.getInternalformatParameter = function(
        target, internalformat, pname
    ) {
        // Block Unity's known invalid queries before they trigger warnings
        if (invalidFormats.has(internalformat)) {
            return null;
        }
        return original.call(this, target, internalformat, pname);
    };
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Post-Build Processing
&lt;/h3&gt;

&lt;p&gt;For production builds, we also modify Unity’s generated files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// post-unity-build.js - Run after Unity build
import fs from 'fs';

const frameworkFile = './public/unity/Build/Game.framework.js';
const loaderFile = './public/unity/Build/Game.loader.js';

// Read Unity framework file
let framework = fs.readFileSync(frameworkFile, 'utf8');

// Prepend WebGL fix
const webglFix = `
(function () {
    const original = WebGL2RenderingContext.prototype.getInternalformatParameter;
    const invalid = new Set([36756, 36757, 36759, 36760, 36761, 36763]);
    WebGL2RenderingContext.prototype.getInternalformatParameter = function(t, i, p) {
        if (invalid.has(i)) return null;
        return original.call(this, t, i, p);
    };
})();
`;

// Replace console.log/warn with void 0
framework = webglFix + framework.replace(/(\W)console\.(log|warn)\([^)]*\);/g, '$1void 0;');

fs.writeFileSync(frameworkFile, framework);

// Suppress Unity Analytics in loader
let loader = fs.readFileSync(loaderFile, 'utf8');

const analyticsFix = `
(function() {
    const originalFetch = window.fetch;
    window.fetch = function(...args) {
        if (typeof args[0] === 'string' &amp;amp;&amp;amp; args[0].includes('unity3d.com')) {
            return Promise.reject(new Error('Analytics disabled'));
        }
        return originalFetch.apply(this, args);
    };
})();
`;

loader = analyticsFix + loader.replace(/console\.(log|warn)\([^)]*\)/g, 'void 0');
fs.writeFileSync(loaderFile, loader);

console.log('Unity build post-processed successfully');
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Final Architecture
&lt;/h2&gt;

&lt;p&gt;After all these iterations, here’s what our architecture looks like:&lt;/p&gt;

&lt;h3&gt;
  
  
  Web Side (TypeScript)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// UnityManager - Low-level communication
class UnityManager {
    private messageQueue: Message[] = [];
    private connectionState: ConnectionState = 'disconnected';

    async initialize(config: UnityConfig): Promise&amp;lt;void&amp;gt; { /* ... */ }
    async loadUnityGame(canvas?: HTMLCanvasElement): Promise&amp;lt;void&amp;gt; { /* ... */ }
    async sendMessage(methodName: string, value?: any): Promise&amp;lt;void&amp;gt; { /* ... */ }
}

// Domain-specific interfaces
class UnitySeedInterface {
    async startSpin(): Promise&amp;lt;void&amp;gt; { /* WebStartSpin */ }
    async startRevealing(seed: string): Promise&amp;lt;void&amp;gt; { /* WebStartRevealing */ }
    async startPayout(amount: string): Promise&amp;lt;void&amp;gt; { /* WebStartPayout */ }
    async completeSpin(): Promise&amp;lt;void&amp;gt; { /* WebCompleteSpin */ }
}

class UnitySettingsInterface {
    async setSound(enabled: boolean): Promise&amp;lt;void&amp;gt; { /* WebToggleSound */ }
    async setLanguage(lang: string): Promise&amp;lt;void&amp;gt; { /* WebSetLanguage */ }
    async setTurbo(enabled: boolean): Promise&amp;lt;void&amp;gt; { /* WebSetTurbo */ }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Unity Side (C#)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public class WebSeedInterface : MonoBehaviour
{
    public UnityEvent OnSpinStarted;
    public UnityEvent&amp;lt;string&amp;gt; OnRevealing;
    public UnityEvent&amp;lt;string&amp;gt; OnPayout;
    public UnityEvent OnSpinCompleted;

    void Start() =&amp;gt; SendToWeb("gameReady", null);

    public void WebStartSpin(string _) =&amp;gt; OnSpinStarted?.Invoke();
    public void WebStartRevealing(string seed) =&amp;gt; OnRevealing?.Invoke(seed);
    public void WebStartPayout(string amount) =&amp;gt; OnPayout?.Invoke(amount);
    public void WebCompleteSpin(string _) =&amp;gt; OnSpinCompleted?.Invoke();

    public void SendToWeb(string action, object data) {
        var json = JsonUtility.ToJson(new { action, data });
        Application.ExternalCall("UnityMessageHandler", json);
    }
}

// WebSettingsInterface.cs - Audio, language, turbo
public class WebSettingsInterface : MonoBehaviour
{
    public static event Action&amp;lt;bool&amp;gt; OnSoundToggled;
    public static event Action&amp;lt;string&amp;gt; OnLanguageChanged;
    public static event Action&amp;lt;bool&amp;gt; OnTurboModeToggled;

    public void WebToggleSound(string val) =&amp;gt; OnSoundToggled?.Invoke(bool.Parse(val));
    public void WebSetLanguage(string lang) =&amp;gt; OnLanguageChanged?.Invoke(lang);
    public void WebSetTurbo(string val) =&amp;gt; OnTurboModeToggled?.Invoke(bool.Parse(val));
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Standardize GameObject names&lt;/strong&gt; – Pick a naming convention and enforce it across all projects.&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Use direct method calls&lt;/strong&gt; – Skip the JSON wrapper for simple values. &lt;code&gt;WebSetDifficulty('hard')&lt;/code&gt; beats &lt;code&gt;ReceiveMessage('{"action":"setDifficulty","data":"hard"}')&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Queue messages until ready&lt;/strong&gt; – Never assume Unity is loaded. Always queue messages and process them when Unity signals readiness.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Everything is a string&lt;/strong&gt; – Remember that &lt;code&gt;SendMessage()&lt;/code&gt; can only pass strings. Convert on web, parse on Unity.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Filter console noise early&lt;/strong&gt; – Set up console filtering before Unity loads, and patch WebGL APIs if necessary.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Post-process Unity builds&lt;/strong&gt; – Remove console calls and Unity analytics from production builds.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Building a web-Unity bridge isn’t rocket science, but the devil is in the details. These patterns have served us well across multiple games, and I hope they save you some of the headaches we experienced along the way.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Have questions or improvements? Feel free to reach out. Happy coding!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The post &lt;a href="https://www.richardfu.net/building-a-web-unity-webgl-bridge-a-practical-guide/" rel="noopener noreferrer"&gt;Building a Web-Unity WebGL Bridge: A Practical Guide&lt;/a&gt; appeared first on &lt;a href="https://www.richardfu.net" rel="noopener noreferrer"&gt;Richard Fu&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webgl</category>
      <category>typescript</category>
      <category>gamedev</category>
      <category>unity3d</category>
    </item>
    <item>
      <title>Introducing Blocky UI Lite: A 3D Blocky-Themed Component Library 🎮</title>
      <dc:creator>Richard Fu</dc:creator>
      <pubDate>Sat, 18 Oct 2025 10:20:09 +0000</pubDate>
      <link>https://forem.com/raw-fun-gaming/introducing-blocky-ui-lite-a-3d-blocky-themed-component-library-58k3</link>
      <guid>https://forem.com/raw-fun-gaming/introducing-blocky-ui-lite-a-3d-blocky-themed-component-library-58k3</guid>
      <description>&lt;p&gt;Ever wanted to give your web apps that distinctive game aesthetic? Meet &lt;strong&gt;Blocky UI Lite&lt;/strong&gt; – a lightweight TypeScript component library that brings 3D blocky styling to your projects with zero dependencies and pure CSS magic.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp5sgem1lavvq7468hbnj.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp5sgem1lavvq7468hbnj.jpg" alt="Blocky UI components with 3D depth effects" width="800" height="774"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Blocky UI components with 3D depth effects&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  ✨ What Makes It Special?
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Pure CSS 3D Effects
&lt;/h3&gt;

&lt;p&gt;No SVG generation, no JavaScript-based styling, no runtime overhead. Every 3D effect is achieved through carefully crafted CSS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
/* Multi-layer box shadows create depth */
box-shadow:
    0 4px 0 rgba(0, 0, 0, 0.3), /* Base shadow */
    0 8px 16px rgba(0, 0, 0, 0.4), /* Far shadow */
    inset 0 2px 0 rgba(255, 255, 255, 0.2), /* Top highlight */
    inset 0 -2px 0 rgba(0, 0, 0, 0.3); /* Bottom shadow */

/* Gradient backgrounds with transparency */
background: linear-gradient(
    180deg,
    rgba(85, 223, 255, 0.95) 0%,
    rgba(85, 223, 255, 0.7) 50%,
    rgba(85, 223, 255, 0.5) 100%
);

/* Radial overlay for extra depth */
&amp;amp;::before {
    background: radial-gradient(
        circle at center,
        rgba(255, 255, 255, 0.2) 0%,
        transparent 70%
    );
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Zero Dependencies
&lt;/h3&gt;

&lt;p&gt;The entire library weighs in at just &lt;strong&gt;~15KB gzipped&lt;/strong&gt; with absolutely zero runtime dependencies. It’s pure TypeScript + CSS, making it incredibly portable and performant.&lt;/p&gt;

&lt;h3&gt;
  
  
  Full TypeScript Support
&lt;/h3&gt;

&lt;p&gt;Every component comes with complete type definitions, making development a breeze with full autocomplete and type checking:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
import { BlockyUI, ComponentVariant } from 'blocky-ui-lite';

// TypeScript knows all available options
const button = BlockyUI.createButton({
    text: 'Click Me',
    variant: 'primary', // 'default' | 'primary' | 'secondary' | 'danger'
    onClick: () =&amp;gt; console.log('Clicked!'),
    disabled: false
});

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🎨 Component Variants System
&lt;/h2&gt;

&lt;p&gt;One of the newest features is the &lt;strong&gt;unified variant system&lt;/strong&gt;. Buttons, Cards, and Tags all support the same four color variants:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;default&lt;/strong&gt; – Neutral gray for standard elements&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;primary&lt;/strong&gt; – Vibrant blue for primary actions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;secondary&lt;/strong&gt; – Bright cyan for secondary actions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;danger&lt;/strong&gt; – Bold red for destructive actions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FfuR-Gaming%2Fblocky-ui%2Fmain%2Fdocs%2Fvariants-demo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FfuR-Gaming%2Fblocky-ui%2Fmain%2Fdocs%2Fvariants-demo.png" alt="Component Variants" width="800" height="400"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;All components support consistent color variants&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  🚀 Quick Start Example
&lt;/h2&gt;

&lt;p&gt;Getting started is incredibly simple. Here’s a complete example creating an interactive game UI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang="en"&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;!-- CSS --&amp;gt;
    &amp;lt;link rel="stylesheet" href="https://unpkg.com/blocky-ui-lite@latest/dist/blocky-ui.css"&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;div id="game-ui"&amp;gt;&amp;lt;/div&amp;gt;

    &amp;lt;!-- JavaScript --&amp;gt;
    &amp;lt;script src="https://unpkg.com/blocky-ui-lite@latest/dist/index.umd.js"&amp;gt;&amp;lt;/script&amp;gt;
    &amp;lt;script&amp;gt;
        const { BlockyUI } = window.BlockyUILite;

        // Create a game stats card
        const statsCard = BlockyUI.createCard({
            title: 'Player Stats',
            variant: 'primary',
            content: `
                &amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;Level:&amp;lt;/strong&amp;gt; 42&amp;lt;/p&amp;gt;
                &amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;Score:&amp;lt;/strong&amp;gt; 9,850&amp;lt;/p&amp;gt;
                &amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;Coins:&amp;lt;/strong&amp;gt; 1,234&amp;lt;/p&amp;gt;
            `
        });

        // Create multiplier tags
        const multiplierTag = BlockyUI.createTag({
            content: '×5.0',
            variant: 'secondary'
        });

        // Create action buttons
        const playButton = BlockyUI.createButton({
            text: 'PLAY NOW',
            variant: 'primary',
            onClick: () =&amp;gt; {
                BlockyUI.showNotification(
                    'Game Started!',
                    'Good luck and have fun!'
                );
            }
        });

        // Add to page
        const container = document.getElementById('game-ui');
        container.appendChild(statsCard);
        container.appendChild(playButton);
    &amp;lt;/script&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🏗️ Technical Deep Dive
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Component Architecture
&lt;/h3&gt;

&lt;p&gt;Each component follows a consistent &lt;strong&gt;static factory pattern&lt;/strong&gt; that returns instances with methods:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
// Factory method creates instance
const modal = BlockyUI.createModal({
    title: 'Confirm Action',
    content: 'Are you sure?',
    buttons: [
        { text: 'Cancel', variant: 'default', onClick: () =&amp;gt; {} },
        { text: 'Confirm', variant: 'primary', onClick: () =&amp;gt; {} }
    ]
});

// Instance methods for control
modal.show(); // Display the modal
modal.close(); // Close programmatically

// Or use convenience methods (auto-shown)
BlockyUI.showNotification('Success!', 'Operation completed.');
BlockyUI.showError('Error!', 'Something went wrong.');
BlockyUI.showConfirmation('Delete?', 'This cannot be undone.', onConfirm, onCancel);

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  CSS Architecture
&lt;/h3&gt;

&lt;p&gt;The library uses CSS custom properties for easy theming and consistency:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
:root {
    /* Colors */
    --blocky-primary: #55dfff;
    --blocky-danger: #ff4444;

    /* 3D Effects */
    --blocky-shadow-base: 0 4px 0 rgba(0, 0, 0, 0.3);
    --blocky-shadow-far: 0 8px 16px rgba(0, 0, 0, 0.4);

    /* Spacing */
    --blocky-padding-md: 12px;
    --blocky-border-radius: 6px;

    /* Z-Index Layers */
    --blocky-z-content: 10;
    --blocky-z-dropdown: 100;
    --blocky-z-overlay-modal: 900;
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Build Pipeline
&lt;/h3&gt;

&lt;p&gt;Blocky UI uses &lt;strong&gt;Rollup&lt;/strong&gt; to generate multiple module formats:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ESM&lt;/strong&gt; (&lt;code&gt;index.esm.js&lt;/code&gt;) – For modern bundlers (Vite, Webpack, etc.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CJS&lt;/strong&gt; (&lt;code&gt;index.cjs.js&lt;/code&gt;) – For Node.js environments&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UMD&lt;/strong&gt; (&lt;code&gt;index.umd.js&lt;/code&gt;) – For direct browser usage via CDN&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TypeScript&lt;/strong&gt; (&lt;code&gt;index.d.ts&lt;/code&gt;) – Full type definitions&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  📦 Available Components
&lt;/h2&gt;

&lt;h3&gt;
  
  
  BlockyButton
&lt;/h3&gt;

&lt;p&gt;Interactive buttons with 4 color variants and 3D hover effects. Perfect for CTAs and game actions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
const button = BlockyUI.createButton({
    text: 'START GAME',
    variant: 'primary',
    onClick: () =&amp;gt; startGame(),
    disabled: false
});

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  BlockyModal
&lt;/h3&gt;

&lt;p&gt;Overlay dialogs with backdrop blur and smooth animations. Returns an instance for manual control.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
const modal = BlockyUI.createModal({
    title: 'Game Over',
    content: 'You scored 1,234 points!',
    showCloseButton: true,
    buttons: [
        { text: 'Play Again', variant: 'primary', onClick: restart }
    ]
});

modal.show(); // Display when ready

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  BlockyCard
&lt;/h3&gt;

&lt;p&gt;Content containers with 3D styling and optional headers. Now with variant support!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
const card = BlockyUI.createCard({
    title: 'Daily Rewards',
    variant: 'secondary',
    content: 'Come back tomorrow for more coins!'
});

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  BlockyTag
&lt;/h3&gt;

&lt;p&gt;Compact labels perfect for multipliers and status indicators.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
const tag = BlockyUI.createTag({
    content: '×2.5',
    variant: 'danger' // Red for high multipliers!
});

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  BlockyInfo
&lt;/h3&gt;

&lt;p&gt;Temporary notifications with auto-dismiss and 5 color themes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
const info = BlockyUI.createInfo({
    title: 'Achievement Unlocked!',
    titleColor: 'yellow',
    content: 'You reached level 10!'
});

document.body.appendChild(info);

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  BlockyPage
&lt;/h3&gt;

&lt;p&gt;Full-screen scrollable overlays with animated gradient borders.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
const page = BlockyUI.createPage({
    content: `
        &amp;lt;h1&amp;gt;Game Rules&amp;lt;/h1&amp;gt;
        &amp;lt;p&amp;gt;Here are the complete game instructions...&amp;lt;/p&amp;gt;
    `,
    onClose: () =&amp;gt; console.log('Rules closed')
});

page.show();

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🎮 Real-World Use Cases
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Casino Game Interfaces
&lt;/h3&gt;

&lt;p&gt;Perfect for slots, roulette, poker interfaces – anywhere you need that “blocky casino” aesthetic.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gaming Dashboards
&lt;/h3&gt;

&lt;p&gt;Player stats, leaderboards, achievement systems, inventory screens.&lt;/p&gt;

&lt;h3&gt;
  
  
  Interactive Web Apps
&lt;/h3&gt;

&lt;p&gt;Any application that wants to stand out with a unique, game-inspired design language.&lt;/p&gt;

&lt;h3&gt;
  
  
  Educational Platforms
&lt;/h3&gt;

&lt;p&gt;Gamified learning interfaces, quiz applications, progress tracking systems.&lt;/p&gt;

&lt;h2&gt;
  
  
  🔧 Installation &amp;amp; Setup
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Via npm/yarn/pnpm
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
# npm
npm install blocky-ui-lite

# yarn
yarn add blocky-ui-lite

# pnpm
pnpm add blocky-ui-lite



// Import CSS (required)
import 'blocky-ui-lite/styles';

// Import components
import { BlockyUI } from 'blocky-ui-lite';

// Use in your app
const button = BlockyUI.createButton({
    text: 'Click Me',
    variant: 'primary'
});

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Via CDN (No Build Step)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
&amp;lt;!-- CSS --&amp;gt;
&amp;lt;link rel="stylesheet" href="https://unpkg.com/blocky-ui-lite@latest/dist/blocky-ui.css"&amp;gt;

&amp;lt;!-- JavaScript (UMD) --&amp;gt;
&amp;lt;script src="https://unpkg.com/blocky-ui-lite@latest/dist/index.umd.js"&amp;gt;&amp;lt;/script&amp;gt;

&amp;lt;script&amp;gt;
    const { BlockyUI } = window.BlockyUILite;
    // Use BlockyUI here
&amp;lt;/script&amp;gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🎨 Framework Integration
&lt;/h2&gt;

&lt;h3&gt;
  
  
  React
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
import { useEffect, useRef } from 'react';
import { BlockyUI } from 'blocky-ui-lite';
import 'blocky-ui-lite/styles';

function GameButton() {
    const containerRef = useRef&amp;lt;HTMLDivElement&amp;gt;(null);

    useEffect(() =&amp;gt; {
        if (containerRef.current) {
            const button = BlockyUI.createButton({
                text: 'PLAY',
                variant: 'primary',
                onClick: () =&amp;gt; console.log('Game started!')
            });
            containerRef.current.appendChild(button);
        }
    }, []);

    return &amp;lt;div ref={containerRef} /&amp;gt;;
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Vue 3
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
&amp;lt;script setup lang="ts"&amp;gt;
import { onMounted, ref } from 'vue';
import { BlockyUI } from 'blocky-ui-lite';
import 'blocky-ui-lite/styles';

const containerRef = ref&amp;lt;HTMLDivElement | null&amp;gt;(null);

onMounted(() =&amp;gt; {
    if (containerRef.value) {
        const button = BlockyUI.createButton({
            text: 'PLAY',
            variant: 'primary',
            onClick: () =&amp;gt; console.log('Game started!')
        });
        containerRef.value.appendChild(button);
    }
});
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
    &amp;lt;div ref="containerRef"&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Svelte
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
&amp;lt;script lang="ts"&amp;gt;
    import { onMount } from 'svelte';
    import { BlockyUI } from 'blocky-ui-lite';
    import 'blocky-ui-lite/styles';

    let container: HTMLDivElement;

    onMount(() =&amp;gt; {
        const button = BlockyUI.createButton({
            text: 'PLAY',
            variant: 'primary',
            onClick: () =&amp;gt; console.log('Game started!')
        });
        container.appendChild(button);
    });
&amp;lt;/script&amp;gt;

&amp;lt;div bind:this={container}&amp;gt;&amp;lt;/div&amp;gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🚀 Performance Considerations
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Bundle Size
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CSS&lt;/strong&gt; : ~8KB minified, ~2KB gzipped&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JavaScript&lt;/strong&gt; : ~12KB minified, ~4KB gzipped&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Total&lt;/strong&gt; : ~20KB minified, ~6KB gzipped&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Runtime Performance
&lt;/h3&gt;

&lt;p&gt;All animations are CSS-based using &lt;code&gt;transform&lt;/code&gt; and &lt;code&gt;opacity&lt;/code&gt;, ensuring smooth 60fps animations with GPU acceleration. No JavaScript animation loops means zero CPU overhead.&lt;/p&gt;

&lt;h3&gt;
  
  
  Load Time Optimization
&lt;/h3&gt;

&lt;p&gt;When using via CDN, both unpkg.com and jsdelivr.net offer automatic minification, compression, and edge caching for blazing-fast delivery worldwide.&lt;/p&gt;

&lt;h2&gt;
  
  
  🔮 Future Roadmap
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;⏳ &lt;strong&gt;More Components&lt;/strong&gt; : Tabs, Tooltips, Dropdowns, Sliders&lt;/li&gt;
&lt;li&gt;⏳ &lt;strong&gt;Theming API&lt;/strong&gt; : Runtime theme switching&lt;/li&gt;
&lt;li&gt;⏳ &lt;strong&gt;Animation Library&lt;/strong&gt; : Pre-built entrance/exit animations&lt;/li&gt;
&lt;li&gt;⏳ &lt;strong&gt;Form Components&lt;/strong&gt; : Inputs, Checkboxes, Radio buttons with 3D styling&lt;/li&gt;
&lt;li&gt;⏳ &lt;strong&gt;Icon System&lt;/strong&gt; : Optional built-in icon support&lt;/li&gt;
&lt;li&gt;⏳ &lt;strong&gt;Enhanced Accessibility&lt;/strong&gt; : ARIA labels, keyboard navigation improvements&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  📚 Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;🌐 &lt;strong&gt;Live Demo&lt;/strong&gt; : &lt;a href="https://fur-gaming.github.io/blocky-ui/" rel="noopener noreferrer"&gt;https://fur-gaming.github.io/blocky-ui/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📦 &lt;strong&gt;npm Package&lt;/strong&gt; : &lt;a href="https://www.npmjs.com/package/blocky-ui-lite" rel="noopener noreferrer"&gt;blocky-ui-lite&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🐙 &lt;strong&gt;GitHub&lt;/strong&gt; : &lt;a href="https://github.com/fuR-Gaming/blocky-ui" rel="noopener noreferrer"&gt;fuR-Gaming/blocky-ui&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📖 &lt;strong&gt;Documentation Wiki&lt;/strong&gt; : &lt;a href="https://github.com/fuR-Gaming/blocky-ui/wiki" rel="noopener noreferrer"&gt;GitHub Wiki&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🎯 &lt;strong&gt;Stack Rush&lt;/strong&gt; : Original game that inspired the design&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🤝 Contributing
&lt;/h2&gt;

&lt;p&gt;Blocky UI Lite is open source and welcomes contributions! Whether it’s bug reports, feature requests, or pull requests – all contributions are appreciated.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Clone the repository
git clone https://github.com/fuR-Gaming/blocky-ui.git

# Install dependencies
npm install

# Start development server
npm run dev

# Build the library
npm run build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  📄 License
&lt;/h2&gt;

&lt;p&gt;MIT License – Free to use in personal and commercial projects. No attribution required (but always appreciated! 🙏)&lt;/p&gt;

&lt;h2&gt;
  
  
  🎉 Conclusion
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Blocky UI Lite&lt;/strong&gt; brings a unique aesthetic to web development – one that’s perfect for gaming interfaces, casino applications, and any project that wants to stand out with bold, 3D styling. With zero dependencies, full TypeScript support, and pure CSS effects, it’s both powerful and lightweight.&lt;/p&gt;

&lt;p&gt;Give it a try in your next project, and let us know what you build with it!&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built with ❤️ by fuR Gaming | Powered by Claude Code 🤖&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The post &lt;a href="https://www.richardfu.net/introducing-blocky-ui-lite-a-3d-blocky-themed-component-library/" rel="noopener noreferrer"&gt;Introducing Blocky UI Lite: A 3D Blocky-Themed Component Library 🎮&lt;/a&gt; appeared first on &lt;a href="https://www.richardfu.net" rel="noopener noreferrer"&gt;Richard Fu&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>zerodependencies</category>
      <category>3deffects</category>
      <category>componentlibrary</category>
    </item>
    <item>
      <title>Planet Blue Invasion: Building the Future of Casino Gaming with WebGPU and Three.js</title>
      <dc:creator>Richard Fu</dc:creator>
      <pubDate>Fri, 12 Sep 2025 14:03:36 +0000</pubDate>
      <link>https://forem.com/raw-fun-gaming/planet-blue-invasion-building-the-future-of-casino-gaming-with-webgpu-and-threejs-45l</link>
      <guid>https://forem.com/raw-fun-gaming/planet-blue-invasion-building-the-future-of-casino-gaming-with-webgpu-and-threejs-45l</guid>
      <description>&lt;p&gt;&lt;em&gt;How we built a cutting-edge 3D Earth simulation casino game using modern web technologies&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.richardfu.net/planet-blue-invasion-building-the-future-of-casino-gaming-with-webgpu-and-three-js/" rel="noopener noreferrer"&gt;View Post&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As a game developer, I’m excited to share the technical journey behind &lt;strong&gt;Planet Blue Invasion&lt;/strong&gt; , now live on &lt;a href="https://stake.com/zh/casino/games/furgaming-planet-blue-invasion" rel="noopener noreferrer"&gt;Stake.com&lt;/a&gt;. This isn’t just another casino game—it’s a showcase of what’s possible when you combine modern web graphics technology with innovative game design.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2lcr1gkrvemv2svi2k54.png" alt="🌍" width="72" height="72"&gt; The Vision: Alien Invasion Meets Casino Gaming
&lt;/h2&gt;

&lt;p&gt;Planet Blue Invasion puts players in the role of elite alien commanders aboard a war-class spaceship. The gameplay is deceptively simple yet thrilling: spin to target random Earth locations, fire orbital lasers, and earn payouts based on real population density data. Hit Shanghai? Massive payout. Strike the Pacific Ocean? Zero reward.&lt;/p&gt;

&lt;p&gt;What makes this special is the underlying technology that creates an immersive, visually stunning experience that feels more like a AAA game than traditional casino fare.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwip4140thkpy78q7t7fv.png" alt="⚡" width="72" height="72"&gt; The Tech Stack: Pushing Web Graphics Forward
&lt;/h2&gt;

&lt;h3&gt;
  
  
  WebGPU: The Graphics Revolution
&lt;/h3&gt;

&lt;p&gt;At the heart of Planet Blue Invasion is &lt;strong&gt;WebGPU&lt;/strong&gt; —the next-generation graphics API for the web. Unlike WebGL, WebGPU provides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Native GPU Performance&lt;/strong&gt; : Direct access to modern GPU features with significantly lower overhead&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Advanced Shading&lt;/strong&gt; : Complex material systems with better performance&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Future-Proof Architecture&lt;/strong&gt; : Built for modern GPUs and rendering pipelines&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We’re using Three.js’s WebGPU renderer, which puts us at the forefront of web graphics technology. The Earth you see isn’t just a textured sphere—it’s a multi-layered planetary system with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dynamic day/night cycles&lt;/li&gt;
&lt;li&gt;Realistic atmospheric scattering&lt;/li&gt;
&lt;li&gt;Volumetric cloud rendering&lt;/li&gt;
&lt;li&gt;Surface detail mapping with bump effects&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  TSL (Three Shading Language): Node-Based Materials
&lt;/h3&gt;

&lt;p&gt;One of the most exciting aspects of development was using &lt;strong&gt;TSL (Three Shading Language)&lt;/strong&gt;. This node-based material system allows us to create complex shaders visually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
// TSL material example for Earth's day/night blending
const earthMaterial = new TSLMaterial({
  nodes: {
    diffuse: mix(
      texture(dayTexture, uv()),
      texture(nightTexture, uv()),
      sunDirection.dot(normal).add(0.5)
    ),
    normal: texture(bumpTexture, uv()).xyz.mul(2).sub(1)
  }
});

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach gives us real-time material editing capabilities and better performance than traditional GLSL shaders.&lt;/p&gt;

&lt;h3&gt;
  
  
  TypeScript: Type Safety in Complex Systems
&lt;/h3&gt;

&lt;p&gt;With a game this complex, &lt;strong&gt;TypeScript&lt;/strong&gt; was essential. Our architecture includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Strict typing&lt;/strong&gt; for 3D math operations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interface contracts&lt;/strong&gt; between game systems&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compile-time safety&lt;/strong&gt; for API integrations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The population data system, for example, handles millions of coordinate lookups with complete type safety:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
interface LocationData {
  latitude: number;
  longitude: number;
  population: number;
  city: string;
  country: string;
  payoutMultiplier: number;
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fihygdnfxaaahzqddtzya.png" alt="🎮" width="72" height="72"&gt; The fuR Gaming Engine: Framework-Agnostic Architecture
&lt;/h2&gt;

&lt;p&gt;Planet Blue Invasion is built on our proprietary &lt;strong&gt;fuR Gaming Engine&lt;/strong&gt; —a framework-agnostic system designed specifically for casino games. The engine provides:&lt;/p&gt;

&lt;h3&gt;
  
  
  Internationalization (i18n) System
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;16 language support&lt;/strong&gt; including RTL languages like Arabic&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic loading&lt;/strong&gt; with fallback chains&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cultural adaptation&lt;/strong&gt; for different markets&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Audio System
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Spatial audio effects&lt;/strong&gt; for orbital laser sounds&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic music&lt;/strong&gt; that responds to game states&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Howler.js integration&lt;/strong&gt; for web audio optimization&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Math &amp;amp; RTP System
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Provably fair mathematics&lt;/strong&gt; with 97% RTP&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Weight distribution algorithms&lt;/strong&gt; for balanced gameplay&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Population-based payout calculations&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhrg3gvhf9v8fub7w3gic.png" alt="🌐" width="72" height="72"&gt; Real-World Data Integration
&lt;/h2&gt;

&lt;p&gt;One of the most challenging aspects was integrating real population data. Our system:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Uses GeoNames database&lt;/strong&gt; with 32,283 cities having 15K+ population&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Queries random coordinates&lt;/strong&gt; via BigDataCloud and Nominatim APIs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validates location data&lt;/strong&gt; – locations without city/country data become “empty payout” entries&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Calculates realistic payouts&lt;/strong&gt; based on actual population density from GeoNames&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The formula is elegantly simple: &lt;code&gt;Math.round((population / 100000) * 100)&lt;/code&gt;, making Shanghai, 24.87M population, the theoretical maximum payout at 248.74x.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0fjgr7bha1n1hjuxtxwv.png" alt="🚀" width="72" height="72"&gt; Performance Optimizations
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Asset Management
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Relative path architecture&lt;/strong&gt; for CDN compatibility&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Texture optimization&lt;/strong&gt; with 4K Earth textures compressed efficiently&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Progressive loading&lt;/strong&gt; for smooth startup experience&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Rendering Pipeline
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Level-of-detail (LOD) systems&lt;/strong&gt; for Earth surface detail&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frustum culling&lt;/strong&gt; for off-screen elements&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Batch rendering&lt;/strong&gt; for UI elements&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpm9bfmo5g7sp2e04z1vq.png" alt="🎯" width="72" height="72"&gt; The Casino Gaming Mathematics
&lt;/h2&gt;

&lt;p&gt;Behind the stunning visuals lies sophisticated gambling mathematics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Target RTP: 97%&lt;/strong&gt; – Industry-leading return to player&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hit Rate: 33%&lt;/strong&gt; – Balanced win frequency&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tier Distribution&lt;/strong&gt; :

&lt;ul&gt;
&lt;li&gt;No Win: 67% (Ocean/uninhabited)&lt;/li&gt;
&lt;li&gt;Small Wins: 23.1% (1x-10x)&lt;/li&gt;
&lt;li&gt;Medium Wins: 8.25% (10x-100x)&lt;/li&gt;
&lt;li&gt;Big Wins: 1.65% (100x+)&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;Our analytics system continuously verifies these mathematics to ensure fair, engaging gameplay.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3ovay7f5094jnq0qf9c2.png" alt="🌟" width="72" height="72"&gt; Bonus Features: Super Spin Mode – Human Radar
&lt;/h2&gt;

&lt;p&gt;The “Super Spin Mode: Human Radar” showcases our engine’s capabilities:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Smart Targeting&lt;/strong&gt; – Advanced alien technology detects human settlements before firing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Guaranteed Impact&lt;/strong&gt; – Every shot hits a populated zone, no wasted ammunition&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Premium Cost&lt;/strong&gt; – Activate for 2x base bet to access elite targeting system&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Higher Rewards&lt;/strong&gt; – Focus destruction on densely populated areas for maximum devastation&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw6n1fekf9sk082jqtwi7.png" alt="🔧" width="72" height="72"&gt; Development Workflow
&lt;/h2&gt;

&lt;p&gt;Our development setup prioritizes modern tooling:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Vite&lt;/strong&gt; for lightning-fast development builds&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local Three.js builds&lt;/strong&gt; for WebGPU feature access&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GLSL shader integration&lt;/strong&gt; with hot reloading&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automated testing&lt;/strong&gt; for gambling mathematics verification&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp02smn7o2y1tl0r7pkby.png" alt="🎉" width="72" height="72"&gt; Play Planet Blue Invasion Today
&lt;/h2&gt;

&lt;p&gt;Planet Blue Invasion represents what’s possible when you combine cutting-edge web technology with innovative game design. Every spin is a journey through real Earth data, every win is backed by provably fair mathematics, and every visual effect is rendered using the latest WebGPU technology.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyqjaui9tsmlra11m5089.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyqjaui9tsmlra11m5089.jpg" alt="Stake New Release - Planet Blue Invasion" width="782" height="1024"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://stake.com/casino/games/furgaming-planet-blue-invasion" rel="noopener noreferrer"&gt;Experience Planet Blue Invasion on Stake.com&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Whether you’re a fellow developer curious about WebGPU implementation, a gaming enthusiast looking for the next evolution in casino games, or someone who simply enjoys blowing up virtual Earth locations for profit—Planet Blue Invasion delivers.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F746uwh5mj78jfvfjigmn.png" alt="🛠" width="72" height="72"&gt; Technical Resources
&lt;/h2&gt;

&lt;p&gt;For developers interested in the technologies used:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://threejs.org/" rel="noopener noreferrer"&gt;Three.js WebGPU Renderer&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/gpuweb/gpuweb" rel="noopener noreferrer"&gt;WebGPU Specification&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.typescriptlang.org/" rel="noopener noreferrer"&gt;TypeScript&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://vitejs.dev/" rel="noopener noreferrer"&gt;Vite Build Tool&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Ready to join the alien invasion? The Earth is waiting… &lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpxklodq1vriqsfyebjm9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpxklodq1vriqsfyebjm9.png" alt="👽" width="72" height="72"&gt;&lt;/a&gt; &lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2lcr1gkrvemv2svi2k54.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2lcr1gkrvemv2svi2k54.png" alt="🌍" width="72" height="72"&gt;&lt;/a&gt; &lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwip4140thkpy78q7t7fv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwip4140thkpy78q7t7fv.png" alt="⚡" width="72" height="72"&gt;&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The post &lt;a href="https://www.richardfu.net/planet-blue-invasion-building-the-future-of-casino-gaming-with-webgpu-and-three-js/" rel="noopener noreferrer"&gt;Planet Blue Invasion: Building the Future of Casino Gaming with WebGPU and Three.js&lt;/a&gt; appeared first on &lt;a href="https://www.richardfu.net" rel="noopener noreferrer"&gt;Richard Fu&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>threejs</category>
      <category>typescript</category>
      <category>gamedev</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Introducing Cosmic UI Lite: A Zero-Dependency Space-Themed UI Library</title>
      <dc:creator>Richard Fu</dc:creator>
      <pubDate>Mon, 01 Sep 2025 09:30:55 +0000</pubDate>
      <link>https://forem.com/raw-fun-gaming/introducing-cosmic-ui-lite-a-zero-dependency-space-themed-ui-library-20bi</link>
      <guid>https://forem.com/raw-fun-gaming/introducing-cosmic-ui-lite-a-zero-dependency-space-themed-ui-library-20bi</guid>
      <description>&lt;p&gt;Ever needed a futuristic, sci-fi UI for your web project but found existing solutions too heavyweight or framework-dependent? That’s exactly the problem I faced when building my game project. Today, I’m excited to introduce &lt;strong&gt;Cosmic UI Lite&lt;/strong&gt; – a lightweight, zero-dependency TypeScript UI component library designed for space-themed interfaces.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🌟 &lt;a href="https://fur-gaming.github.io/cosmic-ui-lite/" rel="noopener noreferrer"&gt;Try the Live Demo&lt;/a&gt;&lt;/strong&gt; | &lt;strong&gt;📦 &lt;a href="https://www.npmjs.com/package/cosmic-ui-lite" rel="noopener noreferrer"&gt;NPM Package&lt;/a&gt;&lt;/strong&gt; | &lt;strong&gt;📚 &lt;a href="https://github.com/fuR-Gaming/cosmic-ui-lite" rel="noopener noreferrer"&gt;GitHub Repository&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq1sclf9q0ckxp1y0dahy.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq1sclf9q0ckxp1y0dahy.jpg" alt="Cosmic UI Lite modal screenshot" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  💡 The Motivation
&lt;/h2&gt;

&lt;p&gt;While working on my game project, I discovered &lt;a href="https://github.com/rizkimuhammada/cosmic-ui" rel="noopener noreferrer"&gt;rizkimuhammada/cosmic-ui&lt;/a&gt; – a beautiful cosmic-themed UI library. However, it had some limitations for my use case:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;React Dependency&lt;/strong&gt; : My game was built with vanilla TypeScript&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Feature Bloat&lt;/strong&gt; : I only needed a handful of essential components&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bundle Size&lt;/strong&gt; : Needed something lightweight for game performance&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build Complexity&lt;/strong&gt; : Wanted a simple, drop-in solution&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I decided to create a focused alternative that prioritizes simplicity, performance, and universal compatibility.&lt;/p&gt;

&lt;h2&gt;
  
  
  🛠️ The Technology Behind Cosmic UI Lite
&lt;/h2&gt;

&lt;h3&gt;
  
  
  SVG-Based Dynamic Rendering
&lt;/h3&gt;

&lt;p&gt;The heart of Cosmic UI Lite lies in its dynamic SVG generation system. Instead of using static images or complex CSS tricks, every component creates its cosmic borders and backgrounds programmatically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
// SVG elements are created on-the-fly
const backgroundSvg = createSvgElement('cosmic-bg', '0 0 474 332');
const gradient = createGradient('cosmicGradient', [
  { offset: '0%', color: '#1a1a2e' },
  { offset: '50%', color: '#2a2a4e' },
  { offset: '100%', color: '#1a1a2e' }
]);

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach provides several advantages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Scalability&lt;/strong&gt; : Vector graphics look crisp at any size&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Customization&lt;/strong&gt; : Colors and gradients can be modified programmatically&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance&lt;/strong&gt; : No external image dependencies to load&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consistency&lt;/strong&gt; : Unified shape language across all components&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Four-Layer Architecture
&lt;/h3&gt;

&lt;p&gt;Each component follows a consistent 4-layer structure:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Wrapper Element&lt;/strong&gt; : Container with positioning and hover effects&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SVG Background&lt;/strong&gt; : Animated gradient fill with cosmic patterns&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SVG Border&lt;/strong&gt; : Glowing outline that responds to interactions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Content Layer&lt;/strong&gt; : Text, buttons, and interactive elements&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Zero Dependencies Philosophy
&lt;/h3&gt;

&lt;p&gt;Cosmic UI Lite is built with pure TypeScript and vanilla JavaScript – no runtime dependencies whatsoever. This means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;Universal Compatibility&lt;/strong&gt; : Works with any framework or vanilla JS&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Tiny Bundle Size&lt;/strong&gt; : No dependency tree bloat&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Security&lt;/strong&gt; : No supply chain vulnerabilities&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Reliability&lt;/strong&gt; : Won’t break due to dependency updates&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🎨 Component Showcase
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Cosmic Button
&lt;/h3&gt;

&lt;p&gt;Animated buttons with hover effects and multiple variants:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
import { CosmicUI } from 'cosmic-ui-lite';

const launchButton = CosmicUI.createButton({
  text: '🚀 Launch Mission',
  variant: 'primary',
  onClick: () =&amp;gt; console.log('Mission started!')
});

document.body.appendChild(launchButton);

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Cosmic Modal
&lt;/h3&gt;

&lt;p&gt;Full-featured modals with backdrop blur and cosmic styling:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
const confirmModal = CosmicUI.createModal({
  title: 'Mission Control',
  content: `
    &amp;lt;p&amp;gt;Are you ready to launch the mission?&amp;lt;/p&amp;gt;
    &amp;lt;p&amp;gt;This action cannot be undone.&amp;lt;/p&amp;gt;
  `,
  buttons: [
    {
      text: 'Cancel',
      variant: 'secondary',
      onClick: () =&amp;gt; console.log('Cancelled')
    },
    {
      text: 'Launch',
      variant: 'danger',
      onClick: () =&amp;gt; console.log('Launching!')
    }
  ]
});

CosmicUI.showModal(confirmModal);

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Utility Methods
&lt;/h3&gt;

&lt;p&gt;Built-in utilities for common patterns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
// Quick confirmation dialog
CosmicUI.showConfirmation(
  'Delete Save File',
  'This will permanently delete your progress.',
  () =&amp;gt; console.log('Deleted'),
  () =&amp;gt; console.log('Cancelled')
);

// Error notifications
CosmicUI.showError(
  'Connection Lost',
  'Unable to connect to mission control.',
  () =&amp;gt; console.log('Acknowledged')
);

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🎯 Perfect for Game Development
&lt;/h2&gt;

&lt;p&gt;Cosmic UI Lite was specifically designed with game development in mind:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Performance Optimized&lt;/strong&gt; : Lightweight components that don’t impact game performance&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Thematic Consistency&lt;/strong&gt; : Space/sci-fi aesthetic perfect for space games, RPGs, and strategy games&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Framework Agnostic&lt;/strong&gt; : Works with any game engine that supports web technologies&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Responsive Design&lt;/strong&gt; : Adapts to different screen sizes and device types&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🚀 Getting Started
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Installation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
# NPM
npm install cosmic-ui-lite

# Yarn
yarn add cosmic-ui-lite

# CDN
&amp;lt;script src="https://unpkg.com/cosmic-ui-lite@latest/dist/index.umd.js"&amp;gt;&amp;lt;/script&amp;gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Basic Usage
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
import { CosmicUI } from 'cosmic-ui-lite';
// CSS is automatically imported

// Create a space dashboard
const statusCard = CosmicUI.createCard({
  title: 'Ship Status',
  content: `
    &amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;Hull Integrity:&amp;lt;/strong&amp;gt; &amp;lt;span style="color: #00ff88;"&amp;gt;100%&amp;lt;/span&amp;gt;&amp;lt;/p&amp;gt;
    &amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;Power Level:&amp;lt;/strong&amp;gt; &amp;lt;span style="color: #00d4ff;"&amp;gt;Optimal&amp;lt;/span&amp;gt;&amp;lt;/p&amp;gt;
    &amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;Shields:&amp;lt;/strong&amp;gt; &amp;lt;span style="color: #ffaa00;"&amp;gt;Charging&amp;lt;/span&amp;gt;&amp;lt;/p&amp;gt;
  `
});

document.body.appendChild(statusCard);

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🎨 Visual Design Philosophy
&lt;/h2&gt;

&lt;p&gt;The visual design draws inspiration from classic sci-fi interfaces with modern web standards:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Angled Corners&lt;/strong&gt; : Distinctive beveled edges that evoke spaceship control panels&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Animated Gradients&lt;/strong&gt; : Subtle particle-like animations that bring interfaces to life&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cosmic Color Palette&lt;/strong&gt; : Deep space blues, electric cyans, and warning oranges&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Glowing Effects&lt;/strong&gt; : Subtle borders that pulse and respond to user interaction&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  📊 Technical Specifications
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;📦 &lt;strong&gt;Bundle Size&lt;/strong&gt; : ~15KB minified + gzipped&lt;/li&gt;
&lt;li&gt;🎯 &lt;strong&gt;TypeScript&lt;/strong&gt; : Full type safety with comprehensive interfaces&lt;/li&gt;
&lt;li&gt;🌐 &lt;strong&gt;Browser Support&lt;/strong&gt; : All modern browsers (ES2020+)&lt;/li&gt;
&lt;li&gt;📱 &lt;strong&gt;Responsive&lt;/strong&gt; : Mobile-first design with adaptive layouts&lt;/li&gt;
&lt;li&gt;♿ &lt;strong&gt;Accessible&lt;/strong&gt; : WCAG-compliant with keyboard navigation&lt;/li&gt;
&lt;li&gt;⚡ &lt;strong&gt;Performance&lt;/strong&gt; : Zero-dependency, optimized for games&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🔮 Future Roadmap
&lt;/h2&gt;

&lt;p&gt;While Cosmic UI Lite is designed to stay lightweight, there are some exciting possibilities on the horizon:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Theme System&lt;/strong&gt; : Multiple cosmic color schemes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Animation Presets&lt;/strong&gt; : Configurable animation intensity levels&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Component Extensions&lt;/strong&gt; : Additional specialized components based on community feedback&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Framework Integrations&lt;/strong&gt; : Optional wrappers for React, Vue, and Angular&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🌟 Try It Today!
&lt;/h2&gt;

&lt;p&gt;Ready to add some cosmic flair to your next project? Check out these resources:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🌐 &lt;strong&gt;&lt;a href="https://fur-gaming.github.io/cosmic-ui-lite/" rel="noopener noreferrer"&gt;Interactive Demo&lt;/a&gt;&lt;/strong&gt; – Try all components live&lt;/li&gt;
&lt;li&gt;📦 &lt;strong&gt;&lt;a href="https://www.npmjs.com/package/cosmic-ui-lite" rel="noopener noreferrer"&gt;NPM Package&lt;/a&gt;&lt;/strong&gt; – Install with your favorite package manager&lt;/li&gt;
&lt;li&gt;📚 &lt;strong&gt;&lt;a href="https://github.com/fuR-Gaming/cosmic-ui-lite/wiki" rel="noopener noreferrer"&gt;Documentation&lt;/a&gt;&lt;/strong&gt; – Complete guides and examples&lt;/li&gt;
&lt;li&gt;🛠️ &lt;strong&gt;&lt;a href="https://github.com/fuR-Gaming/cosmic-ui-lite" rel="noopener noreferrer"&gt;GitHub Repository&lt;/a&gt;&lt;/strong&gt; – Source code and issue tracking&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Whether you’re building a space exploration game, a sci-fi web application, or just want to add some futuristic flair to your project, Cosmic UI Lite provides the perfect balance of visual impact and technical simplicity.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;What kind of cosmic interfaces will you build? I’d love to see your creations – share them in the comments below or tag me on social media!&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;small&gt;&lt;em&gt;Cosmic UI Lite is open source and available under the MIT License. Contributions, feedback, and cosmic creativity are always welcome! 🚀&lt;/em&gt;&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;The post &lt;a href="https://www.richardfu.net/introducing-cosmic-ui-lite-a-zero-dependency-space-themed-ui-library/" rel="noopener noreferrer"&gt;Introducing Cosmic UI Lite: A Zero-Dependency Space-Themed UI Library&lt;/a&gt; appeared first on &lt;a href="https://www.richardfu.net" rel="noopener noreferrer"&gt;Richard Fu&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>webdev</category>
      <category>spacetheme</category>
      <category>scifi</category>
    </item>
    <item>
      <title>Building an Automated LeetCode Solution Post Sync Feature for LeetHub</title>
      <dc:creator>Richard Fu</dc:creator>
      <pubDate>Tue, 29 Jul 2025 14:09:39 +0000</pubDate>
      <link>https://forem.com/raw-fun-gaming/building-an-automated-leetcode-solution-post-sync-feature-for-leethub-2mbb</link>
      <guid>https://forem.com/raw-fun-gaming/building-an-automated-leetcode-solution-post-sync-feature-for-leethub-2mbb</guid>
      <description>&lt;p&gt;As a passionate LeetCode enthusiast, I tackle the daily coding challenge religiously and share my solution insights with the community through detailed posts (often polished with AI assistance). My coding journey is powered by LeetHub, an incredible Chrome extension that automatically synchronizes my LeetCode solutions to my GitHub repository, helping me build a robust coding portfolio over time.&lt;/p&gt;

&lt;p&gt;However, I noticed a gap in my workflow. While LeetHub excellently captures my code solutions as plain script files, my thoughtful solution explanations—complete with intuition, approach breakdowns, and complexity analysis—were stuck on LeetCode’s platform. I found myself manually copying these posts to create &lt;code&gt;Solution.md&lt;/code&gt; files in my GitHub repo, which was both tedious and inconsistent.&lt;/p&gt;

&lt;p&gt;That’s when it hit me: why couldn’t LeetHub handle this automatically? This realization sparked my journey to extend LeetHub’s capabilities, and today I’m excited to share the story of building an automated solution post sync feature that bridges this gap.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the Feature Works
&lt;/h2&gt;

&lt;p&gt;The new feature operates seamlessly in the background, requiring zero additional effort from users:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Detection&lt;/strong&gt; : When you publish a solution post on LeetCode, the extension automatically detects the action by intercepting the GraphQL request&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Content Extraction&lt;/strong&gt; : It captures your solution title, content, and problem metadata from the request payload&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Smart Mapping&lt;/strong&gt; : The extension maps the LeetCode problem slug to your existing GitHub repository folder structure&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Commit Message Consistency&lt;/strong&gt; : It fetches your previous solution commit message from GitHub API (e.g., “Time: 44 ms (100%), Space: 73 MB (100%) – LeetHub”)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automatic Upload&lt;/strong&gt; : Creates a &lt;code&gt;Solution.md&lt;/code&gt; file in the same folder as your code solution with the identical commit message&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The result? Your GitHub repository now contains both your code solutions AND your detailed explanations, creating a complete documentation of your problem-solving approach.&lt;/p&gt;

&lt;h2&gt;
  
  
  Technical Challenges and Solutions
&lt;/h2&gt;

&lt;p&gt;Building this feature presented several fascinating technical challenges that pushed the boundaries of Chrome extension development:&lt;/p&gt;

&lt;h3&gt;
  
  
  Challenge 1: Request Interception in Modern Web Applications
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The Problem&lt;/strong&gt; : LeetCode uses GraphQL requests for solution posting, but traditional content script approaches couldn’t reliably intercept these requests due to Chrome’s security model and timing issues.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Solution&lt;/strong&gt; : We implemented a dual content script architecture using Chrome Extension Manifest V3’s &lt;code&gt;world: "MAIN"&lt;/code&gt; feature:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
// manifest.json
"content_scripts": [
  {
    "js": ["src/js/interceptor.js"],
    "run_at": "document_start",
    "world": "MAIN" // Runs in page's JavaScript context
  },
  {
    "js": ["src/js/leetcode.js"],
    "run_at": "document_idle" // Runs in isolated extension context
  }
]

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach allows the interceptor to catch network requests in the same execution context as LeetCode’s JavaScript while maintaining secure communication with the main extension logic.&lt;/p&gt;

&lt;h3&gt;
  
  
  Challenge 2: Content Security Policy Restrictions
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The Problem&lt;/strong&gt; : LeetCode’s strict Content Security Policy blocked our initial attempts to inject interceptor code directly into the page.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Solution&lt;/strong&gt; : Instead of inline script injection, we created a separate interceptor file that runs in the MAIN world context, bypassing CSP restrictions while maintaining functionality:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
// Intercept both fetch and XMLHttpRequest methods
const originalFetch = window.fetch;
window.fetch = function(...args) {
  // Intercept and process GraphQL requests
  if (body.operationName === 'ugcArticlePublishSolution') {
    // Extract and forward solution data
  }
  return originalFetch.apply(this, args);
};

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Challenge 3: Problem Name Mapping and GitHub Integration
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The Problem&lt;/strong&gt; : LeetCode’s problem slugs (e.g., “maximum-matching-of-players-with-trainers”) needed to be mapped to GitHub folder names (e.g., “2410-maximum-matching-of-players-with-trainers”), and we needed to maintain commit message consistency.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Solution&lt;/strong&gt; : We implemented intelligent problem name matching and GitHub API integration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
async function getLastCommitMessage(problemName) {
  // Fetch commit history from GitHub API
  const commitsUrl = `https://api.github.com/repos/${hook}/commits?path=${folderPath}`;

  // Find the most recent solution commit (skip README/NOTES)
  for (const commit of commits) {
    if (commit.message.includes('Time:') &amp;amp;&amp;amp; commit.message.includes('Space:')) {
      return commit.message; // Use the same format
    }
  }
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Challenge 4: User Experience and Settings Integration
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The Problem&lt;/strong&gt; : The feature needed to be discoverable and controllable without disrupting existing LeetHub workflows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Solution&lt;/strong&gt; : We added a collapsible settings section in the extension popup with a toggle switch (enabled by default), maintaining consistency with LeetHub’s existing UI patterns.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Result
&lt;/h2&gt;

&lt;p&gt;The feature now works flawlessly, automatically creating comprehensive documentation in users’ GitHub repositories. Here’s what a typical problem folder looks like after the enhancement:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
/2410-maximum-matching-of-players-with-trainers/
├── README.md # Problem description
├── 2410-maximum-matching-of-players-with-trainers.ts # Solution code  
└── Solution.md # Solution explanation ✨ NEW!

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both the code and solution post files share the same commit message format, creating a cohesive development history.&lt;/p&gt;

&lt;h2&gt;
  
  
  Support This Feature
&lt;/h2&gt;

&lt;p&gt;I’m thrilled to contribute this feature back to the LeetCode community! The implementation is now live as a pull request in the official LeetHub-3.0 repository.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;👍 If you’d find this feature useful, please show your support by giving a thumbs up to the PR: &lt;a href="https://github.com/raphaelheinz/LeetHub-3.0/pull/69" rel="noopener noreferrer"&gt;https://github.com/raphaelheinz/LeetHub-3.0/pull/69&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Your support helps demonstrate community interest and encourages the maintainers to merge this enhancement, making it available to thousands of LeetCode enthusiasts worldwide.&lt;/p&gt;

&lt;p&gt;This feature transforms LeetHub from a simple code backup tool into a comprehensive documentation system that captures both your solutions and the thought processes behind them. It’s a small change that makes a big difference in how we showcase our problem-solving journey on GitHub.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Ready to supercharge your LeetCode documentation workflow? Check out the PR and let’s make this feature a reality for everyone! 🚀&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The post &lt;a href="https://www.richardfu.net/building-an-automated-leetcode-solution-post-sync-feature-for-leethub/" rel="noopener noreferrer"&gt;Building an Automated LeetCode Solution Post Sync Feature for LeetHub&lt;/a&gt; appeared first on &lt;a href="https://www.richardfu.net" rel="noopener noreferrer"&gt;Richard Fu&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>chromeextension</category>
      <category>webdev</category>
      <category>leetcode</category>
    </item>
    <item>
      <title>Solving Starfield Perspective Distortion in 3D Space: A Three.js Case Study</title>
      <dc:creator>Richard Fu</dc:creator>
      <pubDate>Mon, 28 Jul 2025 08:25:33 +0000</pubDate>
      <link>https://forem.com/raw-fun-gaming/solving-starfield-perspective-distortion-in-3d-space-a-threejs-case-study-44dh</link>
      <guid>https://forem.com/raw-fun-gaming/solving-starfield-perspective-distortion-in-3d-space-a-threejs-case-study-44dh</guid>
      <description>&lt;p&gt;When building a 3D space simulation with Three.js WebGPU, I encountered a challenging visual problem that many 3D developers face: &lt;strong&gt;perspective distortion of billboard sprites&lt;/strong&gt;. Here’s how I identified the issue and implemented an elegant solution.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.richardfu.net/solving-starfield-perspective-distortion-in-3d-space-a-three-js-case-study/" rel="noopener noreferrer"&gt;Read more: Solving Starfield Perspective Distortion in 3D Space: A Three.js Case Study&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: Stretched Stars Behind the Camera
&lt;/h2&gt;

&lt;p&gt;In my Earth simulation game, I implemented a starfield background using 15,000 instanced plane geometries positioned on a sphere around the camera. The stars looked perfect when viewed straight-on, but had severe distortion issues:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Stars behind the camera&lt;/strong&gt; appeared as vertical lines (stretched 2-3x in height)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Brightness varied dramatically&lt;/strong&gt; based on viewing angle&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shape inconsistency&lt;/strong&gt; made the starfield look unrealistic&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Understanding the Root Cause
&lt;/h3&gt;

&lt;p&gt;The distortion occurred due to &lt;strong&gt;perspective projection foreshortening&lt;/strong&gt; :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
// Original problematic approach
const radius = 40 + Math.random() * 20; // Stars too close to camera
const position = new THREE.Vector3(
  radius * Math.sin(phi) * Math.cos(theta),
  radius * Math.sin(phi) * Math.sin(theta), 
  radius * Math.cos(phi)
);

// Static plane orientation - doesn't face camera
const quaternion = new THREE.Quaternion(); // Identity rotation
matrix.compose(position, quaternion, scale);

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why this failed:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Close proximity&lt;/strong&gt; (40-60 units) amplified perspective effects&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fixed orientation&lt;/strong&gt; meant planes viewed at oblique angles appeared stretched&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Perspective projection&lt;/strong&gt; caused extreme foreshortening near camera edges&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Attempted Solutions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Solution 1: Increase Distance
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
// Attempt 1: Move stars further away
const radius = 200 + Math.random() * 100;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt; Stars disappeared beyond the camera’s far clipping plane (100 units).&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution 2: Shader-Based Billboarding
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
// Attempt 2: TSL billboarding (complex, error-prone)
const viewDirection = normalize(cameraPosition.sub(instancePosition));
const right = normalize(cross(viewDirection, vec3(0, 1, 0)));
const up = cross(right, viewDirection);

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt; Complex TSL syntax caused shader compilation errors in WebGPU.&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution 3: CPU Billboarding
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
// Attempt 3: Per-frame matrix updates (expensive)
for (let i = 0; i &amp;lt; starCount; i++) {
  const lookMatrix = new THREE.Matrix4();
  lookMatrix.lookAt(position, camera.position, camera.up);
  // Update 15,000 matrices per frame
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt; 15,000 matrix calculations per frame caused performance issues.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Elegant Solution: Dual-Hemisphere Architecture
&lt;/h2&gt;

&lt;p&gt;The final solution involved &lt;strong&gt;architectural redesign&lt;/strong&gt; rather than complex workarounds:&lt;/p&gt;

&lt;h3&gt;
  
  
  Implementation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
export class Background {
  private frontStarfield!: THREE.InstancedMesh;
  private backStarfield!: THREE.InstancedMesh;
  private starGroup!: THREE.Group;

  createStarfield(): THREE.Group {
    this.starGroup = new THREE.Group();

    // Split into two hemispheres
    this.frontStarfield = this.createStarPlane(7500, true); // Front
    this.backStarfield = this.createStarPlane(7500, false); // Back

    this.starGroup.add(this.frontStarfield);
    this.starGroup.add(this.backStarfield);

    return this.starGroup;
  }

  private createStarPlane(starCount: number, isFront: boolean): THREE.InstancedMesh {
    // Generate hemisphere positions
    for (let i = 0; i &amp;lt; starCount; i++) {
      const phi = Math.acos(2 * Math.random() - 1);
      const theta = Math.random() * Math.PI * 2;

      // Separate front/back hemispheres
      let z = Math.cos(phi);
      if (!isFront) z = -z;

      const distance = 50; // Fixed distance eliminates perspective issues
      position.set(
        distance * Math.sin(phi) * Math.cos(theta),
        distance * Math.sin(phi) * Math.sin(theta),
        distance * z
      );

      // Calculate proper billboard rotation
      const lookDirection = new THREE.Vector3()
        .subVectors(new THREE.Vector3(0, 0, 0), position)
        .normalize();

      // Create rotation matrix to face camera
      const up = new THREE.Vector3(0, 1, 0);
      const right = new THREE.Vector3().crossVectors(up, lookDirection).normalize();
      up.crossVectors(lookDirection, right);

      const rotMatrix = new THREE.Matrix4();
      rotMatrix.makeBasis(right, up, lookDirection);
      rotMatrix.decompose(new THREE.Vector3(), quaternion, new THREE.Vector3());

      matrix.compose(position, quaternion, scale);
    }
  }
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Key Advantages
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Fixed Distance&lt;/strong&gt; : All stars at consistent 50-unit distance eliminates perspective distortion&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Proper Billboarding&lt;/strong&gt; : Each star’s rotation matrix calculated to face camera origin&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hemisphere Separation&lt;/strong&gt; : Dedicated handling for front/back visibility&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance&lt;/strong&gt; : One-time calculation during initialization, not per-frame&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Technical Benefits
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Visual Quality
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Consistent appearance&lt;/strong&gt; across all viewing angles&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No shape distortion&lt;/strong&gt; regardless of camera position&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Uniform brightness&lt;/strong&gt; independent of perspective&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Performance
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zero runtime cost&lt;/strong&gt; for billboard calculations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GPU-optimized&lt;/strong&gt; instanced rendering for 15,000 stars&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scalable&lt;/strong&gt; to even larger star counts&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Maintainability
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Simple architecture&lt;/strong&gt; easier to debug and extend&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clear separation&lt;/strong&gt; between front/back star management&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reusable pattern&lt;/strong&gt; for other billboard sprite scenarios&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Enhanced Features
&lt;/h2&gt;

&lt;p&gt;The solution also enabled advanced visual effects:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
// TSL-based twinkling with individual star timing
const sparkle = sin(mul(timeUniform, sparkleSpeedUniform)
  .add(mul(randomAttr, float(6.28))))
  .mul(float(0.5)).add(float(0.5));

const pulse = sin(mul(timeUniform, pulseSpeedUniform)
  .add(pulsePhase))
  .mul(float(0.4)).add(float(0.6));

// Sharp-edged circular stars
const circularAlpha = smoothstep(float(0.5), float(0.2), distance);
const finalAlpha = mul(circularAlpha, mul(sparkleEnhanced, pulse));

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Features achieved:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Individual star twinkling&lt;/strong&gt; with unique timing per star&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GPU-parallelized effects&lt;/strong&gt; for 15,000 stars with zero performance cost&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Realistic sparkle and pulse&lt;/strong&gt; effects mimicking atmospheric scintillation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sharp-edged circular stars&lt;/strong&gt; with configurable falloff&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Performance Metrics
&lt;/h2&gt;

&lt;p&gt;The final implementation delivers excellent performance:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;15,000 stars&lt;/strong&gt; rendering at 60+ FPS&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero CPU overhead&lt;/strong&gt; for star animation (GPU-only)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Minimal memory footprint&lt;/strong&gt; using instanced rendering&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WebGPU optimized&lt;/strong&gt; with Three.js TSL shaders&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Architectural solutions&lt;/strong&gt; often outperform technical workarounds&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fixed-distance billboards&lt;/strong&gt; eliminate many perspective issues&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hemisphere separation&lt;/strong&gt; provides better control than sphere-based approaches&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GPU shader effects&lt;/strong&gt; can be simpler than CPU-based alternatives&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WebGPU + TSL&lt;/strong&gt; requires different approaches than traditional WebGL&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Common Pitfalls to Avoid
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Camera Clipping Issues
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
// ![❌](https://s.w.org/images/core/emoji/15.1.0/72x72/274c.png) Bad: Stars beyond far plane
const camera = new THREE.PerspectiveCamera(25, aspect, 0.1, 100);
const starDistance = 200; // Beyond far plane!

// ![✅](https://s.w.org/images/core/emoji/15.1.0/72x72/2705.png) Good: Stars within camera range
const camera = new THREE.PerspectiveCamera(25, aspect, 0.1, 100);
const starDistance = 50; // Well within range

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  TSL Shader Compatibility
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
// ![❌](https://s.w.org/images/core/emoji/15.1.0/72x72/274c.png) Bad: onBeforeCompile doesn't work with WebGPU
material.onBeforeCompile = (shader) =&amp;gt; {
  // This won't execute in WebGPU mode
};

// ![✅](https://s.w.org/images/core/emoji/15.1.0/72x72/2705.png) Good: Use TSL node system
const sparkle = sin(mul(timeUniform, speedUniform));
material.opacityNode = sparkle;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Performance Anti-Patterns
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
// ![❌](https://s.w.org/images/core/emoji/15.1.0/72x72/274c.png) Bad: Per-frame matrix updates
animate() {
  for (let i = 0; i &amp;lt; 15000; i++) {
    updateStarMatrix(i); // 15k calculations per frame
  }
}

// ![✅](https://s.w.org/images/core/emoji/15.1.0/72x72/2705.png) Good: One-time setup with GPU animation
createStars() {
  // Calculate matrices once
  material.opacityNode = gpuAnimationNode; // GPU handles animation
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Instead of fighting perspective projection with complex mathematical solutions, restructuring the problem space proved more effective. The dual-hemisphere approach provides perfect visual results while maintaining excellent performance and code clarity.&lt;/p&gt;

&lt;p&gt;This pattern can be applied to any 3D scenario requiring consistent billboard sprite appearance across all viewing angles – from particle systems to UI elements in 3D space.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key takeaways:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Question architectural assumptions when facing visual artifacts&lt;/li&gt;
&lt;li&gt;GPU-based solutions often outperform CPU workarounds&lt;/li&gt;
&lt;li&gt;WebGPU + TSL requires rethinking traditional Three.js patterns&lt;/li&gt;
&lt;li&gt;Fixed-distance billboards solve many perspective distortion issues&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Related Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://sbcode.net/threejs/webgpu-renderer/" rel="noopener noreferrer"&gt;Three WebGPU Renderer&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/mrdoob/three.js/wiki/Three.js-Shading-Language" rel="noopener noreferrer"&gt;TSL (Three.js Shading Language) Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://threejs.org/docs/#api/en/objects/InstancedMesh" rel="noopener noreferrer"&gt;Instanced Rendering Best Practices&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://en.wikipedia.org/wiki/Sprite_(computer_graphics)" rel="noopener noreferrer"&gt;Billboard Sprite Techniques in 3D Graphics&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The post &lt;a href="https://www.richardfu.net/solving-starfield-perspective-distortion-in-3d-space-a-three-js-case-study/" rel="noopener noreferrer"&gt;Solving Starfield Perspective Distortion in 3D Space: A Three.js Case Study&lt;/a&gt; appeared first on &lt;a href="https://www.richardfu.net" rel="noopener noreferrer"&gt;Richard Fu&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>threejs</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Efficient Lazy Loading in Svelte: A Practical Guide for Svelte 4 and Svelte 5 (Runes)</title>
      <dc:creator>Richard Fu</dc:creator>
      <pubDate>Mon, 28 Jul 2025 02:51:34 +0000</pubDate>
      <link>https://forem.com/raw-fun-gaming/efficient-lazy-loading-in-svelte-a-practical-guide-for-svelte-4-and-svelte-5-runes-4951</link>
      <guid>https://forem.com/raw-fun-gaming/efficient-lazy-loading-in-svelte-a-practical-guide-for-svelte-4-and-svelte-5-runes-4951</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy19538islcjmo868cs2z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy19538islcjmo868cs2z.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Modern web applications are growing in complexity and size, often including many components that are not needed on the initial page load. Lazy loading is a powerful technique to optimize bundle size and improve performance by loading components only when they are actually needed—such as widgets, modals, or rarely-used UI elements. In this post, we’ll explore how to implement lazy loading in Svelte, compare the syntax between Svelte 4 and Svelte 5 (runes mode), and discuss the benefits and potential drawbacks of this approach.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Lazy Load Components?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Performance:&lt;/strong&gt; By splitting your bundle and loading code only when required, you reduce the initial load time and memory usage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User Experience:&lt;/strong&gt; Faster initial load means a snappier experience, especially on mobile devices or slow networks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scalability:&lt;/strong&gt; As your app grows, lazy loading keeps it maintainable and modular, making it easier to manage and extend.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Svelte 4 Syntax
&lt;/h2&gt;

&lt;p&gt;In Svelte 4, dynamic imports are combined with the &lt;code&gt;&amp;lt;svelte:component&amp;gt;&lt;/code&gt; tag for lazy loading:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
{#await import('./MyWidget.svelte') then Widget}
  &amp;lt;svelte:component this={Widget.default} someProp={value} /&amp;gt;
{/await}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;&amp;lt;svelte:component&amp;gt;&lt;/code&gt; allows you to render a component dynamically.&lt;/li&gt;
&lt;li&gt;Props are passed as usual.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Svelte 5 (Runes) Syntax
&lt;/h2&gt;

&lt;p&gt;Svelte 5 introduces runes and direct component invocation, but for lazy loading, the recommended approach is to use the imported component as a tag inside an &lt;code&gt;{#await}&lt;/code&gt; block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
{#await import('./MyWidget.svelte') then Widget}
  &amp;lt;Widget.default someProp={value} /&amp;gt;
{/await}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;This is the idiomatic way for lazy loading in Svelte 5.&lt;/li&gt;
&lt;li&gt;Direct invocation (&lt;code&gt;Widget.default({ someProp: value })&lt;/code&gt;) is for advanced use cases, not markup.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Dynamic Import in a Loop
&lt;/h2&gt;

&lt;p&gt;You can dynamically load a component inside an &lt;code&gt;{#each}&lt;/code&gt; loop, which is useful for rendering lists of widgets or cards:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
{#each items as item}
  {#await import('./MyWidget.svelte') then Widget}
    &amp;lt;Widget.default data={item} /&amp;gt;
  {/await}
{/each}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Performance Note:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Svelte will only import the module once, even if the loop has many items. Subsequent imports use the cached module, so there is no repeated network or disk load.&lt;/li&gt;
&lt;li&gt;The only overhead is rendering multiple component instances, which is normal for any list rendering.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Drawbacks and Considerations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Initial Delay:&lt;/strong&gt; The first time a component is loaded, there may be a slight delay as the code is fetched and parsed. For critical UI, consider preloading or keeping it in the main bundle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SSR Compatibility:&lt;/strong&gt; Lazy loading is primarily a client-side optimization. If you use server-side rendering, ensure your approach is compatible or fallback gracefully.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code Splitting:&lt;/strong&gt; Too many small chunks can increase HTTP requests. Group related components when possible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State Management:&lt;/strong&gt; If your lazy-loaded component depends on global state, ensure the state is available when the component mounts.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Lazy loading components in Svelte is a simple yet effective way to optimize your app’s performance and scalability. By leveraging dynamic imports and Svelte’s template syntax, you can keep your initial bundle small and load features only when needed. Both Svelte 4 and Svelte 5 support this pattern, with Svelte 5 offering a more streamlined syntax.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;References:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://svelte.dev/docs/svelte/typescript#The-Component-type" rel="noopener noreferrer"&gt;Svelte Docs: Dynamic Components&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://svelte.dev/docs/svelte/v5-migration-guide#svelte:component-is-no-longer-necessary" rel="noopener noreferrer"&gt;Svelte 5 Migration Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/sveltejs/rfcs/pull/53" rel="noopener noreferrer"&gt;Svelte RFC: Runes&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The post &lt;a href="https://www.richardfu.net/efficient-lazy-loading-in-svelte-a-practical-guide-for-svelte-4-and-svelte-5-runes/" rel="noopener noreferrer"&gt;Efficient Lazy Loading in Svelte: A Practical Guide for Svelte 4 and Svelte 5 (Runes)&lt;/a&gt; appeared first on &lt;a href="https://www.richardfu.net" rel="noopener noreferrer"&gt;Richard Fu&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>svelte</category>
      <category>webdev</category>
      <category>lazyloading</category>
      <category>dynamicimports</category>
    </item>
    <item>
      <title>Keeping iframes Running When Hidden: A Journey Through Browser Optimizations</title>
      <dc:creator>Richard Fu</dc:creator>
      <pubDate>Fri, 20 Jun 2025 03:49:18 +0000</pubDate>
      <link>https://forem.com/raw-fun-gaming/keeping-iframes-running-when-hidden-a-journey-through-browser-optimizations-3c1k</link>
      <guid>https://forem.com/raw-fun-gaming/keeping-iframes-running-when-hidden-a-journey-through-browser-optimizations-3c1k</guid>
      <description>&lt;h2&gt;
  
  
  The Challenge
&lt;/h2&gt;

&lt;p&gt;Have you ever needed to keep an iframe running even when it’s “hidden” from view? This seemingly simple requirement can become surprisingly complex due to browser optimizations. In our case, we needed to maintain active game states in minimized casino widgets without affecting performance or user experience.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9n92czfxnlw8qqpatmzi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9n92czfxnlw8qqpatmzi.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Failed Approaches
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. CSS Visibility and Display Properties
&lt;/h3&gt;

&lt;p&gt;Our first attempt used basic CSS properties to hide the iframes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
.minimized-iframe {
  visibility: hidden;
  display: none;
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This immediately failed as browsers aggressively optimize hidden content, often suspending JavaScript execution and resource loading in hidden iframes.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Opacity and Pointer Events
&lt;/h3&gt;

&lt;p&gt;Next, we tried making the iframe invisible while keeping it in the DOM:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
.minimized-iframe {
  opacity: 0;
  pointer-events: none;
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While this seemed promising, browsers still optimized away the “invisible” content, causing our iframe content to freeze.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. CSS Clip and Transform
&lt;/h3&gt;

&lt;p&gt;We then attempted to keep a tiny portion visible using clipping and scaling:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
.minimized-iframe {
  clip-path: polygon(0 0, 1px 0, 1px 1px, 0 1px);
  transform: scale(0.01);
  position: fixed;
  top: 0;
  left: 0;
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This worked inconsistently across browsers, and the transform sometimes triggered the same optimization behaviors we were trying to avoid.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Working Solution: Layer Management
&lt;/h2&gt;

&lt;p&gt;The solution that finally worked was counterintuitive: instead of trying to hide the iframe, we kept it fully rendered but visually concealed behind a background-matching overlay.&lt;/p&gt;

&lt;p&gt;Here’s a simplified example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
&amp;lt;div class="background-overlay"&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;div class="iframe-container minimized"&amp;gt;
  &amp;lt;iframe src="game.html"&amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;



.iframe-container.minimized {
  position: fixed;
  z-index: -999; /* Behind overlay, but above page background */
  /* Center in viewport */
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}

.background-overlay {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: -998; /* Above iframe, below regular content */
  background-color: var(--page-background-color);
  pointer-events: none;
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why This Works
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;No Browser Optimization Triggers&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;The iframe remains fully rendered and active&lt;/li&gt;
&lt;li&gt;No visibility, opacity, or display properties that might trigger browser optimizations&lt;/li&gt;
&lt;li&gt;Transform is only used for positioning, not scaling&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Visual Concealment&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;The overlay matches the page background color&lt;/li&gt;
&lt;li&gt;Positioned between the iframe and regular content&lt;/li&gt;
&lt;li&gt;Pointer events disabled to prevent interaction&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance Considerations&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Minimal DOM manipulation&lt;/li&gt;
&lt;li&gt;No constant state monitoring needed&lt;/li&gt;
&lt;li&gt;Hardware acceleration can still be utilized&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Key Learnings
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Browser optimizations, while generally beneficial, can work against specific use cases&lt;/li&gt;
&lt;li&gt;Visual hiding ≠ functional hiding&lt;/li&gt;
&lt;li&gt;Sometimes the best solution isn’t about hiding content but managing how it’s layered&lt;/li&gt;
&lt;li&gt;Working with browser behaviors rather than against them leads to more robust solutions&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Caveats and Considerations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Ensure your z-index management is consistent across the application&lt;/li&gt;
&lt;li&gt;Consider memory usage since content remains fully rendered&lt;/li&gt;
&lt;li&gt;Test across different browsers and devices&lt;/li&gt;
&lt;li&gt;Consider fallback strategies for older browsers&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;This solution might seem counterintuitive – after all, we’re not really “hiding” the content, just putting it behind something else. However, it proves to be more reliable than fighting browser optimizations. Sometimes, the best solution isn’t about using more sophisticated techniques, but rather understanding and working with browser behaviors.&lt;/p&gt;

&lt;p&gt;Remember: browsers are optimized for common use cases. When you need to handle edge cases, you might need to think outside the box – or in this case, think in layers rather than visibility states.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This solution was discovered while working on a complex web application that required maintaining active states in minimized iframes. Special thanks to browser optimization algorithms for making this problem interesting enough to write about!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The post &lt;a href="https://www.richardfu.net/keeping-iframes-running-when-hidden-a-journey-through-browser-optimizations/" rel="noopener noreferrer"&gt;Keeping iframes Running When Hidden: A Journey Through Browser Optimizations&lt;/a&gt; appeared first on &lt;a href="https://www.richardfu.net" rel="noopener noreferrer"&gt;Richard Fu&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>css</category>
      <category>typescript</category>
      <category>webdev</category>
      <category>iframe</category>
    </item>
    <item>
      <title>Optimizing Rendering with PixiJS v8: A Deep Dive into the New Culling API</title>
      <dc:creator>Richard Fu</dc:creator>
      <pubDate>Thu, 24 Apr 2025 11:10:16 +0000</pubDate>
      <link>https://forem.com/raw-fun-gaming/optimizing-rendering-with-pixijs-v8-a-deep-dive-into-the-new-culling-api-e52</link>
      <guid>https://forem.com/raw-fun-gaming/optimizing-rendering-with-pixijs-v8-a-deep-dive-into-the-new-culling-api-e52</guid>
      <description>&lt;p&gt;The culling feature has been available as an extension in earlier versions of PixiJS, but with the release of PixiJS v8, it is now officially integrated into the core. This motivated us to revisit the feature and explore how to take advantage of it. However, the current documentation and examples are still limited. In this article, I’ll walk through how culling works in PixiJS v8, highlight some nuances, and share my personal insights on its usage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding the Intuition
&lt;/h2&gt;

&lt;p&gt;In our project, we often render a long list of UI items, which are masked with a rectangular shape. Performance tends to degrade when rendering 500+ items, especially on lower-end devices. Currently, we manually set &lt;code&gt;.visible = false&lt;/code&gt; for any object outside the visible screen. We wanted to explore if culling could help us automate this process efficiently.&lt;/p&gt;

&lt;h3&gt;
  
  
  First Look at PixiJS v8 Culling
&lt;/h3&gt;

&lt;p&gt;Both the &lt;a href="https://pixijs.com/8.x/guides/migrations/v8" rel="noopener noreferrer"&gt;v8 Migration Guide&lt;/a&gt; and &lt;a href="https://pixijs.com/8.x/guides/production/performance-tips" rel="noopener noreferrer"&gt;Performance Tips&lt;/a&gt; mention the new culling API, but they lack detailed usage examples. Most tutorials online still reference older versions. Fortunately, the &lt;a href="https://github.com/pixijs/pixijs/blob/main/src/culling/%20__tests__%20/Culler.test.ts" rel="noopener noreferrer"&gt;Culler test script&lt;/a&gt; proved to be an invaluable resource in understanding how the feature is intended to work.&lt;/p&gt;

&lt;p&gt;From the migration guide, here’s a basic usage example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
const container = new Container();
const view = new Rectangle(0, 0, 800, 600);

container.cullable = true;
container.cullArea = new Rectangle(0, 0, 400, 400);
container.cullableChildren = false;

app.stage.addChild(container);

Culler.shared.cull(container, view);

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Defining the Cull View
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The view&lt;/strong&gt; should be defined as a &lt;code&gt;Rectangle&lt;/code&gt; using global coordinates relative to the canvas.&lt;/li&gt;
&lt;li&gt;If the canvas size is dynamic (e.g., responsive layout), you should update the &lt;code&gt;view&lt;/code&gt; accordingly and invoke &lt;code&gt;Culler.shared.cull()&lt;/code&gt; again. For example:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
let timeSinceLoad = 0;
app.ticker.add(function (ticker) {
  timeSinceLoad += ticker.deltaMS;
  view = new Rectangle(
    app.renderer.width / 2 - 100,
    app.renderer.height / 2 - 100,
    200 + timeSinceLoad * 0.1,
    200 + timeSinceLoad * 0.1
  );
  Culler.shared.cull(container, view, false);
});

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;In most cases, you can use &lt;code&gt;app.stage&lt;/code&gt; and &lt;code&gt;app.screen&lt;/code&gt; directly as inputs for culling. This works well in a game loop or render tick:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
app.ticker.add(() =&amp;gt; {
  Culler.shared.cull(app.stage, app.screen);
});

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Understanding Cull Area
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;cullArea&lt;/code&gt; property can be confusing. It’s critical to note that this rectangle is defined in global coordinates, and does not inherit transformations like position or rotation from the container it’s attached to. This can lead to unexpected behavior, as seen in one of the official test cases:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
const view = { x: 0, y: 0, width: 100, height: 100 };

it('cullable container with cullArea should not be rendered if the bounds do not intersect the frame', () =&amp;gt;
{
    const container = new Container();
    const graphics = container.addChild(new Graphics().rect(0, 0, 10, 10).fill());

    container.cullable = true;
    container.cullArea = new Rectangle(-10, -10, 10, 10);
    container.x = container.y = 107.08;
    container.rotation = Math.PI / 4;

    Culler.shared.cull(container, view, false);

    expect(container.culled).toBe(true);
    expect(graphics.culled).toBe(false);
});

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even though the container and its child graphics are visually inside the view, the &lt;code&gt;cullArea&lt;/code&gt; (defined in global space) is outside the &lt;code&gt;view&lt;/code&gt;. As a result, the container is culled, but its graphics child is not.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Although &lt;code&gt;graphics.culled&lt;/code&gt; is &lt;code&gt;false&lt;/code&gt; here, it is not rendered because its parent &lt;code&gt;container&lt;/code&gt; has been culled.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In practice, I find it rare to define a custom &lt;code&gt;cullArea&lt;/code&gt; that doesn’t align with the container’s own bounds. However, there may be edge cases—like complex layering or shared masks—where this flexibility becomes useful.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;While the built-in culling system in PixiJS v8 simplifies object visibility in static or fixed-size canvases, it adds a bit of complexity when dealing with responsive layouts or dynamically changing viewports. You must ensure that the view and cull areas are accurately maintained in global space.&lt;/p&gt;

&lt;p&gt;In our case, sticking with &lt;a href="https://pixijs.com/8.x/guides/components/containers#masking" rel="noopener noreferrer"&gt;manual masking&lt;/a&gt; combined with setting &lt;code&gt;.visible = false&lt;/code&gt; based on Y-position checks remains the most straightforward and performant approach for large lists of items:&lt;br&gt;&lt;br&gt;
&lt;code&gt;if (y &amp;lt; -object.height / 2 || y &amp;gt; screen.height + object.height / 2) object.visible = false;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;That said, Pixi’s new culling API is a powerful addition—especially for dynamic scenes—and it’s worth exploring to see if it fits your rendering optimization needs.&lt;/p&gt;

&lt;p&gt;The post &lt;a href="https://www.richardfu.net/optimizing-rendering-with-pixijs-v8-a-deep-dive-into-the-new-culling-api/" rel="noopener noreferrer"&gt;Optimizing Rendering with PixiJS v8: A Deep Dive into the New Culling API&lt;/a&gt; appeared first on &lt;a href="https://www.richardfu.net" rel="noopener noreferrer"&gt;Richard Fu&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>pixijs</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Building a Slack Bot for Board Game Nights with TypeScript and GitHub Actions</title>
      <dc:creator>Richard Fu</dc:creator>
      <pubDate>Thu, 17 Apr 2025 15:50:29 +0000</pubDate>
      <link>https://forem.com/raw-fun-gaming/building-a-slack-bot-for-board-game-nights-with-typescript-and-github-actions-2a37</link>
      <guid>https://forem.com/raw-fun-gaming/building-a-slack-bot-for-board-game-nights-with-typescript-and-github-actions-2a37</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fprj4r84a3uxwypov4dx3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fprj4r84a3uxwypov4dx3.png" alt="Building a Slack Bot for Board Game Nights with TypeScript and GitHub Actions&amp;lt;br&amp;gt;
" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As a software engineer and board game enthusiast, I wanted to solve a common problem in our office: organizing our weekly board game nights. While we had a regular schedule (Friday afternoons), we needed a better way to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Remind people about the upcoming session&lt;/li&gt;
&lt;li&gt;Suggest games to play&lt;/li&gt;
&lt;li&gt;Get quick feedback on game preferences&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This led me to create a Slack bot that would handle these tasks automatically. In this post, I’ll share my journey of building this bot using TypeScript, Slack’s Web API, and GitHub Actions.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Initial Idea
&lt;/h2&gt;

&lt;p&gt;Every Friday at 4:30 PM, our team gathers for board games. We have quite a collection – from strategic games like Catan to party games like The Resistance. The challenge was getting everyone on the same page about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which games to bring&lt;/li&gt;
&lt;li&gt;Who’s interested in playing&lt;/li&gt;
&lt;li&gt;What time to meet&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I wanted a bot that would:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Send fun, casual reminders&lt;/li&gt;
&lt;li&gt;Randomly suggest a few games from our collection&lt;/li&gt;
&lt;li&gt;Let people vote on games using emoji reactions&lt;/li&gt;
&lt;li&gt;Run completely automatically&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Technical Stack
&lt;/h2&gt;

&lt;p&gt;After considering various options, I settled on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;TypeScript&lt;/strong&gt; : For type safety and better development experience&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Slack Web API&lt;/strong&gt; : For sending messages and handling reactions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Actions&lt;/strong&gt; : For scheduling and running the bot&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node.js&lt;/strong&gt; : For the runtime environment&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Key Features
&lt;/h2&gt;
&lt;h3&gt;
  
  
  1. Dynamic Message Generation
&lt;/h3&gt;

&lt;p&gt;Instead of sending the same boring message every week, I created a pool of casual, fun messages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
{
  "messages": [
    "Heads up, nerds! It's board game Friday tomorrow 🃏 – 4:30pm at L9!",
    "Warning: Friday fun incoming! Join us 4:30pm tomorrow at L9 🎲",
    "Don't make weekend plans – we roll dice tomorrow, 4:30pm L9 😉"
  ]
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Game Configuration
&lt;/h3&gt;

&lt;p&gt;Each game is configured with its name, Slack emoji name, and player count requirements:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
{
  "games": [
    {
      "name": "Catan",
      "emoji": "rice", // Slack emoji name for reactions
      "emojiUnicode": "🌾", // Unicode emoji for reference
      "minPlayers": 3,
      "maxPlayers": 4
    },
    {
      "name": "Carcassonne",
      "emoji": "european_castle", // Slack emoji name for reactions
      "emojiUnicode": "🏰", // Unicode emoji for reference
      "minPlayers": 2,
      "maxPlayers": 5
    }
  ]
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt; : For emojis, we use Slack emoji names (without colons) instead of Unicode emojis. This ensures compatibility with Slack’s reaction system. For example, use “rice” instead of 🌾, “european_castle” instead of 🏰. You can find available emoji names in your Slack workspace by typing &lt;code&gt;:&lt;/code&gt; in the message input. The &lt;code&gt;emojiUnicode&lt;/code&gt; field is included for reference only and is not used by the bot.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  3. Emoji Reactions
&lt;/h3&gt;

&lt;p&gt;The bot automatically adds emoji reactions for each suggested game, making it easy for people to indicate their preferences:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmz0g7ghukeyp0aovga2m.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmz0g7ghukeyp0aovga2m.jpg" width="800" height="153"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Development Process
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Setting Up the Project
&lt;/h3&gt;

&lt;p&gt;I started with a basic Node.js project and added TypeScript:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
npm init
npm install typescript @types/node --save-dev
npm install @slack/web-api dotenv

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Configuring Slack
&lt;/h3&gt;

&lt;p&gt;The bot requires several Slack permissions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;chat:write&lt;/code&gt;: For sending messages&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;channels:read&lt;/code&gt;: For accessing channels&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;reactions:write&lt;/code&gt;: For adding emoji reactions&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Environment Configuration
&lt;/h3&gt;

&lt;p&gt;I separated the configuration into:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;.env&lt;/code&gt;: For sensitive data (Slack token)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;config.json&lt;/code&gt;: For game list and channel settings (production)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;config.test.json&lt;/code&gt;: For testing configuration (separate channel and settings)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This separation allows us to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keep sensitive data out of version control&lt;/li&gt;
&lt;li&gt;Test new features safely in a separate channel&lt;/li&gt;
&lt;li&gt;Easily switch between production and test environments&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Implementing the Core Logic
&lt;/h3&gt;

&lt;p&gt;The main functionality is in &lt;code&gt;src/board-game-reminder.ts&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Load configurations&lt;/li&gt;
&lt;li&gt;Pick random games&lt;/li&gt;
&lt;li&gt;Send message with game suggestions&lt;/li&gt;
&lt;li&gt;Add emoji reactions&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  5. Setting Up CI/CD
&lt;/h3&gt;

&lt;p&gt;I used GitHub Actions for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Continuous Integration (testing and building)&lt;/li&gt;
&lt;li&gt;Scheduled execution (weekly reminders)&lt;/li&gt;
&lt;li&gt;Automated deployments&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Testing and Iteration
&lt;/h2&gt;

&lt;p&gt;I created a separate test channel (&lt;code&gt;social-board-games-testing&lt;/code&gt;) to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Test message formatting&lt;/li&gt;
&lt;li&gt;Verify emoji reactions&lt;/li&gt;
&lt;li&gt;Check scheduling&lt;/li&gt;
&lt;li&gt;Debug any issues&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This proved invaluable for iterating on the bot’s functionality without spamming the main channel.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deployment and Automation
&lt;/h2&gt;

&lt;p&gt;The bot runs entirely on GitHub Actions, requiring:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A GitHub repository&lt;/li&gt;
&lt;li&gt;Slack Bot Token (stored in GitHub Secrets)&lt;/li&gt;
&lt;li&gt;Scheduled workflow (runs every Thursday)&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Results and Impact
&lt;/h2&gt;

&lt;p&gt;The bot has successfully:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Increased participation in game nights&lt;/li&gt;
&lt;li&gt;Made game selection more democratic&lt;/li&gt;
&lt;li&gt;Reduced coordination overhead&lt;/li&gt;
&lt;li&gt;Added a fun element to our weekly reminders&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Future Improvements
&lt;/h2&gt;

&lt;p&gt;Some ideas for future enhancements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;RSVP functionality&lt;/li&gt;
&lt;li&gt;Game history tracking&lt;/li&gt;
&lt;li&gt;Player count optimization&lt;/li&gt;
&lt;li&gt;Integration with game rules/tutorials&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try It Yourself
&lt;/h2&gt;

&lt;p&gt;The project is open source and available on GitHub. To set up your own instance:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Fork the repository: &lt;code&gt;git clone https://github.com/furic/board-game-slack-reminder.git&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Create a Slack app and get your bot token&lt;/li&gt;
&lt;li&gt;Configure your environment:

&lt;ul&gt;
&lt;li&gt;Add your Slack token&lt;/li&gt;
&lt;li&gt;Customize the game list&lt;/li&gt;
&lt;li&gt;Set your channel name&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Deploy and enjoy automated game night coordination!&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Development with Cursor AI
&lt;/h2&gt;

&lt;p&gt;One of the interesting aspects of this project was my experience using Cursor AI as a development partner. What impressed me most was how the AI assistant helped navigate through some tricky implementation details:&lt;/p&gt;

&lt;h3&gt;
  
  
  Intelligent API Understanding
&lt;/h3&gt;

&lt;p&gt;I was particularly surprised by how Cursor AI understood the Slack API requirements. When implementing emoji reactions, it correctly identified that we needed to use Slack’s emoji format (e.g., “rice” instead of “🌾”) rather than Unicode emojis. This saved me from potential runtime issues and demonstrated the AI’s practical knowledge of API specifications.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step-by-Step Problem Solving
&lt;/h3&gt;

&lt;p&gt;The development process with Cursor AI was methodical and educational:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Initial Implementation&lt;/strong&gt; : We started with Unicode emojis in the configuration files, which seemed logical at first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Problem Identification&lt;/strong&gt; : When testing the bot, we encountered &lt;code&gt;invalid_name&lt;/code&gt; errors with emoji reactions. Cursor AI quickly identified that Slack’s API requires emoji names rather than Unicode characters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Systematic Updates&lt;/strong&gt; : The AI guided me through updating multiple files:

&lt;ul&gt;
&lt;li&gt;Changed emoji format in &lt;code&gt;config.json&lt;/code&gt; and &lt;code&gt;config.test.json&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Updated documentation in README.md&lt;/li&gt;
&lt;li&gt;Added clear examples and explanations in this blog&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Testing and Verification&lt;/strong&gt; : After each change, the AI suggested running tests to verify the fixes, showing a practical understanding of development workflows.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Configuration Management
&lt;/h3&gt;

&lt;p&gt;The AI helped establish a robust configuration system:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
// Before: Using Unicode emojis
{
  "name": "Catan",
  "emoji": "🌾", // This caused issues with Slack's API
}

// After: Using Slack emoji names with Unicode reference
{
  "name": "Catan",
  "emoji": "rice", // This works correctly with Slack reactions
  "emojiUnicode": "🌾", // Kept for reference
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Documentation Focus
&lt;/h3&gt;

&lt;p&gt;What stood out was the AI’s insistence on maintaining clear documentation. It made sure to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Add explanatory notes about emoji usage&lt;/li&gt;
&lt;li&gt;Include practical examples&lt;/li&gt;
&lt;li&gt;Document the reasoning behind technical decisions&lt;/li&gt;
&lt;li&gt;Keep configuration files in sync&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This experience showed how AI can be a valuable development partner, not just for writing code but for maintaining best practices and documentation throughout the development process.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Building this bot was a fun way to solve a real problem while learning about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Slack’s API capabilities&lt;/li&gt;
&lt;li&gt;TypeScript development&lt;/li&gt;
&lt;li&gt;GitHub Actions automation&lt;/li&gt;
&lt;li&gt;Configuration management&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The source code is available on &lt;a href="https://github.com/furic/board-game-slack-reminder" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;, and I welcome contributions and suggestions for improvements!&lt;/p&gt;

&lt;p&gt;The post &lt;a href="https://www.richardfu.net/building-a-slack-bot-for-board-game-nights-with-typescript-and-github-actions/" rel="noopener noreferrer"&gt;Building a Slack Bot for Board Game Nights with TypeScript and GitHub Actions&lt;/a&gt; appeared first on &lt;a href="https://www.richardfu.net" rel="noopener noreferrer"&gt;Richard Fu&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>slackapp</category>
      <category>webdev</category>
      <category>aiassistant</category>
    </item>
    <item>
      <title>Flappy Spaceship – Exploring the Frontiers of Pixi-React v8 Beta</title>
      <dc:creator>Richard Fu</dc:creator>
      <pubDate>Tue, 01 Oct 2024 01:44:48 +0000</pubDate>
      <link>https://forem.com/raw-fun-gaming/flappy-spaceship-exploring-the-frontiers-of-pixi-react-v8-beta-4nae</link>
      <guid>https://forem.com/raw-fun-gaming/flappy-spaceship-exploring-the-frontiers-of-pixi-react-v8-beta-4nae</guid>
      <description>&lt;p&gt;I built a re-skinned version of the classic  &lt;strong&gt;Flappy Bird&lt;/strong&gt;  game called  &lt;strong&gt;Flappy Spaceship&lt;/strong&gt; , with a space theme, using  &lt;strong&gt;Pixi-React v8 beta&lt;/strong&gt;. The goal was to explore Pixi-React as a game engine without adding new game mechanics.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Fl&lt;/strong&gt; a &lt;strong&gt;ppy Spaceship&lt;/strong&gt; is a space-themed reimagining of the classic Flappy Bird game. While retaining the original game’s mechanics, this version gives it a fresh space adventure vibe. Additionally, it serves as a platform to experiment with &lt;a href="https://github.com/pixijs/pixi-react/tree/beta" rel="noopener noreferrer"&gt;Pixi-React v8 beta&lt;/a&gt;, which is still in development.&lt;/p&gt;

&lt;p&gt;This project was submitted to Dev.to’s &lt;a href="https://dev.to/challenges/webgame"&gt;Web Game Challenge&lt;/a&gt;, Build a Game: Alien Edition.&lt;/p&gt;

&lt;p&gt;Inspired by and translated from the original &lt;a href="https://github.com/hairyf/vue3-pixi-flappy-bird/tree/main" rel="noopener noreferrer"&gt;Flappy Bird Game (Vue3 and PixiJS)&lt;/a&gt;. I ported the codebase to React and leveraged the Pixi-React library to showcase its capabilities.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3zvb8hpsk57xt4qiawrf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3zvb8hpsk57xt4qiawrf.png" width="800" height="560"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Live Demo
&lt;/h2&gt;

&lt;p&gt;You can try it out here: &lt;a href="https://flappy-spaceship.vercel.app" rel="noopener noreferrer"&gt;Live Demo&lt;/a&gt;. Use mouse clicks or the spacebar to play.&lt;/p&gt;




&lt;h2&gt;
  
  
  Journey
&lt;/h2&gt;

&lt;p&gt;Here are some key things I learned and encountered while building this game:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Motivation:&lt;/strong&gt; In my day-to-day work, I use Pixi v7 with Svelte, but I was eager to explore Pixi v8 and Pixi-React, especially since Pixi-React is still in beta. This project was the perfect opportunity to dive into it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Setting up the project from scratch:&lt;/strong&gt;
&lt;code&gt;npx create-vite@latest --template react-ts flappy-spaceship&lt;/code&gt;
&lt;code&gt;cd flappy-spaceship&lt;/code&gt;
&lt;code&gt;npm i pixi.js@8 @pixi/react@beta&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Translation from Vue to React:&lt;/strong&gt; There’s a great Pixi Flappy Bird project written in Vue, which I’m not very familiar with. With help from ChatGPT, I translated the code into React. However, since Pixi-React is in beta, I encountered issues where ChatGPT didn’t have the latest info, requiring me to debug certain problems on my own.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Button interaction issue:&lt;/strong&gt; I wanted to use Pixi-UI’s &lt;a href="https://pixijs.io/ui/FancyButton.html" rel="noopener noreferrer"&gt;FancyButton&lt;/a&gt;, but found that the callback functionality doesn’t yet work in Pixi-React. I asked about this in a &lt;a href="https://github.com/pixijs/pixi-react/discussions/546" rel="noopener noreferrer"&gt;discussion thread&lt;/a&gt;, but haven’t received a reply yet. As a workaround, I used a full-screen click to start the game instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom background:&lt;/strong&gt; I created a simple space background inspired by Pixi’s &lt;a href="https://pixijs.com/8.x/examples/advanced/star-warp" rel="noopener noreferrer"&gt;Star Warp example&lt;/a&gt;, replacing the Sprite elements with basic Graphics for simplicity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bug encountered:&lt;/strong&gt; I ran into a bug while using Pixi-React’s &lt;code&gt;useAssets&lt;/code&gt; hook, where loading the same asset multiple times didn’t work. After the first time, the asset failed to load, meaning the rock piles after the first weren’t rendered. This issue seems to be tracked in &lt;a href="https://github.com/pixijs/pixi-react/issues/531" rel="noopener noreferrer"&gt;this bug report&lt;/a&gt;. I ended up using the depreciated &lt;code&gt;useAsset&lt;/code&gt; which is working perfectly fine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context API limitations:&lt;/strong&gt; Initially, I wanted to manage screen dimensions using React’s &lt;code&gt;createContext&lt;/code&gt;, but later discovered that this feature isn’t fully &lt;a href="https://github.com/pixijs/pixi-react/issues/476#issuecomment-1986879449" rel="noopener noreferrer"&gt;supported&lt;/a&gt; in Pixi-React. I may find alternative ways later to handle screen size changes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  TODO
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Add simple spaceship animation&lt;/li&gt;
&lt;li&gt;Incorporate sound effects and background music&lt;/li&gt;
&lt;li&gt;Implement high score saving and loading&lt;/li&gt;
&lt;li&gt;Add a wormhole mechanic to advance the player over a set distance&lt;/li&gt;
&lt;li&gt;Introduce floating quantum cores as an in-game currency&lt;/li&gt;
&lt;li&gt;Create a shop where players can buy new spaceship skins&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;While  &lt;strong&gt;Pixi-React&lt;/strong&gt;  is still in beta and presents some challenges with newer features, it offers significant potential. Some  &lt;strong&gt;Pixi&lt;/strong&gt;  packages, like  &lt;strong&gt;Pixi-UI&lt;/strong&gt; , may need updates to function properly in this environment. Hopefully, future releases of  &lt;strong&gt;Pixi-React&lt;/strong&gt;  will include more detailed documentation and address some of these early-stage issues.&lt;/p&gt;

&lt;p&gt;Feel free to explore the source code and license in the &lt;a href="https://github.com/furic/flappy-spaceship" rel="noopener noreferrer"&gt;GitHub repository&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The post &lt;a href="https://www.richardfu.net/flappy-spaceship-exploring-the-frontiers-of-pixi-react-v8-beta/" rel="noopener noreferrer"&gt;Flappy Spaceship – Exploring the Frontiers of Pixi-React v8 Beta&lt;/a&gt; appeared first on &lt;a href="https://www.richardfu.net" rel="noopener noreferrer"&gt;Richard Fu&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>pixijs</category>
      <category>webdev</category>
      <category>pixireact</category>
      <category>flippybird</category>
    </item>
    <item>
      <title>Pixi’s Asset Pack 1.0: A Step Forward for Asset Management</title>
      <dc:creator>Richard Fu</dc:creator>
      <pubDate>Mon, 30 Sep 2024 08:11:23 +0000</pubDate>
      <link>https://forem.com/raw-fun-gaming/pixis-asset-pack-10-a-step-forward-for-asset-management-5a7</link>
      <guid>https://forem.com/raw-fun-gaming/pixis-asset-pack-10-a-step-forward-for-asset-management-5a7</guid>
      <description>&lt;p&gt;Pixi’s &lt;a href="https://pixijs.io/assetpack/" rel="noopener noreferrer"&gt;Asset Pack&lt;/a&gt; has officially reached &lt;a href="https://pixijs.com/blog/assetpack-1.0.0" rel="noopener noreferrer"&gt;version 1.0&lt;/a&gt;, and after diving deep into it, I can confidently say that it shows a lot of promise for our projects. However, there are still some issues that need ironing out. In this post, I’ll walk you through how it works, the benefits it offers, and a few limitations we’ve encountered.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Use Pixi’s Asset Pack?
&lt;/h2&gt;

&lt;p&gt;Here are some key features that make Asset Pack a valuable addition to your development toolkit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://pixijs.io/assetpack/docs/guide/pipes/texture-packer/" rel="noopener noreferrer"&gt;&lt;strong&gt;Texture Sprite Packing&lt;/strong&gt;&lt;/a&gt;: No more manual work with Texture Packer.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://pixijs.io/assetpack/docs/guide/pipes/compress/" rel="noopener noreferrer"&gt;&lt;strong&gt;Automated Compression &amp;amp; File Formats&lt;/strong&gt;&lt;/a&gt;: Keeps a 100% source texture while automatically generating compressed Png and WebP versions.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://pixijs.io/assetpack/docs/guide/pipes/mipmap/" rel="noopener noreferrer"&gt;&lt;strong&gt;Mipmaps Generation&lt;/strong&gt;&lt;/a&gt;: Ensures devices with lower resolutions use smaller textures, improving performance and reducing game size.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://pixijs.io/assetpack/docs/guide/pipes/manifest/" rel="noopener noreferrer"&gt;&lt;strong&gt;Bundle Manifest Generation&lt;/strong&gt;&lt;/a&gt;: No more hard-coded asset lists.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://pixijs.io/assetpack/docs/guide/pipes/cache-buster/" rel="noopener noreferrer"&gt;&lt;strong&gt;Hash Versioning&lt;/strong&gt;&lt;/a&gt;: Ensures you always load the latest files.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Artist-Friendly Workflow&lt;/strong&gt; : Artists only need to manage raw assets, while the tool handles export automation.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Current Issues
&lt;/h2&gt;

&lt;p&gt;While Asset Pack is promising, there are a few issues we’ve encountered:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;del&gt;&lt;strong&gt;Spine Atlas Export&lt;/strong&gt; : Spine atlas is not correctly exported (submitted a &lt;a href="https://github.com/EsotericSoftware/spine-runtimes/pull/2619" rel="noopener noreferrer"&gt;PR&lt;/a&gt; for spine-runtime).&lt;/del&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bitmap Font Export&lt;/strong&gt; : Bitmap fonts aren’t exporting properly (submitted an &lt;a href="https://github.com/pixijs/assetpack/issues/88" rel="noopener noreferrer"&gt;issue&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sound Sprite Support&lt;/strong&gt; : Currently unsupported (submitted an &lt;a href="https://github.com/pixijs/assetpack/issues/83" rel="noopener noreferrer"&gt;issue&lt;/a&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How to Set Up Asset Pack
&lt;/h2&gt;

&lt;p&gt;Setting up Asset Pack is straightforward. Begin by installing it with this &lt;a href="https://pixijs.io/assetpack/docs/guide/getting-started/installation/" rel="noopener noreferrer"&gt;commend&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
npm install --save-dev @assetpack/core

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, create an &lt;code&gt;.assetpack.js&lt;/code&gt; file with the following content:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
import { pixiPipes } from '@assetpack/core/pixi';

export default {
    entry: './raw-assets',
    output: './public/assets',
    pipes: [
        ...pixiPipes({
            resolutions: { high: 3, default: 2, low: 1 },
            manifest: {
                trimExtensions: true
            },
            texturePacker: { texturePacker: { removeFileExtension: true }},
        }),
    ],
};

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;pixiPipes&lt;/code&gt; function creates a default configuration that works in most cases. Here’s a breakdown of what my preferred settings do:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;resolutions&lt;/code&gt;: Specifies three resolutions (high, default, low). I will explain more about this later.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;manifest:trimExtensions&lt;/code&gt;: Removes texture file extensions in the manifest for easier reference in your code.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;texturePacker:texturePacker:trimExtensions&lt;/code&gt;: Removes texture file extensions in the spritesheet Json when packing textures, again for easier reference.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next, create a folder called &lt;code&gt;raw-assets&lt;/code&gt; in your project root. Each folder inside &lt;code&gt;raw-assets&lt;/code&gt; will represent a bundle, and you can specify it with a &lt;code&gt;{m}&lt;/code&gt; tag. For our project, I created the following bundles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;game-screen{m}&lt;/code&gt;: Contains all game assets and starts loading after the loading screen is shown.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;load-screen{m}&lt;/code&gt;: Assets used in the loading screen. These are unloaded once the screen is hidden to save memory.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;shared{m}&lt;/code&gt;: Assets used by both the loading screen and the game.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ui{m}&lt;/code&gt;&lt;/strong&gt; : UI assets, separated for easier management (though they can be put in the game-screen folder).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2ckhskirbczzipjsnqkn.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2ckhskirbczzipjsnqkn.jpg" width="314" height="224"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Based on asset type, structure the folders like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Single Sprite&lt;/strong&gt; : Place textures in any file path. Reference them by filename (without extension), e.g., &lt;code&gt;background&lt;/code&gt;. If there are two files with the same name, reference them with the relative path.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz7yvfw0p23tekue911c9.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz7yvfw0p23tekue911c9.jpg" width="578" height="472"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sprite Sheet (Texture Packer)&lt;/strong&gt;: Put textures in a folder with a &lt;code&gt;{tps}&lt;/code&gt; tag. Sprite sheet Json files will be generated, and you can access the texture array by folder name, e.g., &lt;code&gt;skins&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F92gqzh12gpsxsa2nz9xw.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F92gqzh12gpsxsa2nz9xw.jpg" width="524" height="806"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Spine&lt;/strong&gt; : Place the Atlas, Json, and texture files in a folder. Access the spine data and atlas with their filenames (without extension). If your Atlas and Json share the same filename, you’ll need to use extensions, e.g., &lt;code&gt;character.json&lt;/code&gt; and &lt;code&gt;character.atlas&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx95qeja4s5pgjyr3nvdl.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx95qeja4s5pgjyr3nvdl.jpg" width="600" height="890"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Finally, run the CLI command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
npm assetpack

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This packs everything into the &lt;code&gt;public&lt;/code&gt; folder and generates a &lt;code&gt;manifest.json&lt;/code&gt; containing all file references. You can also set up a GitHub workflow to automate this process.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Use Packed Assets
&lt;/h2&gt;

&lt;p&gt;Now, let’s look at the code to load the packed assets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
import { type AssetsManifest, Assets } from '@pixi/assets';
import type { Spritesheet } from '@pixi/spritesheet';
import * as utils from '@pixi/utils';
import { Sprite } from '@pixi/sprite';
import { Spine } from '@esotericsoftware/spine-pixi';

(async () =&amp;gt; {
    const baseUrl = './public/assets';
    // const baseUrl = 'https://s3/my-bucket';

    const response = await fetch(baseUrl + '/manifest.json');
    const manifest = (await response.json()) as AssetsManifest;
    if (!manifest.bundles) {
        throw new Error('[Assets] Invalid assets manifest');
    }

    const resolution = Math.min(utils.isMobile.any ? window.devicePixelRatio : 3, 3);

    await Assets.init({
        basePath: baseUrl,
        manifest,
        texturePreference: { resolution: [resolution, 1], format: ['webp', 'png'] },
    });

    await Assets.loadBundle('shared');
    await Assets.loadBundle('load-screen');

    // load-screen assets loaded, show load screen

    await Assets.loadBundle('ui');
    await Assets.loadBundle('game-screen', onProgress); // onProgress to feed the progress bar

    // game assets loaded, wait for player to hide load screen

    Assets.unloadBundle('load-screen');

    const singleSprite = Sprite.from('background');

    const spriteSheet = Assets.cache.get&amp;lt;Spritesheet&amp;gt;('skins');
    const spriteSheetSprite = new Sprite(spriteSheet.textures['hat.png']);

    const spine = Spine.from('character-skel', 'character');
})();

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code loads assets using the manifest and adjusts resolution preferences based on device pixel density. Here are the key steps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;File Location&lt;/strong&gt; : Set &lt;code&gt;baseUrl&lt;/code&gt; as a local or server folder path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resolution Handling&lt;/strong&gt; : Set the preferred resolution (3x, 2x, 1x) based on &lt;code&gt;devicePixelRatio&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Asset Loading:&lt;/strong&gt;  Use &lt;code&gt;Assets.loadBundle()&lt;/code&gt; to load asset bundles defined in the manifest.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unload Bundles:&lt;/strong&gt;  Once the loading screen is no longer needed, unload its assets with &lt;code&gt;Assets.unloadBundle()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accessing Assets:&lt;/strong&gt;  Use &lt;code&gt;Sprite.from()&lt;/code&gt; for single sprites, &lt;code&gt;Assets.cache.get()&lt;/code&gt; for spritesheets, and &lt;code&gt;Spine.from()&lt;/code&gt; for Spine animations.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;While Pixi’s Asset Pack is still evolving, it offers a solid foundation for managing assets efficiently in Pixi-based projects. I believe it has the potential to become an essential tool for many developers. Has anyone else worked with it? I’d love to hear your thoughts and experiences!&lt;/p&gt;

&lt;p&gt;The post &lt;a href="https://www.richardfu.net/pixis-asset-pack-1-0-a-step-forward-for-asset-management/" rel="noopener noreferrer"&gt;Pixi’s Asset Pack 1.0: A Step Forward for Asset Management&lt;/a&gt; appeared first on &lt;a href="https://www.richardfu.net" rel="noopener noreferrer"&gt;RF Game Dev Blog&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>pixijs</category>
      <category>webdev</category>
      <category>pixi</category>
      <category>gamedev</category>
    </item>
  </channel>
</rss>
