v1.0.0: cleanup and documentation
This commit is contained in:
77
README.md
77
README.md
@@ -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
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",
|
"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
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