Layer snippets

Gray Area mini-intensive, Spring 2026

These are all examples of functions that do the primary drawing routine for each frame.

Basic


Using inputs

These examples all use the potentiometer values to draw shapes with variable parameters.

Random glyphs

Draw random characters from the builtin bitmap font with controllable scale and color. Surprisingly fun imo!

void randomGlyphs() {
  display.setFont();

  int glyph = potMap(0, 1, 256);
  int num =   potMap(1, 1, 50);
  int size =  potMap(2, 1, 12);
  int color = potMap(3, 0, 256);

  for (int i = 0; i < num; i++) {
    int x = random(W) / (6 * size) * (6 * size);
    int y = random(H) / (8 * size) * (8 * size);
    display.drawChar(x, y, glyph, color, 0, size);
  }
}

Polygon grid

Overlapping polygons create abstract tiled patterns. Depends on the drawPolygon() helper function, so copy that in too! This screenshot is also using the cutFade() effect from the FX examples.

void polygonGrid() {
  // display.fillScreen(0);

  int r = potMap(0, 1, 150);
  int sides = potMap(1, 3, 20);
  float rotation = (float)potMap(2, 0, 359)/360.0;

  int rowSpacing = potMap(3, 40, HALFH);
  int colSpacing = potMap(3, 40, HALFW);
  int rows = H/rowSpacing;
  int cols = W/colSpacing;

  int c = floor(sineTime(2000, 0) * 255);

  for (int i = 0; i < rows; i++) {
    for (int j = 0; j < cols; j++) {
      int x = j * colSpacing;
      int y = i * rowSpacing;

      drawPolygon(HALFW + x, HALFH + y, r, sides, rotation, c);
      if (i > 0) {
        drawPolygon(HALFW + x, HALFH - y, r, sides, rotation, c);
      }
      if (j > 0) {
        drawPolygon(HALFW - x, HALFH + y, r, sides, rotation, c);
      }          
      if (i > 0 && j > 0) {
        drawPolygon(HALFW - x, HALFH - y, r, sides, rotation, c);
      }
    }
  }
}

// Draw a regular polygon
void drawPolygon(int cx, int cy, int r, int sides, float rotation, int color) {
  int x0 = 0;
  int y0 = 0;
  int x1 = 0;
  int y1 = 0;

  float angle = rotation * TWO_PI; // rotation is 0-1
  float incrementalAngle = TWO_PI/sides;

  for (int i = 0; i < sides; i++) {
    x0 = int(cx + r * sin(angle));
    y0 = int(cy + r * cos(angle));
    x1 = int(cx + r * sin(angle + incrementalAngle));
    y1 = int(cy + r * cos(angle + incrementalAngle));

    display.drawLine(x0, y0, x1, y1, color);
    angle += incrementalAngle;
  }
}

Integrating time

These examples integrate time in different ways, all related to millis() in some fashion.

Twisting lines

A number of lines are distributed around two circles, with some oscillation

void twistingLines() {
  int numLines = potMap(0, 1, 16) * 2 + 1;
  float angle = TWO_PI/(float)numLines;

  int r0 = potMap(1, 4, 200);
  int r1 = potMap(2, 4, 200);
  int c = potMap(3, 0, 256);

  // int period = cyclicalPotMap(3, 3, 0, 20, 1) * 500;
  int period = 5000;
  float rotationOffset = normalizedTime(period, 0) * TWO_PI;
  float phaseOffset = cyclicalPotMap(3, 2, 0, 0, 360)/360.0f * TWO_PI;

  for (int i = 0; i < numLines; i++) {
    int x0 = HALFW + floor(r0 * cos((i * angle) + rotationOffset));
    int y0 = HALFH + floor(r0 * sin((i * angle) + rotationOffset));

    int x1 = HALFW + floor(r1 * cos((i * angle) + rotationOffset + phaseOffset));
    int y1 = HALFH + floor(r1 * sin((i * angle) + rotationOffset + phaseOffset));

    display.drawLine(x0, y0, x1, y1, c);
  }
}

Wobbly lines

One or more lines wobble back and forth within a bounding box due to the out-of-phase offsets of the x coordinates of the top and bottom point of the line.

void wobblyLines() {
  // display.fillScreen(0);

  int copies = potMap(0, 1, 7);
  int xRange = 15; //center +/- 15 = segment width of 30... about 11 copies in 320px
  int xWidth = xRange * 2;
  float phaseOffset = potMap(1, 0, 360)/360.0;
  int period = potMap(2, 5, 1) * 1000;
  int c = potMap(3, 0, 255);

  int x0 = HALFW + floor(sin(normalizedTime(period, 0) * TWO_PI) * xRange);
  int x1 = HALFW + floor(sin(normalizedTime(period, phaseOffset) * TWO_PI) * xRange);
  display.drawLine(x0, 0, x1, H, c);

  //draw copies, if any
  if (copies > 1) {
    for (int i = 1; i < copies; i++) {
      x0 = HALFW + (i * xWidth) + floor(sin(normalizedTime(period, i * phaseOffset) * TWO_PI) * xRange);
      x1 = HALFW + (i * xWidth) + floor(sin(normalizedTime(period, (i + 1) * phaseOffset) * TWO_PI) * xRange);
      display.drawLine(x0, 0, x1, H, c);

      x0 = HALFW - (i * xWidth) + floor(sin(normalizedTime(period, i * phaseOffset) * TWO_PI) * xRange);
      x1 = HALFW - (i * xWidth) + floor(sin(normalizedTime(period, (i + 1) * phaseOffset) * TWO_PI) * xRange);
      display.drawLine(x0, 0, x1, H, c);
    }
  }
}

Expanding circles

Two pairs of circles oscillate in radius. The positions of these circles are controlled with layered cyclical mapping macros since we have more parameters to control than knobs at our disposal.

void expandingCircles() {
  // pairs of circles with opposing motion and position
  int xOffset = cyclicalPotMap(0, 3, 0, 0, HALFW);
  int yOffset = cyclicalPotMap(1, 3, 0, 0, HALFH);

  int period0 = potMap(2, 100, 10) * 100;
  int period1 = potMap(3, 100, 10) * 100;

  int rMax0 = cyclicalPotMap(0, 4, 0, 40, HALFW);
  int rMax1 = cyclicalPotMap(1, 4, 0.5, 40, HALFW);

  int c = floor(normalizedTime(period1, 0) * 255);

  int r0 = floor(normalizedTime(period0, 0) * rMax0) + 1;
  int r1 = floor(normalizedTime(period0, 0.5) * rMax1) + 1;

  // First pair (notice opposing offset signs?)
  display.drawCircle(HALFW + xOffset, HALFH - yOffset, r0, c);
  display.drawCircle(HALFW - xOffset, HALFH + yOffset, r0, c);
  // Second pair
  display.drawCircle(HALFW + xOffset, HALFH + yOffset, r1, c);
  display.drawCircle(HALFW - xOffset, HALFH - yOffset, r1, c);
}

Gradient grid

A variable number of rows and columns create a grid of rectangles with a fill color that gets offset by position in the grid and time. A concise function with many surprising moments!

void gradientGrid() {
  // This one has a gap at the end of the rows because of the rounding errors
  int numRows = potMap(0, 1, 64);
  int numCols = potMap(1, 1, 64);
  int rw = ceil((float)W/numCols);
  int rh = ceil((float)H/numRows);

  int index = 0;
  int c = 0;
  int firstC = (millis() / 10) % 254 + 1;
  int iGap = potMap(2, 0, 255);
  int jGap = potMap(3, 0, 255);

  double period = 500.0 + 1000;

  for (int i = 0; i < numCols; i++) {
    for (int j = 0; j < numRows; j++) {
      c = (firstC + (i * iGap) + (j * jGap)) % 254 + 1;

      display.fillRect(i * rw, j * rh, rw, rh, c);
    }
  }
}

Reflected Rects

Rectangles reflected across vertical and horizontal boundaries create graphic compositions. Uses the randomseed() function to control the pace of random position updates.

