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
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 I need that PB V3 to see if it makes it longer into the strip before that happens.
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)
}
@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.
// 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)
}
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)
}
}