Introduction

Paperbit is a library to create graphics with WebGL. Our main goal is to let the programmer develop his own creativity.

The library gives graphical functions to draw to the canvas, and some utilities to accelerate development process.

Get Started

To use Paperbit you will need to setup a project and understand the core concepts. Then you can read the documentation freely while using the library.

We also recommend to check out some examples to power up, to get inspiration and to understand the capabilities of the library.

Contributing

Feel free to create a pull request to improve this documentation.

Setup a Project

Paperbit is not restricted to any framework, here we let you choose the one which you are most comfortable with. However, if you don’t have experience with node js, we recommend to use the HTML guide.

  • HTML — Create a new project from the start in a simple, good and fast way.
  • Webpack — Create a new project from the start in a (maybe) over engineered way.
  • React — Integrate Paperbit to an existing react app.

HTML

Setting up Paperbit on a HTML file is very straightforward, you only have to follow these four simple steps:

1. Create a project folder

It can have the name you want, the purpose of the folder is to have a place to put all the files without losing them.

2. Create the HTML file

The HTML is going to tell to the browser what to do. We are going to need the following two tags:

  • <style> — to make the body of the page the size of the screen.
  • <script> — to include your code.

The file can have the name you want, the only requisite is that it should end with .html. After creating the file, fill it with the following code:

<!DOCTYPE html>
<html lang="en">
	<head>
		<meta charset="UTF-8">
		<title>Paperbit</title>
		<style> html, body { width: 100%; height: 100%; margin: 0; } </style>
		<script src="./main.js" type="module" defer></script>
	</head>
	<body></body>
</html>

This is a blank template with the two tags we talk about. If you don’t know HTML don’t worry, you are not going to need to modify it.

3. Create the source file

In HTML code, we told the browser to load a script named main.js, so we need to create the file. Paste the next code to the new main.js file.

import { PaperbitCanvas } from "./paperbit.js"

const canvas = new PaperbitCanvas(document.body)
const { ellipse, mouse } = canvas.api

canvas.api.onDraw = () => {
    ellipse(...mouse.pos, .1)
}

This is a blank Paperbit template. In this script we can observe three different parts:

  • Import the library
import { PaperbitCanvas } from "./paperbit.js"

The first line imports the PaperbitCanvas from the paperbit library, so we will have to download the library (witch will be a single file), and move it to the projects folder.

  • Create the canvas
const canvas = new PaperbitCanvas(document.body)
const { ellipse, mouse } = canvas.api

The next two lines are creating a PaperbitCanvas instance in the body of our page. Then we are extracting the ellipse and mouse properties from the canvas api. We could call canvas.api.ellipse every time we need to draw an ellipse to the canvas, but it’s cleaner to store the function in a constant.

  • Draw to the canvas
canvas.api.onDraw = () => {
    ellipse(...mouse.pos, .1)
}

Now that we have the canvas and a draw function (ellipse), we can set up a draw event. When a new frame is requested the onDraw function will fire and the code will be executed.

In this case we have an ellipse being drawn to the mouse position and with a size of 0.1 units. Note that the size of an ellipse usually is defined by two values (width and height). However, in this case that we omitted the height dimension Paperbit is going to create an ellipse of 0.1 units of width and height, aka a circle of 0.1 units of diameter.

4. Open the HTML file

Finally, we need to open the file to see our app. You can do it by opening it with any browser. You should see a beautiful ellipse following your mouse.

Next Steps

Now we have a place to start coding and develop our ideas. To learn how Paperbit works go to the Core Concepts section, and to power up we recommend to take a took to the How To Continue section.

Webpack

This is a step guide for creating a new webpack project, if you already know how to create one, feel free to go to the Use Paperbit section.

System Requirements

Setup

Install the Dependencies

Open the terminal on a new file, and create the npm project with the command:

npm init -y

Install Paperbit

npm install paperbit 

And finally install the development dependencies

npm install -D webpack webpack-cli webpack-dev-server babel-loader @babel/core

Create the first project files

Now that we have all the dependencies, we have to setup them so they work as we want.

Fist we need a webpack.config.js file to tell webpack what to do:

const path = require('path');

module.exports = {
	mode: 'development',
	
	devtool: 'inline-source-map',
	entry: './src/main.js',
	output: {
		filename: 'index.js',
		path: path.resolve(__dirname, 'public'),
	},
	module: {
		rules: [
			{
				test: /\.ts$/,
				exclude: /node_module/,
				use: 'babel-loader'
			}]
	},
	resolve: {
		extensions: ['.js']
	},
	devServer: {
		port: 80,
		static: { directory: path.resolve('public') },
		client: { logging: 'warn' },
	},
}

