Visit the sketch here.
Music of the Wind is a p5 sketch that is inspired in the book The Nature of Code by Daniel Shiffman and Flow Fields by Tyler Hobbs
The idea was to use the free Open Meteo Live Wind Data API to track the wind direction and intensity in real time. This later would be used to draw the wind vehicles that would paint the colors across the canvas following the flow fields drawn by the wind data and some randomness. In the canvas there would be different particles that carry a musical diatonic jazzy chord. The interaction between the particles and the wind vehicles would produce what I called the Music of the Wind.
Here I am loading a the p5.js library and the Tone.js library that would be used to create the sketch and to play the chord sounds. At the same time, I used this to load some fonts that would be used for the text in the piece.
<!DOCTYPE html>
<html>
<head>
<script src="<https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.7.0/p5.min.js>"></script>
<script src="<https://cdnjs.cloudflare.com/ajax/libs/tone/14.8.49/Tone.js>"></script>
<meta charset="utf-8" />
<title>The Music of the Wind</title>
<link rel="preconnect" href="<https://fonts.googleapis.com>">
<link rel="preconnect" href="<https://fonts.gstatic.com>" crossorigin>
<link href="<https://fonts.googleapis.com/css2?family=EB+Garamond:ital,wght@0,400;0,600;1,400&display=swap>" rel="stylesheet">
<link rel="stylesheet" type="text/css" href="style.css" />
</head>
<body>
<script src="wind.js"></script>
<script src="flowfield.js"></script>
<script src="vehicle.js"></script>
<script src="chord.js"></script>
<script src="sketch.js"></script>
</body>
</html>
This css file was used to edit and define the overall aesthetic of the html DOM elements.
html, body {
margin: 0;
padding: 0;
background: #fafafa;
font-family: 'EB Garamond', Georgia, serif;
display: flex;
flex-direction: column;
align-items: center;
min-height: 100vh;
}
canvas {
display: block;
}
.title {
margin: 30px 0 4px;
font-size: 28px;
font-weight: 600;
letter-spacing: 0.05em;
color: #222;
}
.instructions {
margin: 0 0 16px;
font-size: 14px;
font-style: italic;
color: #888;
letter-spacing: 0.02em;
}
The p5 sketch is being used to call the diverse classes for the wind vehicles, wind flow fields, chord particles and the wind data processing. Aside from calling these classes, in this p5 sketch I am also drawing a bitmap/trailBuffer/pixel by pixel image that would allow me to draw the texture on the paper canvas, the wind vehicle brushstrokes and the splattered chord particles without erasing each other by using alpha and allowing me to preserve all the brushstrokes.
Another important thing that is happening in the sketch is fetching the data from the wind API, as well as the creation of the DOM elements.
// The Nature of Code
// Daniel Shiffman
// <http://natureofcode.com>
// Flow Field Following with Live Wind Data
// Via Reynolds: <http://www.red3d.com/cwr/steer/FlowFollow.html>
//inspired by Tyler Hobbs Flow Fields
// via: <https://www.tylerxhobbs.com/words/flow-fields>
// Using this variable to decide whether to draw all the stuff
let debug = false;
// Flowfield object
let flowfield;
// An ArrayList of vehicles
let vehicles = [];
// Wind manager
let wind;
let chords = [];
let colors = [];
let trailBuffer;
function getColors(){
for(let i = 0; i < 7; i++){
let color = [random(255), random(255), random(255)];
colors.push(color);
}
console.log(colors);
return colors;
}
function setup() {
let title = createElement('h1', 'Music of the Wind');
title.class('title');
let instructions = createP('Click the mouse to generate a new flow field and start the sound.<br>Press space to toggle the flow field trace lines.');
instructions.class('instructions');
createCanvas(1280, 480);
trailBuffer = createGraphics(1280, 480);
// Paper texture: dibujar bitmap de pixeles para dibujar la textura pixel por pixel
trailBuffer.loadPixels();
for (let x = 0; x < trailBuffer.width; x++) {
for (let y = 0; y < trailBuffer.height; y++) {
let grain = noise(x * 0.5, y * 0.5) * 30 + noise(x * 3, y * 3) * 15;
let v = 245 - grain;
let idx = (x + y * trailBuffer.width) * 4;
trailBuffer.pixels[idx] = v;
trailBuffer.pixels[idx + 1] = v;
trailBuffer.pixels[idx + 2] = v;
trailBuffer.pixels[idx + 3] = 255;
}
}
trailBuffer.updatePixels();
//get colors
getColors();
// Make a new flow field with "resolution" of 20
flowfield = new FlowField(20);
// Make a whole bunch of vehicles with random maxspeed and maxforce values
for (let i = 0; i < 150; i++) {
let c = random(colors);
vehicles.push(
new Vehicle(random(width), random(height), random(0.5, 2), random(0.05, 0.2), c[0], c[1], c[2])
);
}
// Create wind manager and fetch initial data
wind = new WindManager();
wind.fetchWind().then(() => {
if (wind.hasData()) {
flowfield.init(wind.windAngle, wind.variationRange);
}
});
//create chord particles
for(let i = 0; i < 7; i++){
let c = colors[i];
chords.push(new Chord(random(width), random(height), i + 1, c[0], c[1], c[2]));
}
}
function draw() {
// Auto-refresh wind data every 5 minutes
if (wind && wind.shouldRefresh()) {
wind.fetchWind().then(() => {
if (wind.hasData()) {
flowfield.init(wind.windAngle, wind.variationRange);
}
});
}
// Update vehicles and draw trails onto the buffer
for (let i = 0; i < vehicles.length; i++) {
vehicles[i].follow(flowfield);
vehicles[i].separate(vehicles);
vehicles[i].update();
vehicles[i].borders();
vehicles[i].drawTrail(trailBuffer); // dibuja en este bitmap
}
for(let c of chords){
c.interactWithVehicles(vehicles);
}
// Composite: trail buffer first, then chord particles on top
background(255);
image(trailBuffer, 0, 0);
// Display the flowfield in "debug" mode
if (debug) flowfield.show();
for(let c of chords){
c.separate(chords);
c.update();
c.borders();
c.show();
}
}
function keyPressed() {
if (key == " ") {
debug = !debug;
}
}
// Make a new flowfield — new noise pattern but preserve wind bias
function mousePressed() {
Tone.start();
Tone.Transport.start();
if (wind && wind.hasData()) {
flowfield.init(wind.windAngle, wind.variationRange);
} else {
flowfield.init();
}
}
function drawWindHUD() {
push();
// Semi-transparent background box in top-right
let boxW = 160;
let boxH = 105;
let boxX = width - boxW - 10;
let boxY = 10;
fill(0, 0, 0, 160);
noStroke();
rect(boxX, boxY, boxW, boxH, 6);
fill(255);
noStroke();
textSize(11);
textAlign(LEFT, TOP);
let tx = boxX + 10;
let ty = boxY + 8;
if (wind && wind.hasData()) {
text("Wind: " + nf(wind.windSpeed, 1, 1) + " km/h", tx, ty);
text("Direction: " + wind.directionLabel(), tx, ty + 16);
text("Variation: \u00B1" + nf(degrees(wind.variationRange), 1, 1) + "\u00B0", tx, ty + 32);
// Draw wind direction arrow
let arrowCx = boxX + boxW - 28;
let arrowCy = boxY + boxH - 28;
let arrowLen = 14;
push();
translate(arrowCx, arrowCy);
rotate(wind.windAngle);
stroke(255);
strokeWeight(2);
line(-arrowLen, 0, arrowLen, 0);
// Arrowhead
fill(255);
noStroke();
triangle(arrowLen, 0, arrowLen - 5, -3, arrowLen - 5, 3);
pop();
} else if (wind && wind.error) {
text("Wind: error", tx, ty);
text(wind.error, tx, ty + 16);
text("Using Perlin noise", tx, ty + 32);
} else {
text("Wind: loading...", tx, ty);
}
pop();
}
This vehicle class is inspired on the Nature of Code vehicles theory. The idea was to create these autonomous wind vehicles that would follow a certain trail, separate from one another, re appear once they get out of the borders of the canvas on the opposite border, as well as draw trails by using the bitmap defined on the P5 js sketch.
// The Nature of Code
// Daniel Shiffman
// <http://natureofcode.com>
// The "Vehicle" class
class Vehicle {
constructor(x, y, ms, mf, r, g, b) {
this.position = createVector(x, y);
this.prevPosition = this.position.copy();
this.acceleration = createVector(0, 0);
this.velocity = createVector(0, 0);
this.r = 4;
this.maxspeed = ms;
this.maxforce = mf;
this.red = r;
this.green = g;
this.blue = b;
this.strokeW = random(3, 7);
this.alpha = random(60, 140);
this.bristles = floor(random(3, 16));
this.bristleSpread = this.strokeW * 0.6;
}
run(vehicles) {
this.separate(vehicles);
this.update();
this.borders();
}
// Separation: steer away from nearby vehicles to prevent clumping
separate(vehicles) {
let desiredSeparation = this.r * 3;
let steer = createVector(0, 0);
let count = 0;
for (let other of vehicles) {
let d = p5.Vector.dist(this.position, other.position);
if (d > 0 && d < desiredSeparation) {
let diff = p5.Vector.sub(this.position, other.position);
diff.normalize();
diff.div(d); // Weight by distance — closer = stronger repulsion
steer.add(diff);
count++;
}
}
if (count > 0) {
steer.div(count);
steer.setMag(this.maxspeed);
steer.sub(this.velocity);
steer.limit(this.maxforce);
this.applyForce(steer);
}
}
// Implementing Reynolds' flow field following algorithm
// <http://www.red3d.com/cwr/steer/FlowFollow.html>
follow(flow) {
// What is the vector at that spot in the flow field?
let desired = flow.lookup(this.position);
// Scale it up by maxspeed
desired.mult(this.maxspeed);
// Steering is desired minus velocity
let steer = p5.Vector.sub(desired, this.velocity);
steer.limit(this.maxforce); // Limit to maximum steering force
this.applyForce(steer);
}
applyForce(force) {
// We could add mass here if we want A = F / M
this.acceleration.add(force);
}
// Method to update location
update() {
this.prevPosition = this.position.copy();
// Update velocity
this.velocity.add(this.acceleration);
// Limit speed
this.velocity.limit(this.maxspeed);
this.position.add(this.velocity);
// Reset acceleration to 0 each cycle
this.acceleration.mult(0);
}
// Wraparound — reset prevPosition on wrap to avoid lines across screen
borders() {
let wrapped = false;
if (this.position.x < -this.r) { this.position.x = width + this.r; wrapped = true; }
if (this.position.y < -this.r) { this.position.y = height + this.r; wrapped = true; }
if (this.position.x > width + this.r) { this.position.x = -this.r; wrapped = true; }
if (this.position.y > height + this.r) { this.position.y = -this.r; wrapped = true; }
if (wrapped) this.prevPosition = this.position.copy();
}
drawTrail(pg) {
pg.strokeCap(ROUND);
// Direction perpendicular to the stroke for bristle spread
let dx = this.position.x - this.prevPosition.x;
let dy = this.position.y - this.prevPosition.y;
let len = sqrt(dx * dx + dy * dy);
if (len < 0.01) return;
// Perpendicular unit vector
let px = -dy / len;
let py = dx / len;
for (let i = 0; i < this.bristles; i++) {
let offset = map(i, 0, this.bristles - 1, -this.bristleSpread, this.bristleSpread);
let jitter = random(-0.5, 0.5);
let ox = px * (offset + jitter);
let oy = py * (offset + jitter);
let a = this.alpha * random(0.4, 1.0);
let w = this.strokeW / this.bristles * random(0.8, 1.5);
pg.stroke(this.red, this.green, this.blue, a);
pg.strokeWeight(w);
pg.line(
this.prevPosition.x + ox, this.prevPosition.y + oy,
this.position.x + ox + random(-0.3, 0.3), this.position.y + oy + random(-0.3, 0.3)
);
}
}
}
This class manages the data that is being fetched from the Open Meteo Live Wind API for the state of New York. Basically is defining the data in its own variables and getting those values from the API. At the same time, is converting the degrees obtained from the API into radians to be used in the p5 sketch in that format. Finally is returning some of this obtained data and also some other calculations related to it.