Install Node and Git.
Node is best installed using Node Version Manager.
The project currently requires the following Node and NPM versions:
"engines": {
"node": ">=18",
"npm": ">=9"
}
Install Node.
nvm install v18
To ensure you have the latest NPM:
npm i -g npm@latest
Install Git from the official site.
Clone this repository and install the dependencies:
cd <parent_folder_for_the_generator_builder>
git clone https://github.com/pixelpapercraft/pixel-papercraft-generator-builder generator-builder
cd generator-builder
npm install
Generators are developed using the ReScript programming language.
ReScript development is best done using Visual Studio Code.
After installing VSCode, then install the official ReScript extension rescript-vscode
from the VSCode Extensions panel.
Ensure you are running the correct version of Node:
nvm use
In one terminal, start the ReScript compiler:
npm run res:watch
In another terminal, start the web server:
npm run dev
Then open your browser:
http://localhost:3000
This is a quick example of what a simple generator script might look like.
This will be explained further in the sections below.
// Unique id for the generator
let id = "face-generator"
// Name for the generator
let name = "Face Generator"
// Array of image URLs the generator may use
let images: array<Generator.imageDef> = [
{
id: "Background",
url: Generator.require("./images/Background.png"),
}
]
// Array of texture URLs the generator may use
let textures: array<Generator.textureDef> = [
{
id: "Skin",
url: Generator.require("./textures/Skin.png"),
standardWidth: 64,
standardHeight: 64,
},
]
// The generator script
let script = () => {
Generator.drawImage("Background", (0, 0))
let ox = 74
let oy = 25
Generator.drawTexture(
"Skin",
(16, 8, 8, 8),
(ox + 128, oy + 64, 64, 64),
()
)
}
// Export the generator details
let generator: Generator.generatorDef = {
id: id,
name: name,
images: images,
textures: textures,
script: script,
}
Code for generators is in the src/generators
directory.
You can use any directory structure you like for each generator, but a recommended structure is:
/example
/images
/textures
/Example.res
Each generator should export a property named generator
that has the type Generator.generatorDef
. For example:
let generator: Generator.generatorDef = {
id: id,
name: name,
history: history,
thumbnail: Some(thumbnail),
video: None,
instructions: Some(<Generator.Markdown> {instructions} </Generator.Markdown>),
images: images,
textures: textures,
script: script,
}
These properties are:
Property | Description |
---|---|
id | Unique Id for the generator, used in the URL |
name | Name for the generator |
images | Array of image URLs |
textures | Array of texture image URLs |
script | Generator script |
history | Array of strings, used as the history of updates |
thumbnail | Thumbnail image |
video | Video (optional) |
instructions | Instructions for the generator |
Lastly, add the generator to the src/generators/Generators.res
file.
let generators = [
Example.generator,
]
To contribute to the project, you will need to do a few steps:
Read more about proposing changes on Github.
If you need help, ask in the #generator-help channel on Discord.
Tip: It's best to keep your branches and pull requests small. For example, create a separate branch and pull request for each generator. This helps to get the changes merged into the main project more quickly.
Images are just simple images that can be drawn onto the pages. You cannot draw parts of images, you can only draw the whole image. This makes them very fast to draw. You will typically use images for things like backgrounds and folds.
Textures are used when you want to draw parts of an image onto the page and those parts may be scaled, flipped or rotated, etc. Textures are slow to draw because of the image processing needed.
Images needed by the generator are specified as an array of Generator.imageDef
.
Note that the url
property of the images must be created with Generator.require()
.
let images: array<Generator.imageDef> = [
{
id: "Background",
url: Generator.require("./images/Background.png"),
},
{
id: "Folds",
url: Generator.require("./images/Folds.png"),
},
]
Textures needed by the generator are specified as an array of Generator.textureDef
.
Note that the url
property of the textures must be created with Generator.require()
.
Also note that textures must specify a standardWidth
and standardHeight
. This allows higher resolution versions of those textures to also work.
let textures: array<Generator.textureDef> = [
{
id: "Skin",
url: Generator.require("./textures/Skin.png"),
standardWidth: 64,
standardHeight: 64,
},
]
The script should be specified as the script
property of the generator.
let script = () => {
Generator.drawImage("Background", (0, 0))
}
Use the defineTextureInput
function.
You must also specify some options:
standardWidth
and standardHeight
- the default width and height of the texture. These are required so that higher resolution textures will work.choices
- an array of texture names that may be selected instead of selecting a texture file. Specify an empty array []
if none required.Generator.defineTextureInput(
"Skin",
{
standardWidth: 64,
standardHeight: 64,
choices: ["Steve", "Alex"]
}
)
Use the Generator.drawImage()
function.
To draw an image at position x
= 0
and y
= 0
:
Generator.drawImage("Background", (0, 0))
To draw an image at position x
= 50
and y
= 100
:
Generator.drawImage("Background", (50, 100))
Drawing a texture means that you want to copy a rectangle from the texture onto the page.
To do this, you need to:
For example, suppose we want to copy the face from the following texture onto the page.
This is a rectangle (a square in this case) with the following coordinates and size:
x = 8
y = 8
width = 8
height = 8
In ReScript we will write these coordinates as:
(8, 8, 8, 8)
Next, we need to identify the rectange on the page.
It needs to draw onto the page as a bigger rectangle with the following coordinates and size:
x = 138
y = 89
width = 64
height = 64
In ReScript we write this as:
(138, 89, 64, 64)
Also suppose the name of the texture is Skin
, then to draw this texture onto the page we would write:
Generator.drawTexture(
"Skin",
(8, 8, 8, 8),
(138, 89, 64, 64),
(),
);
When using Generator.drawTexture()
you can rotate textures using the ~rotate
argument.
The ~rotate
argument can be any float
value.
Generator.drawTexture(
"Skin",
(8, 8, 8, 8),
(138, 89, 64, 64),
~rotate=90.0,
(),
);
When using Generator.drawTexture()
you can flip textures using the ~flip
argument.
The ~flip
argument can be either ~flip=#Horizontal
or ~flip=#Vertical
.
Generator.drawTexture(
"Skin",
(8, 8, 8, 8),
(138, 89, 64, 64),
~flip=#Vertical,
(),
);
When drawing textures and the destination shape is different to the source shape it can cause the texture to sometimes appear squashed, which makes the texture look messy. In these cases you can apply a pixelate
option to keep the result looking pixelated.
Generator.drawTexture(
"Skin",
(8, 8, 8, 8), // Square shape
(150, 150, 64, 256), // Rectangle shape
~pixelate=true,
(),
);
When using Generator.drawTexture()
you can blend textures with a color using the ~blend
argument.
This is useful when adding a tint to certain blocks in Minecraft corresponding to a biome.
The ~blend
argument can be either:
#None
meaning no blend.#MultiplyHex(string)
for a hex string, such as #MultiplyHex("#90814D")
.#MultiplyRGB(int, int, int)
for red, green and blue values from 0 to 255, such as #MultiplyRGB(144, 129, 77)
.Generator.drawTexture(
"Skin",
(8, 8, 8, 8),
(138, 89, 64, 64),
~blend=#MultiplyHex("#90814D"),
(),
);
By default the generator gives you one blank page to work with, however some designs may require more than one page.
To specify additional pages you can use the Generator.usePage()
function. You just have to choose a name for each page, which can be any name you like.
// Choose the first page
Generator.usePage("Head and Body");
// Draw images and textures here
// Choose the next page
Generator.usePage("Legs");
// Draw images and textures here
If each page doesn't have a specific purpose, just call them "Page 1", "Page 2", etc.
// Choose the first page
Generator.usePage("Page 1");
// Draw images and textures here
// Then choose the next page
Generator.usePage("Page 2");
// Draw images and textures here
It's sometimes useful to give your user some choices in your generator.
For example, some people want the fold lines, and others don't, or your generator might have list of weapons they can choose from.
Boolean inputs provide a true
or false
choice. They may be used to show or hide certain parts of your generator.
Use Generator.defineBooleanInput()
to create boolean inputs.
Generator.defineBooleanInput("Show Folds", true) // Initially true
Generator.defineBooleanInput("Show Labels", true) // Initially true
To use these values use Generator.getBooleanInputValue()
let showFolds = Generator.getBooleanInputValue("Show Folds");
let showLabels = Generator.getBooleanInputValue("Show Labels");
if (showFolds) {
Generator.drawImage("Folds", (0, 0));
}
if (showLabels) {
Generator.drawImage("Labels", (0, 0));
}
Select variables let your user choose from a list of values that you provide.
Use Generator.defineSelectInput()
to create select inputs.
Generator.defineSelectInput("Weapon", ["None", "Sword", "Crossbow"])
To use these values use Generator.getSelectInputValue()
let weapon = Generator.getSelectInputValue("Weapon");
if weapon === "Sword" {
Generator.drawImage("Sword", (100, 100))
} else if weapon === "Crossbow" {
Generator.drawImage("Crossbow", (100, 100))
}
Or an alternative syntax:
let weapon = Generator.getSelectInputValue("Weapon");
switch weapon {
| "Sword" => Generator.drawImage("Sword", (100, 100))
| "Crossbow" => Generator.drawImage("Crossbow", (100, 100))
| _ => () // All other values, do nothing
}
You can define clickable regions on your pages, and what actions should occur when a region is clicked.
Clickable regions are defined using the Generator.defineRegionInput(region, onClick)
function.
For example, you could toggle a variable using a clickable region.
// Define the input
Generator.defineBooleanInput("Show overlay", false)
// Get the input value
let showOverlay = Generator.getBooleanInputValue("Show overlay")
let region = (0, 0, 100, 100)
let onClick = () => {
// Toggle the input value
Generator.setBooleanInputValue("Show overlay", !showOverlay)
}
// Define the clickable region
Generator.defineRegionInput(region, onClick)
But this could be written more concisely as:
Generator.defineBooleanInput("Show overlay", false)
let showOverlay = Generator.getBooleanInputValue("Show overlay")
Generator.defineRegionInput((0, 0, 100, 100), () => {
Generator.setBooleanInputValue("Show overlay", !showOverlay)
})
You can provide some text instructions and comments with your inputs using Generator.defineText()
.
Generator.defineText("Click body parts to change the texture.")
By default, the generator uses a transparent background. You can fill the background with Generator.fillBackgroundColor()
.
// Fill the background color with red
Generator.fillBackgroundColor("#ff0000")
// Fill the background color with white
Generator.fillBackgroundColorWithWhite()
Note: This does not overwrite any images of textures you've drawn. It just changes the background from transparent to the color you specify.
You can draw straight lines onto the generator canvas. The line color, width, dash pattern, and dash offset can all be customized, but the defaults will make a line that works well for standard tab lines.
// Draw a simple black line starting at (0, 100) and ends at (50, 150)
Generator.drawLine((0, 100), (50, 150), ())
// Draw the same line, but red and with a 4 pixel width
Generator.drawLine((0, 100), (50, 150), ~color="#ff0000", ~width=4, ())
You can also draw standard fold lines with a simpler function:
Generator.drawFoldLine((0, 100), (50, 150))
There are a few functions to get specific pixel colors:
Generator.getTexturePixelColor(textureName, x, y)
Generator.getImagePixelColor(imageName, x, y)
Generator.getCurrentPagePixelColor(x, y)
Generator.getPagePixelColor(pageName, x, y)
For example, using Generator.getTexturePixelColor()
:
If you have a texture named Colors
and you want to get the pixel color at position (0, 0)
then you would call:
let color = Generator.getTexturePixelColor("Colors", 0, 0)
This returns a ReScript Option which may contain a tuple (red, green, blue, alpha)
.
Each value of red
, green
, blue
and alpha
are integers from 0 to 255.
More complete example:
let colorOpt = Generator.getTexturePixelColor("Colors", 0, 0)
let color = switch colorOpt {
| None => "Unknown"
| Some(color) => {
let (r, g, b, a) = color
let r = Belt.Int.toString(r)
let g = Belt.Int.toString(g)
let b = Belt.Int.toString(b)
let a = Belt.Int.toString(a)
`(${r}, ${g}, ${b}, ${a})`
}
}