diff --git a/.classpath b/.classpath new file mode 100644 index 0000000..f7e4a1d --- /dev/null +++ b/.classpath @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b83d222 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/.project b/.project new file mode 100644 index 0000000..f48d4cf --- /dev/null +++ b/.project @@ -0,0 +1,23 @@ + + + Multiavatar + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + + diff --git a/.settings/org.eclipse.core.resources.prefs b/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 0000000..f9fe345 --- /dev/null +++ b/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,4 @@ +eclipse.preferences.version=1 +encoding//src/main/java=UTF-8 +encoding//src/test/java=UTF-8 +encoding/=UTF-8 diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000..2f5cc74 --- /dev/null +++ b/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,8 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled +org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning +org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=ignore +org.eclipse.jdt.core.compiler.release=disabled +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..afab8a3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,176 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Overview + +Multiavatar is a multicultural avatar generator that creates unique SVG avatars from text strings. It can generate 12,230,590,464 (48^6) unique avatars by combining different parts (environment, clothes, head, mouth, eyes, top) from 16 initial characters with 3 color themes each. + +**Available Implementations:** +- **JavaScript**: Single-file, dependency-free vanilla JavaScript implementation with embedded SHA-256 library +- **Java**: Maven-based library (Java 8+) with the same functionality + +## Commands + +### JavaScript Commands + +#### Build +```bash +npm run build +``` +Creates both CommonJS and ESM distributions in the `dist/` directory by: +- Creating necessary dist folders +- Adding a package.json with `"type": "commonjs"` to dist/commonjs +- Copying multiavatar.min.js and appending appropriate module exports + +### Java Commands + +#### Build +```bash +mvn clean compile +``` +Compiles the Java source files to `target/classes/`. + +#### Test +```bash +mvn test +``` +Runs all unit tests (11 test cases covering basic functionality, determinism, special characters, version forcing, etc.). + +#### Package +```bash +mvn package +``` +Creates a JAR file in `target/multiavatar-1.0.7.jar`. + +#### Install to Local Maven Repository +```bash +mvn install +``` +Installs the JAR to your local ~/.m2 repository for use in other projects. + +### Development Build (SVG → JS) +```bash +cd svg +php _build.php +``` +Rebuilds multiavatar.js from the SVG source files. Run this after editing `*_final.svg` files in the svg/ directory. This extracts SVG elements by class (clothes, mouth, eyes, top) from the 16 character files (00_final.svg through 15_final.svg) and injects them into multiavatar.js between the `inject_start` and `inject_end` markers. + +### Testing +Open `demo.html` in a browser to test avatar generation interactively. +Open `svg/index.html` to view all 48 initial unique avatars (16 characters × 3 themes). + +## Architecture + +### Core Algorithm + +The library converts any input string into a deterministic avatar using this flow: + +1. **Hash Generation**: Input string → SHA-256 hash → first 12 digits extracted +2. **Part Selection**: Each pair of digits (0-99) maps to one of 48 variants (0-47) for each of 6 parts +3. **Theme Assignment**: The 48 variants are distributed across 16 base characters × 3 themes (A/B/C): + - 0-15: Theme A + - 16-31: Theme B + - 32-47: Theme C +4. **Color Application**: Each theme defines color palettes for parts; colors replace `#(.*?);` placeholders in SVG strings +5. **SVG Assembly**: Final SVG is assembled in order: environment → head → clothes → top → eyes → mouth + +### Data Structure + +**multiavatar.js** contains: +- `themes` object: Maps character IDs (00-15) → themes (A/B/C) → parts → color arrays +- `sP` object (SVG Parts): Maps character IDs → parts → SVG string templates with color placeholders +- `multiavatar(string, sansEnv, ver)` function + +### The 16 Initial Characters + +Each numbered 00-15 with descriptive names: +00=Robo, 01=Girl, 02=Blonde, 03=Evilnormie, 04=Country, 05=Geeknot, 06=Asian, 07=Punk, 08=Afrohair, 09=Normie Female, 10=Older, 11=Firehair, 12=Blond, 13=Ateam, 14=Rasta, 15=Meta + +### SVG Source Files + +Source files in `svg/`: +- `00.svg` through `15.svg`: Working files for editing +- `00_final.svg` through `15_final.svg`: Optimized SVG files (saved from Inkscape 1.0 as "Optimized SVG") used by _build.php +- Each SVG uses CSS classes to identify parts: `class="clothes"`, `class="mouth"`, `class="eyes"`, `class="top"` +- Head and environment parts use special handling in the build script + +## Function Parameters + +```javascript +multiavatar(string, sansEnv, ver) +``` + +- `string` (required): Input text that determines the avatar +- `sansEnv` (optional): If true, returns avatar without the circular background (environment part) +- `ver` (optional): Object to force a specific character/theme, e.g., `{part: '01', theme: 'A'}` + +## TypeScript Support + +TypeScript type definitions are included: +- `dist/commonjs/index.d.ts` +- `dist/esm/index.d.ts` + +## Distribution + +The package supports both CommonJS and ESM: +- CommonJS: `require('@multiavatar/multiavatar')` → dist/commonjs/index.js +- ESM: `import multiavatar from '@multiavatar/multiavatar/esm'` → dist/esm/index.js + +Both built files are generated from `multiavatar.min.js` with appropriate module syntax appended. + +## Design Workflow + +1. Edit SVG files in svg/ directory using Inkscape or similar vector editor +2. Keep designs in grayscale (colors are applied by the script) +3. Save edited files as `*_final.svg` (Inkscape: "Save As" → "Optimized SVG") +4. Run `php svg/_build.php` to extract SVG elements and update multiavatar.js +5. Update color themes in the `themes` object in multiavatar.js if needed +6. Test changes using demo.html + +## Java Implementation + +### Project Structure + +``` +src/main/java/com/multiavatar/ +├── Multiavatar.java - Main public API class +├── ThemeData.java - Color theme definitions (16 characters × 3 themes) +└── SvgData.java - SVG template strings for all character parts +``` + +### Usage Example + +```java +import com.multiavatar.Multiavatar; + +public class Example { + public static void main(String[] args) { + // Basic usage + String svg = Multiavatar.generate("Binx Bond"); + + // Without background circle + String svgNoBackground = Multiavatar.generate("Binx Bond", true); + + // Generate predefined avatar with specific character/theme + String svgPredefined = Multiavatar.generate(Multiavatar.CharacterType.GIRL, Multiavatar.CharacterTheme.A); + } +} +``` + +### Architecture Notes + +The Java implementation follows the same algorithm as the JavaScript version: +- **ThemeData.java**: Contains all 48 theme configurations (16 characters × 3 themes A/B/C) with color arrays for each part +- **SvgData.java**: Contains all 96 SVG template strings (16 characters × 6 parts) with color placeholders +- **Multiavatar.java**: Main class implementing the algorithm (hashing → part selection → theme assignment → color application → SVG assembly) + +The Java version uses Java's built-in `MessageDigest` for SHA-256 hashing (no external dependencies except JUnit for tests). + +## Important Notes + +- The JavaScript library is intentionally kept as a single file (multiavatar.js) with all SVG data and logic embedded +- SHA-256 library is included inline in JavaScript; Java uses built-in MessageDigest +- Not all 12 billion combinations are visually unique (some parts like eyes/mouth are similar) +- Parts can be hidden using `"none"` as a color value in theme definitions +- The Java implementation maintains full compatibility with the JavaScript version (same input produces same SVG output) diff --git a/README-JAVA.md b/README-JAVA.md new file mode 100644 index 0000000..5f0566d --- /dev/null +++ b/README-JAVA.md @@ -0,0 +1,244 @@ +# Multiavatar - Java Implementation + +[Multiavatar](https://multiavatar.com) is a multicultural avatar generator implemented in Java. + +This is a Maven-based Java port of the JavaScript Multiavatar library, maintaining full compatibility with the original implementation. + +## Features + +- Generate unique SVG avatars from any text string +- 12,230,590,464 possible unique avatars (48^6) +- Deterministic - same input always produces the same avatar +- No external dependencies (except JUnit for tests) +- Java 8+ compatible +- Thread-safe static methods + +## Installation + +### Maven + +Add the project as a dependency (after installing to local Maven repository): + +```bash +mvn install +``` + +Then add to your `pom.xml`: + +```xml + + com.multiavatar + multiavatar + 1.0.7 + +``` + +### Build from Source + +```bash +# Clone the repository +git clone https://github.com/multiavatar/Multiavatar.git +cd Multiavatar + +# Build +mvn clean compile + +# Run tests +mvn test + +# Package as JAR +mvn package +``` + +## Usage + +### Basic Usage + +```java +import com.multiavatar.Multiavatar; + +public class Example { + public static void main(String[] args) { + // Generate avatar SVG + String svgCode = Multiavatar.generate("Binx Bond"); + System.out.println(svgCode); + + // Save to file + try (FileWriter writer = new FileWriter("avatar.svg")) { + writer.write(svgCode); + } + } +} +``` + +### Advanced Usage + +```java +import com.multiavatar.Multiavatar; +import com.multiavatar.CharacterType; +import com.multiavatar.Theme; +import java.util.Random; + +public class AdvancedExample { + public static void main(String[] args) { + // Generate without background circle + String svgWithoutBackground = Multiavatar.generate("User Name", true); + + // Generate predefined avatar with specific character and theme + String svgPredefined = Multiavatar.generate(CharacterType.GIRL, Theme.A); + + // Generate random avatar + Random rnd = new Random(); + String randomAvatar = Multiavatar.generate(rnd); + + // Generate random avatar without background + String randomNoBackground = Multiavatar.generate(rnd, true); + + // Different inputs produce different avatars + String avatar1 = Multiavatar.generate("Alice"); + String avatar2 = Multiavatar.generate("Bob"); + // avatar1 != avatar2 + + // Same input always produces the same avatar + String avatarA = Multiavatar.generate("Same User"); + String avatarB = Multiavatar.generate("Same User"); + // avatarA.equals(avatarB) == true + } +} +``` + +## API Reference + +### `Multiavatar.generate(String id)` + +Generates an avatar SVG from the given identifier (deterministic). + +**Parameters:** +- `id` - The identifier to generate the avatar from (e.g., username, email) + +**Returns:** Complete SVG code as a String + +### `Multiavatar.generate(String id, boolean sansEnv)` + +Generates an avatar SVG from identifier with optional background. + +**Parameters:** +- `id` - The identifier to generate the avatar from +- `sansEnv` - If `true`, returns the avatar without the circular background + +**Returns:** Complete SVG code as a String + +### `Multiavatar.generate(CharacterType character, Theme theme)` + +Generates a predefined avatar SVG with specific character and theme. + +**Parameters:** +- `character` - The character type to use (e.g., `CharacterType.GIRL`, `CharacterType.ROBO`) +- `theme` - The theme to use (`Theme.A`, `Theme.B`, or `Theme.C`) + +**Returns:** Complete SVG code as a String + +### `Multiavatar.generate(CharacterType character, Theme theme, boolean sansEnv)` + +Generates a predefined avatar SVG with optional background. + +**Parameters:** +- `character` - The character type to use +- `theme` - The theme to use +- `sansEnv` - If `true`, returns the avatar without the circular background + +**Returns:** Complete SVG code as a String + +### `Multiavatar.generate(Random rnd)` + +Generates a random avatar SVG using the provided Random instance. + +**Parameters:** +- `rnd` - The Random instance to use for generating random parts + +**Returns:** Complete SVG code as a String + +### `Multiavatar.generate(Random rnd, boolean sansEnv)` + +Generates a random avatar SVG with optional background. + +**Parameters:** +- `rnd` - The Random instance to use for generating random parts +- `sansEnv` - If `true`, returns the avatar without the circular background + +**Returns:** Complete SVG code as a String + +## Character IDs + +The 16 base characters: +- 00: Robo +- 01: Girl +- 02: Blonde +- 03: Guy +- 04: Country +- 05: Geeknot +- 06: Asian +- 07: Punk +- 08: Afrohair +- 09: Normie Female +- 10: Older +- 11: Firehair +- 12: Blond +- 13: Ateam +- 14: Rasta +- 15: Street + +Each character has 3 color themes: A, B, and C. + +## Development + +### Running Tests + +```bash +mvn test +``` + +Test coverage includes: +- Basic SVG generation +- Deterministic output (same input → same output) +- Different inputs produce different outputs +- Null and empty string handling +- Special characters and Unicode support +- Version forcing +- Sans-environment mode + +### Project Structure + +``` +src/main/java/com/multiavatar/ +├── Multiavatar.java - Main public API +├── ThemeData.java - Color themes (48 configurations) +└── SvgData.java - SVG templates (96 parts) + +src/test/java/com/multiavatar/ +└── MultiavatarTest.java - Unit tests +``` + +## Algorithm + +1. **Hash**: Input string → SHA-256 hash → extract first 12 hex digits +2. **Part Selection**: Convert each pair of hex digits (0-99) to part number (0-47) +3. **Theme Assignment**: Map part numbers to character IDs (0-15) and themes (A/B/C) +4. **Color Application**: Replace color placeholders in SVG templates with theme colors +5. **SVG Assembly**: Combine parts in order: env → head → clothes → top → eyes → mouth + +## License + +See [LICENSE](LICENSE) file in the repository. + +## Credits + +- Original JavaScript implementation: [Multiavatar](https://github.com/multiavatar/Multiavatar) +- Author: Gie Katon +- Java port: Based on v1.0.7 of the JavaScript version + +## Links + +- Website: https://multiavatar.com +- JavaScript Repository: https://github.com/multiavatar/Multiavatar +- NPM Package: [@multiavatar/multiavatar](https://www.npmjs.com/package/@multiavatar/multiavatar) diff --git a/TEST-VECTORS.md b/TEST-VECTORS.md new file mode 100644 index 0000000..cb2d3de --- /dev/null +++ b/TEST-VECTORS.md @@ -0,0 +1,136 @@ +# Test Vectors + +This document explains how to generate and update test vectors for cross-platform compatibility testing. + +## Overview + +Test vectors are generated from the JavaScript implementation and used to verify that the Java implementation produces identical output. This ensures cross-platform compatibility between the JavaScript and Java versions of Multiavatar. + +## Test Vector Coverage + +The current test vectors (`test-vectors.json`) include: + +1. **Basic string-based generation**: Various input strings with different characteristics +2. **Sans environment mode**: Avatars without circular background +3. **Special characters**: Unicode, email addresses, special symbols +4. **All pure avatars**: All 16 character types × 3 themes × 2 background modes = 96 combinations +5. **Edge cases**: Empty strings, long strings, numbers + +**Total**: 132 test vectors + +## Generating Test Vectors + +### Prerequisites + +- Node.js installed +- The JavaScript implementation (`multiavatar.js`) must be present + +### Steps + +1. Modify `generate-test-vectors.cjs` if you want to add new test cases: + ```javascript + const testCases = [ + { input: "Your Input", sansEnv: false, version: null }, + // Add more test cases here + ]; + ``` + +2. Run the generation script: + ```bash + node generate-test-vectors.cjs + ``` + +3. The script will: + - Load the JavaScript implementation + - Generate SVG output for each test case + - Write results to `test-vectors.json` + - Display a summary of success/failure + +4. Verify the Java implementation matches: + ```bash + mvn test -Dtest=CrossPlatformCompatibilityTest + ``` + +## Test Case Format + +Each test vector in `test-vectors.json` has the following structure: + +```json +{ + "id": 0, + "input": "Binx Bond", + "sansEnv": false, + "version": null, + "output": " 30 ? '...' : ''}" -> ${svg.length} chars`); + } catch (error) { + console.error(`✗ Case ${i}: "${input}" failed:`, error.message); + results.push({ + id: i, + input: input, + sansEnv: sansEnv, + version: version, + error: error.message + }); + } +} + +// Write results to JSON file +const outputFile = 'test-vectors.json'; +fs.writeFileSync(outputFile, JSON.stringify(results, null, 2)); + +console.log(`\n✓ Test vectors written to ${outputFile}`); +console.log(` Total: ${results.length} test cases`); +console.log(` Success: ${results.filter(r => !r.error).length}`); +console.log(` Failed: ${results.filter(r => r.error).length}`); diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..6842dba --- /dev/null +++ b/pom.xml @@ -0,0 +1,86 @@ + + + 4.0.0 + + com.multiavatar + multiavatar + 1.0.7 + jar + + Multiavatar + Multicultural Avatar Generator + https://multiavatar.com + + + + Custom License + https://github.com/multiavatar/Multiavatar/blob/main/LICENSE + + + + + + Gie Katon + git@giekaton.com + http://giekaton.com/ + + + + + scm:git:https://github.com/multiavatar/Multiavatar.git + scm:git:https://github.com/multiavatar/Multiavatar.git + https://github.com/multiavatar/Multiavatar + + + + 8 + 8 + UTF-8 + + + + + junit + junit + 4.13.2 + test + + + + + + + ${basedir} + + LICENSE + + META-INF + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + 8 + 8 + + + + org.apache.maven.plugins + maven-jar-plugin + 3.3.0 + + + + true + + + + + + + diff --git a/src/main/java/com/multiavatar/Avatar.java b/src/main/java/com/multiavatar/Avatar.java new file mode 100644 index 0000000..0c8ed4e --- /dev/null +++ b/src/main/java/com/multiavatar/Avatar.java @@ -0,0 +1,175 @@ +package com.multiavatar; + +import java.security.MessageDigest; +import java.util.Random; + +/** + * The avatar configuration + */ +public class Avatar { + + private static final String SVG_START = ""; + private static final String SVG_END = ""; + private static final String STROKE = "stroke-linecap:round;stroke-linejoin:round;stroke-width:"; + + Coordinate env; + Coordinate clo; + Coordinate head; + Coordinate mouth; + Coordinate eyes; + Coordinate top; + + Coordinate getValue(AvatarPart part) { + switch (part) { + case ENV: return env; + case CLO: return clo; + case HEAD: return head; + case MOUTH: return mouth; + case EYES: return eyes; + case TOP: return top; + default: return null; + } + } + + /** + * Creates a pure avatar where all parts use the same character and theme. + * + * @param character The {@link CharacterType} to use for all parts + * @param theme The {@link Theme} to use for all parts + * @return A new {@link Avatar} with all parts using the same character and theme + */ + public static Avatar pure(CharacterType character, Theme theme) { + Avatar avatar = new Avatar(); + Coordinate coordinate = Coordinate.coordinate(character, theme); + avatar.env = coordinate; + avatar.clo = coordinate; + avatar.head = coordinate; + avatar.mouth = coordinate; + avatar.eyes = coordinate; + avatar.top = coordinate; + return avatar; + } + + /** + * Creates an avatar from an identifier string using SHA-256 hashing. + * + * @param id The identifier to generate the avatar from (e.g., username, email) + * @return A new {@link Avatar} generated deterministically from the id + */ + public static Avatar fromId(String id) { + // Get SHA-256 hash + String hash = Avatar.sha256(id); + + // Remove all non-digits from hash (JavaScript compatibility) + String hashDigitsOnly = hash.replaceAll("\\D", ""); + + // Extract first 12 digits + String hashString = hashDigitsOnly.substring(0, Math.min(12, hashDigitsOnly.length())); + + // Convert hash string to parts (6 parts, 2 digits each) + return Avatar.fromHash(hashString); + } + + /** + * Calculates SHA-256 hash of a string + */ + private static String sha256(String input) { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hash = digest.digest(input.getBytes("UTF-8")); + StringBuilder hexString = new StringBuilder(); + for (byte b : hash) { + String hex = Integer.toHexString(0xff & b); + if (hex.length() == 1) hexString.append('0'); + hexString.append(hex); + } + return hexString.toString(); + } catch (Exception e) { + throw new RuntimeException("Error calculating SHA-256", e); + } + } + + private static Avatar fromHash(String hashString) { + Avatar avatar = new Avatar(); + // Get parts (range 0-47) and convert to {@link Coordinate} (0-15 + theme) + avatar.env = Avatar.fromPartNumber(getPartNumber(hashString.substring(0, 2))); + avatar.clo = Avatar.fromPartNumber(getPartNumber(hashString.substring(2, 4))); + avatar.head = Avatar.fromPartNumber(getPartNumber(hashString.substring(4, 6))); + avatar.mouth = Avatar.fromPartNumber(getPartNumber(hashString.substring(6, 8))); + avatar.eyes = Avatar.fromPartNumber(getPartNumber(hashString.substring(8, 10))); + avatar.top = Avatar.fromPartNumber(getPartNumber(hashString.substring(10, 12))); + return avatar; + } + + /** + * Creates a random avatar using the provided {@link Random} instance. + * + * @param rnd The {@link Random} instance to use for generating random parts + * @return A new {@link Avatar} with randomly selected parts + */ + public static Avatar fromRandom(Random rnd) { + Avatar avatar = new Avatar(); + // Generate random part numbers (0-47) for each avatar part + avatar.env = Avatar.fromPartNumber(rnd.nextInt(48)); + avatar.clo = Avatar.fromPartNumber(rnd.nextInt(48)); + avatar.head = Avatar.fromPartNumber(rnd.nextInt(48)); + avatar.mouth = Avatar.fromPartNumber(rnd.nextInt(48)); + avatar.eyes = Avatar.fromPartNumber(rnd.nextInt(48)); + avatar.top = Avatar.fromPartNumber(rnd.nextInt(48)); + return avatar; + } + + /** + * Creates a {@link Coordinate} from a part number (0-47) + */ + private static Coordinate fromPartNumber(int nr) { + Theme theme; + + if (nr > 31) { + nr = nr - 32; + theme = Theme.C; + } else if (nr > 15) { + nr = nr - 16; + theme = Theme.B; + } else { + theme = Theme.A; + } + + CharacterType character = CharacterType.fromIndex(nr); + return Coordinate.coordinate(character, theme); + } + + /** + * Converts a 2-digit decimal string (0-99) to a part number (0-47) + */ + private static int getPartNumber(String digitPair) { + int value = Integer.parseInt(digitPair); + return Math.round((47f / 100f) * value); + } + + /** + * Renders this avatar to SVG format + * + * @param sansEnv If true, renders without the circular background + * @return The complete SVG code as a string + */ + public String render(boolean sansEnv) { + StringBuilder result = new StringBuilder(SVG_START); + + // Add generator attribution (fulfills license requirement) + result.append("Multiavatarhttps://multiavatar.com"); + + for (AvatarPart part : AvatarPart.values()) { + if (part == AvatarPart.ENV && sansEnv) { + continue; // Skip environment if sansEnv is true + } + + Coordinate coordinate = getValue(part); + coordinate.renderPart(result, part); + } + + result.append(SVG_END); + return result.toString(); + } + +} \ No newline at end of file diff --git a/src/main/java/com/multiavatar/AvatarPart.java b/src/main/java/com/multiavatar/AvatarPart.java new file mode 100644 index 0000000..b6a0f3d --- /dev/null +++ b/src/main/java/com/multiavatar/AvatarPart.java @@ -0,0 +1,25 @@ +package com.multiavatar; + +/** + * Avatar part names in the order they should be rendered. + * Each part represents a layer of the avatar SVG. + */ +public enum AvatarPart { + /** Environment - the circular background layer */ + ENV, + + /** Head - the face/head shape layer */ + HEAD, + + /** Clothes - the clothing/body layer */ + CLO, + + /** Top - the hair/headwear layer */ + TOP, + + /** Eyes - the eyes layer */ + EYES, + + /** Mouth - the mouth/expression layer */ + MOUTH; +} \ No newline at end of file diff --git a/src/main/java/com/multiavatar/CharacterType.java b/src/main/java/com/multiavatar/CharacterType.java new file mode 100644 index 0000000..7416a43 --- /dev/null +++ b/src/main/java/com/multiavatar/CharacterType.java @@ -0,0 +1,61 @@ +package com.multiavatar; + +/** + * Avatar character types (16 base characters, 00-15) + */ +public enum CharacterType { + ROBO("00", "Robo"), + GIRL("01", "Girl"), + BLONDE("02", "Blonde"), + GUY("03", "Guy"), + COUNTRY("04", "Country"), + GEEKNOT("05", "Geeknot"), + ASIAN("06", "Asian"), + PUNK("07", "Punk"), + AFROHAIR("08", "Afrohair"), + NORMIE_FEMALE("09", "Normie Female"), + OLDER("10", "Older"), + FIREHAIR("11", "Firehair"), + BLOND("12", "Blond"), + ATEAM("13", "Ateam"), + RASTA("14", "Rasta"), + STREET("15", "Street"); + + private final String id; + private final String displayName; + + CharacterType(String id, String displayName) { + this.id = id; + this.displayName = displayName; + } + + public String getId() { + return id; + } + + public String getDisplayName() { + return displayName; + } + + /** + * Get character by ID string (e.g., "00", "15") + */ + public static CharacterType fromId(String id) { + for (CharacterType character : values()) { + if (character.id.equals(id)) { + return character; + } + } + return null; + } + + /** + * Get character by numeric index (0-15) + */ + public static CharacterType fromIndex(int index) { + if (index < 0 || index >= values().length) { + return null; + } + return values()[index]; + } +} \ No newline at end of file diff --git a/src/main/java/com/multiavatar/Coordinate.java b/src/main/java/com/multiavatar/Coordinate.java new file mode 100644 index 0000000..a2052f9 --- /dev/null +++ b/src/main/java/com/multiavatar/Coordinate.java @@ -0,0 +1,50 @@ +package com.multiavatar; + +import com.multiavatar.SvgData.Template; + +/** + * Value class holding an avatar {@link CharacterType} and {@link Theme} together + */ +public class Coordinate { + public static Coordinate coordinate(CharacterType character, Theme theme) { + return new Coordinate(character, theme); + } + + final CharacterType character; + final Theme theme; + + private Coordinate(CharacterType character, Theme theme) { + this.character = character; + this.theme = theme; + } + + /** + * Produces the final SVG string for a part with colors applied from the {@link Theme} + * + * @param result The {@link StringBuilder} to append the SVG content to + * @param part The {@link AvatarPart} to render + */ + public void renderPart(StringBuilder result, AvatarPart part) { + if (character == null) { + return; + } + + // Get theme colors + ThemeData.CharacterThemes characterThemes = ThemeData.getCharacterThemes(character); + if (characterThemes == null) { + return; + } + + ThemeData.Colors themeData = characterThemes.getTheme(theme); + if (themeData == null) { + return; + } + + String[] colors = themeData.getColors(part); + + // Get SVG template + Template svgTemplate = SvgData.getSvgTemplate(character, part); + + svgTemplate.render(result, colors); + } +} \ No newline at end of file diff --git a/src/main/java/com/multiavatar/Multiavatar.java b/src/main/java/com/multiavatar/Multiavatar.java new file mode 100644 index 0000000..8fd3ee6 --- /dev/null +++ b/src/main/java/com/multiavatar/Multiavatar.java @@ -0,0 +1,135 @@ +package com.multiavatar; + +import java.util.Random; + +/** + * Multiavatar - Multicultural Avatar Generator + * + * Generates unique SVG avatars from text strings, predefined characters, or random generation. + * Can generate 12,230,590,464 unique avatars (48^6). + * + * Usage: + *
+ * // Generate from identifier (deterministic)
+ * String svgCode = Multiavatar.generate("Binx Bond");
+ * String svgWithoutBackground = Multiavatar.generate("Binx Bond", true);
+ *
+ * // Generate predefined avatar
+ * String predefined = Multiavatar.generate(CharacterType.GIRL, Theme.A);
+ *
+ * // Generate random avatar with seed (reproducible)
+ * String random = Multiavatar.generate(12345L);
+ *
+ * // Generate random avatar with Random instance
+ * Random rnd = new Random();
+ * String random2 = Multiavatar.generate(rnd);
+ * 
+ * + * @author Gie Katon + * @version 1.0.7 + */ +public class Multiavatar { + + /** + * Generates an avatar SVG from the given identifier. + * + * @param id The identifier to generate the avatar from (e.g., username, email) + * @return The complete SVG code as a string + */ + public static String generate(String id) { + return generate(id, false); + } + + /** + * Generates an avatar SVG from the given identifier. + * + * @param id The identifier to generate the avatar from (e.g., username, email) + * @param sansEnv If true, returns the avatar without the circular background + * @return The complete SVG code as a string + */ + public static String generate(String id, boolean sansEnv) { + if (id == null) { + id = ""; + } + + // Return empty string for empty input (JavaScript compatibility) + if (id.length() == 0) { + return ""; + } + + Avatar avatar = Avatar.fromId(id); + return avatar.render(sansEnv); + } + + /** + * Generates a predefined avatar SVG with specific character and theme. + * + * @param character The {@link CharacterType} to use (e.g., GIRL, ROBO) + * @param theme The {@link Theme} to use (A, B, or C) + * @return The complete SVG code as a string + */ + public static String generate(CharacterType character, Theme theme) { + return generate(character, theme, false); + } + + /** + * Generates a predefined avatar SVG with specific character and theme. + * + * @param character The {@link CharacterType} to use (e.g., GIRL, ROBO) + * @param theme The {@link Theme} to use (A, B, or C) + * @param sansEnv If true, returns the avatar without the circular background + * @return The complete SVG code as a string + */ + public static String generate(CharacterType character, Theme theme, boolean sansEnv) { + Avatar avatar = Avatar.pure(character, theme); + return avatar.render(sansEnv); + } + + /** + * Generates a random avatar SVG using the provided {@link Random} instance. + * + * @param rnd The {@link Random} instance to use for generating random parts + * @return The complete SVG code as a string + */ + public static String generate(Random rnd) { + return generate(rnd, false); + } + + /** + * Generates a random avatar SVG using the provided {@link Random} instance. + * + * @param rnd The {@link Random} instance to use for generating random parts + * @param sansEnv If true, returns the avatar without the circular background + * @return The complete SVG code as a string + */ + public static String generate(Random rnd, boolean sansEnv) { + Avatar avatar = Avatar.fromRandom(rnd); + return avatar.render(sansEnv); + } + + /** + * Generates a random avatar SVG using a seed value. + * Creates a {@link Random} instance with the provided seed for reproducible randomness. + * + * @param seed The seed value for the random number generator + * @return The complete SVG code as a string + */ + public static String generate(long seed) { + return generate(seed, false); + } + + /** + * Generates a random avatar SVG using a seed value. + * Creates a {@link Random} instance with the provided seed for reproducible randomness. + * + * @param seed The seed value for the random number generator + * @param sansEnv If true, returns the avatar without the circular background + * @return The complete SVG code as a string + */ + public static String generate(long seed, boolean sansEnv) { + Random rnd = new Random(seed); + Avatar avatar = Avatar.fromRandom(rnd); + return avatar.render(sansEnv); + } + +} diff --git a/src/main/java/com/multiavatar/SvgData.java b/src/main/java/com/multiavatar/SvgData.java new file mode 100644 index 0000000..f431251 --- /dev/null +++ b/src/main/java/com/multiavatar/SvgData.java @@ -0,0 +1,294 @@ +package com.multiavatar; + +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Contains all SVG template strings for the 16 base characters (00-15). + * Each character has 6 parts: env, clo, head, mouth, eyes, top. + * Color placeholders (#01;, #000;, etc.) are replaced with theme colors at runtime. + */ +class SvgData { + + private static final String ENV = ""; + + private static final String HEAD = ""; + + private static final Pattern PLACEHOLDER_PATTERN = Pattern.compile("#(?:[^;]*)(?=;)"); + + private static final EnumMap> SVG_PARTS = new EnumMap<>(CharacterType.class); + + static { + EnumMap charMap; + + // Character 00: Robo + charMap = new EnumMap<>(AvatarPart.class); + putTemplate(charMap, AvatarPart.ENV, ENV); + putTemplate(charMap, AvatarPart.CLO, ""); + putTemplate(charMap, AvatarPart.HEAD, HEAD); + putTemplate(charMap, AvatarPart.MOUTH, ""); + putTemplate(charMap, AvatarPart.EYES, ""); + putTemplate(charMap, AvatarPart.TOP, ""); + SVG_PARTS.put(CharacterType.ROBO, charMap); + + // Character 01: Girl + charMap = new EnumMap<>(AvatarPart.class); + putTemplate(charMap, AvatarPart.ENV, ENV); + putTemplate(charMap, AvatarPart.CLO, ""); + putTemplate(charMap, AvatarPart.HEAD, HEAD); + putTemplate(charMap, AvatarPart.MOUTH, ""); + putTemplate(charMap, AvatarPart.EYES, ""); + putTemplate(charMap, AvatarPart.TOP, ""); + SVG_PARTS.put(CharacterType.GIRL, charMap); + + // Character 02: Blonde + charMap = new EnumMap<>(AvatarPart.class); + putTemplate(charMap, AvatarPart.ENV, ENV); + putTemplate(charMap, AvatarPart.CLO, ""); + putTemplate(charMap, AvatarPart.HEAD, HEAD); + putTemplate(charMap, AvatarPart.MOUTH, ""); + putTemplate(charMap, AvatarPart.EYES, ""); + putTemplate(charMap, AvatarPart.TOP, ""); + SVG_PARTS.put(CharacterType.BLONDE, charMap); + + // Character 03: Guy + charMap = new EnumMap<>(AvatarPart.class); + putTemplate(charMap, AvatarPart.ENV, ENV); + putTemplate(charMap, AvatarPart.CLO, ""); + putTemplate(charMap, AvatarPart.HEAD, HEAD); + putTemplate(charMap, AvatarPart.MOUTH, ""); + putTemplate(charMap, AvatarPart.EYES, ""); + putTemplate(charMap, AvatarPart.TOP, ""); + SVG_PARTS.put(CharacterType.GUY, charMap); + + // Character 04: Country + charMap = new EnumMap<>(AvatarPart.class); + putTemplate(charMap, AvatarPart.ENV, ENV); + putTemplate(charMap, AvatarPart.CLO, ""); + putTemplate(charMap, AvatarPart.HEAD, HEAD); + putTemplate(charMap, AvatarPart.MOUTH, ""); + putTemplate(charMap, AvatarPart.EYES, ""); + putTemplate(charMap, AvatarPart.TOP, ""); + SVG_PARTS.put(CharacterType.COUNTRY, charMap); + + // Character 05: Geeknot + charMap = new EnumMap<>(AvatarPart.class); + putTemplate(charMap, AvatarPart.ENV, ENV); + putTemplate(charMap, AvatarPart.CLO, ""); + putTemplate(charMap, AvatarPart.HEAD, HEAD); + putTemplate(charMap, AvatarPart.MOUTH, ""); + putTemplate(charMap, AvatarPart.EYES, ""); + putTemplate(charMap, AvatarPart.TOP, ""); + SVG_PARTS.put(CharacterType.GEEKNOT, charMap); + + // Character 06: Asian + charMap = new EnumMap<>(AvatarPart.class); + putTemplate(charMap, AvatarPart.ENV, ENV); + putTemplate(charMap, AvatarPart.CLO, ""); + putTemplate(charMap, AvatarPart.HEAD, HEAD); + putTemplate(charMap, AvatarPart.MOUTH, ""); + putTemplate(charMap, AvatarPart.EYES, ""); + putTemplate(charMap, AvatarPart.TOP, ""); + SVG_PARTS.put(CharacterType.ASIAN, charMap); + + // Character 07: Punk + charMap = new EnumMap<>(AvatarPart.class); + putTemplate(charMap, AvatarPart.ENV, ENV); + putTemplate(charMap, AvatarPart.CLO, ""); + putTemplate(charMap, AvatarPart.HEAD, HEAD); + putTemplate(charMap, AvatarPart.MOUTH, ""); + putTemplate(charMap, AvatarPart.EYES, ""); + putTemplate(charMap, AvatarPart.TOP, ""); + SVG_PARTS.put(CharacterType.PUNK, charMap); + + // Character 08: Afrohair + charMap = new EnumMap<>(AvatarPart.class); + putTemplate(charMap, AvatarPart.ENV, ENV); + putTemplate(charMap, AvatarPart.CLO, ""); + putTemplate(charMap, AvatarPart.HEAD, HEAD); + putTemplate(charMap, AvatarPart.MOUTH, ""); + putTemplate(charMap, AvatarPart.EYES, ""); + putTemplate(charMap, AvatarPart.TOP, ""); + SVG_PARTS.put(CharacterType.AFROHAIR, charMap); + + // Character 09: Normie Female + charMap = new EnumMap<>(AvatarPart.class); + putTemplate(charMap, AvatarPart.ENV, ENV); + putTemplate(charMap, AvatarPart.CLO, ""); + putTemplate(charMap, AvatarPart.HEAD, HEAD); + putTemplate(charMap, AvatarPart.MOUTH, ""); + putTemplate(charMap, AvatarPart.EYES, ""); + putTemplate(charMap, AvatarPart.TOP, ""); + SVG_PARTS.put(CharacterType.NORMIE_FEMALE, charMap); + + // Character 10: Older + charMap = new EnumMap<>(AvatarPart.class); + putTemplate(charMap, AvatarPart.ENV, ENV); + putTemplate(charMap, AvatarPart.CLO, ""); + putTemplate(charMap, AvatarPart.HEAD, HEAD); + putTemplate(charMap, AvatarPart.MOUTH, ""); + putTemplate(charMap, AvatarPart.EYES, ""); + putTemplate(charMap, AvatarPart.TOP, ""); + SVG_PARTS.put(CharacterType.OLDER, charMap); + + // Character 11: Firehair + charMap = new EnumMap<>(AvatarPart.class); + putTemplate(charMap, AvatarPart.ENV, ENV); + putTemplate(charMap, AvatarPart.CLO, ""); + putTemplate(charMap, AvatarPart.HEAD, HEAD); + putTemplate(charMap, AvatarPart.MOUTH, ""); + putTemplate(charMap, AvatarPart.EYES, ""); + putTemplate(charMap, AvatarPart.TOP, ""); + SVG_PARTS.put(CharacterType.FIREHAIR, charMap); + + // Character 12: Blond + charMap = new EnumMap<>(AvatarPart.class); + putTemplate(charMap, AvatarPart.ENV, ENV); + putTemplate(charMap, AvatarPart.CLO, ""); + putTemplate(charMap, AvatarPart.HEAD, HEAD); + putTemplate(charMap, AvatarPart.MOUTH, ""); + putTemplate(charMap, AvatarPart.EYES, ""); + putTemplate(charMap, AvatarPart.TOP, ""); + SVG_PARTS.put(CharacterType.BLOND, charMap); + + // Character 13: Ateam + charMap = new EnumMap<>(AvatarPart.class); + putTemplate(charMap, AvatarPart.ENV, ENV); + putTemplate(charMap, AvatarPart.CLO, ""); + putTemplate(charMap, AvatarPart.HEAD, HEAD); + putTemplate(charMap, AvatarPart.MOUTH, ""); + putTemplate(charMap, AvatarPart.EYES, ""); + putTemplate(charMap, AvatarPart.TOP, ""); + SVG_PARTS.put(CharacterType.ATEAM, charMap); + + // Character 14: Rasta + charMap = new EnumMap<>(AvatarPart.class); + putTemplate(charMap, AvatarPart.ENV, ENV); + putTemplate(charMap, AvatarPart.CLO, ""); + putTemplate(charMap, AvatarPart.HEAD, HEAD); + putTemplate(charMap, AvatarPart.MOUTH, ""); + putTemplate(charMap, AvatarPart.EYES, ""); + putTemplate(charMap, AvatarPart.TOP, ""); + SVG_PARTS.put(CharacterType.RASTA, charMap); + + // Character 15: Street + charMap = new EnumMap<>(AvatarPart.class); + putTemplate(charMap, AvatarPart.ENV, ENV); + putTemplate(charMap, AvatarPart.CLO, ""); + putTemplate(charMap, AvatarPart.HEAD, HEAD); + putTemplate(charMap, AvatarPart.MOUTH, ""); + putTemplate(charMap, AvatarPart.EYES, ""); + putTemplate(charMap, AvatarPart.TOP, ""); + SVG_PARTS.put(CharacterType.STREET, charMap); + + } + + static Template getSvgTemplate(CharacterType character, AvatarPart part) { + EnumMap parts = SVG_PARTS.get(character); + return parts != null ? parts.getOrDefault(part, Template.EMPTY) : Template.EMPTY; + } + + private static void putTemplate(EnumMap charMap, AvatarPart part, String templateSource) { + charMap.put(part, new Template(templateSource)); + } + + static class Template { + + public static final Template EMPTY = new Template(""); + + private static abstract class Segment { + + public abstract String expand(String[] values); + + public abstract Object source(); + + } + + private static class Literal extends Segment { + + private String _svgFragment; + + public Literal(String svgFragment) { + _svgFragment = svgFragment; + } + + @Override + public String expand(String[] values) { + return _svgFragment; + } + + @Override + public Object source() { + return _svgFragment; + } + + } + + private static class Param extends Segment { + + private String _value; + private int _index; + + public Param(String value, int index) { + _value = value; + _index = index; + } + + @Override + public String expand(String[] values) { + return values[_index]; + } + + @Override + public Object source() { + return _value; + } + + } + + private List _segments = new ArrayList<>(); + + public Template(String source) { + Matcher matcher = PLACEHOLDER_PATTERN.matcher(source); + + int paramIndex = 0; + int start = 0; + while (matcher.find(start)) { + int stop = matcher.start(); + + if (stop > start) { + _segments.add(new Literal(source.substring(start, stop))); + } + + String value = matcher.group(); + Param param = new Param(value, paramIndex++); + + _segments.add(param); + + start = matcher.end(); + } + + if (start < source.length()) { + _segments.add(new Literal(source.substring(start))); + } + } + + public String toSource() { + StringBuilder result = new StringBuilder(); + for (Segment segment : _segments) { + result.append(segment.source()); + } + return result.toString(); + } + + public void render(StringBuilder result, String[] colors) { + for (Segment segment : _segments) { + result.append(segment.expand(colors)); + } + } + + } +} diff --git a/src/main/java/com/multiavatar/Theme.java b/src/main/java/com/multiavatar/Theme.java new file mode 100644 index 0000000..a03ce12 --- /dev/null +++ b/src/main/java/com/multiavatar/Theme.java @@ -0,0 +1,32 @@ +package com.multiavatar; + +/** + * Theme variants for each character + */ +public enum Theme { + A('A'), + B('B'), + C('C'); + + private final char code; + + Theme(char code) { + this.code = code; + } + + public char getCode() { + return code; + } + + /** + * Get theme by character code ('A', 'B', or 'C') + */ + public static Theme fromCode(char code) { + for (Theme theme : values()) { + if (theme.code == code) { + return theme; + } + } + return null; + } +} \ No newline at end of file diff --git a/src/main/java/com/multiavatar/ThemeData.java b/src/main/java/com/multiavatar/ThemeData.java new file mode 100644 index 0000000..21e9fbc --- /dev/null +++ b/src/main/java/com/multiavatar/ThemeData.java @@ -0,0 +1,519 @@ +package com.multiavatar; + +import java.util.EnumMap; + +/** + * Contains all theme color data for the 16 base characters. + * Each character has 3 themes (A, B, C) with colors for 6 parts: + * env, clo, head, mouth, eyes, top + */ +class ThemeData { + + static class Colors { + String[] env; + String[] clo; + String[] head; + String[] mouth; + String[] eyes; + String[] top; + + Colors(String[] env, String[] clo, String[] head, String[] mouth, String[] eyes, String[] top) { + this.env = env; + this.clo = clo; + this.head = head; + this.mouth = mouth; + this.eyes = eyes; + this.top = top; + } + + /** + * Gets the color array for a specific part from theme data + */ + public String[] getColors(AvatarPart avatarPart) { + switch (avatarPart) { + case ENV: return env; + case CLO: return clo; + case HEAD: return head; + case MOUTH: return mouth; + case EYES: return eyes; + case TOP: return top; + default: return new String[0]; + } + } + } + + static class CharacterThemes { + private final EnumMap themes; + + CharacterThemes(Colors A, Colors B, Colors C) { + this.themes = new EnumMap<>(Theme.class); + this.themes.put(Theme.A, A); + this.themes.put(Theme.B, B); + this.themes.put(Theme.C, C); + } + + Colors getTheme(Theme theme) { + return themes.get(theme); + } + } + + private static final EnumMap THEMES = createThemes(); + + static CharacterThemes getCharacterThemes(CharacterType character) { + return THEMES.get(character); + } + + private static EnumMap createThemes() { + EnumMap themes = new EnumMap<>(CharacterType.class); + + // Character 00 - Robo + themes.put(CharacterType.ROBO, new CharacterThemes( + new Colors( + new String[]{"#ff2f2b"}, + new String[]{"#fff", "#000"}, + new String[]{"#fff"}, + new String[]{"#fff", "#000", "#000"}, + new String[]{"#000", "none", "#00FFFF"}, + new String[]{"#fff", "#fff"} + ), + new Colors( + new String[]{"#ff1ec1"}, + new String[]{"#000", "#fff"}, + new String[]{"#ffc1c1"}, + new String[]{"#fff", "#000", "#000"}, + new String[]{"#FF2D00", "#fff", "none"}, + new String[]{"#a21d00", "#fff"} + ), + new Colors( + new String[]{"#0079b1"}, + new String[]{"#0e00b1", "#d1fffe"}, + new String[]{"#f5aa77"}, + new String[]{"#fff", "#000", "#000"}, + new String[]{"#0c00de", "#fff", "none"}, + new String[]{"#acfffd", "#acfffd"} + ) + )); + + // Character 01 - Girl + themes.put(CharacterType.GIRL, new CharacterThemes( + new Colors( + new String[]{"#a50000"}, + new String[]{"#f06", "#8e0039"}, + new String[]{"#85492C"}, + new String[]{"#000"}, + new String[]{"#000", "#ff9809"}, + new String[]{"#ff9809", "#ff9809", "none", "none"} + ), + new Colors( + new String[]{"#40E83B"}, + new String[]{"#00650b", "#62ce5a"}, + new String[]{"#f7c1a6"}, + new String[]{"#6e1c1c"}, + new String[]{"#000", "#ff833b"}, + new String[]{"#67FFCC", "none", "none", "#ecff3b"} + ), + new Colors( + new String[]{"#ff2c2c"}, + new String[]{"#fff", "#000"}, + new String[]{"#ffce8b"}, + new String[]{"#000"}, + new String[]{"#000", "#0072ff"}, + new String[]{"#ff9809", "none", "#ffc809", "none"} + ) + )); + + // Character 02 - Blonde + themes.put(CharacterType.BLONDE, new CharacterThemes( + new Colors( + new String[]{"#ff7520"}, + new String[]{"#d12823"}, + new String[]{"#fee3c5"}, + new String[]{"#d12823"}, + new String[]{"#000", "none"}, + new String[]{"#000", "none", "none", "#FFCC00", "red"} + ), + new Colors( + new String[]{"#ff9700"}, + new String[]{"#000"}, + new String[]{"#d2ad6d"}, + new String[]{"#000"}, + new String[]{"#000", "#00ffdc"}, + new String[]{"#fdff00", "#fdff00", "none", "none", "none"} + ), + new Colors( + new String[]{"#26a7ff"}, + new String[]{"#d85cd7"}, + new String[]{"#542e02"}, + new String[]{"#f70014"}, + new String[]{"#000", "magenta"}, + new String[]{"#FFCC00", "#FFCC00", "#FFCC00", "#ff0000", "yellow"} + ) + )); + + // Character 03 - Evilnormie + themes.put(CharacterType.GUY, new CharacterThemes( + new Colors( + new String[]{"#6FC30E"}, + new String[]{"#b4e1fa", "#5b5d6e", "#515262", "#a0d2f0", "#a0d2f0"}, + new String[]{"#fae3b9"}, + new String[]{"#fff", "#000"}, + new String[]{"#000"}, + new String[]{"#8eff45", "#8eff45", "none", "none"} + ), + new Colors( + new String[]{"#00a58c"}, + new String[]{"#000", "none", "none", "none", "none"}, + new String[]{"#FAD2B9"}, + new String[]{"#fff", "#000"}, + new String[]{"#000"}, + new String[]{"#FFC600", "none", "#FFC600", "none"} + ), + new Colors( + new String[]{"#ff501f"}, + new String[]{"#000", "#ff0000", "#ff0000", "#7d7d7d", "#7d7d7d"}, + new String[]{"#fff3dc"}, + new String[]{"#d2001b", "none"}, + new String[]{"#000"}, + new String[]{"#D2001B", "none", "none", "#D2001B"} + ) + )); + + // Character 04 - Country + themes.put(CharacterType.COUNTRY, new CharacterThemes( + new Colors( + new String[]{"#fc0"}, + new String[]{"#901e0e", "#ffbe1e", "#ffbe1e", "#c55f54"}, + new String[]{"#f8d9ad"}, + new String[]{"#000", "none", "#000", "none"}, + new String[]{"#000"}, + new String[]{"#583D00", "#AF892E", "#462D00", "#a0a0a0"} + ), + new Colors( + new String[]{"#386465"}, + new String[]{"#fff", "#333", "#333", "#333"}, + new String[]{"#FFD79D"}, + new String[]{"#000", "#000", "#000", "#000"}, + new String[]{"#000"}, + new String[]{"#27363C", "#5DCAD4", "#314652", "#333"} + ), + new Colors( + new String[]{"#DFFF00"}, + new String[]{"#304267", "#aab0b1", "#aab0b1", "#aab0b1"}, + new String[]{"#e6b876"}, + new String[]{"#50230a", "#50230a", "#50230a", "#50230a"}, + new String[]{"#000"}, + new String[]{"#333", "#afafaf", "#222", "#6d3a1d"} + ) + )); + + // Character 05 - Geeknot + themes.put(CharacterType.GEEKNOT, new CharacterThemes( + new Colors( + new String[]{"#a09300"}, + new String[]{"#c7d4e2", "#435363", "#435363", "#141720", "#141720", "#e7ecf2", "#e7ecf2"}, + new String[]{"#f5d4a6"}, + new String[]{"#000", "#cf9f76"}, + new String[]{"#000", "#000", "#000", "#000", "#000", "#000", "#000", "#000", "#fff", "#fff", "#fff", "#fff"}, + new String[]{"none", "#fdff00"} + ), + new Colors( + new String[]{"#b3003e"}, + new String[]{"#000", "#435363", "#435363", "#000", "none", "#e7ecf2", "#e7ecf2"}, + new String[]{"#f5d4a6"}, + new String[]{"#000", "#af9f94"}, + new String[]{"#9ff3ff;opacity:0.96", "#000", "#9ff3ff;opacity:0.96", "#000", "#2f508a", "#000", "#000", "#000", "none", "none", "none", "none"}, + new String[]{"#ff9a00", "#ff9a00"} + ), + new Colors( + new String[]{"#884f00"}, + new String[]{"#ff0000", "#fff", "#fff", "#141720", "#141720", "#e7ecf2", "#e7ecf2"}, + new String[]{"#c57b14"}, + new String[]{"#000", "#cf9f76"}, + new String[]{"none", "#000", "none", "#000", "#5a0000", "#000", "#000", "#000", "none", "none", "none", "none"}, + new String[]{"#efefef", "none"} + ) + )); + + // Character 06 - Asian + themes.put(CharacterType.ASIAN, new CharacterThemes( + new Colors( + new String[]{"#8acf00"}, + new String[]{"#ee2829", "#ff0"}, + new String[]{"#ffce73"}, + new String[]{"#fff", "#000"}, + new String[]{"#000"}, + new String[]{"#000", "#000", "none", "#000", "#ff4e4e", "#000"} + ), + new Colors( + new String[]{"#00d2a3"}, + new String[]{"#0D0046", "#ffce73"}, + new String[]{"#ffce73"}, + new String[]{"#000", "none"}, + new String[]{"#000"}, + new String[]{"#000", "#000", "#000", "none", "#ffb358", "#000", "none", "none"} + ), + new Colors( + new String[]{"#ff184e"}, + new String[]{"#000", "none"}, + new String[]{"#ffce73"}, + new String[]{"#ff0000", "none"}, + new String[]{"#000"}, + new String[]{"none", "none", "none", "none", "none", "#ffc107", "none", "none"} + ) + )); + + // Character 07 - Punk + themes.put(CharacterType.PUNK, new CharacterThemes( + new Colors( + new String[]{"#00deae"}, + new String[]{"#ff0000"}, + new String[]{"#ffce94"}, + new String[]{"#f73b6c", "#000"}, + new String[]{"#e91e63", "#000", "#e91e63", "#000", "#000", "#000"}, + new String[]{"#dd104f", "#dd104f", "#f73b6c", "#dd104f"} + ), + new Colors( + new String[]{"#181284"}, + new String[]{"#491f49", "#ff9809", "#491f49"}, + new String[]{"#f6ba97"}, + new String[]{"#ff9809", "#000"}, + new String[]{"#c4ffe4", "#000", "#c4ffe4", "#000", "#000", "#000"}, + new String[]{"none", "none", "#d6f740", "#516303"} + ), + new Colors( + new String[]{"#bcf700"}, + new String[]{"#ff14e4", "#000", "#14fffd"}, + new String[]{"#7b401e"}, + new String[]{"#666", "#000"}, + new String[]{"#00b5b4", "#000", "#00b5b4", "#000", "#000", "#000"}, + new String[]{"#14fffd", "#14fffd", "#14fffd", "#0d3a62"} + ) + )); + + // Character 08 - Afrohair + themes.put(CharacterType.AFROHAIR, new CharacterThemes( + new Colors( + new String[]{"#0df"}, + new String[]{"#571e57", "#ff0"}, + new String[]{"#f2c280"}, + new String[]{"#ff0000"}, + new String[]{"#795548", "#000"}, + new String[]{"#de3b00", "none"} + ), + new Colors( + new String[]{"#B400C2"}, + new String[]{"#0D204A", "#00ffdf"}, + new String[]{"#ca8628"}, + new String[]{"#1a1a1a"}, + new String[]{"#cbbdaf", "#000"}, + new String[]{"#000", "#000"} + ), + new Colors( + new String[]{"#ffe926"}, + new String[]{"#00d6af", "#000"}, + new String[]{"#8c5100"}, + new String[]{"#7d0000"}, + new String[]{"none", "#000"}, + new String[]{"#f7f7f7", "none"} + ) + )); + + // Character 09 - Normie Female + themes.put(CharacterType.NORMIE_FEMALE, new CharacterThemes( + new Colors( + new String[]{"#4aff0c"}, + new String[]{"#101010", "#fff", "#fff"}, + new String[]{"#dbbc7f"}, + new String[]{"#000"}, + new String[]{"#000", "none", "none"}, + new String[]{"#531148", "#531148", "#531148", "none"} + ), + new Colors( + new String[]{"#FFC107"}, + new String[]{"#033c58", "#fff", "#fff"}, + new String[]{"#dbc97f"}, + new String[]{"#000"}, + new String[]{"none", "#000", "#fff"}, + new String[]{"#FFEB3B", "#FFEB3B", "none", "#FFEB3B"} + ), + new Colors( + new String[]{"#FF9800"}, + new String[]{"#b40000", "#fff", "#fff"}, + new String[]{"#E2AF6B"}, + new String[]{"#000"}, + new String[]{"none", "#000", "#fff"}, + new String[]{"#ec0000", "#ec0000", "none", "none"} + ) + )); + + // Character 10 - Older + themes.put(CharacterType.OLDER, new CharacterThemes( + new Colors( + new String[]{"#104c8c"}, + new String[]{"#354B65", "#3D8EBB", "#89D0DA", "#00FFFD"}, + new String[]{"#cc9a5c"}, + new String[]{"#222", "#fff"}, + new String[]{"#000", "#000"}, + new String[]{"#fff", "#fff", "none"} + ), + new Colors( + new String[]{"#0DC15C"}, + new String[]{"#212121", "#fff", "#212121", "#fff"}, + new String[]{"#dca45f"}, + new String[]{"#111", "#633b1d"}, + new String[]{"#000", "#000"}, + new String[]{"none", "#792B74", "#792B74"} + ), + new Colors( + new String[]{"#ffe500"}, + new String[]{"#1e5e80", "#fff", "#1e5e80", "#fff"}, + new String[]{"#e8bc86"}, + new String[]{"#111", "none"}, + new String[]{"#000", "#000"}, + new String[]{"none", "none", "#633b1d"} + ) + )); + + // Character 11 - Firehair + themes.put(CharacterType.FIREHAIR, new CharacterThemes( + new Colors( + new String[]{"#4a3f73"}, + new String[]{"#e6e9ee", "#f1543f", "#ff7058", "#fff", "#fff"}, + new String[]{"#b27e5b"}, + new String[]{"#191919", "#191919"}, + new String[]{"#000", "#000", "#57FFFD"}, + new String[]{"#ffc", "#ffc", "#ffc"} + ), + new Colors( + new String[]{"#00a08d"}, + new String[]{"#FFBA32", "#484848", "#4e4e4e", "#fff", "#fff"}, + new String[]{"#ab5f2c"}, + new String[]{"#191919", "#191919"}, + new String[]{"#000", "#ff23fa;opacity:0.39", "#000"}, + new String[]{"#ff90f4", "#ff90f4", "#ff90f4"} + ), + new Colors( + new String[]{"#22535d"}, + new String[]{"#000", "#ff2500", "#ff2500", "#fff", "#fff"}, + new String[]{"#a76c44"}, + new String[]{"#191919", "#191919"}, + new String[]{"#000", "none", "#000"}, + new String[]{"none", "#00efff", "none"} + ) + )); + + // Character 12 - Blond + themes.put(CharacterType.BLOND, new CharacterThemes( + new Colors( + new String[]{"#2668DC"}, + new String[]{"#2385c6", "#b8d0e0", "#b8d0e0"}, + new String[]{"#ad8a60"}, + new String[]{"#000", "#4d4d4d"}, + new String[]{"#7fb5a2", "#d1eddf", "#301e19"}, + new String[]{"#fff510", "#fff510"} + ), + new Colors( + new String[]{"#643869"}, + new String[]{"#D67D1B", "#b8d0e0", "#b8d0e0"}, + new String[]{"#CC985A", "none0000"}, + new String[]{"#000", "#ececec"}, + new String[]{"#1f2644", "#9b97ce", "#301e19"}, + new String[]{"#00eaff", "none"} + ), + new Colors( + new String[]{"#F599FF"}, + new String[]{"#2823C6", "#b8d0e0", "#b8d0e0"}, + new String[]{"#C7873A"}, + new String[]{"#000", "#4d4d4d"}, + new String[]{"#581b1b", "#FF8B8B", "#000"}, + new String[]{"none", "#9c0092"} + ) + )); + + // Character 13 - Ateam + themes.put(CharacterType.ATEAM, new CharacterThemes( + new Colors( + new String[]{"#d10084"}, + new String[]{"#efedee", "#00a1e0", "#00a1e0", "#efedee", "#ffce1c"}, + new String[]{"#b35f49"}, + new String[]{"#3a484a", "#000"}, + new String[]{"#000"}, + new String[]{"#000", "none", "#000", "none"} + ), + new Colors( + new String[]{"#E6C117"}, + new String[]{"#efedee", "#ec0033", "#ec0033", "#efedee", "#f2ff05"}, + new String[]{"#ffc016"}, + new String[]{"#4a3737", "#000"}, + new String[]{"#000"}, + new String[]{"#ffe900", "#ffe900", "none", "#ffe900"} + ), + new Colors( + new String[]{"#1d8c00"}, + new String[]{"#e000cb", "#fff", "#fff", "#e000cb", "#ffce1c"}, + new String[]{"#b96438"}, + new String[]{"#000", "#000"}, + new String[]{"#000"}, + new String[]{"#53ffff", "#53ffff", "none", "none"} + ) + )); + + // Character 14 - Rasta + themes.put(CharacterType.RASTA, new CharacterThemes( + new Colors( + new String[]{"#fc0065"}, + new String[]{"#708913", "#fdea14", "#708913", "#fdea14", "#708913"}, + new String[]{"#DEA561"}, + new String[]{"#444", "#000"}, + new String[]{"#000"}, + new String[]{"#32393f", "#32393f", "#32393f", "#32393f", "#32393f", "#32393f", "#32393f", "#32393f", "#32393f", "#32393f", "#32393f", "#32393f", "#32393f", "#32393f", "#32393f", "#32393f", "#32393f"} + ), + new Colors( + new String[]{"#81f72e"}, + new String[]{"#ff0000", "#ffc107", "#ff0000", "#ffc107", "#ff0000"}, + new String[]{"#ef9831"}, + new String[]{"#6b0000", "#000"}, + new String[]{"#000"}, + new String[]{"#FFFAAD", "#FFFAAD", "#FFFAAD", "#FFFAAD", "#FFFAAD", "#FFFAAD", "#FFFAAD", "#FFFAAD", "#FFFAAD", "#FFFAAD", "#FFFAAD", "#FFFAAD", "#FFFAAD", "none", "none", "none", "none"} + ), + new Colors( + new String[]{"#00D872"}, + new String[]{"#590D00", "#FD1336", "#590D00", "#FD1336", "#590D00"}, + new String[]{"#c36c00"}, + new String[]{"#56442b", "#000"}, + new String[]{"#000"}, + new String[]{"#004E4C", "#004E4C", "#004E4C", "#004E4C", "#004E4C", "#004E4C", "#004E4C", "#004E4C", "#004E4C", "none", "none", "none", "none", "none", "none", "none", "none"} + ) + )); + + // Character 15 - Meta + themes.put(CharacterType.STREET, new CharacterThemes( + new Colors( + new String[]{"#111"}, + new String[]{"#000", "#00FFFF"}, + new String[]{"#755227"}, + new String[]{"#fff", "#000"}, + new String[]{"black", "#008;opacity:0.67", "aqua"}, + new String[]{"#fff", "#fff", "#fff", "#fff", "#fff"} + ), + new Colors( + new String[]{"#00D0D4"}, + new String[]{"#000", "#fff"}, + new String[]{"#755227"}, + new String[]{"#fff", "#000"}, + new String[]{"black", "#1df7ff;opacity:0.64", "#fcff2c"}, + new String[]{"#fff539", "none", "#fff539", "none", "#fff539"} + ), + new Colors( + new String[]{"#DC75FF"}, + new String[]{"#000", "#FFBDEC"}, + new String[]{"#997549"}, + new String[]{"#fff", "#000"}, + new String[]{"black", "black", "aqua"}, + new String[]{"#00fffd", "none", "none", "none", "none"} + ) + )); + + return themes; + } +} diff --git a/src/test/java/com/multiavatar/CompareHash.java b/src/test/java/com/multiavatar/CompareHash.java new file mode 100644 index 0000000..ce53e5e --- /dev/null +++ b/src/test/java/com/multiavatar/CompareHash.java @@ -0,0 +1,52 @@ +package com.multiavatar; + +import java.security.MessageDigest; + +/** + * Debug utility to verify SHA-256 hash implementation matches between Java and JavaScript. + * Run with: mvn test-compile exec:java -Dexec.mainClass="com.multiavatar.CompareHash" -Dexec.classpathScope=test + */ +public class CompareHash { + public static void main(String[] args) throws Exception { + String[] testInputs = {"Binx Bond", "Test", "Alice", "", "test"}; + + System.out.println("=== SHA-256 Hash Comparison ===\n"); + + for (String input : testInputs) { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hash = digest.digest(input.getBytes("UTF-8")); + + StringBuilder hexString = new StringBuilder(); + for (byte b : hash) { + String hex = Integer.toHexString(0xff & b); + if (hex.length() == 1) hexString.append('0'); + hexString.append(hex); + } + + String hashStr = hexString.toString(); + String digitsOnly = hashStr.replaceAll("\\D", ""); + String first12 = digitsOnly.substring(0, Math.min(12, digitsOnly.length())); + + System.out.printf("Input: \"%s\"%n", input); + System.out.printf(" Full hash: %s%n", hashStr); + System.out.printf(" Digits only: %s%n", digitsOnly); + System.out.printf(" First 12 digits: %s%n", first12); + + // Show how these map to part numbers + if (first12.length() >= 12) { + for (int i = 0; i < 6; i++) { + String digitPair = first12.substring(i * 2, i * 2 + 2); + int value = Integer.parseInt(digitPair); + int partNum = Math.round((47f / 100f) * value); + System.out.printf(" Part %d: %s (dec) -> %d (part 0-47)%n", + i, digitPair, partNum); + } + } + System.out.println(); + } + + System.out.println("\nExpected hashes from JavaScript (verify these match):"); + System.out.println(" \"Binx Bond\" -> c24e86de81fa... (first 12: c24e86de81fa)"); + System.out.println(" \"Test\" -> 532eaabd9574... (first 12: 532eaabd9574)"); + } +} diff --git a/src/test/java/com/multiavatar/CompareJsJavaOutput.java b/src/test/java/com/multiavatar/CompareJsJavaOutput.java new file mode 100644 index 0000000..4da3a78 --- /dev/null +++ b/src/test/java/com/multiavatar/CompareJsJavaOutput.java @@ -0,0 +1,123 @@ +package com.multiavatar; + +import java.nio.file.Files; +import java.nio.file.Paths; + +/** + * Debug utility to compare JavaScript and Java outputs directly. + * Run with: mvn test-compile exec:java -Dexec.mainClass="com.multiavatar.CompareJsJavaOutput" -Dexec.classpathScope=test + */ +public class CompareJsJavaOutput { + public static void main(String[] args) throws Exception { + String input = "Test"; + + // Generate using Java (predefined avatar) + String javaOutput = Multiavatar.generate(CharacterType.GEEKNOT, Theme.A); + + // Debug: Print which parts are being used + System.out.println("=== DEBUG: Hash and Parts for: \"" + input + "\" ==="); + java.security.MessageDigest digest = java.security.MessageDigest.getInstance("SHA-256"); + byte[] hash = digest.digest(input.getBytes("UTF-8")); + StringBuilder hexString = new StringBuilder(); + for (byte b : hash) { + String hex = Integer.toHexString(0xff & b); + if (hex.length() == 1) hexString.append('0'); + hexString.append(hex); + } + String hashStr = hexString.toString(); + String digitsOnly = hashStr.replaceAll("\\D", ""); + String first12 = digitsOnly.substring(0, Math.min(12, digitsOnly.length())); + System.out.println("Hash: " + hashStr); + System.out.println("Digits only: " + digitsOnly); + System.out.println("First 12: " + first12); + System.out.println(); + + System.out.println("=== Comparing Output for: \"" + input + "\" [05A] ===\n"); + + System.out.println("Java output length: " + javaOutput.length()); + System.out.println("Java output (first 300 chars):"); + System.out.println(javaOutput.substring(0, Math.min(300, javaOutput.length()))); + System.out.println(); + + // Load JavaScript output from test vectors + try { + String jsData = new String(Files.readAllBytes(Paths.get("test-vectors.json"))); + + // Find the entry for case 15 (Test with version 05A) + int idx = jsData.indexOf("\"id\": 15"); + if (idx > 0) { + int outputIdx = jsData.indexOf("\"output\":", idx); + int outputStart = jsData.indexOf("\"", outputIdx + 9) + 1; + + // Find the end quote (accounting for escaped quotes) + int outputEnd = outputStart; + while (outputEnd < jsData.length()) { + if (jsData.charAt(outputEnd) == '"' && jsData.charAt(outputEnd - 1) != '\\') { + break; + } + outputEnd++; + } + + String jsOutput = jsData.substring(outputStart, outputEnd); + // Unescape JSON + jsOutput = jsOutput.replace("\\\"", "\"") + .replace("\\\\", "\\") + .replace("\\n", "\n") + .replace("\\r", "\r") + .replace("\\t", "\t"); + + System.out.println("JavaScript output length: " + jsOutput.length()); + System.out.println("JavaScript output (first 300 chars):"); + System.out.println(jsOutput.substring(0, Math.min(300, jsOutput.length()))); + System.out.println(); + + // Character-by-character comparison + System.out.println("=== Character-by-Character Comparison (first 100 chars) ==="); + int compareLen = Math.min(100, Math.min(javaOutput.length(), jsOutput.length())); + for (int i = 0; i < compareLen; i++) { + char javaChar = javaOutput.charAt(i); + char jsChar = jsOutput.charAt(i); + if (javaChar != jsChar) { + System.out.printf("Position %d: Java='%c' (0x%02x), JS='%c' (0x%02x)\n", + i, javaChar, (int)javaChar, jsChar, (int)jsChar); + } + } + + // Compare structure + System.out.println("\n=== Structural Comparison ==="); + System.out.println("Java starts with: " + javaOutput.substring(0, Math.min(80, javaOutput.length()))); + System.out.println("JS starts with: " + jsOutput.substring(0, Math.min(80, jsOutput.length()))); + + int len = Math.min(javaOutput.length(), jsOutput.length()); + int firstDiff = -1; + for (int i = 0; i < len; i++) { + if (javaOutput.charAt(i) != jsOutput.charAt(i)) { + firstDiff = i; + break; + } + } + + if (firstDiff >= 0) { + System.out.println("\nFirst difference at position " + firstDiff + ":"); + int start = Math.max(0, firstDiff - 20); + int end = Math.min(len, firstDiff + 60); + System.out.println("Java: ..." + javaOutput.substring(start, end) + "..."); + System.out.println("JS: ..." + jsOutput.substring(start, end) + "..."); + } else { + System.out.println("\nNo character differences found in common length!"); + if (javaOutput.length() != jsOutput.length()) { + System.out.println("But lengths differ: Java=" + javaOutput.length() + ", JS=" + jsOutput.length()); + } + } + + System.out.println("\nMatch: " + javaOutput.equals(jsOutput)); + + } else { + System.out.println("Could not find case 15 in test vectors"); + } + } catch (Exception e) { + System.out.println("Error reading test vectors: " + e.getMessage()); + e.printStackTrace(); + } + } +} diff --git a/src/test/java/com/multiavatar/CrossPlatformCompatibilityTest.java b/src/test/java/com/multiavatar/CrossPlatformCompatibilityTest.java new file mode 100644 index 0000000..7950e04 --- /dev/null +++ b/src/test/java/com/multiavatar/CrossPlatformCompatibilityTest.java @@ -0,0 +1,254 @@ +package com.multiavatar; + +import org.junit.BeforeClass; +import org.junit.Test; +import static org.junit.Assert.*; + +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.List; +import java.util.ArrayList; + +/** + * Cross-platform compatibility test. + * Verifies that the Java implementation produces identical output + * to the JavaScript implementation for the same inputs. + * + * Test vectors are generated by running: node generate-test-vectors.cjs + */ +public class CrossPlatformCompatibilityTest { + + private static class TestVector { + int id; + String input; + boolean sansEnv; + Version version; + String output; + int length; + String error; + + static class Version { + String part; + char theme; + } + } + + private static List testVectors; + + @BeforeClass + public static void loadTestVectors() throws IOException { + testVectors = parseTestVectors("test-vectors.json"); + System.out.println("Loaded " + testVectors.size() + " test vectors from JavaScript implementation"); + } + + @Test + public void testAllVectorsMatch() { + int passed = 0; + int failed = 0; + List failures = new ArrayList<>(); + + for (TestVector vector : testVectors) { + // Skip vectors that had errors in JS + if (vector.error != null) { + continue; + } + + try { + // Generate avatar using Java implementation + String javaOutput; + if (vector.version != null) { + // Use predefined character/theme + CharacterType character = CharacterType.fromId(vector.version.part); + Theme theme = Theme.fromCode(vector.version.theme); + javaOutput = Multiavatar.generate(character, theme, vector.sansEnv); + } else { + // Generate from string ID + javaOutput = Multiavatar.generate(vector.input, vector.sansEnv); + } + + // Strip attribution metadata from Java output for comparison + // (Java adds metadata for license compliance) + String javaOutputStripped = javaOutput.replace("Multiavatarhttps://multiavatar.com", ""); + + // Compare with JavaScript output + if (javaOutputStripped.equals(vector.output)) { + passed++; + } else { + failed++; + String versionStr = vector.version != null ? + " [" + vector.version.part + vector.version.theme + "]" : ""; + String failureMsg = String.format( + "Case %d FAILED: \"%s\"%s sansEnv=%b\n" + + " Expected length: %d, Got: %d (stripped: %d)\n" + + " Output differs from JavaScript implementation\n" + + " Expected: %s\n" + + " Actual: %s\n", + vector.id, + vector.input.length() > 30 ? vector.input.substring(0, 30) + "..." : vector.input, + versionStr, + vector.sansEnv, + vector.output.length(), + javaOutput.length(), + javaOutputStripped.length(), + vector.output, + javaOutputStripped + ); + failures.add(failureMsg); + System.err.println(failureMsg); + + File failureDir = new File("target/failures"); + if (!failureDir.exists()) { + failureDir.mkdirs(); + } + + GenerateExamplesTest.writeToFile(new File(failureDir, vector.id + "-expected.svg"), vector.output); + GenerateExamplesTest.writeToFile(new File(failureDir, vector.id + "-failure.svg"), javaOutputStripped); + } + } catch (Exception e) { + failed++; + String failureMsg = String.format( + "Case %d EXCEPTION: \"%s\" - %s", + vector.id, + vector.input, + e.getMessage() + ); + failures.add(failureMsg); + System.err.println(failureMsg); + } + } + + // Print summary + System.out.println("\n=== Cross-Platform Compatibility Test Results ==="); + System.out.println("Total vectors tested: " + (passed + failed)); + System.out.println("Passed: " + passed); + System.out.println("Failed: " + failed); + + if (failed > 0) { + System.err.println("\n=== Failures ==="); + for (String failure : failures) { + System.err.println(failure); + } + } + + // Assert all tests passed + assertEquals("Java implementation must match JavaScript output for all test vectors", + 0, failed); + } + + /** + * Simple JSON parser for test vectors (avoiding external dependencies) + */ + private static List parseTestVectors(String filename) throws IOException { + String content = new String(Files.readAllBytes(Paths.get(filename))); + List vectors = new ArrayList<>(); + + // Parse JSON array manually (simple approach for our specific format) + String[] items = content.substring(content.indexOf('[') + 1, content.lastIndexOf(']')) + .split("\\},\\s*\\{"); + + for (String item : items) { + item = item.trim(); + if (!item.startsWith("{")) item = "{" + item; + if (!item.endsWith("}")) item = item + "}"; + + TestVector vector = new TestVector(); + + // Extract fields + vector.id = extractInt(item, "\"id\""); + vector.input = extractString(item, "\"input\""); + vector.sansEnv = extractBoolean(item, "\"sansEnv\""); + vector.length = extractInt(item, "\"length\""); + vector.output = extractString(item, "\"output\""); + vector.error = extractString(item, "\"error\""); + + // Extract version if present + int versionStart = item.indexOf("\"version\""); + if (versionStart > 0) { + int versionObjStart = item.indexOf('{', versionStart); + int versionObjEnd = item.indexOf('}', versionObjStart); + if (versionObjStart > 0 && versionObjEnd > 0) { + String versionStr = item.substring(versionObjStart, versionObjEnd + 1); + if (!versionStr.contains("null")) { + TestVector.Version version = new TestVector.Version(); + version.part = extractString(versionStr, "\"part\""); + String themeStr = extractString(versionStr, "\"theme\""); + if (themeStr != null && !themeStr.isEmpty()) { + version.theme = themeStr.charAt(0); + } + vector.version = version; + } + } + } + + vectors.add(vector); + } + + return vectors; + } + + private static String extractString(String json, String key) { + int keyIndex = json.indexOf(key); + if (keyIndex < 0) return null; + + int colonIndex = json.indexOf(':', keyIndex); + int quoteStart = json.indexOf('"', colonIndex); + if (quoteStart < 0) return null; + + int quoteEnd = quoteStart + 1; + while (quoteEnd < json.length()) { + if (json.charAt(quoteEnd) == '"' && json.charAt(quoteEnd - 1) != '\\') { + break; + } + quoteEnd++; + } + + if (quoteEnd >= json.length()) return null; + + String value = json.substring(quoteStart + 1, quoteEnd); + // Unescape JSON strings + value = value.replace("\\\"", "\"") + .replace("\\\\", "\\") + .replace("\\n", "\n") + .replace("\\r", "\r") + .replace("\\t", "\t"); + + return value; + } + + private static int extractInt(String json, String key) { + int keyIndex = json.indexOf(key); + if (keyIndex < 0) return 0; + + int colonIndex = json.indexOf(':', keyIndex); + int numStart = colonIndex + 1; + + // Skip whitespace + while (numStart < json.length() && Character.isWhitespace(json.charAt(numStart))) { + numStart++; + } + + int numEnd = numStart; + while (numEnd < json.length() && (Character.isDigit(json.charAt(numEnd)) || json.charAt(numEnd) == '-')) { + numEnd++; + } + + if (numEnd > numStart) { + return Integer.parseInt(json.substring(numStart, numEnd)); + } + + return 0; + } + + private static boolean extractBoolean(String json, String key) { + int keyIndex = json.indexOf(key); + if (keyIndex < 0) return false; + + int colonIndex = json.indexOf(':', keyIndex); + String afterColon = json.substring(colonIndex + 1).trim(); + + return afterColon.startsWith("true"); + } +} diff --git a/src/test/java/com/multiavatar/DebugTest.java b/src/test/java/com/multiavatar/DebugTest.java new file mode 100644 index 0000000..d9a0536 --- /dev/null +++ b/src/test/java/com/multiavatar/DebugTest.java @@ -0,0 +1,25 @@ +package com.multiavatar; + +import com.multiavatar.SvgData.Template; + +public class DebugTest { + public static void main(String[] args) { + System.out.println("Testing Multiavatar..."); + + String svg = Multiavatar.generate("Test"); + System.out.println("Generated SVG length: " + svg.length()); + System.out.println("SVG content:\n" + svg); + + // Test SVG data + Template svgPart = SvgData.getSvgTemplate(CharacterType.ROBO, AvatarPart.ENV); + System.out.println("\nSVG Part ROBO/ENV: " + (svgPart != null ? svgPart.toSource().substring(0, Math.min(100, svgPart.toSource().length())) : "NULL")); + + // Test theme data + ThemeData.CharacterThemes themes = ThemeData.getCharacterThemes(CharacterType.ROBO); + System.out.println("\nThemes for ROBO: " + (themes != null ? "Found" : "NULL")); + if (themes != null) { + ThemeData.Colors themeA = themes.getTheme(Theme.A); + System.out.println("Theme A env colors: " + java.util.Arrays.toString(themeA.env)); + } + } +} diff --git a/src/test/java/com/multiavatar/GenerateExamplesTest.java b/src/test/java/com/multiavatar/GenerateExamplesTest.java new file mode 100644 index 0000000..ee201ec --- /dev/null +++ b/src/test/java/com/multiavatar/GenerateExamplesTest.java @@ -0,0 +1,92 @@ +package com.multiavatar; + +import org.junit.Test; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.file.Files; + +/** + * Test that generates example SVG files for manual inspection. + * The generated files are placed in target/examples/ + */ +public class GenerateExamplesTest { + + @Test + public void generateExampleAvatars() throws IOException { + // Create examples directory + File examplesDir = new File("target/examples"); + if (!examplesDir.exists()) { + examplesDir.mkdirs(); + } + + // Sample input strings to generate avatars from + String[] inputs = { + "Binx Bond", + "Test User", + "Alice", + "Bob", + "Charlie", + "Diana", + "Emma", + "Frank", + "Grace", + "Henry", + "测试用户", + "user@example.com", + "John Doe", + "Jane Smith", + "Admin123", + "guest", + "developer", + "designer", + "manager", + "support" + }; + + // Generate avatars with background + for (String input : inputs) { + String svg = Multiavatar.generate(input); + String filename = sanitizeFilename(input) + ".svg"; + writeToFile(new File(examplesDir, filename), svg); + } + + // Generate some examples without background + String[] sansEnvInputs = {"Binx Bond", "Alice", "Charlie", "Emma"}; + for (String input : sansEnvInputs) { + String svg = Multiavatar.generate(input, true); + String filename = sanitizeFilename(input) + "_no_background.svg"; + writeToFile(new File(examplesDir, filename), svg); + } + + // Generate examples with forced versions (different themes for same character) + String testInput = "Version Test"; + for (int characterIndex = 0; characterIndex < 3; characterIndex++) { + CharacterType character = CharacterType.fromIndex(characterIndex); + for (Theme theme : Theme.values()) { + String svg = Multiavatar.generate(character, theme); + String filename = "version_char" + character.getId() + "_theme" + theme.getCode() + ".svg"; + writeToFile(new File(examplesDir, filename), svg); + } + } + + System.out.println("Generated " + (inputs.length + sansEnvInputs.length + 9) + " example SVG files in target/examples/"); + System.out.println("Open these files in a web browser to view the avatars."); + } + + /** + * Sanitize filename to be filesystem-safe + */ + private String sanitizeFilename(String input) { + return input.replaceAll("[^a-zA-Z0-9_-]", "_").toLowerCase(); + } + + /** + * Write content to file + */ + static void writeToFile(File file, String content) throws IOException { + try (FileWriter writer = new FileWriter(file)) { + writer.write(content); + } + } +} diff --git a/src/test/java/com/multiavatar/MultiavatarTest.java b/src/test/java/com/multiavatar/MultiavatarTest.java new file mode 100644 index 0000000..abd2752 --- /dev/null +++ b/src/test/java/com/multiavatar/MultiavatarTest.java @@ -0,0 +1,124 @@ +package com.multiavatar; + +import org.junit.Test; +import static org.junit.Assert.*; + +/** + * Unit tests for Multiavatar + */ +public class MultiavatarTest { + + @Test + public void testGenerateBasic() { + String svg = Multiavatar.generate("Binx Bond"); + assertNotNull("SVG should not be null", svg); + assertTrue("SVG should start with ", svg.endsWith("")); + assertTrue("SVG should contain viewBox", svg.contains("viewBox")); + assertTrue("SVG should contain metadata with creator", svg.contains("Multiavatar")); + assertTrue("SVG should contain metadata with source", svg.contains("https://multiavatar.com")); + } + + @Test + public void testGenerateSansEnv() { + String svgWithEnv = Multiavatar.generate("Test User", false); + String svgWithoutEnv = Multiavatar.generate("Test User", true); + + assertNotNull("SVG with env should not be null", svgWithEnv); + assertNotNull("SVG without env should not be null", svgWithoutEnv); + + // Both should be valid SVGs + assertTrue("SVG with env should be valid", svgWithEnv.startsWith(""); + assertEquals("Should have matching svg tags", openTags, closeTags); + } + + private int countOccurrences(String str, String substr) { + int count = 0; + int index = 0; + while ((index = str.indexOf(substr, index)) != -1) { + count++; + index += substr.length(); + } + return count; + } +} diff --git a/src/test/java/com/multiavatar/TestChar05Debug.java b/src/test/java/com/multiavatar/TestChar05Debug.java new file mode 100644 index 0000000..a675260 --- /dev/null +++ b/src/test/java/com/multiavatar/TestChar05Debug.java @@ -0,0 +1,54 @@ +package com.multiavatar; + +public class TestChar05Debug { + public static void main(String[] args) { + // Get theme for character GEEKNOT (05) Theme A + ThemeData.CharacterThemes ct = ThemeData.getCharacterThemes(CharacterType.GEEKNOT); + ThemeData.Colors theme = ct.getTheme(Theme.A); + String[] eyes = theme.eyes; + System.out.println("Character GEEKNOT (05) Theme A eyes colors (" + eyes.length + " total):"); + for (int i = 0; i < eyes.length; i++) { + System.out.println(" " + i + ": " + eyes[i]); + } + + // Generate and check output + String svg = Multiavatar.generate(CharacterType.GEEKNOT, Theme.A); + + // Extract just the eyes SVG for comparison + int eyesStart = svg.indexOf(" 0) { + // Find the last in the eyes section (ends with the last 4;stroke:#fff;"/>) + int searchStart = eyesStart + 1500; + int eyesEnd = -1; + for (int i = 0; i < 5; i++) { + eyesEnd = svg.indexOf("stroke:#fff;\"/>", searchStart); + if (eyesEnd > 0) { + eyesEnd += 14; // length of "stroke:#fff;"/>" + break; + } + searchStart -= 200; + } + if (eyesEnd > eyesStart) { + String eyesSvg = svg.substring(eyesStart, eyesEnd); + try { + java.nio.file.Files.write(java.nio.file.Paths.get("/tmp/java_eyes_output.txt"), eyesSvg.getBytes()); + System.out.println("\nWrote eyes SVG to /tmp/java_eyes_output.txt (" + eyesSvg.length() + " chars)"); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + // Find the stroke color before "m109.32" + int m109Pos = svg.indexOf("m109.32"); + if (m109Pos > 0) { + int strokePos = svg.lastIndexOf("stroke:#", m109Pos); + if (strokePos > 0) { + String strokeColor = svg.substring(strokePos, strokePos + 13); + System.out.println("\nStroke color before 'm109.32': " + strokeColor); + System.out.println("Expected: stroke:#000;"); + System.out.println("Match: " + strokeColor.equals("stroke:#000;")); + } + } + } +} diff --git a/src/test/java/com/multiavatar/TestColorReplacement.java b/src/test/java/com/multiavatar/TestColorReplacement.java new file mode 100644 index 0000000..43a9339 --- /dev/null +++ b/src/test/java/com/multiavatar/TestColorReplacement.java @@ -0,0 +1,36 @@ +package com.multiavatar; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class TestColorReplacement { + public static void main(String[] args) { + // Simplified version of character 05 eyes template (last part) + String template = "stroke-width:4;stroke:#fff;\"/>"; + + // Last 4 colors from theme A eyes (indices 8-11) + String[] colors = new String[]{"#fff", "#fff", "#fff", "#fff", "#000", "#000"}; + + // Java replacement logic from Multiavatar.java + Pattern pattern = Pattern.compile("#(.*?);"); + Matcher matcher = pattern.matcher(template); + + int colorIndex = 0; + StringBuffer sb = new StringBuffer(); + System.out.println("=== Replacements ==="); + while (matcher.find() && colorIndex < colors.length) { + String matched = matcher.group(0); + String replacement = colors[colorIndex] + ";"; + System.out.println((colorIndex + 1) + ". \"" + matched + "\" -> \"" + replacement + "\""); + matcher.appendReplacement(sb, Matcher.quoteReplacement(replacement)); + colorIndex++; + } + matcher.appendTail(sb); + + System.out.println("\n=== Result ==="); + System.out.println(sb.toString()); + + System.out.println("\n=== Expected (last stroke should be #000) ==="); + System.out.println("...stroke:#000;\"/>"); + } +} diff --git a/src/test/java/com/multiavatar/TestRegex.java b/src/test/java/com/multiavatar/TestRegex.java new file mode 100644 index 0000000..902bc16 --- /dev/null +++ b/src/test/java/com/multiavatar/TestRegex.java @@ -0,0 +1,27 @@ +package com.multiavatar; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class TestRegex { + public static void main(String[] args) { + String template = ""; + + Pattern pattern = Pattern.compile("#(.*?);"); + Matcher matcher = pattern.matcher(template); + + System.out.println("=== Regex Matches ==="); + int count = 0; + while (matcher.find()) { + count++; + String match = matcher.group(0); + if (match.length() > 50) { + System.out.println(count + ". " + match.substring(0, 50) + "... (" + match.length() + " chars total)"); + } else { + System.out.println(count + ". " + match); + } + } + System.out.println("\nTotal matches: " + count); + System.out.println("Expected: 2 (two #1a1a1a; and two #333;)"); + } +} diff --git a/test-vectors.json b/test-vectors.json new file mode 100644 index 0000000..864bc5f --- /dev/null +++ b/test-vectors.json @@ -0,0 +1,1403 @@ +[ + { + "id": 0, + "input": "Binx Bond", + "sansEnv": false, + "version": null, + "output": "", + "length": 2077 + }, + { + "id": 1, + "input": "Test User", + "sansEnv": false, + "version": null, + "output": "", + "length": 5633 + }, + { + "id": 2, + "input": "Alice", + "sansEnv": false, + "version": null, + "output": "", + "length": 4519 + }, + { + "id": 3, + "input": "Bob", + "sansEnv": false, + "version": null, + "output": "", + "length": 2706 + }, + { + "id": 4, + "input": "Example", + "sansEnv": true, + "version": null, + "output": "", + "length": 4601 + }, + { + "id": 5, + "input": "No Background", + "sansEnv": true, + "version": null, + "output": "", + "length": 4892 + }, + { + "id": 6, + "input": "", + "sansEnv": false, + "version": null, + "output": "", + "length": 0 + }, + { + "id": 7, + "input": "test", + "sansEnv": false, + "version": null, + "output": "", + "length": 3598 + }, + { + "id": 8, + "input": "TEST", + "sansEnv": false, + "version": null, + "output": "", + "length": 4038 + }, + { + "id": 9, + "input": "user@example.com", + "sansEnv": false, + "version": null, + "output": "", + "length": 2814 + }, + { + "id": 10, + "input": "User Name 123!", + "sansEnv": false, + "version": null, + "output": "", + "length": 3449 + }, + { + "id": 11, + "input": "测试用户", + "sansEnv": false, + "version": null, + "output": "", + "length": 4239 + }, + { + "id": 12, + "input": "user-with-dashes", + "sansEnv": false, + "version": null, + "output": "", + "length": 3964 + }, + { + "id": 13, + "input": "Test", + "sansEnv": false, + "version": { + "part": "00", + "theme": "A" + }, + "output": "", + "length": 3643 + }, + { + "id": 14, + "input": "Test", + "sansEnv": false, + "version": { + "part": "01", + "theme": "A" + }, + "output": "", + "length": 3788 + }, + { + "id": 15, + "input": "Test", + "sansEnv": false, + "version": { + "part": "02", + "theme": "A" + }, + "output": "", + "length": 5034 + }, + { + "id": 16, + "input": "Test", + "sansEnv": false, + "version": { + "part": "03", + "theme": "A" + }, + "output": "", + "length": 2896 + }, + { + "id": 17, + "input": "Test", + "sansEnv": false, + "version": { + "part": "04", + "theme": "A" + }, + "output": "", + "length": 3816 + }, + { + "id": 18, + "input": "Test", + "sansEnv": false, + "version": { + "part": "05", + "theme": "A" + }, + "output": "", + "length": 4718 + }, + { + "id": 19, + "input": "Test", + "sansEnv": false, + "version": { + "part": "06", + "theme": "A" + }, + "output": "", + "length": 2897 + }, + { + "id": 20, + "input": "Test", + "sansEnv": false, + "version": { + "part": "07", + "theme": "A" + }, + "output": "", + "length": 2756 + }, + { + "id": 21, + "input": "Test", + "sansEnv": false, + "version": { + "part": "08", + "theme": "A" + }, + "output": "", + "length": 4529 + }, + { + "id": 22, + "input": "Test", + "sansEnv": false, + "version": { + "part": "09", + "theme": "A" + }, + "output": "", + "length": 3673 + }, + { + "id": 23, + "input": "Test", + "sansEnv": false, + "version": { + "part": "10", + "theme": "A" + }, + "output": "", + "length": 3875 + }, + { + "id": 24, + "input": "Test", + "sansEnv": false, + "version": { + "part": "11", + "theme": "A" + }, + "output": "", + "length": 5690 + }, + { + "id": 25, + "input": "Test", + "sansEnv": false, + "version": { + "part": "12", + "theme": "A" + }, + "output": "", + "length": 3142 + }, + { + "id": 26, + "input": "Test", + "sansEnv": false, + "version": { + "part": "13", + "theme": "A" + }, + "output": "", + "length": 2833 + }, + { + "id": 27, + "input": "Test", + "sansEnv": false, + "version": { + "part": "14", + "theme": "A" + }, + "output": "", + "length": 4695 + }, + { + "id": 28, + "input": "Test", + "sansEnv": false, + "version": { + "part": "15", + "theme": "A" + }, + "output": "", + "length": 4920 + }, + { + "id": 29, + "input": "Theme", + "sansEnv": false, + "version": { + "part": "01", + "theme": "A" + }, + "output": "", + "length": 3788 + }, + { + "id": 30, + "input": "Theme", + "sansEnv": false, + "version": { + "part": "01", + "theme": "B" + }, + "output": "", + "length": 3794 + }, + { + "id": 31, + "input": "Theme", + "sansEnv": false, + "version": { + "part": "01", + "theme": "C" + }, + "output": "", + "length": 3785 + }, + { + "id": 32, + "input": "This is a longer test string with multiple words", + "sansEnv": false, + "version": null, + "output": "", + "length": 3362 + }, + { + "id": 33, + "input": "john.doe@example.com", + "sansEnv": false, + "version": null, + "output": "", + "length": 3539 + }, + { + "id": 34, + "input": "12345", + "sansEnv": false, + "version": null, + "output": "", + "length": 4481 + }, + { + "id": 35, + "input": "User123", + "sansEnv": false, + "version": null, + "output": "", + "length": 4491 + }, + { + "id": 36, + "input": "Robo - Theme A", + "sansEnv": false, + "version": { + "part": "00", + "theme": "A" + }, + "output": "", + "length": 3643 + }, + { + "id": 37, + "input": "Robo - Theme A (no background)", + "sansEnv": true, + "version": { + "part": "00", + "theme": "A" + }, + "output": "", + "length": 3540 + }, + { + "id": 38, + "input": "Robo - Theme B", + "sansEnv": false, + "version": { + "part": "00", + "theme": "B" + }, + "output": "", + "length": 3649 + }, + { + "id": 39, + "input": "Robo - Theme B (no background)", + "sansEnv": true, + "version": { + "part": "00", + "theme": "B" + }, + "output": "", + "length": 3546 + }, + { + "id": 40, + "input": "Robo - Theme C", + "sansEnv": false, + "version": { + "part": "00", + "theme": "C" + }, + "output": "", + "length": 3658 + }, + { + "id": 41, + "input": "Robo - Theme C (no background)", + "sansEnv": true, + "version": { + "part": "00", + "theme": "C" + }, + "output": "", + "length": 3555 + }, + { + "id": 42, + "input": "Girl - Theme A", + "sansEnv": false, + "version": { + "part": "01", + "theme": "A" + }, + "output": "", + "length": 3788 + }, + { + "id": 43, + "input": "Girl - Theme A (no background)", + "sansEnv": true, + "version": { + "part": "01", + "theme": "A" + }, + "output": "", + "length": 3685 + }, + { + "id": 44, + "input": "Girl - Theme B", + "sansEnv": false, + "version": { + "part": "01", + "theme": "B" + }, + "output": "", + "length": 3794 + }, + { + "id": 45, + "input": "Girl - Theme B (no background)", + "sansEnv": true, + "version": { + "part": "01", + "theme": "B" + }, + "output": "", + "length": 3691 + }, + { + "id": 46, + "input": "Girl - Theme C", + "sansEnv": false, + "version": { + "part": "01", + "theme": "C" + }, + "output": "", + "length": 3785 + }, + { + "id": 47, + "input": "Girl - Theme C (no background)", + "sansEnv": true, + "version": { + "part": "01", + "theme": "C" + }, + "output": "", + "length": 3682 + }, + { + "id": 48, + "input": "Blonde - Theme A", + "sansEnv": false, + "version": { + "part": "02", + "theme": "A" + }, + "output": "", + "length": 5034 + }, + { + "id": 49, + "input": "Blonde - Theme A (no background)", + "sansEnv": true, + "version": { + "part": "02", + "theme": "A" + }, + "output": "", + "length": 4931 + }, + { + "id": 50, + "input": "Blonde - Theme B", + "sansEnv": false, + "version": { + "part": "02", + "theme": "B" + }, + "output": "", + "length": 5035 + }, + { + "id": 51, + "input": "Blonde - Theme B (no background)", + "sansEnv": true, + "version": { + "part": "02", + "theme": "B" + }, + "output": "", + "length": 4932 + }, + { + "id": 52, + "input": "Blonde - Theme C", + "sansEnv": false, + "version": { + "part": "02", + "theme": "C" + }, + "output": "", + "length": 5049 + }, + { + "id": 53, + "input": "Blonde - Theme C (no background)", + "sansEnv": true, + "version": { + "part": "02", + "theme": "C" + }, + "output": "", + "length": 4946 + }, + { + "id": 54, + "input": "Guy - Theme A", + "sansEnv": false, + "version": { + "part": "03", + "theme": "A" + }, + "output": "", + "length": 2896 + }, + { + "id": 55, + "input": "Guy - Theme A (no background)", + "sansEnv": true, + "version": { + "part": "03", + "theme": "A" + }, + "output": "", + "length": 2793 + }, + { + "id": 56, + "input": "Guy - Theme B", + "sansEnv": false, + "version": { + "part": "03", + "theme": "B" + }, + "output": "", + "length": 2881 + }, + { + "id": 57, + "input": "Guy - Theme B (no background)", + "sansEnv": true, + "version": { + "part": "03", + "theme": "B" + }, + "output": "", + "length": 2778 + }, + { + "id": 58, + "input": "Guy - Theme C", + "sansEnv": false, + "version": { + "part": "03", + "theme": "C" + }, + "output": "", + "length": 2896 + }, + { + "id": 59, + "input": "Guy - Theme C (no background)", + "sansEnv": true, + "version": { + "part": "03", + "theme": "C" + }, + "output": "", + "length": 2793 + }, + { + "id": 60, + "input": "Country - Theme A", + "sansEnv": false, + "version": { + "part": "04", + "theme": "A" + }, + "output": "", + "length": 3816 + }, + { + "id": 61, + "input": "Country - Theme A (no background)", + "sansEnv": true, + "version": { + "part": "04", + "theme": "A" + }, + "output": "", + "length": 3716 + }, + { + "id": 62, + "input": "Country - Theme B", + "sansEnv": false, + "version": { + "part": "04", + "theme": "B" + }, + "output": "", + "length": 3804 + }, + { + "id": 63, + "input": "Country - Theme B (no background)", + "sansEnv": true, + "version": { + "part": "04", + "theme": "B" + }, + "output": "", + "length": 3701 + }, + { + "id": 64, + "input": "Country - Theme C", + "sansEnv": false, + "version": { + "part": "04", + "theme": "C" + }, + "output": "", + "length": 3825 + }, + { + "id": 65, + "input": "Country - Theme C (no background)", + "sansEnv": true, + "version": { + "part": "04", + "theme": "C" + }, + "output": "", + "length": 3722 + }, + { + "id": 66, + "input": "Geeknot - Theme A", + "sansEnv": false, + "version": { + "part": "05", + "theme": "A" + }, + "output": "", + "length": 4718 + }, + { + "id": 67, + "input": "Geeknot - Theme A (no background)", + "sansEnv": true, + "version": { + "part": "05", + "theme": "A" + }, + "output": "", + "length": 4615 + }, + { + "id": 68, + "input": "Geeknot - Theme B", + "sansEnv": false, + "version": { + "part": "05", + "theme": "B" + }, + "output": "", + "length": 4747 + }, + { + "id": 69, + "input": "Geeknot - Theme B (no background)", + "sansEnv": true, + "version": { + "part": "05", + "theme": "B" + }, + "output": "", + "length": 4644 + }, + { + "id": 70, + "input": "Geeknot - Theme C", + "sansEnv": false, + "version": { + "part": "05", + "theme": "C" + }, + "output": "", + "length": 4715 + }, + { + "id": 71, + "input": "Geeknot - Theme C (no background)", + "sansEnv": true, + "version": { + "part": "05", + "theme": "C" + }, + "output": "", + "length": 4612 + }, + { + "id": 72, + "input": "Asian - Theme A", + "sansEnv": false, + "version": { + "part": "06", + "theme": "A" + }, + "output": "", + "length": 2897 + }, + { + "id": 73, + "input": "Asian - Theme A (no background)", + "sansEnv": true, + "version": { + "part": "06", + "theme": "A" + }, + "output": "", + "length": 2794 + }, + { + "id": 74, + "input": "Asian - Theme B", + "sansEnv": false, + "version": { + "part": "06", + "theme": "B" + }, + "output": "", + "length": 2900 + }, + { + "id": 75, + "input": "Asian - Theme B (no background)", + "sansEnv": true, + "version": { + "part": "06", + "theme": "B" + }, + "output": "", + "length": 2797 + }, + { + "id": 76, + "input": "Asian - Theme C", + "sansEnv": false, + "version": { + "part": "06", + "theme": "C" + }, + "output": "", + "length": 2897 + }, + { + "id": 77, + "input": "Asian - Theme C (no background)", + "sansEnv": true, + "version": { + "part": "06", + "theme": "C" + }, + "output": "", + "length": 2794 + }, + { + "id": 78, + "input": "Punk - Theme A", + "sansEnv": false, + "version": { + "part": "07", + "theme": "A" + }, + "output": "", + "length": 2756 + }, + { + "id": 79, + "input": "Punk - Theme A (no background)", + "sansEnv": true, + "version": { + "part": "07", + "theme": "A" + }, + "output": "", + "length": 2653 + }, + { + "id": 80, + "input": "Punk - Theme B", + "sansEnv": false, + "version": { + "part": "07", + "theme": "B" + }, + "output": "", + "length": 2750 + }, + { + "id": 81, + "input": "Punk - Theme B (no background)", + "sansEnv": true, + "version": { + "part": "07", + "theme": "B" + }, + "output": "", + "length": 2647 + }, + { + "id": 82, + "input": "Punk - Theme C", + "sansEnv": false, + "version": { + "part": "07", + "theme": "C" + }, + "output": "", + "length": 2753 + }, + { + "id": 83, + "input": "Punk - Theme C (no background)", + "sansEnv": true, + "version": { + "part": "07", + "theme": "C" + }, + "output": "", + "length": 2650 + }, + { + "id": 84, + "input": "Afrohair - Theme A", + "sansEnv": false, + "version": { + "part": "08", + "theme": "A" + }, + "output": "", + "length": 4529 + }, + { + "id": 85, + "input": "Afrohair - Theme A (no background)", + "sansEnv": true, + "version": { + "part": "08", + "theme": "A" + }, + "output": "", + "length": 4429 + }, + { + "id": 86, + "input": "Afrohair - Theme B", + "sansEnv": false, + "version": { + "part": "08", + "theme": "B" + }, + "output": "", + "length": 4532 + }, + { + "id": 87, + "input": "Afrohair - Theme B (no background)", + "sansEnv": true, + "version": { + "part": "08", + "theme": "B" + }, + "output": "", + "length": 4429 + }, + { + "id": 88, + "input": "Afrohair - Theme C", + "sansEnv": false, + "version": { + "part": "08", + "theme": "C" + }, + "output": "", + "length": 4529 + }, + { + "id": 89, + "input": "Afrohair - Theme C (no background)", + "sansEnv": true, + "version": { + "part": "08", + "theme": "C" + }, + "output": "", + "length": 4426 + }, + { + "id": 90, + "input": "Normie Female - Theme A", + "sansEnv": false, + "version": { + "part": "09", + "theme": "A" + }, + "output": "", + "length": 3673 + }, + { + "id": 91, + "input": "Normie Female - Theme A (no background)", + "sansEnv": true, + "version": { + "part": "09", + "theme": "A" + }, + "output": "", + "length": 3570 + }, + { + "id": 92, + "input": "Normie Female - Theme B", + "sansEnv": false, + "version": { + "part": "09", + "theme": "B" + }, + "output": "", + "length": 3673 + }, + { + "id": 93, + "input": "Normie Female - Theme B (no background)", + "sansEnv": true, + "version": { + "part": "09", + "theme": "B" + }, + "output": "", + "length": 3570 + }, + { + "id": 94, + "input": "Normie Female - Theme C", + "sansEnv": false, + "version": { + "part": "09", + "theme": "C" + }, + "output": "", + "length": 3670 + }, + { + "id": 95, + "input": "Normie Female - Theme C (no background)", + "sansEnv": true, + "version": { + "part": "09", + "theme": "C" + }, + "output": "", + "length": 3567 + }, + { + "id": 96, + "input": "Older - Theme A", + "sansEnv": false, + "version": { + "part": "10", + "theme": "A" + }, + "output": "", + "length": 3875 + }, + { + "id": 97, + "input": "Older - Theme A (no background)", + "sansEnv": true, + "version": { + "part": "10", + "theme": "A" + }, + "output": "", + "length": 3772 + }, + { + "id": 98, + "input": "Older - Theme B", + "sansEnv": false, + "version": { + "part": "10", + "theme": "B" + }, + "output": "", + "length": 3878 + }, + { + "id": 99, + "input": "Older - Theme B (no background)", + "sansEnv": true, + "version": { + "part": "10", + "theme": "B" + }, + "output": "", + "length": 3775 + }, + { + "id": 100, + "input": "Older - Theme C", + "sansEnv": false, + "version": { + "part": "10", + "theme": "C" + }, + "output": "", + "length": 3872 + }, + { + "id": 101, + "input": "Older - Theme C (no background)", + "sansEnv": true, + "version": { + "part": "10", + "theme": "C" + }, + "output": "", + "length": 3769 + }, + { + "id": 102, + "input": "Firehair - Theme A", + "sansEnv": false, + "version": { + "part": "11", + "theme": "A" + }, + "output": "", + "length": 5690 + }, + { + "id": 103, + "input": "Firehair - Theme A (no background)", + "sansEnv": true, + "version": { + "part": "11", + "theme": "A" + }, + "output": "", + "length": 5587 + }, + { + "id": 104, + "input": "Firehair - Theme B", + "sansEnv": false, + "version": { + "part": "11", + "theme": "B" + }, + "output": "", + "length": 5709 + }, + { + "id": 105, + "input": "Firehair - Theme B (no background)", + "sansEnv": true, + "version": { + "part": "11", + "theme": "B" + }, + "output": "", + "length": 5606 + }, + { + "id": 106, + "input": "Firehair - Theme C", + "sansEnv": false, + "version": { + "part": "11", + "theme": "C" + }, + "output": "", + "length": 5687 + }, + { + "id": 107, + "input": "Firehair - Theme C (no background)", + "sansEnv": true, + "version": { + "part": "11", + "theme": "C" + }, + "output": "", + "length": 5584 + }, + { + "id": 108, + "input": "Blond - Theme A", + "sansEnv": false, + "version": { + "part": "12", + "theme": "A" + }, + "output": "", + "length": 3142 + }, + { + "id": 109, + "input": "Blond - Theme A (no background)", + "sansEnv": true, + "version": { + "part": "12", + "theme": "A" + }, + "output": "", + "length": 3039 + }, + { + "id": 110, + "input": "Blond - Theme B", + "sansEnv": false, + "version": { + "part": "12", + "theme": "B" + }, + "output": "", + "length": 3139 + }, + { + "id": 111, + "input": "Blond - Theme B (no background)", + "sansEnv": true, + "version": { + "part": "12", + "theme": "B" + }, + "output": "", + "length": 3036 + }, + { + "id": 112, + "input": "Blond - Theme C", + "sansEnv": false, + "version": { + "part": "12", + "theme": "C" + }, + "output": "", + "length": 3136 + }, + { + "id": 113, + "input": "Blond - Theme C (no background)", + "sansEnv": true, + "version": { + "part": "12", + "theme": "C" + }, + "output": "", + "length": 3033 + }, + { + "id": 114, + "input": "Ateam - Theme A", + "sansEnv": false, + "version": { + "part": "13", + "theme": "A" + }, + "output": "", + "length": 2833 + }, + { + "id": 115, + "input": "Ateam - Theme A (no background)", + "sansEnv": true, + "version": { + "part": "13", + "theme": "A" + }, + "output": "", + "length": 2730 + }, + { + "id": 116, + "input": "Ateam - Theme B", + "sansEnv": false, + "version": { + "part": "13", + "theme": "B" + }, + "output": "", + "length": 2842 + }, + { + "id": 117, + "input": "Ateam - Theme B (no background)", + "sansEnv": true, + "version": { + "part": "13", + "theme": "B" + }, + "output": "", + "length": 2739 + }, + { + "id": 118, + "input": "Ateam - Theme C", + "sansEnv": false, + "version": { + "part": "13", + "theme": "C" + }, + "output": "", + "length": 2830 + }, + { + "id": 119, + "input": "Ateam - Theme C (no background)", + "sansEnv": true, + "version": { + "part": "13", + "theme": "C" + }, + "output": "", + "length": 2727 + }, + { + "id": 120, + "input": "Rasta - Theme A", + "sansEnv": false, + "version": { + "part": "14", + "theme": "A" + }, + "output": "", + "length": 4695 + }, + { + "id": 121, + "input": "Rasta - Theme A (no background)", + "sansEnv": true, + "version": { + "part": "14", + "theme": "A" + }, + "output": "", + "length": 4592 + }, + { + "id": 122, + "input": "Rasta - Theme B", + "sansEnv": false, + "version": { + "part": "14", + "theme": "B" + }, + "output": "", + "length": 4686 + }, + { + "id": 123, + "input": "Rasta - Theme B (no background)", + "sansEnv": true, + "version": { + "part": "14", + "theme": "B" + }, + "output": "", + "length": 4583 + }, + { + "id": 124, + "input": "Rasta - Theme C", + "sansEnv": false, + "version": { + "part": "14", + "theme": "C" + }, + "output": "", + "length": 4674 + }, + { + "id": 125, + "input": "Rasta - Theme C (no background)", + "sansEnv": true, + "version": { + "part": "14", + "theme": "C" + }, + "output": "", + "length": 4571 + }, + { + "id": 126, + "input": "Street - Theme A", + "sansEnv": false, + "version": { + "part": "15", + "theme": "A" + }, + "output": "", + "length": 4920 + }, + { + "id": 127, + "input": "Street - Theme A (no background)", + "sansEnv": true, + "version": { + "part": "15", + "theme": "A" + }, + "output": "", + "length": 4820 + }, + { + "id": 128, + "input": "Street - Theme B", + "sansEnv": false, + "version": { + "part": "15", + "theme": "B" + }, + "output": "", + "length": 4935 + }, + { + "id": 129, + "input": "Street - Theme B (no background)", + "sansEnv": true, + "version": { + "part": "15", + "theme": "B" + }, + "output": "", + "length": 4832 + }, + { + "id": 130, + "input": "Street - Theme C", + "sansEnv": false, + "version": { + "part": "15", + "theme": "C" + }, + "output": "", + "length": 4914 + }, + { + "id": 131, + "input": "Street - Theme C (no background)", + "sansEnv": true, + "version": { + "part": "15", + "theme": "C" + }, + "output": "", + "length": 4811 + } +] \ No newline at end of file