"use strict"; // // Voicegardens front-end Javascript // // URL which exposes the archive saving API end-point var archiveUrl = window.location + "add-to-archive"; // The x,y coordinates which gives the center of the canvas var centerX; var centerY; // The canvas frame rate which controls how many times the draw function is // called. So far as we have seen, this can affect performance. Try the value // of 30 and also 60 to see the difference. Higher values may also give more // fun animation effects. var frameRate = 30; // Sound recording API objects var microphone; var recorder; var recording; // Boolean which is only true when the user stops the recording of a sound. // This then triggers the generation of a shape based on that recording. var newSoundJustRecorded = false; // All user clickable buttons var playButton; var recordButton; var stopButton; var chorusButton; // Time-out used to stop a recording in case the user forgets to stop it // themselves. It also gurantees to turn off the mic in case of some unknown // error that we didn't take into consideration. This is 30 seconds in // milliseconds. var recordingTimeout = 30000; // The x,y coordinates which shows where the window position is. This is useful // because we use the `translate` function to offset the window view in // relation to the canvas (users can drag their view across the "environment") // and we need to record where that x position leaves us. var screenX = 0; var screenY = 0; var toScreenX = 0; var toScreenY = 0; // All shapes generated var shapes = []; // Sound properties var amplitude; var duration; // random shape positioning var positionTick = false; var timer = 0; function record() { /** * Start recording a sound. **/ if (microphone.enabled) { setTimeout(recorder.record(recording), recordingTimeout); } } function stop() { /** * Stop recording a new sound. **/ if (recorder.recording) { recorder.stop(); // signal to the draw loop that we should generate a new shape newSoundJustRecorded = true; // https://p5js.org/reference/#/p5.SoundFile/getPeaks amplitude = recording.getPeaks(1)[0] * 100; // https://p5js.org/reference/#/p5.SoundFile/duration duration = recording.duration(); // automatically archive this sound to the back-end sendToArchive(); } } function play() { /** * Play the sound just recorded. **/ if (recording.isLoaded()) { recording.play(); } } function sendToArchive() { /** * Send the sound to the back-end for archiving. **/ var soundBlob = recording.getBlob(); var formData = new FormData(); var date = new Date(); var filename = date.getTime().toString() + ".wav"; formData.append("file", soundBlob, filename); var config = new Headers({ "Content-Type": "multipart/form-data" }); axios.post(archiveUrl, formData, config).catch(function(error) { console.log( "Upload failed!", "Received the following message:", error.response.data ); }); } function setupRecording() { /** * Setup logic for sound recording. **/ microphone = new p5.AudioIn(); microphone.start(); recorder = new p5.SoundRecorder(); recorder.setInput(microphone); recording = new p5.SoundFile(); recordButton = createImg("../static/images/RECORD-COLOR.png"); recordButton.position(10, 5); recordButton.mousePressed(record); stopButton = createImg("../static/images/STOP-BW.png"); stopButton.position(10, 40); stopButton.mousePressed(stop); playButton = createImg("../static/images/PLAY-BW.png"); playButton.position(10, 75); playButton.mousePressed(play); chorusButton = createImg("../static/images/CHORUS-BW.png"); chorusButton.position(10, 110); } class GeneratedShape { constructor() { /** * Initialise the new shape. **/ // The opacity of the shape. This controls whether we can see the shape or // not (transparency). It starts at zero as we want to fade the shapes in // when they enter the environment this.opacity = 0; // the colour of the shape this.colour = this.chooseColour(); // Acceleration x,y values which control at which speed the shape // accelerates towards the intended x,y destination. this.accelX = 0.0; this.accelY = 0.0; // The x, y destination values which the shape moves towards. This can be // calculated against the `mouseX` and `mouseY` values, for example. Then // the shape will follow the mouse. this.deltaX = 0.0; this.deltaY = 0.0; // The speed at which the shape 'springs' towards its final destination. this.springing = 0.0009; // The speed at which the shape rocks back and forth when it is in the // process of coming to a halt. this.damping = 0.98; // Value that controls the tightness or looseness of the curves between the // x,y values of the shape. AFAIK, this value can go between -5 and +5. // With +5 we have very sharp curves and edges. this.organicConstant = random(-5, 5); // The x,y values which determine where the shape is currently. These are // required in order to calculate where the shape is currently so that we // can then go about moving it to the new destination. this.startXs = []; this.startYs = []; // The x,y values which track the new position of the shape (and therefore // update the `startXs` and `startYs`) as the shape moves about the // environment this.xs = []; this.ys = []; // vector listing of the above xs, ys so that we can pass these lists to // the collidePolyPoly function for collision detection which expects // vector objects, not plain x, y coordinates as in xs, ys. this.vectors = []; // Angles between xs and ys. this.angles = []; // Curve movement wobble frequencies. this.frequencies = []; // Random x,y values (only randomly chosen once, then fixed) which are used // in the calculation of the curve drawing between the x,y vectors of the // shape this.randXs = []; this.randYs = []; // Number of edges of the shape this.edges = amplitude + 3; // The distance between the xs and ys influencing the size of the shape. // The randomXs, randomYs also influence the shape size as they are added // when drawing the space between xs and ys. this.radius = ceil(duration * 40); // ??? this.angle = radians(360 / this.edges); // ??? this.centerX = random(windowWidth); this.centerY = random(windowHeight); // new destination for the shapes this.destX = random(windowWidth); this.destY = random(windowHeight); // the sound recording attached to this shape this.recording; // the shape that was last collided with this.lastCollidedShape; this.initialise(); } initialise() { /** * Initialise the shape values. **/ for (let i = 0; i < this.edges; i++) { this.startXs[i] = 0; this.startYs[i] = 0; this.xs[i] = 0; this.ys[i] = 0; this.vectors[i] = createVector(this.xs[i], this.ys[i]); this.angles[i] = 0; // this directly influences the shape of the size alongside the // this.radius shape value this.randXs[i] = ceil(duration * random(-30, 30)); this.randYs[i] = ceil(duration * random(-30, 30)); } for (let i = 0; i < this.edges; i++) { this.frequencies[i] = random(5, 12); } } collide(shapes) { /** * Detect if the shape collides with another shape. Returns a tuple of type * [bool, shape] where bool = if there was a collision and shape = the * collided shape. **/ if (shapes.length === 1) { return [false, undefined]; } for (let i = 0; i < shapes.length; i++) { let shape = shapes[i]; if (this === shape || this.lastCollidedShape === shape) { continue; } // don't detect if one shape is fully inside another let interiorCollision = false; var collision = collidePolyPoly( this.vectors, shape.vectors, interiorCollision ); if (collision === true) { this.lastCollidedShape = shape; return [true, shape]; } } return [false, undefined]; } sound() { /** * Play a sound after a collision is detected. **/ if (this.recording.isLoaded()) { this.recording.play(); } } docolour() { /** * Draw colour and fade-in shape. **/ if (this.opacity != 256) { if (this.opacity < 256) { // shape should fade in, so increment alpha value let currentAlpha = this.colour._getAlpha(); this.colour.setAlpha(currentAlpha + random(0, 3)); } else { // shape has faded-in, show it as fully visible now this.opacity = 256; } } fill(this.colour); } chooseColour() { /** * Choose a colour for the shape. **/ // TODO: choose nicer colours // TODO: Can we have gradient colours // TODO: Can we have multiple colours let colourChoices = [ color("red"), color("blue"), color("green"), color("black") ]; let index = floor(random(0, colourChoices.length)); let chosenColour = colourChoices[index]; // set shape opacity to 0 initially to enable fade-in chosenColour.setAlpha(this.opacity); return chosenColour; } curve() { /** * Curve the shape. **/ curveTightness(this.organicConstant); beginShape(); for (let i = 0; i < this.edges; i++) { curveVertex(this.xs[i], this.ys[i]); } endShape(CLOSE); } draw() { /** * Draw the shape vectors. **/ this.docolour(); // set the start x,y positions for the shape on each draw loop for (let i = 0; i < this.edges; i++) { this.startXs[i] = this.centerX + cos(this.angle * i) * this.radius + this.randXs[i]; this.startYs[i] = this.centerY + sin(this.angle * i) * this.radius + this.randYs[i]; } this.curve(); } move() { /** * Move the shape vectors. **/ this.deltaX = this.destX - this.centerX - toScreenX; this.deltaY = this.destY - this.centerY - toScreenY; this.deltaX *= this.springing; this.deltaY *= this.springing; this.accelX += this.deltaX; this.accelY += this.deltaY; this.centerX += this.accelX; this.centerY += this.accelY; this.accelX *= this.damping; this.accelY *= this.damping; this.organicConstant = 1 - (abs(this.accelX) + abs(this.accelY)) * 0.1; for (let i = 0; i < this.edges; i++) { this.xs[i] = this.startXs[i] + sin(radians(this.angles[i])) * (this.accelX * 2); this.ys[i] = this.startYs[i] + sin(radians(this.angles[i])) * (this.accelY * 2); this.vectors[i] = createVector(this.xs[i], this.ys[i]); this.angles[i] += this.frequencies[i]; } } } function setup() { /** * The initial setup function called once on start. **/ createCanvas(windowWidth, windowHeight); frameRate(frameRate); setupRecording(); } function draw() { /** * The draw loop which is called x times a second where x is the frameRate. **/ background("white"); blendMode(BLEND); smooth(); noStroke(); // count random waiting times in seconds until choosing a new destX, destY // for a moving shape let nextPositionTick = random(3000, 8000); let tickingTime = millis(); if (tickingTime >= nextPositionTick + timer) { positionTick = true; timer = millis(); } // offset the window view based on new values of x,y related to the screen. // These values are generated once the user drags the screen with the mouse. screenX = lerp(screenX, toScreenX, 0.2); screenY = lerp(screenY, toScreenY, 0.2); translate(screenX, screenY); // generate a new shape after a sound recording if (newSoundJustRecorded === true) { let newShape = new GeneratedShape(); newShape.recording = recording; shapes.push(newShape); newSoundJustRecorded = false; } for (let i = 0; i < shapes.length; i++) { let shape = shapes[i]; // randomly move the shapes if (positionTick) { shape.destX = random(windowWidth); shape.destY = random(windowHeight); // also reset last collided shape shape.lastCollidedShape = undefined; } shape.draw(); shape.move(); // play recordings when shapes collide let [collision, collidedShape] = shape.collide(shapes); if (collision === true) { shape.sound(); collidedShape.sound(); } } // reset random shape position time ticker positionTick = false; } function mouseDragged() { /** * Mouse drag movement handling. **/ toScreenX += mouseX - pmouseX; toScreenY += mouseY - pmouseY; } function windowResized() { /** * Canvas re-draw handling. **/ resizeCanvas(windowWidth, windowHeight); }