[RELEASE] Chroma4j - Client-sided browser furni renderer

Quackster

Administrator
Staff member
Administrator
Messages
126
Reaction score
189

Chroma4j renders Habbo furni SWFs. The repository contains the original JVM renderer and a TeaVM/WebAssembly browser deployment that renders from SWF bytes entirely client-side.

This project is originally a port of this project which is written in C# and renders .png files of furniture .swf files.

Requirements

  • JDK 17 or newer.
  • Gradle 8.7 or newer, or use the checked-in Gradle wrapper.
  • Node.js for browser JavaScript syntax checks run by the Gradle verification task.
  • A modern browser with WebAssembly GC support.

The browser build fetches SWFs directly from the browser, so HTTP SWF URLs must allow CORS. There is no server-side fetch or rendering fallback in the WASM release.

Downloads

You can download prebuilt releases from the GitHub Releases page:

Download Chroma4j from GitHub Releases

Source code: https://github.com/Quackster/Chroma4j

The WASM release package includes the browser files needed to run the static demo locally or host it on a static web server.

Preview

d037fc41-6633-4736-8283-35b3ace100f7


Build The WASM Release

From the repository root:

.\gradlew.bat :chroma-wasm:clean :chroma-wasm:buildWasmGC

This compiles the TeaVM module and writes:
  • web/dist/wasm-gc/chroma-wasm.wasm
  • web/dist/wasm-gc/chroma-wasm.wasm-runtime.js

The static deployment directory contains:
  • web/dist/index.html
  • web/dist/main.js
  • web/dist/chroma4j.js
  • web/dist/styles.css
  • web/dist/bg.png

The ADD overlay test page lives in web/dist-add-overlay-test and imports the WASM release from web/dist.

web/dist is generated/served output and is ignored by Git.

Run The Static Demo

Serve web/dist with any static HTTP server:

(Requires Python for to test the demo, or use the Node.js
Code:
serve
instead)

Code:
python -m http.server 5177 --directory web/dist

Open:

Code:
http://localhost:5177

Paste a CORS-enabled furni SWF URL, choose render options, and click Render. The page fetches SWF bytes in the browser, passes them to the TeaVM parser, and exports PNG by default, animated GIF when gif: true is selected, or animated PNG when apng: true is selected.

To try the ADD overlay demonstration, serve the web directory and open:

Code:
python -m http.server 5178 --directory web

Code:
http://localhost:5178/dist-add-overlay-test/

JavaScript API

JavaScript:
import { loadChroma4j } from "./chroma4j.js";

const chroma = await loadChroma4j();
const result = await chroma.renderFromUrl("https://example.com/hof_furni/chair.swf", {
state: 0,
direction: 2,
color: 0,
crop: true,
canvas: "transparent"
});

document.body.append(result.canvas);
const png = await result.blob();

PNG is the default output. result.format is png, result.mime is image/png, result.canvas contains the rendered pixels, and result.blob() returns a browser Blob containing the PNG bytes.

To paint PNG output into an existing canvas:

JavaScript:
const canvas = document.querySelector("#preview");
const result = await chroma.renderFromUrl("https://example.com/hof_furni/chair.swf", {
state: 0,
direction: 2
}, canvas);

console.log(result.canvas === canvas); // true

To show the PNG in a normal <img>:

JavaScript:
const img = document.createElement("img");
img.src = await result.dataUrl();
document.body.append(img);

To render an animated GIF, pass gif: true:

JavaScript:
const result = await chroma.renderFromUrl("https://example.com/hof_furni/rare_dragonlamp.swf", {
state: 1,
direction: 4,
color: 0,
crop: true,
canvas: "transparent",
gif: true,
loop: true
});

console.log(result.format);     // "gif"
console.log(result.mime);       // "image/gif"
console.log(result.isAnimated); // true when the selected state has multiple frames

To render animated PNG instead, pass apng: true or format: "apng":

JavaScript:
const result = await chroma.renderFromUrl("https://example.com/hof_furni/rare_dragonlamp.swf", {
state: 1,
direction: 4,
apng: true,
loop: true
});

console.log(result.format); // "apng"
console.log(result.mime);   // "image/png"

Use an <img> for animated GIF or APNG playback:

JavaScript:
const img = document.createElement("img");
img.src = await result.dataUrl();
document.body.append(img);

Or use an object URL:

JavaScript:
const blob = await result.blob();
const img = document.createElement("img");
img.src = URL.createObjectURL(blob);
document.body.append(img);

result.canvas is still populated for animated results, but a canvas cannot play an animated GIF or APNG by itself. It is useful only as a first-frame/static preview. Use result.blob() or result.dataUrl() with an <img> when you want the animation.

You can also provide SWF bytes directly:

JavaScript:
const bytes = await file.arrayBuffer();
const result = await chroma.renderFromBytes(bytes, { sprite: "chair" });

Supported first-release options mirror the server endpoint where applicable:

  • small / s
  • state
  • direction / rotation
  • color / colour
  • crop
  • bg / background
  • shadow
  • canvas: a hex colour, transparent, or an HTTP/HTTPS image URL in the browser build. URL backgrounds must allow CORS pixel reads.
  • addMode: "overlay" by default. "overlay" uses Overlay glow, where ADD ink is returned as a separate layer and composited over the visible page background. "baked" uses Baked glow, where the renderer blends ADD into the output image. "none" disables ADD/glow layers completely and always uses the baked renderer path. separateAdd is still accepted as a low-level alias.
  • icon
  • gif: false by default for PNG output; true returns GIF bytes when the selected state has animation frames.
  • apng: false by default for PNG output; true returns APNG bytes. APNG is served as PNG-compatible image/png.
  • format: optional "png", "gif", or "apng" selector. apng wins if both animated formats are requested.
  • loop: true by default for animated output; set false to emit a non-looping GIF or APNG.

Render With The Java Library

Use chroma-lib directly when you want to render from a normal JVM application instead of the browser WASM build or Spring webapp. The main entry point is com.quackster.chroma.ChromaFurniture.

Java:
import com.quackster.chroma.ChromaFurniture;

import java.nio.file.Files;
import java.nio.file.Path;

public class RenderFurniture {
    public static void main(String[] args) throws Exception {
        ChromaFurniture furni = new ChromaFurniture(
            "swfs/hof_furni/chair.swf",
            false,         // small furni
            0,             // state
            2,             // direction
            -1,            // colour id, or -1 for default
            true,          // render shadows
            false,         // render bg.png background
            "transparent", // canvas colour
            true,          // crop image
            false          // render icon
        );

        furni.run();
        Files.write(Path.of("chair.png"), furni.createImage());
    }
}

run() parses the SWF and writes extracted assets under furni_export/<sprite>, so the process needs write access to the working directory. For animated output, call createGif(true) or createApng(true) after run() and write the returned bytes to a .gif or .png file.


Build The Spring Webapp

The existing server-side renderer can still be built with:

Code:
.\gradlew.bat :chroma-webapp:clean :chroma-webapp:bootJar

Run it with:

java -jar chroma-webapp\build\libs\chroma-webapp-1.0.0.jar

The server listens on port 5000 and expects SWFs under swfs/hof_furni. Use gif=true for GIF output, apng=true or format=apng for APNG output, and loop=false for a non-looping animation.
 
Back
Top Bottom