3D Snake Pattern

I made a snake for my pentagonal hexecontahedron. I’m using the Perceptual Hue code from @jeff to expand on the reds and yellows.

It’s a simple algorithm — keep an array of cell indices, and randomly choose a new cell to go towards, preferring not to overlap yourself unless necessary — but choosing the next cell to enter is complicated because of the shape of the orb cells. I had to manually create the adjacency graph for the orb, which didn’t take too long.

I won’t be uploading this to the pattern library, but I’ll include it here for reference. Warning: not my best code :grimacing:

// Simple snake pattern
// Copyright 2022 Tom Clark
// MIT License — feel free to use it however you want

// The way it works is we have a head and an array of tail segments
// - The minimul snake length is 2 (including the head)
// - Snake grows at the neck (new segments are a clone of the first tail segment)
// - Snake shrinks at the end of the tail
// - Snake tries to not overlap itself when moving around, but can if it's trapped (NB there's a flicker then)
// - Colors are Perceptual Hue by @jeff on Electromage Forum

// how fast the snake moves from 100ms to 1000ms
export var x
export var delay
export function sliderDelay(n) {
  delay = n*975 + 25
}

// how long the snake is
export var snake_length = 10
var tail_length = snake_length - 1
export function sliderSnakeLength(n) {
  snake_length = floor(n*28)+2
  tail_length = snake_length - 1
}

// the starting color aka the color of the head
export var color = 1
export function sliderHeadColor(n) {
  color = n
}

// The remaining colors of the body, spread across the length of the snake.
// this maps 0, 0.25, 0.5, 0.75, 1.0 to 0, 0.5, 1.0, 1.5, 2.0 so there's more of the lower end to choose from.
export var palete = 1
export function sliderBodyPalette(n) {
  if(n<0.5) {
    palete = 2*n
  } else {
    palete = (n-0.5)*2+1
  }
}

// initial values
var tail = array(10)
var head = 1
export var elapsed = 0

// Here we update the global state based on the sliders.
// We also keep track of when the next "frame" is aka when the snake moves into the next cell
export function beforeRender(delta) {
  elapsed += delta

  // enough time has passed to compute the next frame
  if(elapsed > delay) {
    elapsed = elapsed % delay

    // resize the snake if necessary
    if(tail_length < tail.length) {         // shrink
      tail = slice(tail, tail.length - tail_length, tail.length)
    } else if(tail_length > tail.length) {  // grow
      new_tail = array(tail_length)

      // new body segments are the same as the first tail segment (snake grows at the neck)
      for(i = 0; i < tail_length - tail.length; i++) {
        new_tail[i] = tail[0]
      }

      // copy the rest of the tail to the end of the new tail
      for(i = 0; i < tail.length; i++) {
        new_tail[i+ tail_length - tail.length - 1] = tail[i]
      }

      tail = new_tail
    }

    // grow
    next = neighbor(tail, head)
    for(i = 0; i < tail.length - 1; i++) {
      tail[i] = tail[i+1]
    }
    tail[tail.length-1] = head
    head = next
  }

  t1 = time(.1)
}

export function render(index) {
  h = t1 + index
  s = 1
  v = 1

  remaining = (delay-elapsed) / delay

  // should we render this pixel? - we only render cells that are on the snake
  for(i = 0; i < tail.length; i++) {
    if(index == tail[i]) {
      // fade this pixel based on the current frame time and the distance to the head
      h = v = (remaining + i) / (tail.length+1)
      hsv(colorize(h), s, v)
    }
  }

  // head fades in from black - this causes a flicker when it overlaps itself
  if(index == head) {
    h = (remaining + tail.length) / (tail.length+1)
    v = elapsed/delay
    hsv(colorize(h), s, v)
  }
}

// helper function
function colorize(h) {
  return fixH(palete*h+color)
}

// randomly choose a new cell to go into, but keep randomly picking if we're already in that cell
// - give up after 60 iterations and overlap ourself
function neighbor(prev, curr) {
  next = prev[0]

  iterations = 0
  while(iterations < pixelCount && included(prev, curr, next)) {
    cell = neighbors[curr-1]
    random_index = floor(random(cell.length))

    next = cell[random_index]
    iterations++
  }

  return next
}

// Array.includes?(tail, head, value)
function included(arr, extra, e) {
  for(i = 0; i < arr.length; i++) {
    if(e == arr[i]) {
      return true
    }
  }

  return e == extra
}