void reflectedRects() {
  // slow down the pace of random updates 
  // by only allowing new random values once per second
  randomSeed(millis() / 1000);

  // our constraints for the randomness
  int widthLimit = potMap(0, 1, HALFW/2);
  int heightLimit = potMap(1, 1, H/2);
  int quantity = potMap(2, 1, 10);
  int c = potMap(3, 0, 255-20);

  for (int i = 0; i < quantity; i++) {
    int rw = random(1, widthLimit);
    int rh = random(1, heightLimit);
    int x = random(W - rw);
    int y = random(H - rh);
    
    display.fillRect(x, y, rw, rh, c + random(20));
    display.fillRect(W - x - rw, H - y - rh, rw, rh, c + random(20));
    display.fillRect(W - x - rw, y, rw, rh, c + random(20));
    display.fillRect(x, H - y - rh, rw, rh, c + random(20));
  }
}

Bloom

Rings of circles oscillate in radius, rotation, and color for endless blooms of color and shape.

void bloom() {
  int petals = potMap(0, 2, 16);
  int levels = potMap(1, 1, 16);
  int rBase = potMap(2, 2, 24);
  float rGrowth = cyclicalPotMap(3, 2, 0, 1, 100)/10.0f;
  
  float gap = sineTime(4000, 0) * cyclicalPotMap(0, 3, 0, 1, 80);
  float spiral = sineTime(4500, 0) * ((float)cyclicalPotMap(1, 3, 0, 1, 100)/100.0f);
  float offset = sineTime(5700, 0) * ((float)cyclicalPotMap(2, 3, 0, 1, 100)/100.0f);

  int cBase = 0;
  int cPeriod = potMap(3, 300, 30) * 100;
  int cOffset = floor(sineTime(cPeriod, 0) * 255);
  // int color = constrain(cBase + cOffset, 0, 255);
  int color = (cBase + cOffset) % 256;

  int cx = 0;
  int cy = 0;
  int cr = 0;

  for (int i = 0; i < petals; i++) {
    for (int j = 1; j <= levels; j++) {
      cx = floor(HALFW + (sin((float(i) / float(petals)) * TWO_PI + (j * spiral + offset) * TWO_PI) * (j * gap)));
      cy = floor(HALFH + (cos((float(i) / float(petals)) * TWO_PI + (j * spiral + offset) * TWO_PI) * (j * gap)));
      cr = floor(rBase + (j * rGrowth));
      display.fillCircle(cx, cy, cr, color);
    }
  }
}

Bitmaps & text

These example use bitmap graphics. Several of these examples use the "spark" bitmaps I created in class, and one uses the video gem logo. Here are those bitmaps if you'd like to use them — copy both of these text snippets into your bitmaps.h file!

If you want to create your own bitmaps, you can use image2cpp to convert various image types into this char array format.

Sample bitmaps

