Tutorials Hello Color World: p5.strands

Hello Color World: p5.strands

By ilithya

3 Ways to Work with Color in Shaders

Let’s discover a few techniques for manipulating color using p5.strands, and learn a bit of fragment shader fundamentals in the process.

We’ll explore color in shaders to enhance our p5.js sketches with more sophisticated effects that JavaScript alone wouldn’t let us do.

What is p5.strands?

A new way of writing shaders using JavaScript in p5.js version 2.

To understand what advantages p5.strands brings to us when dealing with shaders, we’ll look at each technique through p5.js examples. One sketch will show an approach in p5.strands, while another will compare it with GLSL, a popular language for writing shaders in the browser. Using p5.strands, instead of GLSL, can be practical in your p5.js sketches if you prefer to use familiar p5.js syntax and type less code.

Each technique will present a few ideas for working with p5.js syntax that you can try in the p5.strands sketches. This can help you learn how to combine them.

Here’s an overview with a skip link to every technique we’ll cover:

  1. Solid colors
  2. Textured colors
  3. Image color filters

You can skip to the conclusion as well, if you prefer to read from end to beginning.

What is a shader?

A set of instructions implementing algorithms that control how a mesh is rendered into pixels on the screen. It runs on the GPU. In the browser when working with WebGL, it’s written in GLSL and there are two types: vertex and fragment.

Although GLSL can be used alongside JavaScript in a p5.js sketch, it is a separate shader language that shaders require. With p5.strands you can focus only on JavaScript, given that the GLSL is handled by p5.js.

Let’s start from simple to complex, like having a three-course meal, by first dipping our toes into an appetizer.

1. Solid Colors

We’ll begin by simply painting the canvas of our sketch and one 3D primitive.

What is pixel density?

The number of pixels packed into a given area of a computer display. Screens with smaller pixels (e.g. Retina displays) have a higher pixel density and create sharper images.

Because all of our sketches will be running shaders, the p5.js built-in function pixelDensity(1) is always implemented inside the setup(). We set the pixel density to 1, making the shaders run faster by reducing the number of pixels they need to process, since the default is often 2 on high-density displays.

What is a fragment shader?

A shader that sets the color of each pixel on the screen by outputting RGBA values, each ranging from 0.0 to 1.0.

With p5.strands we can modify the color of a shader while benefiting from several color notations and models that we already know from p5.js, since it translates them for us into the RGB representation used by GLSL with hooks (e.g. finalColor).

These hooks allow us to speak GLSL when we use them with familiar p5.js syntax. The following table shows a p5.strands equivalent:

GLSL (fragment)p5.strands
gl_FragColor = valhook.set(val)
Try this!

We can use p5.js built-in functions like color() to paint with the shader:

  • named color, color('blue')
  • hex color value, color('#0000ff')
  • hsl values, color('hsl(40, 100%, 50%)')
  • RGB 8-bit values, color(0, 255, 0)

sketch with GLSL

sketch with p5.strands

2. Textured Colors

We’ll continue evolving our canvas and 3D primitive sketch as we learn more techniques to color with our fragment shaders. Let’s add a colored texture to them.

What is a p5.strands hook?

An access point into a shader that connects p5.js with GLSL to modify its behavior.

With p5.strands we can apply randomness to a color equation, creating visually smooth organic textures like clouds, as well as rougher textures found in crystals, by using the noise() p5.js built-in function with a hook (e.g. pixelInputs) instead of implementing GLSL algorithms from scratch.

We can also add the time elapsed with millis() to our textures to simulate natural-looking movements. This value becomes a GLSL uniform, useful to adjust the shader.

What is a GLSL uniform?

A global variable that allows the CPU (JavaScript) and GPU (GLSL) to communicate. The shader cannot change it, only read its value to control a specific feature, such as size or motion. It’s set and updated in JavaScript.

p5.strands lets us set shader uniforms with p5.js elements we know, providing a bridge between JavaScript and GLSL.

Here are other p5.strands syntax examples we’ll use, and their GLSL translations:

GLSL (fragment)p5.strands
varying vec2 uvhook.texCoord
gl_FragColorhook.color
Try this!

Experiment with different inputs for the cloud texture:

  • static, noise(uv);
  • animated, noise(uv + millis() * 0.00015);
  • horizontal mouse movement, noise(uv + mouseX / width);
  • vertical mouse movement, noise(uv + mouseY / height);

sketch with GLSL

sketch with p5.strands

3. Image Color Filters

Let’s add an image to our canvas and 3D primitive and learn how we can create a color filter with adjustments in the fragment shader. To simplify things, we’ll use the same image for both.

What is chromatic aberration?

It’s a color distortion that occurs when the colors of light don’t focus on the same point. The RGB channels appear to be separated, making the colors of an image appear shifted.

With p5.strands we can customize an image filter to our liking and create a chromatic aberration effect by using p5.js elements with a hook (e.g. filterColor). We can displace the image colors vertically and horizontally with the p5.js built-in variables mouseX and mouseY, which are translated into GLSL uniforms to control the shader.

We’ll add these p5.strands vs. GLSL translations to our knowledge base:

GLSL (fragment)p5.strands
uniform sampler2D u_texturehook.canvasContent
uniform sampler2D u_textureuniformTexture(texture)
texture2D(u_texture, uv)getTexture(texture, uv)
Try this!

Explore different offsets in both axes (x, y) for the image displacement:

  • animated, noise(uv + millis() * 0.00025)
  • linear animation, sin(millis() * 0.001) * 0.5
  • centered horizontal mouse movement, (0.5 - mouseX / width) * 0.5
  • centered vertical mouse movement, (0.5 - mouseY / height) * 0.5

sketch with GLSL

sketch with p5.strands

Wrapping Up

We learned three ways to work with color in fragment shaders with p5.strands, which can be practical in your p5.js sketches, as you can keep on writing JavaScript using familiar p5.js syntax and type less code.

Instead of this:

// p5.js with GLSL
let vertex = `
precision highp float;

attribute vec3 aPosition;

uniform mat4 uModelViewMatrix;
uniform mat4 uProjectionMatrix;

void main() {
  gl_Position = uProjectionMatrix * uModelViewMatrix * vec4(aPosition, 1.0);
}
`;

let fragment = `
precision highp float;

void main() {
  vec3 color = vec3(0.0, 0.0, 1.0);

  gl_FragColor = vec4(color, 1.0);
}
`;

We can do this:

// p5.js with p5.strands
function fragmentColor() {
  finalColor.begin();

  finalColor.set(color('blue'));

  finalColor.end();
} 
When should I use p5.strands?

It might be ideal for certain shader techniques or features where you want to avoid writing the GLSL needed from scratch, and instead use p5.strands hooks alongside p5.js built-in functions and variables.

If instead of being comfortable with p5.js, you come from GLSL, p5.strands also enables you to use GLSL built-in functions, as for example step() and mix(). Check out our list of supported GLSL built-ins. This list is also part of the interactive p5.strands reference.

Even when we’re in the p5.js ecosystem, learning some GLSL fundamentals might unlock future possibilities to additionally work with other web-based creative coding tools that offer more advanced shader capabilities with equally friendly communities, including Three.js and shadertoy.

You can continue exploring color in shaders with p5.strands using more p5.js elements we didn’t cover in our examples, such as random().

Try this!

Feel free to submit your p5.strands explorations here:

p5.strands is in active development by the community. If you want to get involve, join our discord server, we’ll be happy to meet you!