Created
May 12, 2025 15:24
-
-
Save Klaudioz/6fe3b421b609825483d6bda0be77bb50 to your computer and use it in GitHub Desktop.
A p5.js sketch simulating a "Living Mona Lisa" with an ethereal Sfumato flow effect. Features interactive particles, Perlin noise-driven movement, and a Mona Lisa-inspired color palette.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Living Mona Lisa - Sfumato Flow</title> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.9.0/p5.js"></script> | |
| <style> | |
| body { margin: 0; overflow: hidden; background-color: #0A0A08; /* Very dark base */ } | |
| canvas { display: block; } | |
| /* Optional: Add a subtle vignette effect to focus the center */ | |
| html::before { | |
| content: ''; | |
| position: fixed; | |
| top: 0; left: 0; | |
| width: 100%; height: 100%; | |
| box-shadow: inset 0 0 15vw 5vw rgba(0,0,0,0.5); /* Darker edges */ | |
| pointer-events: none; /* Allows interaction with canvas */ | |
| z-index: 10; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <script> | |
| let glazes = []; | |
| let numGlazes = 150; // Number of "glaze" elements | |
| let noiseTime = 0; // Time variable for Perlin noise evolution | |
| // Colors inspired by the Mona Lisa's palette | |
| const MONA_LISA_PALETTE = [ | |
| '#2A251E', // Deep Umber/Shadow | |
| '#4B4339', // Dark Brown/Robe | |
| '#6F6250', // Muted Brown/Mid-tone | |
| '#9A8C73', // Light Brown/Ochre | |
| '#585A45', // Dark Olive Green/Landscape | |
| '#8D8A6E', // Muted Green/Light Landscape | |
| '#C0A88C', // Fleshtone/Highlight | |
| '#DBCDBE', // Pale Ochre/Sky Haze | |
| '#332D25', // Another dark tone for depth | |
| '#7E7360' // A mid-brown for blending | |
| ]; | |
| function setup() { | |
| createCanvas(windowWidth, windowHeight); | |
| // Initialize glazes | |
| for (let i = 0; i < numGlazes; i++) { | |
| glazes.push(new GlazeParticle(random(width), random(height))); | |
| } | |
| // Initial background, though it will be mostly overwritten | |
| background(10, 10, 8); | |
| } | |
| function draw() { | |
| // Apply a very translucent background to create fading/layering trails (sfumato) | |
| fill(10, 10, 8, 20); // Dark, slightly transparent | |
| noStroke(); | |
| rect(0, 0, width, height); | |
| noiseTime += 0.0005; // Slowly evolve the noise field | |
| for (let i = glazes.length - 1; i >= 0; i--) { | |
| let glaze = glazes[i]; | |
| glaze.applyFlow(); | |
| glaze.interact(); | |
| glaze.update(); | |
| glaze.display(); | |
| if (glaze.isFaded()) { | |
| // Replace faded glazes with new ones | |
| glazes.splice(i, 1); | |
| glazes.push(new GlazeParticle(random(width), random(height * 0.2, height * 0.8))); // New ones appear more centrally vertically | |
| } | |
| } | |
| } | |
| class GlazeParticle { | |
| constructor(x, y) { | |
| this.pos = createVector(x, y); | |
| this.vel = createVector(random(-0.2, 0.2), random(-0.2, 0.2)); // Initial gentle drift | |
| this.acc = createVector(0, 0); | |
| this.baseColor = color(random(MONA_LISA_PALETTE)); | |
| this.lifespan = random(300, 800); // How long it "persists" | |
| this.initialLifespan = this.lifespan; | |
| this.maxOpacity = random(30, 90); // Max opacity for sfumato effect | |
| this.currentOpacity = 0; | |
| this.baseSize = random(50, 250); // Base size of the glaze patch | |
| this.currentSize = this.baseSize; | |
| this.aspectRatio = random(0.3, 0.7); // For elongated shapes, like brush strokes | |
| this.noiseOffsetX = random(1000); // Per-particle noise offset for unique movement | |
| this.noiseOffsetY = random(2000); | |
| this.rotation = random(TWO_PI); | |
| this.rotationSpeed = random(-0.002, 0.002); | |
| } | |
| applyForce(force) { | |
| this.acc.add(force); | |
| } | |
| // Apply a gentle, evolving flow field | |
| applyFlow() { | |
| let noiseScale = 0.002; | |
| let angle = noise(this.pos.x * noiseScale + this.noiseOffsetX, | |
| this.pos.y * noiseScale + this.noiseOffsetY, | |
| noiseTime) * TWO_PI * 3; // Larger angle multiplier for more swirl | |
| let flow = p5.Vector.fromAngle(angle); | |
| flow.mult(0.05); // Very gentle force | |
| this.applyForce(flow); | |
| } | |
| // Mouse interaction | |
| interact() { | |
| if (mouseX > 0 && mouseX < width && mouseY > 0 && mouseY < height) { | |
| let mouseVec = createVector(mouseX, mouseY); | |
| let distToMouse = p5.Vector.dist(this.pos, mouseVec); | |
| let interactionRadius = 150; | |
| if (distToMouse < interactionRadius) { | |
| // Gentle repulsion/disturbance from mouse | |
| let repel = p5.Vector.sub(this.pos, mouseVec); | |
| repel.normalize(); | |
| // Strength of repulsion inversely proportional to distance | |
| let strength = map(distToMouse, 0, interactionRadius, 0.3, 0.01); | |
| repel.mult(strength); | |
| this.applyForce(repel); | |
| // Slightly increase opacity or "awaken" the glaze when mouse is near | |
| this.currentOpacity = min(this.maxOpacity + 20, 120); // Temporarily boost opacity | |
| } | |
| } | |
| } | |
| update() { | |
| this.vel.add(this.acc); | |
| this.vel.limit(0.5); // Max speed | |
| this.pos.add(this.vel); | |
| this.acc.mult(0); // Reset acceleration | |
| this.lifespan--; | |
| this.rotation += this.rotationSpeed; | |
| // Opacity: fade in, stay, fade out | |
| if (this.lifespan > this.initialLifespan - 100) { // Fading in | |
| this.currentOpacity = map(this.lifespan, this.initialLifespan, this.initialLifespan - 100, 0, this.maxOpacity); | |
| } else if (this.lifespan < 150) { // Fading out | |
| this.currentOpacity = map(this.lifespan, 0, 150, 0, this.maxOpacity); | |
| } else { // Stable period | |
| this.currentOpacity = this.maxOpacity; | |
| } | |
| // Size pulsing slightly or shrinking with age | |
| this.currentSize = this.baseSize * (0.8 + 0.2 * sin(this.lifespan * 0.05)); | |
| this.currentSize *= map(this.lifespan, 0, this.initialLifespan, 0.5, 1); // Shrink as it dies | |
| // Keep within bounds by wrapping around (softly) | |
| if (this.pos.x > width + this.currentSize) this.pos.x = -this.currentSize; | |
| if (this.pos.x < -this.currentSize) this.pos.x = width + this.currentSize; | |
| if (this.pos.y > height + this.currentSize) this.pos.y = -this.currentSize; | |
| if (this.pos.y < -this.currentSize) this.pos.y = height + this.currentSize; | |
| } | |
| display() { | |
| push(); // Isolate transformations | |
| translate(this.pos.x, this.pos.y); | |
| rotate(this.rotation); | |
| // Set fill color with current opacity | |
| let displayColor = color(red(this.baseColor), green(this.baseColor), blue(this.baseColor), this.currentOpacity); | |
| fill(displayColor); | |
| noStroke(); | |
| // Draw as an elongated ellipse to simulate a soft brush stroke or glaze | |
| ellipse(0, 0, this.currentSize, this.currentSize * this.aspectRatio); | |
| pop(); // Restore transformations | |
| } | |
| isFaded() { | |
| return this.lifespan <= 0; | |
| } | |
| } | |
| function windowResized() { | |
| resizeCanvas(windowWidth, windowHeight); | |
| // Could re-initialize or simply let existing particles adapt. | |
| // For simplicity, we'll let them adapt. A full reset might be jarring. | |
| background(10,10,8); // Reset background on resize | |
| } | |
| // Optional: Add a subtle message or title | |
| // function mousePressed() { | |
| // // Example: could trigger a new burst of particles or change a parameter | |
| // } | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment