Task #8: Serpentine Square!

This one is free form, and partly related to St. Paddy’s day (just passed):

Make a snake with your matrix… Could be a line that moves, could be a wriggle, could be a full-on Snake game (self playing).

You decide how to make it Snake-y.

I would be remiss to not point at and recommend the 3D printed 16x16 matrix frame

Very slick, and I might print one myself soon. (Need to get my printer working again)

This was really fun. Here’s what I came up with.

I’ll post the code in the library at the end of the week.

6 Likes

Fantastic! Brilliant! Organic motion. Love it!

As someone who lives around a lot of 'em, I heartily approve that snake! :snake:

(Dog is very well trained concerning snakes. She stays well back, but she is not letting that guy get up on the porch. I used a broom to sweep him into a bucket, popped the lid on and released him well away from the house. They eat the incredibly destructive packrats, so they’re good to have around. Just another normal day in the desert… )

3 Likes

Yup. That’s one big 'ol snake! And a very good dog. Glad you don’t kill 'em, those snakes.

1 Like

@jeff,
That is so cool! I couldn’t help but stare and wonder, and mentally try to reverse engineer it :slight_smile: I can’t wait to play with it when you post the code. The way it jumps in speed is pretty cool too! And the softness/antialiasing of the “head” :100:

1 Like

Cool, I just uploaded. I know we don’t want spoilers, so I’ve included it here using the forum’s handy “Hide details” thing.

Click to expand code for Snake 2D
/* 
  Snake - Generate a serpentine path around a matrix
  
  This pattern relies on functions present in Pixelblaze v3 (hypot(), 
  mod(), arrayMutate()) but can be adapted to v2.
  
  Demo on 16x16: https://youtu.be/RfDidPGp3Vg

  Jeff Vyduna 2021 / MIT License
*/

// User-adjustable parameters
var width = 16          // Width of matrix, in pixels
var edgePct = .15       // Edge width, in percentage of the lesser of width or height. Snake tries to avoid edges. 
var radius = sqrt(pixelCount) * 3/16   // Radius of the snake head in pixels. 3 on a 16x16
var nominalDecay = .98  // Fade each pixel per frame. Adjusted for speed and delta.
var maxDeflection = .2  // Radians per frame when escaping a boundary (edge)
var nominalDeflection = .1  // Radians per frame when slithering

var nominalSpeed = 0.02, speed = nominalSpeed
export function sliderSpeed (_v) { 
  speed = 2 * nominalSpeed * _v
}

var height = pixelCount / width // Height of matrix, in pixels
var posX = 0, posY = 0          // Positon of snakehead, in pixels. Set initial position.
var bearing = PI / 4            // Direction of travel, in radians
var deflection = nominalDeflection // Turn by deflecting the bearing each frame (radians)
var adjSpeed                    // User-commanded speed modified by the wandering fn and proximity to edges
var adjDeflection               // Deflection angle modified for the adjusted speed
var thisTurn                    // Accumulated radians in the current slither / turn
var nominalDelta = 15           // A reference delta used to adapt turns for running speed (e.g. number of pixels)
var cx = width / 2, cy = height / 2 // Centers in x and y, in pixels
var cornerDist = hypot(cx, cy)  // Distance from center to a corner
var edge = min(width, height) * edgePct // Edge border thickness, in pixels
var escapeDeflection =  0       // The current corrective deflection to escape edges or corners
var matrix = array(width)       // Array of pixels in matrix, [width][height]. Stores "heat".
matrix.mutate((v) => array(height))


export function beforeRender(delta) {
  t1 = time(6  / 65.535) // Hue modifier
  t2 = time(30 / 65.535) // Speed wandering period
  
  changeDirRandomly(delta)
  avoidWalls()
  
  // Speed wanders around the user-specified speed, and gets faster when near the edges
  adjSpeed = speed * wander(t2) * (.5 + hypot(posX - cx, posY - cy)/cornerDist) 
  // Adjust the bearing deflection (curve) for speed input and delta
  adjDeflection = deflection * adjSpeed / nominalSpeed * delta / nominalDelta
  bearing = mod(bearing + adjDeflection, PI2) // Make the turn, keep bearing in 0..PI2
  
  slither() // Reverse the turn so it doesn't normally eat its own tail
  
  posX += adjSpeed * delta * cos(bearing)
  posY += adjSpeed * delta * sin(bearing)
  
  bounceOffWalls()
  prerender()
}


// Moody snake doesn't always slither predictibly
function changeDirRandomly(_delta) {
  // At 100FPS, this is roughly reversing once within 3 seconds
  if(random(30 * _delta) < 1) deflection *= -1
}

function avoidWalls() {
  var towardsVecX = 0, towardsVecY = 0
  if (width - posX < edge) towardsVecX = 1
  if (height - posY < edge) towardsVecY = 1
  if (posX < edge) towardsVecX = -1
  if (posY < edge) towardsVecY = -1
  // If set, (towardsVecX, towardsVecY) is now the vector to the closest edge or corner

  if (towardsVecX == 0 && towardsVecY == 0) {  // Not near an edge
    if (escapeDeflection) { // Was just in an edge (but now not)
      deflection = nominalDeflection * ((random(2) > 1) ? -1 : 1)
      thisTurn = escapeDeflection = 0
    }
    return
  }
  
  // If we get here, we're in range of an edge or corner.
  
  // Angle towards nearest wall or corner, 0..PI2
  towardsAngle = (atan2(towardsVecY, towardsVecX) + PI2) % PI2
  
  if (towardsVecX * towardsVecY != 0) {
    // In a corner; keep an existing turn going or set one up
    escapeDeflection = escapeDeflection || maxDeflection
  } else {
    // Near an edge. Deflect away from it.
    deviation = angleDiff(bearing, towardsAngle)
    escapeDeflection = deviation > 0 ? maxDeflection : -maxDeflection
  }

  // We will escape if our bearing is within 45 degrees of the vector away from edge of corner
  var willEscape = abs(angleDiff(towardsAngle + PI, bearing)) < PI / 8

  deflection = willEscape ? 0 : escapeDeflection // End turns if headed away from the edge
}

