
Inspired by this picture, we decided to create a melody and make its zigzag line move behind the mountain image.
We generated an algorithmic melody using a C Major Pentatonic scale with five notes: C (60), D (62), E (64), G (67), and A (69), creating a harmonious and relaxing melody, and playing it in a loop with a p5.js MonoSynth.
let synth;
let melodyNotes;
let noteDurations;
let currentNote = 0;
let scale = [60, 62, 64, 67, 69]; // C Major Pentatonic scale
let bpm = 180;
let noteInterval;
let startTime;
let noiseOffset = 0;
function setup() {
createCanvas(400, 400);
synth = new p5.MonoSynth();
// Seed noise and random
noiseSeed(0);
randomSeed(0);
// Generate a melody and rhythm algorithmically
melodyNotes = generateMelody();
noteDurations = generateDurations(melodyNotes.length);
// Calculate time between notes
noteInterval = (60 / bpm) * 1000;
startTime = millis();
}
function draw() {
background(200, 150, 250);
// Play notes based on the algorithm
if (millis() - startTime >= noteInterval) {
playNote();
startTime = millis();
}
}
function generateMelody() {
let melody = [];
for (let i = 0; i < 32; i++) { // 32 notes for 60 seconds
let noiseValue = noise(noiseOffset);
let noteIndex = floor(map(noiseValue, 0, 1, 0, scale.length));
melody.push(scale[noteIndex]);
noiseOffset += 0.1;
}
return melody;
}
function generateDurations(length) {
let durations = [];
for (let i = 0; i < length; i++) {
durations.push(random([0.25, 0.5, 1]) * (60 / bpm)); // quarter, half, whole notes
}
return durations;
}
function playNote() {
if (currentNote < melodyNotes.length) {
let note = midiToFreq(melodyNotes[currentNote]);
let duration = noteDurations[currentNote];
synth.play(note, 0.5, 0, duration);
currentNote++;
} else {
currentNote = 0; // Loop the melody
}
}
We created a zigzag line that moves in sync with the MIDI notes of the melody. When the melody plays, the line moves along with the MIDI notes. When the melody stops, the line remains flat.
let zigzagLinePoints = [];
let maxPoints = 50;
let amplitude = 30;
let yOffset = -50;
let isPlaying = true;
// Initialize the zigzag line points
for (let i = 0; i < maxPoints; i++) {
zigzagLinePoints.push({
x: i * (width / maxPoints),
y: height / 2 + yOffset,
});
}
}
function draw() {
background(0);
// Draw the zigzag line
noFill();
stroke(255);
strokeWeight(4);
beginShape();
for (let point of zigzagLinePoints) {
vertex(point.x, point.y);
}
endShape();
// Play notes and update zigzag line
if (isPlaying && millis() - startTime >= noteInterval) {
playNote(); // Sync sound and zigzag line update
startTime = millis();
} else if (!isPlaying) {
// Keep the zigzag line flat when paused
maintainZigzagLine();
}
}
function updateZigzagLine() {
// Remove the leftmost point
zigzagLinePoints.shift();
// Calculate the y-coordinate of the new point based on the current MIDI note
let midiValue = melodyNotes[currentNote % melodyNotes.length]; // Current MIDI note
let newY =
map(
midiValue,
min(scale),
max(scale),
height / 2 - amplitude * 2,
height / 2 + amplitude * 2
) + yOffset;
// Add a new point to the rightmost end of the line
let newX = width;
zigzagLinePoints.push({ x: newX, y: newY });
// Move all points to the left to maintain the dynamic effect
for (let point of zigzagLinePoints) {
point.x -= width / maxPoints;
}
}
// Maintain a flat line when paused
function maintainZigzagLine() {
zigzagLinePoints.shift();
let newX = width;
let newY = height / 2 + yOffset;
zigzagLinePoints.push({ x: newX, y: newY });
// Move all points to the left to maintain the dynamic effect
for (let point of zigzagLinePoints) {
point.x -= width / maxPoints;
}
}
function mousePressed() {
isPlaying = !isPlaying;
}
We want the line to move behind the mountain, so we use Photoshop to cut out the mountain from the background.


Then we imported both images and drew the cut-out image on top of the zigzag line.
let img;
let topImage;
function preload() {
img = loadImage("mountain2.png");
topImage = loadImage("mountain.png");
}
function draw() {
background(0);
tint(200);
image(img, 0, 0, width, height);
// Draw the zigzag line
noFill();
stroke(255);
strokeWeight(4);
beginShape();
for (let point of zigzagLinePoints) {
vertex(point.x, point.y);
}
endShape()
// Draw the top image
image(topImage, 0, 0, width, height);
}
In the end, we added some text to indicate the actions “Pause” and “Continue.”
function draw() {
// Draw the text
fill(255);
noStroke();
textAlign(CENTER, CENTER);
textSize(16);
let message;
if (isPlaying) {
message = "Click to Pause";
} else {
message = "Click to Continue";
}
text(message, width / 2, height / 2 + 170);
}