// 'Video gem logo small', 32x24px
const unsigned char epd_bitmap_Video_gem_logo_small [] PROGMEM = {
	0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x01, 0x80, 0x00, 0x00, 0x01, 0x80, 0x00, 0x00, 0x01, 
	0x80, 0x61, 0x80, 0x01, 0x9b, 0x07, 0x9c, 0x71, 0x9b, 0x6d, 0xb6, 0xd9, 0x9b, 0x6d, 0xbe, 0xd9, 
	0x9b, 0x6d, 0xb0, 0xd9, 0x9a, 0x6d, 0xb2, 0xd9, 0x9c, 0x67, 0x9c, 0x71, 0x80, 0x00, 0x00, 0x01, 
	0x80, 0x00, 0x00, 0x01, 0x8f, 0x39, 0xec, 0x01, 0x9b, 0x6d, 0xb6, 0x01, 0x9b, 0x7d, 0xb6, 0x01, 
	0x9b, 0x61, 0xb6, 0x01, 0x9b, 0x65, 0xb6, 0x01, 0x8f, 0x39, 0xb6, 0x01, 0x93, 0x00, 0x00, 0x01, 
	0x8e, 0x00, 0x00, 0x01, 0x80, 0x00, 0x00, 0x01, 0x80, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0xff
};
// 'Spark 01', 17x17px
const unsigned char epd_bitmap_Spark_01 [] PROGMEM = {
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
	0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x07, 0x70, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 
	0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00
};
// 'Spark 02', 17x17px
const unsigned char epd_bitmap_Spark_02 [] PROGMEM = {
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x40, 0x00, 0x00, 
	0x80, 0x00, 0x00, 0x80, 0x00, 0x08, 0x08, 0x00, 0x06, 0x30, 0x00, 0x08, 0x08, 0x00, 0x00, 0x80, 
	0x00, 0x00, 0x80, 0x00, 0x01, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00
};
// 'Spark 03', 17x17px
const unsigned char epd_bitmap_Spark_03 [] PROGMEM = {
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x20, 0x00, 0x02, 0xa0, 0x00, 0x00, 
	0x80, 0x00, 0x18, 0x8c, 0x00, 0x00, 0x00, 0x00, 0x0e, 0x38, 0x00, 0x00, 0x00, 0x00, 0x18, 0x8c, 
	0x00, 0x00, 0x80, 0x00, 0x02, 0xa0, 0x00, 0x02, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00
};
// 'Spark 04', 17x17px
const unsigned char epd_bitmap_Spark_04 [] PROGMEM = {
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x30, 0x00, 0x06, 0xb0, 0x00, 0x00, 0x80, 0x00, 0x30, 
	0x86, 0x00, 0x30, 0x06, 0x00, 0x01, 0x40, 0x00, 0x1c, 0x1c, 0x00, 0x01, 0x40, 0x00, 0x30, 0x06, 
	0x00, 0x30, 0x86, 0x00, 0x00, 0x80, 0x00, 0x06, 0xb0, 0x00, 0x06, 0x30, 0x00, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00
};
// 'Spark 05', 17x17px
const unsigned char epd_bitmap_Spark_05 [] PROGMEM = {
	0x18, 0x8c, 0x00, 0x18, 0x8c, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x01, 0x80, 0xcc, 0x99, 0x80, 0x0c, 
	0x98, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xcc, 0x99, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 
	0x00, 0x0c, 0x98, 0x00, 0xcc, 0x99, 0x80, 0xc0, 0x01, 0x80, 0x00, 0x00, 0x00, 0x18, 0x8c, 0x00, 
	0x18, 0x8c, 0x00
};
// 'Spark 06', 17x17px
const unsigned char epd_bitmap_Spark_06 [] PROGMEM = {
	0x10, 0x84, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x80, 0x08, 0x88, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x99, 0x4c, 0x80, 0x00, 0x80, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0x08, 0x88, 0x00, 0x80, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
	0x10, 0x84, 0x00
};

// Array of all spark bitmaps for convenience. (Total bytes used to store images in PROGMEM = 480)
const int spark_bitmap_array_LEN = 6;
const unsigned char* spark_bitmap_array[6] = {
	epd_bitmap_Spark_01,
	epd_bitmap_Spark_02,
	epd_bitmap_Spark_03,
	epd_bitmap_Spark_04,
	epd_bitmap_Spark_05,
	epd_bitmap_Spark_06
};

Random Sparks

Draw a selected bitmap from the array of sparks bitmaps in a selected color to random locations on screen.

void randomSparks() {
  // display.fillScreen(0);

  int bitmap = potMap(0, 0, 6);
  int xRange = potMap(1, 0, W);
  int yRange = potMap(2, 0, H);
  int c = potMap(3, 0, 256);

  int x = HALFW + random(xRange) - (xRange/2);
  int y = HALFH + random(yRange) - (yRange/2);

  for (int i = 0; i < 10; i++) {
    display.drawBitmap(x, y, spark_bitmap_array[bitmap], 17, 17, c);
  }
}

Animated Sparks

Use the spark bitmaps as an animated sequence that we oscillate back and forth through, with phase offsets to the animation frame and color that is set by the grid. Kind of like gradientGrid() but for bitmaps!

void animatedSpark() {
  display.fillScreen(0);

  for (int i = 0; i < 16; i++) {
    for (int j = 0; j < 12; j++) {
      int x = i * 20;
      int y = j * 20;

      float xPhaseIncrement = potMap(0, 0, 21) / 20.0;
      float yPhaseIncrement = potMap(1, 0, 21) / 20.0;
      float phaseOffset = i * xPhaseIncrement + j * yPhaseIncrement;
      int bitmapIndex = floor(pingPongTime(1000, phaseOffset) * 5); // 0-5

      int xColorIncrement = potMap(2, 0, 256);
      int yColorIncrement = potMap(3, 0, 256);
      int c = (i * xColorIncrement + j * yColorIncrement) % 255;

      display.drawBitmap(x, y, spark_bitmap_array[bitmapIndex], 17, 17, c);
    }
  }
}

