v1.0.0: cleanup and documentation

This commit is contained in:
2024-03-22 13:05:23 +01:00
parent 56767a0f06
commit c4a238c10c
8 changed files with 199 additions and 145 deletions

View File

@@ -1,15 +1,76 @@
# generative-art # generative-art
An easy-to-use library to generate random images from a set of layers.
To install dependencies: ## Installation
NPM:
```bash ```console
bun install $ npm install layered-generative-art
```
Bun:
```console
$ bun add layered-generative-art
``` ```
To run: ## Example
```ts
// import generate function
import { generateRandomImage } from 'layered-generative-art';
import { writeFileSync } from 'node:fs';
```bash // ...
bun run index.ts
// generate a random character from a fixed base shape, randomly choose between brown or
// blue eyes (blue eyes are 5 times as rare) and an optional hat at a 25% probability
const result: GenerativeArtOutput = generateRandomImage('./assets/base_shape.png', [
{
traitName: 'Eyes',
path: './assets/eyes/',
options: [
{
name: 'Blue',
// this path will be combined with the layer's path (in this case => './assets/eyes/blue_eyes.png')
path: 'blue_eyes.png',
weight: 1
},
{
name: 'Brown',
path: 'brown.png',
// Brown eyes now have 5 times the probability of blue eyes
weight: 5
}
]
},
{
traitName: 'Hat',
path: './assets/hats/',
// Set the probability of a hat occouring to 25%
probability: 0.25,
options: [
{
name: 'Red Beanie',
path: 'red_beanie.png',
},
{
name: 'Green Cap',
path: 'green_cap.png',
}
]
},
]);
// write the newly generated character to a file
writeFileSync('./output/my_character.png', result.buffer);
// log the choices that were taken by the generator
console.log(result.choices);
``` ```
This project was created using `bun init` in bun v1.0.33. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. ## Features
- optional layers based on a probability
- weighted layer options to create rare trait options
## Contributing
Feel free to open pull requests
## License
MIT

8
build.mjs Normal file
View File

@@ -0,0 +1,8 @@
import dts from "bun-plugin-dts";
await Bun.build({
entrypoints: ["./src/index.ts"],
outdir: "./dist",
minify: true,
plugins: [dts()],
});

BIN
bun.lockb

Binary file not shown.

View File

@@ -1,52 +0,0 @@
import sharp, { type OverlayOptions } from 'sharp';
import { LAYERS, weightedChoice } from './layers';
const BASE_SHAPE_PATH = './assets/base_shape.png';
export type Trait = 'skin' | 'nose' | 'eyes' | 'mouth' | 'detail';
export type Layer = {
trait: Trait,
path: string,
chance?: number,
options: TraitOption[]
};
export type TraitOption = {
fileName: string,
weight?: number
};
async function generateRandomImage(fileName: string, orderedLayers: Layer[]) {
const layers: { input: string }[] = [];
for (const layer of orderedLayers) {
if (layer.chance && Math.random() > layer.chance) {
continue;
}
const choice = weightedChoice(layer.options);
layers.push({ input: `${layer.path}${choice}` });
}
const image = sharp(BASE_SHAPE_PATH).composite(layers);
await Bun.write(`./output/${fileName}`, await image.toBuffer());
}
for (let i = 0; i < 100; i++) {
await generateRandomImage(`characters/${i}.png`, LAYERS);
}
// combine into a single grid image
const characters: OverlayOptions[] = [];
for (let i = 0; i < 100; i++) {
characters.push({ input: `./output/characters/${i}.png`, left: (i % 10) * 16, top: Math.floor(i / 10.0) * 16 });
}
const grid = sharp({ create: { width: 160, height: 160, channels: 4, background: { r: 0, g: 0, b: 0, alpha: 0 } } }).composite(characters).png();
await Bun.write('./output/grid.png', await grid.toBuffer());
console.log('Finished generation');

View File

