Code for Task #2 - No early leaks!

You are welcome to post your Task #2 - Be like Water solutions here. Please keep the original post free of code so new people can work through it themselves if they wish.

Keep this focused on basic techniques to solve the task, though. Put the more advanced stuff in Golfing Task #2 - Water Hazards

argh… I’m not seeing what is wrong… but I am getting a KIT like effect in one direction… not an accelerating movement.

You need to use larger numbers I think.

You seem to want a time increasing acceleration.

That will work for the fall, for sure. Just play with the values.

Then you can add the start and the finish

I think I found my mistake… I was not taking into account the “pixel_pitch”

1 Like

Next time, don’t delete your thought process, others benefit.

Oh it’s in the history, ok.

Sorry with the code I thought I might be giving too much away.

1 Like

So I think this is right but I have not done the “maths”. Has anyone else noticed that with a one meter length of pixels… that the animation is smooth until 1/4 to 1/3 into the strip the time where the time to update is longer than the position of the drop… ie pixels are skipped and that number keeps increasing?
and I’m only using 3.0 for gravity :slight_smile: I need that PB V3 to see if it makes it longer into the strip before that happens.

It’s fun to use a really low gravity

1 Like

So it’s a week, time to begin dropping your drip code!!

Guess I’ll go first, with something that’s a little more normal than my last one.

This is a minimalist implementation, based on a parametric sigmoid curve generator instead of physics. It’s worth noting that if I were building an actual slime dripping dragon skull, I’d go to great lengths to make it look more realistic(?). But the sigmoid curve was fun to play with too!


// GLOBAL VARIABLES
export var speed = 68;
var hue = 0.3333; 
var sat = 1;
var bri = 0.7;
var t0,t1,t2;
var gamma;

// UI sliders
export function sliderSpeed(v) {
  speed = 128 * v;
}

export function hsvPickerColor(h, s, v) {
  hue = h; sat = s; bri = v;
}

// PARAMETRIC SIGMOID GENERATOR - sigmoid easing curve with
// controllable slope broken into halves so we can guarantee
// that it starts at t==0, ends at t==1, and hits its halfway point at t==0.5
// 
// https://medium.com/hackernoon/ease-in-out-the-sigmoid-factory-c5116d8abce9
function halfSigmoid(t,slope) {
  return (1 / (1 + exp(-slope * t))) - 0.5
}

function sigmoid(t,slope) {
  s1 = 0.5 / halfSigmoid(1,slope);
  return s1 * halfSigmoid(2 * t - 1,slope) + 0.5;
}

// DRAWING - we use time() to generate a single sawtooth wave
// as a base, then use it to build a gamma adjusted sine wave
// to control brightness over time, and also to increase the 
// maximum slope of our sigmoid curve over time, creating
// the appearance of acceleration due to gravity without doing any
// actual physics.
// 
export function beforeRender(delta) {
  t0 = time(0.15);              // base sawtooth for timing everything
  t2 = wave(0.75 + t0);        // phase adjusted sine wave  
  t1 = floor(sigmoid(t0,speed * (1.5 * t2)) * pixelCount);
  gamma = bri * t2 * t2 * t2;   // gamma adjusted  brightness

}

// light pixels based on how far they are from the current target point,
// generated by the sigmoid function.  Use the precomputed gamma 
// curve to determine brightness, so we slowly fade in and out.
export function render(index) {
  var b = abs(index - t1) <= (6 * t2)
  hsv(hue,sat,b * gamma)
}
1 Like

@devoh,
If you are having partial strip updates, it could be WS2812 latches and the unbuffered driver. Are you using WS2812 and the buffered option?

@wizard. I think my “issue” was that by the time you get to 1/3 of the way down a 60led/meter strip the acceleration has your velocity so high that the amount of time to render a frame is longer than the time for the pixel to change from one to the next… So when I get back to the next beforeRender the pixel position has moved a couple. I actually think it’s correct and not any kind of bug, just odd until I started thinking about the “:maths”. Playing with gravity/acceleration and using a really low number showed me it was acting correctly.

Finally made a little dripper tonight.

// Drizzle on a roof

var surfacePct = .3 // Percent of strip the is a surface that gets drizzled on
var surfaceEnd = floor(surfacePct * pixelCount)
var quiver = 0 // A quivering surface as the drip buolds
var pixels = array(surfaceEnd + 1)
var maxDrizzle = .1

var rollThreshold = .5  // Level of water before a drop starts to roll down surface
var residue = rollThreshold / 5 // How much water is left behind as a drop rolls
var dripThreshold = 10 // How much water can the edge of the surface hold before it drips
var G = 700 // Pixels per second squared

dripCount = 5 // Max simulataneous drips to animate
var dripsT = array(dripCount) // Seconds since each drip started
var nextDrip = 0
var totalDrips = 0 // How many drips have finished (fills the "bucket")

export function beforeRender(delta) {
  // Drizzle
  for (i = 0; i < surfaceEnd; i++) if (random(1000) < delta) pixels[i] += random(maxDrizzle)
  
  // Roll down surface if we overcome surface tension. Leave some residue.
  for (i = surfaceEnd - 1; i >= 0; i--) {
    if (pixels[i] > rollThreshold) {
      pixels[i+1] += pixels[i] - residue
      pixels[i] = residue
    }
  }
  
  // Should we kick off a new freefall drip?
  if (pixels[surfaceEnd] > dripThreshold) {
    dripsT[nextDrip] = 0.001 // Kick the drip off
    nextDrip = (nextDrip + 1) % dripCount
    pixels[surfaceEnd] -= random(dripThreshold)
  }
  
  // Calculate how long each drip has been in freefall
  for (d = 0; d < dripCount; d++) {
    // Dripped all the way. Maybe start a splash anim.
    if (dripX(dripsT[d]) > pixelCount) {
      dripsT[d] = 0
      totalDrips++
    }
    if (dripsT[d] > 0) dripsT[d] += delta / 1000
  }
  
  totalDrips %= 10
}

