https://github.com/quackster/chroma4j
The repository contains the original JVM renderer and a TeaVM/WebAssembly browser deployment that renders from SWF bytes entirely client-side.
https://github.com/quackster/chroma4j
Last synced: 29 days ago
JSON representation
The repository contains the original JVM renderer and a TeaVM/WebAssembly browser deployment that renders from SWF bytes entirely client-side.
- Host: GitHub
- URL: https://github.com/quackster/chroma4j
- Owner: Quackster
- License: gpl-3.0
- Created: 2026-01-08T22:52:47.000Z (7 months ago)
- Default Branch: master
- Last Pushed: 2026-06-23T11:53:20.000Z (about 1 month ago)
- Last Synced: 2026-06-23T13:30:22.217Z (about 1 month ago)
- Language: Java
- Homepage:
- Size: 528 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE.txt
Awesome Lists containing this project
README
# Chroma4j
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.
## Requirements
- JDK 17 or newer.
- Gradle 8.7 or newer, or use the checked-in Gradle wrapper.
- Python 3 for serving `web/dist` locally.
- 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.
## Preview

## Build The WASM Release
From the repository root:
```powershell
.\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:
```powershell
python -m http.server 5177 --directory web/dist
```
Open:
```text
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:
```powershell
python -m http.server 5178 --directory web
```
```text
http://localhost:5178/dist-add-overlay-test/
```
## JavaScript API
```js
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:
```js
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 `
`:
```js
const img = document.createElement("img");
img.src = await result.dataUrl();
document.body.append(img);
```
To render an animated GIF, pass `gif: true`:
```js
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"`:
```js
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 `
` for animated GIF or APNG playback:
```js
const img = document.createElement("img");
img.src = await result.dataUrl();
document.body.append(img);
```
Or use an object URL:
```js
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 `
` when you want the animation.
You can also provide SWF bytes directly:
```js
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/`, 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:
```powershell
.\gradlew.bat :chroma-webapp:clean :chroma-webapp:bootJar
```
Run it with:
```powershell
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.