@@ -1,72 +0,0 @@
import type { Layer, TraitOption } from ".";
export const LAYERS: Layer[] = [
{
trait: 'skin',
path: './assets/skin/',
options: [
{ fileName: 'sand.png' },
{ fileName: 'sienna.png' },
{ fileName: 'bole.png' },
{ fileName: 'chocolate.png' }
]
},
{
trait: 'nose',
path: './assets/nose/',
options: [
{ fileName: 'small.png' },
{ fileName: 'big.png' }
]
},
{
trait: 'eyes',
path: './assets/eyes/',
options: [
{ fileName: 'blue.png', weight: 27 },
{ fileName: 'green.png', weight: 9 },
{ fileName: 'brown.png', weight: 19 },
{ fileName: 'dark_brown.png', weight: 45 }
]
},
{
trait: 'mouth',
path: './assets/mouth/',
options: [
{ fileName: 'neutral.png', weight: 8 },
{ fileName: 'happy.png', weight: 4 },
{ fileName: 'smirk.png', weight: 2 },
{ fileName: 'shock.png', weight: 1 }
]
},
{
trait: 'detail',
chance: 0.1,
path: './assets/detail/',
options: [
{ fileName: 'halo.png', weight: 1 },
{ fileName: 'red_beanie.png', weight: 25 },
{ fileName: 'green_beanie.png', weight: 25 }
]
}
];
export function weightedChoice(options: TraitOption[]): string {
let i;
let weights: number[] = [options[0].weight ?? 1];
for (i = 1; i < options.length; i++) {
weights[i] = (options[i].weight ?? 1) + weights[i - 1];
}
const random = Math.random() * weights[weights.length - 1];
for (i = 0; i < weights.length; i++) {
if (weights[i] > random) {
break;
}
}
return options[i].fileName;
}

View File

@@ -1,14 +1,38 @@
{ {
"name": "generative-art", "name": "layered-generative-art",
"module": "index.ts", "description": "An easy-to-use library to generate random images from a set of layers",
"devDependencies": { "version": "1.0.0",
"@types/bun": "latest" "main": "dist/index.js",
}, "types": "dist/index.d.ts",
"peerDependencies": { "author": "409",
"typescript": "^5.0.0" "type": "module",
}, "module": "index.ts",
"type": "module", "license": "MIT",
"dependencies": { "files": [
"sharp": "^0.33.2" "dist"
} ],
} "keywords": [
"generative",
"random",
"art",
"image",
"images",
"generative-art"
],
"homepage": "https://github.com/4-0-9/layered-generative-art",
"repository": {
"type": "git",
"url": "git+https://github.com/4-0-9/layered-generative-art.git"
},
"bugs": "https://github.com/4-0-9/layered-generative-art/issues",
"dependencies": {
"sharp": "^0.33.2"
},
"peerDependencies": {
"typescript": "^5.0.0"
},
"devDependencies": {
"@types/bun": "latest",
"bun-plugin-dts": "^0.2.1"
}
}

60
src/index.ts Normal file
View File

@@ -0,0 +1,60 @@
import sharp, { type OverlayOptions } from 'sharp';
import { type Layer, type TraitOption } from './layers';
/** Generates a random image from a base shape and a set of layers
* @param baseShapePath - The path to the base shape of the image. This image determines the resolution of the image. (set this to a transparent image if you don't want a base shape)
* @param orderedLayers - The layers to choose from in ascending order
* @returns A buffer containing the newly generated image
*/
async function generateRandomImage(baseShapePath: string, orderedLayers: Layer[]): Promise<GenerativeArtOutput> {
const layers: OverlayOptions[] = [];
const choices: Record<string, string> = {};
for (const layer of orderedLayers) {
if (layer.probability && Math.random() > layer.probability) {
continue;
}
const choice = weightedChoice(layer.options);
layers.push({ input: `${layer.path}${choice.fileName}` });
choices[layer.traitName] = choice.name;
}
const image = sharp(baseShapePath).composite(layers);
const buffer = await image.toBuffer();
return { buffer: buffer, choices };
}
// https://stackoverflow.com/questions/43566019/how-to-choose-a-weighted-random-array-element-in-javascript
function weightedChoice(options: TraitOption[]): TraitOption {
let i;
let weights: number[] = [options[0].weight ?? 1];
for (i = 1; i < options.length; i++) {
weights[i] = (options[i].weight ?? 1) + weights[i - 1];
}
const random = Math.random() * weights[weights.length - 1];
for (i = 0; i < weights.length; i++) {
if (weights[i] > random) {
break;
}
}
return options[i];
}
/**
* A piece of randomly generated art
*/
export type GenerativeArtOutput = {
/** The buffer containing the image */
buffer: Buffer,
/** An object mapping traits to the choices' names. If a layer was not used due the probability field it will **not** be present in this object */
choices: Record<string, string>,
};

25
src/layers.ts Normal file
View File

@@ -0,0 +1,25 @@
/**
* A generative art layer
*/
export type Layer = {
/** The trait's name */
traitName: string,
/** The base path (**use trailing slash**, e.g. ./assets/eyes/) */
path: string,
/** The probability of this trait occouring. Range: 0.0-1.0 (e.g. 0.5 for 50%). **Defaults to 1.0** */
probability?: number,
/** The options to randomly pick from. Can optionally contain weights to increase certain options' rarity */
options: TraitOption[]
};
/**
* A trait option with an optional weight
*/
export type TraitOption = {
/** The option's name (e.g: 'Blue') */
name: string,
/** The option's file name (e.g.: 'blue_eyes.png') */
fileName: string,
/** The option's weight. Defaults to 1 */
weight?: number
};