v1.0.0: cleanup and documentation
This commit is contained in:
77
README.md
77
README.md
@@ -1,15 +1,76 @@
|
||||
# generative-art
|
||||
An easy-to-use library to generate random images from a set of layers.
|
||||
|
||||
To install dependencies:
|
||||
|
||||
```bash
|
||||
bun install
|
||||
## Installation
|
||||
NPM:
|
||||
```console
|
||||
$ 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
8
build.mjs
Normal file
@@ -0,0 +1,8 @@
|
||||
import dts from "bun-plugin-dts";
|
||||
|
||||
await Bun.build({
|
||||
entrypoints: ["./src/index.ts"],
|
||||
outdir: "./dist",
|
||||
minify: true,
|
||||
plugins: [dts()],
|
||||
});
|
||||
52
index.ts
52
index.ts
@@ -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');
|
||||
72
layers.ts
72
layers.ts
@@ -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;
|
||||
}
|
||||
50
package.json
50
package.json
@@ -1,14 +1,38 @@
|
||||
{
|
||||
"name": "generative-art",
|
||||
"module": "index.ts",
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"sharp": "^0.33.2"
|
||||
}
|
||||
}
|
||||
"name": "layered-generative-art",
|
||||
"description": "An easy-to-use library to generate random images from a set of layers",
|
||||
"version": "1.0.0",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"author": "409",
|
||||
"type": "module",
|
||||
"module": "index.ts",
|
||||
"license": "MIT",
|
||||
"files": [
|
||||
"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
60
src/index.ts
Normal 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
25
src/layers.ts
Normal 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
|
||||
};
|
||||
Reference in New Issue
Block a user