Particle Effects

Examples made using the experiment

All the code that runs this experiment is bellow, apart from any external libraries (i.e. p5.js)

'use strict'

class Particle {
  constructor(sketch, maximum_distance, circle_color, line_color, min_radius = 5, max_radius = 15) {
    this.sketch = sketch

    this.max_speed    = 2
    this.maximum_distance = maximum_distance

    this.circle_color = circle_color || "#999999"
    this.line_color = line_color || "gray"

    this.min_radius = min_radius
    this.max_radius = max_radius

    this.reset()
  }

  reset() {
    this.radius   = this.sketch.random(this.min_radius, this.max_radius)
    this.position = this.sketch.createVector(this.sketch.random(this.sketch.width),this.sketch.random(this.sketch.height))
    this.velocity = this.sketch.createVector(this.sketch.random(this.max_speed * -1, this.max_speed),this.sketch.random(this.max_speed * -1, this.max_speed))
  }

  update() {
    this.position.add(this.velocity)
    this.edges()
  }

  // Bounce at the edges
  edges() {
    if (this.position.x <= 0 || this.position.x >= this.sketch.width) this.velocity.x *= -1
    if (this.position.y <= 0 || this.position.y >= this.sketch.height) this.velocity.y *= -1
  }

  draw() {
    this.sketch.push()

    this.sketch.fill(this.circle_color)
    this.sketch.stroke(this.circle_color)

    this.sketch.circle(this.position.x, this.position.y, this.radius)

    this.sketch.pop()
  }

  drawLinkToNearParticles(particles) {
    this.sketch.push()
    this.sketch.stroke(this.line_color)
    particles.forEach(particle => {
      if (this.sketch.dist(this.position.x, this.position.y, particle.position.x, particle.position.y) < this.maximum_distance) {
        this.sketch.line(this.position.x, this.position.y, particle.position.x, particle.position.y)
      }
    })
    this.sketch.pop()
  }
}