function dripX(t) {
  return floor(surfaceEnd + .5 * G * t * t)
}

export function render(index) {
  s = 1
  if (index < surfaceEnd) { // On the surface
    v = pixels[index]
  } else if (index == surfaceEnd) { // On the edge
    v = pixels[surfaceEnd] / dripThreshold
    if (random(10 * v) > 6 && random(4) < 1) quiver = !quiver
    if (quiver) v = .7
    v *= v
  } else if (index > pixelCount - totalDrips - 1) { // In the bucket
    v = .2; s = 0 // Bucket is filling
  }
  else { // In the freefall range
    v = 0
    // Couldn't figure how to do this as:
    //   dripsT.reduce((a,t) => a || dripX(t) == index, 0)
    for (d = 0; d < dripCount; d++) {
      if (dripX(dripsT[d]) == index) v = 1
    }
  }
  
  surfaceHue = clamp((.5 - v)/(.5-residue), 0, 1) * (index < surfaceEnd)
  
  hsv(.666 - .2 * surfaceHue, s, v * v)
}
5 Likes

nice!! I love how the length changes

I agree with @devoh. This is a pretty pattern! It’d look fantastic on a bunch of “icicle” strands hanging down from a roof or window.

1 Like

I don’t love how I implemented the drop speed up from gravity, but I’m not sure it matters aesthetically

// A bunch of settings that can be customized.
var numPuddlePixels = 2

// Min/max thresholds below determine how random the dripping is

// When the puddle gets too full it will empty with increasing probability from the minimum to the maximum threshold
var minOverflowThreshold = 600 // The minimum threshold which will may trigger an overflow
var maxOverflowThreshold = 1000 // The maximum rain that can be accumulated in the puddle before an overflow happens

// When the puddle overflows a random percent will flow out into a drop between the minimum and maximum
var minOverflowPercent = 70 // The minimum amount of the total puddle which overflows
var maxOverflowPercent = 95 // The maximum amount of the total puddle which overflows

var rainSpeed = 1 // Amount of rain which is falling into the puddle each cycle
var dropSpeed = 10 // Lower numbers are slower
var puddleHue = 0.60 // Hue of the puddle
var dropHue = 0.66 // Hue of the drop

// Pattern state
var puddle = 0 // The puddle which accumulates rain
var dropAnimation = 0 // Counter for drop animation
// An array of drops containing the pixel position of the drop or -1 if there is no drop
// Allow 1/5th of the pixel count for overflow drops, seems like a good enough number :)
var drops = array(pixelCount/5) 
for(i = 0; i < drops.length; i++) {
  drops[i] = -1
}

// The rain speed can be controlled by a slider
export function sliderRainSpeed(v) {
  rainSpeed = 1 + v*12
}

export function beforeRender(delta) {
  // Modify the pattern state as needed.
  
  // Accumulate some rain into the puddle
  puddle += rainSpeed
  
  // Check if the puddle has overflowed
  var overflowed = puddle >= maxOverflowThreshold
  // If it hasn't overflowed but is past the minimum threshold, randomly check if it overflowed
  if (!overflowed && puddle > minOverflowThreshold) {
    // The chance to overflow increases linearly from the minimum (20%) to the maximum (100%)
    var chanceToOverflow = 20 + (puddle - minOverflowThreshold) / (maxOverflowThreshold - minOverflowThreshold) * 80
    overflowed = (random(100) < chanceToOverflow)
  }
  
  if (overflowed) {
    // Initiate a drop
    // Find the position of the first free drop slot
    var dropNum = -1;
    for(i = 0; i < drops.length; i++) {
      if (drops[i] == -1) {
        dropNum = i;
        break;
      }
    }
    if (dropNum != -1) {
      // Remove a random amount between the min/max overflow percent
      var percentOverflow = (minOverflowPercent + random(maxOverflowPercent - minOverflowPercent))/100
      puddle -= percentOverflow * puddle
      
      // Start the drop down the path
      drops[dropNum] = .3
    }
  }
  
  // Accumulate the drop animation counter
  dropAnimation += delta
  if (dropAnimation > dropSpeed) {
    dropAnimation = 0
    
    // Increase each of the drop distances at an ever increasing rate
    for(i = 0; i < drops.length; i++) {
      // Ignore any inactive drops
      if (drops[i] == -1) continue
      
      // Speed accumulates as a power
      var speed = clamp(drops[i] * 0.1, 0, 1)
      drops[i] += speed
      
      // Check if the drop hit the end of the strip and remove it
      if (drops[i] > (pixelCount - 1)) {
        drops[i] = -1
      }
    }
  }
}

export function render(index) {
  // Display the puddle pixels
  if (index < numPuddlePixels) {
    var percentFull = clamp(puddle / maxOverflowThreshold, 0, 1)
    hsv(puddleHue, 1, percentFull * percentFull)
    
  // The remainder of the strip displays the drops
  } else {
    var brightness = 0
    for(i = 0; i < drops.length; i++) {
      // Ignore any inactive drops
      if (drops[i] == -1) continue
      
      if (abs((index - numPuddlePixels + 1) - drops[i]) < .5) {
        brightness = 1
      }
    }
    hsv(dropHue, 1, brightness)
  }
}
3 Likes