xxxxxxxxxx
439
//How to play: click on canvas
// 3D spiral animation with interactive sound-generating balls
// dynamic background styles that change on mouse clicks.
// I. declare all variables used!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// 1. Spiral properties
let spirals = 5; // Total number of complete spirals
let pointsPerSpiral = 30; // Number of points that define each spiral segment
let maxRadius = 150; // Maximum radius of the spiral from the center
let height = 400; // Height of the spiral
let totalPoints = spirals * pointsPerSpiral; // Total number of points in all spirals
let activeBackgrounds = []; // Stores the indices of active backgrounds
// 2. Sound and Ball properties
let balls = []; // Array to hold Ball objects
let scale = [261.63, 293.66, 329.63, 349.23, 392.0, 440.0, 493.88, 523.25];
// Frequencies for a C major scale
let usedIndices = []; // Keeps track of which indices from the scale array have been used
// 3. Background style properties
let backgroundStyle = 0; // Current index of the background style
let maxBackgroundStyles = 10; // Total number of background styles
// II. Setup function!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
function setup() {
createCanvas(600, 700, WEBGL); // Create a 600x700 pixel canvas in WEBGL mode for 3D rendering
angleMode(DEGREES); // Set angle mode to degrees for easier use of trigonometric functions
}
// III. The draw function!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
function draw() {
background(0); // Set background color
// 1. Background
// Render the current background based on the value of backgroundStyle
switch (backgroundStyle) {
case 0:
drawGradientBackground();
break;
case 1:
drawGridBackground();
break;
case 2:
drawStarryBackground();
break;
case 3:
drawWaveBackground();
break;
case 4:
drawRotatingDiamondGridBackground();
break;
case 5:
drawRippleBackground();
break;
case 6:
drawDynamicGridBackground();
break;
case 7:
drawStripedBackground();
break;
case 8:
drawArcBackground();
break;
case 9:
drawFlickeringStarsBackground();
break;
}
//2. The scence
rotateX(60); // Rotate the entire scene by 60 degrees on the X-axis for a better perspective
// Lighting setup to enhance 3D effects
ambientLight(50); // Adds a base level of light for the entire scene
pointLight(255, 255, 255, 0, 0, 300); // Adds a white point light positioned in front of the scene
pointLight(100, 100, 255, -300, 300, 200); // Adds a blue light from a diagonal angle
// 3. Triangle strip that forms the 3D spiral
noFill();
beginShape(TRIANGLE_STRIP); // Start defining a connected triangle strip
for (let i = 0; i <= totalPoints; i++) {
// Map the point index to the corresponding angle in the spiral
let angle = map(i, 0, totalPoints, 0, 360 * spirals);
// Map the point index to the corresponding radius
let rad = map(i, 0, totalPoints, 20, maxRadius);
// Map the point index to the corresponding height along the Z-axis
let z = map(i, 0, totalPoints, -height / 2, height / 2);
// Convert polar coordinates (angle, radius) to Cartesian coordinates (x, y)
let x = rad * cos(angle); // X-coordinate
let y = rad * sin(angle); // Y-coordinate
// Calculate dynamic RGB values for the current point based on time and height
let r = map(sin(frameCount + z), -1, 1, 100, 200); // Red value oscillates over time
let g = map(cos(frameCount + z), -1, 1, 100, 200); // Green value oscillates over time
let b = map(sin(frameCount + z), -1, 1, 200, 100); // Blue value oscillates over time
stroke(r, g, b); // Set the stroke color for the current vertex
vertex(x, y, z); // Add the current point as a vertex in 3D space
// Calculate the next vertex for the triangle strip
let nextRad = rad * 0.95; // Decrease the radius slightly for the next point
let nextZ = z + height / totalPoints; // Move slightly upwards along the height
let nextX = nextRad * cos(angle + 2); // Calculate the next X-coordinate
let nextY = nextRad * sin(angle + 2); // Calculate the next Y-coordinate
vertex(nextX, nextY, nextZ); // Add the next point as a vertex in 3D space
}
endShape(); // End the TRIANGLE_STRIP definition
// 4. Draw Balls
// Update and render all active balls
for (let j = balls.length - 1; j >= 0; j--) {
balls[j].update(); // Update the ball's position, sound, and particles
balls[j].display(); // Render the ball on the screen
if (balls[j].isDone()) {
balls.splice(j, 1); // Remove the ball from the array if its animation is complete
}
}
}
// 5. Interaction
// a. UI: Mouse press event handler
function mousePressed() {
// Reset usedIndices if all scale notes have been used
if (usedIndices.length >= scale.length) {
usedIndices = []; // Clear the used indices
}
// b. Sound: Select a random note that has not yet been used
let availableIndices = scale
.map((_, index) => index)
.filter((index) => !usedIndices.includes(index));
let noteIndex = random(availableIndices); // Randomly pick an available index
usedIndices.push(noteIndex); // Mark the index as used
// c. Ball: Create a new Ball object with the selected frequency
let frequency = scale[noteIndex]; // Get the frequency corresponding to the selected note
let newBall = new Ball(0, 0, -height / 2, 10, frequency); // Instantiate the new ball
newBall.startMoving(); // Start the ball's animation
balls.push(newBall); // Add the ball to the array
// d. Background: Cycle to the next background style
backgroundStyle = (backgroundStyle + 1) % maxBackgroundStyles; // Wrap around if exceeding available styles
// Manage the active backgrounds array
if (activeBackgrounds.length >= 2) {
activeBackgrounds.shift(); // Remove the oldest background style
}
activeBackgrounds.push(backgroundStyle); // Add the current style to the active list
}
// IV. Ball Class!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// The Ball class represents a sound-emitting sphere that moves along the spiral.
// Each Ball creates dynamic particle effects as it moves, and plays a note from the C major scale.
class Ball {
constructor(x, y, z, r, frequency) {
// Initial position and radius of the ball
this.x = x; // X-coordinate of the ball
this.y = y; // Y-coordinate of the ball
this.z = z; // Z-coordinate of the ball
this.r = r; // Radius of the ball
// Sound properties
this.frequency = frequency; // Frequency of the sound the ball will play
this.moving = false; // Indicates whether the ball is moving
this.pathIndex = 0; // Tracks the ball's position along the spiral path
// Oscillator for sound generation
this.osc = new p5.Oscillator("sine"); // Create a sine wave oscillator
this.osc.freq(frequency); // Set the frequency of the oscillator
this.osc.start(); // Start the oscillator
this.osc.amp(0); // Initially set amplitude to 0 (no sound)
// Reverb effect for sound
this.reverb = new p5.Reverb(); // Create a reverb effect
this.reverb.process(this.osc, 1, 2); // Attach the oscillator to the reverb with decay and time settings
// Delay effect for sound
this.delay = new p5.Delay(); // Create a delay effect
this.delay.process(this.osc, 0.05, 0.7, 800); // Add a ping-pong delay with time, feedback, and lowpass filter
this.delay.setType("pingPong"); // Use a ping-pong delay type
// Particle effects
this.particles = []; // Array to store particles emitted by the ball
}
// Updates the ball's position, generates particles, and manages sound amplitude
update() {
if (this.moving) {
if (this.pathIndex < totalPoints) {
// Map the ball's current index to a position along the spiral
let angle = map(this.pathIndex, 0, totalPoints, 0, 360 * spirals);
this.x =
map(this.pathIndex, 0, totalPoints, 20, maxRadius) * cos(angle); // Update x-coordinate
this.y =
map(this.pathIndex, 0, totalPoints, 20, maxRadius) * sin(angle); // Update y-coordinate
this.z = map(this.pathIndex, 0, totalPoints, -height / 2, height / 2); // Update z-coordinate
// Move to the next point in the spiral path
this.pathIndex++;
// Gradually increase the sound amplitude
this.osc.amp(0.5, 0.1); // Smoothly fade in the sound
// Create a new particle at the ball's current position
this.particles.push(new Particle(this.x, this.y, this.z));
} else {
this.stopMoving(); // Stop the ball if it has reached the end of the path
}
}
// Update all particles and remove any that have faded out
for (let i = this.particles.length - 1; i >= 0; i--) {
this.particles[i].update(); // Update particle properties
if (this.particles[i].isDone()) {
this.particles.splice(i, 1); // Remove the particle if its lifespan is over
}
}
}
// Draws the ball and its particle effects
display() {
push(); // Save the current transformation state
translate(this.x, this.y, this.z); // Move to the ball's position
emissiveMaterial(255, 0, 0); // Set the ball's material to red with an emissive glow
sphere(this.r); // Draw the ball as a sphere
pop(); // Restore the previous transformation state
// Draw each particle associated with the ball
for (let particle of this.particles) {
particle.display();
}
}
// Starts the ball's movement and resets its position along the spiral
startMoving() {
this.moving = true; // Set the ball to moving state
this.pathIndex = 0; // Reset the path index to the start of the spiral
}
// Stops the ball's movement and fades out its sound
stopMoving() {
this.moving = false; // Set the ball to stopped state
this.osc.amp(0, 0.5); // Smoothly fade out the sound amplitude
this.osc.stop(0.5); // Stop the oscillator after a short delay
}
// Checks if the ball has finished its animation
isDone() {
return !this.moving && this.pathIndex >= totalPoints; // Returns true if the ball has stopped and completed its path
}
}
// V. Particle class!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// The Particle class represents small particles emitted by the Ball object
class Particle {
constructor(x, y, z) {
this.x = x; // X-coordinate of the particle
this.y = y; // Y-coordinate of the particle
this.z = z; // Z-coordinate of the particle
this.lifespan = 255; // Initial lifespan of the particle (fully visible)
}
// Updates the particle's properties over time
update() {
this.lifespan -= 5; // Gradually reduce the lifespan, causing it to fade out
}
// Draws the particle as a small sphere with fading opacity
display() {
push(); // Save the current transformation state
translate(this.x, this.y, this.z); // Move to the particle's position
noStroke(); // Disable stroke for the particle
fill(255, 255, 100, this.lifespan); // Set the fill color with fading opacity
sphere(3); // Draw the particle as a small sphere
pop(); // Restore the previous transformation state
}
// Checks if the particle has completely faded out
isDone() {
return this.lifespan <= 0; // Returns true if the particle's lifespan is zero or less
}
}
// VI. Background styles!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
function drawGradientBackground() {
push();
resetMatrix();
translate(-width / 2, -height / 2, -500);
for (let y = 0; y < height; y++) {
let r = map(y, 0, height, 100, 255);
let g = map(y, 0, height, 50, 200);
let b = map(y, 0, height, 150, 255);
stroke(r, g, b);
line(0, y, width, y);
}
pop();
}
function drawGridBackground() {
push();
resetMatrix();
translate(-width / 2, -height / 2, -500);
stroke(50, 150, 255, 150);
for (let x = 0; x <= width; x += 20) {
for (let y = 0; y <= height; y += 20) {
point(x, y);
}
}
pop();
}
function drawStarryBackground() {
push();
resetMatrix();
translate(-width / 2, -height / 2, -500);
background(10, 10, 30); // Deep space color
for (let i = 0; i < 200; i++) {
let x = random(width);
let y = random(height);
let brightness = random(150, 255);
stroke(brightness);
point(x, y);
}
pop();
}
function drawWaveBackground() {
push();
resetMatrix();
translate(-width / 2, -height / 2, -500);
noFill();
for (let y = 0; y < height; y += 10) {
beginShape();
for (let x = 0; x < width; x += 10) {
let wave = sin(frameCount * 0.05 + x * 0.1 + y * 0.1) * 20;
stroke(100, 150, 255);
vertex(x, y + wave);
}
endShape();
}
pop();
}
function drawRippleBackground() {
push();
resetMatrix(); // 重置坐标系
translate(0, 0, -500); // 确保 z 轴设置在背景层
translate(width / 2, height / 2); // 将波纹的中心移动到画布中心
noFill();
for (let r = 50; r < width * 1.5; r += 20) {
// 扩展到画布边缘之外
let offset = sin(frameCount * 0.02 + r * 0.1) * 10; // 动态波动效果
stroke(
map(r, 50, width, 100, 255), // 动态 R 颜色
150, // 固定 G 颜色
map(r, 50, width, 255, 100) // 动态 B 颜色
);
strokeWeight(1.5);
ellipse(0, 0, r + offset, r + offset); // 绘制动态波纹
}
pop();
}
function drawRotatingDiamondGridBackground() {
push();
resetMatrix();
translate(-width / 2, -height / 2); // 将网格绘制区域对齐屏幕左上角
translate(width / 2, height / 2); // 平移到屏幕中心
rotate(frameCount * 0.01); // 动态旋转网格
stroke(150, 200, 255, 150); // 设置网格颜色和透明度
let gridSize = 40; // 网格菱形的间距
for (let x = -width; x <= width; x += gridSize) {
for (let y = -height; y <= height; y += gridSize) {
let offset = sin(frameCount * 0.05 + (x + y) * 0.02) * 10; // 动态偏移
line(x - offset, y, x + gridSize / 2, y + gridSize / 2); // 左上到右下
line(x + offset, y, x - gridSize / 2, y - gridSize / 2); // 左下到右上
}
}
pop();
}
function drawDynamicGridBackground() {
push();
resetMatrix();
translate(-width / 2, -height / 2, -500);
for (let x = 0; x < width; x += 50) {
for (let y = 0; y < height; y += 50) {
let offset = sin(frameCount * 0.05 + (x + y) * 0.02) * 20;
stroke(255, 100, map(offset, -20, 20, 100, 255));
noFill();
rect(x + offset, y + offset, 30, 30);
}
}
pop();
}
function drawStripedBackground() {
push();
resetMatrix();
translate(-width / 2, -height / 2, -500);
noStroke();
for (let y = 0; y < height; y += 10) {
let r = map(sin(frameCount * 0.01 + y * 0.1), -1, 1, 100, 255);
let g = map(cos(frameCount * 0.02 + y * 0.2), -1, 1, 50, 200);
let b = map(sin(frameCount * 0.03 + y * 0.3), -1, 1, 150, 255);
fill(r, g, b);
rect(0, y, width, 10);
}
pop();
}
function drawArcBackground() {
push();
resetMatrix();
translate(width / 2, height / 2, -500);
noFill();
for (let i = 0; i < 360; i += 20) {
let offset = sin(frameCount * 0.05 + i * 0.1) * 50;
stroke(map(i, 0, 360, 100, 255), 150, 200);
arc(0, 0, 300 + offset, 300 + offset, radians(i), radians(i + 20));
}
pop();
}
function drawFlickeringStarsBackground() {
push();
resetMatrix();
translate(-width / 2, -height / 2, -500);
background(0, 0, 20); // Deep space background
for (let i = 0; i < 300; i++) {
let x = random(width);
let y = random(height);
let brightness = map(sin(frameCount * 0.05 + i), -1, 1, 100, 255);
stroke(brightness);
point(x, y);
}
pop();
}