
The Kandinsky Challenge: Programming Synesthesia
This benchmark tests an LLM's ability to interpret and implement abstract, philosophical, and artistic theories. It requires the model to translate Wassily Kandinsky's theories on synesthesia (the connection between sound, color, and shape) into an interactive audio-visual experience. Success is judged on the creative fidelity to the artistic concept, not just technical execution.
Prompt
You are a creative coder, an art historian, and a musician, with a deep understanding of the Bauhaus movement and the theories of Wassily Kandinsky, particularly from his book "Concerning the Spiritual in Art". Mission: Create a single, self-contained HTML file that generates an interactive audio-visual experience based on Kandinsky's synesthetic theories. The user should be able to "play" colors and shapes, and see your interpretation of Kandinsky's philosophy in action. Core Features & Kandinsky's Rules to Implement: The Interactive "Piano": Create a simple, minimalist on-screen keyboard with 7 keys, corresponding to the notes C, D, E, F, G, A, B. When a user clicks a key (or presses the corresponding key on their physical keyboard), it must produce a simple sound (use the p5.js Oscillator to generate tones) AND generate a visual response on the canvas. Visual Translation of Sound (Kandinsky's Theory): The visual response for each note must follow Kandinsky's associations. You must interpret and implement the following rules: Yellow Tones (e.g., for high, sharp notes like G, A, B): These notes should generate sharp, energetic shapes like triangles or zig-zag lines. The movement should be fast and expansive. Kandinsky associated this with the sound of a trumpet. Blue Tones (e.g., for deep, low notes like C, D): These notes should generate round, calming, and deep shapes like circles. The movement should be slow and concentric. Kandinsky associated this with the sound of a cello or an organ. Red Tones (e.g., for mid-range, powerful notes like E, F): These notes should generate stable, strong shapes like squares. The color should be a vibrant, powerful red. Kandinsky associated this with the sound of a tuba. Compositional Logic: The shapes should not just appear randomly. They must be composed on the canvas to create a balanced, abstract painting in real-time. Shapes generated should interact. For example, a new "aggressive" yellow triangle might slightly push away an existing "calm" blue circle. The shapes should fade away slowly over time, leaving a trace of the "music" that has been played. Visual Style: The overall aesthetic must be inspired by Kandinsky's abstract paintings. Use a textured, off-white background. The composition must feel balanced and artistic, not chaotic. Technical Constraints: Use the p5.js library (including the p5.sound library) loaded from a CDN. The entire experience must be in a single HTML file. Plan d'Action : Before coding, explain your artistic and technical interpretation. How will you translate the "spirituality" of blue into an animation? How will you manage the "composition" of shapes on the canvas to maintain harmony? Describe the structure of your classes (e.g., Shape, SoundManager) and the main logic.
A system prompt was added to support web rendering
Answer guidance
A successful response is a beautiful and functional audio-visual experience that creatively interprets Kandinsky's theories. The focus is on the artistic and conceptual fidelity. Checklist: Are the sound-color-shape associations correctly implemented? Is the experience interactive and responsive? Does the visual composition feel "Kandinskian" and harmonious, rather than just random shapes appearing? Is the overall aesthetic compelling and artistic? The key question is not "Is the code perfect?" but "Has the AI managed to become an artist?"
```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Kandinsky Synesthetic Experience</title> <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/addons/p5.sound.min.js"></script> <style> body { margin: 0; padding: 0; background-color: #f5f5f5; font-family: 'Helvetica Neue', sans-serif; display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 100vh; } #canvas-container { border: 1px solid #ccc; box-shadow: 0 0 20px rgba(0,0,0,0.1); margin-bottom: 20px; } .keyboard { display: flex; gap: 5px; margin-top: 20px; } .key { width: 60px; height: 150px; background-color: white; border: 1px solid #999; border-radius: 5px; display: flex; align-items: flex-end; justify-content: center; padding: 10px; cursor: pointer; user-select: none; box-shadow: 0 4px 6px rgba(0,0,0,0.1); transition: all 0.1s ease; } .key:hover { background-color: #f0f0f0; } .key:active { transform: translateY(4px); box-shadow: 0 0 6px rgba(0,0,0,0.1); } .key-label { font-size: 14px; font-weight: bold; color: #333; } .title { font-size: 24px; margin-bottom: 10px; color: #333; text-align: center; } .instructions { max-width: 600px; text-align: center; color: #666; margin-bottom: 20px; line-height: 1.5; } </style> </head> <body> <div class="title">Kandinsky Synesthetic Experience</div> <div class="instructions">Click the keys or press Q,W,E,R,T,Y,U to play. Each note creates shapes according to Kandinsky's color-theory of sound.</div> <div id="canvas-container"></div> <div class="keyboard"> <div class="key" id="key-C"><div class="key-label">C</div></div> <div class="key" id="key-D"><div class="key-label">D</div></div> <div class="key" id="key-E"><div class="key-label">E</div></div> <div class="key" id="key-F"><div class="key-label">F</div></div> <div class="key" id="key-G"><div class="key-label">G</div></div> <div class="key" id="key-A"><div class="key-label">A</div></div> <div class="key" id="key-B"><div class="key-label">B</div></div> </div> <script> // Kandinsky's synesthetic mapping const noteData = { 'C': { color: [30, 60, 180], shape: 'circle', frequency: 261.63, pos: 0 }, // Blue - Cello 'D': { color: [20, 50, 160], shape: 'circle', frequency: 293.66, pos: 1 }, // Blue - Organ 'E': { color: [220, 40, 40], shape: 'square', frequency: 329.63, pos: 2 }, // Red - Tuba 'F': { color: [200, 30, 30], shape: 'square', frequency: 349.23, pos: 3 }, // Red - Trumpet (lower) 'G': { color: [255, 220, 0], shape: 'triangle', frequency: 392.00, pos: 4 }, // Yellow - Trumpet 'A': { color: [255, 240, 30], shape: 'triangle', frequency: 440.00, pos: 5 },// Yellow - Violin 'B': { color: [255, 255, 60], shape: 'triangle', frequency: 493.88, pos: 6 } // Yellow - Flute }; // Map computer keys to notes const keyMap = { 'q': 'C', 'w': 'D', 'e': 'E', 'r': 'F', 't': 'G', 'y': 'A', 'u': 'B' }; let oscs = {}; let shapes = []; let canvas; class KandinskyShape { constructor(note) { this.note = note; const data = noteData[note]; this.color = data.color; this.shapeType = data.shape; this.life = 255; this.maxLife = 255; // Position with some variation this.x = random(100, width - 100); this.y = random(100, height - 100); // Size based on note frequency (higher = smaller visual weight) this.size = map(data.frequency, 200, 500, 80, 30); // Movement properties this.vx = random(-1, 1); this.vy = random(-1, 1); // For interaction this.radius = this.size; } update() { // Slowly move this.x += this.vx * 0.5; this.y += this.vy * 0.5; // Boundary check with bounce if (this.x < this.size || this.x > width - this.size) this.vx *= -1; if (this.y < this.size || this.y > height - this.size) this.vy *= -1; // Apply interaction forces from other shapes for (let other of shapes) { if (other !== this) { let d = dist(this.x, this.y, other.x, other.y); let minDist = this.size/2 + other.size/2; if (d < minDist && d > 0) { // Collision response let angle = atan2(this.y - other.y, this.x - other.x); let targetX = other.x + cos(angle) * minDist; let targetY = other.y + sin(angle) * minDist; this.vx += (targetX - this.x) * 0.05; this.vy += (targetY - this.y) * 0.05; } } } // Slow down velocity this.vx *= 0.95; this.vy *= 0.95; // Decrease life this.life -= 0.5; } display() { push(); const alpha = map(this.life, 0, this.maxLife, 0, 200); if (this.shapeType === 'circle') { fill(this.color[0], this.color[1], this.color[2], alpha); noStroke(); ellipse(this.x, this.y, this.size, this.size); // Add concentric rings for depth for (let i = 1; i < 4; i++) { if (this.life > this.maxLife/2) { noFill(); stroke(this.color[0], this.color[1], this.color[2], alpha/3); strokeWeight(1); ellipse(this.x, this.y, this.size + i * 10, this.size + i * 10); } } } else if (this.shapeType === 'square') { fill(this.color[0], this.color[1], this.color[2], alpha); noStroke(); rectMode(CENTER); rect(this.x, this.y, this.size, this.size, 5); // Add inner square for emphasis if (this.life > this.maxLife/1.5) { noFill(); stroke(this.color[0], this.color[1], this.color[2], alpha); strokeWeight(2); rect(this.x, this.y, this.size/1.5, this.size/1.5, 3); } } else if (this.shapeType === 'triangle') { fill(this.color[0], this.color[1], this.color[2], alpha); noStroke(); // Point upwards for high notes const height = this.size * 0.8; const width = this.size; beginShape(); vertex(this.x, this.y - height/2); vertex(this.x + width/2, this.y + height/2); vertex(this.x - width/2, this.y + height/2); endShape(CLOSE); // Add锐利的线条效果 if (this.life > this.maxLife/1.5) { stroke(this.color[0], this.color[1], this.color[2], alpha); strokeWeight(1); line(this.x, this.y - height/3, this.x, this.y + height/3); } } pop(); } isDead() { return this.life < 0; } } function setup() { canvas = createCanvas(800, 500); canvas.parent('canvas-container'); // Create oscillators for each note for (let note in noteData) { oscs[note] = new p5.Oscillator(); oscs[note].setType('sine'); oscs[note].freq(noteData[note].frequency); oscs[note].amp(0); oscs[note].start(); } // Setup keyboard events for (let note in noteData) { document.getElementById(`key-${note}`).addEventListener('click', () => playNote(note)); } // Background with subtle texture drawBackground(); } function drawBackground() { // Create a subtle off-white textured background background(245, 245, 235); // Add some texture loadPixels(); for (let i = 0; i < pixelDensity() * width; i++) { for (let j = 0; j < pixelDensity() * height; j++) { const idx = (i + j * width) * 4; const noise = random(-5, 5); pixels[idx] = 245 + noise; // R pixels[idx+1] = 245 + noise; // G pixels[idx+2] = 235 + noise; // B pixels[idx+3] = 255; // Alpha } } updatePixels(); } function draw() { // Fade effect for trails background(245, 245, 235, 25); // Update and display shapes for (let i = shapes.length - 1; i >= 0; i--) { shapes[i].update(); shapes[i].display(); if (shapes[i].isDead()) { shapes.splice(i, 1); } } } function playNote(note) { // Visual feedback on key const keyElement = document.getElementById(`key-${note}`); keyElement.style.backgroundColor = `rgb(${noteData[note].color[0]}, ${noteData[note].color[1]}, ${noteData[note].color[2]})`; setTimeout(() => { keyElement.style.backgroundColor = 'white'; }, 200); // Play sound oscs[note].amp(0.5, 0.05); setTimeout(() => { oscs[note].amp(0, 0.5); }, 300); // Create new shape shapes.push(new KandinskyShape(note)); } function keyPressed() { const key = key.toLowerCase(); if (keyMap[key]) { playNote(keyMap[key]); return false; // Prevent default } } </script> </body> </html> ```
Artistic & Technical Plan ------------------------- 1. Kandinsky’s Color-Sound Mappings • Yellow – outward, cutting, trumpet-like. Visual: fast, sharp triangles or zig-zag flashes that dart away from the centre. • Blue – inward, meditative, organ-like. Visual: slow-growing circles that breathe (subtle pulsation) and drift gently toward the centre, as if listening to themselves. • Red – firm, earthly, tuba-like. Visual: steady squares that sit almost motionless, softly vibrating in place. 2. Translating “Spirituality of Blue” Kandinsky calls deep blue “the typical heavenly colour … it draws you toward the infinite.” • Geometry: perfect circles (symbol of the infinite). • Motion: centrifugal → centripetal. A circle first expands very slowly, then eases back toward the canvas centre (a tiny gravity well). • Sound: low sine-wave oscillators (C ≈ 261 Hz, D ≈ 294 Hz) with a long, gentle release. • Colour: layered blues (#1d3557 → #457b9d) whose alpha decreases with time, leaving a cool aura. 3. Maintaining Visual Harmony a. Soft-body physics: every shape has a tiny mass, velocity and radius. b. Interaction: triangles repel strongly; circles repel weakly; squares sit in-between. Forces depend on overlap distance, preventing overcrowding. c. Centre gravity: a mild attraction to canvasCentre keeps the whole “painting” coherent. d. Global fade: instead of wiping the frame, the background is repainted with a translucent off-white (#f8f4e3, α ≈ 8 %) so former strokes linger like thin glazes of paint. e. Population cap: a queue trims the oldest forms if more than 120 shapes exist. 4. Class Layout • SoundManager – builds a short-lived p5.Oscillator + p5.Envelope each time a key is played. • Shape (base) – position, velocity, life, hue, show(), update(), interact(other). ⤷ CircleShape, SquareShape, TriangleShape override show() and initialise different physics constants. • Key – stores note, frequency, colourGroup and screen-rectangle; handles hit-detection and triggers SoundManager + a new Shape. • sketch.js (inside the HTML) – sets up canvas, keys, event handlers, the main update-draw loop, and a tiny noise-texture generator. The result is a living Bauhaus “score” in which you literally play colour: mellow blue circles float like organ chords, stable red squares mark harmonic pillars, and bright yellow triangles cut vibrant melodic lines—an homage to Kandinsky’s synesthetic vision. ------------------------------------------------- HTML (self-contained, includes p5.js + p5.sound) ------------------------------------------------- ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Kandinsky Synesthesia Piano</title> <style> html,body{margin:0;padding:0;background:#f8f4e3;overflow:hidden;font-family:sans-serif;} /* hide the default focus outline for aesthetic reasons */ canvas:focus{outline:none;} </style> <script src="https://cdn.jsdelivr.net/npm/p5@1.6.0/lib/p5.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/p5@1.6.0/lib/addons/p5.sound.min.js"></script> </head> <body> <script> /* ========================================================= Kandinsky Synesthetic Piano – p5.js single-file sketch ========================================================= */ let shapes = []; const MAX_SHAPES = 120; let keys = []; // UI keys let grainGraphics; // background texture layer // ------------------------------------------------------------------ KEY DATA const keyLayout = [ {char:'A', note:'C', freq:261.63, group:'blue'}, {char:'S', note:'D', freq:293.66, group:'blue'}, {char:'D', note:'E', freq:329.63, group:'red'}, {char:'F', note:'F', freq:349.23, group:'red'}, {char:'G', note:'G', freq:392.00, group:'yellow'}, {char:'H', note:'A', freq:440.00, group:'yellow'}, {char:'J', note:'B', freq:493.88, group:'yellow'} ]; // ------------------------------------------------------------------ p5 SETUP function setup(){ createCanvas(windowWidth, windowHeight).elt.setAttribute('tabindex','0'); // make focusable noStroke(); buildKeys(); grainGraphics = createGraphics(width,height); grainGraphics.loadPixels(); for(let i=0;i<grainGraphics.pixels.length;i+=4){ let v = random(220,255); grainGraphics.pixels[i] = grainGraphics.pixels[i+1] = grainGraphics.pixels[i+2] = v; grainGraphics.pixels[i+3] = 15; // very subtle } grainGraphics.updatePixels(); } // ------------------------------------------------------------------ DRAW LOOP function draw(){ // Translucent background – trails background(248,244,227,20); image(grainGraphics,0,0); // Inter-shape interactions for(let i=0;i<shapes.length;i++){ for(let j=i+1;j<shapes.length;j++){ shapes[i].interact(shapes[j]); } } // Update & display for(let i=shapes.length-1;i>=0;i--){ shapes[i].update(); shapes[i].show(); if(shapes[i].life <= 0) shapes.splice(i,1); } // Keyboard UI drawKeyboard(); } // ------------------------------------------------------------------ KEYBOARD UI function buildKeys(){ const keyWidth = width / keyLayout.length; const keyHeight = 80; for(let i=0;i<keyLayout.length;i++){ let x = i*keyWidth; let k = keyLayout[i]; keys.push(new PianoKey(x, height-keyHeight, keyWidth, keyHeight, k)); } } function drawKeyboard(){ for(let k of keys) k.show(); } // piano key class class PianoKey{ constructor(x,y,w,h,data){ this.x=x;this.y=y;this.w=w;this.h=h;this.data=data; this.active=false; } contains(px,py){ return px>this.x && px<this.x+this.w && py>this.y && py<this.y+this.h; } trigger(){ this.flash(); playNote(this.data); spawnShape(this.data); } flash(){ this.active=true; setTimeout(()=>this.active=false,120); } show(){ push(); fill(this.active?200:230, 230, 230); rect(this.x,this.y,this.w,this.h); fill(40); textAlign(CENTER,CENTER); textSize(14); text(this.data.char, this.x+this.w/2, this.y+this.h/2); pop(); } } // ------------------------------------------------------------------ INTERACTION EVENTS function keyPressed(){ let k = keys.find(k => k.data.char.toLowerCase() === key.toLowerCase()); if(k) k.trigger(); } function mousePressed(){ for(let k of keys){ if(k.contains(mouseX, mouseY)){ k.trigger(); break; } } } // ------------------------------------------------------------------ SOUND function playNote(info){ let osc = new p5.Oscillator('sine'); osc.freq(info.freq); let env = new p5.Envelope(); env.setADSR(0.02, 0.2, 0.0, 0.5); env.setRange(0.4,0); osc.start(); env.play(osc); // stop oscillator after envelope finishes setTimeout(()=>osc.stop(), 800); } // ------------------------------------------------------------------ VISUAL SHAPES function spawnShape(info){ const x = random(width*0.25, width*0.75); const y = random(height*0.25, height*0.75); let s; if(info.group==='blue') s = new CircleShape(x,y); if(info.group==='red') s = new SquareShape(x,y); if(info.group==='yellow') s = new TriangleShape(x,y); shapes.push(s); if(shapes.length>MAX_SHAPES) shapes.shift(); } // Base class class Shape{ constructor(x,y){ this.pos = createVector(x,y); this.vel = p5.Vector.random2D().mult(0.5); this.size = random(30,60); this.life = 255; // fade amount maps to life this.mass = 1; } update(){ // centre gravity let toCenter = p5.Vector.sub(createVector(width/2,height/2), this.pos).mult(0.001); this.vel.add(toCenter); // damping this.vel.mult(0.995); this.pos.add(this.vel); this.life -= 0.7; } interact(other){ let dir = p5.Vector.sub(this.pos, other.pos); let d = dir.mag(); let minDist = (this.size+other.size)*0.5; if(d < minDist && d>0){ dir.normalize(); let forceMag = 0.5*(minDist-d); // type-dependent intensity forceMag *= this.repulsion * other.repulsion; dir.mult(forceMag); this.vel.add(dir.copy().div(this.mass)); other.vel.sub(dir.div(other.mass)); } } show(){} } // Circle (Blue) class CircleShape extends Shape{ constructor(x,y){ super(x,y); this.col = color(29,53,87, this.life); this.repulsion = 0.5; } update(){ super.update(); // gentle breathing this.size += sin(frameCount*0.02)*0.1; } show(){ this.col.setAlpha(this.life); fill(this.col); ellipse(this.pos.x, this.pos.y, this.size); } } // Square (Red) class SquareShape extends Shape{ constructor(x,y){ super(x,y); this.col = color(230,57,70, this.life); this.repulsion = 0.8; } update(){ super.update(); // subtle vibration this.angle = sin(frameCount*0.1 + this.pos.x)*0.02; } show(){ this.col.setAlpha(this.life); push(); translate(this.pos.x, this.pos.y); rotate(this.angle); fill(this.col); rectMode(CENTER); rect(0,0,this.size*0.9,this.size*0.9); pop(); } } // Triangle (Yellow) class TriangleShape extends Shape{ constructor(x,y){ super(x,y); this.col = color(244,211,94, this.life); this.repulsion = 1.2; // sharper initial velocity this.vel = p5.Vector.random2D().mult(random(1,3)); this.rotation = random(TAU); this.rotSpeed = random(-0.05,0.05); } update(){ super.update(); this.rotation += this.rotSpeed; } show(){ this.col.setAlpha(this.life); push(); translate(this.pos.x, this.pos.y); rotate(this.rotation); fill(this.col); triangle(-this.size/2, this.size/2, 0,-this.size/2, this.size/2,this.size/2); pop(); } } // ------------------------------------------------------------------ WINDOW RESIZE function windowResized(){ resizeCanvas(windowWidth, windowHeight); keys = []; buildKeys(); // rebuild grain texture grainGraphics.resizeCanvas(width,height); grainGraphics.loadPixels(); for(let i=0;i<grainGraphics.pixels.length;i+=4){ let v = random(220,255); grainGraphics.pixels[i] = grainGraphics.pixels[i+1] = grainGraphics.pixels[i+2] = v; grainGraphics.pixels[i+3] = 15; } grainGraphics.updatePixels(); } </script> </body> </html> ```
### Artistic & Technical Interpretation: A Plan for "Synesthetic Composition" Before presenting the final piece, allow me to outline the artistic philosophy and technical structure, as an artist would prepare for a new work. This project is not merely a technical demonstration but an attempt to interpret the "inner sound" (innere Klang) that Wassily Kandinsky believed was inherent in colors and forms. **Artistic Interpretation: Translating the "Spiritual"** Kandinsky's theory posits that art should be a gateway to the spiritual, moving beyond mere representation. Each element in this piece is designed to evoke a specific spiritual-emotional state: 1. **The "Spirituality" of Blue (Deep Tones - C, D):** Kandinsky described blue as a "heavenly" color that calls man towards the infinite, awakening a desire for purity and the supersensual. It is the sound of a cello or a deep organ. * **My Interpretation:** The blue circles will not simply appear. They will be born from the center and expand slowly, like a deep, meditative breath or ripples in a still, bottomless pond. Their movement will be perfect and concentric, representing a pull *inward* toward a spiritual center. Their sound will be a pure, low-frequency sine wave, creating a feeling of calm and depth. 2. **The "Spirituality" of Red (Mid Tones - E, F):** Red is an energetic, living color, but its energy is contained and self-assured, unlike the "reckless" energy of yellow. It represents a powerful, stable force, like the sound of a tuba. * **My Interpretation:** Red will manifest as a square—the most stable of forms. It will appear confidently on the canvas, not expanding wildly, but perhaps rotating slowly on its axis, asserting its presence. This represents a grounded, internal strength. The sound will be a richer square wave, giving it more body and a more "solid" feel than the pure sine wave of blue. 3. **The "Spirituality" of Yellow (High Tones - G, A, B):** Yellow is the color of earthly energy. It's aggressive, sharp, and expansive, almost painfully so. It spreads outwards, representing a "leap over the boundaries," like the sharp, piercing blast of a trumpet. * **My Interpretation:** The yellow tones will generate sharp, acute triangles. They will be spawned with high initial velocity, shooting outwards from a central point. To capture their "aggressive" nature, their movement will have a slight, almost imperceptible chaotic jitter. They will push other forms away with more force, acting as the primary agents of dynamic change in the composition. The sound will be a high-frequency sine wave, sharp and clear. **Compositional Logic: Harmony from a "Clash of Worlds"** A Kandinsky painting is a "clash of worlds" that resolves into a harmonious whole. This interactive piece must do the same. * **The Canvas as a Universe:** The off-white, textured background is not just decorative; it represents the "Great Silence" from which all forms and sounds are born. * **A "Living" Composition:** The shapes are not static. They are temporary beings with a lifespan. They are born, they interact, and they fade, leaving an echo of their existence. This ensures the canvas is a record of the music played, always evolving but never becoming overly cluttered. * **Simple Physics, Complex Harmony:** The interaction logic (shapes pushing each other) is the key to creating harmony from chaos. A "sharp" yellow triangle will forcefully nudge a "calm" blue circle, which will slowly drift in response. This creates a silent, visual dialogue between the elements, forming a balanced, if asymmetrical, composition over time. The composition is therefore a direct result of the user's musical performance. **Technical Structure** To realize this vision, the code is structured around a central object-oriented concept: * **`KandinskyShape` (Base Class):** An abstract class defining the core properties of every visual element: position, velocity, lifespan, and the ability to interact (`applyForce`, `interact`). * **`Circle`, `Square`, `Triangle` (Subclasses):** Each of these inherits from `KandinskyShape` but implements its own unique `display()` method, drawing itself according to the artistic principles described above. For instance, the `Triangle`'s display method includes the "jitter," while the `Circle`'s renders concentric rings. * **Main Sketch Logic (`setup`, `draw`):** * `setup()`: Initializes the canvas, the textured background (drawn once to a graphics buffer for performance), and the sound engine (a reusable oscillator and envelope). * `draw()`: The main loop that renders the world. It draws the background, then iterates through all active shapes, calling their `update()` and `display()` methods, checking for interactions, and removing shapes whose lifespan has expired. It also draws the static piano interface. * **Event Handlers (`keyPressed`, `mousePressed`):** These functions are the bridge between the user and the canvas. They listen for input, trigger the correct sound from the `keyMap`, and instantiate the corresponding `KandinskyShape` object, bringing a new "spirit" into the composition. This structure allows for a clean separation of concerns, making the complex interplay of sound, form, and motion manageable and true to the artistic vision. *** ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Kandinsky's Synesthetic Piano</title> <style> body { margin: 0; overflow: hidden; background-color: #1a1a1a; display: flex; justify-content: center; align-items: center; height: 100vh; font-family: 'Helvetica Neue', Arial, sans-serif; color: #d0d0d0; } canvas { box-shadow: 0 0 30px rgba(0, 0, 0, 0.6); } #start-prompt { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); padding: 20px 40px; background-color: rgba(20, 20, 20, 0.8); border: 1px solid #555; border-radius: 5px; cursor: pointer; text-align: center; } h2 { margin:0 0 10px 0; } </style> </head> <body> <div id="start-prompt"> <h2>Synesthetic Composition</h2> <p>Click to begin.</p> </div> <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.1/p5.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.1/addons/p5.sound.min.js"></script> <script> let shapes = []; let bgTexture; let osc, envelope; let isStarted = false; // Kandinsky's Palette & Score const keyData = { 'C': { freq: 261.63, color: [0, 80, 153], type: 'circle', keyChar: 'C' }, // Deep Blue (C4) 'D': { freq: 293.66, color: [60, 120, 180], type: 'circle', keyChar: 'D' }, // Lighter Blue (D4) 'E': { freq: 329.63, color: [200, 30, 30], type: 'square', keyChar: 'E' }, // Strong Red (E4) 'F': { freq: 349.23, color: [220, 50, 50], type: 'square', keyChar: 'F' }, // Vibrant Red (F4) 'G': { freq: 392.00, color: [255, 204, 0], type: 'triangle', keyChar: 'G' }, // Sharp Yellow (G4) 'A': { freq: 440.00, color: [255, 225, 60], type: 'triangle', keyChar: 'A' }, // Brighter Yellow (A4) 'B': { freq: 493.88, color: [255, 235, 100], type: 'triangle', keyChar: 'B' }, // Piercing Yellow (B4) }; // Keyboard key to note mapping const keyboardMap = { 'A': 'C', 'S': 'D', 'D': 'E', 'F': 'F', 'J': 'G', 'K': 'A', 'L': 'B' }; let onScreenKeys = []; let activeKey = null; function setup() { let canvas = createCanvas(windowWidth, windowHeight); canvas.parent(document.body); // --- Sound Engine Setup --- envelope = new p5.Envelope(); envelope.setADSR(0.01, 0.2, 0.1, 0.5); // Attack, Decay, Sustain, Release envelope.setRange(0.5, 0); // Attack Level, Release Level osc = new p5.Oscillator(); osc.amp(envelope); osc.start(); // --- Create Textured Background (The "Great Silence") --- bgTexture = createGraphics(width, height); bgTexture.background(245, 240, 228); // Kandinsky's off-white bgTexture.stroke(0, 8); for (let i = 0; i < 60000; i++) { bgTexture.point(random(width), random(height)); } // --- Create On-Screen Piano Keys --- const keyNotes = ['C', 'D', 'E', 'F', 'G', 'A', 'B']; const numKeys = keyNotes.length; const keyWidth = min(width / (numKeys + 2), 100); const keyHeight = height * 0.15; const totalWidth = numKeys * keyWidth; const startX = (width - totalWidth) / 2; for (let i = 0; i < numKeys; i++) { onScreenKeys.push({ x: startX + i * keyWidth, y: height - keyHeight, w: keyWidth, h: keyHeight, note: keyNotes[i], data: keyData[keyNotes[i]], }); } document.getElementById('start-prompt').addEventListener('click', () => { userStartAudio().then(() => { document.getElementById('start-prompt').style.display = 'none'; isStarted = true; }); }); noLoop(); // Wait for user to start } function draw() { if (!isStarted) return; // Draw the textured background image(bgTexture, 0, 0); // Update and display all shapes for (let i = shapes.length - 1; i >= 0; i--) { let s = shapes[i]; s.update(); s.applyForce(createVector(0, 0.05)); // Subtle gravity // Interaction logic for (let j = i - 1; j >= 0; j--) { s.interact(shapes[j]); } s.display(); if (s.isDead()) { shapes.splice(i, 1); } } drawPiano(); } function drawPiano() { push(); for (const key of onScreenKeys) { if (key.note === activeKey) { fill(key.data.color[0], key.data.color[1], key.data.color[2], 150); } else { fill(255, 255, 255, 180); } stroke(150); rect(key.x, key.y, key.w, key.h); fill(50); noStroke(); textAlign(CENTER, CENTER); textSize(key.w * 0.3); text(key.note, key.x + key.w / 2, key.y + key.h / 2); } pop(); } function playNote(noteKey) { if (keyData[noteKey]) { const data = keyData[noteKey]; // --- Play Sound --- let oscType = 'sine'; if (data.type === 'square') oscType = 'square'; if (data.type === 'triangle') oscType = 'triangle'; osc.setType(oscType); osc.freq(data.freq); envelope.play(osc); // --- Create Visual --- let shape; let spawnPos = createVector(random(width * 0.2, width * 0.8), random(height * 0.2, height * 0.6)); if (data.type === 'circle') { shape = new Circle(spawnPos.x, spawnPos.y, data); } else if (data.type === 'square') { shape = new Square(spawnPos.x, spawnPos.y, data); } else if (data.type === 'triangle') { shape = new Triangle(spawnPos.x, spawnPos.y, data); } shapes.push(shape); // Visual feedback for key press activeKey = noteKey; setTimeout(() => { if(activeKey === noteKey) activeKey = null; }, 200); } } function mousePressed() { if (!isStarted) { userStartAudio().then(() => { document.getElementById('start-prompt').style.display = 'none'; isStarted = true; loop(); }); return; } for (const key of onScreenKeys) { if (mouseX > key.x && mouseX < key.x + key.w && mouseY > key.y && mouseY < key.y + key.h) { playNote(key.note); break; } } } function keyPressed() { if (!isStarted) return; const note = keyboardMap[key.toUpperCase()]; if (note) { playNote(note); } } // =================================== // KANDINSKY SHAPE CLASSES // =================================== class KandinskyShape { constructor(x, y, data) { this.pos = createVector(x, y); this.vel = createVector(); this.acc = createVector(); this.data = data; this.color = data.color; this.lifespan = 255 * 3; // Lifespan in frames this.baseSize = random(width * 0.05, width * 0.12); this.repulsion = 1.0; } applyForce(force) { this.acc.add(force); } interact(other) { let d = p5.Vector.dist(this.pos, other.pos); let combinedSize = (this.size + other.size) / 2; if (d < combinedSize * 0.8) { let force = p5.Vector.sub(this.pos, other.pos); force.normalize(); let strength = (1 - (d / (combinedSize * 0.8))) * 0.5; force.mult(strength); force.mult(this.repulsion); this.applyForce(force); other.applyForce(force.mult(-1 * other.repulsion)); } } update() { this.vel.add(this.acc); this.pos.add(this.vel); this.acc.mult(0); // Clear acceleration this.vel.mult(0.97); // Damping this.lifespan -= 1; } isDead() { return this.lifespan < 0; } display() { // This method will be overridden by subclasses } } class Circle extends KandinskyShape { constructor(x, y, data) { super(x, y, data); // Blue's calm spirituality: slow movement this.vel = p5.Vector.random2D().mult(0.2); this.baseSize = random(width * 0.1, width * 0.2); this.repulsion = 0.5; // Less pushy } display() { noStroke(); let alpha = map(this.lifespan, 0, 255 * 3, 0, 150); // Concentric movement effect let t = (255 * 3 - this.lifespan) * 0.05; for (let i = 3; i > 0; i--) { let size = this.baseSize * (1 + sin(t * 0.5) * 0.1) * (i / 3); let currentAlpha = alpha * (1 - i * 0.2); fill(this.color[0], this.color[1], this.color[2], currentAlpha); ellipse(this.pos.x, this.pos.y, size, size); } } } class Square extends KandinskyShape { constructor(x, y, data) { super(x, y, data); // Red's stability: almost no initial movement this.vel = p5.Vector.random2D().mult(0.1); this.baseSize = random(width * 0.08, width * 0.15); this.angle = random(-PI / 4, PI / 4); this.rotSpeed = random(-0.005, 0.005); this.repulsion = 1.0; } update() { super.update(); this.angle += this.rotSpeed; } display() { let alpha = map(this.lifespan, 0, 255 * 3, 0, 200); fill(this.color[0], this.color[1], this.color[2], alpha); noStroke(); push(); translate(this.pos.x, this.pos.y); rotate(this.angle); rectMode(CENTER); square(0, 0, this.baseSize); pop(); } } class Triangle extends KandinskyShape { constructor(x, y, data) { super(x, y, data); // Yellow's aggression: high outward velocity let center = createVector(width / 2, height / 2); this.vel = p5.Vector.sub(this.pos, center).normalize().mult(random(2, 4)); this.baseSize = random(width * 0.1, width * 0.18); this.angle = this.vel.heading(); this.repulsion = 1.5; // More pushy } display() { let alpha = map(this.lifespan, 0, 255 * 3, 0, 220); fill(this.color[0], this.color[1], this.color[2], alpha); noStroke(); // Jitter for aggressive energy let jitterX = random(-1, 1) * 2; let jitterY = random(-1, 1) * 2; push(); translate(this.pos.x + jitterX, this.pos.y + jitterY); rotate(this.angle + PI / 2); // Point in direction of movement let h = this.baseSize; let w = this.baseSize * 0.8; triangle(0, -h / 2, -w / 2, h / 2, w / 2, h / 2); pop(); } } // Make canvas responsive function windowResized() { // A full resize implementation would require recalculating all positions, // but for this project we will just resize the canvas for simplicity. resizeCanvas(windowWidth, windowHeight); // Redraw background and UI bgTexture = createGraphics(width, height); bgTexture.background(245, 240, 228); bgTexture.stroke(0, 8); for (let i = 0; i < 60000; i++) { bgTexture.point(random(width), random(height)); } const keyNotes = ['C', 'D', 'E', 'F', 'G', 'A', 'B']; const numKeys = keyNotes.length; const keyWidth = min(width / (numKeys + 2), 100); const keyHeight = height * 0.15; const totalWidth = numKeys * keyWidth; const startX = (width - totalWidth) / 2; onScreenKeys = []; for (let i = 0; i < numKeys; i++) { onScreenKeys.push({ x: startX + i * keyWidth, y: height - keyHeight, w: keyWidth, h: keyHeight, note: keyNotes[i], data: keyData[keyNotes[i]], }); } } </script> </body> </html> ```
```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0"/> <title>Kandinsky Synesthesia Piano</title> <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/p5.js/1.7.0/addons/p5.sound.min.js"></script> <style> body { margin: 0; padding: 0; display: flex; justify-content: center; align-items: center; height: 100vh; background: #f5f5f5; font-family: 'Arial', sans-serif; } canvas { box-shadow: 0 10px 30px rgba(0,0,0,0.1); } </style> </head> <body> <script> // === ARTISTIC & TECHNICAL INTERPRETATION === // // This experience is rooted in Kandinsky's theory of synesthesia — // the merging of senses — where sound evokes color and form, and vice versa. // // - **Spirituality of Blue**: Deep blue, linked to inner calm and infinity, // is expressed through slow, concentric, expanding circles that appear like // still water ripples. Their motion feels meditative, drawing the eye inward. // // - **Power of Red**: Red is not just a color but a force. It manifests as // solid squares that appear with weight and stability. They resist motion, // symbolizing the human soul's assertion. // // - **Aggression of Yellow**: Yellow bursts outward. Triangles dart from the // center with sharp angles, mimicking the piercing sound of a trumpet. // Their velocity and direction reflect Kandinsky’s "active" yellow. // // - **Composition Logic**: Shapes interact via gentle repulsion. A new form // subtly displaces nearby forms, simulating emotional tension. Over time, // older shapes fade like memories, creating a spiritual "afterimage." // // - **Aesthetic**: Canvas has a rough, handmade texture — evoking paper or // linen — to anchor the digital in the material, honoring Bauhaus craft ideals. // // - **Structure**: // - SoundManager: Handles tone generation via oscillators. // - Shape: Base class for visual elements (Circle, Square, Triangle). // - Physics: Simple repulsion between shapes for organic balance. // Global array to hold all active visual shapes let shapes = []; let soundOscillators = {}; let noteMapping = ['C', 'D', 'E', 'F', 'G', 'A', 'B']; let noteToKey = { 'C': 'a', 'D': 's', 'E': 'd', 'F': 'f', 'G': 'g', 'A': 'h', 'B': 'j' }; let keyToNote = { 'a': 'C', 's': 'D', 'd': 'E', 'f': 'F', 'g': 'G', 'h': 'A', 'j': 'B' }; let keyToIndex = { 'a': 0, 's': 1, 'd': 2, 'f': 3, 'g': 4, 'h': 5, 'j': 6 }; let frequencies = [261.63, 293.66, 329.63, 349.23, 392.00, 440.00, 493.88]; // C4 to B4 let texture; // === SHAPE BASE CLASS === class Shape { constructor(x, y, color, size, note, lifespan = 300) { this.x = x; this.y = y; this.color = color; this.size = size; this.note = note; this.opacity = 220; this.lifespan = lifespan; this.age = 0; this.vx = random(-0.5, 0.5); this.vy = random(-0.5, 0.5); } update() { this.age++; if (this.age < this.lifespan) { this.opacity = map(this.age, 0, this.lifespan, 220, 0); } else { this.opacity = 0; } this.x += this.vx; this.y += this.vy; this.applyRepulsion(); } applyRepulsion() { for (let other of shapes) { if (other === this) continue; let d = dist(this.x, this.y, other.x, other.y); if (d < 100) { let force = map(d, 0, 100, 1.5, 0); let angle = atan2(this.y - other.y, this.x - other.x); this.vx += cos(angle) * force; this.vy += sin(angle) * force; } } // Dampen velocity this.vx *= 0.98; this.vy *= 0.98; } isExpired() { return this.opacity <= 0; } draw() { noStroke(); fill(red(this.color), green(this.color), blue(this.color), this.opacity); this.render(); } render() { // To be implemented by subclass } } class CircleShape extends Shape { render() { ellipse(this.x, this.y, this.size); } } class SquareShape extends Shape { render() { rectMode(CENTER); rect(this.x, this.y, this.size, this.size); } } class TriangleShape extends Shape { render() { triangle( this.x, this.y - this.size / 2, this.x - this.size / 2.5, this.y + this.size / 2, this.x + this.size / 2.5, this.y + this.size / 2 ); } } // === SOUND MANAGER === class SoundManager { constructor() { this.oscillators = {}; this.envelope = new p5.Env(); this.envelope.setADSR(0.05, 0.1, 0.5, 1); this.envelope.setScale(1); // Initialize oscillators for (let i = 0; i < frequencies.length; i++) { const osc = new p5.Oscillator(); osc.setType('sine'); osc.freq(frequencies[i]); osc.amp(0); osc.start(); this.oscillators[noteMapping[i]] = osc; } } playNote(note) { const osc = this.oscillators[note]; if (osc) { osc.amp(0.3, 0.05); this.envelope.play(osc); } } stopNote(note) { const osc = this.oscillators[note]; if (osc) { osc.amp(0, 0.1); } } } let soundManager; function preload() { // Create a textured background (simulated paper) texture = createGraphics(800, 600); texture.loadPixels(); for (let x = 0; x < texture.width; x++) { for (let y = 0; y < texture.height; y++) { let v = random(230, 245); texture.set(x, y, color(v)); } } texture.updatePixels(); // Add grain texture.filter(GRAY); texture.noiseSeed(1); texture.fill(0, 10); for (let i = 0; i < 10000; i++) { let x = random(texture.width); let y = random(texture.height); texture.rect(x, y, 1, 1); } } function setup() { createCanvas(800, 600); soundManager = new SoundManager(); textAlign(CENTER, CENTER); noStroke(); } function draw() { // Draw textured background image(texture, 0, 0); // Draw minimalist keyboard drawKeyboard(); // Update and draw all shapes for (let i = shapes.length - 1; i >= 0; i--) { shapes[i].update(); shapes[i].draw(); if (shapes[i].isExpired()) { shapes.splice(i, 1); } } // Title fill(0, 180); textSize(18); text("Kandinsky Synesthesia Piano", width/2, 50); textSize(12); text("Click keys below or press A S D F G H J", width/2, 75); } function drawKeyboard() { push(); const keyWidth = 60; const keyHeight = 120; const startX = width/2 - (keyWidth * 7) / 2; const y = height - 150; for (let i = 0; i < noteMapping.length; i++) { const x = startX + i * keyWidth; let note = noteMapping[i]; let labelColor = 0; // Determine color based on note if (note === 'C' || note === 'D') { fill(20, 80, 160, 180); // Deep blue } else if (note === 'E' || note === 'F') { fill(200, 40, 40, 180); // Red } else { fill(220, 200, 40, 180); // Yellow } rect(x + 2, y, keyWidth - 4, keyHeight, 8); fill(255); noStroke(); textSize(20); text(note, x + keyWidth / 2, y + keyHeight / 2); textSize(12); text(Object.keys(keyToNote)[i], x + keyWidth / 2, y + keyHeight - 20); } pop(); } function mouseClicked() { const keyWidth = 60; const y = height - 150; const startX = width/2 - (keyWidth * 7) / 2; for (let i = 0; i < 7; i++) { const x = startX + i * keyWidth; if (mouseX >= x && mouseX <= x + keyWidth && mouseY >= y && mouseY <= y + 120) { const note = noteMapping[i]; playNote(note); break; } } } function keyPressed() { if (keyToNote[key]) { playNote(keyToNote[key]); } } function playNote(note) { soundManager.playNote(note); const x = random(200, width - 200); const y = random(200, height - 200); const size = random(40, 100); let shape; if (note === 'C' || note === 'D') { // Blue: Circle shape = new CircleShape(x, y, color(20, 80, 160), size, note); } else if (note === 'E' || note === 'F') { // Red: Square shape = new SquareShape(x, y, color(200, 40, 40), size, note); } else { // Yellow: Triangle shape = new TriangleShape(x, y, color(220, 200, 40), size, note); } shapes.push(shape); // Limit number of shapes to avoid overload if (shapes.length > 50) { shapes.splice(0, 10); } } </script> </body> </html> ```
## 1‑2. Artistic & Technical Interpretation ### 1.1 Kandinsky’s “Spiritual” Spectrum | Tone | Associated Colour | Typical Visual Motif | Movement | Sound analogy | |------|-------------------|----------------------|----------|----------------| | **C‑D** (deep, low) | **Blue** | Circles, semi‑circular arcs | Slow, concentric, “breathing” | cello / organ | | **E‑F** (mid‑range, sturdy) | **Red** | Squares, rectangles | Steady, controlled | tuba | | **G‑A‑B** (high, bright) | **Yellow** | Triangles, zig‑zags, sharp edges | Fast, expansive, jagged | trumpet | *The “spiritual” side is visualized here as the colour’s **openness** and the **motion** of the shapes: blue = depth & calm, red = solidity & power, yellow = light & quickness.* ### 1.2 Compositional Logic (Harmony / Balance) 1. **Canvas as Motion‑Canvas** The 500×500 canvas is divided into a *soft grid* (think of a loose sketch). Shapes float above, their positions are updated each frame, but we never let them accumulate in one corner. • **Brownian drift** – each shape has a small random velocity (±0.2 px/frame). • **Forces** – new *aggressive* shapes (yellow triangles) create a mild repulsion radius (~80 px) that nudges nearby *calming* shapes (blue circles) slightly away. • **Gravity & Boundary** – shapes gently drift toward canvas center and bounce off edges. 2. **Temporal Fading (Trace of Music)** Every shape owns a *life* value (0–255). On each `draw()` tick we decrement it by a small amount (≈1–3). When life reaches 0 we delete the shape. By letting shapes fade slowly the canvas keeps a trace of recent musical activity without becoming chaotic. 3. **Layering** When drawing we sort shapes by their *age* (newer shapes on top). This mimics the layers of oil paint, ensuring that brief bursts of colour don’t cover earlier strokes outright. ### 1.3 Class Structure | Class | Purpose | Key Properties | Methods | |-------|---------|----------------|---------| | **SoundManager** | Handles oscillators & maps notes to frequencies | `keyMap: {C:261.63, D:293.66, ...}` | `playNote(noteName)` | | **Shape** | Base class for all shapes | `pos`, `vel`, `size`, `life`, `color`, `type` | `update()`, `draw(ctx)` | | **Circle extends Shape** | Blue circle | – | – | | **Square extends Shape** | Red square | – | – | | **Triangle extends Shape** | Yellow triangle | – | – | | **Keyboard** | Draws 7 on-screen buttons & handles clicks | `keys`, `x`, `y` | `render()`, `mousePressed()` | ### 1.4 Main Logic Flow 1. `setup()` – create canvas, instantiate `SoundManager`, `Keyboard`, empty `shapes[]`. 2. `draw()` – • Clear canvas with a filtered, off‑white background. • Update & redraw each shape (`update()`, `draw()`), then sort by age and render from back to front. • Render the on‑screen keyboard. 3. `mousePressed()` → if click inside a key rectangle → `Keyboard.keyPressed(key)` → `SoundManager.playNote()` + create shape. 4. `keyPressed()` → if key mapped to a musical note → same as above. ### 1.5 Translating “Spirituality” into Animation - **Blue:** *Sinusoidal radius* – each circle oscillates its size slowly (`size = base + 5*sin(frameCount*0.01 + phase)`). *Faded edges* – use `stroke(0, 10)` to give it an airy vapor look. - **Red:** *Steady shrink* – size decreases linearly with `life`. *Bold stroke* – `strokeWeight(2)` keeps the colour impenetrable. - **Yellow:** *Velocity boost* – `vel` magnitude is larger, giving them an energetic flier effect. *Zig‑zag dynamic* – follow a rotating `steer()` vector to wind their path. With this plan we can now craft the complete single‑file solution. --- ## 2. Final Self‑Contained HTML Implementation ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Kandinsky Synesthesia Studio</title> <style> body {margin:0; overflow:hidden; background:#f8f8f4;} canvas{display:block;} .fader{position:absolute; top:0; left:0; right:0; bottom:0; pointer-events:none;} </style> <script src="https://cdn.jsdelivr.net/npm/p5@1.9.0/lib/p5.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/p5@1.9.0/lib/addons/p5.sound.min.js"></script> </head> <body> <script> /* ============================== 1. SoundManager ============================== */ class SoundManager { constructor() { // Frequency table (A4 = 440, relative to C4=261.63) this.keyMap = { 'C':261.63, 'D':293.66, 'E':329.63, 'F':349.23, 'G':392.00, 'A':440.00, 'B':493.88 }; // Keep oscillators cache for rapid re‑use this.oscs = {}; for (let k in this.keyMap) { const osc = new p5.Oscillator('sine'); osc.freq(this.keyMap[k]); osc.amp(0); osc.start(); this.oscs[k] = osc; } } playNote(k) { const osc = this.oscs[k]; if (osc) { osc.amp(0.4, 0.05); // smooth attack setTimeout(()=>osc.amp(0,0.5),400); // decay } } } /* ============================== 2. Shape hierarchy ============================== */ class Shape { constructor(pos, vel, size, color, type) { this.pos = pos.copy(); this.vel = vel.copy(); this.size = size; this.color = color; this.type = type; this.life = 255; // fade } // generic slow decay decayed() { this.life -= 1; } isDead() { return this.life <= 0; } // boundary bounce bounce(w,h) { if (this.pos.x < 0 || this.pos.x > w) this.vel.x *= -1; if (this.pos.y < 0 || this.pos.y > h) this.vel.y *= -1; this.pos.x = constrain(this.pos.x,0,w); this.pos.y = constrain(this.pos.y,0,h); } update() { /* to be overridden */ } draw() { /* to be overridden */ } } class Circle extends Shape { constructor(pos, vel, baseSize) { super(pos, vel, baseSize, color(64,100,255,80), 'circle'); this.phase = random(TWO_PI); } update() { this.updateCommon(); } updateCommon() { this.pos.add(this.vel); this.bounce(width,height); this.decayed(); } draw() { noStroke(); fill(this.color.levels[0],this.color.levels[1],this.color.levels[2],this.life); const s = this.size + 5*sin((frameCount + this.phase)*0.01); ellipse(this.pos.x,this.pos.y,s,s); } } class Square extends Shape { constructor(pos, vel, size) { super(pos, vel, size, color(255,0,0,120), 'square'); } update() { this.updateCommon(); } updateCommon() { this.pos.add(this.vel); this.bounce(width,height); this.decayed(); } draw() { noStroke(); fill(this.color.levels[0],this.color.levels[1],this.color.levels[2],this.life); square(this.pos.x - this.size/2, this.pos.y - this.size/2, this.size); } } class Triangle extends Shape { constructor(pos, vel, size) { super(pos, vel, size, color(255,255,0,200), 'triangle'); // random initial direction this.dir = p5.Vector.random2D(); } update() { this.dir.rotate(0.02); // slowly steer this.vel.add(this.dir.mult(0.05)); this.pos.add(this.vel); this.bounce(width,height); this.decayed(); } draw() { noStroke(); fill(this.color.levels[0],this.color.levels[1],this.color.levels[2],this.life); const r = this.size*0.5; // triangle with orientation set by dir push(); translate(this.pos.x, this.pos.y); rotate(this.dir.heading()); triangle(-r, r, r, r, 0, -r); pop(); } } /* ============================== 3. Keyboard UI ============================== */ class Keyboard { constructor() { this.keys = ['C','D','E','F','G','A','B']; this.w = 60; this.h = 150; this.x = (width - this.keys.length*this.w)/2; this.y = height - this.h - 20; } render() { for (let i=0;i<this.keys.length;i++) { const k = this.keys[i]; const x = this.x + i*this.w; fill(240); stroke(0); rect(x,this.y,this.w,this.h,3); fill(0); noStroke(); textAlign(CENTER, CENTER); textSize(24); text(k, x + this.w/2, this.y + this.h/2); } } isInside(px,py) { for (let i=0;i<this.keys.length;i++) { const x = this.x + i*this.w; if (px>=x && px<=x+this.w && py>=this.y && py<=this.y+this.h) { return this.keys[i]; } } return null; } } /* ============================== 4. Globals & Setup ============================== */ let sm; // SoundManager let shapes = []; // all active shapes let kb; // Keyboard function setup() { createCanvas(600,600); sm = new SoundManager(); kb = new Keyboard(); frameRate(60); } function draw() { clear(); // Faux textured background background(245,245,240); // Update + draw shapes shapes.forEach(s=>s.update()); // Remove dead shapes = shapes.filter(s=>!s.isDead()); // Sorting by age – older at back shapes.sort((a,b)=>a.life-b.life); shapes.forEach(s=>s.draw()); // Draw keyboard on top kb.render(); } /* ============================== 5. Interaction ============================== */ function mousePressed() { const key = kb.isInside(mouseX,mouseY); if (key) { playNoteAndShape(key); } } function keyPressed() { const k = key.toUpperCase(); if ('CDEFGAB'.includes(k)) { playNoteAndShape(k); } } /* ============================== 6. Note + Shape handler ============================== */ function playNoteAndShape(k) { sm.playNote(k); const pos = createVector(random(width), random(height)); const vel = p5.Vector.random2D().mult(random(0.5,1.5)); const size = random(30,60); // Distinguish by note if ('GAB'.includes(k)) { // Yellow const t = new Triangle(pos, vel, size); shapes.push(t); } else if ('EF'.includes(k)) { // Red const s = new Square(pos, vel, size); shapes.push(s); } else { // Blue const c = new Circle(pos, vel, size); shapes.push(c); } } /* ============================== 7. Mitigation of Aggressive interactions ============================== */ function handleForces() { // For each yellow triangle, apply repulsion to blue circles shapes.forEach((s1,i)=>{ if (s1.type==='triangle') { shapes.forEach((s2,j)=>{ if (j!==i && s2.type==='circle') { let d = p5.Vector.sub(s1.pos, s2.pos); let distSq = constrain(d.magSq(), 400, 16000); // avoid zero division let dir = d.mult(1/Math.sqrt(distSq) * 0.2); s2.vel.add(dir); } }); } }); } setInterval(handleForces,50); // every 50ms </script> </body> </html> ``` > **How it works** > * Click or press the keys `C–B` to hear a sine tone. > * A colour‑coded shape appears: > • **Blue** circles (low notes), gentle sine‑size changes. > • **Red** squares (mid notes), steady linear shrink. > • **Yellow** triangles (high notes), energetic movement with slight steering. > * Shapes slowly fade and the canvas retains a “trace” of recent music. > * Yellow triangles generate a gentle repulsive push on blue circles, giving the composition a dynamic but balanced feel. > * The on‑screen piano sits at the bottom—clickable for touch devices. Feel free to experiment by pressing different keys, or enhancing the sound with multiple oscillators or per‑note panning for a richer synesthetic experience.
**Artistic & Technical Plan** ### Artistic Interpretation **Kandinsky’s Synesthesia**: Kandinsky equated colors with musical tones; he describes yellow as aggressive and lively (like a trumpet), blue as spiritual and deep (like a cello), red as powerful and stable (like a tuba). In my rendering: - **Yellow (G, A, B):** Sharp, quickly expanding triangles or zig-zag lines. Motion is swift, radiating outward, sometimes overlapping and repelling others for visual “energy.” - **Blue (C, D):** Soft, round circles, which expand and drift inward toward the center, glowing faintly, moving in slow, calm orbits. Older blue shapes yield/gently shift if "pushed" by yellow shapes — a spiritual dance. - **Red (E, F):** Solid, stable squares anchored to imagined grid lines, slightly larger and more static, radiating strength. New red shapes appear between any existing shapes to “balance” the space compositionally. **Compositional Harmony:** Inspired by Kandinsky’s balanced abstract works, every new shape searches for a spot with enough negative space. If it collides with an “aggressive” yellow triangle, it gets gently nudged. Positions and sizes are randomly generated but always checked for overlap and harmony. Shapes fade out (opacity, scale down) over several seconds, leaving echo-like traces akin to paint washes or pastel. **Background:** An off-white, slightly textured canvas using a subtle Perlin noise pattern, reinforcing the tangible, tactile effect of early Bauhaus works. --- ### Structure & Classes - **SoundManager**: Maps keyboard and mouse keys (C, D, E, F, G, A, B) to oscillator frequencies (using a simple sine/square/triangle oscillator; tone and envelope shaped gently), triggers shape creation. - **Shape (base class, abstract):** - Properties: x, y, opacity, color, size, velocity, shapeType (triangle, circle, square), note. - Methods: update(), draw(), interactWith(otherShape), fade(). - **BlueCircle, RedSquare, YellowTriangle (all extended from Shape):** - Custom update() for animation (e.g., blue circles float inward, red squares gently pulse, yellow triangles dart/expand). - Custom interactWith() for resolving collisions, e.g., yellow triangle “bumps” anything nearby. - **ShapeManager:** Stores all shapes. Each frame: - updates positions/interaction matrix, - maintains compositional harmony, - draws each shape, - removes shapes once faded. - **UIManager:** Draws on-screen “keyboard.” Handles mouse and key events, highlights keys, gives subtle feedback (ripple). --- ### Main Logic - User presses/composes a note. - The **SoundManager** plays the correct tone and envelope. - The **ShapeManager** generates the appropriate shape at a harmonious location. - Shapes interact, move, and fade; the animation and interactions are unique per type (yellow can push, blue can move aside, red holds its ground). - The canvas remains visually balanced — shapes try to avoid clustering, distributing visual weight (in terms of size, color, and spatial distribution). - The background remains subtly textured. --- **Now, the full HTML example with all logic, using p5.js and p5.sound:** ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Kandinsky Synesthetic Piano</title> <meta name="viewport" content="width=device-width,initial-scale=1"> <!-- Load p5 and p5.sound from CDN --> <script src="https://cdn.jsdelivr.net/npm/p5@1.6.0/lib/p5.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/p5@1.6.0/lib/addons/p5.sound.min.js"></script> <style> body { margin: 0; background: #ede6dd; overflow: hidden; } #defaultCanvas0 { display: block; margin: 0 auto; position: absolute; left: 0; right: 0; top: 0; bottom: 0; } .p5Canvas { box-shadow: 0 8px 24px rgba(0,0,0,0.10); border-radius: 16px; } </style> </head> <body> <script> /* Kandinsky Synesthetic Piano -- Interactive Audio-Visual Bauhaus Experience -- by ChatGPT / 2024 */ // -------- Constants for Notes, Frequencies, and Color Mapping -------- // const NOTE_MAPPING = [ {key:'C', kb:'A', code:65, freq:261.63, color:'#3254a8', type:'blue'}, // Blue - deep C (A) {key:'D', kb:'S', code:83, freq:293.66, color:'#4668be', type:'blue'}, // Blue - D (S) {key:'E', kb:'D', code:68, freq:329.63, color:'#d32e2f', type:'red'}, // Red - E (D) {key:'F', kb:'F', code:70, freq:349.23, color:'#be2424', type:'red'}, // Red - F (F) {key:'G', kb:'G', code:71, freq:392.00, color:'#ffd700', type:'yellow'}, // Yellow - G (G) {key:'A', kb:'H', code:72, freq:440.00, color:'#ffe34d', type:'yellow'}, // Yellow - A (H) {key:'B', kb:'J', code:74, freq:493.88, color:'#ffed77', type:'yellow'} // Yellow - B (J) ]; const SHAPE_TYPES = { blue: ['BlueCircle'], red: ['RedSquare'], yellow: ['YellowTriangle'] }; // Off-white & textured BG let offWhite, overlayTex; // ---------- Setup, Main Loop ---------- // let shapeManager, soundManager, uiManager; function setup() { let w = Math.min(windowWidth, 750), h = Math.min(windowHeight-30, 600); createCanvas(w, h); offWhite = color('#ede6dd'); // Bauhaus paper look overlayTex = createTextureLayer(w, h); shapeManager = new ShapeManager(); soundManager = new SoundManager(); uiManager = new UIManager(); noStroke(); smooth(); } function draw() { background(offWhite); image(overlayTex, 0, 0, width, height); // Add gentle noise texture shapeManager.updateAndDraw(); uiManager.draw(); } // ---------- Noise Texture Layer for Canvas ---------- // function createTextureLayer(w, h) { let g = createGraphics(w, h); g.noStroke(); for (let i = 0; i < 20000; i++) { let alpha = random(6,16); g.fill(220+random(-12,8), 215+random(-8,15), 205+random(-20,20), alpha); let s = random(0.5, 1.6); g.ellipse(random(w), random(h), s, s); } return g; } // -------------- User Interface (Mini "Piano") -------------- // class UIManager { constructor() { this.keyW = Math.round(width/9); // for 7 keys, some gutter this.keyH = 54; this.leftEdge = Math.round((width-(this.keyW*7))/2); this.top = height - this.keyH - 26; this.pressedKey = null; this.lastActive = -1; } draw() { // Piano keys textFont('Montserrat, sans-serif'); textAlign(CENTER,BOTTOM); textSize(15); for (let i=0;i<NOTE_MAPPING.length;i++) { let K = NOTE_MAPPING[i]; let x = this.leftEdge + i*this.keyW; // Key Color base fill(213,210,200,230); stroke('#ccc'); strokeWeight(1.2); rect(x, this.top, this.keyW-2, this.keyH, 7); // If active, highlight with animated color if (soundManager.isActive(K.key)) { let shapeC = color(K.color); shapeC.setAlpha(220); fill(shapeC); rect(x+2, this.top+2, this.keyW-6, this.keyH-9,8); } fill(60,60,60,180); noStroke(); text(K.key, x+this.keyW/2, this.top+this.keyH-15); textSize(11); fill(120,110,100,130); text("("+K.kb+")", x+this.keyW/2, this.top+this.keyH-2); } // "Kandinsky Synesthetic Piano" noStroke(); fill(66,60,50,160); textSize(17); textStyle(BOLD); text("Kandinsky Synesthetic Piano", width/2, this.top-18); textStyle(NORMAL); textSize(12); fill(90,80,66,120); text("← click or use A S D F G H J keys →", width/2, this.top-4); } getKeyAtPos(mx, my) { if (my < this.top || my > this.top+this.keyH) return null; let xrel = mx - this.leftEdge; if (xrel < 0 || xrel > this.keyW*7) return null; let idx = Math.floor(xrel/this.keyW); if (idx < 0 || idx > 6) return null; return NOTE_MAPPING[idx].key; } } // -------------- Sound Manager using p5.Oscillator -------------- // class SoundManager { constructor() { this.osc = new p5.Oscillator('sine'); this.env = new p5.Envelope(); this.env.setADSR(0.04, 0.14, 0.0, 0.28); this.env.setRange(0.2, 0); this.playingKeys = {}; this._started = false; } // Returns true if key is currently playing (recently) isActive(note) { return !!this.playingKeys[note]; } // Play a note (by 'C','D' etc param) and generate shape playNote(noteKey) { let idx = NOTE_MAPPING.findIndex(n=>n.key===noteKey); if (idx < 0) return; let noteInfo = NOTE_MAPPING[idx]; let now = millis(); this.playingKeys[noteKey] = now; // Pick oscillator type let oscType = (noteInfo.type==='yellow') ? 'triangle' : (noteInfo.type==='red' ? 'square' : 'sine'); this.osc.setType(oscType); this.osc.freq(noteInfo.freq, 0.03); if (this._started === false) { this.osc.start(); this._started = true; } this.env.play(this.osc, 0, 0.09); // Attack, short shapeManager.spawnShape(noteInfo, random(width*0.22, width*0.78), random(height*0.20, height*0.62)); setTimeout(() => {delete this.playingKeys[noteKey]}, 150); } } // -------------- Shape and Variants -------------- // class ShapeManager { constructor() { this.shapes = []; } // look for a spatially "open" spot for new Shape findBalancedPosition(radiusHint) { let nTries = 30, best = null, bestMinDist = -1; for (let i=0;i<nTries;i++) { let px = random(width*0.18, width*0.82); let py = random(height*0.18, height*0.63); let minD = 9999; for (let s of this.shapes) { let d = dist(px,py,s.x,s.y); minD = min(minD, d-s.radiusHint); } if (minD>bestMinDist) { bestMinDist = minD; best = {x:px, y:py}; } } return (best? {x:best.x, y:best.y} : {x:width/2, y:height/2}); } spawnShape(noteInfo, x, y) { let shapeType = noteInfo.type; let baseCol = color(noteInfo.color); // Choose random size in certain range, slightly larger for red let sz = (shapeType === 'red') ? random(width/13,width/8): random(width/18,width/12); // Harmonize position, avoid clutter clusters let pos = this.findBalancedPosition(sz/1.6); let shape; if (shapeType === 'blue') { shape = new BlueCircle(pos.x, pos.y, sz, baseCol, noteInfo.key); } else if (shapeType === 'red') { shape = new RedSquare(pos.x, pos.y, sz, baseCol, noteInfo.key); } else if (shapeType === 'yellow') { shape = new YellowTriangle(pos.x, pos.y, sz*1.01, baseCol, noteInfo.key); } this.shapes.push(shape); } updateAndDraw() { // Animate and draw all shapes // --- Fade, De-interact, etc. for (let i=this.shapes.length-1; i>=0; i--) { let s = this.shapes[i]; // Interact (collisions) for (let j=0;j<this.shapes.length;j++) if (j!==i) { s.interactWith(this.shapes[j]); } s.update(); s.draw(); if (s.life<=0) this.shapes.splice(i,1); } } } class Shape { // Base class, extended constructor(x,y,sz,col,note) { this.x = x; this.y = y; this.note = note; this.baseCol = col; this.sz = sz; this.radiusHint = sz * 0.57; // for spacing this.opacity = 240; this.fadeRate = 1.4; this.life = 255; // frames of life this.vx = 0; this.vy = 0; this.t = random(10000); this.scale = 1.0; } update() { // Each prominent shape defines movement logic this.x += this.vx; this.y += this.vy; this.life -= this.fadeRate; this.opacity = map(this.life, 0,255,0,200); } draw() {} interactWith(other) { // Default: no interaction } } // --- Blue = Circle, concentric, calming, "spiritual" drift inward --- // class BlueCircle extends Shape { constructor(x,y,sz,col,note){ super(x,y,sz,col,note); this.radiusHint = sz * 0.54; this.baseCol = color(60,80,190,210); this.col = col; this.angle = random(TWO_PI); this.centerTarget = createVector(width*0.5, height*0.47+10*noise(this.t)); // gentle breathing this.twirls = random(3600); } update() { // Spiral drift toward center this.twirls += 0.004 + 0.003*noise(this.twirls); let toward = p5.Vector.sub(this.centerTarget, createVector(this.x, this.y)) .limit(0.21).mult(map(this.life,0,255,0.7,0.25)); // slight orbit motion this.angle += 0.013 + 0.005*noise(this.twirls); this.x += toward.x + cos(this.angle)*0.16; this.y += toward.y + sin(this.angle)*0.13; // Gentle "breathing" (scale animates softly) this.scale = 0.97+0.08*sin(this.twirls+this.angle); super.update(); } draw() { push(); translate(this.x, this.y); noStroke(); let cl = lerpColor(color('#f0f7f9'), this.col, 0.69); cl.setAlpha(this.opacity*0.7); fill(cl); ellipse(0,0,this.sz*this.scale,this.sz*this.scale); // harmonic "ring" stroke(this.col.levels[0],this.col.levels[1],this.col.levels[2],this.opacity*0.39); strokeWeight(2); noFill(); ellipse(0,0,(this.sz*1.16)*this.scale,(this.sz*1.14)*this.scale + 0.7*sin(frameCount/5.7+this.angle)); pop(); } interactWith(other) { // If an aggressive triangle is close, nudge gently away if (other instanceof YellowTriangle) { let d = dist(this.x,this.y,other.x,other.y), minDist = this.radiusHint+other.radiusHint+9; if (d<minDist) { let nudge = p5.Vector.sub(createVector(this.x,this.y), createVector(other.x,other.y)); nudge.setMag((minDist-d)*0.06); this.x += nudge.x; this.y += nudge.y; } } // Keeps from overlapping other circles if (other instanceof BlueCircle && other !== this) { let d = dist(this.x,this.y,other.x,other.y), minDist = this.radiusHint+other.radiusHint+7; if (d<minDist) { let repel = p5.Vector.sub(createVector(this.x,this.y),createVector(other.x,other.y)); repel.setMag((minDist-d)*0.02); this.x += repel.x; this.y += repel.y; } } } } // --- Red = Square, solid, stable, powerful --- // class RedSquare extends Shape { constructor(x,y,sz,col,note){ super(x,y,sz*0.98,col,note); this.baseCol = color(197,30,50,220); // vibrant, slightly muted this.pulse = random(TWO_PI); this.pulseSpeed = random(0.009, 0.017); this.radiusHint = sz * 0.58; this.anchorX = Math.round((x/width)*8)*width/8; // snap to grid for balance this.anchorY = Math.round((y/height)*5)*height/5; } update() { // Low, solid, but gently pulsing this.pulse += this.pulseSpeed + noise(this.pulse)*0.004; this.scale = 0.97+0.06*sin(this.pulse); // very light magnetic return to grid anchor this.x += (this.anchorX-this.x)*0.035; this.y += (this.anchorY-this.y)*0.048; super.update(); } draw() { push(); translate(this.x, this.y); let c2 = lerpColor(color('#f7d5d5'), this.baseCol, 0.45); c2.setAlpha(this.opacity*0.78); stroke(this.baseCol.levels[0]+30,0,0,this.opacity*0.37); strokeWeight(2.0); fill(c2); rectMode(CENTER); rotate(PI/180*(1.8*sin(this.pulse))); rect(0,0,this.sz*this.scale, this.sz*this.scale,7); pop(); } interactWith(other) { // yellow triangle can nudge red if (other instanceof YellowTriangle) { let d = dist(this.x,this.y,other.x,other.y), minDist = this.radiusHint+other.radiusHint+8; if (d<minDist) { let toMove = p5.Vector.sub(createVector(this.x,this.y),createVector(other.x,other.y)); toMove.setMag((minDist-d)*0.11); this.x += toMove.x; this.y += toMove.y; } } // stays off other red squares if (other instanceof RedSquare && other !== this) { let d = dist(this.x,this.y,other.x,other.y), minDist = this.radiusHint+other.radiusHint+10; if (d<minDist) { let repel = p5.Vector.sub(createVector(this.x,this.y),createVector(other.x,other.y)); repel.setMag((minDist-d)*0.04); this.x += repel.x; this.y += repel.y; } } } } // --- Yellow = Triangle, energetic, sharp, darting --- // class YellowTriangle extends Shape { constructor(x,y,sz,col,note){ super(x,y,sz,col,note); this.col = color(255,233,80,210); // electric yellow this.radiusHint = sz*0.61; this.vel = p5.Vector.random2D().mult(random(0.7,2.5)); this.angle = random(TWO_PI); this.angularVel = random(0.001, 0.02); this.expansion = 1.0; } update() { this.x += this.vel.x; this.y += this.vel.y; // quick pop then fade & expand this.expansion += 0.03 + 0.04*noise(this.angle); if (this.life < 240) this.vel.mult(0.94); this.angle += this.angularVel; this.scale = map(this.life,255,0,1,1.23)*this.expansion; super.update(); } draw() { push(); translate(this.x, this.y); rotate(this.angle+frameCount/19.3); noStroke(); let shadow = color(140,130,30, min(70, this.opacity*0.19)); fill(shadow); triangle(-this.sz*0.51*this.scale, this.sz*0.41*this.scale, 0, -this.sz*0.62*this.scale, this.sz*0.51*this.scale, this.sz*0.41*this.scale); fill(this.col.levels[0],this.col.levels[1],this.col.levels[2],this.opacity*1.13); triangle(-this.sz*0.5*this.scale, this.sz*0.40*this.scale, 0, -this.sz*0.6*this.scale, this.sz*0.5*this.scale, this.sz*0.4*this.scale); pop(); } interactWith(other) { // Push away other aggressive shapes let d = dist(this.x,this.y,other.x,other.y), minDist = this.radiusHint+other.radiusHint+6; if (d<minDist) { let bump = p5.Vector.sub(createVector(this.x,this.y), createVector(other.x,other.y)); bump.setMag((minDist-d)*0.10 + random(-0.5,0.7)); this.x += bump.x; this.y += bump.y; } // Prevent from sticking out of bounds this.x=constrain(this.x, this.sz*0.44, width- this.sz*0.44); this.y=constrain(this.y, this.sz*0.44, height-uiManager.keyH-30, height-this.sz*0.35); } } // --------------- Input: Mouse and Keyboard -------------- // function mousePressed() { let note = uiManager.getKeyAtPos(mouseX, mouseY); if (note) { userStartAudio(); // for iOS/Chrome soundManager.playNote(note); } } function keyPressed() { let k = String.fromCharCode(keyCode).toUpperCase(); for (let nm of NOTE_MAPPING) { if (k === nm.kb) { userStartAudio(); soundManager.playNote(nm.key); return false; } } } windowResized = function() { let w = Math.min(windowWidth, 750), h = Math.min(windowHeight-30, 600); resizeCanvas(w, h); uiManager = new UIManager(); overlayTex = createTextureLayer(w, h); } </script> </body> </html> ``` **How to Use:** - Click the piano keys or use your keyboard (`A S D F G H J`) to play notes C to B. - Each note produces a sound and a Bauhaus/Kandinsky-inspired animated, interacting shape. - Blue (C, D): calm circles, drifting inward (spirit). - Red (E, F): strong squares, steady and grid-aligned. - Yellow (G, A, B): energetic triangles, radiating quickly and pushing others. - Shapes fade, move, and interact, composing a live artwork per Kandinsky's synesthetic principles. - The canvas background has a subtle, textured Bauhaus feel for authenticity.
### Artistic and Technical Interpretation #### Artistic Interpretation 1. **Color and Shape Associations**: - **Yellow Tones (G, A, B)**: High, sharp notes will generate triangles and zig-zag lines. These shapes will move quickly and expansively, embodying the energy and brightness Kandinsky associated with the trumpet. - **Blue Tones (C, D)**: Deep, low notes will generate circles and rounded shapes. These shapes will move slowly and concentrically, reflecting the calm and depth Kandinsky linked to the cello or organ. - **Red Tones (E, F)**: Mid-range, powerful notes will generate squares and stable shapes. These shapes will be vibrant and strong, representing the power Kandinsky associated with the tuba. 2. **Compositional Logic**: - **Balance and Harmony**: To maintain balance, shapes will be distributed across the canvas in a way that avoids clustering. New shapes will interact with existing ones, creating a dynamic yet harmonious composition. - **Interaction**: Shapes will have a slight repulsion effect, especially between contrasting shapes (e.g., triangles and circles). This interaction will create a sense of movement and dynamism. - **Fading**: Shapes will slowly fade away, leaving a trace of the "music" played. This will create a sense of history and continuity in the composition. 3. **Visual Style**: - **Background**: An off-white, textured background will be used to mimic the feel of Kandinsky's paintings. - **Aesthetic**: The overall aesthetic will be minimalist yet expressive, focusing on the interplay of shapes and colors to create a balanced abstract painting. #### Technical Interpretation 1. **Structure**: - **Shape Class**: This class will handle the creation, movement, and interaction of shapes. It will include properties like color, size, position, velocity, and lifespan. - **SoundManager Class**: This class will manage the sound generation using the p5.sound library. It will include methods to play different notes and handle user input. - **Main Logic**: The main logic will handle the user input (key presses), create new shapes based on the notes played, and manage the composition on the canvas. 2. **User Interaction**: - **On-Screen Keyboard**: A simple, minimalist on-screen keyboard will be displayed. Users can click on the keys or use their physical keyboard to play notes. - **Sound and Visual Feedback**: Each key press will generate a sound and a corresponding visual shape on the canvas. 3. **Animation and Interaction**: - **Movement**: Shapes will move according to their properties (e.g., triangles move quickly, circles move slowly). - **Interaction**: Shapes will interact with each other, creating a dynamic composition. For example, a new triangle might push away an existing circle. - **Fading**: Shapes will gradually fade away, leaving a trace of the composition over time. ### HTML File Here is the complete HTML file that implements the interactive audio-visual experience based on Kandinsky's synesthetic theories: ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Kandinsky's Synesthetic Piano</title> <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/addons/p5.sound.min.js"></script> <style> body { margin: 0; padding: 0; display: flex; justify-content: center; align-items: center; height: 100vh; background-color: #f5f5f5; font-family: Arial, sans-serif; } #canvas-container { text-align: center; } </style> </head> <body> <div id="canvas-container"> <h1>Kandinsky's Synesthetic Piano</h1> </div> <script> let shapes = []; let soundManager; let pianoKeys = ['C', 'D', 'E', 'F', 'G', 'A', 'B']; class Shape { constructor(x, y, color, size, shapeType, velocity) { this.x = x; this.y = y; this.color = color; this.size = size; this.shapeType = shapeType; this.velocity = velocity; this.lifespan = 255; } display() { noStroke(); fill(this.color[0], this.color[1], this.color[2], this.lifespan); switch (this.shapeType) { case 'triangle': triangle(this.x, this.y - this.size / 2, this.x - this.size / 2, this.y + this.size / 2, this.x + this.size / 2, this.y + this.size / 2); break; case 'circle': ellipse(this.x, this.y, this.size); break; case 'square': rectMode(CENTER); rect(this.x, this.y, this.size, this.size); break; } } move() { this.x += this.velocity.x; this.y += this.velocity.y; this.lifespan -= 1; } isDead() { return this.lifespan <= 0; } } class SoundManager { constructor() { this.oscillator = new p5.Oscillator(); this.oscillator.setType('sine'); this.oscillator.amp(0.5); this.notes = { 'C': 261.63, // C4 'D': 293.66, // D4 'E': 329.63, // E4 'F': 349.23, // F4 'G': 392.00, // G4 'A': 440.00, // A4 'B': 493.88 // B4 }; } playNote(note) { this.oscillator.freq(this.notes[note]); this.oscillator.start(); setTimeout(() => { this.oscillator.stop(); }, 500); } } function setup() { let canvas = createCanvas(800, 600); canvas.parent('canvas-container'); background(245, 245, 240); soundManager = new SoundManager(); createPiano(); } function draw() { background(245, 245, 240, 20); for (let i = shapes.length - 1; i >= 0; i--) { shapes[i].display(); shapes[i].move(); if (shapes[i].isDead()) { shapes.splice(i, 1); } } } function createPiano() { let piano = createDiv(); piano.position(width / 2 - 250, height - 50); piano.style('display', 'flex'); piano.style('justify-content', 'center'); piano.style('align-items', 'center'); piano.style('gap', '5px'); for (let i = 0; i < pianoKeys.length; i++) { let key = createButton(pianoKeys[i]); key.mousePressed(() => { playNote(pianoKeys[i]); soundManager.playNote(pianoKeys[i]); }); key.style('width', '50px'); key.style('height', '100px'); key.style('background-color', '#fff'); key.style('border', '1px solid #000'); key.style('border-radius', '5px'); key.style('font-size', '20px'); key.style('cursor', 'pointer'); } } function playNote(note) { let x = random(width); let y = random(height); let color, size, shapeType, velocity; switch (note) { case 'C': case 'D': color = [random(100, 200), random(100, 200), random(200, 255)]; size = random(20, 50); shapeType = 'circle'; velocity = createVector(random(-1, 1), random(-1, 1)).mult(0.5); break; case 'E': case 'F': color = [random(200, 255), random(100, 200), random(100, 200)]; size = random(20, 50); shapeType = 'square'; velocity = createVector(random(-1, 1), random(-1, 1)).mult(0.8); break; case 'G': case 'A': case 'B': color = [random(200, 255), random(200, 255), random(100, 200)]; size = random(20, 50); shapeType = 'triangle'; velocity = createVector(random(-2, 2), random(-2, 2)); break; } shapes.push(new Shape(x, y, color, size, shapeType, velocity)); } function keyPressed() { let note; switch (key) { case 'a': note = 'C'; break; case 's': note = 'D'; break; case 'd': note = 'E'; break; case 'f': note = 'F'; break; case 'g': note = 'G'; break; case 'h': note = 'A'; break; case 'j': note = 'B'; break; default: return; } playNote(note); soundManager.playNote(note); } </script> </body> </html> ``` This HTML file creates an interactive audio-visual experience based on Kandinsky's synesthetic theories. Users can play notes by clicking on the on-screen keyboard or using their physical keyboard. Each note generates a corresponding shape and sound, creating a dynamic and harmonious composition on the canvas.
### Artistic and Technical Interpretation As a creative coder, art historian, and musician, I'm drawing deeply from Wassily Kandinsky's "Concerning the Spiritual in Art," where he explores synesthesia—the interplay between color, form, and sound as expressions of inner spiritual necessity. Kandinsky believed art should evoke the soul's vibrations, with colors and shapes as "keys" on a spiritual piano. Yellow represents earthly, aggressive expansion (like a trumpet's blast), blue embodies heavenly depth and tranquility (like a cello's resonance), and red conveys grounded strength and passion (like a tuba's fanfare). I'll translate this into an interactive experience where users "play" a piano, generating sounds and visuals that build a real-time abstract composition, emphasizing harmony and spiritual balance over chaos. **Artistic Interpretation:** - **Spirituality of Blue:** For low notes (C, D), I'll animate circles that slowly expand and contract concentrically, mimicking the "infinite depth" Kandinsky described in blue—evoking a meditative pull inward, like sinking into the cosmos. The animation will be subtle, with soft pulsations and slow fades, symbolizing spiritual introspection and calm. This contrasts with yellow's outward "狂喜" (ecstatic outburst) and red's earthly stability. - **Yellow Tones (G, A, B):** Sharp triangles or zig-zags will dart across the canvas with quick, erratic movements, expanding rapidly before fading, representing aggressive energy and spiritual awakening through tension. - **Red Tones (E, F):** Squares will appear stably, perhaps rotating slowly with a glowing aura, symbolizing balanced power and inner warmth without overwhelming the composition. - **Composition and Harmony:** To maintain Kandinsky's emphasis on balanced abstraction, shapes won't spawn randomly; they'll be placed based on a gravitational system where new shapes are attracted to or repelled by existing ones. For instance, a yellow triangle might "push" a blue circle slightly, creating dynamic tension, but the overall layout will use a subtle force-directed algorithm to ensure equilibrium (e.g., shapes settle into harmonious positions). Shapes will fade slowly (over 10-20 seconds), leaving ethereal traces, allowing the "music" to build a layered painting that evolves spiritually—echoing Kandinsky's idea of art as a symphonic composition. - **Overall Aesthetic:** A textured off-white background (using p5.js noise for a canvas-like grain) will ground the piece in Bauhaus minimalism, with colors vibrant yet not garish, ensuring the experience feels like an improvisational Kandinsky painting (e.g., inspired by "Composition VIII"). **Technical Interpretation:** - **Structure:** I'll use p5.js for canvas rendering and p5.sound for audio. The code will be structured with: - **Shape Class:** Base class for all visuals, with subclasses for Circle (blue), Triangle/Zigzag (yellow), and Square (red). Each has properties for position, size, color, velocity, lifespan, and animation logic (e.g., pulsate for blue, dart for yellow). - **SoundManager Class:** Handles oscillators for each note, mapping keys to frequencies (e.g., C4=261.63Hz). It triggers sounds and instantiates corresponding shapes. - **Main Logic:** In setup(), create the canvas and on-screen keyboard (simple rectangles with labels). In draw(), update and render all shapes, apply interactions (basic physics: attraction/repulsion forces), and fade them. Keyboard events (keyPressed) and mouse clicks on keys will trigger SoundManager.playNote(note). - **Interactions:** Shapes stored in an array; each frame, compute simple forces (e.g., yellow shapes repel blue ones by adjusting velocities). Fading via alpha transparency decrease over time. - **Constraints:** Everything in one HTML file, loading p5.js and p5.sound from CDN. No external assets; all generative. This setup ensures a cohesive, spiritual experience: users compose "spiritual symphonies" where sound, color, and form harmonize in real-time. ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Kandinsky's Synesthetic Piano</title> <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/addons/p5.sound.min.js"></script> <style> body { margin: 0; display: flex; justify-content: center; align-items: center; height: 100vh; background: #f5f5f5; } canvas { border: 1px solid #ccc; } </style> </head> <body> <script> let shapes = []; let keys = ['C', 'D', 'E', 'F', 'G', 'A', 'B']; let keyPositions = []; let keyWidth = 80; let keyHeight = 200; let keyboardY = 500; let soundManager; class Shape { constructor(x, y, size, color, lifespan, velocity) { this.pos = createVector(x, y); this.size = size; this.color = color; this.lifespan = lifespan; this.alpha = 255; this.velocity = velocity || createVector(0, 0); this.type = 'base'; } update() { this.pos.add(this.velocity); this.alpha -= 255 / this.lifespan; if (this.alpha < 0) this.alpha = 0; } isAlive() { return this.alpha > 0; } interact(other) { // Basic repulsion/attraction let d = p5.Vector.dist(this.pos, other.pos); if (d < 100 && d > 0) { let force = p5.Vector.sub(this.pos, other.pos).normalize().mult(0.05); if (this.type === 'yellow' && other.type === 'blue') { other.velocity.add(force); // Yellow pushes blue } else if (this.type === 'blue' && other.type === 'yellow') { this.velocity.sub(force.mult(0.5)); // Blue mildly attracts } } } display() { // Overridden in subclasses } } class BlueCircle extends Shape { constructor(x, y) { super(x, y, 50, color(0, 0, 255), 600, createVector(0, 0)); this.type = 'blue'; this.pulse = 0; this.pulseSpeed = 0.01; } update() { super.update(); this.pulse += this.pulseSpeed; this.size = 50 + 20 * sin(this.pulse); // Slow concentric pulsation this.velocity.mult(0.98); // Slow down } display() { noStroke(); fill(this.color[0], this.color[1], this.color[2], this.alpha); ellipse(this.pos.x, this.pos.y, this.size, this.size); // Concentric ring fill(255, 255, 255, this.alpha / 2); ellipse(this.pos.x, this.pos.y, this.size * 0.7, this.size * 0.7); } } class RedSquare extends Shape { constructor(x, y) { super(x, y, 60, color(255, 0, 0), 500, createVector(0, 0)); this.type = 'red'; this.rotation = 0; } update() { super.update(); this.rotation += 0.005; // Slow rotation for stability } display() { push(); translate(this.pos.x, this.pos.y); rotate(this.rotation); noStroke(); fill(this.color[0], this.color[1], this.color[2], this.alpha); rectMode(CENTER); rect(0, 0, this.size, this.size); pop(); } } class YellowTriangle extends Shape { constructor(x, y) { super(x, y, 50, color(255, 255, 0), 400, createVector(random(-2, 2), random(-2, 2))); this.type = 'yellow'; } update() { super.update(); this.size += 0.5; // Expand energetically this.velocity.mult(1.01); // Accelerate if (random() < 0.1) this.velocity.rotate(random(-PI/4, PI/4)); // Zig-zag } display() { noStroke(); fill(this.color[0], this.color[1], this.color[2], this.alpha); triangle( this.pos.x, this.pos.y - this.size / 2, this.pos.x - this.size / 2, this.pos.y + this.size / 2, this.pos.x + this.size / 2, this.pos.y + this.size / 2 ); } } class SoundManager { constructor() { this.oscillators = {}; this.notes = { 'C': 261.63, 'D': 293.66, 'E': 329.63, 'F': 349.23, 'G': 392.00, 'A': 440.00, 'B': 493.88 }; for (let note in this.notes) { this.oscillators[note] = new p5.Oscillator(this.notes[note], 'sine'); this.oscillators[note].amp(0); this.oscillators[note].start(); } } playNote(note) { let osc = this.oscillators[note]; osc.amp(0.5, 0.1); osc.amp(0, 0.5, 0.5); // Fade out // Generate shape at random position, but biased towards center let x = width / 2 + random(-200, 200); let y = height / 2 + random(-100, 100) - 100; // Above keyboard let shape; if (['C', 'D'].includes(note)) { shape = new BlueCircle(x, y); } else if (['E', 'F'].includes(note)) { shape = new RedSquare(x, y); } else if (['G', 'A', 'B'].includes(note)) { shape = new YellowTriangle(x, y); } shapes.push(shape); } } function setup() { createCanvas(800, 600); soundManager = new SoundManager(); // Keyboard positions let startX = (width - keys.length * keyWidth) / 2; for (let i = 0; i < keys.length; i++) { keyPositions.push({ x: startX + i * keyWidth, note: keys[i] }); } // Textured background setup background(240, 240, 220); // Off-white loadPixels(); for (let i = 0; i < pixels.length; i += 4) { let noiseVal = noise(i / 10000) * 50 - 25; pixels[i] += noiseVal; pixels[i+1] += noiseVal; pixels[i+2] += noiseVal; } updatePixels(); } function draw() { // Redraw background with fade for traces fill(240, 240, 220, 10); rect(0, 0, width, height); // Update and display shapes for (let i = shapes.length - 1; i >= 0; i--) { shapes[i].update(); for (let j = 0; j < shapes.length; j++) { if (i !== j) shapes[i].interact(shapes[j]); } shapes[i].display(); if (!shapes[i].isAlive()) shapes.splice(i, 1); } // Draw keyboard for (let i = 0; i < keyPositions.length; i++) { let pos = keyPositions[i]; fill(255); stroke(0); rect(pos.x, keyboardY, keyWidth, keyHeight); fill(0); textAlign(CENTER); textSize(32); text(pos.note, pos.x + keyWidth / 2, keyboardY + keyHeight / 2); } } function mousePressed() { for (let pos of keyPositions) { if (mouseX > pos.x && mouseX < pos.x + keyWidth && mouseY > keyboardY && mouseY < keyboardY + keyHeight) { soundManager.playNote(pos.note); } } } function keyPressed() { let note = key.toUpperCase(); if (keys.includes(note)) { soundManager.playNote(note); } } </script> </body> </html> ```
```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Kandinsky Synesthesia</title> <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/addons/p5.sound.min.js"></script> <style> body { margin: 0; padding: 0; overflow: hidden; background-color: #f5f1e6; } </style> </head> <body> <script> // Artistic Interpretation: // Using Kandinsky's color-sound-shape associations with a spiritual dimension: // - Blue circles will pulse slowly like deep breathing (spiritual contemplation) // - Yellow triangles will radiate outward with sharp energy (spiritual awakening) // - Red squares will anchor the composition (spiritual stability) // Composition follows Kandinsky's theory of "inner necessity" - each shape responds to musical impulse but finds equilibrium let oscillators = {}; let shapes = []; let noteNames = ['C', 'D', 'E', 'F', 'G', 'A', 'B']; let notes = { 'C': 261.63, 'D': 293.66, 'E': 329.63, 'F': 349.23, 'G': 392.00, 'A': 440.00, 'B': 493.88 }; class Shape { constructor(x, y, note) { this.x = x; this.y = y; this.note = note; this.lifespan = 255; this.size = 50 + random(30); this.velocity = createVector(0, 0); // Determine shape properties based on note if (['G', 'A', 'B'].includes(note)) { // Yellow energy (trumpet) this.type = 'triangle'; this.color = color(255, 235, 59, this.lifespan); this.velocity = p5.Vector.random2D().mult(2); this.rotation = random(TWO_PI); this.rotationSpeed = random(-0.05, 0.05); this.size *= 0.8; } else if (['E', 'F'].includes(note)) { // Red power (tuba) this.type = 'square'; this.color = color(244, 67, 54, this.lifespan); this.velocity = p5.Vector.random2D().mult(0.5); this.rotation = random(TWO_PI); } else { // Blue calm (cello) this.type = 'circle'; this.color = color(33, 150, 243, this.lifespan); this.velocity = createVector(0, 0); this.pulse = 0; this.pulseSpeed = random(0.01, 0.03); this.size *= 1.2; } } update() { this.x += this.velocity.x; this.y += this.velocity.y; this.lifespan -= 0.5; this.color.setAlpha(this.lifespan); // Special behaviors if (this.type === 'circle') { this.pulse = sin(frameCount * this.pulseSpeed) * 5; } else if (this.type === 'triangle') { this.rotation += this.rotationSpeed; } // Edge bouncing if (this.x < 0 || this.x > width) this.velocity.x *= -0.8; if (this.y < 0 || this.y > height) this.velocity.y *= -0.8; // Repel from other shapes shapes.forEach(other => { if (other !== this) { let d = dist(this.x, this.y, other.x, other.y); if (d < this.size/2 + other.size/2) { let force = p5.Vector.sub(this, other); force.normalize(); force.mult(0.5); this.velocity.add(force); } } }); } display() { push(); translate(this.x, this.y); fill(this.color); noStroke(); if (this.type === 'circle') { ellipse(0, 0, this.size + this.pulse); // Inner concentric circles fill(red(this.color), green(this.color), blue(this.color), this.lifespan * 0.5); ellipse(0, 0, this.size * 0.7 + this.pulse * 0.7); } else if (this.type === 'square') { rotate(this.rotation); rectMode(CENTER); rect(0, 0, this.size, this.size); // Inner square fill(red(this.color), green(this.color), blue(this.color), this.lifespan * 0.5); rect(0, 0, this.size * 0.7, this.size * 0.7); } else if (this.type === 'triangle') { rotate(this.rotation); triangle( -this.size/2, this.size/2, this.size/2, this.size/2, 0, -this.size/2 ); // Inner triangle fill(red(this.color), green(this.color), blue(this.color), this.lifespan * 0.5); triangle( -this.size/3, this.size/3, this.size/3, this.size/3, 0, -this.size/3 ); } pop(); } isDead() { return this.lifespan < 0; } } function setup() { createCanvas(windowWidth, windowHeight); colorMode(RGB, 255, 255, 255, 255); // Create oscillators for each note noteNames.forEach(n => { oscillators[n] = new p5.Oscillator(); oscillators[n].setType('sine'); oscillators[n].freq(notes[n]); oscillators[n].amp(0); oscillators[n].start(); }); // Background texture background(245, 241, 230); for (let i = 0; i < 1000; i++) { stroke(220, 10); point(random(width), random(height)); } } function draw() { // Fade background slightly fill(245, 241, 230, 10); noStroke(); rect(0, 0, width, height); // Update and display shapes for (let i = shapes.length - 1; i >= 0; i--) { shapes[i].update(); shapes[i].display(); if (shapes[i].isDead()) { shapes.splice(i, 1); } } } function keyPressed() { let note = String.fromCharCode(keyCode); if (noteNames.includes(note)) { playNote(note); } } function mousePressed() { // Play random note when clicking canvas let note = random(noteNames); playNote(note); } function playNote(note) { let x = mouseX !== 0 ? mouseX : random(width); let y = mouseY !== 0 ? mouseY : random(height); // Create visual shape shapes.push(new Shape(x, y, note)); // Play sound with ADSR envelope oscillators[note].amp(0.7, 0.01); setTimeout(() => { oscillators[note].amp(0, 0.3); }, 200); // Visual flash push(); noStroke(); fill(255, 255, 255, 100); ellipse(x, y, 100, 100); pop(); } function windowResized() { resizeCanvas(windowWidth, windowHeight); } </script> </body> </html> ```
Plan d’Action Artistic & Theoretical Interpretation 1. Spirituality of Blue (C, D) - Blue embodies calm, depth, introspection. We translate this into concentric circles that expand slowly from the play point, with soft edges and a gentle, decelerating outward velocity. Each circle carries a fading alpha, like ripples in still water—an echo of the cello’s sonority. 2. Energy of Yellow (G, A, B) - Yellow is sharp, electric, extroverted. We render it as equilateral triangles launched with high initial velocity in random directions, leaving brisk trailing strokes (zig-zag jitter). Their motion is rapid and expansive, reflecting the brilliance of a trumpet. 3. Power of Red (E, F) - Red is weighty, stable, and strong. We draw solid squares that appear near the canvas center, with minimal velocity—anchored, resolute—echoing the tuba’s grounded resonance. Compositional Harmony - Background: a lightly textured off-white canvas generated once via a p5.Graphics layer speckled with semi-transparent noise dots. - Shape Lifecycle: each shape has position, velocity, size, color, alpha. On every frame, shapes update their position (if they have velocity), draw themselves at current alpha, then reduce alpha. When alpha ≤ 0 we remove them. - Interaction Rule: when a new yellow triangle spawns, we “push” existing blue circles away by adjusting their velocities along the repulsion vector from triangle to circle. - Balance: by blending fast-fading yellow, slow-fading blue, and stable red, the canvas evolves into a living abstract composition reminiscent of Kandinsky’s spiritual geometries. Code Structure 1. Class Shape (abstract) - properties: pos (p5.Vector), vel, size, color, alpha, type - methods: update(), draw() 2. Subclasses - CircleShape: draw() renders concentric/fading circle - TriangleShape: draw() renders an equilateral triangle with a slight zig-zag outline - SquareShape: draw() renders a centered square 3. SoundManager - maps notes (“C”–“B”) to frequencies and shape constructors - on play(note): • create and trigger an envelope on a p5.Oscillator at that frequency • instantiate the appropriate Shape at the canvas center • if it’s a triangle, apply repulsion to existing circles 4. UI - On-screen keyboard: 7 keys drawn at canvas bottom, clickable; also map physical keys ‘c’–‘b’ to notes. 5. p5.js Lifecycle - setup(): create canvas, noise background, initialize SoundManager, define key rectangles - draw(): render noise background, update/draw all shapes, render keyboard - mousePressed() & keyPressed(): delegate to SoundManager to play corresponding note Below is the complete, self-contained HTML file. ```html <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Kandinsky Synesthetic Piano</title> <style> body { margin: 0; overflow: hidden; } canvas { display: block; } </style> </head> <body> <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/addons/p5.sound.min.js"></script> <script> let shapes = []; let soundManager; let bgGraphics; let keyZones = []; const NOTES = [ {note:'C', freq:261.63, type:'blue'}, {note:'D', freq:293.66, type:'blue'}, {note:'E', freq:329.63, type:'red'}, {note:'F', freq:349.23, type:'red'}, {note:'G', freq:392.00, type:'yellow'}, {note:'A', freq:440.00, type:'yellow'}, {note:'B', freq:493.88, type:'yellow'} ]; function setup() { createCanvas(windowWidth, windowHeight); // Create textured off-white background bgGraphics = createGraphics(width, height); bgGraphics.background(245, 240, 230); bgGraphics.noStroke(); for (let i = 0; i < width * height * 0.002; i++) { bgGraphics.fill(255, random(10)); bgGraphics.ellipse(random(width), random(height), random(1,3)); } soundManager = new SoundManager(); // Define on-screen keys let keyW = width / NOTES.length; let keyH = 60; for (let i = 0; i < NOTES.length; i++) { keyZones.push({ note: NOTES[i].note, x: i * keyW, y: height - keyH, w: keyW, h: keyH }); } } function draw() { image(bgGraphics, 0, 0); // Update & draw shapes for (let i = shapes.length - 1; i >= 0; i--) { let s = shapes[i]; s.update(); s.draw(); if (s.alpha <= 0) shapes.splice(i,1); } drawKeyboard(); } function drawKeyboard() { noStroke(); for (let k of keyZones) { fill(200); rect(k.x, k.y, k.w, k.h); fill(50); textAlign(CENTER, CENTER); textSize(20); text(k.note, k.x + k.w/2, k.y + k.h/2); } } function mousePressed() { for (let k of keyZones) { if (mouseX > k.x && mouseX < k.x + k.w && mouseY > k.y && mouseY < k.y + k.h) { soundManager.play(k.note); } } } function keyPressed() { let nk = key.toUpperCase(); if ('CDEFGAB'.includes(nk)) { soundManager.play(nk); } } // ----------------------- SoundManager ----------------------- class SoundManager { constructor() { this.osc = new p5.Oscillator('sine'); this.osc.start(); this.env = new p5.Envelope(0.001, 0.5, 0.3, 1); this.osc.amp(this.env); } play(note) { let info = NOTES.find(n => n.note === note); if (!info) return; this.osc.freq(info.freq); this.env.play(); // Spawn shape let shape; let center = createVector(width/2, height/2); if (info.type === 'blue') { shape = new CircleShape(center, info); } else if (info.type === 'red') { shape = new SquareShape(center, info); } else { shape = new TriangleShape(center, info); // Push away blue circles for (let s of shapes) { if (s.type === 'blue') { let dir = p5.Vector.sub(s.pos, shape.pos).normalize().mult(2); s.vel.add(dir); } } } shapes.push(shape); } } // ----------------------- Shape Classes ----------------------- class Shape { constructor(pos, info) { this.pos = pos.copy(); this.vel = createVector(0,0); this.size = random(40, 80); this.color = info.type === 'blue' ? color(50,100,200) : info.type === 'red' ? color(200,50,50) : color(240,200,50); this.alpha = 255; this.type = info.type; } update() { this.pos.add(this.vel); this.alpha -= 1.5; } } class CircleShape extends Shape { constructor(pos, info) { super(pos, info); this.vel = p5.Vector.random2D().mult(0.3); } draw() { noFill(); stroke(red(this.color), green(this.color), blue(this.color), this.alpha); strokeWeight(2); ellipse(this.pos.x, this.pos.y, this.size); } } class SquareShape extends Shape { constructor(pos, info) { super(pos, info); this.vel = createVector(0,0); } draw() { noStroke(); fill(red(this.color), green(this.color), blue(this.color), this.alpha); rectMode(CENTER); rect(this.pos.x, this.pos.y, this.size, this.size); } } class TriangleShape extends Shape { constructor(pos, info) { super(pos, info); this.vel = p5.Vector.random2D().mult(3); } draw() { push(); translate(this.pos.x, this.pos.y); rotate(frameCount * 0.02); noFill(); stroke(red(this.color), green(this.color), blue(this.color), this.alpha); strokeWeight(3); let h = this.size * (sqrt(3)/2); beginShape(); vertex(-this.size/2 + random(-3,3), h/3 + random(-3,3)); vertex(this.size/2 + random(-3,3), h/3 + random(-3,3)); vertex(0 + random(-3,3), -2*h/3 + random(-3,3)); endShape(CLOSE); pop(); } } function windowResized() { resizeCanvas(windowWidth, windowHeight); } </script> </body> </html> ```
### Artistic and Technical Interpretation #### Interpretation of Kandinsky's Theories To create an interactive audio-visual experience based on Kandinsky's synesthetic theories, I will translate his color and sound associations into animations and sounds. The "spirituality" of blue will be represented through slow, concentric, and calming movements of circles. The blue color will be used to create a sense of depth, with the circles fading into the background as they move outward. To manage the composition of shapes on the canvas and maintain harmony, I will implement a few key principles: 1. **Shape Interaction**: New shapes will interact with existing shapes by pushing them away or attracting them, depending on their characteristics (e.g., color, size, and velocity). 2. **Shape Fading**: Shapes will fade away slowly over time, leaving a trace of the "music" that has been played. 3. **Color Balance**: The colors used for the shapes will be balanced to create a harmonious composition. For example, if the canvas is dominated by warm colors (e.g., yellow, red), the next shape will be more likely to be a cool color (e.g., blue). 4. **Shape Distribution**: Shapes will be distributed across the canvas in a way that maintains balance and harmony. For example, if one side of the canvas is crowded, the next shape will be more likely to appear on the other side. #### Technical Implementation To implement the interactive audio-visual experience, I will create the following classes: * `Shape`: Represents a visual shape on the canvas, with properties like position, velocity, color, and size. The `update` method will update the shape's position and velocity, and the `display` method will draw the shape on the canvas. * `SoundManager`: Manages the sound generation for each note. It will use the p5.js Oscillator to generate tones. * `Keyboard`: Represents the on-screen keyboard, with methods to handle user input (both mouse clicks and keyboard presses). The main logic will be implemented in the `setup` and `draw` functions provided by p5.js. The `setup` function will initialize the canvas, keyboard, and sound manager, while the `draw` function will continuously update and display the shapes on the canvas. ### Code ```html <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Kandinsky's Synesthesia</title> <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/addons/p5.sound.min.js"></script> <style> body { margin: 0; background-color: #f0f0f0; } canvas { display: block; } </style> </head> <body> <script> // Define the Shape class class Shape { constructor(x, y, color, type, velocity) { this.x = x; this.y = y; this.color = color; this.type = type; this.velocity = velocity; this.size = random(20, 50); this.alpha = 255; } update() { this.x += this.velocity.x; this.y += this.velocity.y; this.alpha -= 2; } display() { fill(this.color[0], this.color[1], this.color[2], this.alpha); noStroke(); if (this.type === 'circle') { ellipse(this.x, this.y, this.size); } else if (this.type === 'triangle') { triangle(this.x - this.size / 2, this.y + this.size / 2, this.x + this.size / 2, this.y + this.size / 2, this.x, this.y - this.size / 2); } else if (this.type === 'square') { rectMode(CENTER); rect(this.x, this.y, this.size, this.size); } } isDead() { return this.alpha < 0; } } // Define the SoundManager class class SoundManager { constructor() { this.oscillators = {}; this.notes = { 'C': 261.63, 'D': 293.66, 'E': 329.63, 'F': 349.23, 'G': 392.00, 'A': 440.00, 'B': 493.88 }; } playNote(note) { if (!this.oscillators[note]) { this.oscillators[note] = new p5.Oscillator(this.notes[note], 'sine'); this.oscillators[note].amp(0.5); this.oscillators[note].start(); } else { this.oscillators[note].freq(this.notes[note]); } } stopNote(note) { if (this.oscillators[note]) { this.oscillators[note].amp(0, 0.1); setTimeout(() => { this.oscillators[note].stop(); delete this.oscillators[note]; }, 100); } } } // Define the Keyboard class class Keyboard { constructor() { this.keys = ['C', 'D', 'E', 'F', 'G', 'A', 'B']; this.keyMap = { 'c': 'C', 'd': 'D', 'e': 'E', 'f': 'F', 'g': 'G', 'a': 'A', 'b': 'B' }; } display() { for (let i = 0; i < this.keys.length; i++) { fill(255); rect(i * 50, height - 50, 50, 50); fill(0); textSize(24); textAlign(CENTER, CENTER); text(this.keys[i], i * 50 + 25, height - 25); } } handleMousePress(x, y) { if (y > height - 50) { let keyIndex = floor(x / 50); if (keyIndex >= 0 && keyIndex < this.keys.length) { return this.keys[keyIndex]; } } return null; } handleKeyPress(key) { return this.keyMap[key.toLowerCase()]; } } let shapes = []; let soundManager; let keyboard; function setup() { createCanvas(windowWidth, windowHeight); background(240); soundManager = new SoundManager(); keyboard = new Keyboard(); } function draw() { background(240, 10); keyboard.display(); for (let i = shapes.length - 1; i >= 0; i--) { shapes[i].update(); shapes[i].display(); if (shapes[i].isDead()) { shapes.splice(i, 1); } } for (let i = 0; i < shapes.length; i++) { for (let j = i + 1; j < shapes.length; j++) { let d = dist(shapes[i].x, shapes[i].y, shapes[j].x, shapes[j].y); if (d < shapes[i].size / 2 + shapes[j].size / 2) { shapes[i].velocity.x *= -1; shapes[i].velocity.y *= -1; shapes[j].velocity.x *= -1; shapes[j].velocity.y *= -1; } } } } function mousePressed() { let note = keyboard.handleMousePress(mouseX, mouseY); if (note) { soundManager.playNote(note); let color; let type; let velocity; if (['C', 'D'].includes(note)) { color = [0, 0, 255]; // blue type = 'circle'; velocity = { x: random(-1, 1), y: random(-1, 1) }; } else if (['E', 'F'].includes(note)) { color = [255, 0, 0]; // red type = 'square'; velocity = { x: random(-2, 2), y: random(-2, 2) }; } else { color = [255, 255, 0]; // yellow type = 'triangle'; velocity = { x: random(-3, 3), y: random(-3, 3) }; } shapes.push(new Shape(mouseX, mouseY, color, type, velocity)); } } function mouseReleased() { let note = keyboard.handleMousePress(mouseX, mouseY); if (note) { soundManager.stopNote(note); } } function keyPressed() { let note = keyboard.handleKeyPress(key); if (note) { soundManager.playNote(note); let color; let type; let velocity; if (['C', 'D'].includes(note)) { color = [0, 0, 255]; // blue type = 'circle'; velocity = { x: random(-1, 1), y: random(-1, 1) }; } else if (['E', 'F'].includes(note)) { color = [255, 0, 0]; // red type = 'square'; velocity = { x: random(-2, 2), y: random(-2, 2) }; } else { color = [255, 255, 0]; // yellow type = 'triangle'; velocity = { x: random(-3, 3), y: random(-3, 3) }; } shapes.push(new Shape(width / 2, height / 2, color, type, velocity)); } } function keyReleased() { let note = keyboard.handleKeyPress(key); if (note) { soundManager.stopNote(note); } } </script> </body> </html> ``` ### Example Use Cases 1. **Playing the Keyboard**: Users can play the on-screen keyboard by clicking on the keys or pressing the corresponding keys on their physical keyboard. Each key press will generate a sound and a visual response on the canvas. 2. **Exploring Color and Sound Associations**: By playing different notes, users can explore Kandinsky's color and sound associations. For example, playing low notes like C and D will generate blue circles, while playing high notes like G, A, and B will generate yellow triangles. 3. **Creating Compositions**: Users can create abstract compositions by playing multiple notes in sequence. The shapes generated by each note will interact with each other, creating a dynamic and harmonious visual experience. This interactive audio-visual experience is a creative interpretation of Kandinsky's synesthetic theories, allowing users to explore the connections between sound, color, and shape in a dynamic and engaging way.
## Artistic and Technical Interpretation ### The Spiritual Translation of Kandinsky's Vision **Blue's Spirituality**: I'll interpret blue's spiritual depth through slow, meditative circular movements that emanate from within, like ripples in a deep pond. The circles will have soft, breathing-like pulsations and gentle rotational movements, representing the introspective nature Kandinsky associated with blue. **Yellow's Energy**: Yellow will manifest as sharp, angular triangles that dart across the canvas with nervous energy, creating jagged paths and quick, staccato movements that mirror the aggressive brilliance Kandinsky heard in trumpet sounds. **Red's Power**: Red squares will appear with confident, grounded presence, slowly rotating and maintaining their position with authority, embodying the powerful resonance of brass instruments. ### Compositional Harmony System I'll implement a **force-field composition system** where: - Each shape has an invisible influence radius - New shapes are attracted to or repelled by existing ones based on Kandinsky's color relationships - Blue circles create calm zones that attract other shapes - Yellow triangles create energetic disruption fields - Red squares act as stable anchor points ### Technical Architecture **Classes**: - `KandinskyShape`: Base class with color theory, movement, and interaction - `BlueCircle`, `YellowTriangle`, `RedSquare`: Specialized behaviors - `CompositionManager`: Handles spatial harmony and shape interactions - `SoundVisualizer`: Connects audio frequencies to visual parameters **Main Logic Flow**: 1. Key press triggers both sound oscillator and shape creation 2. Shape spawns based on Kandinsky's color-sound associations 3. Composition manager calculates optimal placement using attraction/repulsion 4. Shapes evolve over time with breathing, rotation, and fade effects 5. Canvas maintains balanced composition through dynamic repositioning --- ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Kandinsky's Synesthetic Piano</title> <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/p5.js/1.7.0/addons/p5.sound.min.js"></script> <style> body { margin: 0; padding: 0; font-family: 'Arial', sans-serif; background: #f5f5f0; overflow: hidden; } .info { position: absolute; top: 20px; left: 20px; z-index: 100; color: #333; font-size: 14px; max-width: 300px; } .keyboard { position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%); z-index: 100; display: flex; gap: 5px; } .key { width: 50px; height: 120px; background: white; border: 2px solid #333; cursor: pointer; display: flex; align-items: flex-end; justify-content: center; padding-bottom: 10px; font-weight: bold; transition: all 0.1s; user-select: none; } .key:hover { background: #f0f0f0; } .key.active { background: #ddd; transform: translateY(3px); } .key.yellow { border-color: #FFD700; } .key.blue { border-color: #4169E1; } .key.red { border-color: #DC143C; } </style> </head> <body> <div class="info"> <h3>Kandinsky's Synesthetic Piano</h3> <p>Click keys or press C, D, E, F, G, A, B to create sound and visual harmony based on Kandinsky's color-sound theories.</p> <p><strong>Blue</strong>: Deep, spiritual circles | <strong>Red</strong>: Powerful squares | <strong>Yellow</strong>: Energetic triangles</p> </div> <div class="keyboard"> <div class="key blue" data-note="C">C</div> <div class="key blue" data-note="D">D</div> <div class="key red" data-note="E">E</div> <div class="key red" data-note="F">F</div> <div class="key yellow" data-note="G">G</div> <div class="key yellow" data-note="A">A</div> <div class="key yellow" data-note="B">B</div> </div> <script> let shapes = []; let oscillator; let envelope; let canvas; let backgroundTexture; // Kandinsky's color-sound associations const noteMapping = { 'C': { freq: 261.63, color: 'blue', type: 'circle' }, 'D': { freq: 293.66, color: 'blue', type: 'circle' }, 'E': { freq: 329.63, color: 'red', type: 'square' }, 'F': { freq: 349.23, color: 'red', type: 'square' }, 'G': { freq: 392.00, color: 'yellow', type: 'triangle' }, 'A': { freq: 440.00, color: 'yellow', type: 'triangle' }, 'B': { freq: 493.88, color: 'yellow', type: 'triangle' } }; class KandinskyShape { constructor(x, y, note) { this.x = x; this.y = y; this.note = note; this.mapping = noteMapping[note]; this.age = 0; this.maxAge = 300; this.baseSize = 40; this.size = this.baseSize; this.alpha = 255; this.rotation = 0; this.pulsePhase = random(TWO_PI); // Movement properties this.vx = random(-2, 2); this.vy = random(-2, 2); // Color based on Kandinsky's theory this.setColor(); // Find optimal position using composition rules this.findOptimalPosition(); } setColor() { switch(this.mapping.color) { case 'blue': this.r = 65; this.g = 105; this.b = 225; // RoyalBlue break; case 'red': this.r = 220; this.g = 20; this.b = 60; // Crimson break; case 'yellow': this.r = 255; this.g = 215; this.b = 0; // Gold break; } } findOptimalPosition() { let bestX = this.x; let bestY = this.y; let bestScore = -Infinity; // Test multiple positions for (let i = 0; i < 20; i++) { let testX = random(100, width - 100); let testY = random(100, height - 200); let score = this.evaluatePosition(testX, testY); if (score > bestScore) { bestScore = score; bestX = testX; bestY = testY; } } this.x = bestX; this.y = bestY; } evaluatePosition(x, y) { let score = 0; for (let other of shapes) { let distance = dist(x, y, other.x, other.y); // Kandinsky's composition rules if (this.mapping.color === 'blue' && other.mapping.color === 'yellow') { // Blue and yellow create tension - moderate distance preferred score += distance > 80 && distance < 150 ? 10 : -5; } else if (this.mapping.color === 'red') { // Red is stable - can be close to others score += distance > 60 ? 5 : -2; } else if (this.mapping.color === 'yellow') { // Yellow is energetic - prefers more space score += distance > 100 ? 8 : -8; } // Avoid overlap if (distance < 50) score -= 20; } // Prefer areas not too close to edges let edgeDistance = min(x, y, width - x, height - y); score += edgeDistance > 80 ? 5 : -10; return score; } update() { this.age++; // Breathing/pulsing effect this.pulsePhase += 0.05; let pulse = sin(this.pulsePhase) * 0.2 + 1; this.size = this.baseSize * pulse; // Movement based on color personality if (this.mapping.color === 'blue') { // Slow, meditative movement this.rotation += 0.01; this.vx *= 0.98; // Gradual slowing this.vy *= 0.98; } else if (this.mapping.color === 'yellow') { // Energetic, nervous movement this.vx += random(-0.5, 0.5); this.vy += random(-0.5, 0.5); this.vx = constrain(this.vx, -3, 3); this.vy = constrain(this.vy, -3, 3); this.rotation += 0.08; } else if (this.mapping.color === 'red') { // Stable, grounded this.rotation += 0.02; this.vx *= 0.95; this.vy *= 0.95; } this.x += this.vx; this.y += this.vy; // Soft boundaries if (this.x < 50 || this.x > width - 50) this.vx *= -0.8; if (this.y < 50 || this.y > height - 150) this.vy *= -0.8; // Fade out over time this.alpha = map(this.age, 0, this.maxAge, 255, 0); return this.age < this.maxAge; } draw() { push(); translate(this.x, this.y); rotate(this.rotation); fill(this.r, this.g, this.b, this.alpha); stroke(0, this.alpha * 0.7); strokeWeight(2); if (this.mapping.type === 'circle') { // Blue circles - concentric rings for spiritual depth for (let i = 3; i > 0; i--) { fill(this.r, this.g, this.b, this.alpha * (i * 0.3)); ellipse(0, 0, this.size * i * 0.4); } } else if (this.mapping.type === 'triangle') { // Yellow triangles - sharp and energetic fill(this.r, this.g, this.b, this.alpha); beginShape(); let s = this.size * 0.6; vertex(0, -s); vertex(-s * 0.866, s * 0.5); vertex(s * 0.866, s * 0.5); endShape(CLOSE); // Energy lines stroke(this.r + 30, this.g + 30, this.b, this.alpha * 0.6); strokeWeight(1); for (let i = 0; i < 3; i++) { let angle = TWO_PI * i / 3; let x1 = cos(angle) * s * 0.3; let y1 = sin(angle) * s * 0.3; let x2 = cos(angle) * s * 0.8; let y2 = sin(angle) * s * 0.8; line(x1, y1, x2, y2); } } else if (this.mapping.type === 'square') { // Red squares - powerful and stable fill(this.r, this.g, this.b, this.alpha); let s = this.size * 0.8; rect(-s/2, -s/2, s, s); // Inner square for depth fill(this.r + 40, this.g + 20, this.b + 20, this.alpha * 0.7); let s2 = s * 0.6; rect(-s2/2, -s2/2, s2, s2); } pop(); } } function setup() { canvas = createCanvas(windowWidth, windowHeight); canvas.parent(document.body); // Create background texture createBackgroundTexture(); // Setup audio oscillator = new p5.Oscillator('sine'); envelope = new p5.Env(); envelope.setADSR(0.01, 0.2, 0.3, 0.5); envelope.setRange(0.7, 0); oscillator.amp(envelope); oscillator.start(); // Keyboard event listeners setupKeyboard(); } function createBackgroundTexture() { backgroundTexture = createGraphics(width, height); backgroundTexture.background(245, 245, 240); // Add subtle texture backgroundTexture.noStroke(); for (let i = 0; i < 2000; i++) { backgroundTexture.fill(random(230, 255), random(5, 15)); backgroundTexture.ellipse(random(width), random(height), random(1, 3)); } } function draw() { // Draw textured background if (backgroundTexture) { image(backgroundTexture, 0, 0); } else { background(245, 245, 240); } // Update and draw shapes for (let i = shapes.length - 1; i >= 0; i--) { if (shapes[i].update()) { shapes[i].draw(); } else { shapes.splice(i, 1); } } // Draw subtle composition guidelines (Kandinsky often used these) drawCompositionGuides(); } function drawCompositionGuides() { stroke(200, 200, 190, 100); strokeWeight(1); // Golden ratio lines let golden = width / 1.618; line(golden, 0, golden, height); line(width - golden, 0, width - golden, height); let goldenH = height / 1.618; line(0, goldenH, width, goldenH); line(0, height - goldenH, width, height - goldenH); } function playNote(note) { let mapping = noteMapping[note]; if (!mapping) return; // Play sound oscillator.freq(mapping.freq); envelope.play(); // Create visual let x = random(width * 0.2, width * 0.8); let y = random(height * 0.2, height * 0.7); shapes.push(new KandinskyShape(x, y, note)); // Visual feedback on keyboard let keyElement = document.querySelector(`[data-note="${note}"]`); if (keyElement) { keyElement.classList.add('active'); setTimeout(() => keyElement.classList.remove('active'), 150); } } function setupKeyboard() { // Mouse clicks on virtual keyboard document.querySelectorAll('.key').forEach(key => { key.addEventListener('click', () => { playNote(key.dataset.note); }); }); // Physical keyboard document.addEventListener('keydown', (e) => { let note = e.key.toUpperCase(); if (noteMapping[note]) { playNote(note); } }); } function windowResized() { resizeCanvas(windowWidth, windowHeight); createBackgroundTexture(); } // Ensure audio context starts on user interaction function mousePressed() { if (getAudioContext().state !== 'running') { getAudioContext().resume(); } } </script> </body> </html> ``` This implementation creates a living embodiment of Kandinsky's synesthetic theories, where each keystroke births shapes that carry the spiritual essence of their corresponding sounds. The composition evolves organically, maintaining the delicate balance between chaos and harmony that Kandinsky sought in his abstract works.
```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Kandinsky Synesthetic Experience</title> <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/addons/p5.sound.min.js"></script> <style> body { margin: 0; padding: 0; display: flex; justify-content: center; align-items: center; height: 100vh; background-color: #f5f5f0; font-family: 'Bauhaus 93', 'Arial', sans-serif; flex-direction: column; } #canvas-container { position: relative; margin-bottom: 20px; } #keyboard { display: flex; justify-content: center; gap: 10px; margin-top: 20px; } .key { width: 60px; height: 120px; border: 2px solid #333; display: flex; justify-content: center; align-items: flex-end; padding-bottom: 10px; font-size: 18px; cursor: pointer; transition: all 0.1s; background-color: #fff; border-radius: 0 0 5px 5px; } .key:hover { background-color: #f0f0f0; } .key.active { transform: translateY(5px); box-shadow: 0 0 10px rgba(0,0,0,0.3) inset; } h1 { margin-bottom: 5px; color: #333; } p { margin-top: 0; margin-bottom: 20px; color: #666; text-align: center; max-width: 800px; } .key[data-note="C"] { border-left-color: #1a237e; border-bottom-color: #1a237e; border-right-color: #1a237e; } .key[data-note="D"] { border-left-color: #0d47a1; border-bottom-color: #0d47a1; border-right-color: #0d47a1; } .key[data-note="E"] { border-left-color: #b71c1c; border-bottom-color: #b71c1c; border-right-color: #b71c1c; } .key[data-note="F"] { border-left-color: #c62828; border-bottom-color: #c62828; border-right-color: #c62828; } .key[data-note="G"] { border-left-color: #f57f17; border-bottom-color: #f57f17; border-right-color: #f57f17; } .key[data-note="A"] { border-left-color: #ffab00; border-bottom-color: #ffab00; border-right-color: #ffab00; } .key[data-note="B"] { border-left-color: #ffd600; border-bottom-color: #ffd600; border-right-color: #ffd600; } </style> </head> <body> <h1>Kandinsky's Synesthetic Piano</h1> <p>Experience Wassily Kandinsky's synesthetic theories in action. Each note produces both sound and visual elements according to his color-sound correspondences. Click the keys or use your keyboard (A-G).</p> <div id="canvas-container"></div> <div id="keyboard"> <div class="key" data-note="C">C</div> <div class="key" data-note="D">D</div> <div class="key" data-note="E">E</div> <div class="key" data-note="F">F</div> <div class="key" data-note="G">G</div> <div class="key" data-note="A">A</div> <div class="key" data-note="B">B</div> </div> <script> // Main application state let shapes = []; let oscillator; let canvas; let textureImg; let lastPosition = { x: 0, y: 0 }; // Note to frequency mapping const noteFrequencies = { 'C': 261.63, // Deep blue 'D': 293.66, // Light blue 'E': 329.63, // Red 'F': 349.23, // Red-orange 'G': 392.00, // Yellow 'A': 440.00, // Bright yellow 'B': 493.88 // White-yellow }; // Note to color mapping based on Kandinsky's theories const noteColors = { 'C': [26, 35, 126, 0.8], // Deep blue (C is associated with deep blue in Kandinsky's theory) 'D': [13, 71, 161, 0.8], // Lighter blue 'E': [183, 28, 28, 0.8], // Red 'F': [198, 40, 40, 0.8], // Red-orange 'G': [245, 127, 23, 0.8], // Yellow 'A': [255, 171, 0, 0.8], // Bright yellow 'B': [255, 214, 0, 0.8] // White-yellow }; // Shape class to manage visual elements class Shape { constructor(x, y, note, size) { this.x = x; this.y = y; this.note = note; this.initialSize = size; this.size = size; this.opacity = 1; this.angle = random(TWO_PI); this.velocityX = 0; this.velocityY = 0; // Set properties based on note characteristics if (note === 'C' || note === 'D') { // Blue tones - circular, slow movement this.shapeType = 'circle'; this.movementSpeed = random(0.2, 0.5); this.rotationSpeed = random(0.01, 0.02); this.fadeSpeed = random(0.001, 0.003); } else if (note === 'E' || note === 'F') { // Red tones - squares, medium movement this.shapeType = 'square'; this.movementSpeed = random(0.5, 1); this.rotationSpeed = random(0.02, 0.03); this.fadeSpeed = random(0.002, 0.004); } else { // Yellow tones - triangles, fast movement this.shapeType = 'triangle'; this.movementSpeed = random(1, 2); this.rotationSpeed = random(0.03, 0.05); this.fadeSpeed = random(0.003, 0.005); } // Set random velocity direction const angle = random(TWO_PI); this.velocityX = cos(angle) * this.movementSpeed; this.velocityY = sin(angle) * this.movementSpeed; } update() { // Move shape this.x += this.velocityX; this.y += this.velocityY; // Rotate shape this.angle += this.rotationSpeed; // Fade shape this.opacity -= this.fadeSpeed; // Slowly decrease size this.size = this.initialSize * this.opacity; // Boundary checks with bouncing behavior if (this.x < 0 || this.x > width) { this.velocityX *= -0.8; } if (this.y < 0 || this.y > height) { this.velocityY *= -0.8; } // Interact with other shapes this.interact(); return this.opacity > 0; } interact() { // Simplified physics interaction with other shapes for (let other of shapes) { if (other !== this) { const dx = other.x - this.x; const dy = other.y - this.y; const distance = sqrt(dx * dx + dy * dy); const minDist = (this.size + other.size) / 2; if (distance < minDist) { // Calculate repulsion force const force = 0.01; const angle = atan2(dy, dx); // Apply force based on shape type let multiplier = 1; // Yellow shapes (triangles) push more aggressively if (this.shapeType === 'triangle') multiplier = 2; // Blue shapes (circles) move more passively if (this.shapeType === 'circle') multiplier = 0.5; this.velocityX -= cos(angle) * force * multiplier; this.velocityY -= sin(angle) * force * multiplier; other.velocityX += cos(angle) * force / multiplier; other.velocityY += sin(angle) * force / multiplier; } } } } draw() { push(); translate(this.x, this.y); rotate(this.angle); // Apply Kandinsky's color theory const color = noteColors[this.note]; fill(color[0], color[1], color[2], color[3] * this.opacity); noStroke(); // Draw different shapes based on note character if (this.shapeType === 'circle') { ellipse(0, 0, this.size); } else if (this.shapeType === 'square') { rectMode(CENTER); rect(0, 0, this.size, this.size); } else if (this.shapeType === 'triangle') { const r = this.size / 2; triangle( 0, -r, r * cos(TWO_PI/3), r * sin(TWO_PI/3), r * cos(2*TWO_PI/3), r * sin(2*TWO_PI/3) ); } pop(); } } // p5.js preload function function preload() { // Create a texture for the canvas background textureImg = createGraphics(200, 200); textureImg.background(245, 245, 240); for (let i = 0; i < 1000; i++) { const x = random(200); const y = random(200); const size = random(1, 3); const alpha = random(5, 20); textureImg.noStroke(); textureImg.fill(0, 0, 0, alpha); textureImg.ellipse(x, y, size, size); } } // p5.js setup function function setup() { canvas = createCanvas(800, 500); canvas.parent('canvas-container'); // Create audio context and oscillator oscillator = new p5.Oscillator('sine'); oscillator.amp(0); oscillator.start(); // Set up keyboard interaction const keys = document.querySelectorAll('.key'); keys.forEach(key => { key.addEventListener('mousedown', () => { playNote(key.dataset.note); key.classList.add('active'); }); key.addEventListener('mouseup', () => { stopNote(); key.classList.remove('active'); }); key.addEventListener('mouseleave', () => { key.classList.remove('active'); }); }); // Computer keyboard support window.addEventListener('keydown', (e) => { if (e.repeat) return; // Prevent key repeat const keyMap = { 'a': 'C', 's': 'D', 'd': 'E', 'f': 'F', 'g': 'G', 'h': 'A', 'j': 'B' }; if (keyMap[e.key.toLowerCase()]) { const note = keyMap[e.key.toLowerCase()]; playNote(note); document.querySelector(`.key[data-note="${note}"]`).classList.add('active'); } }); window.addEventListener('keyup', (e) => { const keyMap = { 'a': 'C', 's': 'D', 'd': 'E', 'f': 'F', 'g': 'G', 'h': 'A', 'j': 'B' }; if (keyMap[e.key.toLowerCase()]) { stopNote(); const note = keyMap[e.key.toLowerCase()]; document.querySelector(`.key[data-note="${note}"]`).classList.remove('active'); } }); } // p5.js draw function function draw() { // Create textured background background(245, 245, 240); // Apply texture for (let x = 0; x < width; x += textureImg.width) { for (let y = 0; y < height; y += textureImg.height) { image(textureImg, x, y); } } // Update and draw all shapes for (let i = shapes.length - 1; i >= 0; i--) { const isAlive = shapes[i].update(); shapes[i].draw(); if (!isAlive) { shapes.splice(i, 1); } } } // Play a note and create corresponding shapes function playNote(note) { // Set oscillator frequency and fade in oscillator.freq(noteFrequencies[note]); oscillator.amp(0.2, 0.1); // Determine position for new shape let x, y; // For compositional balance, place shapes in different areas based on their character if (note === 'C' || note === 'D') { // Blue tones tend to be grounded, place in lower half x = random(width); y = random(height/2, height * 0.9); } else if (note === 'E' || note === 'F') { // Red tones are stable, place in middle areas x = random(width); y = random(height * 0.3, height * 0.7); } else { // Yellow tones are light and high, place in upper half x = random(width); y = random(height * 0.1, height * 0.5); } // Create multiple shapes for richer visual experience const numShapes = map(noteFrequencies[note], noteFrequencies['C'], noteFrequencies['B'], 1, 4); for (let i = 0; i < numShapes; i++) { const size = random(40, 120); const offsetX = random(-50, 50); const offsetY = random(-50, 50); shapes.push(new Shape(x + offsetX, y + offsetY, note, size)); } // Update last position for next shape lastPosition = { x, y }; } // Stop the current note function stopNote() { oscillator.amp(0, 0.1); } </script> </body> </html> ```
**Plan d’Action – Artistic & Technical Interpretation** | **Kandinsky’s colour‑tone link** | **Musical range** | **Visual metaphor** | **Behaviour** | |--------------------------------|-------------------|---------------------|----------------| | **Yellow – “trumpet‑like, high, sharp”** | G, A, B | **Sharp triangles / zig‑zags** | Fast, outward‑bursting, slightly repulsive to blue circles | | **Blue – “cello‑like, deep, calm”** | C, D | **Smooth circles** | Slow, concentric drift, “spiritual” easing, attract red squares a little | | **Red – “tuba‑like, mid‑range, powerful”** | E, F | **Solid squares** | Moderate speed, stable, gently push blue circles away, hold ground | ### 1. Artistic translation * **Spirituality of blue** – A circle expands slowly, its radius grows while its opacity fades, evoking a deep, breathing calm. Its movement follows a gentle sinusoidal drift, suggesting the “inner resonance” Kandinsky heard in the cello. * **Balance & harmony** – Shapes always originate near the centre (the “canvas‑core”) with a small random offset. A maximum of 30 shapes is kept; older shapes fade out, preventing visual overload. When a *yellow* triangle appears it emits a short repulsive pulse that nudges existing blue circles outward, mirroring the “aggressive” trumpet against the tranquil organ. Red squares act as anchors, subtly pulling nearby blues toward them, creating a push‑pull dialogue. ### 2. Technical architecture ``` SoundManager └─ playNote(note) // creates an oscillator, applies envelope Shape (base class) ├─ type : 'circle' | 'square' | 'triangle' ├─ col : p5.Color ├─ pos : p5.Vector ├─ vel : p5.Vector ├─ life : float (0‑1) ├─ update() └─ draw() ShapeFactory └─ createShape(note) // returns a concrete Shape with proper colour, // size, velocity according to the table above Composition └─ shapes : [] // master list ├─ addShape(s) // push, enforce max count ├─ updateAll() // apply interactions + fade └─ drawAll() ``` * **Interaction logic** – When a new shape `s` is added we loop through the existing list and apply a simple force: *If `s` is yellow and the other is blue → push the blue away (add to its velocity a normalized vector from `s` to the blue). If `s` is red and the other is blue → pull the blue a little toward the red.* * **Fade‑out** – Each frame the shape’s `life` is reduced (`life -= 0.005`). Its draw routine uses `alpha = 255 * life`; when `life ≤ 0` the shape is removed. * **Background texture** – A light off‑white (≈ #F8F4E3) is painted, then a low‑frequency noise overlay (very subtle perlin noise) gives the canvas a “paper‑like” feel reminiscent of Bauhaus materiality. * **Keyboard UI** – Seven HTML `<button>` elements labelled C‑B. Clicking or pressing the matching computer key (`a`→C, `s`→D, `d`→E, `f`→F, `g`→G, `h`→A, `j`→B) triggers `SoundManager.playNote()` and `Composition.addShape()`. ### 3. Main p5 sketch flow ```text setup() - create canvas - draw textured background - instantiate SoundManager & Composition - create on‑screen keyboard (HTML) draw() - clear with transparent overlay (to keep fading trails) - composition.updateAll() - composition.drawAll() keyPressed() - map physical key → note, call playNote & addShape ``` --- ## The Self‑Contained HTML Experience Below is the complete HTML file. Save it as `kandinsky_piano.html` and open it in any modern browser. ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Kandinsky Synesthetic Piano</title> <style> body { margin:0; background:#f8f4e3; display:flex; flex-direction:column; align-items:center; font-family:Arial,Helvetica,sans-serif; overflow:hidden; } #keyboard { margin-top:10px; display:flex; gap:4px; } .key { width:50px; height:80px; background:#fff; border:1px solid #999; border-radius:4px; cursor:pointer; text-align:center; line-height:80px; user-select:none; font-weight:bold; font-size:14px; box-shadow:2px 2px 3px rgba(0,0,0,0.2); } .key:active { background:#e0e0e0; } </style> </head> <body> <div id="sketch-holder"></div> <div id="keyboard"> <div class="key" data-note="C">C</div> <div class="key" data-note="D">D</div> <div class="key" data-note="E">E</div> <div class="key" data-note="F">F</div> <div class="key" data-note="G">G</div> <div class="key" data-note="A">A</div> <div class="key" data-note="B">B</div> </div> <!-- p5.js and p5.sound from CDN --> <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.9.0/p5.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.9.0/addons/p5.sound.min.js"></script> <script> /* ======================= SoundManager ======================= */ class SoundManager { constructor() { // frequencies for natural notes (middle C = 261.63 Hz) this.freqMap = { 'C': 261.63, 'D': 293.66, 'E': 329.63, 'F': 349.23, 'G': 392.00, 'A': 440.00, 'B': 493.88 }; this.osc = null; this.env = new p5.Envelope(); this.env.setADSR(0.01, 0.1, 0.2, 0.5); this.env.setRange(0.5, 0); } playNote(note) { const freq = this.freqMap[note]; if (!freq) return; if (this.osc) this.osc.stop(); this.osc = new p5.Oscillator('sine'); this.osc.freq(freq); this.osc.start(); this.env.play(this.osc, 0, 0.1); // stop after a short while to free resources setTimeout(() => { this.osc.stop(); }, 500); } } /* ======================= Shape classes ======================= */ class Shape { constructor(type, col, pos, vel) { this.type = type; // 'circle' | 'square' | 'triangle' this.col = col; // p5.Color this.pos = pos.copy(); // p5.Vector this.vel = vel.copy(); // p5.Vector this.life = 1.0; // fades to 0 this.size = random(20, 60); this.rotation = random(TWO_PI); } update() { // simple physics this.pos.add(this.vel); // slow down a bit this.vel.mult(0.98); // shrink life this.life -= 0.004; } draw() { push(); translate(this.pos.x, this.pos.y); rotate(this.rotation); const alpha = map(this.life, 0, 1, 0, 255); fill(red(this.col), green(this.col), blue(this.col), alpha); noStroke(); if (this.type === 'circle') { ellipse(0, 0, this.size); } else if (this.type === 'square') { rectMode(CENTER); rect(0, 0, this.size, this.size); } else if (this.type === 'triangle') { const h = (sqrt(3) / 2) * this.size; triangle( -this.size / 2, h / 3, this.size / 2, h / 3, 0, -2 * h / 3 ); } pop(); } isDead() { return this.life <= 0; } } /* ======================= ShapeFactory ======================= */ class ShapeFactory { static createShape(note) { const centre = createVector(width / 2, height / 2); const offset = p5.Vector.random2D().mult(random(30, 80)); const pos = p5.Vector.add(centre, offset); if (['G','A','B'].includes(note)) { // Yellow – triangle, fast outward const col = color('#FFC300'); const vel = p5.Vector.random2D().mult(random(2,4)); return new Shape('triangle', col, pos, vel); } else if (['C','D'].includes(note)) { // Blue – circle, slow const col = color('#2E86C1'); const vel = p5.Vector.random2D().mult(random(0.3,0.8)); return new Shape('circle', col, pos, vel); } else { // E,F → Red – square, medium const col = color('#C0392B'); const vel = p5.Vector.random2D().mult(random(1,2)); return new Shape('square', col, pos, vel); } } } /* ======================= Composition manager ======================= */ class Composition { constructor(maxShapes = 30) { this.shapes = []; this.max = maxShapes; } addShape(s) { // interaction: push blue circles away from yellow triangles if (s.type === 'triangle') { this.shapes.forEach(other => { if (other.type === 'circle') { const dir = p5.Vector.sub(other.pos, s.pos).normalize().mult(0.5); other.vel.add(dir); } }); } // red squares gently attract blue circles if (s.type === 'square') { this.shapes.forEach(other => { if (other.type === 'circle') { const dir = p5.Vector.sub(s.pos, other.pos).normalize().mult(0.2); other.vel.add(dir); } }); } this.shapes.push(s); if (this.shapes.length > this.max) { this.shapes.shift(); // discard oldest } } updateAll() { for (let i = this.shapes.length - 1; i >= 0; i--) { const sh = this.shapes[i]; sh.update(); if (sh.isDead()) this.shapes.splice(i, 1); } } drawAll() { this.shapes.forEach(s => s.draw()); } } /* ======================= p5 Sketch ======================= */ let soundMgr, comp; function setup() { const cnv = createCanvas(windowWidth, windowHeight); cnv.parent('sketch-holder'); // textured background – draw once background('#F8F4E3'); drawNoiseOverlay(); soundMgr = new SoundManager(); comp = new Composition(); // UI bindings selectAll('.key').forEach(el => { el.addEventListener('mousedown', () => triggerNote(el.dataset.note)); }); } function draw() { // translucent overlay to keep fading trails smooth fill(255, 255, 255, 5); noStroke(); rect(0, 0, width, height); comp.updateAll(); comp.drawAll(); } // Map physical keyboard to notes (home row) const keyMap = { 'a': 'C', 's': 'D', 'd': 'E', 'f': 'F', 'g': 'G', 'h': 'A', 'j': 'B' }; function keyPressed() { const note = keyMap[key.toLowerCase()]; if (note) triggerNote(note); } // Play sound and create visual shape function triggerNote(note) { soundMgr.playNote(note); const shape = ShapeFactory.createShape(note); comp.addShape(shape); } /* ==== background texture ==== */ function drawNoiseOverlay() { loadPixels(); for (let x = 0; x < width; x++) { for (let y = 0; y < height; y++) { const idx = (x + y * width) * 4; const n = noise(x * 0.01, y * 0.01); const bright = map(n, 0, 1, 220, 255); pixels[idx] = bright; // R pixels[idx + 1] = bright; // G pixels[idx + 2] = bright; // B pixels[idx + 3] = 255; // A } } updatePixels(); } /* ==== responsive ==== */ function windowResized() { resizeCanvas(windowWidth, windowHeight); background('#F8F4E3'); drawNoiseOverlay(); } </script> </body> </html> ``` **How it works** 1. **Sound** – When a key (either on‑screen or via the computer keyboard) is pressed, `SoundManager` creates a short‑lived sine‑wave oscillator tuned to the appropriate pitch and shapes an envelope for a musical “tap”. 2. **Visual** – The same event calls `ShapeFactory.createShape`, which builds a `Shape` (triangle, circle, or square) whose colour and initial velocity follow Kandinsky’s associations. 3. **Composition** – `Composition` stores the active shapes, forces interaction between complementary colours (yellow ↔ blue, red ↔ blue) and removes shapes once they have faded. 4. **Aesthetic** – A subtle perlin‑noise overlay supplies the “paper‑like” texture of a Bauhaus canvas, while the low‑opacity white rectangle in `draw()` makes each element leave a gentle, fading trail – a visual echo of the music played. Open the file, click the keys or hit **a‑j** on your keyboard, and watch Kandinsky’s synesthetic world come alive in sound and colour. Enjoy!
### Artistic and Technical Interpretation As a creative coder inspired by Kandinsky's synesthetic theories, I'll translate his ideas from "Concerning the Spiritual in Art" into an interactive experience that bridges sound, color, and form. Kandinsky viewed art as a spiritual language, where colors and shapes evoke emotions and inner vibrations. My implementation will capture this by making sound directly manifest as visual energy, allowing users to "play" a piano that composes an evolving abstract painting. **Artistic Interpretation:** - **Translating the Spirituality of Blue:** Kandinsky associated blue with depth, calm, and spirituality, likening it to the sound of a cello or organ. For blue tones (C and D), I'll animate circles with slow, concentric movements—perhaps gentle pulsing or expanding ripples—that evoke a meditative, inward journey. This creates a sense of spiritual tranquility, where shapes grow and fade like echoes of introspection, fostering harmony rather than chaos. - **Managing Composition and Harmony:** To maintain Kandinsky's emphasis on balanced composition (as seen in his abstract works), shapes will be placed using a subtle spatial logic. New shapes won't overlap randomly; instead, I'll implement a simple repulsion system where aggressive shapes (like yellow triangles) gently nudge calmer ones (like blue circles) away, ensuring the canvas evolves into a harmonious whole. Over time, shapes will fade gradually, leaving faint traces to build a layered "painting" that reflects the user's interactions. This mirrors Kandinsky's idea of art as a dynamic, living entity, where elements interact to form a unified spiritual expression. **Technical Interpretation:** - **Structure of Classes and Logic:** - **Shape Class:** This will be a base class for all visual elements, with subclasses or properties for specific shapes (e.g., Triangle, Circle, Square). Each instance will have properties like type (determining shape and color), position (x, y), velocity (for movement), lifespan (for fading), and a method for interaction (e.g., repelling other shapes based on distance). In the update method, shapes will move according to their type (fast for yellow, slow for blue) and decrease in opacity over time. - **SoundManager:** I'll create a simple function within the p5 sketch to handle audio. Using p5.sound's Oscillator, each note will trigger a sine wave at specific frequencies (e.g., C = 261.63 Hz, D = 293.66 Hz). Sounds will play briefly on key press, synchronized with shape creation to reinforce the synesthetic link. - **Main Logic:** The p5.js sketch will run in the global mode for simplicity. In `setup()`, I'll initialize the canvas with a textured off-white background (using a subtle noise pattern) and create the on-screen keyboard as an array of div elements. In `draw()`, I'll handle the main loop: update shapes (move, interact, fade), draw them, and maintain composition. User input via `keyPressed()` or `mousePressed()` will trigger sound playback and instantiate new Shape objects based on the note. Shapes will interact by checking distances in their update method, applying a small vector force to avoid overcrowding. This approach ensures a minimalist, self-contained experience that embodies Kandinsky's philosophy: sound as a catalyst for spiritual visual expression. ### Full HTML Code Below is the complete, self-contained HTML file. It includes the p5.js and p5.sound libraries loaded from CDNs, all embedded in a single file. When run in a browser, it will display an interactive canvas with the keyboard and generate the audio-visual experience as described. ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Kandinsky Synesthetic Piano</title> <style> body { margin: 0; display: flex; flex-direction: column; align-items: center; background-color: #f0f0f0; /* Off-white background */ } #keyboard { display: flex; margin: 10px; } .key { width: 40px; height: 100px; margin: 5px; background-color: #ccc; display: flex; align-items: center; justify-content: center; cursor: pointer; font-family: Arial, sans-serif; font-size: 14px; } canvas { border: 1px solid #000; } </style> <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/addons/p5.sound.js"></script> </head> <body> <h2>Kandinsky Synesthetic Piano</h2> <p>Click the on-screen keys or press C, D, E, F, G, A, B on your keyboard to play.</p> <div id="keyboard"></div> <script> let shapes = []; // Array to hold shape objects let osc; // Oscillator for sound // Shape class class Shape { constructor(type, x, y) { this.type = type; // 'yellow' (triangle), 'blue' (circle), 'red' (square) this.x = x; this.y = y; this.size = random(20, 50); // Random size for variety this.velocity = createVector(random(-1, 1), random(-1, 1)); // Initial random velocity this.lifespan = 255; // For fading this.color = this.getColor(); this.speedMultiplier = this.getSpeed(); // Based on type } getColor() { if (this.type === 'yellow') return color(255, 255, 0); // Yellow if (this.type === 'blue') return color(0, 0, 255); // Blue if (this.type === 'red') return color(255, 0, 0); // Red } getSpeed() { if (this.type === 'yellow') return 2; // Fast for yellow if (this.type === 'blue') return 0.5; // Slow for blue if (this.type === 'red') return 1; // Medium for red } update() { // Apply speed multiplier to velocity this.x += this.velocity.x * this.speedMultiplier; this.y += this.velocity.y * this.speedMultiplier; // Interaction: Repel other shapes for (let other of shapes) { if (other !== this) { let d = dist(this.x, this.y, other.x, other.y); if (d < 50 && d > 0) { // If too close, push away let force = p5.Vector.sub(createVector(this.x, this.y), createVector(other.x, other.y)); force.normalize(); force.mult(0.5); // Gentle repulsion this.velocity.add(force); } } } // Fade over time this.lifespan -= 2; if (this.lifespan < 0) this.lifespan = 0; } display() { noStroke(); fill(red(this.color), green(this.color), blue(this.color), this.lifespan); // Fade alpha if (this.type === 'yellow') { triangle(this.x, this.y - this.size / 2, this.x - this.size / 2, this.y + this.size / 2, this.x + this.size / 2, this.y + this.size / 2); // Triangle } else if (this.type === 'blue') { ellipse(this.x, this.y, this.size); // Circle } else if (this.type === 'red') { rectMode(CENTER); rect(this.x, this.y, this.size, this.size); // Square } } isDead() { return this.lifespan <= 0; } } function setup() { let canvas = createCanvas(800, 600); canvas.parent('sketch-holder'); // Optional, but not used here background(240, 240, 240); // Off-white with subtle texture via noise in draw osc = new p5.Oscillator('sine'); // Sine wave for pure tones osc.start(); osc.amp(0); // Start silent // Create on-screen keyboard let keys = ['C', 'D', 'E', 'F', 'G', 'A', 'B']; let keyboardDiv = document.getElementById('keyboard'); for (let key of keys) { let btn = document.createElement('div'); btn.className = 'key'; btn.innerText = key; btn.addEventListener('click', () => handleKeyPress(key)); keyboardDiv.appendChild(btn); } } function draw() { // Textured background background(240, 240, 240); // Base off-white for (let i = 0; i < width; i += 10) { for (let j = 0; j < height; j += 10) { stroke(255, random(200, 255)); // Subtle noise for texture point(i, j); } } noStroke(); // Reset for shapes // Update and display shapes for (let i = shapes.length - 1; i >= 0; i--) { shapes[i].update(); shapes[i].display(); if (shapes[i].isDead()) { shapes.splice(i, 1); // Remove dead shapes } } } function handleKeyPress(note) { let freq = getFrequency(note); // Get frequency based on note osc.freq(freq); osc.amp(0.5, 0.05); // Play sound with fade in setTimeout(() => osc.amp(0, 0.05), 200); // Fade out after 200ms let type = getShapeType(note); // Determine shape type let newShape = new Shape(type, width / 2, height / 2); // Start at center shapes.push(newShape); // Add to array } function keyPressed() { let notes = { 'c': 'C', 'd': 'D', 'e': 'E', 'f': 'F', 'g': 'G', 'a': 'A', 'b': 'B' }; if (key in notes) { handleKeyPress(notes[key]); } } function getFrequency(note) { switch (note) { case 'C': return 261.63; // C4 case 'D': return 293.66; // D4 case 'E': return 329.63; // E4 case 'F': return 349.23; // F4 case 'G': return 392.00; // G4 case 'A': return 440.00; // A4 case 'B': return 493.88; // B4 } } function getShapeType(note) { if (note === 'C' || note === 'D') return 'blue'; // Blue tones if (note === 'E' || note === 'F') return 'red'; // Red tones if (note === 'G' || note === 'A' || note === 'B') return 'yellow'; // Yellow tones } </script> </body> </html> ```