# 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

``````// Simple snake pattern
// 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
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)
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
for(i = 0; i < tail.length - 1; i++) {
tail[i] = tail[i+1]
}
}

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
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
}

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