// Array.slice(arr, start, end)
function slice(a, s, e) {
  if(s == e) { return [] }

  sliced = array(e-s)
  for(i = 0; i+s < e; i++) {
    sliced[i] = a[i+s]
  }

  return sliced
}


/*  fixH(pH) => h
    Returns 0-1 hue values for hsv()
    Takes a "perceptual hue" (pH) that aspires to progress evenly across a human-perceived rainbow
*/
var hMapSize = 10
var hMap = array(hMapSize+1)
// The values below were subjectively chosen for perceived equidistant color
hMap[0] = 0.00  // red
hMap[1] = 0.015 // orange
hMap[2] = 0.08  // yellow
hMap[3] = 0.30  // green
hMap[4] = 0.44  // cyan
hMap[5] = 0.65  // blue
hMap[6] = 0.70  // indigo
hMap[7] = 0.77  // purple
hMap[8] = 0.985 // pink
hMap[9] = 1.00  // red again - same as 0
hMap[10] = 1.00 // overflow bin

function fixH(pH) {  // pH = "Perceptual Hue"
  pH = pH % 1  // Wrap inputs
  
  binWidth = 1/(hMapSize - 1)  // A 10-point map divides hue's 0-1 phase into 9 arcs of length (binWidth) 0.111
  bin = floor(pH / binWidth)  // Calculate pH's starting bin index, 0..(hMapSize-1)
  binPct = (pH % binWidth) / binWidth  // Find pH's percentage into that bin index
  base = hMap[bin]  // base value in hsv()'s h unit
  gap = hMap[bin + 1] - base  // gap is the distance in hsv()'s h units between this base bin and the next
  
  return base + binPct * gap  // Interpolate the result between the base bin's h value and the next bin's
}

// Pentagonal Hexecontahedron cells.  Some are missing because I've yet to close up the last 5.
var neighbors = [
  [ 2, 5, 6,10,11],
  [ 1, 3,11,15,16],
  [ 2, 4,16,20,21],
  [ 3, 5,21,25,26],
  [ 1, 4, 6,26,29],
  [ 1, 5, 7,10,29],
  [ 6, 8,29,30,31],
  [ 7, 9,31,32],
  [ 8,10,12],
  [ 1, 6, 9,11,12],
  [ 1, 2,10,12,15],
  [ 9,10,11,13],
  [12,14,54],
  [13,15,17,54,55],
  [ 2,11,14,16,17],
  [ 2, 3,15,17,20],
  [14,15,16,18,55],
  [17,19,43,51,55],
  [18,20,22,42,43],
  [ 3,16,19,21,22],
  [ 3, 4,20,22,25],
  [19,20,21,23,42],
  [22,24,39,41,42],
  [23,25,27,39,40],
  [ 4,21,24,26,27],
  [ 4, 5,25,27,29],
  [24,25,26,28,40],
  [27,30,35,36,40],
  [ 5, 6, 7,26,30],
  [ 7,28,29,31,35],
  [ 7, 8,30,32,35],
  [ 8,31,33],
  [32,34,47,48],
  [33,35,36,37,47],
  [28,30,31,34,36],
  [28,34,35,37,40],
  [34,36,38,46,47],
  [37,39,41,45,46],
  [23,24,38,40,41],
  [24,27,28,36,39],
  [23,38,39,42,45],
  [19,22,23,41,43],
  [18,19,42,44,51],
  [43,45,50,51,52],
  [38,41,44,46,50],
  [37,38,45,47,50],
  [33,34,37,46,48],
  [33,47,49],
  [48,50,52,53],
  [44,45,46,49,52],
  [18,43,44,52,55],
  [44,49,50,51,53],
  [49,52,54],
  [13,14,53,55],
  [14,17,18,51,54],
]
2 Likes

I love it! Looks great! What a beautiful build.

If you’d like to enhance it, check out my Snake 2D code in the library, and specifically check out the wander() fake-random function I made for varying its speed. I think that was what really added the most organic life to it.

1 Like

Oh nice! I see you’re using a bearing in radians. That’s challenging for me because I’m not encoding direction in the cell neighbors array; the most I can do is choose a random next cell that’s not a cell I’m already in.

I like the speed changes though! Will continue to poke through your code some more. Like most things, this was a quick hack that is now becoming a platform.

My next upgrade: multiple snakes! I think 3 short snakes would look cool if they start flocking together.

1 Like