// Avoiding Global Mode: https://github.com/processing/p5.js/wiki/Global-and-instance-mode
const experiment = ( sketch ) => {
  //
  // Configuration
  //
  let container = document.getElementById('sketch-holder')
  let canvas_el

  let particle_number_range  = [10, 500]
  let maximum_distance_range = [10, 500]

  let maximum_distance = 100;
  let number_of_particles;
  let particles = [];

  // Color Schemes
  let color_scheme
  let color_sets = {
    black_white: {
      background: "black",
      label: "Black Background / Light Elements",
      circle: [
        'rgb(240, 240, 240)'
      ],
      line: [
        'rgb(200, 200, 200)'
      ],
      min_radius: 1,
      max_radius: 5
    },
    white_black: {
      background: "white",
      label: "White Background / Dark Elements",
      circle: [
        'rgb(40, 40, 40)'
      ],
      line: [
        'rgba(100, 100, 100, 0.8)'
      ],
      min_radius: 5,
      max_radius: 10
    },
    black_color: {
      background: "black",
      label: "Black Background / Colorful Elements",
      circle: [
        '#04E762',
        '#F5B700',
        '#00A1E4',
        '#DC0073',
        '#89FC00'
      ],
      line: [
        '#04E76288',
        '#F5B70088',
        '#00A1E488',
        '#DC007388',
        '#89FC0088'
      ],
      min_radius: 5,
      max_radius: 10
    },
    red_white: {
      background: "red",
      label: "Red Background / Large White Circles",
      circle: ["white"],
      line: ["white"],
      min_radius: 10,
      max_radius: 30
    }
  }

  // Controls
  let controls = {
    play_pause_toggle: document.getElementById("experiment-control-pause-toggle"),
    particle_count: document.getElementById("experiment-control-particle-count"),
    maximum_distance: document.getElementById("experiment-control-maximum-distance"),
    color_scheme_picker: document.getElementById("experiment-control-color-scheme"),
    fullscreen: document.getElementById("experiment-control-fullscreen-toggle"),
    save: document.getElementById("experiment-control-save")
  }

  let control_labels = {
    particle_count: document.getElementById("experiment-label-particle-counter"),
    maximum_distance: document.getElementById("experiment-label-maximum-distance"),
  }

  //
  // Control Handling
  //

  function setupControls() {
    controls.particle_count.min = particle_number_range[0]
    controls.particle_count.max = particle_number_range[1]

    control_labels.maximum_distance.innerHTML = maximum_distance
    controls.maximum_distance.min = maximum_distance_range[0]
    controls.maximum_distance.max = maximum_distance_range[1]

    Object.entries(color_sets).forEach(([key, scheme]) => {
      var option = document.createElement("option")
      option.value = key
      option.text = scheme.label
      if (key == color_scheme) option.selected = "SELECTED"

      controls.color_scheme_picker.add(option)
    })
  }

  function updateControls() {
    controls.particle_count.value = number_of_particles
    control_labels.particle_count.innerHTML = number_of_particles
  }

  function installControlHandlers() {
    //
    // Pause / Play
    //
    controls.play_pause_toggle.addEventListener("click", function() {
      this.querySelectorAll('i').forEach( (el) => {
        if (!el.classList.contains('hide')) { // Look for the active icon
          if (el.classList.contains('fa-pause-circle')) {
            sketch.noLoop()
          } else if (el.classList.contains('fa-play-circle')) {
            sketch.loop()
          }
        }

        el.classList.toggle('hide')
      })
    })

    //
    // Number of Particles slider
    //
    controls.particle_count.addEventListener("input", function(event) {
      control_labels.particle_count.innerHTML = event.target.value
    })
    controls.particle_count.addEventListener("change", function(event) {
      updateParticleCount(event.target.value)
    })

    //
    // Maximum Distance Slider
    //
    controls.maximum_distance.addEventListener("input", function(event) {
      control_labels.maximum_distance.innerHTML = event.target.value
    })
    controls.maximum_distance.addEventListener("change", function(event) {
      maximum_distance = parseInt(event.target.value)
      particles.forEach(particle => { particle.maximum_distance = maximum_distance })
    })

    //
    // Color Scheme
    //
    controls.color_scheme_picker.addEventListener("change", function(event) {
      color_scheme = event.target.value
      particles.forEach(particle => {
        particle.circle_color = sketch.random(color_sets[color_scheme].circle)
        particle.line_color   = sketch.random(color_sets[color_scheme].line)

        if (color_sets[color_scheme].min_radius && color_sets[color_scheme].max_radius) {
          particle.radius = sketch.random([color_sets[color_scheme].min_radius, color_sets[color_scheme].max_radius])
        }
      })

      if (!sketch.isLooping()) sketch.draw()  // Draw a frame for your witcher
    })

    // Save file
    controls.save.addEventListener("click", function() {
      sketch.save(canvas_el, `particle_effects_${color_scheme}_${number_of_particles}_${maximum_distance}_${Math.floor(Date.now() / 1000)}`, 'png')
    })

    // Fullscreen
    controls.fullscreen.addEventListener("click", function() {
      this.querySelectorAll('i').forEach( (el) => {
        if (!el.classList.contains('hide')) { // Look for the active icon
          if (el.classList.contains('fa-compress')) {
            sketch.fullscreen(false)
          } else {
            sketch.fullscreen(true)
          }
        }

        el.classList.toggle('hide')
      })
    })
  }

  //
  // P5.js touchpoints
  //
  sketch.setup = () => {
    canvas_el = sketch.createCanvas(container.clientWidth, container.clientHeight)
    canvas_el.parent(container)

    color_scheme = sketch.random(Object.keys(color_sets))

    updateParticleCount()
    setupControls()
    updateControls()
    installControlHandlers()
  }

  sketch.windowResized = () => {
    sketch.resizeCanvas(container.clientWidth, container.clientHeight)
    sketch.draw()
  }

  sketch.draw = () => {
    sketch.background(color_sets[color_scheme].background)

    particles.forEach((particle, index) => {
      particle.drawLinkToNearParticles(particles.slice(index))
      particle.draw()
      particle.update()
    })
  }

  //
  // Helper methods
  //

  function buildParticle() {
    return new Particle(
      sketch,
      maximum_distance,
      sketch.random(color_sets[color_scheme].circle),
      sketch.random(color_sets[color_scheme].line),
      color_sets[color_scheme].min_radius,
      color_sets[color_scheme].max_radius

    )
  }

  function updateParticleCount(value = null) {
    number_of_particles = parseInt(value) || Math.floor(container.clientWidth / 10)

    if (number_of_particles < particles.length) {
      particles.splice(number_of_particles)
    } else {
      for(var i = particles.length; i < number_of_particles; i++) {
        particles[i] = buildParticle()
      }
    }
  }
}


// Wait for everything to load
if (document.readyState === 'complete') {
  new p5(experiment)
} else {
  window.onload = (event) => {
    new p5(experiment)
  }
}