// Parameters! int stepsPerSecond = 30; float maxEnergy = 20; float minEnergy = 4; float minSaturation = 10; float maxSaturation = 100; float energyChangeRate = 0.01; float hueShift = (100.0 / 2) + 1.2; float alpha = 10; float energyExp = 3.5; OPC opc; TriangleGrid triangle; // Current snake float currentHue; int currentCell; // Per-cell hue and energy float[] cellHues; int[] cellEnergies; // Step counter int stepNumber = 0; void setup() { size(200, 200); background(0); // Pacing frameRate(stepsPerSecond); // Connect to the local instance of fcserver. You can change this line to connect to another computer's fcserver opc = new OPC(this, "127.0.0.1", 7890); // Map our triangle grid to the center of the window triangle = new TriangleGrid(); triangle.grid16(); triangle.mirror(); triangle.rotate(radians(60)); triangle.scale(height * 0.2); triangle.translate(width * 0.5, height * 0.57); triangle.leds(opc, 0); // Initialize cells cellHues = new float[triangle.cells.length]; cellEnergies = new int[triangle.cells.length]; currentHue = random(100); currentCell = int(random(triangle.cells.length)); // Hide LED location dots, so we can alpha blend against the previous frame without // these dots stomping on the pixel data we care about. opc.showLocations(false); colorMode(HSB, 100); } int chooseMaximum(float[] scores) { // Return the index of the largest nonzero member of 'scores'. // Returns -1 if all members are <= 0. int result = -1; float best = 0; for (int i = 0; i < scores.length; i++) { if (scores[i] > best) { result = i; best = scores[i]; } } return result; } int chooseNeighbor(int current) { // Look for a neighboring cell to move to. Each neighbor gets a // score, either a random positive number or zero if it's unsuitable. float[] scores; scores = new float[3]; for (int i = 0; i < scores.length; i++) { int neighbor = triangle.cells[current].neighbors[i]; if (neighbor >= 0 && cellEnergies[neighbor] == 0) { scores[i] = random(1, 2); } } int neighbor = chooseMaximum(scores); if (neighbor < 0) { return -1; } return triangle.cells[current].neighbors[neighbor]; } int chooseEmpty() { // Look for a random empty cell float[] scores; scores = new float[triangle.cells.length]; for (int i = 0; i < scores.length; i++) { if (cellEnergies[i] == 0) { scores[i] = random(1, 2); } } return chooseMaximum(scores); } void draw() { stepNumber++; // Cell energies vary in sinusoidal epochs, causing the shape of the snakes // to shift accordingly as the space becomes more or less fragmented. float e = map(cos(stepNumber * energyChangeRate), 1, -1, 0, 1); e = pow(e, energyExp); int currentEnergy = int(map(e, 0, 1, minEnergy, maxEnergy)); // Each live cell decays by one step, no cells can live longer than currentEnergy. // This creates a cadence to the life of each snake, and periodic waves of extinction. for (int i = 0; i < triangle.cells.length; i++) { cellEnergies[i] = max(0, min(currentEnergy, cellEnergies[i] - 1)); } // Can we keep going? if (currentCell >= 0) { currentCell = chooseNeighbor(currentCell); } if (currentCell < 0) { // New snake currentCell = chooseEmpty(); if (currentCell >= 0) { // Hues rotate between three complementary colors, shifting slowly around the color wheel. currentHue = (currentHue + hueShift) % 100; } } if (currentCell >= 0) { // We have somewhere to go. cellEnergies[currentCell] = currentEnergy; cellHues[currentCell] = currentHue; } // Overall saturation shows the current energy level. Extinction periods (low currentEnergy) // correspond with flashes of white. float saturation = map(currentEnergy, minEnergy, maxEnergy, minSaturation, maxSaturation); // Draw the current state of each cell for (int i = 0; i < triangle.cells.length; i++) { float size = height * 0.1; fill(cellHues[i], saturation, map(cellEnergies[i], 0, currentEnergy, 0, 100), alpha); ellipse(triangle.cells[i].center.x, triangle.cells[i].center.y, size, size); } }