Spark brush

Use the spark bitmap as a brush for a painterly effect on a curving lissajous pattern defined by an x period and a y period.

void sparkBrush() {
  // display.fillScreen(0);

  int xPeriod = potMap(0, 50, 10) * 100;
  int yPeriod = potMap(1, 50, 10) * 100;
  int bitmap = potMap(2, 0, 6);
  int c = potMap(3, 0, 256);

  int x = floor(sineTime(xPeriod, 0.5) * HALFW) + HALFW;
  int y = floor(sineTime(yPeriod, 0) * HALFH) + HALFH;

  display.drawBitmap(x - 8, y - 8, spark_bitmap_array[bitmap], 17, 17, c);
}

Draw a big bitmap in the middle of the screen. Not really a video synth program but maybe useful for a boot screen or something :) This function is also dependent on a helper function, included below.

void scaledLogo() {
  display.fillScreen(0);
  int scale = 3;
  int w = 32;
  int h = 24;
  int x = HALFW - (w * scale)/2;
  int y = HALFH - (h * scale)/2;

  drawScaledBitmap(x, y, epd_bitmap_Video_gem_logo_small, w, h, scale, 255);
}

void drawScaledBitmap(int x, int y, const unsigned char* bitmap, int w, int h, int scale, int color) {
  for (int row = 0; row < h; row++) {
    for (int col = 0; col < w; col++) {
      int bitIndex = row * w + col;
      uint8_t byte = pgm_read_byte(&bitmap[bitIndex / 8]);
      bool lit = (byte >> (7 - (bitIndex % 8))) & 1;
      display.fillRect(x + col * scale, y + row * scale, scale, scale, lit ? color : 0);
    }
  }
}

void drawScaledBitmapWithTransparency(int x, int y, const unsigned char* bitmap, int w, int h, int scale, int color) {
  for (int row = 0; row < h; row++) {
    for (int col = 0; col < w; col++) {
      int bitIndex = row * w + col;
      uint8_t byte = pgm_read_byte(&bitmap[bitIndex / 8]);
      bool lit = (byte >> (7 - (bitIndex % 8))) & 1;
      if (lit) {
        display.fillRect(x + col * scale, y + row * scale, scale, scale, color);
      }
    }
  }
}

Moving words

Draw some text to the screen in a custom font at random locations with controllable color, size, and update rate.

void movingWords() {
  display.setFont(&FreeSerifItalic12pt7b);
  display.setTextWrap(false);

  int period = potMap(0, 50, 5) * 100;
  randomSeed(millis() / period);

  int x = random(W);
  int y = random(H);

  int widthMultiple = potMap(1, 1, 5);
  int heightMultiple = potMap(2, 1, 5);
  int c = potMap(3, 0, 256);


  display.setCursor(x, y);
  display.setTextColor(c);
  display.setTextSize(widthMultiple, heightMultiple);

  display.print("RMNA");
}

Simple sentence

Draw a longer string to text to demonstrate how it wraps automatically.

void simpleSentence() {
  display.fillScreen(0);
  display.setFont(&FreeSerifItalic12pt7b);
  display.setTextWrap(true);

  int x = potMap(0, 0, W);
  int y = potMap(1, 0, H);
  int sizeMultiple = potMap(2, 1, 5);
  int c = potMap(3, 0, 256);


  display.setCursor(x, y);
  display.setTextSize(sizeMultiple);
  display.setTextColor(c);

  display.print("Made with love by RMNA!");
}
void blinkyDots() {
  if (millis() % 1000 > 500) {
    display.fillCircle(80, 60, 40, 255);
  }
  if (millis() % 750 > 300) {
    display.fillCircle(240, 60, 40, 255);
  }
  if (millis() % 290 > 100) {
    display.fillCircle(80, 180, 40, 255);
  }
  if (millis() % 3000 > 2000) {
    display.fillCircle(240, 180, 40, 255);
  }
}
void randomCircle() {
  // This is a hack to get a random value that only updates every so often
  // instead of every time this function is called
  // The random seed provides a predictable starting point for the randomness!
  randomSeed(millis() / 500);

  int x = random(W);
  int y = random(H);
  int r = random(10, 50);

  display.fillCircle(x, y, r, 255);
}