Mike Andrews
Mike Andrews

Reputation: 3206

Actively drawing "under" a translucent area in p5.js

I ran into a puzzling problem when I was developing a larger p5.js sketch, and my solution is not sitting right with me. So, I've boiled it down to this (admittedly lame) sketch.

This p5.js sketch draws several randomly sized and colored dots each frame. In the center of the canvas is a translucent blue filled rectangle that appears to be between the viewer and the dots. It's the translucent blue area that is the problem. The sketch works, but I can't help but think there's a better way to implement the translucency.

var cells;
var cellsz = 10;
var wid, hgt;

function setup()
{
  wid = floor(windowWidth / cellsz);
  hgt = floor(windowHeight / cellsz);
  createCanvas(windowWidth, windowHeight);
  frameRate(15);

  cells = new Array(wid);

  for (x = 0; x < wid; x++) {
    cells[x] = new Array(hgt);
    for (y = 0; y < hgt; y++) {
      cells[x][y] = false;
    }
  }
}

function cell_draw(c)
{
  strokeWeight(1);
  stroke(c.r, c.g, c.b);
  fill(c.r, c.g, c.b);
  ellipse(c.x, c.y, c.w, c.w);
}

function cell_new()
{
  var x = int(floor(random(wid)));
  var y = int(floor(random(hgt)));

  var c = {
    x: x * cellsz,
    y: y * cellsz,
    w: random(cellsz * 2),
    r: floor(random(256)),
    g: floor(random(256)),
    b: floor(random(256))
  };

  cells[x][y] = c;
  cell_draw(c);
}

// draw a translucent blue filled rectangle in the center of the window
function overlay()
{
  strokeWeight(1);
  stroke(0, 0, 255, 75);
  fill(0, 0, 255, 75);
  var w = windowWidth / 4;
  var h = windowHeight / 4;
  rect(w, h, w * 2, h * 2);
}

// erase what's in the center of the window, then redraw the underlying cells
function underlay()
{
  strokeWeight(1);
  stroke(255);
  fill(255);
  var w = windowWidth / 4;
  var h = windowHeight / 4;
  rect(w, h, w * 2, h * 2);

  var x0 = floor((w / cellsz) - 2);
  var y0 = floor((h / cellsz) - 2);
  var x1 = floor(x0 + 3 + ((w * 2) / cellsz));
  var y1 = ceil(y0 + 3 + ((h * 2) / cellsz));

  for (x = x0; x <= x1; x++) {
    for (y = y0; y <= y1; y++) {
      if (cells[x][y]) {
        cell_draw(cells[x][y]);
      }
    }
  }
}

function draw()
{
  underlay();

  for (i = 0; i < 5; i++) {
    cell_new();
  }

  overlay();
}
body {padding: 0; margin: 0;}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.5.0/p5.js"></script>

The basic idea behind the code is that the canvas is quantized into fixed-size cells. Each cell holds zero or one dot object (its position, diameter, and color). As each new random dot is chosen, it is saved into the appropriate cell. This functions as a logical memory for what's in the canvas. (It's not perfect, though, as it does not handle the order in which dots are drawn. But, whatever, we're all friends here.)

I struggled with the basic problem of the translucent area. Initially, I was redrawing the entire frame each time, as seems to be the Processing way... But there were just too many objects. It was taking far too long to draw each frame. In the end, I ended up blowing away the area under the translucent rectangle, redrawing just the affected objects, and then laying down a new translucent rectangle on top.

Is there a technique that I can apply here that performs better, or uses less code, or... (gasp) both?

Upvotes: 2

Views: 482

Answers (1)

Kevin Workman
Kevin Workman

Reputation: 42176

Your approach is pretty reasonable, but you could simplify it by using a buffer image to store your underlay instead of a 2D array. Draw your dots to that, then each frame draw the entire buffer to the screen, then draw the rectangle overlay on top of that. This has the benefit of not restricting yourself to array positions, and it'll work for similar layering issues in the future.

See my answer here for more info, but the approach would be something like this:

var buffer;

function setup() {
  createCanvas(windowWidth, windowHeight);
  frameRate(15);

  buffer = createGraphics(width, height);
  
  //start with white background
  buffer.background(255);
}

function drawRandomCircleToBuffer() {
  buffer.noStroke();
  buffer.fill(random(255), random(255), random(255));
  var diameter = random(5, 20);
  buffer.ellipse(random(buffer.width), random(buffer.height), diameter, diameter);
}

function overlay() {
  strokeWeight(1);
  stroke(0, 0, 255, 75);
  fill(0, 0, 255, 75);
  rect(mouseX, mouseY, 200, 200);
}

function draw() {
  
  drawRandomCircleToBuffer();

  image(buffer, 0, 0);
  overlay();
}
body {padding: 0; margin: 0;}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.5.0/p5.js"></script>

Upvotes: 4

Related Questions