Mastering the Glow Effect in Processing and p5.js

Achieving a convincing glow effect in creative coding requires a specific approach.

Glow effect in 'Processing/p5.js'.

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.

[Japanese version / 日本語版はこちら]

 

 

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

The three primary colors of light.

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);
}

Circles of red, green, and blue color.

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

The three primary colors of light.

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);
    }
}

Creating the effect "Oh! It's glowing!".

It's an animation of drawing over and over.

 

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.

Glowing with the saturation = 90.
Glowing with the saturation = 60.
Glowing with the saturation = 30.

This is the animation example of the blue and bluish-purple circles. It changes the saturation value of the bluish-purple circle only.

 

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);

}

 

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:

  • ADD: Intense, brilliant, and prone to "burning out" to white (great for sparks or high-energy light).
  • SCREEN: A gentler, more controlled glow that preserves more detail.
  • Glowing with the blendMode(ADD).
    Glowing with the blendMode(SCREEN).

     

    Recap: Best Practices for Glow Effects

    • Use blendMode(ADD) or SCREEN.
    • Layer multiple shapes with low brightness.
    • Switch to BLEND mode 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/>
    */
    
    
    

     

    Next Post Previous Post
    No Comment
    Add Comment
    comment url