Mastering the Glow Effect in Processing and p5.js
Achieving a convincing glow effect in creative coding requires a specific approach.
In this post, I will break down the techniques for creating light using p5.js for the core concepts and Processing for more advanced animations.
Understanding Light on Digital Screens
Strictly speaking, every pixel on your screen is already emitting light.
Since computer displays use the RGB color model, any color you output is technically "shining."
Red, Green, and Blue = RGB
https://en.wikipedia.org/wiki/RGB_color_model
However, simply drawing a color doesn't always look like it's glowing to the human eye.
Creating the Glow
So, how can we make something on the screen appear to glow?
One of the most effective ways to simulate light is by using blendMode(ADD). As the name suggests, this mode adds color values together, mimicking how physical light behaves.
For example, this code draws circles of red, green, and blue color.
const w = 640;
const h = w;
function setup() {
createCanvas(w, h);
colorMode(HSB, 360, 100, 100, 100);
noLoop();
background(0, 0, 0, 100);
noStroke();
// Red
fill(0, 90, 30, 100);
circle(w * 0.4, h * 0.4, w * 0.5);
// Green
fill(120, 90, 30, 100);
circle(w * 0.6, h * 0.4, w * 0.5);
// Blue
fill(240, 90, 30, 100);
circle(w * 0.5, h * 0.6, w * 0.5);
}
This produces a standard result.
By calling blendMode(ADD) before drawing, it does the addition of red, green, and blue colors. And the result is the picture of three primary colors of light I showed before.
background(0, 0, 0, 100);
noStroke();
blendMode(ADD);
// Red
Red + Green = Yellow, Red + Green + Blue = White
By layering multiple shapes with low brightness, the color values accumulate and brighten, creating a radiant, luminous look.
const w = 640;
const h = w;
function setup() {
createCanvas(w, h);
colorMode(HSB, 360, 100, 100, 100);
noLoop();
frameRate(15);
background(0, 0, 0, 100);
noStroke();
blendMode(ADD);
for (let r = 0.0; r < 0.5; r += 0.01) {
// Red
fill(0, 90, 5, 100);
circle(w * 0.4, h * 0.4, w * r);
// Green
fill(120, 90, 5, 100);
circle(w * 0.6, h * 0.4, w * r);
// Blue
fill(240, 90, 5, 100);
circle(w * 0.5, h * 0.6, w * r);
}
}
It's an animation of drawing over and over.
光の三原色に灼かれる悦びを噛み締めよ!🌞#p5js #creativecoding pic.twitter.com/wwuHpSGYAU
— deconbatch (@deconbatch) January 10, 2022
Optimizing the Effect with HSB
When layering light, fine-tuning Hue, Saturation, and Brightness is crucial. Since the default RGB mode makes it difficult to control these independently, I highly recommend switching to HSB mode.
colorMode(HSB, 360, 100, 100, 100);
For example, it looks different even you just change the saturation value.
This is the animation example of the blue and bluish-purple circles. It changes the saturation value of the bluish-purple circle only.
これはアニメーションではありません。
— deconbatch (@deconbatch) January 10, 2022
今、あなたのモニターが焼きついているところです。😈#p5js #creativecoding pic.twitter.com/lOLcCCgrHw
Managing Blend Modes: 100 + 0 = 100
Since ADD is purely additive, "painting" with black (brightness = 0) has no effect—it's like adding zero. If you need to clear the screen or draw a dark background every frame, remember to switch back to blendMode(BLEND). Forgetting this step often leads to the screen quickly turning completely white (whiting out).
I'll give an example of changing saturation value and switching the blend mode. You can see how the effect works when you comment out the 'blendMode(BLEND)'.
const w = 640;
const h = w;
function setup() {
createCanvas(w, h);
colorMode(HSB, 360, 100, 100, 100);
}
function draw() {
let frmRatio = map(frameCount % 120, 0, 120, 1.0, 0.0);
blendMode(BLEND);
background(240, 100, 30, 100);
noStroke();
// sun
blendMode(ADD);
for (let r = 0.0; r < 1.0; r += 0.01) {
fill(280, frmRatio * 100, (1.0 - r) * 5, 100);
circle(w * 0.6, h * 0.5, w * r);
}
// planet
blendMode(BLEND);
fill(240, 100, frmRatio * 80, 100);
circle(w * 0.5, h, w * 0.8);
}
時は来たれり👼#p5js #creativecoding pic.twitter.com/sMJV909RYo
— deconbatch (@deconbatch) January 10, 2022
ADD vs. SCREEN: Choosing Your Light
There is the 'blendMode(SCREEN)' similar to the 'blendMode(ADD)'. While both modes are useful for light effects, they offer different visual qualities:
Recap: Best Practices for Glow Effects
- Use
blendMode(ADD)orSCREEN. - Layer multiple shapes with low brightness.
- Switch to
BLENDmode when clearing the background. - Experiment with HSB values to tune the "temperature" of your light.
The Example Code of p5.js
I wrote an example code of a glowing animation using 'blendMode(SCREEN)' in p5.js (JavaScript). The title is 'City lights'. Please run it and enjoy how it shows.
Please feel free to use this example code under the terms of the GPL.
// City Lights
const w = 720;
const h = 480;
const blobNum = 20;
function setup() {
createCanvas(w, h);
colorMode(HSB, 360, 100, 100, 100);
frameRate(15);
}
function draw() {
let frmRatio = map(frameCount % 120, 0, 120, 0, 1);
blendMode(BLEND);
background(240, 100, 20, 100);
for (let i = 0; i < blobNum; i++) {
let bTime = sin(PI * ((frmRatio + noise(10, i)) % 1));
let bHue = (360 * frmRatio + noise(20, i) * 240) % 360;
blob(i / blobNum, noise(30, i), bTime, bHue);
}
}
function blob(_x, _y, _t, _hue) {
blendMode(SCREEN);
noStroke();
for (let r = 0.0; r < 0.2; r += 0.002) {
fill(_hue, 100, r * 3, 100);
circle(_x * w, _y * h, w * r * 0.5);
fill(_hue, _t * 100, (1.0 - r) * 3, 100);
circle(_x * w, _y * h, w * r);
}
}
/*
Copyright (C) 2022- deconbatch
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>
*/
The Example Code of Processing.
This example code shows sparkling lights rather than glowing. Please feel free to use this example code under the terms of the GPL.
/**
* Light Years.
* simple animation using Node-Garden technique.
*
* @author @deconbatch
* @version 0.1
* @license GPL Version 3 http://www.gnu.org/licenses/
* Processing 3.5.3
* 2022.01.15
*/
void setup() {
size(720, 480, P2D);
colorMode(HSB, 360.0, 100.0, 100.0, 100.0);
smooth();
noLoop();
}
void draw() {
int frmRate = 30;
int cycleSec = 8;
int cycleMax = 3;
int nodeNum = 15;
float orbitBase = min(width, height) * 0.25;
float rangeS = orbitBase * 0.2;
float rangeL = orbitBase * 0.4;
float hueBase = random(360.0);
// nodes clusters
ArrayList<Cluster> clusters = getClusters(nodeNum, orbitBase);
int clusterMax = clusters.size();
// nodes
ArrayList<Node> nodes = getNodes(clusterMax * nodeNum, orbitBase);
int nodeMax = nodes.size();
// easing functions
ArrayList<Ease> easing = new ArrayList<Ease>();
easing.add(new Four());
easing.add(new Quadratic());
easing.add(new Cos());
easing.add(new Pow());
easing.add(new Cubic());
int easeMax = easing.size();
int frmMax = frmRate * cycleSec * cycleMax;
int frmCycleMax = frmRate * cycleSec;
for (int frmCnt = 0; frmCnt < frmMax; frmCnt++) {
int cycleCnt = frmCnt / frmCycleMax;
float frmRatio = map(frmCnt % frmCycleMax, 0, frmCycleMax, 0.0, 1.0);
// animate calculation
for (int clusterCnt = 0; clusterCnt < clusterMax; clusterCnt++) {
float waveRatio = easing.get((cycleCnt + clusterCnt) % easeMax).ease(frmRatio);
Cluster cluster = clusters.get(clusterCnt);
for (int idx = cluster.nodeFrom; idx < cluster.nodeTo; idx++) {
nodes.get(idx).animate(waveRatio, frmRatio, cluster.x, cluster.y);
}
}
blendMode(BLEND);
background(0.0, 0.0, 0.0, 100.0);
// draw
blendMode(ADD);
for (int clusterCnt = 0; clusterCnt < clusterMax; clusterCnt++) {
float clusterRatio = map(clusterCnt, 0, clusterMax, 0.0, 1.0);
float rangeWave = abs(sin(PI * (frmRatio + clusterRatio)));
// lines between clusters
Cluster cluster = clusters.get(clusterCnt);
stroke(cluster.hueVal % 360.0, 90.0, 30.0, 100.0 * rangeWave);
for (Cluster c : clusters) {
float d = dist(cluster.x, cluster.y, c.x, c.y);
strokeWeight(d / orbitBase);
if (d < orbitBase * 1.5) {
line(cluster.x, cluster.y, c.x, c.y);
}
}
for (int i = cluster.nodeFrom; i < cluster.nodeTo; i++) {
Node n = nodes.get(i);
// nodes
noStroke();
fill(cluster.hueVal % 360.0, 80.0, 70.0, 100.0);
ellipse(n.x, n.y, 3.0, 3.0);
// lines between nodes
for (int j = i + 1; j < nodeMax; j++) {
Node m = nodes.get(j);
float d = dist(n.x, n.y, m.x, m.y);
if (d < rangeL * rangeWave && d > rangeS * rangeWave) {
stroke(cluster.hueVal % 360.0, 80.0, 40.0, 100.0);
strokeWeight(2);
line(n.x, n.y, m.x, m.y);
noStroke();
fill((cluster.hueVal + 300.0) % 360.0, 40.0, 10.0, 100.0);
ellipse(n.x, n.y, 8.0, 8.0);
fill((cluster.hueVal + 30.0 + 60.0 * (d - rangeS) / (rangeL - rangeS)) % 360.0, 40.0, 3.0, 100.0);
ellipse(m.x, m.y, 15.0, 15.0);
}
}
}
}
saveFrame("frames/" + String.format("%04d", frmCnt) + ".png");
}
exit();
}
/**
* getClusters : returns whole clusters.
*/
ArrayList<Cluster> getClusters(int _nodeNum, float _radius) {
int tryMax = 100;
float spacing = _radius * 0.75;
float hueBase = random(360.0);
ArrayList<Cluster> clusters = new ArrayList<Cluster>();
// circle packing
int cnt = 0;
for (int i = 0; i < tryMax; i++) {
int x = floor(random(spacing, width) - spacing * 0.5);
int y = floor(random(spacing, height) - spacing * 0.5);
boolean hit = false;
for (Cluster c : clusters) {
float d = dist(x, y, c.x, c.y);
if (d < spacing) {
hit = true;
break;
}
}
if (!hit) {
clusters.add(new Cluster(cnt++, _nodeNum, x, y, hueBase + cnt * 90.0));
}
}
return clusters;
}
/**
* getNodes : returns whole nodes.
*/
ArrayList<Node> getNodes(int _cnt, float _radius) {
ArrayList<Node> nodes = new ArrayList<Node>();
for (int i = 0; i < _cnt; i++) {
float r = random(_radius);
float t = random(TWO_PI);
nodes.add(new Node(
r,
t
));
}
return nodes;
}
/**
* Node : hold node.
*/
public class Node {
public float x, y; // coordinate of node
private float r, t; // radius and theta to calculate the x, y
private float oR, oT; // original radius and theta
private float tPhase; // random phase of theta
Node(float _oR, float _oT) {
oR = _oR;
oT = _oT;
tPhase = random(PI);
}
public void animate(float _rRatio, float _tRatio, float _oX, float _oY) {
r = oR * abs(sin(TWO_PI * _rRatio + tPhase));
t = oT + TWO_PI * ((_tRatio + sin(tPhase)) % 1.0);
x = _oX + r * cos(t);
y = _oY + r * sin(t);
}
}
/**
* Cluster : hold cluster.
*/
public class Cluster {
public int nodeFrom, nodeTo; // number of nodes belong to the cluster
public float x, y; // coordinate of node
public float hueVal; // hue value of node
Cluster(int _no, int _nodeNum, float _x, float _y, float _hue) {
nodeFrom = _no * _nodeNum;
nodeTo = nodeFrom + _nodeNum - 1;
x = _x;
y = _y;
hueVal = _hue;
}
}
/**
* Ease : hold easing functions.
*/
public interface Ease {
public float ease(float _t);
}
public class Cos implements Ease {
public float ease(float _t) {
return 1.0 - cos(HALF_PI * _t);
}
}
public class Quadratic implements Ease {
public float ease(float _t) {
_t *= 2.0;
if (_t < 1.0) {
return pow(_t, 2) / 2.0;
}
_t -= 1.0;
return -(_t * (_t - 2) - 1.0) / 2.0;
}
}
public class Cubic implements Ease {
public float ease(float _t) {
_t *= 2.0;
if (_t < 1.0) {
return pow(_t, 3) / 2.0;
}
_t -= 2.0;
return (pow(_t, 3) + 2.0) / 2.0;
}
}
public class Four implements Ease {
public float ease(float _t) {
return 1.0 - pow(1.0 - _t, 4);
}
}
public class Pow implements Ease {
public float ease(float _t) {
return pow(_t, 2);
}
}
/*
Copyright (C) 2022- deconbatch
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>
*/