image-processingprocessingpgraphics

Draw permanently on PGraphics (Processing)


I would like to create a brush for drawing on a PGraphics element with Processing. I would like past brush strokes to be visible. However, since the PGraphics element is loaded every frame, previous brush strokes disappear immediatly.

My idea was then to create PGraphics pg in setup(), make a copy of it in void(), alter the original graphic pg and update the copy at every frame. This produces a NullPointerException, most likely because pg is defined locally in setup().

This is what I have got so far:

PGraphics pg; PFont font;

void setup (){
  font = createFont("Pano Bold Kopie.otf", 600);
  size(800, 800, P2D);
  pg = createGraphics(800, 800, P2D);
  pg.beginDraw();
  pg.background(0);
  pg.fill(255);
  pg.textFont(font);
  pg.textSize(400);
  pg.pushMatrix();
  pg.translate(width/2, height/2-140);
  pg.textAlign(CENTER, CENTER);
  pg.text("a", 0 , 0);
  pg.popMatrix();
  pg.endDraw();
}

void draw () {
  copy(pg, 0, 0, width, height, 0, 0, width, height);
  loop();
  int c;

  loadPixels();
  for (int x=0; x<width; x++) {
    for (int y=0; y<height; y++) {
      pg.pixels[mouseX+mouseY*width]=0;
    }
  }
  updatePixels();
}

My last idea, which I have not attempted to implement yet, is to append pixels which have been touched by the mouse to a list and to draw from this list each frame. But this seems quite complicated to me as it might result into super long arrays needing to be processed on top of the original image. So, I hope there is another way around!

EDIT: My goal is to create a smudge brush, hence a brush which kind of copies areas from one part of the image to other parts.


Solution

  • There's no need to manually copy pixels like that. The PGraphics class extends PImage, which means you can simply render it with image(pg,0,0); for example.

    The other thing you could do is an old trick to fade the background: instead of clearing pixels completely you can render a sketch size slightly opaque rectangle with no stroke.

    Here's a quick proof of concept based on your code:

    PFont font;
    PGraphics pg;
    
    void setup (){
      //font = createFont("Pano Bold Kopie.otf", 600);
      font = createFont("Verdana",600);
    
      size(800, 800, P2D);
      // clear main background once
      background(0);
      // prep fading background
      noStroke();
      // black fill with 10/255 transparnecy
      fill(0,10);
    
      pg = createGraphics(800, 800, P2D);
      pg.beginDraw();
      // leave the PGraphics instance transparent
      //pg.background(0);
      pg.fill(255);
      pg.textFont(font);
      pg.textSize(400);
      pg.pushMatrix();
      pg.translate(width/2, height/2-140);
      pg.textAlign(CENTER, CENTER);
      pg.text("a", 0 , 0);
      pg.popMatrix();
      pg.endDraw();
    }
    
    void draw () {
      // test with mouse pressed
      if(mousePressed){
        // slowly fade/clear the background by drawing a slightly opaque rectangle
        rect(0,0,width,height);
      }
      // don't clear the background, render the PGraphics layer directly
      image(pg, mouseX - pg.width / 2, mouseY - pg.height / 2);
    }
    

    If you hold the mouse pressed you can see the fade effect. (changing transparency to 10 to a higher value with make the fade quicker)

    Update To create a smudge brush you can still sample pixels and then manipulate the read colours to some degree. There are many ways to implement a smudge effect based on what you want to achieve visually.

    Here's a very rough proof of concept:

    PFont font;
    PGraphics pg;
    
    int pressX;
    int pressY;
    
    void setup (){
      //font = createFont("Pano Bold Kopie.otf", 600);
      font = createFont("Verdana",600);
    
      size(800, 800, P2D);
      // clear main background once
      background(0);
      // prep fading background
      noStroke();
      // black fill with 10/255 transparnecy
      fill(0,10);
    
      pg = createGraphics(800, 800, JAVA2D);
      pg.beginDraw();
      // leave the PGraphics instance transparent
      //pg.background(0);
      pg.fill(255);
      pg.noStroke();
      pg.textFont(font);
      pg.textSize(400);
      pg.pushMatrix();
      pg.translate(width/2, height/2-140);
      pg.textAlign(CENTER, CENTER);
      pg.text("a", 0 , 0);
      pg.popMatrix();
      pg.endDraw();
    }
    
    void draw () {
      image(pg,0,0);
    }
    
    void mousePressed(){
      pressX = mouseX;
      pressY = mouseY;
    }
    
    void mouseDragged(){
      // sample the colour where mouse was pressed
      color sample = pg.get(pressX,pressY);
      // calculate the distance from where the "smudge" started to where it is
      float distance = dist(pressX,pressY,mouseX,mouseY);
      // map this distance to transparency so the further the distance the less smudge (e.g. short distance, high alpha, large distnace, small alpha)
      float alpha = map(distance,0,30,255,0);
      // map distance to "brush size"
      float size = map(distance,0,30,30,0);
      // extract r,g,b values
      float r = red(sample);
      float g = green(sample);
      float b = blue(sample);
      // set new r,g,b,a values
      pg.beginDraw();
      pg.fill(r,g,b,alpha);
      pg.ellipse(mouseX,mouseY,size,size);
      pg.endDraw();
    }
    

    As the comments mention, one idea is to sample colour on press then use the sample colour and fade it as your drag away from the source area. This shows simply reading a single pixel. You may want to experiment with sampling/reading more pixels (e.g. a rectangle or ellipse).

    smudge a demo

    Additionally, the code above isn't optimised. A few things could be sped up a bit, like reading pixels, extracting colours, calculating distance, etc.

    For example:

    void mouseDragged(){
      // sample the colour where mouse was pressed
      color sample = pg.pixels[pressX + (pressY * pg.width)];
      // calculate the distance from where the "smudge" started to where it is (can use manual distance squared if this is too slow)
      float distance = dist(pressX,pressY,mouseX,mouseY);
      // map this distance to transparency so the further the distance the less smudge (e.g. short distance, high alpha, large distnace, small alpha)
      float alpha = map(distance,0,30,255,0);
      // map distance to "brush size"
      float size = map(distance,0,30,30,0);
      // extract r,g,b values
      int r = (sample >> 16) & 0xFF; // Like red(), but faster
      int g = (sample >> 8) & 0xFF;
      int b =  sample & 0xFF;
      // set new r,g,b,a values
      pg.beginDraw();
      pg.fill(r,g,b,alpha);
      pg.ellipse(mouseX,mouseY,size,size);
      pg.endDraw();
    }
    

    The idea is to start simple with clear, readable code and only at the end, if needed look into optimisations.