In that configuration we tolled to webpack:

  • Our javascript source code is in the src folder, and the entrypoint is the file main.js
  • Bundle the source code in a single file index.js
  • And serve the folder public on http://localhost

Now we have to create the src folder with the main.js file:

console.log("Hello world!")

We also need a index.html file on the public folder, which will tell the web client to load the bundled code:

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>Paperbit</title>
	<style>
		html, body {
			width: 100%;
			height: 100%;
			margin: 0px;
		}
	</style>
	<script src="./index.js" defer></script>
</head>
<body>
</body>
</html>

Serve the web

Now we have all set up, to launch the website you can use this single command:

npx webpack serve

It will serve on the http://localhost website and it will update when you change the source code.

It’s recommended to add the command to the package.json file, inside the “scripts” add the following line

// [...]
// "scripts": {
		"serve": "webpack serve",
// [...]

and now you can serve with the following

npm run serve

Use Paperbit

Finally we have our project setup, now we can use the Paperbit library.

All the javascript code will be inside the src folder, so feel free to add new files when your main gets big. At the moment, we have to initialize the library, so replace the code you have in the entrypoint (main.js) with the following:

import { PaperbitCanvas } from "paperbit"

const canvas = new PaperbitCanvas(document.body)
const { ellipse, mouse } = canvas.api

canvas.api.onDraw = () => {
	ellipse(...mouse.pos, .1)
}

Now serve it (npm run serve) and the fun part begins.

Next Steps

Now we have a place to start coding and develop our ideas. To learn how Paperbit works go to the Core Concepts section, and to power up we recommend to take a took to the How To Continue section.

React

Core Concepts

Paperbit has two distinct parts, the graphics engine and the utilities.

Graphics Engine

PaperbitCanvas

The first thing you will do in a Paperbit project, is to create a PaperbitCanvas object. It is the responsible of creating the canvas. His job is to request frames to the PaperbitAPI. When a frame request is complete, it sends to the gpu all the geometry in a single draw call.

PaperbitAPI

When calling new PaperbitCanvas(...), Paperbit is going to create automatically the a PaperbitAPI linked with the canvas. When using Paperbit the 99% of the time we are going to interact with the PaperbitAPI because it is the responsible of creating the frame data. When the PaperbitCanvas request a frame, PaperbitAPI will fire the needed events (onStart, onDraw, onMouseMove, …) and when all return, it will send back to the PaperbitCanvas all the geometry.

Having this distinction of two classes (canvas and api) let us to create the canvas on the main thread and use the api on a web worker.

Utilities

When using a graphics library, it is always nice to have some common features in our disposal:

The main purpose of the utilities is to avoid reinventing the wheel in each project. It is not necessary to use them, but in some circumstances they are be very helpful.

How To Continue

After creating a project and reading the core concepts, you should have enough background to start developing making use of the reference section.

If you still don’t know how to continue, we recommend to check out some examples and read the [PaperbitAPI] documentation.

Paperbit Constructor

A Paperbit context consist of the PaperbitCanvas, and the PaperbitAPI. When calling new PaperbitCanvas(...) it will also create a PaperbitAPI, but we are not forced to used it (as demonstrated on the Web worker section).

For most user cases, this is the better (and simplest) approach. PaperbitCanvas is creating the canvas at the provided container (document.body), and it is also creating a PaperbitAPI instance already linked with the canvas.

const bit = new PaperbitCanvas(document.body)
const { ellipse, mouse } = bit.api

bit.api.onDraw = () => {
	ellipse(...mouse.pos, .1)
}

Web worker

⚠️ This is still in a very early access, things might change.

If we want to isolate or don’t block the main thead, using a web worker should be a choice to consider.

On the main thread we can create only the canvas and WebGL context. For Paperbit to understand that we only need the canvas, we need to get it’s onFrameRequest event. This way when a frame is requested, we will need to send it to the API.

  • Main Thread Code:
let resolveFrame
onmessage = event => {
	if (event.data.type == "frameResult")
		resolveFrame(event.data.frameData)
}

const onFrameRequest =  frameData => {
	postMessage({ type: "doFrame", frameData })
	return new Promise(r => resolveFrame = r)
}

const paperbit = new PaperbitCanvas(document.body, onFrameRequest)

On the worker side, we can create a PaperbitAPI, it will return the api and a doFrame function. When we receive the frame request we can use the doFrame function, witch will start calling all the events (onStart, onDraw, mouse.onMove, …). When all events return the function will return as well. Finally we only need to send the frameData to the main thread to end the frame.

  • Web Worker Code:
onmessage = event => {
	if (event.data.type == "doFrame")
		postMessage({ type: "frameResult", frameData: doFrame(event.data.frameData)})
}

const [api, doFrame] = new PaperbitAPI()
const { ellipse, mouse } = api

api.onDraw = () => {
	ellipse(...mouse.pos, .1)
}

PaperbitAPI

The graphics API is composed of three components.

As we can see, the only purpose of the API is to create geometry, so understanding who geometry works is understanding who PaperbitAPI also works.

Geometry

When calling any graphical function, geometry is always created. In other words, some triangles are pushed to the frame buffer. Note that ellipses are not made with a polygon with a lot of sides because it wold be very inefficient (more detail on ellipse).

The Canvas Space

The space where geometry moves is more important that the geometry itself. This space is defined by a simple Cartesian coordinate system.

Most graphical programs have the origin on the left and top of the screen. This is caused in the history. Early computers had Cathode Ray Tubes which “draw” the image with a cathode ray from the upper left corner to the lower right.

For our purposes we used a centered origin, and a scale that is not fixed by the screen resolution. This way we don’t have to worry about the different screen sizes. It can be seen in the following screens:

1:1
9:16
16:9

The cross (✖) represents the center of the screen, and it is the origin of our coordinate system. The dashed lines indicates where the x and y coordinate equals 1. This system is very useful to work with different monitor sizes, because it automatically rescales the content.

However, some times we will need to work in pixel units. For that purpose we have frame.pixelSize that indicates the size of a single pixel in the screen. Se more about how to use it on the frame section.

Graphic Functions

For drawing to the canvas we need to create geometry. This functions only request position and size, the color, texture, and other configurations are managed by the State.

  • rect — Draw a rectangle on the canvas from a (x, y) coordinate and a (width, height) size.
  • ellipse — Draw a ellipse on the canvas from a (x, y) coordinate and a (width, height) size.
  • triangle — Draw a triangle on the canvas from three coordinates.
  • triangleStrip — Draw a triangle strip on the canvas from a list of coordinates.
  • line — Draw a line on the canvas from a list of coordinates.

Rectangle

const { rect, mouse, state, frame } = canvas.api
rect(0, 0, 1)
rect(...mouse.pos, 1, .3)
state.rectOrigin = [-1, -1]
rect(0, 0, ...mouse.pos)

Function

rect(x, y, width, height?)
ParameterTypeDefault Value
xnumber-
ynumber-
widthnumber-
heightnumberwidth

State Configuration

The comportment of the function can be modified by editing this state properties:

Ellipse

const { ellipse, state, frame } = canvas.api
ellipse(0, 0, 1)
const t = 2 * frame.time
for (let i = 0; i < 6; ++i) {
	const x = 0.6 * Math.sin(t - i)
	const y = 0.6 * Math.cos(t - i)
	ellipse(x, y, 0.6 - i / 10)
}
ellipse(0, 0, 1 + 2 * frame.pixelSize)
state.color = 1
ellipse(0, 0, 1)

Function

ellipse(x, y, width, height?)
ParameterTypeDefault Value
xnumber-
ynumber-
widthnumber-
heightnumberwidth

State Configuration

The comportment of the function can be modified by editing this state property:

Triangle

const { triangle, mouse, state, frame } = canvas.api
triangle([0, -0.8], [-1, 0.8], [1, 0.8])
triangle([-1, 0.5], [1, 0.5], [0, -1])
triangle([-1, -0.5], [1, -0.5], [0, 1])
const topL = [-frame.size[0], frame.size[1]]
const topR = [frame.size[0], frame.size[1]]
triangle(topL, topR, mouse.pos)

const bottomL = [-frame.size[0], -frame.size[1]]
const bottomR = [frame.size[0], -frame.size[1]]
triangle(bottomL, bottomR, mouse.pos)

Function

triangle(vertexA, vertexB, vertexC)
ParameterTypeDefault Value
vertexA[number, number, number = 0]-
vertexB[number, number, number = 0]-
vertexC[number, number, number = 0]-

State Configuration

The comportment of the function can be modified by editing this state property:

Triangle Strip

const { triangleStrip, mouse, state, frame } = canvas.api
triangleStrip([
	[0, 0.5],
	[-1, -0.5],
	[1, -0.5],
	[0, -1]
])
const mesh = []
const step = 0.5 + Math.sin(frame.time) * .5
for (let angle = 0; angle < 4; angle += step) {
	mesh.push([
		Math.cos(angle), 
		Math.sin(angle)* 2 - 1
	])
	mesh.push([
		Math.cos(angle) * 0.5, 
		Math.sin(angle) * 1 - 1
	])
}
triangleStrip(mesh)