// Pseudo gradient noise
// https://www.desmos.com/calculator/rvtrkskoqa
function wander(t) {
  t *= 49.261 // Define period for t in 0..1; see graph
  return .6 + 3 * wave(t/2)*wave(t/3)*wave(t/5)*wave(t/7)
}

// Turn the other direction if it's been a half turn
function slither() {
  thisTurn += adjDeflection
  if (abs(thisTurn) > PI) {
    deflection *= -1
    thisTurn = 0
  }
}

// Return (angle - targetAngle) within -PI..PI
function angleDiff(angle, target) {
  return mod(angle - target + PI, PI2) - PI
}

// Bounce like a billiard ball
function bounceOffWalls() {
  if (posX <= 0 || posX >= width) {
    bearing = -bearing + PI
    posX = clamp(posX, 0, width)
  }
  if (posY <= 0 || posY >= height) {
    bearing *= -1
    posY = clamp(posY, 0, height)
  }
}

// A 2D array holds each pixel's heat. Cool each pixel, and heat pixels 
// within `radius` of head (posX, posY). Since the inner loop runs as 
// much as render(), there are modifications for speed over readability.
function prerender() {
  // Adjust the exponential decay (tail cooling) for the speed of travel
  var decay = pow(2, log2(nominalDecay) / (nominalSpeed / adjSpeed))
  var speedFactor = adjSpeed / nominalSpeed

  // Heat up pixels near the snake's head
  for(x = 0; x < width; x++) {
    for(y = 0; y < height; y++) {
      matrix[x][y] *= decay
      var distanceToHead = hypot(posX - x, posY - y)
      if (distanceToHead > radius) continue // Boosts FPS
      proximity = 1 - distanceToHead / radius
      matrix[x][y] = min(1, matrix[x][y] + pow(proximity, 6) * speedFactor)
    }
  }
}



export function render2D(index, x, y) {
  v = matrix[x * width][y * height]
  s = 1 - pow(v, 8)
  h = v / 4 + t1
  hsv(h, s, v * v)
  
  // Uncomment to see a line showing the current commanded bearing.
  // Helpful when debugging edge avoidance. Costs half your FPS though!
  // plotAngle(bearing, x * width, y * height)
}


// Utility to plot an angle from the center of the matrix to the 
// boundary of the border `edge`. Can be called from within prerender() or render() 
// Usage: plotAngle(bearing, x * width, y * height)
function plotAngle(a, x, y) {
  x -= cx; y -= cy
  var pixelAng = atan2(y, x)
  var ad = abs(angleDiff(a, pixelAng)) // Angle difference between a and this pixel
  var hc = (width - 2 * edge) / 2      // Horizontal center box height
  var vc = (height - 2 * edge) / 2
  var bx = cos(a), by = sin(a)
  var dist = abs(x * by - y * bx) / hypot(bx, by) // Distance from pixel to the line to be plotted

  if (abs(x) < hc && abs(y) < vc && ad < PI/2) hsv(0, 0, 1 - dist * 2)
}
5 Likes

@jeff, this is simply beautiful code! And @scruffynerf, thanks again for managing the tasks. These challenges are helping everyone up their game, as well as being a whole lot of fun.

4 Likes

@jeff, you have created my new favorite desk pet!

2 Likes

My Pixelbaze2 does not like

return mod(angle - target + PI, PI2) - PI

tells me “Undefined symbol mod” is this a pizelblaze V3 code?

mod() is a PB3 thing – as I understand it, it’s actually a remainder operation as opposed to the usual %. From the PB3 docs:

mod(x,y)

The floored remainder of the division x/y. The result uses the same sign as y. For example mod(-3.5, 3)==2.5 whereas -3.5 % 3==-.5. This provides the same “wrapping” behavior used in the animation functions like triangle when y==1.

The % and mod functions very quite a bit from language to language. The new mod function is a floored remainder operation, which is what Python would use, but JavaScript would not. It is very handy and works well with the way Pixelblaze treads cyclical values between 0-1.

From Wikipedia:

“[…] the remainder would have the same sign as the divisor . Due to the floor function, the quotient is always rounded downwards, even if it is already negative.”

The implementation is pretty simple. Using %, if the result has a sign that doesn’t equal the sign of the divisor, add the divisor to it.

function flooredMod(dividend, divisor) {
  var res = dividend % divisor
  if ((res > 0) != (divisor > 0)) { //this compares the sign of both
    res += divisor
  }
  return res
}

As you can see, they will only differ from % when there are negative numbers mixed with positive.
As a quick example, using the % operator if you take -1.2 % 1 you get -0.2. This makes sense if you think -1.2/1 == -1.2 == -1 + -0.2 so taking away the whole integer part, leaving -0.2 is the remainder.

Using mod on V3, or the equivalent flooredMod above gets you a different result. mod(-1.2, 1) results in 0.8 a positive number. This is very useful if you are trying to “wrap” a number between 0 and 1 (or some other range), and might end up with a negative number. This makes sense if you think about modulus math as a number line that wraps around in a circle with a circumference equal to the divisor. Imagine a circle with 0-360 degrees. Where is -45 degrees? 315 degrees! This is where the floored division modulus comes in handy: mod(-45, 360) == 315.

3 Likes