🔍

experimental painterly drawing application with p5.js

let particles = [];
let capture;
let n, s, maxR;

let DETAIL = 20;

function setup() {
  createCanvas(768, 768);
  background(255);
  smooth();
  
  capture = createCapture(VIDEO);
  capture.size(768, 768);
  capture.hide();
  
  n = 4000;
  s = 12;
  DETAIL = s;
  maxR = height / 2 - height / 20;
  
  particles = [];
  initParticles();
}

function draw() {
  translate(width / 2, height / 2);
  noStroke();
  
  capture.loadPixels();
  
  if (particles.length != 0) {
    for (let i = 0; i < particles.length; i++) {
      let p = particles[i];
      p.show();
      p.move();
      
      if (p.isOutOfBounds()) {
        p.reattach();
      }
    }
  }
}

function initParticles() {
  for (let i = 0; i < n; i++) {
    particles.push(new Particle(s));
  }
}

function keyPressed() {
  if (key === 's' || key === 'S') { // Check if the 's' key is pressed
    
    DETAIL--;
    for (let i = 0; i < particles.length; i++) {
      particles[i].shrink();
    }
  }
}

class Particle {
  constructor(s_) {
    this.s = s_;
    this.bri = 0;
    this.init();
    this.mainsize = random(0.5, 1);
  }
  
  init() {
    this.pos = createVector(random(-width / 2, width / 2), random(-height / 2, height / 2));
    this.vel = createVector(random(-1, 1), random(-1, 1)); // Random initial motion vector
    this.c = color(255);
    this.targetColor = color(255); // Initialize target color
    this.offset = random(-30, 30); // Random brightness or color tone offset
    this.lifetime = int(random(60, 100)); // Random lifetime in frames (1 to 2 seconds at 60 fps)
    this.maxLifetime = this.lifetime; // Store the maximum lifetime
  }

  show() {
    // Reduce particle size as its lifetime decreases
    let sizeFactor = map(this.lifetime, 0, this.maxLifetime, 1, 0.5);
    let displaySize = (this.s * sizeFactor + this.bri * 0.01) * this.mainsize;

    // Sample color from the capture pixels directly under the particle
    let x = int(map(this.pos.x, -width / 2, width / 2, 0, capture.width));
    let y = int(map(this.pos.y, -height / 2, height / 2, 0, capture.height));
    let index = (x + y * capture.width) * 4;
    let r = capture.pixels[index];
    let g = capture.pixels[index + 1];
    let b = capture.pixels[index + 2];
    let a = 255; // Set alpha to 255

    // Draw the particle using the sampled color with the offset applied
    fill(r + this.offset, g + this.offset, b + this.offset );
     
    circle(this.pos.x, this.pos.y, displaySize * 2);
  }

 updateColor() {
    // Only update the color if the particle hasn't reached its maximum lifetime yet
    if (this.lifetime > 0) {
      let x = int(map(this.pos.x, -width / 2, width / 2, 0, capture.width));
      let y = int(map(this.pos.y, -height / 2, height / 2, 0, capture.height));
      let index = (x + y * capture.width) * 4;
      let r = capture.pixels[index];
      let g = capture.pixels[index + 1];
      let b = capture.pixels[index + 2];
      let targetColor = color(r + this.offset, g + this.offset, b + this.offset); // Calculate target color
      
      // Smoothly transition to the target color using linear interpolation (lerp)
      this.c = lerpColor(this.c, targetColor, 0.001); // Try increasing the interpolation amount
    }
  }
  
  move() {
    let x = int(map(this.pos.x, -width / 2, width / 2, 0, capture.width));
    let y = int(map(this.pos.y, -height / 2, height / 2, 0, capture.height));
    let index = (x + y * capture.width) * 4;
    let r = capture.pixels[index];
    let g = capture.pixels[index + 1];
    let b = capture.pixels[index + 2];
    let brightnessValue = (r + g + b) / 3;
  
    this.bri = brightnessValue;
    
    let mousey = createVector(0, 0);
    
    if (mouseIsPressed) {
      // Calculate normalized direction from particle to mouse position
      let dx = mouseX - x;
      let dy = mouseY - y;
      mousey = createVector(dx, dy).normalize().mult(2); // Normalize the vector and scale it
    }
    

    let angle = map(brightnessValue, 0, 255, 0,  PI); // Introduce slight randomness
    let noiseAngle = map(noise(this.pos.x * 0.004, this.pos.y * 0.004, millis() * 0.001), 0, 1, -TWO_PI, PI );
    let noiseVel = p5.Vector.fromAngle(noiseAngle); // Random motion vector based on Perlin noise
   
    this.vel.add(noiseVel);
    
    this.vel.setMag(DETAIL/2); // Set magnitude to control speed
   
   this.vel.rotate(angle * 0.15);
   this.vel.add(mousey); // Add mousey vector directly to velocity
   this.pos.add(this.vel);
    
    this.lifetime--; // Decrease lifetime
  }


  shrink() {
    this.s -= 1; // Shrink dot size by 1 unit
  }

  isOutOfBounds() {
    return (this.pos.x < -width / 2 || this.pos.x > width / 2 || this.pos.y < -height / 2 || this.pos.y > height / 2 || this.lifetime <= 0);
  }

  reattach() {
    // Store the current color before resetting the particle
    let previousColor = this.c;
    
    this.init(); // Reset particle properties
    
    // Restore the previous color
    this.c = previousColor;
  }
}