Function

triangleStrip(vertices)
ParameterTypeDefault Value
vertices[number, number, number = 0][]-

State Configuration

The comportment of the function can be modified by editing this state property:

Line

State

We can customize the geometry and modify the graphics functions comportment by editing the state. The State can be classified with two different sections:

Get and Set

Color

  • color — Get and set the color of the geometry [r, g, b, a] with floats between 0 to 1.
  • colorHex — Get and set the color with a single hexadecimal number (0xFF00FF).
  • colorAlpha — Get and set the alpha component of the color (the transparency).

Texture

  • texture — Get and set the texture used in the geometry.
  • textureColorBlend — Get and set the blend formula to blend the color with the texture.

Rect

  • rectOrigin — Get and set the origin of the rect.
  • rectUV — Get and set the texture coordinates.

Text

  • textOrigin — Get and set the origin of the text.
  • font — Get and set the font used to draw text.

Line

  • lineWidth — Get and set the line width.
  • lineCap — Get and set the type of the line end (round, square, …).
  • lineJoin — Get and set the type of the joints which connect line segments (round, square, …).

Matrix

  • matrix — Get and set the transformation matrix used in the geometry.
  • inverseMatrix — Get the inverse of the transformation matrix.

Modifiers

Conserve the State

  • scope — Create a scope for the state.

Spatial transformation

  • translate — Move the geometry.
  • rotate — Rotate the geometry with any axis.
  • rotateX — Rotate the geometry with X axis.
  • rotateY — Rotate the geometry with Y axis.
  • rotateZ — Rotate the geometry with Z axis.
  • scale — Scale the geometry.

Canvas Data

Mouse

Keyboard

Frame

Events

Finally, the api manages events so we can execute the code when it is required. There are

  • onStart — Fires after the first frame.
  • onDraw — Fires every frame.

Mouse

The api.mouse manages their own events, but instead of using them we could check the mouse data every frame.

Keyboard

The api.keyboard functions the same way that the mouse. It manages their own events, but instead of using them we could check the keyboard data every frame.

  • onDown — Fires when a key is pressed, ‘ó’ would create two events (‘´’ and ‘o’).
  • onUp — Fires when a key is released, ‘ó’ would create two events (‘´’ and ‘o’).
  • onType — Fires a letter when it is typed, ‘ó’ would create a single event.

Frame

The api.frame functions the same way that the mouse and keyboard. It manages their own events, but instead of using them we could check the frame data every frame.

  • onResize — Fires when the canvas size has been resized.

Vector

Import

import { vec } from "paperbit"

Content

MethodArguments
equalv, u, tolerance = 1e-9
addv, ...vectors
subv, ...vectors
multv, ...vectors
divv, ...vectors
dotv, u
roundv
floorv
ceilv
lengthv
distancea, b
normalizev
resizev, newSize

Examples

const { ellipse, rect, state, mouse, frame } = canvas.api
ellipse(...frame.size, 2)
ellipse(...vec.mult(-1, frame.size), 2)
ellipse(...vec.mult([1, -1], frame.size), 2)
ellipse(...vec.mult([-1, 1], frame.size), 2)
ellipse(0, 0, 2)
state.color = 1
ellipse(...vec.resize(mouse.pos, .4), 1)
ellipse(0, 0, 2)
state.color = 1
const len = Math.min(.4, vec.length(mouse.pos))
ellipse(...vec.resize(mouse.pos, len), 1)
const p = vec.mult(2, mouse.pos)
rect(...vec.div(vec.round(p), 2), .5)
ellipse(0, 0, 2 * vec.length(mouse.pos))
const a = [-1.2, -0.7]
const b = [1, 0.5]

state.colorAlpha = .5

ellipse(...a, 2 * vec.distance(a, mouse.pos))
ellipse(...b, 2 * vec.distance(b, mouse.pos))

state.color = [.8, .2, .2]
ellipse(...mouse.pos, .1)
const pos = [-.2, .1]
const size = 1.3

if (!vec.equal(mouse.pos, pos, size/2))
	rect(...pos, size)
const pos = [-.2, .1]
const size = [1.6, 0.6]

if (vec.equal(mouse.pos, pos, vec.div(size, 2)))
	state.color = [.8, .2, .2]

rect(...pos, ...size)

Matrix

Geometry

SmoothBit